SPU: Add time stretched audio output
This commit is contained in:
@ -1,6 +1,11 @@
|
||||
#include "cubeb_audio_stream.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common_host.h"
|
||||
#include "core/host.h"
|
||||
#include "core/host_settings.h"
|
||||
#include "cubeb/cubeb.h"
|
||||
Log_SetChannel(CubebAudioStream);
|
||||
|
||||
#ifdef _WIN32
|
||||
@ -9,154 +14,188 @@ Log_SetChannel(CubebAudioStream);
|
||||
#pragma comment(lib, "Ole32.lib")
|
||||
#endif
|
||||
|
||||
CubebAudioStream::CubebAudioStream() = default;
|
||||
static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state);
|
||||
|
||||
CubebAudioStream::CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch)
|
||||
: AudioStream(sample_rate, channels, buffer_ms, stretch)
|
||||
{
|
||||
}
|
||||
|
||||
CubebAudioStream::~CubebAudioStream()
|
||||
{
|
||||
if (IsOpen())
|
||||
CubebAudioStream::CloseDevice();
|
||||
DestroyContextAndStream();
|
||||
}
|
||||
|
||||
bool CubebAudioStream::OpenDevice()
|
||||
void CubebAudioStream::LogCallback(const char* fmt, ...)
|
||||
{
|
||||
Assert(!IsOpen());
|
||||
std::va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::string msg(StringUtil::StdStringFromFormatV(fmt, ap));
|
||||
va_end(ap);
|
||||
Log_DevPrintf("(Cubeb): %s", msg.c_str());
|
||||
}
|
||||
|
||||
void CubebAudioStream::DestroyContextAndStream()
|
||||
{
|
||||
if (stream)
|
||||
{
|
||||
cubeb_stream_stop(stream);
|
||||
cubeb_stream_destroy(stream);
|
||||
stream = nullptr;
|
||||
}
|
||||
|
||||
if (m_context)
|
||||
{
|
||||
cubeb_destroy(m_context);
|
||||
m_context = nullptr;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_com_initialized_by_us)
|
||||
{
|
||||
CoUninitialize();
|
||||
m_com_initialized_by_us = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CubebAudioStream::Initialize(u32 latency_ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
m_com_initialized_by_us = SUCCEEDED(hr);
|
||||
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE && hr != S_FALSE)
|
||||
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to initialize COM");
|
||||
Host::ReportErrorAsync("Error", "Failed to initialize COM for Cubeb");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
int rv = cubeb_init(&m_cubeb_context, "DuckStation", nullptr);
|
||||
cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback);
|
||||
|
||||
std::string backend(Host::GetStringSettingValue("Audio", "CubebBackend"));
|
||||
|
||||
int rv = cubeb_init(&m_context, "DuckStation", backend.empty() ? nullptr : backend.c_str());
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("Could not initialize cubeb context: %d", rv);
|
||||
Host::ReportFormattedErrorAsync("Error", "Could not initialize cubeb context: %d", rv);
|
||||
return false;
|
||||
}
|
||||
|
||||
cubeb_stream_params params = {};
|
||||
params.format = CUBEB_SAMPLE_S16LE;
|
||||
params.rate = m_output_sample_rate;
|
||||
params.rate = m_sample_rate;
|
||||
params.channels = m_channels;
|
||||
params.layout = CUBEB_LAYOUT_UNDEFINED;
|
||||
params.prefs = CUBEB_STREAM_PREF_PERSIST;
|
||||
params.prefs = CUBEB_STREAM_PREF_NONE;
|
||||
|
||||
u32 latency_frames = 0;
|
||||
rv = cubeb_get_min_latency(m_cubeb_context, ¶ms, &latency_frames);
|
||||
u32 latency_frames = GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms);
|
||||
u32 min_latency_frames = 0;
|
||||
rv = cubeb_get_min_latency(m_context, ¶ms, &min_latency_frames);
|
||||
if (rv == CUBEB_ERROR_NOT_SUPPORTED)
|
||||
{
|
||||
Log_WarningPrintf("Cubeb backend does not support latency queries, using buffer size of %u.", m_buffer_size);
|
||||
latency_frames = m_buffer_size;
|
||||
Log_DevPrintf("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).",
|
||||
m_buffer_ms, latency_frames);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("Could not get minimum latency: %d", rv);
|
||||
DestroyContext();
|
||||
Log_ErrorPrintf("(Cubeb) Could not get minimum latency: %d", rv);
|
||||
DestroyContextAndStream();
|
||||
return false;
|
||||
}
|
||||
|
||||
Log_InfoPrintf("Minimum latency in frames: %u", latency_frames);
|
||||
if (latency_frames > m_buffer_size)
|
||||
const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames);
|
||||
Log_DevPrintf("(Cubeb) Minimum latency: %u ms (%u audio frames)", minimum_latency_ms, min_latency_frames);
|
||||
if (latency_ms == 0)
|
||||
{
|
||||
Log_WarningPrintf("Minimum latency is above buffer size: %u vs %u, adjusting to compensate.", latency_frames,
|
||||
m_buffer_size);
|
||||
|
||||
if (!SetBufferSize(latency_frames))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to set new buffer size of %u frames", latency_frames);
|
||||
DestroyContext();
|
||||
return false;
|
||||
}
|
||||
// use minimum
|
||||
latency_frames = min_latency_frames;
|
||||
}
|
||||
else
|
||||
else if (minimum_latency_ms > latency_ms)
|
||||
{
|
||||
latency_frames = m_buffer_size;
|
||||
Log_WarningPrintf("(Cubeb) Minimum latency is above requested latency: %u vs %u, adjusting to compensate.",
|
||||
min_latency_frames, latency_frames);
|
||||
latency_frames = min_latency_frames;
|
||||
}
|
||||
}
|
||||
|
||||
char stream_name[32];
|
||||
std::snprintf(stream_name, sizeof(stream_name), "AudioStream_%p", this);
|
||||
BaseInitialize();
|
||||
m_volume = 100;
|
||||
m_paused = false;
|
||||
|
||||
rv = cubeb_stream_init(m_cubeb_context, &m_cubeb_stream, stream_name, nullptr, nullptr, nullptr, ¶ms,
|
||||
latency_frames, DataCallback, StateCallback, this);
|
||||
char stream_name[32];
|
||||
std::snprintf(stream_name, sizeof(stream_name), "%p", this);
|
||||
|
||||
rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, ¶ms, latency_frames,
|
||||
&CubebAudioStream::DataCallback, StateCallback, this);
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("Could not create stream: %d", rv);
|
||||
DestroyContext();
|
||||
Log_ErrorPrintf("(Cubeb) Could not create stream: %d", rv);
|
||||
DestroyContextAndStream();
|
||||
return false;
|
||||
}
|
||||
|
||||
rv = cubeb_stream_start(stream);
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("(Cubeb) Could not start stream: %d", rv);
|
||||
DestroyContextAndStream();
|
||||
return false;
|
||||
}
|
||||
|
||||
cubeb_stream_set_volume(m_cubeb_stream, static_cast<float>(m_output_volume) / 100.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CubebAudioStream::PauseDevice(bool paused)
|
||||
void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state)
|
||||
{
|
||||
if (paused == m_paused)
|
||||
// noop
|
||||
}
|
||||
|
||||
long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
|
||||
long nframes)
|
||||
{
|
||||
static_cast<CubebAudioStream*>(user_ptr)->ReadFrames(static_cast<s16*>(output_buffer), static_cast<u32>(nframes));
|
||||
return nframes;
|
||||
}
|
||||
|
||||
void CubebAudioStream::SetPaused(bool paused)
|
||||
{
|
||||
if (paused == m_paused || !stream)
|
||||
return;
|
||||
|
||||
int rv = paused ? cubeb_stream_stop(m_cubeb_stream) : cubeb_stream_start(m_cubeb_stream);
|
||||
const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream);
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("cubeb_stream_%s failed: %d", paused ? "stop" : "start", rv);
|
||||
Log_ErrorPrintf("Could not %s stream: %d", paused ? "pause" : "resume", rv);
|
||||
return;
|
||||
}
|
||||
|
||||
m_paused = paused;
|
||||
}
|
||||
|
||||
void CubebAudioStream::CloseDevice()
|
||||
{
|
||||
Assert(IsOpen());
|
||||
|
||||
if (!m_paused)
|
||||
{
|
||||
cubeb_stream_stop(m_cubeb_stream);
|
||||
m_paused = true;
|
||||
}
|
||||
|
||||
cubeb_stream_destroy(m_cubeb_stream);
|
||||
m_cubeb_stream = nullptr;
|
||||
|
||||
DestroyContext();
|
||||
}
|
||||
|
||||
long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
|
||||
long nframes)
|
||||
{
|
||||
CubebAudioStream* const this_ptr = static_cast<CubebAudioStream*>(user_ptr);
|
||||
this_ptr->ReadFrames(reinterpret_cast<SampleType*>(output_buffer), static_cast<u32>(nframes), false);
|
||||
return nframes;
|
||||
}
|
||||
|
||||
void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) {}
|
||||
|
||||
void CubebAudioStream::FramesAvailable() {}
|
||||
|
||||
void CubebAudioStream::SetOutputVolume(u32 volume)
|
||||
{
|
||||
AudioStream::SetOutputVolume(volume);
|
||||
cubeb_stream_set_volume(m_cubeb_stream, static_cast<float>(m_output_volume) / 100.0f);
|
||||
if (volume == m_volume)
|
||||
return;
|
||||
|
||||
int rv = cubeb_stream_set_volume(stream, static_cast<float>(volume) / 100.0f);
|
||||
if (rv != CUBEB_OK)
|
||||
{
|
||||
Log_ErrorPrintf("cubeb_stream_set_volume() failed: %d", rv);
|
||||
return;
|
||||
}
|
||||
|
||||
m_volume = volume;
|
||||
}
|
||||
|
||||
void CubebAudioStream::DestroyContext()
|
||||
std::unique_ptr<AudioStream> CommonHost::CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms,
|
||||
u32 latency_ms, AudioStretchMode stretch)
|
||||
{
|
||||
cubeb_destroy(m_cubeb_context);
|
||||
m_cubeb_context = nullptr;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_com_initialized_by_us)
|
||||
CoUninitialize();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStream> CubebAudioStream::Create()
|
||||
{
|
||||
return std::make_unique<CubebAudioStream>();
|
||||
std::unique_ptr<CubebAudioStream> stream(
|
||||
std::make_unique<CubebAudioStream>(sample_rate, channels, buffer_ms, stretch));
|
||||
if (!stream->Initialize(latency_ms))
|
||||
stream.reset();
|
||||
return stream;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user