From ca091eeea953181caf59d3a66f523ccbd8df674c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 24 Apr 2024 22:29:49 +1000 Subject: [PATCH] Host: Add AudioStream --- pcsx2/CMakeLists.txt | 19 +- pcsx2/Host/AudioStream.cpp | 885 ++++++++++++++++++++++++++++++++ pcsx2/Host/AudioStream.h | 265 ++++++++++ pcsx2/Host/AudioStreamTypes.h | 80 +++ pcsx2/Host/CubebAudioStream.cpp | 356 +++++++++++++ pcsx2/Host/SDLAudioStream.cpp | 148 ++++++ pcsx2/pcsx2.vcxproj | 9 + pcsx2/pcsx2.vcxproj.filters | 18 + 8 files changed, 1775 insertions(+), 5 deletions(-) create mode 100644 pcsx2/Host/AudioStream.cpp create mode 100644 pcsx2/Host/AudioStream.h create mode 100644 pcsx2/Host/AudioStreamTypes.h create mode 100644 pcsx2/Host/CubebAudioStream.cpp create mode 100644 pcsx2/Host/SDLAudioStream.cpp diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index fb084a4708..53353bb3c4 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -795,6 +795,15 @@ set(pcsx2DebugToolsHeaders DebugTools/DisVUops.h DebugTools/BiosDebugData.h) +set(pcsx2HostSources + Host/AudioStream.cpp + Host/CubebAudioStream.cpp + Host/SDLAudioStream.cpp) + +set(pcsx2HostHeaders + Host/AudioStream.h + Host/AudioStreamTypes.h) + set(pcsx2ImGuiSources ImGui/FullscreenUI.cpp ImGui/ImGuiFullscreen.cpp @@ -1052,6 +1061,8 @@ target_sources(PCSX2 PRIVATE ${pcsx2RecordingSources} ${pcsx2DebugToolsSources} ${pcsx2DebugToolsHeaders} + ${pcsx2HostSources} + ${pcsx2HostHeaders} ${pcsx2ImGuiSources} ${pcsx2ImGuiHeaders} ${pcsx2InputSources} @@ -1105,6 +1116,9 @@ target_link_libraries(PCSX2_FLAGS INTERFACE cubeb rcheevos discord-rpc + demangler + simpleini + freesurround SDL2::SDL2 ZLIB::ZLIB LZ4::LZ4 @@ -1115,11 +1129,6 @@ target_link_libraries(PCSX2_FLAGS INTERFACE ${LIBC_LIBRARIES} ) -target_link_libraries(PCSX2_FLAGS INTERFACE - demangler - simpleini -) - if(WIN32) target_link_libraries(PCSX2_FLAGS INTERFACE WIL::WIL diff --git a/pcsx2/Host/AudioStream.cpp b/pcsx2/Host/AudioStream.cpp new file mode 100644 index 0000000000..babf9f8e06 --- /dev/null +++ b/pcsx2/Host/AudioStream.cpp @@ -0,0 +1,885 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: GPL-3.0+ + +#include "Host/AudioStream.h" +#include "FreeSurroundDecoder.h" +#include "Host.h" +#include "GS/GSVector.h" + +#include "common/Assertions.h" +#include "common/BitUtils.h" +#include "common/Console.h" +#include "common/Error.h" +#include "common/Pcsx2Defs.h" +#include "common/SettingsWrapper.h" +#include "common/SmallString.h" +#include "common/Timer.h" + +#include "SoundTouch.h" + +#include +#include +#include +#include +#include + +//#define LOG_UNDERRUN(...) DEV_LOG(__VA_ARGS__) +#define LOG_UNDERRUN(...) (void)0 +static constexpr bool LOG_TIMESTRETCH_STATS = false; + +static constexpr const std::array, static_cast(AudioExpansionMode::Count)> + s_expansion_channel_count = {{ + {u8(2), u8(2)}, // Disabled + {u8(3), u8(3)}, // StereoLFE + {u8(5), u8(4)}, // Quadraphonic + {u8(5), u8(5)}, // QuadraphonicLFE + {u8(6), u8(6)}, // Surround51 + {u8(8), u8(8)}, // Surround71 + }}; + +AudioStream::DeviceInfo::DeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_) + : name(std::move(name_)) + , display_name(std::move(display_name_)) + , minimum_latency_frames(minimum_latency_) +{ +} + +AudioStream::DeviceInfo::~DeviceInfo() = default; + +AudioStream::AudioStream(u32 sample_rate, const AudioStreamParameters& parameters) + : m_sample_rate(sample_rate) + , m_parameters(parameters) + , m_internal_channels(s_expansion_channel_count[static_cast(parameters.expansion_mode)].first) + , m_output_channels(s_expansion_channel_count[static_cast(parameters.expansion_mode)].second) +{ +} + +AudioStream::~AudioStream() +{ + DestroyBuffer(); +} + +std::unique_ptr AudioStream::CreateNullStream(u32 sample_rate, u32 buffer_ms) +{ + // no point stretching with no output + AudioStreamParameters params; + params.expansion_mode = AudioExpansionMode::Disabled; + params.buffer_ms = static_cast(buffer_ms); + + std::unique_ptr stream(new AudioStream(sample_rate, params)); + stream->BaseInitialize(&StereoSampleReaderImpl, false); + stream->SetOutputVolume(0); + return stream; +} + +std::vector> AudioStream::GetDriverNames(AudioBackend backend) +{ + std::vector> ret; + switch (backend) + { + case AudioBackend::Cubeb: + ret = GetCubebDriverNames(); + break; + + default: + break; + } + + return ret; +} + +std::vector AudioStream::GetOutputDevices(AudioBackend backend, const char* driver) +{ + std::vector ret; + switch (backend) + { + case AudioBackend::Cubeb: + ret = GetCubebOutputDevices(driver); + break; + + default: + break; + } + + return ret; +} + +std::unique_ptr AudioStream::CreateStream(AudioBackend backend, u32 sample_rate, const AudioStreamParameters& parameters, + const char* driver_name, const char* device_name, bool stretch_enabled, Error* error) +{ + INFO_LOG("Creating {} audio stream, sample rate = {}, expansion = {}, buffer = {}, latency = {}, stretching {}, driver = {}, device = {}", + GetBackendName(backend), sample_rate, GetExpansionModeName(parameters.expansion_mode), parameters.buffer_ms, parameters.output_latency_ms, + stretch_enabled ? "enabled" : "disabled", driver_name, device_name); + + switch (backend) + { + case AudioBackend::Cubeb: + return CreateCubebAudioStream(sample_rate, parameters, driver_name, device_name, stretch_enabled, error); + + case AudioBackend::SDL: + return CreateSDLAudioStream(sample_rate, parameters, stretch_enabled, error); + + case AudioBackend::Null: + return CreateNullStream(sample_rate, parameters.buffer_ms); + + default: + Error::SetStringView(error, "Unknown audio backend."); + return nullptr; + } +} + +u32 AudioStream::GetAlignedBufferSize(u32 size) +{ + static_assert(std::has_single_bit(CHUNK_SIZE)); + return Common::AlignUpPow2(size, CHUNK_SIZE); +} + +u32 AudioStream::GetBufferSizeForMS(u32 sample_rate, u32 ms) +{ + return GetAlignedBufferSize((ms * sample_rate) / 1000u); +} + +u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size) +{ + buffer_size = GetAlignedBufferSize(buffer_size); + return (buffer_size * 1000u) / sample_rate; +} + +static constexpr const std::array s_backend_names = { + "Null", + "Cubeb", + "SDL", +}; +static constexpr const std::array s_backend_display_names = { + TRANSLATE_NOOP("AudioStream", "Null (No Output)"), + TRANSLATE_NOOP("AudioStream", "Cubeb"), + TRANSLATE_NOOP("AudioStream", "SDL"), +}; + +std::optional AudioStream::ParseBackendName(const char* str) +{ + int index = 0; + for (const char* name : s_backend_names) + { + if (std::strcmp(name, str) == 0) + return static_cast(index); + + index++; + } + + return std::nullopt; +} + +const char* AudioStream::GetBackendName(AudioBackend backend) +{ + return s_backend_names[static_cast(backend)]; +} + +const char* AudioStream::GetBackendDisplayName(AudioBackend backend) +{ + return Host::TranslateToCString("AudioStream", s_backend_display_names[static_cast(backend)]); +} + +static constexpr const std::array s_expansion_mode_names = { + "Disabled", + "StereoLFE", + "Quadraphonic", + "QuadraphonicLFE", + "Surround51", + "Surround71", +}; +static constexpr const std::array s_expansion_mode_display_names = { + TRANSLATE_NOOP("AudioStream", "Disabled (Stereo)"), + TRANSLATE_NOOP("AudioStream", "Stereo with LFE"), + TRANSLATE_NOOP("AudioStream", "Quadraphonic"), + TRANSLATE_NOOP("AudioStream", "Quadraphonic with LFE"), + TRANSLATE_NOOP("AudioStream", "5.1 Surround"), + TRANSLATE_NOOP("AudioStream", "7.1 Surround"), +}; + +const char* AudioStream::GetExpansionModeName(AudioExpansionMode mode) +{ + return (static_cast(mode) < s_expansion_mode_names.size()) ? s_expansion_mode_names[static_cast(mode)] : ""; +} + +const char* AudioStream::GetExpansionModeDisplayName(AudioExpansionMode mode) +{ + return (static_cast(mode) < s_expansion_mode_display_names.size()) ? + Host::TranslateToCString("AudioStream", s_expansion_mode_display_names[static_cast(mode)]) : + ""; +} + +std::optional AudioStream::ParseExpansionMode(const char* name) +{ + for (u8 i = 0; i < static_cast(AudioExpansionMode::Count); i++) + { + if (std::strcmp(name, s_expansion_mode_names[i]) == 0) + return static_cast(i); + } + + return std::nullopt; +} + +u32 AudioStream::GetBufferedFramesRelaxed() const +{ + const u32 rpos = m_rpos.load(std::memory_order_relaxed); + const u32 wpos = m_wpos.load(std::memory_order_relaxed); + return (wpos + m_buffer_size - rpos) % m_buffer_size; +} + +void AudioStream::ReadFrames(SampleType* samples, u32 num_frames) +{ + const u32 available_frames = GetBufferedFramesRelaxed(); + u32 frames_to_read = num_frames; + u32 silence_frames = 0; + + if (m_filling) + { + u32 toFill = m_buffer_size / (IsStretchEnabled() ? 32 : 400); + toFill = GetAlignedBufferSize(toFill); + + if (available_frames < toFill) + { + silence_frames = num_frames; + frames_to_read = 0; + } + else + { + m_filling = false; + LOG_UNDERRUN("Underrun compensation done ({} frames buffered)", toFill); + } + } + + if (available_frames < frames_to_read) + { + silence_frames = frames_to_read - available_frames; + frames_to_read = available_frames; + m_filling = true; + + if (IsStretchEnabled()) + StretchUnderrun(); + } + + if (frames_to_read > 0) + { + u32 rpos = m_rpos.load(std::memory_order_acquire); + + u32 end = m_buffer_size - rpos; + if (end > frames_to_read) + end = frames_to_read; + + // towards the end of the buffer + if (end > 0) + { + m_sample_reader(samples, &m_buffer[rpos * m_internal_channels], end); + rpos += end; + rpos = (rpos == m_buffer_size) ? 0 : rpos; + } + + // after wrapping around + const u32 start = frames_to_read - end; + if (start > 0) + { + m_sample_reader(&samples[end * m_output_channels], &m_buffer[0], start); + rpos = start; + } + + m_rpos.store(rpos, std::memory_order_release); + } + + if (silence_frames > 0) + { + if (frames_to_read > 0) + { + // super basic resampler - spread the input samples evenly across the output samples. will sound like ass and have + // aliasing, but better than popping by inserting silence. + const u32 increment = + static_cast(65536.0f * (static_cast(frames_to_read) / static_cast(num_frames))); + + SampleType* resample_ptr = + static_cast(alloca(frames_to_read * m_output_channels * sizeof(SampleType))); + std::memcpy(resample_ptr, samples, frames_to_read * m_output_channels * sizeof(SampleType)); + + SampleType* out_ptr = samples; + const u32 copy_stride = sizeof(SampleType) * m_output_channels; + u32 resample_subpos = 0; + for (u32 i = 0; i < num_frames; i++) + { + std::memcpy(out_ptr, resample_ptr, copy_stride); + out_ptr += m_output_channels; + + resample_subpos += increment; + resample_ptr += (resample_subpos >> 16) * m_output_channels; + resample_subpos %= 65536u; + } + + LOG_UNDERRUN("Audio buffer underflow, resampled {} frames to {}", frames_to_read, num_frames); + } + else + { + // no data, fall back to silence + std::memset(samples + (frames_to_read * m_output_channels), 0, silence_frames * m_output_channels * sizeof(s16)); + } + } + + if (m_volume != 100) + { + const s32 volume_mult = static_cast((static_cast(m_volume) / 100.0f) * 32768.0f); + + u32 num_samples = num_frames * m_output_channels; + while (num_samples > 0) + { + *samples = static_cast((static_cast(*samples) * volume_mult) >> 15); + samples++; + num_samples--; + } + } +} + +void AudioStream::StereoSampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames) +{ + std::memcpy(dest, src, num_frames * 2 * sizeof(SampleType)); +} + +void AudioStream::InternalWriteFrames(const SampleType* data, u32 num_frames) +{ + const u32 free = m_buffer_size - GetBufferedFramesRelaxed(); + if (free <= num_frames) + { + if (IsStretchEnabled()) + { + StretchOverrun(); + } + else + { + LOG_UNDERRUN("Buffer overrun, chunk dropped"); + return; + } + } + + u32 wpos = m_wpos.load(std::memory_order_acquire); + + // wrapping around the end of the buffer? + if ((m_buffer_size - wpos) <= num_frames) + { + // needs to be written in two parts + const u32 end = m_buffer_size - wpos; + const u32 start = num_frames - end; + + // start is zero when this chunk reaches exactly the end + std::memcpy(&m_buffer[wpos * m_internal_channels], data, end * m_internal_channels * sizeof(SampleType)); + if (start > 0) + std::memcpy(&m_buffer[0], data + end * m_internal_channels, start * m_internal_channels * sizeof(SampleType)); + + wpos = start; + } + else + { + // no split + std::memcpy(&m_buffer[wpos * m_internal_channels], data, num_frames * m_internal_channels * sizeof(SampleType)); + wpos += num_frames; + } + + m_wpos.store(wpos, std::memory_order_release); +} + +void AudioStream::BaseInitialize(SampleReader sample_reader, bool stretch_enabled) +{ + m_stretch_enabled = stretch_enabled; + m_paused = false; + m_sample_reader = sample_reader; + + AllocateBuffer(); + ExpandAllocate(); + StretchAllocate(); +} + +void AudioStream::AllocateBuffer() +{ + // use a larger buffer when time stretching, since we need more input + const u32 multiplier = IsStretchEnabled() ? 16 : 1; + m_buffer_size = GetAlignedBufferSize(((m_parameters.buffer_ms * multiplier) * m_sample_rate) / 1000); + m_target_buffer_size = GetAlignedBufferSize((m_sample_rate * m_parameters.buffer_ms) / 1000u); + + m_buffer = std::make_unique(m_buffer_size * m_internal_channels); + m_staging_buffer = std::make_unique(CHUNK_SIZE * m_internal_channels); + m_float_buffer = std::make_unique(CHUNK_SIZE * m_internal_channels); + + if (IsExpansionEnabled()) + m_expand_buffer = std::make_unique(m_parameters.expand_block_size * NUM_INPUT_CHANNELS); + + DEV_LOG( + "Allocated buffer of {} frames for buffer of {} ms [expansion {} (block size {}), stretch {}, target size {}].", + m_buffer_size, m_parameters.buffer_ms, GetExpansionModeName(m_parameters.expansion_mode), + m_parameters.expand_block_size, m_stretch_enabled ? "enabled" : "disabled", m_target_buffer_size); +} + +void AudioStream::DestroyBuffer() +{ + m_expand_buffer.reset(); + m_staging_buffer.reset(); + m_float_buffer.reset(); + m_buffer.reset(); + m_buffer_size = 0; + m_wpos.store(0, std::memory_order_release); + m_rpos.store(0, std::memory_order_release); +} + +void AudioStream::EmptyBuffer() +{ + if (IsExpansionEnabled()) + { + m_expander->Flush(); + m_expand_output_buffer = nullptr; + m_expand_buffer_pos = 0; + } + + if (IsStretchEnabled()) + { + m_soundtouch->clear(); + if (IsStretchEnabled()) + m_soundtouch->setTempo(m_nominal_rate); + } + + m_wpos.store(m_rpos.load(std::memory_order_acquire), std::memory_order_release); +} + +void AudioStream::SetNominalRate(float tempo) +{ + m_nominal_rate = tempo; +} + +void AudioStream::UpdateTargetTempo(float tempo) +{ + if (!IsStretchEnabled()) + return; + + // undo sqrt() + if (tempo) + tempo *= tempo; + + m_average_position = AVERAGING_WINDOW; + m_average_available = AVERAGING_WINDOW; + std::fill_n(m_average_fullness.data(), AVERAGING_WINDOW, tempo); + m_soundtouch->setTempo(tempo); + m_stretch_reset = 0; + m_stretch_inactive = false; + m_stretch_ok_count = 0; + m_dynamic_target_usage = static_cast(m_target_buffer_size) * m_nominal_rate; +} + +void AudioStream::SetStretchEnabled(bool enabled) +{ + if (m_stretch_enabled == enabled) + return; + + // can't resize the buffers while paused + const bool paused = m_paused; + if (!paused) + SetPaused(true); + + DestroyBuffer(); + StretchDestroy(); + m_stretch_enabled = enabled; + + AllocateBuffer(); + StretchAllocate(); + + if (!paused) + SetPaused(false); +} + +void AudioStream::SetPaused(bool paused) +{ + m_paused = paused; +} + +void AudioStream::SetOutputVolume(u32 volume) +{ + m_volume = volume; +} + +void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames) +{ + *buffer_ptr = &m_staging_buffer[m_staging_buffer_pos]; + *num_frames = CHUNK_SIZE - (m_staging_buffer_pos / NUM_INPUT_CHANNELS); +} + +void AudioStream::WriteFrame(const SampleType* frame) +{ + pxAssert((CHUNK_SIZE - (m_staging_buffer_pos / NUM_INPUT_CHANNELS)) > 0); + std::memcpy(&m_staging_buffer[m_staging_buffer_pos], frame, sizeof(SampleType) * NUM_INPUT_CHANNELS); + EndWrite(1); +} + +static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples) +{ + constexpr GSVector4 S16_TO_FLOAT_V = GSVector4::cxpr(1.0f / 32767.0f); + + const u32 iterations = (num_samples + 7) / 8; + for (u32 i = 0; i < iterations; i++) + { + const GSVector4i sv = GSVector4i::load(src); + src += 8; + + GSVector4i iv1 = sv.upl16(sv); // [0, 0, 1, 1, 2, 2, 3, 3] + GSVector4i iv2 = sv.uph16(sv); // [4, 4, 5, 5, 6, 6, 7, 7] + iv1 = iv1.sra32<16>(); // [0, 1, 2, 3] + iv2 = iv2.sra32<16>(); // [4, 5, 6, 7] + GSVector4 fv1 = GSVector4(iv1); // [f0, f1, f2, f3] + GSVector4 fv2 = GSVector4(iv2); // [f4, f5, f6, f7] + fv1 = fv1 * S16_TO_FLOAT_V; + fv2 = fv2 * S16_TO_FLOAT_V; + + GSVector4::store(dst + 0, fv1); + GSVector4::store(dst + 4, fv2); + dst += 8; + } +} + +static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples) +{ + const GSVector4 FLOAT_TO_S16_V = GSVector4::cxpr(32767.0f); + + const u32 iterations = (num_samples + 7) / 8; + for (u32 i = 0; i < iterations; i++) + { + GSVector4 fv1 = GSVector4::load(src + 0); + GSVector4 fv2 = GSVector4::load(src + 4); + src += 8; + + fv1 = fv1 * FLOAT_TO_S16_V; + fv2 = fv2 * FLOAT_TO_S16_V; + GSVector4i iv1 = GSVector4i(fv1); + GSVector4i iv2 = GSVector4i(fv2); + + const GSVector4i iv = iv1.ps32(iv2); + GSVector4i::store(dst, iv); + dst += 8; + } +} + +void AudioStream::ExpandAllocate() +{ + pxAssert(!m_expander); + if (m_parameters.expansion_mode == AudioExpansionMode::Disabled) + return; + + static constexpr std::array, + static_cast(AudioExpansionMode::Count)> + channel_setup_mapping = {{ + {FreeSurroundDecoder::ChannelSetup::Stereo, false}, // Disabled + {FreeSurroundDecoder::ChannelSetup::Stereo, true}, // StereoLFE + {FreeSurroundDecoder::ChannelSetup::Surround41, false}, // Quadraphonic + {FreeSurroundDecoder::ChannelSetup::Surround41, true}, // QuadraphonicLFE + {FreeSurroundDecoder::ChannelSetup::Surround51, true}, // Surround51 + {FreeSurroundDecoder::ChannelSetup::Surround71, true}, // Surround71 + }}; + + const auto [fs_setup, fs_lfe] = channel_setup_mapping[static_cast(m_parameters.expansion_mode)]; + + m_expander = std::make_unique(fs_setup, m_parameters.expand_block_size); + m_expander->SetBassRedirection(fs_lfe); + m_expander->SetCircularWrap(m_parameters.expand_circular_wrap); + m_expander->SetShift(m_parameters.expand_shift); + m_expander->SetDepth(m_parameters.expand_depth); + m_expander->SetFocus(m_parameters.expand_focus); + m_expander->SetCenterImage(m_parameters.expand_center_image); + m_expander->SetFrontSeparation(m_parameters.expand_front_separation); + m_expander->SetRearSeparation(m_parameters.expand_rear_separation); + m_expander->SetLowCutoff(static_cast(m_parameters.expand_low_cutoff) / m_sample_rate * 2); + m_expander->SetHighCutoff(static_cast(m_parameters.expand_high_cutoff) / m_sample_rate * 2); +} + +void AudioStream::EndWrite(u32 num_frames) +{ + // don't bother committing anything when muted + if (m_volume == 0) + return; + + m_staging_buffer_pos += num_frames * NUM_INPUT_CHANNELS; + pxAssert(m_staging_buffer_pos <= (CHUNK_SIZE * NUM_INPUT_CHANNELS)); + if ((m_staging_buffer_pos / NUM_INPUT_CHANNELS) < CHUNK_SIZE) + return; + + m_staging_buffer_pos = 0; + WriteChunk(m_staging_buffer.get()); +} + +void AudioStream::WriteChunk(const SampleType* chunk) +{ + if (!IsExpansionEnabled() && !IsStretchEnabled()) + { + InternalWriteFrames(chunk, CHUNK_SIZE); + return; + } + + if (IsExpansionEnabled()) + { + // StretchWriteBlock() overwrites the staging buffer on output, so we need to copy into the expand buffer first. + S16ChunkToFloat(chunk, m_expand_buffer.get() + m_expand_buffer_pos * NUM_INPUT_CHANNELS, CHUNK_SIZE * NUM_INPUT_CHANNELS); + + // Output the corresponding block. + if (m_expand_output_buffer) + StretchWriteBlock(m_expand_output_buffer + m_expand_buffer_pos * m_internal_channels); + + // Decode the next block if we buffered enough. + m_expand_buffer_pos += CHUNK_SIZE; + if (m_expand_buffer_pos == m_parameters.expand_block_size) + { + m_expand_buffer_pos = 0; + m_expand_output_buffer = m_expander->Decode(m_expand_buffer.get()); + } + } + else + { + S16ChunkToFloat(chunk, m_float_buffer.get(), CHUNK_SIZE * NUM_INPUT_CHANNELS); + StretchWriteBlock(m_float_buffer.get()); + } +} + +template +__fi static bool IsInRange(const T& val, const T& min, const T& max) +{ + return (min <= val && val <= max); +} + +void AudioStream::StretchAllocate() +{ + if (!IsStretchEnabled()) + return; + + m_soundtouch = std::make_unique(); + m_soundtouch->setSampleRate(m_sample_rate); + m_soundtouch->setChannels(m_internal_channels); + + m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, m_parameters.stretch_use_quickseek); + m_soundtouch->setSetting(SETTING_USE_AA_FILTER, m_parameters.stretch_use_aa_filter); + + m_soundtouch->setSetting(SETTING_SEQUENCE_MS, m_parameters.stretch_sequence_length_ms); + m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, m_parameters.stretch_seekwindow_ms); + m_soundtouch->setSetting(SETTING_OVERLAP_MS, m_parameters.stretch_overlap_ms); + + m_soundtouch->setTempo(m_nominal_rate); + + m_stretch_reset = STRETCH_RESET_THRESHOLD; + m_stretch_inactive = false; + m_stretch_ok_count = 0; + m_dynamic_target_usage = 0.0f; + m_average_position = 0; + m_average_available = 0; + + m_staging_buffer_pos = 0; +} + +void AudioStream::StretchDestroy() +{ + m_soundtouch.reset(); +} + +void AudioStream::StretchWriteBlock(const float* block) +{ + if (IsStretchEnabled()) + { + m_soundtouch->putSamples(block, CHUNK_SIZE); + + u32 tempProgress; + while (tempProgress = m_soundtouch->receiveSamples(m_float_buffer.get(), CHUNK_SIZE), tempProgress != 0) + { + FloatChunkToS16(m_staging_buffer.get(), m_float_buffer.get(), tempProgress * m_internal_channels); + InternalWriteFrames(m_staging_buffer.get(), tempProgress); + } + + if (IsStretchEnabled()) + UpdateStretchTempo(); + } + else + { + FloatChunkToS16(m_staging_buffer.get(), block, CHUNK_SIZE * m_internal_channels); + InternalWriteFrames(m_staging_buffer.get(), CHUNK_SIZE); + } +} + +float AudioStream::AddAndGetAverageTempo(float val) +{ + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + m_average_available = 0; + if (m_average_available < AVERAGING_BUFFER_SIZE) + m_average_available++; + + m_average_fullness[m_average_position] = val; + m_average_position = (m_average_position + 1U) % AVERAGING_BUFFER_SIZE; + + const u32 actual_window = std::min(m_average_available, AVERAGING_WINDOW); + const u32 first_index = (m_average_position - actual_window + AVERAGING_BUFFER_SIZE) % AVERAGING_BUFFER_SIZE; + + float sum = 0; + for (u32 i = first_index; i < first_index + actual_window; i++) + sum += m_average_fullness[i % AVERAGING_BUFFER_SIZE]; + sum = sum / actual_window; + + return (sum != 0.0f) ? sum : 1.0f; +} + +void AudioStream::UpdateStretchTempo() +{ + static constexpr float MIN_TEMPO = 0.05f; + static constexpr float MAX_TEMPO = 50.0f; + + // Which range we will run in 1:1 mode for. + static constexpr float INACTIVE_GOOD_FACTOR = 1.04f; + static constexpr float INACTIVE_BAD_FACTOR = 1.2f; + static constexpr u32 INACTIVE_MIN_OK_COUNT = 50; + static constexpr u32 COMPENSATION_DIVIDER = 100; + + float base_target_usage = static_cast(m_target_buffer_size) * m_nominal_rate; + + // state vars + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + { + LOG_UNDERRUN("___ Stretcher is being reset."); + m_stretch_inactive = false; + m_stretch_ok_count = 0; + m_dynamic_target_usage = base_target_usage; + } + + const u32 ibuffer_usage = GetBufferedFramesRelaxed(); + float buffer_usage = static_cast(ibuffer_usage); + float tempo = buffer_usage / m_dynamic_target_usage; + tempo = AddAndGetAverageTempo(tempo); + + // Dampening when we get close to target. + if (tempo < 2.0f) + tempo = std::sqrt(tempo); + + tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO); + + if (tempo < 1.0f) + base_target_usage /= std::sqrt(tempo); + + m_dynamic_target_usage += + static_cast(base_target_usage / tempo - m_dynamic_target_usage) / static_cast(COMPENSATION_DIVIDER); + if (IsInRange(tempo, 0.9f, 1.1f) && + IsInRange(m_dynamic_target_usage, base_target_usage * 0.9f, base_target_usage * 1.1f)) + { + m_dynamic_target_usage = base_target_usage; + } + + if (!m_stretch_inactive) + { + if (IsInRange(tempo, 1.0f / INACTIVE_GOOD_FACTOR, INACTIVE_GOOD_FACTOR)) + m_stretch_ok_count++; + else + m_stretch_ok_count = 0; + + if (m_stretch_ok_count >= INACTIVE_MIN_OK_COUNT) + { + LOG_UNDERRUN("=== Stretcher is now inactive."); + m_stretch_inactive = true; + } + } + else if (!IsInRange(tempo, 1.0f / INACTIVE_BAD_FACTOR, INACTIVE_BAD_FACTOR)) + { + LOG_UNDERRUN("~~~ Stretcher is now active @ tempo {}.", tempo); + m_stretch_inactive = false; + m_stretch_ok_count = 0; + } + + if (m_stretch_inactive) + tempo = m_nominal_rate; + + if constexpr (LOG_TIMESTRETCH_STATS) + { + static int iterations = 0; + static u64 last_log_time = 0; + + const u64 now = Common::Timer::GetCurrentValue(); + + if (Common::Timer::ConvertValueToSeconds(now - last_log_time) > 1.0f) + { + DEV_LOG("buffers: {:4} ms ({:3.0f}%), tempo: {}, comp: {:2.3f}, iters: {}, reset:{}", + (ibuffer_usage * 1000u) / m_sample_rate, 100.0f * buffer_usage / base_target_usage, tempo, + m_dynamic_target_usage / base_target_usage, iterations, m_stretch_reset); + + last_log_time = now; + iterations = 0; + } + + iterations++; + } + + m_soundtouch->setTempo(tempo); + + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + m_stretch_reset = 0; +} + +void AudioStream::StretchUnderrun() +{ + // Didn't produce enough frames in time. + m_stretch_reset++; +} + +void AudioStream::StretchOverrun() +{ + // Produced more frames than can fit in the buffer. + m_stretch_reset++; + + // Drop two packets to give the time stretcher a bit more time to slow things down. + const u32 discard = CHUNK_SIZE * 2; + m_rpos.store((m_rpos.load(std::memory_order_acquire) + discard) % m_buffer_size, std::memory_order_release); +} + +void AudioStreamParameters::LoadSave(SettingsWrapper& wrap, const char* section) +{ + wrap.EnumEntry(section, "ExpansionMode", expansion_mode, &AudioStream::ParseExpansionMode, &AudioStream::GetExpansionModeName, DEFAULT_EXPANSION_MODE); + minimal_output_latency = wrap.EntryBitBool(section, "OutputLatencyMinimal", DEFAULT_OUTPUT_LATENCY_MINIMAL); + buffer_ms = static_cast(std::clamp(wrap.EntryBitfield(section, "BufferMS", buffer_ms, DEFAULT_BUFFER_MS), 0, std::numeric_limits::max())); + output_latency_ms = static_cast(std::clamp(wrap.EntryBitfield(section, "OutputLatencyMS", buffer_ms, DEFAULT_OUTPUT_LATENCY_MS), 0, std::numeric_limits::max())); + + stretch_sequence_length_ms = static_cast(std::clamp(wrap.EntryBitfield(section, "StretchSequenceLengthMS", DEFAULT_STRETCH_SEQUENCE_LENGTH), 0, std::numeric_limits::max())); + stretch_seekwindow_ms = static_cast(std::clamp(wrap.EntryBitfield(section, "StretchSeekWindowMS", DEFAULT_STRETCH_SEEKWINDOW), 0, std::numeric_limits::max())); + stretch_overlap_ms = static_cast(std::clamp(wrap.EntryBitfield(section, "StretchOverlapMS", DEFAULT_STRETCH_OVERLAP), 0, std::numeric_limits::max())); + stretch_use_quickseek = wrap.EntryBitBool(section, "StretchUseQuickSeek", DEFAULT_STRETCH_USE_QUICKSEEK); + stretch_use_aa_filter = wrap.EntryBitBool(section, "StretchUseAAFilter", DEFAULT_STRETCH_USE_AA_FILTER); + + expand_block_size = static_cast(std::clamp(wrap.EntryBitfield(section, "ExpandBlockSize", DEFAULT_EXPAND_BLOCK_SIZE), 0, std::numeric_limits::max())); + wrap.Entry(section, "ExpandCircularWrap", expand_circular_wrap, DEFAULT_EXPAND_CIRCULAR_WRAP); + wrap.Entry(section, "ExpandShift", expand_shift, DEFAULT_EXPAND_SHIFT); + wrap.Entry(section, "ExpandDepth", expand_depth, DEFAULT_EXPAND_DEPTH); + wrap.Entry(section, "ExpandFocus", expand_focus, DEFAULT_EXPAND_FOCUS); + wrap.Entry(section, "ExpandCenterImage", expand_center_image, DEFAULT_EXPAND_CENTER_IMAGE); + wrap.Entry(section, "ExpandFrontSeparation", expand_front_separation, DEFAULT_EXPAND_FRONT_SEPARATION); + wrap.Entry(section, "ExpandRearSeparation", expand_rear_separation, DEFAULT_EXPAND_REAR_SEPARATION); + expand_low_cutoff = static_cast(std::clamp(wrap.EntryBitfield(section, "ExpandLowCutoff", DEFAULT_EXPAND_LOW_CUTOFF), 0, std::numeric_limits::max())); + expand_high_cutoff = static_cast(std::clamp(wrap.EntryBitfield(section, "ExpandHighCutoff", DEFAULT_EXPAND_HIGH_CUTOFF), 0, std::numeric_limits::max())); + + // Clamping of values. + if (wrap.IsLoading()) + { + stretch_sequence_length_ms = std::clamp(stretch_sequence_length_ms, 20, 100); + stretch_seekwindow_ms = std::clamp(stretch_seekwindow_ms, 10, 30); + stretch_overlap_ms = std::clamp(stretch_overlap_ms, 5, 15); + + expand_block_size = std::clamp(std::has_single_bit(expand_block_size) ? expand_block_size : std::bit_ceil(expand_block_size), 128, 8192); + expand_circular_wrap = std::clamp(expand_circular_wrap, 0.0f, 360.0f); + expand_shift = std::clamp(expand_shift, -1.0f, 1.0f); + expand_depth = std::clamp(expand_depth, 0.0f, 5.0f); + expand_focus = std::clamp(expand_focus, -1.0f, 1.0f); + expand_center_image = std::clamp(expand_center_image, 0.0f, 1.0f); + expand_front_separation = std::clamp(expand_front_separation, 0.0f, 10.0f); + expand_rear_separation = std::clamp(expand_rear_separation, 0.0f, 10.0f); + expand_low_cutoff = std::min(expand_low_cutoff, 100); + expand_high_cutoff = std::min(expand_high_cutoff, 100); + } +} + +bool AudioStreamParameters::operator!=(const AudioStreamParameters& rhs) const +{ + return (std::memcmp(this, &rhs, sizeof(*this)) != 0); +} + +bool AudioStreamParameters::operator==(const AudioStreamParameters& rhs) const +{ + return (std::memcmp(this, &rhs, sizeof(*this)) == 0); +} diff --git a/pcsx2/Host/AudioStream.h b/pcsx2/Host/AudioStream.h new file mode 100644 index 0000000000..dc1ce598a0 --- /dev/null +++ b/pcsx2/Host/AudioStream.h @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Host/AudioStreamTypes.h" + +#include +#include +#include +#include +#include +#include +#include + +class Error; + +class FreeSurroundDecoder; +namespace soundtouch +{ + class SoundTouch; +} + +class AudioStream +{ +public: + using SampleType = s16; + + static constexpr u32 NUM_INPUT_CHANNELS = 2; + static constexpr u32 MAX_OUTPUT_CHANNELS = 8; + static constexpr u32 CHUNK_SIZE = 64; + static constexpr u32 MIN_EXPANSION_BLOCK_SIZE = 256; + static constexpr u32 MAX_EXPANSION_BLOCK_SIZE = 4096; + + struct DeviceInfo + { + std::string name; + std::string display_name; + u32 minimum_latency_frames; + + DeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_); + ~DeviceInfo(); + }; + +public: + virtual ~AudioStream(); + + static u32 GetAlignedBufferSize(u32 size); + static u32 GetBufferSizeForMS(u32 sample_rate, u32 ms); + static u32 GetMSForBufferSize(u32 sample_rate, u32 buffer_size); + + static std::optional ParseBackendName(const char* str); + static const char* GetBackendName(AudioBackend backend); + static const char* GetBackendDisplayName(AudioBackend backend); + + static const char* GetExpansionModeName(AudioExpansionMode mode); + static const char* GetExpansionModeDisplayName(AudioExpansionMode mode); + static std::optional ParseExpansionMode(const char* name); + + __fi u32 GetSampleRate() const { return m_sample_rate; } + __fi u32 GetInternalChannels() const { return m_internal_channels; } + __fi u32 GetOutputChannels() const { return m_internal_channels; } + __fi u32 GetBufferSize() const { return m_buffer_size; } + __fi u32 GetTargetBufferSize() const { return m_target_buffer_size; } + __fi u32 GetOutputVolume() const { return m_volume; } + __fi float GetNominalTempo() const { return m_nominal_rate; } + __fi AudioExpansionMode GetExpansionMode() const { return m_parameters.expansion_mode; } + __fi bool IsExpansionEnabled() const { return m_parameters.expansion_mode != AudioExpansionMode::Disabled; } + __fi bool IsStretchEnabled() const { return m_stretch_enabled; } + __fi bool IsPaused() const { return m_paused; } + + u32 GetBufferedFramesRelaxed() const; + + /// Temporarily pauses the stream, preventing it from requesting data. + virtual void SetPaused(bool paused); + + void SetOutputVolume(u32 volume); + + void WriteChunk(const SampleType* chunk); + + void BeginWrite(SampleType** buffer_ptr, u32* num_frames); + void WriteFrame(const SampleType* frame); + void EndWrite(u32 num_frames); + void EmptyBuffer(); + + /// Nominal rate is used for both resampling and timestretching, input samples are assumed to be this amount faster + /// than the sample rate. + void SetNominalRate(float tempo); + void UpdateTargetTempo(float tempo); + + void SetStretchEnabled(bool enabled); + + static std::vector> GetDriverNames(AudioBackend backend); + static std::vector GetOutputDevices(AudioBackend backend, const char* driver); + static std::unique_ptr CreateStream(AudioBackend backend, u32 sample_rate, const AudioStreamParameters& parameters, + const char* driver_name, const char* device_name, bool stretch_enabled, Error* error = nullptr); + static std::unique_ptr CreateNullStream(u32 sample_rate, u32 buffer_ms); + +protected: + enum ReadChannel : u8 + { + READ_CHANNEL_FRONT_LEFT, + READ_CHANNEL_FRONT_CENTER, + READ_CHANNEL_FRONT_RIGHT, + READ_CHANNEL_SIDE_LEFT, + READ_CHANNEL_SIDE_RIGHT, + READ_CHANNEL_REAR_LEFT, + READ_CHANNEL_REAR_RIGHT, + READ_CHANNEL_LFE, + READ_CHANNEL_NONE + }; + + using SampleReader = void (*)(SampleType* dest, const SampleType* src, u32 num_frames); + + AudioStream(u32 sample_rate, const AudioStreamParameters& parameters); + void BaseInitialize(SampleReader sample_reader, bool stretch_enabled); + + void ReadFrames(SampleType* samples, u32 num_frames); + + template + static void SampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames); + static void StereoSampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames); + + u32 m_sample_rate = 0; + u32 m_volume = 100; + AudioStreamParameters m_parameters; + u8 m_internal_channels = 0; + u8 m_output_channels = 0; + bool m_stretch_enabled = false; + bool m_stretch_inactive = false; + bool m_filling = false; + bool m_paused = false; + +private: + static constexpr u32 AVERAGING_BUFFER_SIZE = 256; + static constexpr u32 AVERAGING_WINDOW = 50; + static constexpr u32 STRETCH_RESET_THRESHOLD = 5; + static constexpr u32 TARGET_IPS = 691; + + static std::vector> GetCubebDriverNames(); + static std::vector GetCubebOutputDevices(const char* driver); + static std::unique_ptr CreateCubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + const char* driver_name, const char* device_name, bool stretch_enabled, Error* error); + + static std::unique_ptr CreateSDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + bool stretch_enabled, Error* error); + + void AllocateBuffer(); + void DestroyBuffer(); + + void InternalWriteFrames(const SampleType* samples, u32 num_frames); + + void ExpandAllocate(); + + void StretchAllocate(); + void StretchDestroy(); + void StretchWriteBlock(const float* block); + void StretchUnderrun(); + void StretchOverrun(); + + float AddAndGetAverageTempo(float val); + void UpdateStretchTempo(); + + u32 m_buffer_size = 0; + std::unique_ptr m_buffer; + SampleReader m_sample_reader = nullptr; + + std::atomic m_rpos{0}; + std::atomic m_wpos{0}; + + std::unique_ptr m_soundtouch; + + u32 m_target_buffer_size = 0; + u32 m_stretch_reset = STRETCH_RESET_THRESHOLD; + + u32 m_stretch_ok_count = 0; + float m_nominal_rate = 1.0f; + float m_dynamic_target_usage = 0.0f; + + u32 m_average_position = 0; + u32 m_average_available = 0; + u32 m_staging_buffer_pos = 0; + + std::array m_average_fullness = {}; + + // temporary staging buffer, used for timestretching + std::unique_ptr m_staging_buffer; + + // float buffer, soundtouch only accepts float samples as input + std::unique_ptr m_float_buffer; + + std::unique_ptr m_expander; + + // block buffer for expansion + std::unique_ptr m_expand_buffer; + float* m_expand_output_buffer = nullptr; + u32 m_expand_buffer_pos = 0; +}; + +template +void AudioStream::SampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames) +{ + static_assert(READ_CHANNEL_NONE == MAX_OUTPUT_CHANNELS); + static constexpr const std::array, u8>, + static_cast(AudioExpansionMode::Count)> + luts = {{ + // FL FC FR SL SR RL RR LFE + {{0, -1, 1, -1, -1, -1, -1, -1}, 2}, // Disabled + {{0, -1, 1, -1, -1, -1, -1, 2}, 3}, // StereoLFE + {{0, -1, 1, -1, -1, 2, 3, -1}, 5}, // Quadraphonic + {{0, -1, 2, -1, -1, 2, 3, 4}, 5}, // QuadraphonicLFE + {{0, 1, 2, -1, -1, 3, 4, 5}, 6}, // Surround51 + {{0, 1, 2, 3, 4, 5, 6, 7}, 8}, // Surround71 + }}; + constexpr const auto& lut = luts[static_cast(mode)].first; + for (u32 i = 0; i < num_frames; i++) + { + if constexpr (c0 != READ_CHANNEL_NONE) + { + static_assert(lut[c0] >= 0 && lut[c0] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c0]]; + } + if constexpr (c1 != READ_CHANNEL_NONE) + { + static_assert(lut[c1] >= 0 && lut[c1] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c1]]; + } + if constexpr (c2 != READ_CHANNEL_NONE) + { + static_assert(lut[c2] >= 0 && lut[c2] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c2]]; + } + if constexpr (c3 != READ_CHANNEL_NONE) + { + static_assert(lut[c3] >= 0 && lut[c3] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c3]]; + } + if constexpr (c4 != READ_CHANNEL_NONE) + { + static_assert(lut[c4] >= 0 && lut[c4] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c4]]; + } + if constexpr (c5 != READ_CHANNEL_NONE) + { + static_assert(lut[c5] >= 0 && lut[c5] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c5]]; + } + if constexpr (c6 != READ_CHANNEL_NONE) + { + static_assert(lut[c6] >= 0 && lut[c6] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c6]]; + } + if constexpr (c7 != READ_CHANNEL_NONE) + { + static_assert(lut[c7] >= 0 && lut[c7] < MAX_OUTPUT_CHANNELS); + *(dest++) = src[lut[c7]]; + } + + src += luts[static_cast(mode)].second; + } +} diff --git a/pcsx2/Host/AudioStreamTypes.h b/pcsx2/Host/AudioStreamTypes.h new file mode 100644 index 0000000000..73dd92249d --- /dev/null +++ b/pcsx2/Host/AudioStreamTypes.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +class SettingsWrapper; + +enum class AudioBackend : u8 +{ + Null, + Cubeb, + SDL, + Count +}; + +enum class AudioExpansionMode : u8 +{ + Disabled, + StereoLFE, + Quadraphonic, + QuadraphonicLFE, + Surround51, + Surround71, + Count +}; + +struct AudioStreamParameters +{ + AudioExpansionMode expansion_mode = DEFAULT_EXPANSION_MODE; + bool minimal_output_latency = DEFAULT_OUTPUT_LATENCY_MINIMAL; + u16 buffer_ms = DEFAULT_BUFFER_MS; + u16 output_latency_ms = DEFAULT_OUTPUT_LATENCY_MS; + + u16 stretch_sequence_length_ms = DEFAULT_STRETCH_SEQUENCE_LENGTH; + u16 stretch_seekwindow_ms = DEFAULT_STRETCH_SEEKWINDOW; + u16 stretch_overlap_ms = DEFAULT_STRETCH_OVERLAP; + bool stretch_use_quickseek = DEFAULT_STRETCH_USE_QUICKSEEK; + bool stretch_use_aa_filter = DEFAULT_STRETCH_USE_AA_FILTER; + + float expand_circular_wrap = DEFAULT_EXPAND_CIRCULAR_WRAP; + float expand_shift = DEFAULT_EXPAND_SHIFT; + float expand_depth = DEFAULT_EXPAND_DEPTH; + float expand_focus = DEFAULT_EXPAND_FOCUS; + float expand_center_image = DEFAULT_EXPAND_CENTER_IMAGE; + float expand_front_separation = DEFAULT_EXPAND_FRONT_SEPARATION; + float expand_rear_separation = DEFAULT_EXPAND_REAR_SEPARATION; + u16 expand_block_size = DEFAULT_EXPAND_BLOCK_SIZE; + u8 expand_low_cutoff = DEFAULT_EXPAND_LOW_CUTOFF; + u8 expand_high_cutoff = DEFAULT_EXPAND_HIGH_CUTOFF; + + static constexpr AudioExpansionMode DEFAULT_EXPANSION_MODE = AudioExpansionMode::Disabled; + static constexpr u16 DEFAULT_BUFFER_MS = 50; + static constexpr u16 DEFAULT_OUTPUT_LATENCY_MS = 20; + static constexpr bool DEFAULT_OUTPUT_LATENCY_MINIMAL = false; + + static constexpr u16 DEFAULT_EXPAND_BLOCK_SIZE = 2048; + static constexpr float DEFAULT_EXPAND_CIRCULAR_WRAP = 90.0f; + static constexpr float DEFAULT_EXPAND_SHIFT = 0.0f; + static constexpr float DEFAULT_EXPAND_DEPTH = 1.0f; + static constexpr float DEFAULT_EXPAND_FOCUS = 0.0f; + static constexpr float DEFAULT_EXPAND_CENTER_IMAGE = 1.0f; + static constexpr float DEFAULT_EXPAND_FRONT_SEPARATION = 1.0f; + static constexpr float DEFAULT_EXPAND_REAR_SEPARATION = 1.0f; + static constexpr u8 DEFAULT_EXPAND_LOW_CUTOFF = 40; + static constexpr u8 DEFAULT_EXPAND_HIGH_CUTOFF = 90; + + static constexpr u16 DEFAULT_STRETCH_SEQUENCE_LENGTH = 30; + static constexpr u16 DEFAULT_STRETCH_SEEKWINDOW = 20; + static constexpr u16 DEFAULT_STRETCH_OVERLAP = 10; + + static constexpr bool DEFAULT_STRETCH_USE_QUICKSEEK = false; + static constexpr bool DEFAULT_STRETCH_USE_AA_FILTER = false; + + void LoadSave(SettingsWrapper& wrap, const char* section); + + bool operator==(const AudioStreamParameters& rhs) const; + bool operator!=(const AudioStreamParameters& rhs) const; +}; diff --git a/pcsx2/Host/CubebAudioStream.cpp b/pcsx2/Host/CubebAudioStream.cpp new file mode 100644 index 0000000000..7ed9ecf05e --- /dev/null +++ b/pcsx2/Host/CubebAudioStream.cpp @@ -0,0 +1,356 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: GPL-3.0+ + +#include "Host/AudioStream.h" +#include "Host.h" + +#include "common/Assertions.h" +#include "common/Console.h" +#include "common/Error.h" +#include "common/ScopedGuard.h" +#include "common/SmallString.h" +#include "common/StringUtil.h" + +#include "cubeb/cubeb.h" +#include "fmt/format.h" +#include "IconsFontAwesome5.h" + +#ifdef _WIN32 +#include "common/RedtapeWindows.h" +#include +#endif + +namespace +{ + class CubebAudioStream : public AudioStream + { + public: + CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters); + ~CubebAudioStream(); + + void SetPaused(bool paused) override; + + bool Initialize(const char* driver_name, const char* device_name, bool stretch_enabled, Error* error); + + private: + static void LogCallback(const char* fmt, ...); + static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, + long nframes); + static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state); + + void DestroyContextAndStream(); + + cubeb* m_context = nullptr; + cubeb_stream* stream = nullptr; + }; +} // namespace + +static TinyString GetCubebErrorString(int rv) +{ + TinyString ret; + switch (rv) + { + // clang-format off +#define C(e) case e: ret.assign(#e); break + // clang-format on + + C(CUBEB_OK); + C(CUBEB_ERROR); + C(CUBEB_ERROR_INVALID_FORMAT); + C(CUBEB_ERROR_INVALID_PARAMETER); + C(CUBEB_ERROR_NOT_SUPPORTED); + C(CUBEB_ERROR_DEVICE_UNAVAILABLE); + + default: + return "CUBEB_ERROR_UNKNOWN"; + +#undef C + } + + ret.append_format(" ({})", rv); + return ret; +} + +CubebAudioStream::CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters) + : AudioStream(sample_rate, parameters) +{ +} + +CubebAudioStream::~CubebAudioStream() +{ + DestroyContextAndStream(); +} + +void CubebAudioStream::LogCallback(const char* fmt, ...) +{ + SmallString str; + std::va_list ap; + va_start(ap, fmt); + str.vsprintf(fmt, ap); + va_end(ap); + DEV_LOG(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; + } +} + +bool CubebAudioStream::Initialize(const char* driver_name, const char* device_name, bool stretch_enabled, Error* error) +{ + cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback); + + int rv = cubeb_init(&m_context, "PCSX2", (driver_name && *driver_name) ? driver_name : nullptr); + if (rv != CUBEB_OK) + { + Error::SetStringFmt(error, "Could not initialize cubeb context: {}", GetCubebErrorString(rv)); + return false; + } + + static constexpr const std::array, + static_cast(AudioExpansionMode::Count)> + channel_setups = {{ + // Disabled + {CUBEB_LAYOUT_STEREO, StereoSampleReaderImpl}, + // StereoLFE + {CUBEB_LAYOUT_STEREO_LFE, &SampleReaderImpl}, + // Quadraphonic + {CUBEB_LAYOUT_QUAD, &SampleReaderImpl}, + // QuadraphonicLFE + {CUBEB_LAYOUT_QUAD_LFE, + &SampleReaderImpl}, + // Surround51 + {CUBEB_LAYOUT_3F2_LFE_BACK, + &SampleReaderImpl}, + // Surround71 + {CUBEB_LAYOUT_3F4_LFE, + &SampleReaderImpl}, + }}; + + cubeb_stream_params params = {}; + params.format = CUBEB_SAMPLE_S16LE; + params.rate = m_sample_rate; + params.channels = m_output_channels; + params.layout = channel_setups[static_cast(m_parameters.expansion_mode)].first; + params.prefs = CUBEB_STREAM_PREF_NONE; + + u32 latency_frames = GetBufferSizeForMS( + m_sample_rate, (m_parameters.minimal_output_latency) ? m_parameters.buffer_ms : m_parameters.output_latency_ms); + u32 min_latency_frames = 0; + rv = cubeb_get_min_latency(m_context, ¶ms, &min_latency_frames); + if (rv == CUBEB_ERROR_NOT_SUPPORTED) + { + DEV_LOG("Cubeb backend does not support latency queries, using latency of {} ms ({} frames).", + m_parameters.buffer_ms, latency_frames); + } + else + { + if (rv != CUBEB_OK) + { + Error::SetStringFmt(error, "cubeb_get_min_latency() failed: {}", GetCubebErrorString(rv)); + DestroyContextAndStream(); + return false; + } + + const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames); + DEV_LOG("Minimum latency: {} ms ({} audio frames)", minimum_latency_ms, min_latency_frames); + if (m_parameters.minimal_output_latency) + { + // use minimum + latency_frames = min_latency_frames; + } + else if (minimum_latency_ms > m_parameters.output_latency_ms) + { + WARNING_LOG("Minimum latency is above requested latency: {} vs {}, adjusting to compensate.", + min_latency_frames, latency_frames); + latency_frames = min_latency_frames; + } + } + + cubeb_devid selected_device = nullptr; + cubeb_device_collection devices; + bool devices_valid = false; + if (device_name && *device_name) + { + rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); + devices_valid = (rv == CUBEB_OK); + if (rv == CUBEB_OK) + { + for (size_t i = 0; i < devices.count; i++) + { + const cubeb_device_info& di = devices.device[i]; + if (di.device_id && std::strcmp(device_name, di.device_id) == 0) + { + INFO_LOG("Using output device '{}' ({}).", di.device_id, + di.friendly_name ? di.friendly_name : di.device_id); + selected_device = di.devid; + break; + } + } + + if (!selected_device) + { + Host::AddIconOSDMessage("AudioDeviceUnavailable", ICON_FA_VOLUME_UP, + fmt::format("Requested audio output device '{}' not found, using default.", device_name), + Host::OSD_ERROR_DURATION); + } + } + else + { + WARNING_LOG("cubeb_enumerate_devices() returned {}, using default device.", GetCubebErrorString(rv)); + } + } + + BaseInitialize(channel_setups[static_cast(m_parameters.expansion_mode)].second, stretch_enabled); + + char stream_name[32]; + std::snprintf(stream_name, sizeof(stream_name), "%p", this); + + rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, ¶ms, latency_frames, + &CubebAudioStream::DataCallback, StateCallback, this); + + if (devices_valid) + cubeb_device_collection_destroy(m_context, &devices); + + if (rv != CUBEB_OK) + { + Error::SetStringFmt(error, "cubeb_stream_init() failed: {}", GetCubebErrorString(rv)); + DestroyContextAndStream(); + return false; + } + + rv = cubeb_stream_start(stream); + if (rv != CUBEB_OK) + { + Error::SetStringFmt(error, "cubeb_stream_start() failed: {}", GetCubebErrorString(rv)); + DestroyContextAndStream(); + return false; + } + + return true; +} + +void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) +{ + // noop +} + +long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, + long nframes) +{ + static_cast(user_ptr)->ReadFrames(static_cast(output_buffer), static_cast(nframes)); + return nframes; +} + +void CubebAudioStream::SetPaused(bool paused) +{ + if (paused == m_paused || !stream) + return; + + const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream); + if (rv != CUBEB_OK) + { + ERROR_LOG("Could not {} stream: {}", paused ? "pause" : "resume", GetCubebErrorString(rv)); + return; + } + + m_paused = paused; +} + +std::unique_ptr AudioStream::CreateCubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + const char* driver_name, const char* device_name, bool stretch_enabled, Error* error) +{ + std::unique_ptr stream = std::make_unique(sample_rate, parameters); + if (!stream->Initialize(driver_name, device_name, stretch_enabled, error)) + stream.reset(); + return stream; +} + +std::vector> AudioStream::GetCubebDriverNames() +{ + std::vector> names; + names.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default")); + + const char** cubeb_names = cubeb_get_backend_names(); + for (u32 i = 0; cubeb_names[i] != nullptr; i++) + names.emplace_back(cubeb_names[i], cubeb_names[i]); + + return names; +} + +std::vector AudioStream::GetCubebOutputDevices(const char* driver) +{ + std::vector ret; + ret.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"), 0); + + cubeb* context; + int rv = cubeb_init(&context, "PCSX2", (driver && *driver) ? driver : nullptr); + if (rv != CUBEB_OK) + { + ERROR_LOG("cubeb_init() failed: {}", GetCubebErrorString(rv)); + return ret; + } + + ScopedGuard context_cleanup([context]() { cubeb_destroy(context); }); + + cubeb_device_collection devices; + rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); + if (rv != CUBEB_OK) + { + ERROR_LOG("cubeb_enumerate_devices() failed: {}", GetCubebErrorString(rv)); + return ret; + } + + ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); }); + + // we need stream parameters to query latency + cubeb_stream_params params = {}; + params.format = CUBEB_SAMPLE_S16LE; + params.rate = 48000; + params.channels = 2; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + u32 min_latency = 0; + cubeb_get_min_latency(context, ¶ms, &min_latency); + ret[0].minimum_latency_frames = min_latency; + + for (size_t i = 0; i < devices.count; i++) + { + const cubeb_device_info& di = devices.device[i]; + if (!di.device_id) + continue; + + ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, min_latency); + } + + return ret; + + for (size_t i = 0; i < devices.count; i++) + { + const cubeb_device_info& di = devices.device[i]; + if (!di.device_id) + continue; + + ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, 0); + } + + return ret; +} diff --git a/pcsx2/Host/SDLAudioStream.cpp b/pcsx2/Host/SDLAudioStream.cpp new file mode 100644 index 0000000000..24743a171d --- /dev/null +++ b/pcsx2/Host/SDLAudioStream.cpp @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: GPL-3.0+ + +#include "Host/AudioStream.h" + +#include "common/Assertions.h" +#include "common/Console.h" +#include "common/Error.h" + +#include + +namespace +{ + class SDLAudioStream final : public AudioStream + { + public: + SDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters); + ~SDLAudioStream(); + + void SetPaused(bool paused) override; + + bool OpenDevice(bool stretch_enabled, Error* error); + void CloseDevice(); + + protected: + __fi bool IsOpen() const { return (m_device_id != 0); } + + static void AudioCallback(void* userdata, uint8_t* stream, int len); + + u32 m_device_id = 0; + }; +} // namespace + +static bool InitializeSDLAudio(Error* error) +{ + static bool initialized = false; + if (initialized) + return true; + + // May as well keep it alive until the process exits. + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) + { + Error::SetStringFmt(error, "SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError()); + return false; + } + + std::atexit([]() { SDL_QuitSubSystem(SDL_INIT_AUDIO); }); + + initialized = true; + return true; +} + +SDLAudioStream::SDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters) + : AudioStream(sample_rate, parameters) +{ +} + +SDLAudioStream::~SDLAudioStream() +{ + if (IsOpen()) + SDLAudioStream::CloseDevice(); +} + +std::unique_ptr AudioStream::CreateSDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + bool stretch_enabled, Error* error) +{ + if (!InitializeSDLAudio(error)) + return {}; + + std::unique_ptr stream = std::make_unique(sample_rate, parameters); + if (!stream->OpenDevice(stretch_enabled, error)) + stream.reset(); + + return stream; +} + +bool SDLAudioStream::OpenDevice(bool stretch_enabled, Error* error) +{ + pxAssert(!IsOpen()); + + static constexpr const std::array(AudioExpansionMode::Count)> sample_readers = {{ + // Disabled + &StereoSampleReaderImpl, + // StereoLFE + &SampleReaderImpl, + // Quadraphonic + &SampleReaderImpl, + // QuadraphonicLFE + &SampleReaderImpl, + // Surround51 + &SampleReaderImpl, + // Surround71 + &SampleReaderImpl, + }}; + + SDL_AudioSpec spec = {}; + spec.freq = m_sample_rate; + spec.channels = m_output_channels; + spec.format = AUDIO_S16; + spec.samples = static_cast(GetBufferSizeForMS( + m_sample_rate, (m_parameters.minimal_output_latency) ? m_parameters.buffer_ms : m_parameters.output_latency_ms)); + spec.callback = AudioCallback; + spec.userdata = static_cast(this); + + SDL_AudioSpec obtained_spec = {}; + m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, SDL_AUDIO_ALLOW_SAMPLES_CHANGE); + if (m_device_id == 0) + { + Error::SetStringFmt(error, "SDL_OpenAudioDevice() failed: {}", SDL_GetError()); + return false; + } + + DEV_LOG("Requested {} frame buffer, got {} frame buffer", spec.samples, obtained_spec.samples); + + BaseInitialize(sample_readers[static_cast(m_parameters.expansion_mode)], stretch_enabled); + SDL_PauseAudioDevice(m_device_id, 0); + + return true; +} + +void SDLAudioStream::SetPaused(bool paused) +{ + if (m_paused == paused) + return; + + SDL_PauseAudioDevice(m_device_id, paused ? 1 : 0); + m_paused = paused; +} + +void SDLAudioStream::CloseDevice() +{ + SDL_CloseAudioDevice(m_device_id); + m_device_id = 0; +} + +void SDLAudioStream::AudioCallback(void* userdata, uint8_t* stream, int len) +{ + SDLAudioStream* const this_ptr = static_cast(userdata); + const u32 num_frames = len / sizeof(SampleType) / this_ptr->m_output_channels; + + this_ptr->ReadFrames(reinterpret_cast(stream), num_frames); +} diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index d30dc8ec2a..9adb41b85b 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -53,6 +53,7 @@ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\vulkan-headers\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\d3d12memalloc\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\lzma\include + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\freesurround\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\xbyak %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zydis\include;$(SolutionDir)3rdparty\zydis\dependencies\zycore\include Use @@ -198,6 +199,9 @@ + + + @@ -547,6 +551,8 @@ + + @@ -820,6 +826,9 @@ {e960dfdf-1bd3-4c29-b251-d1a0919c9b09} + + {1dd0b31f-37f0-4a36-a521-74133aca4737} + {20b2e9fe-f020-42a0-b324-956f5b06ea68} diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters index 371f98fa46..98dd9d7924 100644 --- a/pcsx2/pcsx2.vcxproj.filters +++ b/pcsx2/pcsx2.vcxproj.filters @@ -277,6 +277,9 @@ {2ef3ebf7-d2a3-4b33-8059-a09b82c085ed} + + {9f0d3bda-76d4-42d3-87e9-ce65db9163ef} + @@ -1410,6 +1413,15 @@ System\Ps2\USB\usb-eyetoy + + Misc\Host + + + Misc\Host + + + Misc\Host + @@ -2330,6 +2342,12 @@ System\Ps2\USB\usb-eyetoy + + Misc\Host + + + Misc\Host +