From e8ecb09890284a3e675663c1815acf292bfb857d Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 16 Apr 2024 16:45:15 +1000 Subject: [PATCH] AudioStream: Add surround expansion via FreeSurround --- src/core/fullscreen_ui.cpp | 23 +- src/core/host.cpp | 31 - src/core/host.h | 6 +- src/core/settings.cpp | 83 +-- src/core/settings.h | 31 +- src/core/spu.cpp | 25 +- src/core/system.cpp | 17 +- src/core/types.h | 19 - src/duckstation-qt/CMakeLists.txt | 2 + .../audioexpansionsettingsdialog.ui | 476 ++++++++++++++ src/duckstation-qt/audiosettingswidget.cpp | 218 ++++++- src/duckstation-qt/audiosettingswidget.h | 6 + src/duckstation-qt/audiosettingswidget.ui | 174 +++--- .../audiostretchsettingsdialog.ui | 204 ++++++ src/duckstation-qt/duckstation-qt.vcxproj | 6 + .../duckstation-qt.vcxproj.filters | 2 + src/duckstation-qt/qthost.cpp | 5 + src/duckstation-qt/qthost.h | 3 + src/duckstation-qt/qtutils.cpp | 13 +- src/duckstation-qt/qtutils.h | 5 + src/util/CMakeLists.txt | 7 +- src/util/audio_stream.cpp | 582 ++++++++++++++---- src/util/audio_stream.h | 288 +++++++-- src/util/cubeb_audio_stream.cpp | 113 +++- src/util/input_manager.cpp | 16 +- src/util/input_manager.h | 5 +- src/util/input_source.h | 5 +- src/util/sdl_audio_stream.cpp | 59 +- src/util/util.props | 5 +- src/util/util.vcxproj | 3 + src/util/xaudio2_audio_stream.cpp | 95 +-- 31 files changed, 1975 insertions(+), 552 deletions(-) create mode 100644 src/duckstation-qt/audioexpansionsettingsdialog.ui create mode 100644 src/duckstation-qt/audiostretchsettingsdialog.ui diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 2aada5f09..e287f68cd 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -3628,14 +3628,12 @@ void FullscreenUI::DrawControllerSettingsPage() MenuHeading(FSUI_CSTR("Input Sources")); -#ifdef ENABLE_SDL2 DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_COG, "Enable SDL Input Source"), FSUI_CSTR("The SDL input source supports most controllers."), "InputSources", "SDL", true, true, false); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WIFI, "SDL DualShock 4 / DualSense Enhanced Mode"), FSUI_CSTR("Provides vibration and LED control support over Bluetooth."), "InputSources", "SDLControllerEnhancedMode", false, bsi->GetBoolValue("InputSources", "SDL", true), false); -#endif #ifdef _WIN32 DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_COG, "Enable XInput Input Source"), FSUI_CSTR("The XInput source provides support for XBox 360/XBox One/XBox Series controllers."), @@ -4800,25 +4798,30 @@ void FullscreenUI::DrawAudioSettingsPage() DrawEnumSetting( bsi, FSUI_CSTR("Audio Backend"), FSUI_CSTR("The audio backend determines how frames produced by the emulator are submitted to the host."), "Audio", - "Backend", Settings::DEFAULT_AUDIO_BACKEND, &Settings::ParseAudioBackend, &Settings::GetAudioBackendName, - &Settings::GetAudioBackendDisplayName, AudioBackend::Count); + "Backend", AudioStream::DEFAULT_BACKEND, &AudioStream::ParseBackendName, &AudioStream::GetBackendName, + &AudioStream::GetBackendDisplayName, AudioBackend::Count); + DrawEnumSetting(bsi, FSUI_CSTR("Expansion Mode"), + FSUI_CSTR("Determines how audio is expanded from stereo to surround for supported games."), "Audio", + "ExpansionMode", AudioStreamParameters::DEFAULT_EXPANSION_MODE, &AudioStream::ParseExpansionMode, + &AudioStream::GetExpansionModeName, &AudioStream::GetExpansionModeDisplayName, + AudioExpansionMode::Count); DrawEnumSetting(bsi, FSUI_CSTR("Stretch Mode"), FSUI_CSTR("Determines quality of audio when not running at 100% speed."), "Audio", "StretchMode", - Settings::DEFAULT_AUDIO_STRETCH_MODE, &AudioStream::ParseStretchMode, + AudioStreamParameters::DEFAULT_STRETCH_MODE, &AudioStream::ParseStretchMode, &AudioStream::GetStretchModeName, &AudioStream::GetStretchModeDisplayName, AudioStretchMode::Count); DrawIntRangeSetting(bsi, FSUI_CSTR("Buffer Size"), FSUI_CSTR("Determines the amount of audio buffered before being pulled by the host API."), - "Audio", "BufferMS", Settings::DEFAULT_AUDIO_BUFFER_MS, 10, 500, "%d ms"); + "Audio", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS, 10, 500, "%d ms"); const u32 output_latency = - GetEffectiveUIntSetting(bsi, "Audio", "OutputLatencyMS", Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS); + GetEffectiveUIntSetting(bsi, "Audio", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); bool output_latency_minimal = (output_latency == 0); if (ToggleButton(FSUI_CSTR("Minimal Output Latency"), FSUI_CSTR("When enabled, the minimum supported output latency will be used for the host API."), &output_latency_minimal)) { bsi->SetUIntValue("Audio", "OutputLatencyMS", - output_latency_minimal ? 0 : Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS); + output_latency_minimal ? 0 : AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); SetSettingsChanged(bsi); } if (!output_latency_minimal) @@ -4827,7 +4830,7 @@ void FullscreenUI::DrawAudioSettingsPage() bsi, FSUI_CSTR("Output Latency"), FSUI_CSTR("Determines how much latency there is between the audio being picked up by the host API, and " "played through speakers."), - "Audio", "OutputLatencyMS", Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS, 1, 500, "%d ms"); + "Audio", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS, 1, 500, "%d ms"); } EndMenuButtons(); @@ -7109,6 +7112,7 @@ TRANSLATE_NOOP("FullscreenUI", "Depth Buffer"); TRANSLATE_NOOP("FullscreenUI", "Desktop Mode"); TRANSLATE_NOOP("FullscreenUI", "Details"); TRANSLATE_NOOP("FullscreenUI", "Details unavailable for game not scanned in game list."); +TRANSLATE_NOOP("FullscreenUI", "Determines how audio is expanded from stereo to surround for supported games."); TRANSLATE_NOOP("FullscreenUI", "Determines how large the on-screen messages and monitor are."); TRANSLATE_NOOP("FullscreenUI", "Determines how much latency there is between the audio being picked up by the host API, and played through speakers."); TRANSLATE_NOOP("FullscreenUI", "Determines how much of the area typically not visible on a consumer TV set to crop/hide."); @@ -7182,6 +7186,7 @@ TRANSLATE_NOOP("FullscreenUI", "Exit And Save State"); TRANSLATE_NOOP("FullscreenUI", "Exit DuckStation"); TRANSLATE_NOOP("FullscreenUI", "Exit Without Saving"); TRANSLATE_NOOP("FullscreenUI", "Exits Big Picture mode, returning to the desktop interface."); +TRANSLATE_NOOP("FullscreenUI", "Expansion Mode"); TRANSLATE_NOOP("FullscreenUI", "Failed to copy text to clipboard."); TRANSLATE_NOOP("FullscreenUI", "Failed to delete save state."); TRANSLATE_NOOP("FullscreenUI", "Failed to delete {}."); diff --git a/src/core/host.cpp b/src/core/host.cpp index 0e5b80fa8..0eca6d88b 100644 --- a/src/core/host.cpp +++ b/src/core/host.cpp @@ -380,34 +380,3 @@ void Host::ReleaseGPUDevice() g_gpu_device.reset(); } -#ifndef __ANDROID__ - -std::unique_ptr Host::CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch) -{ - switch (backend) - { -#ifdef ENABLE_CUBEB - case AudioBackend::Cubeb: - return AudioStream::CreateCubebAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch); -#endif - -#ifdef ENABLE_SDL2 - case AudioBackend::SDL: - return AudioStream::CreateSDLAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch); -#endif - -#ifdef _WIN32 - case AudioBackend::XAudio2: - return AudioStream::CreateXAudio2Stream(sample_rate, channels, buffer_ms, latency_ms, stretch); -#endif - - case AudioBackend::Null: - return AudioStream::CreateNullStream(sample_rate, channels, buffer_ms); - - default: - return nullptr; - } -} - -#endif diff --git a/src/core/host.h b/src/core/host.h index 7b9917112..9f07ae8ab 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -21,6 +21,7 @@ class SettingsInterface; struct WindowInfo; enum class AudioBackend : u8; +enum class AudioExpansionMode : u8; enum class AudioStretchMode : u8; enum class RenderAPI : u32; class AudioStream; @@ -72,11 +73,6 @@ SettingsInterface* GetSettingsInterface(); /// If an input profile is being used, this will be the input layer, otherwise the layered interface. SettingsInterface* GetSettingsInterfaceForBindings(); - - -std::unique_ptr CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch); - /// Debugger feedback. void ReportDebuggerMessage(const std::string_view& message); void ReportFormattedDebuggerMessage(const char* format, ...); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index d75edd672..9dc5fa20e 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -306,16 +306,12 @@ void Settings::Load(SettingsInterface& si) cdrom_seek_speedup = si.GetIntValue("CDROM", "SeekSpeedup", 1); audio_backend = - ParseAudioBackend(si.GetStringValue("Audio", "Backend", GetAudioBackendName(DEFAULT_AUDIO_BACKEND)).c_str()) - .value_or(DEFAULT_AUDIO_BACKEND); + AudioStream::ParseBackendName( + si.GetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioStream::DEFAULT_BACKEND)).c_str()) + .value_or(AudioStream::DEFAULT_BACKEND); audio_driver = si.GetStringValue("Audio", "Driver"); audio_output_device = si.GetStringValue("Audio", "OutputDevice"); - audio_stretch_mode = - AudioStream::ParseStretchMode( - si.GetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(DEFAULT_AUDIO_STRETCH_MODE)).c_str()) - .value_or(DEFAULT_AUDIO_STRETCH_MODE); - audio_output_latency_ms = si.GetUIntValue("Audio", "OutputLatencyMS", DEFAULT_AUDIO_OUTPUT_LATENCY_MS); - audio_buffer_ms = si.GetUIntValue("Audio", "BufferMS", DEFAULT_AUDIO_BUFFER_MS); + audio_stream_parameters.Load(si, "Audio"); audio_output_volume = si.GetUIntValue("Audio", "OutputVolume", 100); audio_fast_forward_volume = si.GetUIntValue("Audio", "FastForwardVolume", 100); @@ -422,6 +418,9 @@ void Settings::Load(SettingsInterface& si) si.GetIntValue("TextureReplacements", "DumpVRAMWriteHeightThreshold", 128); #ifdef __ANDROID__ + // No expansion due to license incompatibility. + audio_expansion_mode = AudioExpansionMode::Disabled; + // Android users are incredibly silly and don't understand that stretch is in the aspect ratio list... if (si.GetBoolValue("Display", "Stretch", false)) display_aspect_ratio = DisplayAspectRatio::MatchWindow; @@ -563,12 +562,10 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const si.SetIntValue("CDROM", "ReadSpeedup", cdrom_read_speedup); si.SetIntValue("CDROM", "SeekSpeedup", cdrom_seek_speedup); - si.SetStringValue("Audio", "Backend", GetAudioBackendName(audio_backend)); + si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(audio_backend)); si.SetStringValue("Audio", "Driver", audio_driver.c_str()); si.SetStringValue("Audio", "OutputDevice", audio_output_device.c_str()); - si.SetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(audio_stretch_mode)); - si.SetUIntValue("Audio", "BufferMS", audio_buffer_ms); - si.SetUIntValue("Audio", "OutputLatencyMS", audio_output_latency_ms); + audio_stream_parameters.Save(si, "Audio"); si.SetUIntValue("Audio", "OutputVolume", audio_output_volume); si.SetUIntValue("Audio", "FastForwardVolume", audio_fast_forward_volume); si.SetBoolValue("Audio", "OutputMuted", audio_output_muted); @@ -1570,64 +1567,9 @@ const char* Settings::GetDisplayScreenshotFormatExtension(DisplayScreenshotForma return s_display_screenshot_format_extensions[static_cast(format)]; } -static constexpr const std::array s_audio_backend_names = { - "Null", -#ifdef ENABLE_CUBEB - "Cubeb", -#endif -#ifdef ENABLE_SDL2 - "SDL", -#endif -#ifdef _WIN32 - "XAudio2", -#endif -#ifdef __ANDROID__ - "AAudio", "OpenSLES", -#endif -}; -static constexpr const std::array s_audio_backend_display_names = { - TRANSLATE_NOOP("AudioBackend", "Null (No Output)"), -#ifdef ENABLE_CUBEB - TRANSLATE_NOOP("AudioBackend", "Cubeb"), -#endif -#ifdef ENABLE_SDL2 - TRANSLATE_NOOP("AudioBackend", "SDL"), -#endif -#ifdef _WIN32 - TRANSLATE_NOOP("AudioBackend", "XAudio2"), -#endif -#ifdef __ANDROID__ - "AAudio", - "OpenSL ES", -#endif -}; - -std::optional Settings::ParseAudioBackend(const char* str) -{ - int index = 0; - for (const char* name : s_audio_backend_names) - { - if (StringUtil::Strcasecmp(name, str) == 0) - return static_cast(index); - - index++; - } - - return std::nullopt; -} - -const char* Settings::GetAudioBackendName(AudioBackend backend) -{ - return s_audio_backend_names[static_cast(backend)]; -} - -const char* Settings::GetAudioBackendDisplayName(AudioBackend backend) -{ - return Host::TranslateToCString("AudioBackend", s_audio_backend_display_names[static_cast(backend)]); -} - static constexpr const std::array s_controller_type_names = { - "None", "DigitalController", "AnalogController", "AnalogJoystick", "GunCon", "PlayStationMouse", "NeGcon", "NeGconRumble"}; + "None", "DigitalController", "AnalogController", "AnalogJoystick", + "GunCon", "PlayStationMouse", "NeGcon", "NeGconRumble"}; static constexpr const std::array s_controller_display_names = { TRANSLATE_NOOP("ControllerType", "None"), TRANSLATE_NOOP("ControllerType", "Digital Controller"), @@ -2031,8 +1973,9 @@ static const char* s_log_filters[] = { "WAVWriter", "WindowInfo", -#ifdef ENABLE_CUBEB +#ifndef __ANDROID__ "CubebAudioStream", + "SDLAudioStream", #endif #ifdef ENABLE_OPENGL diff --git a/src/core/settings.h b/src/core/settings.h index 71837cf53..75b5cc395 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -178,14 +178,12 @@ struct Settings u32 cdrom_read_speedup = 1; u32 cdrom_seek_speedup = 1; - AudioBackend audio_backend = DEFAULT_AUDIO_BACKEND; - AudioStretchMode audio_stretch_mode = DEFAULT_AUDIO_STRETCH_MODE; std::string audio_driver; std::string audio_output_device; - u32 audio_output_latency_ms = DEFAULT_AUDIO_OUTPUT_LATENCY_MS; - u32 audio_buffer_ms = DEFAULT_AUDIO_BUFFER_MS; u32 audio_output_volume = 100; u32 audio_fast_forward_volume = 100; + AudioStreamParameters audio_stream_parameters; + AudioBackend audio_backend = AudioStream::DEFAULT_BACKEND; bool audio_output_muted : 1 = false; bool use_old_mdec_routines : 1 = false; @@ -432,10 +430,6 @@ struct Settings static const char* GetDisplayScreenshotFormatDisplayName(DisplayScreenshotFormat mode); static const char* GetDisplayScreenshotFormatExtension(DisplayScreenshotFormat mode); - static std::optional ParseAudioBackend(const char* str); - static const char* GetAudioBackendName(AudioBackend backend); - static const char* GetAudioBackendDisplayName(AudioBackend backend); - static std::optional ParseControllerTypeName(std::string_view str); static const char* GetControllerTypeName(ControllerType type); static const char* GetControllerTypeDisplayName(ControllerType type); @@ -478,18 +472,6 @@ struct Settings static constexpr CPUFastmemMode DEFAULT_CPU_FASTMEM_MODE = CPUFastmemMode::Disabled; #endif -#if defined(ENABLE_CUBEB) - static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::Cubeb; -#elif defined(_WIN32) - static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::XAudio2; -#elif defined(__ANDROID__) - static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::AAudio; -#elif defined(ENABLE_SDL2) - static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::SDL; -#else - static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::Null; -#endif - static constexpr DisplayDeinterlacingMode DEFAULT_DISPLAY_DEINTERLACING_MODE = DisplayDeinterlacingMode::Adaptive; static constexpr DisplayCropMode DEFAULT_DISPLAY_CROP_MODE = DisplayCropMode::Overscan; static constexpr DisplayAspectRatio DEFAULT_DISPLAY_ASPECT_RATIO = DisplayAspectRatio::Auto; @@ -517,15 +499,6 @@ struct Settings static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO; -#ifndef __ANDROID__ - static constexpr u32 DEFAULT_AUDIO_BUFFER_MS = 50; - static constexpr u32 DEFAULT_AUDIO_OUTPUT_LATENCY_MS = 20; -#else - static constexpr u32 DEFAULT_AUDIO_BUFFER_MS = 100; - static constexpr u32 DEFAULT_AUDIO_OUTPUT_LATENCY_MS = 20; -#endif - static constexpr AudioStretchMode DEFAULT_AUDIO_STRETCH_MODE = AudioStretchMode::TimeStretch; - static constexpr bool DEFAULT_SAVE_STATE_COMPRESSION = true; // Enable console logging by default on Linux platforms. diff --git a/src/core/spu.cpp b/src/core/spu.cpp index b44c9c9e5..8d260dcc9 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -16,6 +16,7 @@ #include "common/bitfield.h" #include "common/bitutils.h" +#include "common/error.h" #include "common/fifo_queue.h" #include "common/log.h" #include "common/path.h" @@ -43,7 +44,6 @@ namespace { enum : u32 { SPU_BASE = 0x1F801C00, - NUM_CHANNELS = 2, NUM_VOICES = 24, NUM_VOICE_REGISTERS = 8, VOICE_ADDRESS_SHIFT = 3, @@ -422,7 +422,7 @@ void SPU::Initialize() &SPU::Execute, nullptr, false); s_transfer_event = TimingEvents::CreateTimingEvent( "SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD, &SPU::ExecuteTransfer, nullptr, false); - s_null_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms); + s_null_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, g_settings.audio_stream_parameters.buffer_ms); CreateOutputStream(); Reset(); @@ -430,19 +430,24 @@ void SPU::Initialize() void SPU::CreateOutputStream() { - Log_InfoFmt("Creating '{}' audio stream, sample rate = {}, channels = {}, buffer = {}, latency = {}, stretching = {}", - Settings::GetAudioBackendName(g_settings.audio_backend), static_cast(SAMPLE_RATE), - static_cast(NUM_CHANNELS), g_settings.audio_buffer_ms, g_settings.audio_output_latency_ms, - AudioStream::GetStretchModeName(g_settings.audio_stretch_mode)); + Log_InfoFmt( + "Creating '{}' audio stream, sample rate = {}, expansion = {}, buffer = {}, latency = {}, stretching = {}", + AudioStream::GetBackendName(g_settings.audio_backend), static_cast(SAMPLE_RATE), + AudioStream::GetExpansionModeName(g_settings.audio_stream_parameters.expansion_mode), + g_settings.audio_stream_parameters.buffer_ms, g_settings.audio_stream_parameters.output_latency_ms, + AudioStream::GetStretchModeName(g_settings.audio_stream_parameters.stretch_mode)); + Error error; s_audio_stream = - Host::CreateAudioStream(g_settings.audio_backend, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms, - g_settings.audio_output_latency_ms, g_settings.audio_stretch_mode); + AudioStream::CreateStream(g_settings.audio_backend, SAMPLE_RATE, g_settings.audio_stream_parameters, &error); if (!s_audio_stream) { - Host::ReportErrorAsync("Error", "Failed to create or configure audio stream, falling back to null output."); + Host::ReportErrorAsync( + "Error", + fmt::format("Failed to create or configure audio stream, falling back to null output. The error was:\n{}", + error.GetDescription())); s_audio_stream.reset(); - s_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms); + s_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, g_settings.audio_stream_parameters.buffer_ms); } s_audio_stream->SetOutputVolume(System::GetAudioOutputVolume()); diff --git a/src/core/system.cpp b/src/core/system.cpp index 177fb6c54..1ea12a38e 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2748,8 +2748,8 @@ void System::UpdateSpeedLimiterState() s_pre_frame_sleep = s_throttler_enabled && g_settings.display_pre_frame_sleep; s_syncing_to_host = false; - if (g_settings.sync_to_host_refresh_rate && (g_settings.audio_stretch_mode != AudioStretchMode::Off) && - s_target_speed == 1.0f && IsValid()) + if (g_settings.sync_to_host_refresh_rate && + (g_settings.audio_stream_parameters.stretch_mode != AudioStretchMode::Off) && s_target_speed == 1.0f && IsValid()) { float host_refresh_rate; if (g_gpu_device->GetHostRefreshRate(&host_refresh_rate)) @@ -2780,7 +2780,8 @@ void System::UpdateSpeedLimiterState() // Adjust nominal rate when resampling, or syncing to host. const bool rate_adjust = - (s_syncing_to_host || g_settings.audio_stretch_mode == AudioStretchMode::Resample) && s_target_speed > 0.0f; + (s_syncing_to_host || g_settings.audio_stream_parameters.stretch_mode == AudioStretchMode::Resample) && + s_target_speed > 0.0f; stream->SetNominalRate(rate_adjust ? s_target_speed : 1.0f); if (old_target_speed < s_target_speed) @@ -3714,17 +3715,15 @@ void System::CheckForSettingsChanges(const Settings& old_settings) { Host::AddIconOSDMessage("AudioBackendSwitch", ICON_FA_HEADPHONES, fmt::format(TRANSLATE_FS("OSDMessage", "Switching to {} audio backend."), - Settings::GetAudioBackendName(g_settings.audio_backend)), + AudioStream::GetBackendDisplayName(g_settings.audio_backend)), Host::OSD_INFO_DURATION); } SPU::RecreateOutputStream(); } - if (g_settings.audio_stretch_mode != old_settings.audio_stretch_mode) - SPU::GetOutputStream()->SetStretchMode(g_settings.audio_stretch_mode); - if (g_settings.audio_buffer_ms != old_settings.audio_buffer_ms || - g_settings.audio_output_latency_ms != old_settings.audio_output_latency_ms || - g_settings.audio_stretch_mode != old_settings.audio_stretch_mode) + if (g_settings.audio_stream_parameters.stretch_mode != old_settings.audio_stream_parameters.stretch_mode) + SPU::GetOutputStream()->SetStretchMode(g_settings.audio_stream_parameters.stretch_mode); + if (g_settings.audio_stream_parameters != old_settings.audio_stream_parameters) { SPU::RecreateOutputStream(); UpdateSpeedLimiterState(); diff --git a/src/core/types.h b/src/core/types.h index 8f51e3e13..ffc31559f 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -185,25 +185,6 @@ enum class DisplayScreenshotFormat : u8 Count }; -enum class AudioBackend : u8 -{ - Null, -#ifdef ENABLE_CUBEB - Cubeb, -#endif -#ifdef ENABLE_SDL2 - SDL, -#endif -#ifdef _WIN32 - XAudio2, -#endif -#ifdef __ANDROID__ - AAudio, - OpenSLES, -#endif - Count -}; - enum class ControllerType { None, diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index bc7cd8a2a..266b5f31b 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -20,9 +20,11 @@ set(SRCS advancedsettingswidget.cpp advancedsettingswidget.h advancedsettingswidget.ui + audioexpansionsettingsdialog.ui audiosettingswidget.cpp audiosettingswidget.h audiosettingswidget.ui + audiostretchsettingsdialog.ui autoupdaterdialog.cpp autoupdaterdialog.h autoupdaterdialog.ui diff --git a/src/duckstation-qt/audioexpansionsettingsdialog.ui b/src/duckstation-qt/audioexpansionsettingsdialog.ui new file mode 100644 index 000000000..45a5c0a28 --- /dev/null +++ b/src/duckstation-qt/audioexpansionsettingsdialog.ui @@ -0,0 +1,476 @@ + + + AudioExpansionSettingsDialog + + + + 0 + 0 + 614 + 371 + + + + Audio Expansion Settings + + + + + + Circular Wrap: + + + + + + + + + 0 + + + 360 + + + 90 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 10 + + + + + + + 30 + + + + + + + + + Shift: + + + + + + + + + -100 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + Depth: + + + + + + + + + 0 + + + 50 + + + 10 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 2 + + + + + + + 10 + + + + + + + + + Focus: + + + + + + + + + -100 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + Center Image: + + + + + + + + + 0 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + Front Separation: + + + + + + + + + 0 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + Rear Separation: + + + + + + + + + 0 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + Low Cutoff: + + + + + + + + + 0 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + High Cutoff: + + + + + + + + + 0 + + + 100 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 5 + + + + + + + 20 + + + + + + + + + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::RestoreDefaults + + + + + + + 10 + + + + + + 32 + 32 + + + + + 32 + 32 + + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Audio Expansion Settings</span><br/>These settings fine-tune the behavior of the FreeSurround-based channel expander.</p></body></html> + + + Qt::TextFormat::RichText + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + true + + + + + + + + + Block Size: + + + + + + + + + 0 + + + 8192 + + + 16 + + + 128 + + + 1024 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 128 + + + + + + + 30 + + + + + + + + + + diff --git a/src/duckstation-qt/audiosettingswidget.cpp b/src/duckstation-qt/audiosettingswidget.cpp index 3468b95fb..8a4478c69 100644 --- a/src/duckstation-qt/audiosettingswidget.cpp +++ b/src/duckstation-qt/audiosettingswidget.cpp @@ -1,9 +1,12 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "audiosettingswidget.h" +#include "qtutils.h" #include "settingswindow.h" #include "settingwidgetbinder.h" +#include "ui_audioexpansionsettingsdialog.h" +#include "ui_audiostretchsettingsdialog.h" #include "core/spu.h" @@ -18,19 +21,41 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent m_ui.setupUi(this); for (u32 i = 0; i < static_cast(AudioBackend::Count); i++) - m_ui.audioBackend->addItem(QString::fromUtf8(Settings::GetAudioBackendDisplayName(static_cast(i)))); + m_ui.audioBackend->addItem(QString::fromUtf8(AudioStream::GetBackendDisplayName(static_cast(i)))); - SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", &Settings::ParseAudioBackend, - &Settings::GetAudioBackendName, Settings::DEFAULT_AUDIO_BACKEND); + for (u32 i = 0; i < static_cast(AudioExpansionMode::Count); i++) + { + m_ui.expansionMode->addItem( + QString::fromUtf8(AudioStream::GetExpansionModeDisplayName(static_cast(i)))); + } + + for (u32 i = 0; i < static_cast(AudioStretchMode::Count); i++) + { + m_ui.stretchMode->addItem( + QString::fromUtf8(AudioStream::GetStretchModeDisplayName(static_cast(i)))); + } + + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", + &AudioStream::ParseBackendName, &AudioStream::GetBackendName, + AudioStream::DEFAULT_BACKEND); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.expansionMode, "Audio", "ExpansionMode", + &AudioStream::ParseExpansionMode, &AudioStream::GetExpansionModeName, + AudioStreamParameters::DEFAULT_EXPANSION_MODE); SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.stretchMode, "Audio", "StretchMode", &AudioStream::ParseStretchMode, &AudioStream::GetStretchModeName, - Settings::DEFAULT_AUDIO_STRETCH_MODE); + AudioStreamParameters::DEFAULT_STRETCH_MODE); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "Audio", "BufferMS", - Settings::DEFAULT_AUDIO_BUFFER_MS); + AudioStreamParameters::DEFAULT_BUFFER_MS); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "Audio", "OutputLatencyMS", - Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS); + AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muteCDAudio, "CDROM", "MuteCDAudio", false); connect(m_ui.audioBackend, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDriverNames); + connect(m_ui.expansionMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onExpansionModeChanged); + connect(m_ui.expansionSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onExpansionSettingsClicked); + connect(m_ui.stretchMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onStretchModeChanged); + connect(m_ui.stretchSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onStretchSettingsClicked); + onExpansionModeChanged(); + onStretchModeChanged(); updateDriverNames(); m_ui.outputLatencyMinimal->setChecked(m_ui.outputLatencyMS->value() == 0); @@ -79,6 +104,9 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent dialog->registerWidgetHelp(m_ui.muteCDAudio, tr("Mute CD Audio"), tr("Unchecked"), tr("Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable " "background music in some games.")); + dialog->registerWidgetHelp(m_ui.expansionMode, tr("Expansion Mode"), tr("Disabled (Stereo)"), + tr("Determines how audio is expanded from stereo to surround for supported games. This " + "includes games that support Dolby Pro Logic/Pro Logic II.")); dialog->registerWidgetHelp( m_ui.stretchMode, tr("Stretch Mode"), tr("Time Stretching"), tr("When running outside of 100% speed, adjusts the tempo on audio instead of dropping frames. Produces " @@ -87,25 +115,46 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent AudioSettingsWidget::~AudioSettingsWidget() = default; +void AudioSettingsWidget::onExpansionModeChanged() +{ + const AudioExpansionMode expansion_mode = + AudioStream::ParseExpansionMode( + m_dialog + ->getEffectiveStringValue("Audio", "ExpansionMode", + AudioStream::GetExpansionModeName(AudioStreamParameters::DEFAULT_EXPANSION_MODE)) + .c_str()) + .value_or(AudioStreamParameters::DEFAULT_EXPANSION_MODE); + m_ui.expansionSettings->setEnabled(expansion_mode != AudioExpansionMode::Disabled); +} + +void AudioSettingsWidget::onStretchModeChanged() +{ + const AudioStretchMode stretch_mode = + AudioStream::ParseStretchMode( + m_dialog + ->getEffectiveStringValue("Audio", "StretchMode", + AudioStream::GetStretchModeName(AudioStreamParameters::DEFAULT_STRETCH_MODE)) + .c_str()) + .value_or(AudioStreamParameters::DEFAULT_STRETCH_MODE); + m_ui.stretchSettings->setEnabled(stretch_mode != AudioStretchMode::Off); +} + void AudioSettingsWidget::updateDriverNames() { const AudioBackend backend = - Settings::ParseAudioBackend( - m_dialog - ->getEffectiveStringValue("Audio", "Backend", Settings::GetAudioBackendName(Settings::DEFAULT_AUDIO_BACKEND)) + AudioStream::ParseBackendName( + m_dialog->getEffectiveStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioStream::DEFAULT_BACKEND)) .c_str()) - .value_or(Settings::DEFAULT_AUDIO_BACKEND); + .value_or(AudioStream::DEFAULT_BACKEND); std::vector names; std::vector> devices; -#ifdef ENABLE_CUBEB if (backend == AudioBackend::Cubeb) { names = AudioStream::GetCubebDriverNames(); devices = AudioStream::GetCubebOutputDevices(m_dialog->getEffectiveStringValue("Audio", "Driver", "").c_str()); } -#endif m_ui.driver->disconnect(); m_ui.driver->clear(); @@ -171,7 +220,7 @@ void AudioSettingsWidget::updateVolumeLabel() void AudioSettingsWidget::onMinimalOutputLatencyChecked(bool new_value) { - const u32 value = new_value ? 0u : Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS; + const u32 value = new_value ? 0u : AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS; m_dialog->setIntSettingValue("Audio", "OutputLatencyMS", value); QSignalBlocker sb(m_ui.outputLatencyMS); m_ui.outputLatencyMS->setValue(value); @@ -211,3 +260,144 @@ void AudioSettingsWidget::onOutputMutedChanged(int new_state) Host::CommitBaseSettingChanges(); g_emu_thread->setAudioOutputMuted(muted); } + +void AudioSettingsWidget::onExpansionSettingsClicked() +{ + QDialog dlg(QtUtils::GetRootWidget(this)); + Ui::AudioExpansionSettingsDialog dlgui; + dlgui.setupUi(&dlg); + dlgui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("volume-up-line")).pixmap(32, 32)); + + SettingsInterface* sif = m_dialog->getSettingsInterface(); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.blockSize, "Audio", "ExpandBlockSize", + AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE, 0); + QtUtils::BindLabelToSlider(dlgui.blockSize, dlgui.blockSizeLabel); + SettingWidgetBinder::BindWidgetToFloatSetting(sif, dlgui.circularWrap, "Audio", "ExpandCircularWrap", + AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP); + QtUtils::BindLabelToSlider(dlgui.circularWrap, dlgui.circularWrapLabel); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.shift, "Audio", "ExpandShift", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_SHIFT); + QtUtils::BindLabelToSlider(dlgui.shift, dlgui.shiftLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.depth, "Audio", "ExpandDepth", 10.0f, + AudioStreamParameters::DEFAULT_EXPAND_DEPTH); + QtUtils::BindLabelToSlider(dlgui.depth, dlgui.depthLabel, 10.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.focus, "Audio", "ExpandFocus", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_FOCUS); + QtUtils::BindLabelToSlider(dlgui.focus, dlgui.focusLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.centerImage, "Audio", "ExpandCenterImage", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE); + QtUtils::BindLabelToSlider(dlgui.centerImage, dlgui.centerImageLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.frontSeparation, "Audio", "ExpandFrontSeparation", + 10.0f, AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION); + QtUtils::BindLabelToSlider(dlgui.frontSeparation, dlgui.frontSeparationLabel, 10.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.rearSeparation, "Audio", "ExpandRearSeparation", 10.0f, + AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION); + QtUtils::BindLabelToSlider(dlgui.rearSeparation, dlgui.rearSeparationLabel, 10.0f); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.lowCutoff, "Audio", "ExpandLowCutoff", + AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF); + QtUtils::BindLabelToSlider(dlgui.lowCutoff, dlgui.lowCutoffLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.highCutoff, "Audio", "ExpandHighCutoff", + AudioStreamParameters::DEFAULT_EXPAND_HIGH_CUTOFF); + QtUtils::BindLabelToSlider(dlgui.highCutoff, dlgui.highCutoffLabel); + + connect(dlgui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, &dlg, &QDialog::accept); + connect(dlgui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this, &dlg]() { + m_dialog->setIntSettingValue("Audio", "ExpandBlockSize", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE)); + + m_dialog->setFloatSettingValue("Audio", "ExpandCircularWrap", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP)); + m_dialog->setFloatSettingValue( + "Audio", "ExpandShift", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_SHIFT)); + m_dialog->setFloatSettingValue( + "Audio", "ExpandDepth", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_DEPTH)); + m_dialog->setFloatSettingValue( + "Audio", "ExpandFocus", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_FOCUS)); + m_dialog->setFloatSettingValue("Audio", "ExpandCenterImage", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE)); + m_dialog->setFloatSettingValue("Audio", "ExpandFrontSeparation", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION)); + m_dialog->setFloatSettingValue("Audio", "ExpandRearSeparation", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION)); + m_dialog->setIntSettingValue("Audio", "ExpandLowCutoff", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF)); + m_dialog->setIntSettingValue("Audio", "ExpandHighCutoff", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_HIGH_CUTOFF)); + + dlg.done(0); + + QMetaObject::invokeMethod(this, &AudioSettingsWidget::onExpansionSettingsClicked, Qt::QueuedConnection); + }); + + dlg.exec(); +} + +void AudioSettingsWidget::onStretchSettingsClicked() +{ + QDialog dlg(QtUtils::GetRootWidget(this)); + Ui::AudioStretchSettingsDialog dlgui; + dlgui.setupUi(&dlg); + dlgui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("volume-up-line")).pixmap(32, 32)); + + SettingsInterface* sif = m_dialog->getSettingsInterface(); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.sequenceLength, "Audio", "StretchSequenceLengthMS", + AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH, 0); + QtUtils::BindLabelToSlider(dlgui.sequenceLength, dlgui.sequenceLengthLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.seekWindowSize, "Audio", "StretchSeekWindowMS", + AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW, 0); + QtUtils::BindLabelToSlider(dlgui.seekWindowSize, dlgui.seekWindowSizeLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.overlap, "Audio", "StretchOverlapMS", + AudioStreamParameters::DEFAULT_STRETCH_OVERLAP, 0); + QtUtils::BindLabelToSlider(dlgui.overlap, dlgui.overlapLabel); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useQuickSeek, "Audio", "StretchUseQuickSeek", + AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useAAFilter, "Audio", "StretchUseAAFilter", + AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER); + + connect(dlgui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, &dlg, &QDialog::accept); + connect(dlgui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this, &dlg]() { + m_dialog->setIntSettingValue("Audio", "StretchSequenceLengthMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH)); + m_dialog->setIntSettingValue("Audio", "StretchSeekWindowMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW)); + m_dialog->setIntSettingValue("Audio", "StretchOverlapMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_OVERLAP)); + m_dialog->setBoolSettingValue("Audio", "StretchUseQuickSeek", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK)); + m_dialog->setBoolSettingValue("Audio", "StretchUseAAFilter", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER)); + + dlg.done(0); + + QMetaObject::invokeMethod(this, &AudioSettingsWidget::onStretchSettingsClicked, Qt::QueuedConnection); + }); + + dlg.exec(); +} diff --git a/src/duckstation-qt/audiosettingswidget.h b/src/duckstation-qt/audiosettingswidget.h index 71ada0564..b6b75a647 100644 --- a/src/duckstation-qt/audiosettingswidget.h +++ b/src/duckstation-qt/audiosettingswidget.h @@ -18,6 +18,9 @@ public: ~AudioSettingsWidget(); private Q_SLOTS: + void onExpansionModeChanged(); + void onStretchModeChanged(); + void updateDriverNames(); void updateLatencyLabel(); void updateVolumeLabel(); @@ -26,6 +29,9 @@ private Q_SLOTS: void onFastForwardVolumeChanged(int new_value); void onOutputMutedChanged(int new_state); + void onExpansionSettingsClicked(); + void onStretchSettingsClicked(); + private: Ui::AudioSettingsWidget m_ui; diff --git a/src/duckstation-qt/audiosettingswidget.ui b/src/duckstation-qt/audiosettingswidget.ui index a37de918c..a5b5316f2 100644 --- a/src/duckstation-qt/audiosettingswidget.ui +++ b/src/duckstation-qt/audiosettingswidget.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 435 + 523 + 478 @@ -29,13 +29,40 @@ Configuration + + + - - + + - Output Latency: + Driver: + + + + + + + Maximum latency: 0 frames (0.00ms) + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Backend: + + + + + + + Stretch Mode: @@ -46,7 +73,51 @@ - + + + + + + 500 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBothSides + + + 20 + + + + + + + Minimal + + + + + + + + + + + + Buffer Size: + + + + + + + Expansion Mode: + + + + 15 @@ -74,95 +145,40 @@ - - + + - Backend: + Output Latency: - - - - Driver: - - - - - - - - - - Stretch Mode: - - - - - + + - - - 500 - - - Qt::Orientation::Horizontal - - - QSlider::TickPosition::TicksBothSides - - - 20 - - + - - - Minimal + + + - - - - - - - Buffer Size: - - - - - - - Maximum latency: 0 frames (0.00ms) - - - Qt::AlignmentFlag::AlignCenter - - - - - + + - - Off (Noisy) - + - - Resampling (Pitch Shift) - + + + + + - - - Time Stretch (Tempo Change, Best Sound) - - - + diff --git a/src/duckstation-qt/audiostretchsettingsdialog.ui b/src/duckstation-qt/audiostretchsettingsdialog.ui new file mode 100644 index 000000000..42d50ffce --- /dev/null +++ b/src/duckstation-qt/audiostretchsettingsdialog.ui @@ -0,0 +1,204 @@ + + + AudioStretchSettingsDialog + + + + 0 + 0 + 501 + 248 + + + + Audio Stretch Settings + + + + + + Sequence Length: + + + + + + + + + 20 + + + 100 + + + 30 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 10 + + + + + + + 30 + + + + + + + + + Seekwindow Size: + + + + + + + + + 10 + + + 30 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 2 + + + + + + + 20 + + + + + + + + + Overlap: + + + + + + + + + 5 + + + 15 + + + 10 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 1 + + + + + + + 10 + + + + + + + + + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::RestoreDefaults + + + + + + + 10 + + + + + + 32 + 32 + + + + + 32 + 32 + + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Audio Stretch Settings</span><br/>These settings fine-tune the behavior of the SoundTouch audio time stretcher when running outside of 100% speed.</p></body></html> + + + Qt::TextFormat::RichText + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + true + + + + + + + + + Use Quickseek + + + + + + + Use Anti-Aliasing Filter + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index fad2ae64d..c6fee9304 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -338,6 +338,12 @@ Document + + Document + + + Document + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index cf4c51343..c69caafd2 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -294,6 +294,8 @@ + + diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 3d640aff0..9e57fa0b5 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -166,6 +166,11 @@ QString QtHost::GetResourcesBasePath() return QString::fromStdString(EmuFolders::Resources); } +INISettingsInterface* QtHost::GetBaseSettingsInterface() +{ + return s_base_settings_interface.get(); +} + QIcon QtHost::GetAppIcon() { return QIcon(QStringLiteral(":/icons/duck.png")); diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 31166f2aa..4f0aee4b6 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -275,6 +275,9 @@ QIcon GetAppIcon(); /// Returns the base path for resources. This may be : prefixed, if we're using embedded resources. QString GetResourcesBasePath(); +/// Returns the base settings interface. Should lock before manipulating. +INISettingsInterface* GetBaseSettingsInterface(); + /// Downloads the specified URL to the provided path. bool DownloadFile(QWidget* parent, const QString& title, std::string url, const char* path); diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index b5106a45c..d51df44a1 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "qtutils.h" @@ -17,9 +17,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -218,6 +220,15 @@ void SetWidgetFontForInheritedSetting(QWidget* widget, bool inherited) } } +void BindLabelToSlider(QSlider* slider, QLabel* label, float range /*= 1.0f*/) +{ + auto update_label = [label, range](int new_value) { + label->setText(QString::number(static_cast(new_value) / range)); + }; + update_label(slider->value()); + QObject::connect(slider, &QSlider::valueChanged, label, std::move(update_label)); +} + void SetWindowResizeable(QWidget* widget, bool resizeable) { if (QMainWindow* window = qobject_cast(widget); window) diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index 5ecbdd48c..291f82ae8 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -21,6 +21,8 @@ class ByteStream; class QComboBox; class QFrame; class QKeyEvent; +class QLabel; +class QSlider; class QTableView; class QTreeView; class QVariant; @@ -96,6 +98,9 @@ QString StringViewToQString(const std::string_view& str); /// Sets a widget to italics if the setting value is inherited. void SetWidgetFontForInheritedSetting(QWidget* widget, bool inherited); +/// Binds a label to a slider's value. +void BindLabelToSlider(QSlider* slider, QLabel* label, float range = 1.0f); + /// Changes whether a window is resizable. void SetWindowResizeable(QWidget* widget, bool resizeable); diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 0bb1aa64b..cadcc4bcd 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -179,12 +179,9 @@ if(NOT ANDROID) sdl_input_source.cpp sdl_input_source.h ) - target_compile_definitions(util PUBLIC - "ENABLE_CUBEB=1" - "ENABLE_SDL2=1" - ) target_link_libraries(util PUBLIC cubeb + freesurround SDL2::SDL2 ) endif() @@ -225,7 +222,7 @@ if(WIN32) xinput_source.h ) target_link_libraries(util PRIVATE d3d12ma) - target_link_libraries(util PRIVATE d3d11.lib d3d12.lib d3dcompiler.lib dxgi.lib winmm.lib Dwmapi.lib winhttp.lib) + target_link_libraries(util PRIVATE d3d11.lib d3d12.lib d3dcompiler.lib dxgi.lib winmm.lib Dwmapi.lib winhttp.lib xaudio2.lib) if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") target_link_libraries(util PRIVATE WinPixEventRuntime::WinPixEventRuntime) diff --git a/src/util/audio_stream.cpp b/src/util/audio_stream.cpp index 1eb8cd4a8..ebf5e2216 100644 --- a/src/util/audio_stream.cpp +++ b/src/util/audio_stream.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "audio_stream.h" @@ -6,22 +6,42 @@ #include "common/align.h" #include "common/assert.h" +#include "common/error.h" #include "common/intrin.h" #include "common/log.h" +#include "common/settings_interface.h" +#include "common/small_string.h" #include "common/timer.h" #include "SoundTouch.h" +#ifndef __ANDROID__ +#include "freesurround_decoder.h" +#endif + #include #include #include +#include Log_SetChannel(AudioStream); static constexpr bool LOG_TIMESTRETCH_STATS = false; -AudioStream::AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) - : m_sample_rate(sample_rate), m_channels(channels), m_buffer_ms(buffer_ms), m_stretch_mode(stretch) +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::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) { } @@ -30,13 +50,48 @@ AudioStream::~AudioStream() DestroyBuffer(); } -std::unique_ptr AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms) +std::unique_ptr AudioStream::CreateNullStream(u32 sample_rate, u32 buffer_ms) { - std::unique_ptr stream(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off)); - stream->BaseInitialize(); + // no point stretching with no output + AudioStreamParameters params; + params.expansion_mode = AudioExpansionMode::Disabled; + params.stretch_mode = AudioStretchMode::Off; + params.buffer_ms = static_cast(buffer_ms); + + std::unique_ptr stream(new AudioStream(sample_rate, params)); + stream->BaseInitialize(&StereoSampleReaderImpl); return stream; } +#ifndef __ANDROID__ + +std::unique_ptr AudioStream::CreateStream(AudioBackend backend, u32 sample_rate, + const AudioStreamParameters& parameters, Error* error) +{ + switch (backend) + { + case AudioBackend::Cubeb: + return CreateCubebAudioStream(sample_rate, parameters, error); + + case AudioBackend::SDL: + return CreateSDLAudioStream(sample_rate, parameters, error); + +#ifdef _WIN32 + case AudioBackend::XAudio2: + return CreateXAudio2Stream(sample_rate, parameters, error); +#endif + + case AudioBackend::Null: + return CreateNullStream(sample_rate, parameters.buffer_ms); + + default: + Error::SetStringView(error, "Unknown audio backend."); + return nullptr; + } +} + +#endif + u32 AudioStream::GetAlignedBufferSize(u32 size) { static_assert(Common::IsPow2(CHUNK_SIZE)); @@ -54,10 +109,99 @@ u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size) return (buffer_size * 1000u) / sample_rate; } -static constexpr const std::array s_stretch_mode_names = {"None", "Resample", "TimeStretch"}; -static constexpr const std::array s_stretch_mode_display_names = {TRANSLATE_NOOP("AudioStream", "None"), - TRANSLATE_NOOP("AudioStream", "Resampling"), - TRANSLATE_NOOP("AudioStream", "Time Stretching")}; +static constexpr const std::array s_backend_names = { + "Null", +#ifndef __ANDROID__ + "Cubeb", + "SDL", +#else + "AAudio", + "OpenSLES", +#endif +#ifdef _WIN32 + "XAudio2", +#endif +}; +static constexpr const std::array s_backend_display_names = { + TRANSLATE_NOOP("AudioStream", "Null (No Output)"), +#ifndef __ANDROID__ + TRANSLATE_NOOP("AudioStream", "Cubeb"), + TRANSLATE_NOOP("AudioStream", "SDL"), +#else + "AAudio", + "OpenSL ES", +#endif +#ifdef _WIN32 + TRANSLATE_NOOP("AudioStream", "XAudio2"), +#endif +}; + +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; +} + +static constexpr const std::array s_stretch_mode_names = { + "None", + "Resample", + "TimeStretch", +}; +static constexpr const std::array s_stretch_mode_display_names = { + TRANSLATE_NOOP("AudioStream", "Off (Noisy)"), + TRANSLATE_NOOP("AudioStream", "Resampling (Pitch Shift)"), + TRANSLATE_NOOP("AudioStream", "Time Stretch (Tempo Change, Best Sound)"), +}; const char* AudioStream::GetStretchModeName(AudioStretchMode mode) { @@ -89,7 +233,7 @@ u32 AudioStream::GetBufferedFramesRelaxed() const return (wpos + m_buffer_size - rpos) % m_buffer_size; } -void AudioStream::ReadFrames(s16* samples, u32 num_frames) +void AudioStream::ReadFrames(SampleType* samples, u32 num_frames) { const u32 available_frames = GetBufferedFramesRelaxed(); u32 frames_to_read = num_frames; @@ -97,7 +241,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) if (m_filling) { - u32 toFill = m_buffer_size / ((m_stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400); + u32 toFill = m_buffer_size / ((m_parameters.stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400); toFill = GetAlignedBufferSize(toFill); if (available_frames < toFill) @@ -118,7 +262,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) frames_to_read = available_frames; m_filling = true; - if (m_stretch_mode == AudioStretchMode::TimeStretch) + if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) StretchUnderrun(); } @@ -133,7 +277,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) // towards the end of the buffer if (end > 0) { - std::memcpy(samples, &m_buffer[rpos], sizeof(s32) * end); + m_sample_reader(samples, &m_buffer[rpos * m_internal_channels], end); rpos += end; rpos = (rpos == m_buffer_size) ? 0 : rpos; } @@ -142,7 +286,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) const u32 start = frames_to_read - end; if (start > 0) { - std::memcpy(&samples[end * 2], &m_buffer[0], sizeof(s32) * start); + m_sample_reader(&samples[end * m_output_channels], &m_buffer[0], start); rpos = start; } @@ -158,19 +302,20 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) 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_channels * sizeof(SampleType))); - std::memcpy(resample_ptr, samples, frames_to_read * m_channels * sizeof(SampleType)); + 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_channels; + 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_channels; + out_ptr += m_output_channels; resample_subpos += increment; - resample_ptr += (resample_subpos >> 16) * m_channels; + resample_ptr += (resample_subpos >> 16) * m_output_channels; resample_subpos %= 65536u; } @@ -179,19 +324,23 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames) else { // no data, fall back to silence - std::memset(samples + (frames_to_read * m_channels), 0, sizeof(s16) * m_channels * silence_frames); + std::memset(samples + (frames_to_read * m_output_channels), 0, silence_frames * m_output_channels * sizeof(s16)); } } } -void AudioStream::ApplyVolume(s16* samples, u32 num_frames) +void AudioStream::StereoSampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames) +{ + std::memcpy(dest, src, num_frames * 2 * sizeof(SampleType)); +} + +void AudioStream::ApplyVolume(s16* samples, u32 num_samples) { if (m_volume == 100) return; const s32 volume_mult = static_cast((static_cast(m_volume) / 100.0f) * 32768.0f); - u32 num_samples = num_frames * m_channels; while (num_samples > 0) { *samples = static_cast((static_cast(*samples) * volume_mult) >> 15); @@ -200,12 +349,12 @@ void AudioStream::ApplyVolume(s16* samples, u32 num_frames) } } -void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples) +void AudioStream::InternalWriteFrames(s16* data, u32 num_frames) { const u32 free = m_buffer_size - GetBufferedFramesRelaxed(); - if (free <= nSamples) + if (free <= num_frames) { - if (m_stretch_mode == AudioStretchMode::TimeStretch) + if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) { StretchOverrun(); } @@ -219,49 +368,66 @@ void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples) u32 wpos = m_wpos.load(std::memory_order_acquire); // wrapping around the end of the buffer? - if ((m_buffer_size - wpos) <= nSamples) + if ((m_buffer_size - wpos) <= num_frames) { // needs to be written in two parts const u32 end = m_buffer_size - wpos; - const u32 start = nSamples - end; + const u32 start = num_frames - end; // start is zero when this chunk reaches exactly the end - std::memcpy(&m_buffer[wpos], bData, end * sizeof(s32)); + std::memcpy(&m_buffer[wpos * m_internal_channels], data, end * m_internal_channels * sizeof(SampleType)); if (start > 0) - std::memcpy(&m_buffer[0], bData + end, start * sizeof(s32)); + 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], bData, nSamples * sizeof(s32)); - wpos += nSamples; + 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() +void AudioStream::BaseInitialize(SampleReader sample_reader) { + 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 multplier = - (m_stretch_mode == AudioStretchMode::TimeStretch) ? 16 : ((m_stretch_mode == AudioStretchMode::Off) ? 1 : 2); - m_buffer_size = GetAlignedBufferSize(((m_buffer_ms * multplier) * m_sample_rate) / 1000); - m_target_buffer_size = GetAlignedBufferSize((m_sample_rate * m_buffer_ms) / 1000u); - m_buffer = std::unique_ptr(new s32[m_buffer_size]); - Log_DevPrintf("Allocated buffer of %u frames for buffer of %u ms [stretch %s, target size %u].", m_buffer_size, - m_buffer_ms, GetStretchModeName(m_stretch_mode), m_target_buffer_size); + // TODO: do we really? it's more the output... + const u32 multiplier = (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) ? + 16 : + ((m_parameters.stretch_mode == AudioStretchMode::Off) ? 1 : 2); + 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); + + Log_DevFmt( + "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, GetStretchModeName(m_parameters.stretch_mode), 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); @@ -270,10 +436,19 @@ void AudioStream::DestroyBuffer() void AudioStream::EmptyBuffer() { - if (m_stretch_mode != AudioStretchMode::Off) +#ifndef __ANDROID__ + if (IsExpansionEnabled()) + { + m_expander->Flush(); + m_expand_output_buffer = nullptr; + m_expand_buffer_pos = 0; + } +#endif + + if (IsStretchEnabled()) { m_soundtouch->clear(); - if (m_stretch_mode == AudioStretchMode::TimeStretch) + if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) m_soundtouch->setTempo(m_nominal_rate); } @@ -283,13 +458,13 @@ void AudioStream::EmptyBuffer() void AudioStream::SetNominalRate(float tempo) { m_nominal_rate = tempo; - if (m_stretch_mode == AudioStretchMode::Resample) + if (m_parameters.stretch_mode == AudioStretchMode::Resample) m_soundtouch->setRate(tempo); } void AudioStream::UpdateTargetTempo(float tempo) { - if (m_stretch_mode != AudioStretchMode::TimeStretch) + if (m_parameters.stretch_mode != AudioStretchMode::TimeStretch) return; // undo sqrt() @@ -308,7 +483,7 @@ void AudioStream::UpdateTargetTempo(float tempo) void AudioStream::SetStretchMode(AudioStretchMode mode) { - if (m_stretch_mode == mode) + if (m_parameters.stretch_mode == mode) return; // can't resize the buffers while paused @@ -318,10 +493,10 @@ void AudioStream::SetStretchMode(AudioStretchMode mode) DestroyBuffer(); StretchDestroy(); - m_stretch_mode = mode; + m_parameters.stretch_mode = mode; AllocateBuffer(); - if (m_stretch_mode != AudioStretchMode::Off) + if (m_parameters.stretch_mode != AudioStretchMode::Off) StretchAllocate(); if (!paused) @@ -341,8 +516,8 @@ void AudioStream::SetOutputVolume(u32 volume) void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames) { // TODO: Write directly to buffer when not using stretching. - *buffer_ptr = reinterpret_cast(&m_staging_buffer[m_staging_buffer_pos]); - *num_frames = CHUNK_SIZE - m_staging_buffer_pos; + *buffer_ptr = &m_staging_buffer[m_staging_buffer_pos]; + *num_frames = CHUNK_SIZE - (m_staging_buffer_pos / NUM_INPUT_CHANNELS); } void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames) @@ -350,41 +525,20 @@ void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames) Panic("not implemented"); } -void AudioStream::EndWrite(u32 num_frames) -{ - // don't bother committing anything when muted - if (m_volume == 0) - return; - - m_staging_buffer_pos += num_frames; - DebugAssert(m_staging_buffer_pos <= CHUNK_SIZE); - if (m_staging_buffer_pos < CHUNK_SIZE) - return; - - m_staging_buffer_pos = 0; - - if (m_stretch_mode != AudioStretchMode::Off) - StretchWrite(); - else - InternalWriteFrames(m_staging_buffer.data(), CHUNK_SIZE); -} - static constexpr float S16_TO_FLOAT = 1.0f / 32767.0f; static constexpr float FLOAT_TO_S16 = 32767.0f; #if defined(CPU_ARCH_NEON) -static void S16ChunkToFloat(const s32* src, float* dst) +static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples) { - static_assert((AudioStream::CHUNK_SIZE % 4) == 0); - constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; - const float32x4_t S16_TO_FLOAT_V = vdupq_n_f32(S16_TO_FLOAT); + const u32 iterations = (num_samples + 7) / 8; for (u32 i = 0; i < iterations; i++) { const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src)); - src += 4; + src += 8; int32x4_t iv1 = vreinterpretq_s32_s16(vzip1q_s16(sv, sv)); // [0, 0, 1, 1, 2, 2, 3, 3] int32x4_t iv2 = vreinterpretq_s32_s16(vzip2q_s16(sv, sv)); // [4, 4, 5, 5, 6, 6, 7, 7] @@ -401,13 +555,11 @@ static void S16ChunkToFloat(const s32* src, float* dst) } } -static void FloatChunkToS16(s32* dst, const float* src, uint size) +static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples) { static_assert((AudioStream::CHUNK_SIZE % 4) == 0); - constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; - - const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16); + const u32 iterations = (num_samples + 7) / 8; for (u32 i = 0; i < iterations; i++) { float32x4_t fv1 = vld1q_f32(src + 0); @@ -421,23 +573,21 @@ static void FloatChunkToS16(s32* dst, const float* src, uint size) int16x8_t iv = vcombine_s16(vqmovn_s32(iv1), vqmovn_s32(iv2)); vst1q_s32(dst, vreinterpretq_s32_s16(iv)); - dst += 4; + dst += 8; } } #elif defined(CPU_ARCH_SSE) -static void S16ChunkToFloat(const s32* src, float* dst) +static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples) { - static_assert((AudioStream::CHUNK_SIZE % 4) == 0); - constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; - const __m128 S16_TO_FLOAT_V = _mm_set1_ps(S16_TO_FLOAT); + const u32 iterations = (num_samples + 7) / 8; for (u32 i = 0; i < iterations; i++) { const __m128i sv = _mm_load_si128(reinterpret_cast(src)); - src += 4; + src += 8; __m128i iv1 = _mm_unpacklo_epi16(sv, sv); // [0, 0, 1, 1, 2, 2, 3, 3] __m128i iv2 = _mm_unpackhi_epi16(sv, sv); // [4, 4, 5, 5, 6, 6, 7, 7] @@ -454,13 +604,11 @@ static void S16ChunkToFloat(const s32* src, float* dst) } } -static void FloatChunkToS16(s32* dst, const float* src, uint size) +static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples) { - static_assert((AudioStream::CHUNK_SIZE % 4) == 0); - constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; - const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16); + const u32 iterations = (num_samples + 7) / 8; for (u32 i = 0; i < iterations; i++) { __m128 fv1 = _mm_load_ps(src + 0); @@ -474,33 +622,107 @@ static void FloatChunkToS16(s32* dst, const float* src, uint size) __m128i iv = _mm_packs_epi32(iv1, iv2); _mm_store_si128(reinterpret_cast<__m128i*>(dst), iv); - dst += 4; + dst += 8; } } #else -static void S16ChunkToFloat(const s32* src, float* dst) +static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples) { - for (uint i = 0; i < AudioStream::CHUNK_SIZE; ++i) - { - *(dst++) = static_cast(static_cast((u32)*src)) / 32767.0f; - *(dst++) = static_cast(static_cast(((u32)*src) >> 16)) / 32767.0f; - src++; - } + for (u32 i = 0; i < num_samples; ++i) + *(dst++) = static_cast(*(src++)) / 32767.0f; } -static void FloatChunkToS16(s32* dst, const float* src, uint size) +static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples) { - for (uint i = 0; i < size; ++i) - { - const s16 left = static_cast((*(src++) * 32767.0f)); - const s16 right = static_cast((*(src++) * 32767.0f)); - *(dst++) = (static_cast(left) & 0xFFFFu) | (static_cast(right) << 16); - } + for (u32 i = 0; i < num_samples; ++i) + *(dst++) = static_cast((*(src++) * 32767.0f)); } #endif +void AudioStream::ExpandAllocate() +{ + DebugAssert(!m_expander); + if (m_parameters.expansion_mode == AudioExpansionMode::Disabled) + return; + +#ifndef __ANDROID__ + 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); +#else + Panic("Attempting to use expansion on Android."); +#endif +} + +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; + DebugAssert(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; + + if (!IsExpansionEnabled() && !IsStretchEnabled()) + { + InternalWriteFrames(m_staging_buffer.get(), CHUNK_SIZE); + return; + } + +#ifndef __ANDROID__ + if (IsExpansionEnabled()) + { + // StretchWriteBlock() overwrites the staging buffer on output, so we need to copy into the expand buffer first. + S16ChunkToFloat(m_staging_buffer.get(), 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 +#endif + { + S16ChunkToFloat(m_staging_buffer.get(), m_float_buffer.get(), CHUNK_SIZE * NUM_INPUT_CHANNELS); + StretchWriteBlock(m_float_buffer.get()); + } +} + // Time stretching algorithm based on PCSX2 implementation. template @@ -511,21 +733,21 @@ ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max) void AudioStream::StretchAllocate() { - if (m_stretch_mode == AudioStretchMode::Off) + if (m_parameters.stretch_mode == AudioStretchMode::Off) return; m_soundtouch = std::make_unique(); m_soundtouch->setSampleRate(m_sample_rate); - m_soundtouch->setChannels(m_channels); + m_soundtouch->setChannels(m_internal_channels); - m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, 0); - m_soundtouch->setSetting(SETTING_USE_AA_FILTER, 0); + 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, 30); - m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, 20); - m_soundtouch->setSetting(SETTING_OVERLAP_MS, 10); + 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); - if (m_stretch_mode == AudioStretchMode::Resample) + if (m_parameters.stretch_mode == AudioStretchMode::Resample) m_soundtouch->setRate(m_nominal_rate); else m_soundtouch->setTempo(m_nominal_rate); @@ -545,21 +767,27 @@ void AudioStream::StretchDestroy() m_soundtouch.reset(); } -void AudioStream::StretchWrite() +void AudioStream::StretchWriteBlock(const float* block) { - S16ChunkToFloat(m_staging_buffer.data(), m_float_buffer.data()); - - m_soundtouch->putSamples(m_float_buffer.data(), CHUNK_SIZE); - - int tempProgress; - while (tempProgress = m_soundtouch->receiveSamples((float*)m_float_buffer.data(), CHUNK_SIZE), tempProgress != 0) + if (IsStretchEnabled()) { - FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress); - InternalWriteFrames(m_staging_buffer.data(), tempProgress); - } + m_soundtouch->putSamples(block, CHUNK_SIZE); - if (m_stretch_mode == AudioStretchMode::TimeStretch) - UpdateStretchTempo(); + 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 (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) + 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) @@ -691,3 +919,113 @@ void AudioStream::StretchOverrun() 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::Load(SettingsInterface& si, const char* section) +{ + stretch_mode = + AudioStream::ParseStretchMode( + si.GetStringValue(section, "StretchMode", AudioStream::GetStretchModeName(DEFAULT_STRETCH_MODE)).c_str()) + .value_or(DEFAULT_STRETCH_MODE); +#ifndef __ANDROID__ + expansion_mode = + AudioStream::ParseExpansionMode( + si.GetStringValue(section, "ExpansionMode", AudioStream::GetExpansionModeName(DEFAULT_EXPANSION_MODE)).c_str()) + .value_or(DEFAULT_EXPANSION_MODE); +#else + expansion_mode = AudioExpansionMode::Disabled; +#endif + output_latency_ms = static_cast(std::min( + si.GetUIntValue(section, "OutputLatencyMS", DEFAULT_OUTPUT_LATENCY_MS), std::numeric_limits::max())); + buffer_ms = static_cast( + std::min(si.GetUIntValue(section, "BufferMS", DEFAULT_BUFFER_MS), std::numeric_limits::max())); + + stretch_sequence_length_ms = + static_cast(std::min(si.GetUIntValue(section, "StretchSequenceLengthMS", DEFAULT_STRETCH_SEQUENCE_LENGTH), + std::numeric_limits::max())); + stretch_seekwindow_ms = static_cast(std::min( + si.GetUIntValue(section, "StretchSeekWindowMS", DEFAULT_STRETCH_SEEKWINDOW), std::numeric_limits::max())); + stretch_overlap_ms = static_cast(std::min( + si.GetUIntValue(section, "StretchOverlapMS", DEFAULT_STRETCH_OVERLAP), std::numeric_limits::max())); + stretch_use_quickseek = si.GetBoolValue(section, "StretchUseQuickSeek", DEFAULT_STRETCH_USE_QUICKSEEK); + stretch_use_aa_filter = si.GetBoolValue(section, "StretchUseAAFilter", DEFAULT_STRETCH_USE_AA_FILTER); + + expand_block_size = static_cast(std::min( + si.GetUIntValue(section, "ExpandBlockSize", DEFAULT_EXPAND_BLOCK_SIZE), std::numeric_limits::max())); + expand_block_size = std::clamp( + Common::IsPow2(expand_block_size) ? expand_block_size : Common::NextPow2(expand_block_size), 128, 8192); + expand_circular_wrap = + std::clamp(si.GetFloatValue(section, "ExpandCircularWrap", DEFAULT_EXPAND_CIRCULAR_WRAP), 0.0f, 360.0f); + expand_shift = std::clamp(si.GetFloatValue(section, "ExpandShift", DEFAULT_EXPAND_SHIFT), -1.0f, 1.0f); + expand_depth = std::clamp(si.GetFloatValue(section, "ExpandDepth", DEFAULT_EXPAND_DEPTH), 0.0f, 5.0f); + expand_focus = std::clamp(si.GetFloatValue(section, "ExpandFocus", DEFAULT_EXPAND_FOCUS), -1.0f, 1.0f); + expand_center_image = + std::clamp(si.GetFloatValue(section, "ExpandCenterImage", DEFAULT_EXPAND_CENTER_IMAGE), 0.0f, 1.0f); + expand_front_separation = + std::clamp(si.GetFloatValue(section, "ExpandFrontSeparation", DEFAULT_EXPAND_FRONT_SEPARATION), 0.0f, 10.0f); + expand_rear_separation = + std::clamp(si.GetFloatValue(section, "ExpandRearSeparation", DEFAULT_EXPAND_REAR_SEPARATION), 0.0f, 10.0f); + expand_low_cutoff = + static_cast(std::min(si.GetUIntValue(section, "ExpandLowCutoff", DEFAULT_EXPAND_LOW_CUTOFF), 100)); + expand_high_cutoff = + static_cast(std::min(si.GetUIntValue(section, "ExpandHighCutoff", DEFAULT_EXPAND_HIGH_CUTOFF), 100)); +} + +void AudioStreamParameters::Save(SettingsInterface& si, const char* section) const +{ + si.SetStringValue(section, "StretchMode", AudioStream::GetStretchModeName(stretch_mode)); + si.SetStringValue(section, "ExpansionMode", AudioStream::GetExpansionModeName(expansion_mode)); + si.SetUIntValue(section, "BufferMS", buffer_ms); + si.SetUIntValue(section, "OutputLatencyMS", output_latency_ms); + + si.SetUIntValue(section, "StretchSequenceLengthMS", stretch_sequence_length_ms); + si.SetUIntValue(section, "StretchSeekWindowMS", stretch_seekwindow_ms); + si.SetUIntValue(section, "StretchOverlapMS", stretch_overlap_ms); + si.SetBoolValue(section, "StretchUseQuickSeek", stretch_use_quickseek); + si.SetBoolValue(section, "StretchUseAAFilter", stretch_use_aa_filter); + + si.SetUIntValue(section, "ExpandBlockSize", expand_block_size); + si.SetFloatValue(section, "ExpandCircularWrap", expand_circular_wrap); + si.SetFloatValue(section, "ExpandShift", expand_shift); + si.SetFloatValue(section, "ExpandDepth", expand_depth); + si.SetFloatValue(section, "ExpandFocus", expand_focus); + si.SetFloatValue(section, "ExpandCenterImage", expand_center_image); + si.SetFloatValue(section, "ExpandFrontSeparation", expand_front_separation); + si.SetFloatValue(section, "ExpandRearSeparation", expand_rear_separation); + si.SetUIntValue(section, "ExpandLowCutoff", expand_low_cutoff); + si.SetUIntValue(section, "ExpandHighCutoff", expand_high_cutoff); +} + +void AudioStreamParameters::Clear(SettingsInterface& si, const char* section) +{ + si.DeleteValue(section, "StretchMode"); + si.DeleteValue(section, "ExpansionMode"); + si.DeleteValue(section, "BufferMS"); + si.DeleteValue(section, "OutputLatencyMS"); + + si.DeleteValue(section, "StretchSequenceLengthMS"); + si.DeleteValue(section, "StretchSeekWindowMS"); + si.DeleteValue(section, "StretchOverlapMS"); + si.DeleteValue(section, "StretchUseQuickSeek"); + si.DeleteValue(section, "StretchUseAAFilter"); + + si.DeleteValue(section, "ExpandBlockSize"); + si.DeleteValue(section, "ExpandCircularWrap"); + si.DeleteValue(section, "ExpandShift"); + si.DeleteValue(section, "ExpandDepth"); + si.DeleteValue(section, "ExpandFocus"); + si.DeleteValue(section, "ExpandCenterImage"); + si.DeleteValue(section, "ExpandFrontSeparation"); + si.DeleteValue(section, "ExpandRearSeparation"); + si.DeleteValue(section, "ExpandLowCutoff"); + si.DeleteValue(section, "ExpandHighCutoff"); +} + +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/src/util/audio_stream.h b/src/util/audio_stream.h index 76f2b7b7e..aee948b72 100644 --- a/src/util/audio_stream.h +++ b/src/util/audio_stream.h @@ -1,8 +1,10 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once + #include "common/types.h" + #include #include #include @@ -15,10 +17,30 @@ #pragma warning(disable : 4324) // warning C4324: structure was padded due to alignment specifier #endif +class Error; +class SettingsInterface; + +class FreeSurroundDecoder; namespace soundtouch { class SoundTouch; } +enum class AudioBackend : u8 +{ + Null, +#ifndef __ANDROID__ + Cubeb, + SDL, +#else + AAudio, + OpenSLES, +#endif +#ifdef _WIN32 + XAudio2, +#endif + Count +}; + enum class AudioStretchMode : u8 { Off, @@ -27,16 +49,92 @@ enum class AudioStretchMode : u8 Count }; +enum class AudioExpansionMode : u8 +{ + Disabled, + StereoLFE, + Quadraphonic, + QuadraphonicLFE, + Surround51, + Surround71, + Count +}; + +struct AudioStreamParameters +{ + AudioStretchMode stretch_mode = DEFAULT_STRETCH_MODE; + AudioExpansionMode expansion_mode = DEFAULT_EXPANSION_MODE; + 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 AudioStretchMode DEFAULT_STRETCH_MODE = AudioStretchMode::TimeStretch; + static constexpr AudioExpansionMode DEFAULT_EXPANSION_MODE = AudioExpansionMode::Disabled; +#ifndef __ANDROID__ + static constexpr u16 DEFAULT_BUFFER_MS = 50; + static constexpr u16 DEFAULT_OUTPUT_LATENCY_MS = 20; +#else + static constexpr u16 DEFAULT_BUFFER_MS = 100; + static constexpr u16 DEFAULT_OUTPUT_LATENCY_MS = 20; +#endif + static constexpr u16 DEFAULT_EXPAND_BLOCK_SIZE = 1024; + 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 Load(SettingsInterface& si, const char* section); + void Save(SettingsInterface& si, const char* section) const; + void Clear(SettingsInterface& si, const char* section); + + bool operator==(const AudioStreamParameters& rhs) const; + bool operator!=(const AudioStreamParameters& rhs) const; +}; + class AudioStream { public: using SampleType = s16; - enum : u32 - { - CHUNK_SIZE = 64, - MAX_CHANNELS = 2 - }; + 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; + +#ifndef __ANDROID__ + static constexpr AudioBackend DEFAULT_BACKEND = AudioBackend::Cubeb; +#else + static constexpr AudioBackend DEFAULT_BACKEND = AudioBackend::AAudio; +#endif public: virtual ~AudioStream(); @@ -45,12 +143,21 @@ public: 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); + static const char* GetStretchModeName(AudioStretchMode mode); static const char* GetStretchModeDisplayName(AudioStretchMode mode); static std::optional ParseStretchMode(const char* name); ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } - ALWAYS_INLINE u32 GetChannels() const { return m_channels; } + ALWAYS_INLINE u32 GetInternalChannels() const { return m_internal_channels; } + ALWAYS_INLINE u32 GetOutputChannels() const { return m_internal_channels; } ALWAYS_INLINE u32 GetBufferSize() const { return m_buffer_size; } ALWAYS_INLINE u32 GetTargetBufferSize() const { return m_target_buffer_size; } ALWAYS_INLINE u32 GetOutputVolume() const { return m_volume; } @@ -77,57 +184,74 @@ public: void SetStretchMode(AudioStretchMode mode); - static std::unique_ptr CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms); + static std::unique_ptr CreateStream(AudioBackend backend, u32 sample_rate, + const AudioStreamParameters& parameters, Error* error = nullptr); + static std::unique_ptr CreateNullStream(u32 sample_rate, u32 buffer_ms); -#ifdef ENABLE_CUBEB - static std::unique_ptr CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch); +#ifndef __ANDROID__ static std::vector GetCubebDriverNames(); static std::vector> GetCubebOutputDevices(const char* driver); #endif -#ifdef ENABLE_SDL2 - static std::unique_ptr CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, - AudioStretchMode stretch); -#endif -#ifdef _WIN32 - static std::unique_ptr CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, - AudioStretchMode stretch); -#endif protected: - AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); - void BaseInitialize(); + 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 + }; - void ReadFrames(s16* samples, u32 num_frames); - void ApplyVolume(s16* samples, u32 num_frames); + using SampleReader = void (*)(SampleType* dest, const SampleType* src, u32 num_frames); + + AudioStream(u32 sample_rate, const AudioStreamParameters& parameters); + void BaseInitialize(SampleReader sample_reader); + + 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); + + void ApplyVolume(SampleType* samples, u32 num_samples); u32 m_sample_rate = 0; - u32 m_channels = 0; - u32 m_buffer_ms = 0; u32 m_volume = 0; - - AudioStretchMode m_stretch_mode = AudioStretchMode::Off; + AudioStreamParameters m_parameters; + u8 m_internal_channels = 0; + u8 m_output_channels = 0; bool m_stretch_inactive = false; bool m_filling = false; bool m_paused = false; private: - enum : u32 - { - AVERAGING_BUFFER_SIZE = 256, - AVERAGING_WINDOW = 50, - STRETCH_RESET_THRESHOLD = 5, - TARGET_IPS = 691, - }; + 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; + + ALWAYS_INLINE bool IsExpansionEnabled() const { return m_parameters.expansion_mode != AudioExpansionMode::Disabled; } + ALWAYS_INLINE bool IsStretchEnabled() const { return m_parameters.stretch_mode != AudioStretchMode::Off; } void AllocateBuffer(); void DestroyBuffer(); - void InternalWriteFrames(s32* bData, u32 nFrames); + void InternalWriteFrames(SampleType* samples, u32 num_frames); + +#ifndef __ANDROID__ + void ExpandAllocate(); +#endif void StretchAllocate(); void StretchDestroy(); - void StretchWrite(); + void StretchWriteBlock(const float* block); void StretchUnderrun(); void StretchOverrun(); @@ -135,7 +259,8 @@ private: void UpdateStretchTempo(); u32 m_buffer_size = 0; - std::unique_ptr m_buffer; + std::unique_ptr m_buffer; + SampleReader m_sample_reader = nullptr; std::atomic m_rpos{0}; std::atomic m_wpos{0}; @@ -156,12 +281,97 @@ private: std::array m_average_fullness = {}; // temporary staging buffer, used for timestretching - alignas(16) std::array m_staging_buffer; + std::unique_ptr m_staging_buffer; // float buffer, soundtouch only accepts float samples as input - alignas(16) std::array m_float_buffer; + std::unique_ptr m_float_buffer; + +#ifndef __ANDROID__ + 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; +#endif + +#ifndef __ANDROID__ + static std::unique_ptr CreateCubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + Error* error); + static std::unique_ptr CreateSDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, + Error* error); +#endif +#ifdef _WIN32 + static std::unique_ptr CreateXAudio2Stream(u32 sample_rate, const AudioStreamParameters& parameters, + Error* error); +#endif }; +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; + } +} + #ifdef _MSC_VER #pragma warning(pop) #endif diff --git a/src/util/cubeb_audio_stream.cpp b/src/util/cubeb_audio_stream.cpp index 3b4cc7e25..911858ccc 100644 --- a/src/util/cubeb_audio_stream.cpp +++ b/src/util/cubeb_audio_stream.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "host.h" @@ -7,6 +7,7 @@ #include "core/settings.h" #include "common/assert.h" +#include "common/error.h" #include "common/log.h" #include "common/scoped_guard.h" #include "common/string_util.h" @@ -26,13 +27,13 @@ namespace { class CubebAudioStream : public AudioStream { public: - CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); + CubebAudioStream(u32 sample_rate, const AudioStreamParameters& parameters); ~CubebAudioStream(); void SetPaused(bool paused) override; void SetOutputVolume(u32 volume) override; - bool Initialize(u32 latency_ms); + bool Initialize(Error* error); private: static void LogCallback(const char* fmt, ...); @@ -51,8 +52,34 @@ private: }; } // namespace -CubebAudioStream::CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) - : AudioStream(sample_rate, channels, buffer_ms, stretch) +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) { } @@ -95,14 +122,14 @@ void CubebAudioStream::DestroyContextAndStream() #endif } -bool CubebAudioStream::Initialize(u32 latency_ms) +bool CubebAudioStream::Initialize(Error* error) { #ifdef _WIN32 HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); m_com_initialized_by_us = SUCCEEDED(hr); if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { - Host::ReportErrorAsync("Error", "Failed to initialize COM for Cubeb"); + Error::SetHResult(error, "CoInitializeEx() failed: ", hr); return false; } #endif @@ -113,45 +140,72 @@ bool CubebAudioStream::Initialize(u32 latency_ms) cubeb_init(&m_context, "DuckStation", g_settings.audio_driver.empty() ? nullptr : g_settings.audio_driver.c_str()); if (rv != CUBEB_OK) { - Host::ReportFormattedErrorAsync("Error", "Could not initialize cubeb context: %d", rv); + 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_channels; - params.layout = CUBEB_LAYOUT_UNDEFINED; + 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, (latency_ms == 0) ? m_buffer_ms : latency_ms); + u32 latency_frames = GetBufferSizeForMS( + m_sample_rate, (m_parameters.output_latency_ms == 0) ? 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) { - Log_DevPrintf("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).", - m_buffer_ms, latency_frames); + Log_DevFmt("Cubeb backend does not support latency queries, using latency of {} ms ({} frames).", + m_parameters.buffer_ms, latency_frames); } else { if (rv != CUBEB_OK) { - Log_ErrorPrintf("(Cubeb) Could not get minimum latency: %d", rv); + 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); - Log_DevPrintf("(Cubeb) Minimum latency: %u ms (%u audio frames)", minimum_latency_ms, min_latency_frames); - if (latency_ms == 0) + Log_DevFmt("Minimum latency: {} ms ({} audio frames)", minimum_latency_ms, min_latency_frames); + if (m_parameters.output_latency_ms == 0) { // use minimum latency_frames = min_latency_frames; } - else if (minimum_latency_ms > latency_ms) + else if (minimum_latency_ms > m_parameters.output_latency_ms) { - Log_WarningPrintf("(Cubeb) Minimum latency is above requested latency: %u vs %u, adjusting to compensate.", - min_latency_frames, latency_frames); + Log_WarningFmt("Minimum latency is above requested latency: {} vs {}, adjusting to compensate.", + min_latency_frames, latency_frames); latency_frames = min_latency_frames; } } @@ -171,8 +225,8 @@ bool CubebAudioStream::Initialize(u32 latency_ms) const cubeb_device_info& di = devices.device[i]; if (di.device_id && selected_device_name == di.device_id) { - Log_InfoPrintf("Using output device '%s' (%s).", di.device_id, - di.friendly_name ? di.friendly_name : di.device_id); + Log_InfoFmt("Using output device '{}' ({}).", di.device_id, + di.friendly_name ? di.friendly_name : di.device_id); selected_device = di.devid; break; } @@ -186,11 +240,11 @@ bool CubebAudioStream::Initialize(u32 latency_ms) } else { - Log_WarningPrintf("cubeb_enumerate_devices() returned %d, using default device.", rv); + Log_WarningFmt("cubeb_enumerate_devices() returned {}, using default device.", GetCubebErrorString(rv)); } } - BaseInitialize(); + BaseInitialize(channel_setups[static_cast(m_parameters.expansion_mode)].second); m_volume = 100; m_paused = false; @@ -205,7 +259,7 @@ bool CubebAudioStream::Initialize(u32 latency_ms) if (rv != CUBEB_OK) { - Log_ErrorPrintf("(Cubeb) Could not create stream: %d", rv); + Error::SetStringFmt(error, "cubeb_stream_init() failed: {}", GetCubebErrorString(rv)); DestroyContextAndStream(); return false; } @@ -213,7 +267,7 @@ bool CubebAudioStream::Initialize(u32 latency_ms) rv = cubeb_stream_start(stream); if (rv != CUBEB_OK) { - Log_ErrorPrintf("(Cubeb) Could not start stream: %d", rv); + Error::SetStringFmt(error, "cubeb_stream_start() failed: {}", GetCubebErrorString(rv)); DestroyContextAndStream(); return false; } @@ -263,12 +317,11 @@ void CubebAudioStream::SetOutputVolume(u32 volume) m_volume = volume; } -std::unique_ptr AudioStream::CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch) +std::unique_ptr AudioStream::CreateCubebAudioStream(u32 sample_rate, + const AudioStreamParameters& parameters, Error* error) { - std::unique_ptr stream( - std::make_unique(sample_rate, channels, buffer_ms, stretch)); - if (!stream->Initialize(latency_ms)) + std::unique_ptr stream = std::make_unique(sample_rate, parameters); + if (!stream->Initialize(error)) stream.reset(); return stream; } diff --git a/src/util/input_manager.cpp b/src/util/input_manager.cpp index 851fafecf..eb7c451f7 100644 --- a/src/util/input_manager.cpp +++ b/src/util/input_manager.cpp @@ -602,10 +602,9 @@ static std::array(InputSourceType::Count)> s_input "XInput", "RawInput", #endif -#ifdef ENABLE_SDL2 +#ifndef __ANDROID__ "SDL", -#endif -#ifdef __ANDROID__ +#else "Android", #endif }}; @@ -638,12 +637,10 @@ bool InputManager::GetInputSourceDefaultEnabled(InputSourceType type) return false; #endif -#ifdef ENABLE_SDL2 +#ifndef __ANDROID__ case InputSourceType::SDL: return true; -#endif - -#ifdef __ANDROID__ +#else case InputSourceType::Android: return true; #endif @@ -1953,10 +1950,9 @@ void InputManager::ReloadSources(SettingsInterface& si, std::unique_lock CreateXInputSource(); static std::unique_ptr CreateWin32RawInputSource(); #endif -#ifdef ENABLE_SDL2 +#ifndef __ANDROID__ static std::unique_ptr CreateSDLSource(); -#endif -#ifdef __ANDROID__ +#else static std::unique_ptr CreateAndroidSource(); #endif }; diff --git a/src/util/sdl_audio_stream.cpp b/src/util/sdl_audio_stream.cpp index 94206b312..75c8b3f21 100644 --- a/src/util/sdl_audio_stream.cpp +++ b/src/util/sdl_audio_stream.cpp @@ -5,6 +5,7 @@ #include "common/assert.h" #include "common/log.h" +#include "common/error.h" #include @@ -14,13 +15,13 @@ namespace { class SDLAudioStream final : public AudioStream { public: - SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); + SDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters); ~SDLAudioStream(); void SetPaused(bool paused) override; void SetOutputVolume(u32 volume) override; - bool OpenDevice(u32 latency_ms); + bool OpenDevice(Error* error); void CloseDevice(); protected: @@ -32,17 +33,16 @@ protected: }; } // namespace -static bool InitializeSDLAudio() +static bool InitializeSDLAudio(Error* error) { static bool initialized = false; if (initialized) return true; // May as well keep it alive until the process exits. - const int error = SDL_InitSubSystem(SDL_INIT_AUDIO); - if (error != 0) + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { - Log_ErrorFmt("SDL_InitSubSystem(SDL_INIT_AUDIO) returned {}", error); + Error::SetStringFmt(error, "SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError()); return false; } @@ -52,8 +52,8 @@ static bool InitializeSDLAudio() return true; } -SDLAudioStream::SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) - : AudioStream(sample_rate, channels, buffer_ms, stretch) +SDLAudioStream::SDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters) + : AudioStream(sample_rate, parameters) { } @@ -63,28 +63,49 @@ SDLAudioStream::~SDLAudioStream() SDLAudioStream::CloseDevice(); } -std::unique_ptr AudioStream::CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch) +std::unique_ptr AudioStream::CreateSDLAudioStream(u32 sample_rate, const AudioStreamParameters& parameters, Error* error) { - if (!InitializeSDLAudio()) + if (!InitializeSDLAudio(error)) return {}; - std::unique_ptr stream = std::make_unique(sample_rate, channels, buffer_ms, stretch); - if (!stream->OpenDevice(latency_ms)) + std::unique_ptr stream = std::make_unique(sample_rate, parameters); + if (!stream->OpenDevice(error)) stream.reset(); return stream; } -bool SDLAudioStream::OpenDevice(u32 latency_ms) +bool SDLAudioStream::OpenDevice(Error* error) { DebugAssert(!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 = static_cast(m_channels); + spec.channels = m_output_channels; spec.format = AUDIO_S16; - spec.samples = static_cast(GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms)); + spec.samples = static_cast(GetBufferSizeForMS( + m_sample_rate, (m_parameters.output_latency_ms == 0) ? m_parameters.buffer_ms : m_parameters.output_latency_ms)); spec.callback = AudioCallback; spec.userdata = static_cast(this); @@ -92,13 +113,13 @@ bool SDLAudioStream::OpenDevice(u32 latency_ms) m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, SDL_AUDIO_ALLOW_SAMPLES_CHANGE); if (m_device_id == 0) { - Log_ErrorFmt("SDL_OpenAudioDevice() failed: {}", SDL_GetError()); + Error::SetStringFmt(error, "SDL_OpenAudioDevice() failed: {}", SDL_GetError()); return false; } Log_DevFmt("Requested {} frame buffer, got {} frame buffer", spec.samples, obtained_spec.samples); - BaseInitialize(); + BaseInitialize(sample_readers[static_cast(m_parameters.expansion_mode)]); m_volume = 100; m_paused = false; SDL_PauseAudioDevice(m_device_id, 0); @@ -124,7 +145,7 @@ void SDLAudioStream::CloseDevice() 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_channels; + const u32 num_frames = len / sizeof(SampleType) / this_ptr->m_output_channels; this_ptr->ReadFrames(reinterpret_cast(stream), num_frames); this_ptr->ApplyVolume(reinterpret_cast(stream), num_frames); diff --git a/src/util/util.props b/src/util/util.props index b066d4f96..033b23ba8 100644 --- a/src/util/util.props +++ b/src/util/util.props @@ -5,17 +5,16 @@ %(PreprocessorDefinitions);SOUNDTOUCH_FLOAT_SAMPLES;SOUNDTOUCH_ALLOW_SSE;ST_NO_EXCEPTION_HANDLING=1;SHADERC_SHAREDLIB - ENABLE_CUBEB=1;ENABLE_SDL2=1;%(PreprocessorDefinitions) %(PreprocessorDefinitions);ENABLE_OPENGL=1;ENABLE_VULKAN=1 %(PreprocessorDefinitions);SOUNDTOUCH_USE_NEON - %(AdditionalIncludeDirectories);$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\soundtouch\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\d3d12ma\include + %(AdditionalIncludeDirectories);$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\freesurround\include;$(SolutionDir)dep\kissfft\include;$(SolutionDir)dep\soundtouch\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\d3d12ma\include %(AdditionalIncludeDirectories);$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan\include;$(SolutionDir)dep\glslang - %(AdditionalDependencies);d3d11.lib;d3d12.lib;d3dcompiler.lib;dxgi.lib;Dwmapi.lib;winhttp.lib + %(AdditionalDependencies);d3d11.lib;d3d12.lib;d3dcompiler.lib;dxgi.lib;Dwmapi.lib;winhttp.lib;xaudio2.lib %(AdditionalDependencies);opengl32.lib diff --git a/src/util/util.vcxproj b/src/util/util.vcxproj index 5fd5404ef..43238b6df 100644 --- a/src/util/util.vcxproj +++ b/src/util/util.vcxproj @@ -241,6 +241,9 @@ {f351c4d8-594a-4850-b77b-3c1249812cce} + + {1b0366e5-6f82-47b4-9fdd-d699c86aa077} + {bb08260f-6fbc-46af-8924-090ee71360c6} diff --git a/src/util/xaudio2_audio_stream.cpp b/src/util/xaudio2_audio_stream.cpp index 5c504b4c0..ae077c39b 100644 --- a/src/util/xaudio2_audio_stream.cpp +++ b/src/util/xaudio2_audio_stream.cpp @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "util/audio_stream.h" #include "common/assert.h" +#include "common/error.h" #include "common/log.h" #include "common/windows_headers.h" @@ -20,13 +21,13 @@ namespace { class XAudio2AudioStream final : public AudioStream, private IXAudio2VoiceCallback { public: - XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); + XAudio2AudioStream(u32 sample_rate, const AudioStreamParameters& parameters); ~XAudio2AudioStream(); void SetPaused(bool paused) override; void SetOutputVolume(u32 volume) override; - bool OpenDevice(u32 latency_ms); + bool OpenDevice(Error* error); void CloseDevice(); void EnqueueBuffer(); @@ -56,15 +57,13 @@ private: u32 m_enqueue_buffer_size = 0; u32 m_current_buffer = 0; bool m_buffer_enqueued = false; - - HMODULE m_xaudio2_library = {}; bool m_com_initialized_by_us = false; }; } // namespace -XAudio2AudioStream::XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) - : AudioStream(sample_rate, channels, buffer_ms, stretch) +XAudio2AudioStream::XAudio2AudioStream(u32 sample_rate, const AudioStreamParameters& parameters) + : AudioStream(sample_rate, parameters) { } @@ -73,98 +72,110 @@ XAudio2AudioStream::~XAudio2AudioStream() if (IsOpen()) CloseDevice(); - if (m_xaudio2_library) - FreeLibrary(m_xaudio2_library); - if (m_com_initialized_by_us) CoUninitialize(); } -std::unique_ptr AudioStream::CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, - u32 latency_ms, AudioStretchMode stretch) +std::unique_ptr AudioStream::CreateXAudio2Stream(u32 sample_rate, const AudioStreamParameters& parameters, + Error* error) { - std::unique_ptr stream( - std::make_unique(sample_rate, channels, buffer_ms, stretch)); - if (!stream->OpenDevice(latency_ms)) + std::unique_ptr stream(std::make_unique(sample_rate, parameters)); + if (!stream->OpenDevice(error)) stream.reset(); return stream; } -bool XAudio2AudioStream::OpenDevice(u32 latency_ms) +bool XAudio2AudioStream::OpenDevice(Error* error) { DebugAssert(!IsOpen()); - m_xaudio2_library = LoadLibraryW(XAUDIO2_DLL_W); - if (!m_xaudio2_library) + if (m_parameters.expansion_mode == AudioExpansionMode::QuadraphonicLFE) { - Log_ErrorPrintf("Failed to load '%s', make sure you're using Windows 10", XAUDIO2_DLL_A); + Log_ErrorPrint("QuadraphonicLFE is not supported by XAudio2."); return false; } - using PFNXAUDIO2CREATE = - HRESULT(STDAPICALLTYPE*)(IXAudio2 * *ppXAudio2, UINT32 Flags, XAUDIO2_PROCESSOR XAudio2Processor); - PFNXAUDIO2CREATE xaudio2_create = - reinterpret_cast(GetProcAddress(m_xaudio2_library, "XAudio2Create")); - if (!xaudio2_create) - return false; + static constexpr const std::array(AudioExpansionMode::Count)> sample_readers = {{ + // Disabled + &StereoSampleReaderImpl, + // StereoLFE + &SampleReaderImpl, + // Quadraphonic + &SampleReaderImpl, + // QuadraphonicLFE + nullptr, + // Surround51 + &SampleReaderImpl, + // Surround71 + &SampleReaderImpl, + }}; HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); m_com_initialized_by_us = SUCCEEDED(hr); if (FAILED(hr) && hr != RPC_E_CHANGED_MODE && hr != S_FALSE) { - Log_ErrorPrintf("Failed to initialize COM"); + Error::SetHResult(error, "CoInitializeEx() failed: ", hr); return false; } - hr = xaudio2_create(m_xaudio.ReleaseAndGetAddressOf(), 0, XAUDIO2_DEFAULT_PROCESSOR); + hr = XAudio2Create(m_xaudio.ReleaseAndGetAddressOf(), 0, XAUDIO2_DEFAULT_PROCESSOR); if (FAILED(hr)) { - Log_ErrorPrintf("XAudio2Create() failed: %08X", hr); + Error::SetHResult(error, "XAudio2Create() failed: ", hr); return false; } - hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_channels, m_sample_rate, 0, nullptr); + hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_output_channels, m_sample_rate, 0, nullptr); if (FAILED(hr)) { - Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr); + Error::SetHResult(error, "CreateMasteringVoice() failed: ", hr); return false; } + // TODO: CHANNEL LAYOUT WAVEFORMATEX wf = {}; wf.cbSize = sizeof(wf); - wf.nAvgBytesPerSec = m_sample_rate * m_channels * sizeof(s16); - wf.nBlockAlign = static_cast(sizeof(s16) * m_channels); - wf.nChannels = static_cast(m_channels); + wf.nAvgBytesPerSec = m_sample_rate * m_output_channels * sizeof(s16); + wf.nBlockAlign = static_cast(sizeof(s16) * m_output_channels); + wf.nChannels = static_cast(m_output_channels); wf.nSamplesPerSec = m_sample_rate; wf.wBitsPerSample = sizeof(s16) * 8; wf.wFormatTag = WAVE_FORMAT_PCM; hr = m_xaudio->CreateSourceVoice(&m_source_voice, &wf, 0, 1.0f, this); if (FAILED(hr)) { - Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr); + Error::SetHResult(error, "CreateMasteringVoice() failed: ", hr); return false; } hr = m_source_voice->SetFrequencyRatio(1.0f); if (FAILED(hr)) { - Log_ErrorPrintf("SetFrequencyRatio() failed: %08X", hr); + Error::SetHResult(error, "SetFrequencyRatio() failed: ", hr); return false; } - m_enqueue_buffer_size = std::max(INTERNAL_BUFFER_SIZE, GetBufferSizeForMS(m_sample_rate, latency_ms)); + m_enqueue_buffer_size = + std::max(INTERNAL_BUFFER_SIZE, GetBufferSizeForMS(m_sample_rate, (m_parameters.output_latency_ms == 0) ? + m_parameters.buffer_ms : + m_parameters.output_latency_ms)); Log_DevPrintf("Allocating %u buffers of %u frames", NUM_BUFFERS, m_enqueue_buffer_size); for (u32 i = 0; i < NUM_BUFFERS; i++) - m_enqueue_buffers[i] = std::make_unique(m_enqueue_buffer_size * m_channels); + m_enqueue_buffers[i] = std::make_unique(m_enqueue_buffer_size * m_output_channels); - BaseInitialize(); + BaseInitialize(sample_readers[static_cast(m_parameters.expansion_mode)]); m_volume = 100; m_paused = false; hr = m_source_voice->Start(0, 0); if (FAILED(hr)) { - Log_ErrorPrintf("Start() failed: %08X", hr); + Error::SetHResult(error, "Start() failed: ", hr); return false; } @@ -220,9 +231,9 @@ void XAudio2AudioStream::EnqueueBuffer() ReadFrames(samples, m_enqueue_buffer_size); const XAUDIO2_BUFFER buf = { - static_cast(0), // flags - static_cast(sizeof(s16) * m_channels * m_enqueue_buffer_size), // bytes - reinterpret_cast(samples), // data + static_cast(0), // flags + static_cast(sizeof(s16) * m_output_channels * m_enqueue_buffer_size), // bytes + reinterpret_cast(samples), // data 0u, 0u, 0u,