mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-06-24 05:45:01 -06:00
183 lines
5.9 KiB
C++
183 lines
5.9 KiB
C++
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <cstdarg>
|
|
#include <cubeb/cubeb.h>
|
|
|
|
#include "common/assert.h"
|
|
#include "common/ringbuffer.h"
|
|
#include "core/libraries/audio/audioout.h"
|
|
#include "core/libraries/audio/cubeb_audio.h"
|
|
|
|
namespace Libraries::AudioOut {
|
|
|
|
constexpr int AUDIO_STREAM_BUFFER_THRESHOLD = 65536; // Define constant for buffer threshold
|
|
|
|
static cubeb_channel_layout GetCubebChannelLayout(int num_channels) {
|
|
switch (num_channels) {
|
|
case 1:
|
|
return CUBEB_LAYOUT_MONO;
|
|
case 2:
|
|
return CUBEB_LAYOUT_STEREO;
|
|
case 8:
|
|
return CUBEB_LAYOUT_3F4_LFE;
|
|
default:
|
|
UNREACHABLE();
|
|
}
|
|
}
|
|
|
|
struct CubebStream {
|
|
cubeb_stream* stream{};
|
|
size_t frame_size;
|
|
ring_buffer_base<u8> buffer;
|
|
|
|
CubebStream(cubeb* ctx, const PortOut& port)
|
|
: frame_size(port.channels_num * port.sample_size),
|
|
buffer(static_cast<int>(port.samples_num * frame_size) * 4) {
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
cubeb_stream_params stream_params = {
|
|
.format = port.is_float ? CUBEB_SAMPLE_FLOAT32LE : CUBEB_SAMPLE_S16LE,
|
|
.rate = port.freq,
|
|
.channels = static_cast<u32>(port.channels_num),
|
|
.layout = GetCubebChannelLayout(port.channels_num),
|
|
};
|
|
u32 latency_frames = 512;
|
|
if (const auto ret = cubeb_get_min_latency(ctx, &stream_params, &latency_frames);
|
|
ret != CUBEB_OK) {
|
|
LOG_WARNING(Lib_AudioOut, "Could not get minimum cubeb audio latency: {}", ret);
|
|
}
|
|
if (const auto ret = cubeb_stream_init(ctx, &stream, "shadPS4", nullptr, nullptr, nullptr,
|
|
&stream_params, latency_frames, &DataCallback,
|
|
&StateCallback, this);
|
|
ret != CUBEB_OK) {
|
|
LOG_ERROR(Lib_AudioOut, "Failed to create cubeb stream: {}", ret);
|
|
return;
|
|
}
|
|
if (const auto ret = cubeb_stream_start(stream); ret != CUBEB_OK) {
|
|
LOG_ERROR(Lib_AudioOut, "Failed to start cubeb stream: {}", ret);
|
|
return;
|
|
}
|
|
}
|
|
|
|
~CubebStream() {
|
|
if (!stream) {
|
|
return;
|
|
}
|
|
if (const auto ret = cubeb_stream_stop(stream); ret != CUBEB_OK) {
|
|
LOG_WARNING(Lib_AudioOut, "Failed to stop cubeb stream: {}", ret);
|
|
}
|
|
cubeb_stream_destroy(stream);
|
|
stream = nullptr;
|
|
}
|
|
|
|
void Output(void* ptr, size_t size) {
|
|
auto* data = static_cast<u8*>(ptr);
|
|
while (size > 0) {
|
|
const auto queued = buffer.enqueue(data, static_cast<int>(size));
|
|
size -= queued;
|
|
data += queued;
|
|
if (size > 0) {
|
|
// If the data is too large for the ring buffer, yield execution and give it a
|
|
// chance to drain.
|
|
std::this_thread::yield();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetVolume(const std::array<int, 8>& ch_volumes) {
|
|
if (!stream) {
|
|
return;
|
|
}
|
|
// Cubeb does not have per-channel volumes, for now just take the maximum of the channels.
|
|
const auto vol = *std::ranges::max_element(ch_volumes);
|
|
if (const auto ret =
|
|
cubeb_stream_set_volume(stream, static_cast<float>(vol) / SCE_AUDIO_OUT_VOLUME_0DB);
|
|
ret != CUBEB_OK) {
|
|
LOG_WARNING(Lib_AudioOut, "Failed to change cubeb stream volume: {}", ret);
|
|
}
|
|
}
|
|
|
|
static long DataCallback(cubeb_stream* stream, void* user_data, const void* in, void* out,
|
|
long num_frames) {
|
|
auto* stream_data = static_cast<CubebStream*>(user_data);
|
|
const auto out_data = static_cast<u8*>(out);
|
|
const auto requested_size = static_cast<int>(num_frames * stream_data->frame_size);
|
|
const auto dequeued_size = stream_data->buffer.dequeue(out_data, requested_size);
|
|
if (dequeued_size < requested_size) {
|
|
// Need to fill remaining space with silence.
|
|
std::memset(out_data + dequeued_size, 0, requested_size - dequeued_size);
|
|
}
|
|
return num_frames;
|
|
}
|
|
|
|
static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {
|
|
switch (state) {
|
|
case CUBEB_STATE_STARTED:
|
|
LOG_INFO(Lib_AudioOut, "Cubeb stream started");
|
|
break;
|
|
case CUBEB_STATE_STOPPED:
|
|
LOG_INFO(Lib_AudioOut, "Cubeb stream stopped");
|
|
break;
|
|
case CUBEB_STATE_DRAINED:
|
|
LOG_INFO(Lib_AudioOut, "Cubeb stream drained");
|
|
break;
|
|
case CUBEB_STATE_ERROR:
|
|
LOG_ERROR(Lib_AudioOut, "Cubeb stream encountered an error");
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
CubebAudioOut::CubebAudioOut() {
|
|
cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback);
|
|
if (const auto ret = cubeb_init(&ctx, "shadPS4", nullptr); ret != CUBEB_OK) {
|
|
LOG_CRITICAL(Lib_AudioOut, "Failed to create cubeb context: {}", ret);
|
|
}
|
|
}
|
|
|
|
CubebAudioOut::~CubebAudioOut() {
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
cubeb_destroy(ctx);
|
|
ctx = nullptr;
|
|
}
|
|
|
|
void* CubebAudioOut::Open(PortOut& port) {
|
|
return new CubebStream(ctx, port);
|
|
}
|
|
|
|
void CubebAudioOut::Close(void* impl) {
|
|
if (!impl) {
|
|
return;
|
|
}
|
|
delete static_cast<CubebStream*>(impl);
|
|
}
|
|
|
|
void CubebAudioOut::Output(void* impl, void* ptr, size_t size) {
|
|
if (!impl) {
|
|
return;
|
|
}
|
|
static_cast<CubebStream*>(impl)->Output(ptr, size);
|
|
}
|
|
|
|
void CubebAudioOut::SetVolume(void* impl, const std::array<int, 8>& ch_volumes) {
|
|
if (!impl) {
|
|
return;
|
|
}
|
|
static_cast<CubebStream*>(impl)->SetVolume(ch_volumes);
|
|
}
|
|
|
|
void CubebAudioOut::LogCallback(const char* format, ...) {
|
|
std::array<char, 512> buffer{};
|
|
std::va_list args;
|
|
va_start(args, format);
|
|
vsnprintf(buffer.data(), buffer.size(), format, args);
|
|
va_end(args);
|
|
LOG_DEBUG(Lib_Audio3d, "[cubeb] {}", buffer.data());
|
|
}
|
|
|
|
} // namespace Libraries::AudioOut
|