From 68b5dd869c31e6f78aa2d17d90958737b60b115a Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 28 Jul 2022 00:42:41 +1000 Subject: [PATCH] SPU: Add time stretched audio output --- duckstation.sln | 1 - src/core/host.h | 4 +- src/core/settings.cpp | 22 +- src/core/settings.h | 14 +- src/core/spu.cpp | 24 +- src/core/system.cpp | 53 +- src/duckstation-qt/audiosettingswidget.cpp | 75 +- src/duckstation-qt/audiosettingswidget.h | 3 +- src/duckstation-qt/audiosettingswidget.ui | 167 ++-- src/frontend-common/common_host.cpp | 25 +- src/frontend-common/common_host.h | 17 + src/frontend-common/cubeb_audio_stream.cpp | 207 +++-- src/frontend-common/cubeb_audio_stream.h | 30 +- src/frontend-common/fullscreen_ui.cpp | 10 +- src/frontend-common/imgui_overlays.cpp | 12 + src/frontend-common/sdl_audio_stream.cpp | 61 +- src/frontend-common/sdl_audio_stream.h | 13 +- src/frontend-common/xaudio2_audio_stream.cpp | 98 ++- src/frontend-common/xaudio2_audio_stream.h | 39 +- src/util/CMakeLists.txt | 4 +- src/util/audio_stream.cpp | 852 ++++++++++++------- src/util/audio_stream.h | 192 +++-- src/util/null_audio_stream.cpp | 25 - src/util/null_audio_stream.h | 15 - src/util/util.props | 6 +- src/util/util.vcxproj | 2 - src/util/util.vcxproj.filters | 2 - 27 files changed, 1165 insertions(+), 808 deletions(-) delete mode 100644 src/util/null_audio_stream.cpp delete mode 100644 src/util/null_audio_stream.h diff --git a/duckstation.sln b/duckstation.sln index 88b286634..a7541cd52 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -102,7 +102,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "dep\fmt\fmt.vcxproj" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "util", "src\util\util.vcxproj", "{57F6206D-F264-4B07-BAF8-11B9BBE1F455}" ProjectSection(ProjectDependencies) = postProject - {39F0ADFF-3A84-470D-9CF0-CA49E164F2F3} = {39F0ADFF-3A84-470D-9CF0-CA49E164F2F3} {425D6C99-D1C8-43C2-B8AC-4D7B1D941017} = {425D6C99-D1C8-43C2-B8AC-4D7B1D941017} {751D9F62-881C-454E-BCE8-CB9CF5F1D22F} = {751D9F62-881C-454E-BCE8-CB9CF5F1D22F} {EE054E08-3799-4A59-A422-18259C105FFD} = {EE054E08-3799-4A59-A422-18259C105FFD} diff --git a/src/core/host.h b/src/core/host.h index 26392d107..571918cb7 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -13,6 +13,7 @@ struct WindowInfo; enum class AudioBackend : u8; +enum class AudioStretchMode : u8; class AudioStream; class CDImage; @@ -77,7 +78,8 @@ std::optional GetResourceFileTimestamp(const char* filename); TinyString TranslateString(const char* context, const char* str, const char* disambiguation = nullptr, int n = -1); std::string TranslateStdString(const char* context, const char* str, const char* disambiguation = nullptr, int n = -1); -std::unique_ptr CreateAudioStream(AudioBackend backend); +std::unique_ptr CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms, + u32 latency_ms, AudioStretchMode stretch); /// Returns the scale of OSD elements. float GetOSDScale(); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index f0179f451..236bb0216 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -275,10 +275,15 @@ void Settings::Load(SettingsInterface& si) audio_backend = ParseAudioBackend(si.GetStringValue("Audio", "Backend", GetAudioBackendName(DEFAULT_AUDIO_BACKEND)).c_str()) .value_or(DEFAULT_AUDIO_BACKEND); - audio_output_volume = si.GetIntValue("Audio", "OutputVolume", 100); - audio_fast_forward_volume = si.GetIntValue("Audio", "FastForwardVolume", 100); - audio_buffer_size = si.GetIntValue("Audio", "BufferSize", DEFAULT_AUDIO_BUFFER_SIZE); - audio_resampling = si.GetBoolValue("Audio", "Resampling", true); + 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_output_volume = si.GetUIntValue("Audio", "OutputVolume", 100); + audio_fast_forward_volume = si.GetUIntValue("Audio", "FastForwardVolume", 100); + audio_output_muted = si.GetBoolValue("Audio", "OutputMuted", false); audio_sync_enabled = si.GetBoolValue("Audio", "Sync", true); audio_dump_on_boot = si.GetBoolValue("Audio", "DumpOnBoot", false); @@ -472,10 +477,11 @@ void Settings::Save(SettingsInterface& si) const si.SetIntValue("CDROM", "SeekSpeedup", cdrom_seek_speedup); si.SetStringValue("Audio", "Backend", GetAudioBackendName(audio_backend)); - si.SetIntValue("Audio", "OutputVolume", audio_output_volume); - si.SetIntValue("Audio", "FastForwardVolume", audio_fast_forward_volume); - si.SetIntValue("Audio", "BufferSize", audio_buffer_size); - si.SetBoolValue("Audio", "Resampling", audio_resampling); + si.SetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(audio_stretch_mode)); + si.SetUIntValue("Audio", "BufferMS", audio_buffer_ms); + si.SetUIntValue("Audio", "OutputLatencyMS", audio_output_latency_ms); + si.SetUIntValue("Audio", "OutputVolume", audio_output_volume); + si.SetUIntValue("Audio", "FastForwardVolume", audio_fast_forward_volume); si.SetBoolValue("Audio", "OutputMuted", audio_output_muted); si.SetBoolValue("Audio", "Sync", audio_sync_enabled); si.SetBoolValue("Audio", "DumpOnBoot", audio_dump_on_boot); diff --git a/src/core/settings.h b/src/core/settings.h index e4abb1986..1e79d9c6d 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -3,6 +3,7 @@ #include "common/settings_interface.h" #include "common/string.h" #include "types.h" +#include "util/audio_stream.h" #include #include #include @@ -142,10 +143,11 @@ struct Settings u32 cdrom_seek_speedup = 1; AudioBackend audio_backend = DEFAULT_AUDIO_BACKEND; - s32 audio_output_volume = 100; - s32 audio_fast_forward_volume = 100; - u32 audio_buffer_size = DEFAULT_AUDIO_BUFFER_SIZE; - bool audio_resampling = true; + AudioStretchMode audio_stretch_mode = DEFAULT_AUDIO_STRETCH_MODE; + 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; bool audio_output_muted = false; bool audio_sync_enabled = true; bool audio_dump_on_boot = false; @@ -400,7 +402,9 @@ struct Settings static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO; - static constexpr u32 DEFAULT_AUDIO_BUFFER_SIZE = 2048; + static constexpr u32 DEFAULT_AUDIO_BUFFER_MS = 50; + static constexpr u32 DEFAULT_AUDIO_OUTPUT_LATENCY_MS = 20; + static constexpr AudioStretchMode DEFAULT_AUDIO_STRETCH_MODE = AudioStretchMode::TimeStretch; // Enable console logging by default on Linux platforms. #if defined(__linux__) && !defined(__ANDROID__) diff --git a/src/core/spu.cpp b/src/core/spu.cpp index 922cebbd4..37ee040f9 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -30,8 +30,7 @@ void SPU::Initialize() "SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD, [](void* param, TickCount ticks, TickCount ticks_late) { static_cast(param)->ExecuteTransfer(ticks); }, this, false); - m_null_audio_stream = AudioStream::CreateNullAudioStream(); - m_null_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, Settings::DEFAULT_AUDIO_BUFFER_SIZE); + m_null_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms); CreateOutputStream(); Reset(); @@ -39,22 +38,23 @@ void SPU::Initialize() void SPU::CreateOutputStream() { - Log_InfoPrintf("Creating '%s' audio stream, sample rate = %u, channels = %u, buffer size = %u", - Settings::GetAudioBackendName(g_settings.audio_backend), SAMPLE_RATE, NUM_CHANNELS, - g_settings.audio_buffer_size); + Log_InfoPrintf( + "Creating '%s' audio stream, sample rate = %u, channels = %u, buffer = %u, latency = %u, stretching = %s", + Settings::GetAudioBackendName(g_settings.audio_backend), SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms, + g_settings.audio_output_latency_ms, AudioStream::GetStretchModeName(g_settings.audio_stretch_mode)); - m_audio_stream = Host::CreateAudioStream(g_settings.audio_backend); - - if (!m_audio_stream || - !m_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_size)) + m_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); + if (!m_audio_stream) { Host::ReportErrorAsync("Error", "Failed to create or configure audio stream, falling back to null output."); m_audio_stream.reset(); - m_audio_stream = AudioStream::CreateNullAudioStream(); - m_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_size); + m_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms); } m_audio_stream->SetOutputVolume(System::GetAudioOutputVolume()); + m_audio_stream->SetPaused(System::IsPaused()); } void SPU::RecreateOutputStream() @@ -77,7 +77,7 @@ void SPU::Shutdown() m_tick_event.reset(); m_transfer_event.reset(); m_dump_writer.reset(); - m_audio_stream = nullptr; + m_audio_stream.reset(); } void SPU::Reset() diff --git a/src/core/system.cpp b/src/core/system.cpp index 4a0894844..6e97bde7f 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -923,9 +923,7 @@ void System::PauseSystem(bool paused) return; SetState(paused ? State::Paused : State::Running); - if (!paused) - g_spu.GetOutputStream()->EmptyBuffers(); - g_spu.GetOutputStream()->PauseOutput(paused); + g_spu.GetOutputStream()->SetPaused(paused); if (paused) { @@ -1179,7 +1177,7 @@ bool System::BootSystem(SystemBootParameters parameters) // Good to go. Host::OnSystemStarted(); UpdateSoftwareCursor(); - g_spu.GetOutputStream()->PauseOutput(false); + g_spu.GetOutputStream()->SetPaused(false); // Initial state must be set before loading state. s_state = @@ -1813,7 +1811,7 @@ bool System::DoLoadState(ByteStream* state, bool force_software_renderer, bool u if (s_state == State::Starting) s_state = State::Running; - g_spu.GetOutputStream()->EmptyBuffers(); + g_spu.GetOutputStream()->EmptyBuffer(); ResetPerformanceCounters(); ResetThrottler(); return true; @@ -2035,14 +2033,6 @@ void System::ResetThrottler() void System::Throttle() { - // Reset the throttler on audio buffer overflow, so we don't end up out of phase. - if (g_spu.GetOutputStream()->DidUnderflow() && s_target_speed >= 1.0f) - { - Log_VerbosePrintf("Audio buffer underflowed, resetting throttler"); - ResetThrottler(); - return; - } - // Allow variance of up to 40ms either way. #ifndef __ANDROID__ static constexpr double MAX_VARIANCE_TIME_NS = 40 * 1000000; @@ -2181,7 +2171,8 @@ void System::UpdateSpeedLimiterState() m_display_all_frames = !m_throttler_enabled || g_settings.display_all_frames; bool syncing_to_host = false; - if (g_settings.sync_to_host_refresh_rate && g_settings.audio_resampling && target_speed == 1.0f && IsRunning()) + if (g_settings.sync_to_host_refresh_rate && (g_settings.audio_stretch_mode != AudioStretchMode::Off) && + target_speed == 1.0f && IsValid()) { float host_refresh_rate; if (g_host_display->GetHostRefreshRate(&host_refresh_rate)) @@ -2212,21 +2203,18 @@ void System::UpdateSpeedLimiterState() UpdateThrottlePeriod(); ResetThrottler(); - const u32 input_sample_rate = (target_speed == 0.0f || !g_settings.audio_resampling) ? - SPU::SAMPLE_RATE : - static_cast(static_cast(SPU::SAMPLE_RATE) * target_speed); - Log_InfoPrintf("Audio input sample rate: %u hz", input_sample_rate); - AudioStream* stream = g_spu.GetOutputStream(); - stream->SetInputSampleRate(input_sample_rate); - stream->SetWaitForBufferFill(true); - if (g_settings.audio_fast_forward_volume != g_settings.audio_output_volume) stream->SetOutputVolume(GetAudioOutputVolume()); - stream->SetSync(audio_sync_enabled); - if (audio_sync_enabled) - stream->EmptyBuffers(); + // Adjust nominal rate when resampling, or syncing to host. + const bool rate_adjust = + (syncing_to_host || g_settings.audio_stretch_mode == AudioStretchMode::Resample) && target_speed > 0.0f; + stream->SetNominalRate(rate_adjust ? target_speed : 1.0f); + + // stream->SetSync(audio_sync_enabled); + // if (audio_sync_enabled) + // stream->EmptyBuffer(); } g_host_display->SetDisplayMaxFPS(max_display_fps); @@ -3034,8 +3022,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings) UpdateOverclock(); } - if (g_settings.audio_backend != old_settings.audio_backend || - g_settings.audio_buffer_size != old_settings.audio_buffer_size) + if (g_settings.audio_backend != old_settings.audio_backend) { if (g_settings.audio_backend != old_settings.audio_backend) { @@ -3044,7 +3031,15 @@ void System::CheckForSettingsChanges(const Settings& old_settings) } g_spu.RecreateOutputStream(); - g_spu.GetOutputStream()->PauseOutput(IsPaused()); + } + if (g_settings.audio_stretch_mode != old_settings.audio_stretch_mode) + g_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) + { + g_spu.RecreateOutputStream(); + UpdateSpeedLimiterState(); } if (g_settings.emulation_speed != old_settings.emulation_speed) @@ -3169,7 +3164,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings) g_dma.SetHaltTicks(g_settings.dma_halt_ticks); if (g_settings.audio_backend != old_settings.audio_backend || - g_settings.audio_buffer_size != old_settings.audio_buffer_size || g_settings.video_sync_enabled != old_settings.video_sync_enabled || g_settings.audio_sync_enabled != old_settings.audio_sync_enabled || g_settings.increase_timer_resolution != old_settings.increase_timer_resolution || @@ -3177,7 +3171,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings) g_settings.fast_forward_speed != old_settings.fast_forward_speed || g_settings.display_max_fps != old_settings.display_max_fps || g_settings.display_all_frames != old_settings.display_all_frames || - g_settings.audio_resampling != old_settings.audio_resampling || g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate) { UpdateSpeedLimiterState(); diff --git a/src/duckstation-qt/audiosettingswidget.cpp b/src/duckstation-qt/audiosettingswidget.cpp index 6a860ae42..b118a3044 100644 --- a/src/duckstation-qt/audiosettingswidget.cpp +++ b/src/duckstation-qt/audiosettingswidget.cpp @@ -20,14 +20,24 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", &Settings::ParseAudioBackend, &Settings::GetAudioBackendName, Settings::DEFAULT_AUDIO_BACKEND); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToOutput, "Audio", "Sync", true); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferSize, "Audio", "BufferSize", - Settings::DEFAULT_AUDIO_BUFFER_SIZE); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.stretchMode, "Audio", "StretchMode", + &AudioStream::ParseStretchMode, &AudioStream::GetStretchModeName, + Settings::DEFAULT_AUDIO_STRETCH_MODE); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "Audio", "BufferMS", + Settings::DEFAULT_AUDIO_BUFFER_MS); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "Audio", "OutputLatencyMS", + Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startDumpingOnBoot, "Audio", "DumpOnBoot", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muteCDAudio, "CDROM", "MuteCDAudio", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.resampling, "Audio", "Resampling", true); - connect(m_ui.bufferSize, &QSlider::valueChanged, this, &AudioSettingsWidget::updateBufferingLabel); - updateBufferingLabel(); + m_ui.outputLatencyMinimal->setChecked(m_ui.outputLatencyMS->value() == 0); + m_ui.outputLatencyMS->setEnabled(m_ui.outputLatencyMinimal->isChecked()); + m_ui.driver->setEnabled(false); + + connect(m_ui.bufferMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel); + connect(m_ui.outputLatencyMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel); + connect(m_ui.outputLatencyMinimal, &QCheckBox::toggled, this, &AudioSettingsWidget::onMinimalOutputLatencyChecked); + updateLatencyLabel(); // for per-game, just use the normal path, since it needs to re-read/apply if (!dialog->isPerGameSettings()) @@ -53,7 +63,7 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent "lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio " "output.")); dialog->registerWidgetHelp( - m_ui.bufferSize, tr("Buffer Size"), QStringLiteral("2048"), + m_ui.outputLatencyMS, tr("Output Latency"), QStringLiteral("50 ms"), tr("The buffer size determines the size of the chunks of audio which will be pulled by the " "host. Smaller values reduce the output latency, but may cause hitches if the emulation " "speed is inconsistent. Note that the Cubeb backend uses smaller chunks regardless of " @@ -75,27 +85,31 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent 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.resampling, tr("Resampling"), tr("Checked"), - tr("When running outside of 100% speed, resamples audio from the target speed instead of dropping frames. Produces " + 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 " "much nicer fast forward/slowdown audio at a small cost to performance.")); } AudioSettingsWidget::~AudioSettingsWidget() = default; -void AudioSettingsWidget::updateBufferingLabel() +void AudioSettingsWidget::updateLatencyLabel() { - constexpr float step = 128; - const u32 actual_buffer_size = - static_cast(std::round(static_cast(m_ui.bufferSize->value()) / step) * step); - if (static_cast(m_ui.bufferSize->value()) != actual_buffer_size) + const u32 output_latency_ms = static_cast(m_ui.outputLatencyMS->value()); + const u32 output_latency_frames = AudioStream::GetBufferSizeForMS(SPU::SAMPLE_RATE, output_latency_ms); + const u32 buffer_ms = static_cast(m_ui.bufferMS->value()); + const u32 buffer_frames = AudioStream::GetBufferSizeForMS(SPU::SAMPLE_RATE, buffer_ms); + if (output_latency_ms > 0) { - m_ui.bufferSize->setValue(static_cast(actual_buffer_size)); - return; + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 frames / %2 ms (%3ms buffer + %5ms output)") + .arg(buffer_frames + output_latency_frames) + .arg(buffer_ms + output_latency_ms) + .arg(buffer_ms) + .arg(output_latency_ms)); + } + else + { + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 frames / %2 ms").arg(buffer_frames).arg(buffer_ms)); } - - const float max_latency = AudioStream::GetMaxLatency(SPU::SAMPLE_RATE, actual_buffer_size); - m_ui.bufferingLabel->setText(tr("Maximum Latency: %n frames (%1ms)", "", actual_buffer_size) - .arg(static_cast(max_latency) * 1000.0, 0, 'f', 2)); } void AudioSettingsWidget::updateVolumeLabel() @@ -104,9 +118,21 @@ void AudioSettingsWidget::updateVolumeLabel() m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value())); } +void AudioSettingsWidget::onMinimalOutputLatencyChecked(bool new_value) +{ + const u32 value = new_value ? 0u : Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS; + m_dialog->setIntSettingValue("Audio", "OutputLatencyMS", value); + QSignalBlocker sb(m_ui.outputLatencyMS); + m_ui.outputLatencyMS->setValue(value); + m_ui.outputLatencyMS->setEnabled(!new_value); + updateLatencyLabel(); +} + void AudioSettingsWidget::onOutputVolumeChanged(int new_value) { - m_dialog->setIntSettingValue("Audio", "OutputVolume", new_value); + // only called for base settings + DebugAssert(!m_dialog->isPerGameSettings()); + Host::SetBaseIntSettingValue("Audio", "OutputVolume", new_value); g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value()); updateVolumeLabel(); @@ -114,7 +140,9 @@ void AudioSettingsWidget::onOutputVolumeChanged(int new_value) void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value) { - m_dialog->setIntSettingValue("Audio", "FastForwardVolume", new_value); + // only called for base settings + DebugAssert(!m_dialog->isPerGameSettings()); + Host::SetBaseIntSettingValue("Audio", "FastForwardVolume", new_value); g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value); updateVolumeLabel(); @@ -122,7 +150,10 @@ void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value) void AudioSettingsWidget::onOutputMutedChanged(int new_state) { + // only called for base settings + DebugAssert(!m_dialog->isPerGameSettings()); + const bool muted = (new_state != 0); - m_dialog->setBoolSettingValue("Audio", "OutputMuted", muted); + Host::SetBaseBoolSettingValue("Audio", "OutputMuted", muted); g_emu_thread->setAudioOutputMuted(muted); } diff --git a/src/duckstation-qt/audiosettingswidget.h b/src/duckstation-qt/audiosettingswidget.h index 0c60bf89d..65c7bb77f 100644 --- a/src/duckstation-qt/audiosettingswidget.h +++ b/src/duckstation-qt/audiosettingswidget.h @@ -15,8 +15,9 @@ public: ~AudioSettingsWidget(); private Q_SLOTS: - void updateBufferingLabel(); + void updateLatencyLabel(); void updateVolumeLabel(); + void onMinimalOutputLatencyChecked(bool new_value); void onOutputVolumeChanged(int new_value); void onFastForwardVolumeChanged(int new_value); void onOutputMutedChanged(int new_state); diff --git a/src/duckstation-qt/audiosettingswidget.ui b/src/duckstation-qt/audiosettingswidget.ui index bdc7608ee..e1c7fb3d1 100644 --- a/src/duckstation-qt/audiosettingswidget.ui +++ b/src/duckstation-qt/audiosettingswidget.ui @@ -6,8 +6,8 @@ 0 0 - 502 - 312 + 516 + 435 @@ -31,7 +31,21 @@ Configuration - + + + + + Buffer Size: + + + + + + + Start Dumping On Boot + + + @@ -39,55 +53,87 @@ + + + + + + 500 + + + Qt::Horizontal + + + QSlider::TicksBothSides + + + 20 + + + + + + + Minimal + + + + + + + + + + + + + Off (Noisy) + + + + + Resampling (Pitch Shift) + + + + + Time Stretch (Tempo Change, Best Sound) + + + + + + + + Output Latency: + + + - + - Buffer Size: - - - - - - - 1024 - - - 8192 - - - 128 - - - 1024 - - - Qt::Horizontal - - - QSlider::TicksBothSides - - - 1024 + Driver: - - - Qt::Horizontal + + + Stretch Mode: - - - 40 - 20 - - - + - + + + + Sync To Output + + + + Maximum latency: 0 frames (0.00ms) @@ -97,24 +143,31 @@ - - - - Sync To Output + + + + 15 - - - - - - Resampling + + 500 - - - - - - Start Dumping On Boot + + 1 + + + 5 + + + 50 + + + Qt::Horizontal + + + QSlider::TicksBothSides + + + 20 diff --git a/src/frontend-common/common_host.cpp b/src/frontend-common/common_host.cpp index 5b4e19a13..f244cf660 100644 --- a/src/frontend-common/common_host.cpp +++ b/src/frontend-common/common_host.cpp @@ -66,14 +66,6 @@ #include #endif -namespace FrontendCommon { - -#ifdef _WIN32 -std::unique_ptr CreateXAudio2AudioStream(); -#endif - -} // namespace FrontendCommon - Log_SetChannel(CommonHostInterface); namespace CommonHost { @@ -148,26 +140,27 @@ void CommonHost::ReleaseHostDisplayResources() // } -std::unique_ptr Host::CreateAudioStream(AudioBackend backend) +std::unique_ptr Host::CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms, + u32 latency_ms, AudioStretchMode stretch) { switch (backend) { case AudioBackend::Null: - return AudioStream::CreateNullAudioStream(); + return AudioStream::CreateNullStream(sample_rate, channels, buffer_ms); #ifndef _UWP case AudioBackend::Cubeb: - return CubebAudioStream::Create(); + return CommonHost::CreateCubebAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch); #endif #ifdef _WIN32 case AudioBackend::XAudio2: - return FrontendCommon::CreateXAudio2AudioStream(); + return CommonHost::CreateXAudio2Stream(sample_rate, channels, buffer_ms, latency_ms, stretch); #endif #ifdef WITH_SDL2 case AudioBackend::SDL: - return SDLAudioStream::Create(); + return CommonHost::CreateSDLAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch); #endif default: @@ -927,7 +920,7 @@ DEFINE_HOTKEY("AudioMute", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE("Hotke { g_settings.audio_output_muted = !g_settings.audio_output_muted; const s32 volume = System::GetAudioOutputVolume(); - g_spu.GetOutputStream()->SetOutputVolume(volume); + // g_spu.GetOutputStream()->SetOutputVolume(volume); if (g_settings.audio_output_muted) { Host::AddKeyedOSDMessage("AudioControlHotkey", Host::TranslateStdString("OSDMessage", "Volume: Muted"), 2.0f); @@ -959,7 +952,7 @@ DEFINE_HOTKEY("AudioVolumeUp", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE("H const s32 volume = std::min(System::GetAudioOutputVolume() + 10, 100); g_settings.audio_output_volume = volume; g_settings.audio_fast_forward_volume = volume; - g_spu.GetOutputStream()->SetOutputVolume(volume); + // g_spu.GetOutputStream()->SetOutputVolume(volume); Host::AddKeyedFormattedOSDMessage("AudioControlHotkey", 2.0f, Host::TranslateString("OSDMessage", "Volume: %d%%"), volume); } @@ -973,7 +966,7 @@ DEFINE_HOTKEY("AudioVolumeDown", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE( const s32 volume = std::max(System::GetAudioOutputVolume() - 10, 0); g_settings.audio_output_volume = volume; g_settings.audio_fast_forward_volume = volume; - g_spu.GetOutputStream()->SetOutputVolume(volume); + // g_spu.GetOutputStream()->SetOutputVolume(volume); Host::AddKeyedFormattedOSDMessage("AudioControlHotkey", 2.0f, Host::TranslateString("OSDMessage", "Volume: %d%%"), volume); } diff --git a/src/frontend-common/common_host.h b/src/frontend-common/common_host.h index c69d04ebe..016b8d79a 100644 --- a/src/frontend-common/common_host.h +++ b/src/frontend-common/common_host.h @@ -1,9 +1,13 @@ #pragma once #include "core/system.h" +#include #include class SettingsInterface; +class AudioStream; +enum class AudioStretchMode : u8; + namespace CommonHost { /// Initializes configuration. void UpdateLogSettings(); @@ -25,6 +29,19 @@ void OnGameChanged(const std::string& disc_path, const std::string& game_serial, void PumpMessagesOnCPUThread(); bool CreateHostDisplayResources(); void ReleaseHostDisplayResources(); + +#ifdef _WIN32 +std::unique_ptr CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, + AudioStretchMode stretch); +#endif +#ifdef WITH_SDL2 +std::unique_ptr CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, + AudioStretchMode stretch); +#endif +#ifndef _UWP +std::unique_ptr CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, + AudioStretchMode stretch); +#endif } // namespace CommonHost namespace ImGuiManager { diff --git a/src/frontend-common/cubeb_audio_stream.cpp b/src/frontend-common/cubeb_audio_stream.cpp index 099e62b5b..1e4b35b6b 100644 --- a/src/frontend-common/cubeb_audio_stream.cpp +++ b/src/frontend-common/cubeb_audio_stream.cpp @@ -1,6 +1,11 @@ #include "cubeb_audio_stream.h" #include "common/assert.h" #include "common/log.h" +#include "common/string_util.h" +#include "common_host.h" +#include "core/host.h" +#include "core/host_settings.h" +#include "cubeb/cubeb.h" Log_SetChannel(CubebAudioStream); #ifdef _WIN32 @@ -9,154 +14,188 @@ Log_SetChannel(CubebAudioStream); #pragma comment(lib, "Ole32.lib") #endif -CubebAudioStream::CubebAudioStream() = default; +static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state); + +CubebAudioStream::CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) + : AudioStream(sample_rate, channels, buffer_ms, stretch) +{ +} CubebAudioStream::~CubebAudioStream() { - if (IsOpen()) - CubebAudioStream::CloseDevice(); + DestroyContextAndStream(); } -bool CubebAudioStream::OpenDevice() +void CubebAudioStream::LogCallback(const char* fmt, ...) { - Assert(!IsOpen()); + std::va_list ap; + va_start(ap, fmt); + std::string msg(StringUtil::StdStringFromFormatV(fmt, ap)); + va_end(ap); + Log_DevPrintf("(Cubeb): %s", msg.c_str()); +} +void CubebAudioStream::DestroyContextAndStream() +{ + if (stream) + { + cubeb_stream_stop(stream); + cubeb_stream_destroy(stream); + stream = nullptr; + } + + if (m_context) + { + cubeb_destroy(m_context); + m_context = nullptr; + } + +#ifdef _WIN32 + if (m_com_initialized_by_us) + { + CoUninitialize(); + m_com_initialized_by_us = false; + } +#endif +} + +bool CubebAudioStream::Initialize(u32 latency_ms) +{ #ifdef _WIN32 HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); m_com_initialized_by_us = SUCCEEDED(hr); - if (FAILED(hr) && hr != RPC_E_CHANGED_MODE && hr != S_FALSE) + if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { - Log_ErrorPrintf("Failed to initialize COM"); + Host::ReportErrorAsync("Error", "Failed to initialize COM for Cubeb"); return false; } #endif - int rv = cubeb_init(&m_cubeb_context, "DuckStation", nullptr); + cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback); + + std::string backend(Host::GetStringSettingValue("Audio", "CubebBackend")); + + int rv = cubeb_init(&m_context, "DuckStation", backend.empty() ? nullptr : backend.c_str()); if (rv != CUBEB_OK) { - Log_ErrorPrintf("Could not initialize cubeb context: %d", rv); + Host::ReportFormattedErrorAsync("Error", "Could not initialize cubeb context: %d", rv); return false; } cubeb_stream_params params = {}; params.format = CUBEB_SAMPLE_S16LE; - params.rate = m_output_sample_rate; + params.rate = m_sample_rate; params.channels = m_channels; params.layout = CUBEB_LAYOUT_UNDEFINED; - params.prefs = CUBEB_STREAM_PREF_PERSIST; + params.prefs = CUBEB_STREAM_PREF_NONE; - u32 latency_frames = 0; - rv = cubeb_get_min_latency(m_cubeb_context, ¶ms, &latency_frames); + u32 latency_frames = GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms); + u32 min_latency_frames = 0; + rv = cubeb_get_min_latency(m_context, ¶ms, &min_latency_frames); if (rv == CUBEB_ERROR_NOT_SUPPORTED) { - Log_WarningPrintf("Cubeb backend does not support latency queries, using buffer size of %u.", m_buffer_size); - latency_frames = m_buffer_size; + Log_DevPrintf("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).", + m_buffer_ms, latency_frames); } else { if (rv != CUBEB_OK) { - Log_ErrorPrintf("Could not get minimum latency: %d", rv); - DestroyContext(); + Log_ErrorPrintf("(Cubeb) Could not get minimum latency: %d", rv); + DestroyContextAndStream(); return false; } - Log_InfoPrintf("Minimum latency in frames: %u", latency_frames); - if (latency_frames > m_buffer_size) + const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames); + Log_DevPrintf("(Cubeb) Minimum latency: %u ms (%u audio frames)", minimum_latency_ms, min_latency_frames); + if (latency_ms == 0) { - Log_WarningPrintf("Minimum latency is above buffer size: %u vs %u, adjusting to compensate.", latency_frames, - m_buffer_size); - - if (!SetBufferSize(latency_frames)) - { - Log_ErrorPrintf("Failed to set new buffer size of %u frames", latency_frames); - DestroyContext(); - return false; - } + // use minimum + latency_frames = min_latency_frames; } - else + else if (minimum_latency_ms > latency_ms) { - latency_frames = m_buffer_size; + Log_WarningPrintf("(Cubeb) Minimum latency is above requested latency: %u vs %u, adjusting to compensate.", + min_latency_frames, latency_frames); + latency_frames = min_latency_frames; } } - char stream_name[32]; - std::snprintf(stream_name, sizeof(stream_name), "AudioStream_%p", this); + BaseInitialize(); + m_volume = 100; + m_paused = false; - rv = cubeb_stream_init(m_cubeb_context, &m_cubeb_stream, stream_name, nullptr, nullptr, nullptr, ¶ms, - latency_frames, DataCallback, StateCallback, this); + char stream_name[32]; + std::snprintf(stream_name, sizeof(stream_name), "%p", this); + + rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, ¶ms, latency_frames, + &CubebAudioStream::DataCallback, StateCallback, this); if (rv != CUBEB_OK) { - Log_ErrorPrintf("Could not create stream: %d", rv); - DestroyContext(); + Log_ErrorPrintf("(Cubeb) Could not create stream: %d", rv); + DestroyContextAndStream(); + return false; + } + + rv = cubeb_stream_start(stream); + if (rv != CUBEB_OK) + { + Log_ErrorPrintf("(Cubeb) Could not start stream: %d", rv); + DestroyContextAndStream(); return false; } - cubeb_stream_set_volume(m_cubeb_stream, static_cast(m_output_volume) / 100.0f); return true; } -void CubebAudioStream::PauseDevice(bool paused) +void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) { - if (paused == m_paused) + // noop +} + +long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, + long nframes) +{ + static_cast(user_ptr)->ReadFrames(static_cast(output_buffer), static_cast(nframes)); + return nframes; +} + +void CubebAudioStream::SetPaused(bool paused) +{ + if (paused == m_paused || !stream) return; - int rv = paused ? cubeb_stream_stop(m_cubeb_stream) : cubeb_stream_start(m_cubeb_stream); + const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream); if (rv != CUBEB_OK) { - Log_ErrorPrintf("cubeb_stream_%s failed: %d", paused ? "stop" : "start", rv); + Log_ErrorPrintf("Could not %s stream: %d", paused ? "pause" : "resume", rv); return; } m_paused = paused; } -void CubebAudioStream::CloseDevice() -{ - Assert(IsOpen()); - - if (!m_paused) - { - cubeb_stream_stop(m_cubeb_stream); - m_paused = true; - } - - cubeb_stream_destroy(m_cubeb_stream); - m_cubeb_stream = nullptr; - - DestroyContext(); -} - -long CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, - long nframes) -{ - CubebAudioStream* const this_ptr = static_cast(user_ptr); - this_ptr->ReadFrames(reinterpret_cast(output_buffer), static_cast(nframes), false); - return nframes; -} - -void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) {} - -void CubebAudioStream::FramesAvailable() {} - void CubebAudioStream::SetOutputVolume(u32 volume) { - AudioStream::SetOutputVolume(volume); - cubeb_stream_set_volume(m_cubeb_stream, static_cast(m_output_volume) / 100.0f); + if (volume == m_volume) + return; + + int rv = cubeb_stream_set_volume(stream, static_cast(volume) / 100.0f); + if (rv != CUBEB_OK) + { + Log_ErrorPrintf("cubeb_stream_set_volume() failed: %d", rv); + return; + } + + m_volume = volume; } -void CubebAudioStream::DestroyContext() +std::unique_ptr CommonHost::CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, + u32 latency_ms, AudioStretchMode stretch) { - cubeb_destroy(m_cubeb_context); - m_cubeb_context = nullptr; - -#ifdef _WIN32 - if (m_com_initialized_by_us) - CoUninitialize(); -#endif -} - -std::unique_ptr CubebAudioStream::Create() -{ - return std::make_unique(); + std::unique_ptr stream( + std::make_unique(sample_rate, channels, buffer_ms, stretch)); + if (!stream->Initialize(latency_ms)) + stream.reset(); + return stream; } diff --git a/src/frontend-common/cubeb_audio_stream.h b/src/frontend-common/cubeb_audio_stream.h index 5a526b3a7..22b5de266 100644 --- a/src/frontend-common/cubeb_audio_stream.h +++ b/src/frontend-common/cubeb_audio_stream.h @@ -1,34 +1,30 @@ #pragma once -#include "cubeb/cubeb.h" #include "util/audio_stream.h" #include -class CubebAudioStream final : public AudioStream +struct cubeb; +struct cubeb_stream; + +class CubebAudioStream : public AudioStream { public: - CubebAudioStream(); + CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); ~CubebAudioStream(); - static std::unique_ptr Create(); - -protected: - bool IsOpen() const { return m_cubeb_stream != nullptr; } - - bool OpenDevice() override; - void PauseDevice(bool paused) override; - void CloseDevice() override; - void FramesAvailable() override; + void SetPaused(bool paused) override; void SetOutputVolume(u32 volume) override; - void DestroyContext(); + bool Initialize(u32 latency_ms); +private: + static void LogCallback(const char* fmt, ...); static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, long nframes); - static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state); - cubeb* m_cubeb_context = nullptr; - cubeb_stream* m_cubeb_stream = nullptr; - bool m_paused = true; + void DestroyContextAndStream(); + + cubeb* m_context = nullptr; + cubeb_stream* stream = nullptr; #ifdef _WIN32 bool m_com_initialized_by_us = false; diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 9eb7d203a..63d0836e5 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -3152,18 +3152,18 @@ void FullscreenUI::DrawAudioSettingsPage() "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); - DrawIntRangeSetting("Buffer Size", + DrawIntRangeSetting("Latency", "The buffer size determines the size of the chunks of audio which will be pulled by the host.", - "Audio", "BufferSize", Settings::DEFAULT_AUDIO_BUFFER_SIZE, 1024, 8192, "%d Frames"); + "Audio", "Latency", Settings::DEFAULT_AUDIO_BUFFER_MS, 10, 500, "%d ms"); DrawToggleSetting("Sync To Output", "Throttles the emulation speed based on the audio backend pulling audio " "frames. Enable to reduce the chances of crackling.", "Audio", "Sync", true); DrawToggleSetting( - "Resampling", - "When running outside of 100% speed, resamples audio from the target speed instead of dropping frames.", "Audio", - "Resampling", true); + "Time Stretching", + "When running outside of 100% speed, adjusts tempo on audio from the target speed instead of dropping frames.", + "Audio", "TimeStretching", true); EndMenuButtons(); } diff --git a/src/frontend-common/imgui_overlays.cpp b/src/frontend-common/imgui_overlays.cpp index dae5776e8..d9d3b01b2 100644 --- a/src/frontend-common/imgui_overlays.cpp +++ b/src/frontend-common/imgui_overlays.cpp @@ -13,6 +13,7 @@ #include "core/host_display.h" #include "core/host_settings.h" #include "core/settings.h" +#include "core/spu.h" #include "core/system.h" #include "fmt/chrono.h" #include "fmt/format.h" @@ -23,6 +24,7 @@ #include "imgui_internal.h" #include "imgui_manager.h" #include "input_manager.h" +#include "util/audio_stream.h" #include #include #include @@ -172,6 +174,16 @@ void ImGuiManager::DrawPerformanceOverlay() FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime()); DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255)); } + +#if 0 + { + AudioStream* stream = g_spu.GetOutputStream(); + const u32 frames = stream->GetBufferedFramesRelaxed(); + text.Clear(); + text.Fmt("Audio: {:<4u}f/{:<3u}ms", frames, AudioStream::GetMSForBufferSize(stream->GetSampleRate(), frames)); + DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255)); + } +#endif } if (g_settings.display_show_status_indicators) diff --git a/src/frontend-common/sdl_audio_stream.cpp b/src/frontend-common/sdl_audio_stream.cpp index 29c1c0ea4..36ce986ee 100644 --- a/src/frontend-common/sdl_audio_stream.cpp +++ b/src/frontend-common/sdl_audio_stream.cpp @@ -1,11 +1,15 @@ #include "sdl_audio_stream.h" #include "common/assert.h" #include "common/log.h" +#include "common_host.h" #include "sdl_initializer.h" #include Log_SetChannel(SDLAudioStream); -SDLAudioStream::SDLAudioStream() = default; +SDLAudioStream::SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) + : AudioStream(sample_rate, channels, buffer_ms, stretch) +{ +} SDLAudioStream::~SDLAudioStream() { @@ -13,12 +17,16 @@ SDLAudioStream::~SDLAudioStream() SDLAudioStream::CloseDevice(); } -std::unique_ptr SDLAudioStream::Create() +std::unique_ptr CommonHost::CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, + u32 latency_ms, AudioStretchMode stretch) { - return std::make_unique(); + std::unique_ptr stream(std::make_unique(sample_rate, channels, buffer_ms, stretch)); + if (!stream->OpenDevice(latency_ms)) + stream.reset(); + return stream; } -bool SDLAudioStream::OpenDevice() +bool SDLAudioStream::OpenDevice(u32 latency_ms) { DebugAssert(!IsOpen()); @@ -31,22 +39,15 @@ bool SDLAudioStream::OpenDevice() } SDL_AudioSpec spec = {}; - spec.freq = m_output_sample_rate; + spec.freq = m_sample_rate; spec.channels = static_cast(m_channels); spec.format = AUDIO_S16; - spec.samples = static_cast(m_buffer_size); + spec.samples = static_cast(GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms)); spec.callback = AudioCallback; spec.userdata = static_cast(this); SDL_AudioSpec obtained_spec = {}; - -#ifdef SDL_AUDIO_ALLOW_SAMPLES_CHANGE - const u32 allowed_change_flags = SDL_AUDIO_ALLOW_SAMPLES_CHANGE; -#else - const u32 allowed_change_flags = 0; -#endif - - m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, allowed_change_flags); + m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, SDL_AUDIO_ALLOW_SAMPLES_CHANGE); if (m_device_id == 0) { Log_ErrorPrintf("SDL_OpenAudioDevice() failed: %s", SDL_GetError()); @@ -54,25 +55,23 @@ bool SDLAudioStream::OpenDevice() return false; } - if (obtained_spec.samples > spec.samples) - { - Log_WarningPrintf("Requested buffer size %u, got buffer size %u. Adjusting to compensate.", spec.samples, - obtained_spec.samples); + Log_DevPrintf("Requested %u frame buffer, got %u frame buffer", spec.samples, obtained_spec.samples); - if (!SetBufferSize(obtained_spec.samples)) - { - Log_ErrorPrintf("Failed to set new buffer size of %u", obtained_spec.samples); - CloseDevice(); - return false; - } - } + BaseInitialize(); + m_volume = 100; + m_paused = false; + SDL_PauseAudioDevice(m_device_id, 0); return true; } -void SDLAudioStream::PauseDevice(bool paused) +void SDLAudioStream::SetPaused(bool paused) { + if (m_paused == paused) + return; + SDL_PauseAudioDevice(m_device_id, paused ? 1 : 0); + m_paused = paused; } void SDLAudioStream::CloseDevice() @@ -87,7 +86,13 @@ 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; - this_ptr->ReadFrames(reinterpret_cast(stream), num_frames, true); + this_ptr->ReadFrames(reinterpret_cast(stream), num_frames); } -void SDLAudioStream::FramesAvailable() {} +void SDLAudioStream::SetOutputVolume(u32 volume) +{ + if (m_volume == volume) + return; + + Panic("Fixme"); +} diff --git a/src/frontend-common/sdl_audio_stream.h b/src/frontend-common/sdl_audio_stream.h index bd950b64a..601759ff7 100644 --- a/src/frontend-common/sdl_audio_stream.h +++ b/src/frontend-common/sdl_audio_stream.h @@ -5,19 +5,18 @@ class SDLAudioStream final : public AudioStream { public: - SDLAudioStream(); + SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); ~SDLAudioStream(); - static std::unique_ptr Create(); + void SetPaused(bool paused) override; + void SetOutputVolume(u32 volume) override; + + bool OpenDevice(u32 latency_ms); + void CloseDevice(); protected: ALWAYS_INLINE bool IsOpen() const { return (m_device_id != 0); } - bool OpenDevice() override; - void PauseDevice(bool paused) override; - void CloseDevice() override; - void FramesAvailable() override; - static void AudioCallback(void* userdata, uint8_t* stream, int len); u32 m_device_id = 0; diff --git a/src/frontend-common/xaudio2_audio_stream.cpp b/src/frontend-common/xaudio2_audio_stream.cpp index 088630834..138a2487c 100644 --- a/src/frontend-common/xaudio2_audio_stream.cpp +++ b/src/frontend-common/xaudio2_audio_stream.cpp @@ -1,6 +1,7 @@ #include "xaudio2_audio_stream.h" #include "common/assert.h" #include "common/log.h" +#include "common_host.h" #include #include Log_SetChannel(XAudio2AudioStream); @@ -9,12 +10,15 @@ Log_SetChannel(XAudio2AudioStream); #pragma comment(lib, "xaudio2.lib") #endif -XAudio2AudioStream::XAudio2AudioStream() = default; +XAudio2AudioStream::XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch) + : AudioStream(sample_rate, channels, buffer_ms, stretch) +{ +} XAudio2AudioStream::~XAudio2AudioStream() { if (IsOpen()) - XAudio2AudioStream::CloseDevice(); + CloseDevice(); #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) if (m_xaudio2_library) @@ -25,8 +29,20 @@ XAudio2AudioStream::~XAudio2AudioStream() #endif } -bool XAudio2AudioStream::Initialize() +std::unique_ptr CommonHost::CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, + u32 latency_ms, AudioStretchMode stretch) { + std::unique_ptr stream( + std::make_unique(sample_rate, channels, buffer_ms, stretch)); + if (!stream->OpenDevice(latency_ms)) + stream.reset(); + return stream; +} + +bool XAudio2AudioStream::OpenDevice(u32 latency_ms) +{ + DebugAssert(!IsOpen()); + #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) m_xaudio2_library = LoadLibraryW(XAUDIO2_DLL_W); if (!m_xaudio2_library) @@ -36,13 +52,6 @@ bool XAudio2AudioStream::Initialize() } #endif - return true; -} - -bool XAudio2AudioStream::OpenDevice() -{ - DebugAssert(!IsOpen()); - HRESULT hr; #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) using PFNXAUDIO2CREATE = @@ -70,7 +79,7 @@ bool XAudio2AudioStream::OpenDevice() return false; } - hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_channels, m_output_sample_rate, 0, nullptr); + hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_channels, m_sample_rate, 0, nullptr); if (FAILED(hr)) { Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr); @@ -79,10 +88,10 @@ bool XAudio2AudioStream::OpenDevice() WAVEFORMATEX wf = {}; wf.cbSize = sizeof(wf); - wf.nAvgBytesPerSec = m_output_sample_rate * m_channels * sizeof(s16); + wf.nAvgBytesPerSec = m_sample_rate * m_channels * sizeof(s16); wf.nBlockAlign = static_cast(sizeof(s16) * m_channels); wf.nChannels = static_cast(m_channels); - wf.nSamplesPerSec = m_output_sample_rate; + 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); @@ -99,13 +108,27 @@ bool XAudio2AudioStream::OpenDevice() return false; } + m_enqueue_buffer_size = std::max(INTERNAL_BUFFER_SIZE, GetBufferSizeForMS(m_sample_rate, 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_buffers[i] = std::make_unique(m_buffer_size * m_channels); + m_enqueue_buffers[i] = std::make_unique(m_enqueue_buffer_size * m_channels); + BaseInitialize(); + m_volume = 100; + m_paused = false; + + hr = m_source_voice->Start(0, 0); + if (FAILED(hr)) + { + Log_ErrorPrintf("Start() failed: %08X", hr); + return false; + } + + EnqueueBuffer(); return true; } -void XAudio2AudioStream::PauseDevice(bool paused) +void XAudio2AudioStream::SetPaused(bool paused) { if (m_paused == paused) return; @@ -124,6 +147,9 @@ void XAudio2AudioStream::PauseDevice(bool paused) } m_paused = paused; + + if (!m_buffer_enqueued) + EnqueueBuffer(); } void XAudio2AudioStream::CloseDevice() @@ -139,29 +165,20 @@ void XAudio2AudioStream::CloseDevice() m_source_voice = nullptr; m_mastering_voice = nullptr; m_xaudio.Reset(); - m_buffers = {}; + m_enqueue_buffers = {}; m_current_buffer = 0; m_paused = true; } -void XAudio2AudioStream::FramesAvailable() -{ - if (!m_buffer_enqueued) - { - m_buffer_enqueued = true; - EnqueueBuffer(); - } -} - void XAudio2AudioStream::EnqueueBuffer() { - SampleType* samples = m_buffers[m_current_buffer].get(); - ReadFrames(samples, m_buffer_size, false); + SampleType* samples = m_enqueue_buffers[m_current_buffer].get(); + ReadFrames(samples, m_enqueue_buffer_size); const XAUDIO2_BUFFER buf = { - static_cast(0), // flags - static_cast(sizeof(s16) * m_channels * m_buffer_size), // bytes - reinterpret_cast(samples) // data + static_cast(0), // flags + static_cast(sizeof(s16) * m_channels * m_enqueue_buffer_size), // bytes + reinterpret_cast(samples) // data }; HRESULT hr = m_source_voice->SubmitSourceBuffer(&buf, nullptr); @@ -173,10 +190,14 @@ void XAudio2AudioStream::EnqueueBuffer() void XAudio2AudioStream::SetOutputVolume(u32 volume) { - AudioStream::SetOutputVolume(volume); - HRESULT hr = m_mastering_voice->SetVolume(static_cast(m_output_volume) / 100.0f); + HRESULT hr = m_mastering_voice->SetVolume(static_cast(m_volume) / 100.0f); if (FAILED(hr)) + { Log_ErrorPrintf("SetVolume() failed: %08X", hr); + return; + } + + m_volume = volume; } void __stdcall XAudio2AudioStream::OnVoiceProcessingPassStart(UINT32 BytesRequired) {} @@ -195,16 +216,3 @@ void __stdcall XAudio2AudioStream::OnBufferEnd(void* pBufferContext) void __stdcall XAudio2AudioStream::OnLoopEnd(void* pBufferContext) {} void __stdcall XAudio2AudioStream::OnVoiceError(void* pBufferContext, HRESULT Error) {} - -namespace FrontendCommon { - -std::unique_ptr CreateXAudio2AudioStream() -{ - std::unique_ptr stream = std::make_unique(); - if (!stream->Initialize()) - return {}; - - return stream; -} - -} // namespace FrontendCommon \ No newline at end of file diff --git a/src/frontend-common/xaudio2_audio_stream.h b/src/frontend-common/xaudio2_audio_stream.h index ea99127a6..2ee3637a2 100644 --- a/src/frontend-common/xaudio2_audio_stream.h +++ b/src/frontend-common/xaudio2_audio_stream.h @@ -14,45 +14,42 @@ class XAudio2AudioStream final : public AudioStream, private IXAudio2VoiceCallback { public: - XAudio2AudioStream(); + XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); ~XAudio2AudioStream(); - bool Initialize(); - + void SetPaused(bool paused) override; void SetOutputVolume(u32 volume) override; -protected: + bool OpenDevice(u32 latency_ms); + void CloseDevice(); + void EnqueueBuffer(); + +private: enum : u32 { - NUM_BUFFERS = 2 + NUM_BUFFERS = 2, + INTERNAL_BUFFER_SIZE = 512, }; ALWAYS_INLINE bool IsOpen() const { return static_cast(m_xaudio); } - bool OpenDevice() override; - void PauseDevice(bool paused) override; - void CloseDevice() override; - void FramesAvailable() override; - // Inherited via IXAudio2VoiceCallback - virtual void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override; - virtual void __stdcall OnVoiceProcessingPassEnd(void) override; - virtual void __stdcall OnStreamEnd(void) override; - virtual void __stdcall OnBufferStart(void* pBufferContext) override; - virtual void __stdcall OnBufferEnd(void* pBufferContext) override; - virtual void __stdcall OnLoopEnd(void* pBufferContext) override; - virtual void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override; - - void EnqueueBuffer(); + void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override; + void __stdcall OnVoiceProcessingPassEnd(void) override; + void __stdcall OnStreamEnd(void) override; + void __stdcall OnBufferStart(void* pBufferContext) override; + void __stdcall OnBufferEnd(void* pBufferContext) override; + void __stdcall OnLoopEnd(void* pBufferContext) override; + void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override; Microsoft::WRL::ComPtr m_xaudio; IXAudio2MasteringVoice* m_mastering_voice = nullptr; IXAudio2SourceVoice* m_source_voice = nullptr; - std::array, NUM_BUFFERS> m_buffers; + std::array, NUM_BUFFERS> m_enqueue_buffers; + u32 m_enqueue_buffer_size = 0; u32 m_current_buffer = 0; bool m_buffer_enqueued = false; - bool m_paused = true; #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) HMODULE m_xaudio2_library = {}; diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 8cf36ce22..c64ba512a 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -27,8 +27,6 @@ add_library(util iso_reader.h jit_code_buffer.cpp jit_code_buffer.h - null_audio_stream.cpp - null_audio_stream.h memory_arena.cpp memory_arena.h page_fault_handler.cpp @@ -44,4 +42,4 @@ add_library(util target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_link_libraries(util PUBLIC common simpleini) -target_link_libraries(util PRIVATE libchdr samplerate zlib) +target_link_libraries(util PRIVATE libchdr samplerate zlib soundtouch) diff --git a/src/util/audio_stream.cpp b/src/util/audio_stream.cpp index 057eac1e8..a2cff1911 100644 --- a/src/util/audio_stream.cpp +++ b/src/util/audio_stream.cpp @@ -1,387 +1,615 @@ #include "audio_stream.h" -#include "assert.h" +#include "SoundTouch.h" +#include "common/align.h" +#include "common/assert.h" #include "common/log.h" -#include "samplerate.h" +#include "common/make_array.h" +#include "common/timer.h" #include +#include #include Log_SetChannel(AudioStream); -AudioStream::AudioStream() = default; +#if defined(_M_ARM64) +#include +#elif defined(__aarch64__) +#include +#elif defined(_M_IX86) || defined(_M_AMD64) +#include +#endif + +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) +{ +} AudioStream::~AudioStream() { - DestroyResampler(); + DestroyBuffer(); } -bool AudioStream::Reconfigure(u32 input_sample_rate /* = DefaultInputSampleRate */, - u32 output_sample_rate /* = DefaultOutputSampleRate */, u32 channels /* = 1 */, - u32 buffer_size /* = DefaultBufferSize */) +std::unique_ptr AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms) { - std::unique_lock buffer_lock(m_buffer_mutex); - std::unique_lock resampler_Lock(m_resampler_mutex); + return std::unique_ptr(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off)); +} - DestroyResampler(); - if (IsDeviceOpen()) - CloseDevice(); +u32 AudioStream::GetAlignedBufferSize(u32 size) +{ + static_assert(Common::IsPow2(CHUNK_SIZE)); + return Common::AlignUpPow2(size, CHUNK_SIZE); +} - m_output_sample_rate = output_sample_rate; - m_channels = channels; - m_buffer_size = buffer_size; - m_buffer_filling.store(m_wait_for_buffer_fill); - m_output_paused = true; +u32 AudioStream::GetBufferSizeForMS(u32 sample_rate, u32 ms) +{ + return GetAlignedBufferSize((ms * sample_rate) / 1000u); +} - if (!SetBufferSize(buffer_size)) - return false; +u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size) +{ + buffer_size = GetAlignedBufferSize(buffer_size); + return (buffer_size * 1000u) / sample_rate; +} - if (!OpenDevice()) +static constexpr const auto s_stretch_mode_names = make_array("None", "Resample", "TimeStretch"); + +const char* AudioStream::GetStretchModeName(AudioStretchMode mode) +{ + return (static_cast(mode) < s_stretch_mode_names.size()) ? s_stretch_mode_names[static_cast(mode)] : ""; +} + +std::optional AudioStream::ParseStretchMode(const char* name) +{ + for (u8 i = 0; i < static_cast(AudioStretchMode::Count); i++) { - LockedEmptyBuffers(); - m_buffer_size = 0; - m_output_sample_rate = 0; - m_channels = 0; - return false; + if (std::strcmp(name, s_stretch_mode_names[i]) == 0) + return static_cast(i); } - CreateResampler(); - InternalSetInputSampleRate(input_sample_rate); - - return true; + return std::nullopt; } -void AudioStream::SetInputSampleRate(u32 sample_rate) +u32 AudioStream::GetBufferedFramesRelaxed() const { - std::unique_lock buffer_lock(m_buffer_mutex); - std::unique_lock resampler_lock(m_resampler_mutex); - - InternalSetInputSampleRate(sample_rate); + const u32 rpos = m_rpos.load(std::memory_order_relaxed); + const u32 wpos = m_wpos.load(std::memory_order_relaxed); + return (wpos + m_buffer_size - rpos) % m_buffer_size; } -void AudioStream::SetWaitForBufferFill(bool enabled) +void AudioStream::ReadFrames(s16* bData, u32 nFrames) { - std::unique_lock buffer_lock(m_buffer_mutex); - m_wait_for_buffer_fill = enabled; - if (enabled && m_buffer.IsEmpty()) - m_buffer_filling.store(true); + const u32 available_frames = GetBufferedFramesRelaxed(); + u32 frames_to_read = nFrames; + u32 silence_frames = 0; + + if (m_filling) + { + u32 toFill = m_buffer_size / ((m_stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400); + toFill = GetAlignedBufferSize(toFill); + + if (available_frames < toFill) + { + silence_frames = nFrames; + frames_to_read = 0; + } + else + { + m_filling = false; + Log_VerbosePrintf("Underrun compensation done (%d frames buffered)", toFill); + } + } + else if (available_frames < nFrames) + { + silence_frames = nFrames - available_frames; + frames_to_read = available_frames; + m_filling = true; + + if (m_stretch_mode == AudioStretchMode::TimeStretch) + StretchUnderrun(); + } + + if (frames_to_read > 0) + { + u32 rpos = m_rpos.load(std::memory_order_acquire); + + u32 end = m_buffer_size - rpos; + if (end > frames_to_read) + end = frames_to_read; + + // towards the end of the buffer + if (end > 0) + { + std::memcpy(bData, &m_buffer[rpos], sizeof(s32) * end); + rpos += end; + rpos = (rpos == m_buffer_size) ? 0 : rpos; + } + + // after wrapping around + const u32 start = frames_to_read - end; + if (start > 0) + { + std::memcpy(&bData[end * 2], &m_buffer[0], sizeof(s32) * start); + rpos = start; + } + + m_rpos.store(rpos, std::memory_order_release); + } + + // TODO: Bring back the crappy resampler? + if (silence_frames > 0) + std::memset(bData + frames_to_read, 0, sizeof(s32) * silence_frames); } -void AudioStream::InternalSetInputSampleRate(u32 sample_rate) +void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples) { - if (m_input_sample_rate == sample_rate) + const u32 free = m_buffer_size - GetBufferedFramesRelaxed(); + if (free <= nSamples) + { + if (m_stretch_mode == AudioStretchMode::TimeStretch) + { + StretchOverrun(); + } + else + { + Log_DebugPrintf("Buffer overrun, chunk dropped"); + return; + } + } + + u32 wpos = m_wpos.load(std::memory_order_acquire); + + // wrapping around the end of the buffer? + if ((m_buffer_size - wpos) <= nSamples) + { + // needs to be written in two parts + const u32 end = m_buffer_size - wpos; + const u32 start = nSamples - end; + + // start is zero when this chunk reaches exactly the end + std::memcpy(&m_buffer[wpos], bData, end * sizeof(s32)); + if (start > 0) + std::memcpy(&m_buffer[0], bData + end, start * sizeof(s32)); + + wpos = start; + } + else + { + // no split + std::memcpy(&m_buffer[wpos], bData, nSamples * sizeof(s32)); + wpos += nSamples; + } + + m_wpos.store(wpos, std::memory_order_release); +} + +void AudioStream::BaseInitialize() +{ + AllocateBuffer(); + 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); +} + +void AudioStream::DestroyBuffer() +{ + m_buffer.reset(); + m_buffer_size = 0; + m_wpos.store(0, std::memory_order_release); + m_rpos.store(0, std::memory_order_release); +} + +void AudioStream::EmptyBuffer() +{ + if (m_stretch_mode != AudioStretchMode::Off) + { + m_soundtouch->clear(); + if (m_stretch_mode == AudioStretchMode::TimeStretch) + m_soundtouch->setTempo(m_nominal_rate); + } + + m_wpos.store(m_rpos.load(std::memory_order_acquire), std::memory_order_release); +} + +void AudioStream::SetNominalRate(float tempo) +{ + m_nominal_rate = tempo; + if (m_stretch_mode == AudioStretchMode::Resample) + m_soundtouch->setRate(tempo); +} + +void AudioStream::SetStretchMode(AudioStretchMode mode) +{ + if (m_stretch_mode == mode) return; - m_input_sample_rate = sample_rate; - m_resampler_ratio = static_cast(m_output_sample_rate) / static_cast(sample_rate); - src_set_ratio(static_cast(m_resampler_state), m_resampler_ratio); - ResetResampler(); + // can't resize the buffers while paused + bool paused = m_paused; + if (!paused) + SetPaused(true); + + DestroyBuffer(); + StretchDestroy(); + m_stretch_mode = mode; + + AllocateBuffer(); + if (m_stretch_mode != AudioStretchMode::Off) + StretchAllocate(); + + if (!paused) + SetPaused(false); +} + +void AudioStream::SetPaused(bool paused) +{ + m_paused = paused; } void AudioStream::SetOutputVolume(u32 volume) { - std::unique_lock lock(m_buffer_mutex); - m_output_volume = volume; -} - -void AudioStream::PauseOutput(bool paused) -{ - if (m_output_paused == paused) - return; - - PauseDevice(paused); - m_output_paused = paused; - - // Empty buffers on pause. - if (paused) - EmptyBuffers(); -} - -void AudioStream::Shutdown() -{ - if (!IsDeviceOpen()) - return; - - CloseDevice(); - EmptyBuffers(); - m_buffer_size = 0; - m_output_sample_rate = 0; - m_channels = 0; - m_output_paused = true; + m_volume = volume; } void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames) { - m_buffer_mutex.lock(); - - const u32 requested_frames = std::min(*num_frames, m_buffer_size); - EnsureBuffer(requested_frames * m_channels); - - *buffer_ptr = m_buffer.GetWritePointer(); - *num_frames = std::min(m_buffer_size, m_buffer.GetContiguousSpace() / m_channels); + // 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; } void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames) { - Assert(num_frames <= m_buffer_size); - const u32 num_samples = num_frames * m_channels; - { - std::unique_lock lock(m_buffer_mutex); - EnsureBuffer(num_samples); - m_buffer.PushRange(frames, num_samples); - } - - FramesAvailable(); + Panic("not implemented"); } void AudioStream::EndWrite(u32 num_frames) { - m_buffer.AdvanceTail(num_frames * m_channels); - if (m_buffer_filling.load()) - { - if ((m_buffer.GetSize() / m_channels) >= m_buffer_size) - m_buffer_filling.store(false); - } - m_buffer_mutex.unlock(); - FramesAvailable(); -} - -float AudioStream::GetMaxLatency(u32 sample_rate, u32 buffer_size) -{ - return (static_cast(buffer_size) / static_cast(sample_rate)); -} - -bool AudioStream::SetBufferSize(u32 buffer_size) -{ - const u32 buffer_size_in_samples = buffer_size * m_channels; - const u32 max_samples = buffer_size_in_samples * 2u; - if (max_samples > m_buffer.GetCapacity()) - return false; - - m_buffer_size = buffer_size; - m_max_samples = max_samples; - return true; -} - -u32 AudioStream::GetSamplesAvailable() const -{ - // TODO: Use atomic loads - u32 available_samples; - { - std::unique_lock lock(m_buffer_mutex); - available_samples = m_buffer.GetSize(); - } - - return available_samples / m_channels; -} - -u32 AudioStream::GetSamplesAvailableLocked() const -{ - return m_buffer.GetSize() / m_channels; -} - -void AudioStream::ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume) -{ - const u32 total_samples = num_frames * m_channels; - u32 samples_copied = 0; - std::unique_lock buffer_lock(m_buffer_mutex); - if (!m_buffer_filling.load()) - { - if (m_input_sample_rate == m_output_sample_rate) - { - samples_copied = std::min(m_buffer.GetSize(), total_samples); - if (samples_copied > 0) - m_buffer.PopRange(samples, samples_copied); - - ReleaseBufferLock(std::move(buffer_lock)); - } - else - { - if (m_resampled_buffer.GetSize() < total_samples) - ResampleInput(std::move(buffer_lock)); - else - ReleaseBufferLock(std::move(buffer_lock)); - - samples_copied = std::min(m_resampled_buffer.GetSize(), total_samples); - if (samples_copied > 0) - m_resampled_buffer.PopRange(samples, samples_copied); - } - } - else - { - ReleaseBufferLock(std::move(buffer_lock)); - } - - if (samples_copied < total_samples) - { - if (samples_copied > 0) - { - m_resample_buffer.resize(samples_copied); - std::memcpy(m_resample_buffer.data(), samples, sizeof(SampleType) * samples_copied); - - // super basic resampler - spread the input samples evenly across the output samples. will sound like ass and have - // aliasing, but better than popping by inserting silence. - const u32 increment = - static_cast(65536.0f * (static_cast(samples_copied / m_channels) / static_cast(num_frames))); - - SampleType* out_ptr = samples; - const SampleType* resample_ptr = m_resample_buffer.data(); - const u32 copy_stride = sizeof(SampleType) * m_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; - - resample_subpos += increment; - resample_ptr += (resample_subpos >> 16) * m_channels; - resample_subpos %= 65536u; - } - - Log_VerbosePrintf("Audio buffer underflow, resampled %u frames to %u", samples_copied / m_channels, num_frames); - m_underflow_flag.store(true); - } - else - { - // read nothing, so zero-fill - std::memset(samples, 0, sizeof(SampleType) * total_samples); - Log_VerbosePrintf("Audio buffer underflow with no samples, added %u frames silence", num_frames); - m_underflow_flag.store(true); - } - - m_buffer_filling.store(m_wait_for_buffer_fill); - } - - if (apply_volume && m_output_volume != FullVolume) - { - SampleType* current_ptr = samples; - const SampleType* end_ptr = samples + (num_frames * m_channels); - while (current_ptr != end_ptr) - { - *current_ptr = ApplyVolume(*current_ptr, m_output_volume); - current_ptr++; - } - } -} - -void AudioStream::EnsureBuffer(u32 size) -{ - DebugAssert(size <= (m_buffer_size * m_channels)); - if (GetBufferSpace() >= size) + // don't bother committing anything when muted + if (m_volume == 0) return; - if (m_sync) - { - std::unique_lock lock(m_buffer_mutex, std::adopt_lock); - m_buffer_draining_cv.wait(lock, [this, size]() { return GetBufferSpace() >= size; }); - lock.release(); - } + 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(_M_ARM64) || defined(__aarch64__) + +static void S16ChunkToFloat(const s32* src, float* dst) +{ + 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); + + for (u32 i = 0; i < iterations; i++) { - m_buffer.Remove(size); + const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src)); + src += 4; + + 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] + iv1 = vshrq_n_s32(iv1, 16); // [0, 1, 2, 3] + iv2 = vshrq_n_s32(iv2, 16); // [4, 5, 6, 7] + float32x4_t fv1 = vcvtq_f32_s32(iv1); // [f0, f1, f2, f3] + float32x4_t fv2 = vcvtq_f32_s32(iv2); // [f4, f5, f6, f7] + fv1 = vmulq_f32(fv1, S16_TO_FLOAT_V); + fv2 = vmulq_f32(fv2, S16_TO_FLOAT_V); + + vst1q_f32(dst + 0, fv1); + vst1q_f32(dst + 4, fv2); + dst += 8; } } -void AudioStream::DropFrames(u32 count) +static void FloatChunkToS16(s32* dst, const float* src, uint size) { - std::unique_lock lock(m_buffer_mutex); - m_buffer.Remove(count); -} + static_assert((AudioStream::CHUNK_SIZE % 4) == 0); + constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; -void AudioStream::EmptyBuffers() -{ - std::unique_lock lock(m_buffer_mutex); - std::unique_lock resampler_lock(m_resampler_mutex); - LockedEmptyBuffers(); -} + const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16); -void AudioStream::LockedEmptyBuffers() -{ - m_buffer.Clear(); - m_underflow_flag.store(false); - m_buffer_filling.store(m_wait_for_buffer_fill); - ResetResampler(); -} - -void AudioStream::CreateResampler() -{ - m_resampler_state = src_new(SRC_SINC_MEDIUM_QUALITY, static_cast(m_channels), nullptr); - if (!m_resampler_state) - Panic("Failed to allocate resampler"); -} - -void AudioStream::DestroyResampler() -{ - if (m_resampler_state) + for (u32 i = 0; i < iterations; i++) { - src_delete(static_cast(m_resampler_state)); - m_resampler_state = nullptr; + float32x4_t fv1 = vld1q_s32(src + 0); + float32x4_t fv2 = vld1q_s32(src + 4); + src += 8; + + fv1 = vmulq_f32(fv1, FLOAT_TO_S16_V); + fv2 = vmulq_f32(fv2, FLOAT_TO_S16_V); + int32x4_t iv1 = vcvtq_s32_f32(fv1); + int32x4_t iv2 = vcvtq_s32_f32(fv2); + + int16x8_t iv = vcombine_s16(vqmovn_s32(iv1), vqmovn_s32(iv2)); + vst1q_s32(dst, vreinterpretq_s32_s16(iv)); + dst += 4; } } -void AudioStream::ResetResampler() +#elif defined(_M_IX86) || defined(_M_AMD64) + +static void S16ChunkToFloat(const s32* src, float* dst) { - m_resampled_buffer.Clear(); - m_resample_in_buffer.clear(); - m_resample_out_buffer.clear(); - src_reset(static_cast(m_resampler_state)); + 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); + + for (u32 i = 0; i < iterations; i++) + { + const __m128i sv = _mm_load_si128(reinterpret_cast(src)); + src += 4; + + __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] + iv1 = _mm_srai_epi32(iv1, 16); // [0, 1, 2, 3] + iv2 = _mm_srai_epi32(iv2, 16); // [4, 5, 6, 7] + __m128 fv1 = _mm_cvtepi32_ps(iv1); // [f0, f1, f2, f3] + __m128 fv2 = _mm_cvtepi32_ps(iv2); // [f4, f5, f6, f7] + fv1 = _mm_mul_ps(fv1, S16_TO_FLOAT_V); + fv2 = _mm_mul_ps(fv2, S16_TO_FLOAT_V); + + _mm_store_ps(dst + 0, fv1); + _mm_store_ps(dst + 4, fv2); + dst += 8; + } } -void AudioStream::ResampleInput(std::unique_lock buffer_lock) +static void FloatChunkToS16(s32* dst, const float* src, uint size) { - std::unique_lock resampler_lock(m_resampler_mutex); + static_assert((AudioStream::CHUNK_SIZE % 4) == 0); + constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4; - const u32 input_space_from_output = (m_resampled_buffer.GetSpace() * m_output_sample_rate) / m_input_sample_rate; - u32 remaining = std::min(m_buffer.GetSize(), input_space_from_output); - if (m_resample_in_buffer.size() < remaining) + const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16); + + for (u32 i = 0; i < iterations; i++) { - remaining -= static_cast(m_resample_in_buffer.size()); - m_resample_in_buffer.reserve(m_resample_in_buffer.size() + remaining); - while (remaining > 0) + __m128 fv1 = _mm_load_ps(src + 0); + __m128 fv2 = _mm_load_ps(src + 4); + src += 8; + + fv1 = _mm_mul_ps(fv1, FLOAT_TO_S16_V); + fv2 = _mm_mul_ps(fv2, FLOAT_TO_S16_V); + __m128i iv1 = _mm_cvtps_epi32(fv1); + __m128i iv2 = _mm_cvtps_epi32(fv2); + + __m128i iv = _mm_packs_epi32(iv1, iv2); + _mm_store_si128(reinterpret_cast<__m128i*>(dst), iv); + dst += 4; + } +} + +#else + +static void S16ChunkToFloat(const s32* src, float* dst) +{ + 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++; + } +} + +static void FloatChunkToS16(s32* dst, const float* src, uint size) +{ + 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); + } +} +#endif + +// Time stretching algorithm based on PCSX2 implementation. + +template +ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max) +{ + return (min <= val && val <= max); +} + +void AudioStream::StretchAllocate() +{ + if (m_stretch_mode == AudioStretchMode::Off) + return; + + m_soundtouch = std::make_unique(); + m_soundtouch->setSampleRate(m_sample_rate); + m_soundtouch->setChannels(m_channels); + + m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, 0); + m_soundtouch->setSetting(SETTING_USE_AA_FILTER, 0); + + m_soundtouch->setSetting(SETTING_SEQUENCE_MS, 30); + m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, 20); + m_soundtouch->setSetting(SETTING_OVERLAP_MS, 10); + + if (m_stretch_mode == AudioStretchMode::Resample) + m_soundtouch->setRate(m_nominal_rate); + else + m_soundtouch->setTempo(m_nominal_rate); + + m_stretch_reset = STRETCH_RESET_THRESHOLD; + m_stretch_inactive = false; + m_stretch_ok_count = 0; + m_dynamic_target_usage = 0.0f; + m_average_position = 0; + m_average_available = 0; + + m_staging_buffer_pos = 0; +} + +void AudioStream::StretchDestroy() +{ + m_soundtouch.reset(); +} + +void AudioStream::StretchWrite() +{ + 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) + { + FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress); + InternalWriteFrames(m_staging_buffer.data(), tempProgress); + } + + if (m_stretch_mode == AudioStretchMode::TimeStretch) + UpdateStretchTempo(); +} + +float AudioStream::AddAndGetAverageTempo(float val) +{ + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + m_average_available = 0; + if (m_average_available < AVERAGING_BUFFER_SIZE) + m_average_available++; + + m_average_fullness[m_average_position] = val; + m_average_position = (m_average_position + 1U) % AVERAGING_BUFFER_SIZE; + + const u32 actual_window = std::min(m_average_available, AVERAGING_WINDOW); + const u32 first_index = (m_average_position - actual_window + AVERAGING_BUFFER_SIZE) % AVERAGING_BUFFER_SIZE; + + float sum = 0; + for (u32 i = first_index; i < first_index + actual_window; i++) + sum += m_average_fullness[i % AVERAGING_BUFFER_SIZE]; + sum = sum / actual_window; + + return (sum != 0.0f) ? sum : 1.0f; +} + +void AudioStream::UpdateStretchTempo() +{ + static constexpr float MIN_TEMPO = 0.05f; + static constexpr float MAX_TEMPO = 50.0f; + + // Which range we will run in 1:1 mode for. + static constexpr float INACTIVE_GOOD_FACTOR = 1.04f; + static constexpr float INACTIVE_BAD_FACTOR = 1.2f; + static constexpr u32 INACTIVE_MIN_OK_COUNT = 50; + static constexpr u32 COMPENSATION_DIVIDER = 100; + + float base_target_usage = static_cast(m_target_buffer_size) * m_nominal_rate; + + // state vars + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + { + Log_VerbosePrintf("___ Stretcher is being reset."); + m_stretch_inactive = false; + m_stretch_ok_count = 0; + m_dynamic_target_usage = base_target_usage; + } + + const u32 ibuffer_usage = GetBufferedFramesRelaxed(); + float buffer_usage = static_cast(ibuffer_usage); + float tempo = buffer_usage / m_dynamic_target_usage; + tempo = AddAndGetAverageTempo(tempo); + + // Dampening when we get close to target. + if (tempo < 2.0f) + tempo = std::sqrt(tempo); + + tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO); + + if (tempo < 1.0f) + base_target_usage /= std::sqrt(tempo); + + m_dynamic_target_usage += + static_cast(base_target_usage / tempo - m_dynamic_target_usage) / static_cast(COMPENSATION_DIVIDER); + if (IsInRange(tempo, 0.9f, 1.1f) && + IsInRange(m_dynamic_target_usage, base_target_usage * 0.9f, base_target_usage * 1.1f)) + { + m_dynamic_target_usage = base_target_usage; + } + + if (!m_stretch_inactive) + { + if (IsInRange(tempo, 1.0f / INACTIVE_GOOD_FACTOR, INACTIVE_GOOD_FACTOR)) + m_stretch_ok_count++; + else + m_stretch_ok_count = 0; + + if (m_stretch_ok_count >= INACTIVE_MIN_OK_COUNT) { - const u32 read_len = std::min(m_buffer.GetContiguousSize(), remaining); - const size_t old_pos = m_resample_in_buffer.size(); - m_resample_in_buffer.resize(m_resample_in_buffer.size() + read_len); - src_short_to_float_array(m_buffer.GetReadPointer(), m_resample_in_buffer.data() + old_pos, - static_cast(read_len)); - m_buffer.Remove(read_len); - remaining -= read_len; + Log_VerbosePrintf("=== Stretcher is now inactive."); + m_stretch_inactive = true; } } - - ReleaseBufferLock(std::move(buffer_lock)); - - const u32 potential_output_size = - (static_cast(m_resample_in_buffer.size()) * m_input_sample_rate) / m_output_sample_rate; - const u32 output_size = std::min(potential_output_size, m_resampled_buffer.GetSpace()); - m_resample_out_buffer.resize(output_size); - - SRC_DATA sd = {}; - sd.data_in = m_resample_in_buffer.data(); - sd.data_out = m_resample_out_buffer.data(); - sd.input_frames = static_cast(m_resample_in_buffer.size()) / m_channels; - sd.output_frames = output_size / m_channels; - sd.src_ratio = m_resampler_ratio; - - const int error = src_process(static_cast(m_resampler_state), &sd); - if (error) + else if (!IsInRange(tempo, 1.0f / INACTIVE_BAD_FACTOR, INACTIVE_BAD_FACTOR)) { - Log_ErrorPrintf("Resampler error %d", error); - m_resample_in_buffer.clear(); - m_resample_out_buffer.clear(); - return; + Log_VerbosePrintf("~~~ Stretcher is now active @ tempo %f.", tempo); + m_stretch_inactive = false; + m_stretch_ok_count = 0; } - m_resample_in_buffer.erase(m_resample_in_buffer.begin(), - m_resample_in_buffer.begin() + (static_cast(sd.input_frames_used) * m_channels)); + if (m_stretch_inactive) + tempo = m_nominal_rate; - const float* write_ptr = m_resample_out_buffer.data(); - remaining = static_cast(sd.output_frames_gen) * m_channels; - while (remaining > 0) + if constexpr (LOG_TIMESTRETCH_STATS) { - const u32 samples_to_write = std::min(m_resampled_buffer.GetContiguousSpace(), remaining); - src_float_to_short_array(write_ptr, m_resampled_buffer.GetWritePointer(), static_cast(samples_to_write)); - m_resampled_buffer.AdvanceTail(samples_to_write); - write_ptr += samples_to_write; - remaining -= samples_to_write; + static int iterations = 0; + static u64 last_log_time = 0; + + const u64 now = Common::Timer::GetCurrentValue(); + + if (Common::Timer::ConvertValueToSeconds(now - last_log_time) > 1.0f) + { + Log_VerbosePrintf("buffers: %4u ms (%3.0f%%), tempo: %f, comp: %2.3f, iters: %d, reset:%d", + (ibuffer_usage * 1000u) / m_sample_rate, 100.0f * buffer_usage / base_target_usage, tempo, + m_dynamic_target_usage / base_target_usage, iterations, m_stretch_reset); + + last_log_time = now; + iterations = 0; + } + + iterations++; } - m_resample_out_buffer.erase(m_resample_out_buffer.begin(), - m_resample_out_buffer.begin() + (static_cast(sd.output_frames_gen) * m_channels)); -} \ No newline at end of file + + m_soundtouch->setTempo(tempo); + + if (m_stretch_reset >= STRETCH_RESET_THRESHOLD) + m_stretch_reset = 0; +} + +void AudioStream::StretchUnderrun() +{ + // Didn't produce enough frames in time. + m_stretch_reset++; +} + +void AudioStream::StretchOverrun() +{ + // Produced more frames than can fit in the buffer. + m_stretch_reset++; + + // Drop two packets to give the time stretcher a bit more time to slow things down. + const u32 discard = CHUNK_SIZE * 2; + m_rpos.store((m_rpos.load(std::memory_order_acquire) + discard) % m_buffer_size, std::memory_order_release); +} diff --git a/src/util/audio_stream.h b/src/util/audio_stream.h index c5cb8a22d..b2d9802be 100644 --- a/src/util/audio_stream.h +++ b/src/util/audio_stream.h @@ -1,13 +1,26 @@ #pragma once -#include "common/fifo_queue.h" #include "common/types.h" +#include #include -#include #include -#include -#include +#include -// Uses signed 16-bits samples. +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4324) // warning C4324: structure was padded due to alignment specifier +#endif + +namespace soundtouch { +class SoundTouch; +} + +enum class AudioStretchMode : u8 +{ + Off, + Resample, + TimeStretch, + Count +}; class AudioStream { @@ -16,111 +29,116 @@ public: enum : u32 { - DefaultInputSampleRate = 44100, - DefaultOutputSampleRate = 44100, - DefaultBufferSize = 2048, - MaxSamples = 32768, - FullVolume = 100 + CHUNK_SIZE = 64, + MAX_CHANNELS = 2 }; - AudioStream(); +public: virtual ~AudioStream(); - u32 GetOutputSampleRate() const { return m_output_sample_rate; } - u32 GetChannels() const { return m_channels; } - u32 GetBufferSize() const { return m_buffer_size; } - s32 GetOutputVolume() const { return m_output_volume; } - bool IsSyncing() const { return m_sync; } + static u32 GetAlignedBufferSize(u32 size); + static u32 GetBufferSizeForMS(u32 sample_rate, u32 ms); + static u32 GetMSForBufferSize(u32 sample_rate, u32 buffer_size); - bool Reconfigure(u32 input_sample_rate = DefaultInputSampleRate, u32 output_sample_rate = DefaultOutputSampleRate, - u32 channels = 1, u32 buffer_size = DefaultBufferSize); - void SetSync(bool enable) { m_sync = enable; } + static const char* GetStretchModeName(AudioStretchMode mode); + static std::optional ParseStretchMode(const char* name); - void SetInputSampleRate(u32 sample_rate); - void SetWaitForBufferFill(bool enabled); + ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } + ALWAYS_INLINE u32 GetChannels() const { return m_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; } + ALWAYS_INLINE float GetNominalTempo() const { return m_nominal_rate; } + ALWAYS_INLINE bool IsPaused() const { return m_paused; } + + u32 GetBufferedFramesRelaxed() const; + + /// Temporarily pauses the stream, preventing it from requesting data. + virtual void SetPaused(bool paused); virtual void SetOutputVolume(u32 volume); - void PauseOutput(bool paused); - void EmptyBuffers(); - - void Shutdown(); - void BeginWrite(SampleType** buffer_ptr, u32* num_frames); void WriteFrames(const SampleType* frames, u32 num_frames); void EndWrite(u32 num_frames); - bool DidUnderflow() - { - bool expected = true; - return m_underflow_flag.compare_exchange_strong(expected, false); - } + void EmptyBuffer(); - static std::unique_ptr CreateNullAudioStream(); + /// Nominal rate is used for both resampling and timestretching, input samples are assumed to be this amount faster + /// than the sample rate. + void SetNominalRate(float tempo); - // Latency computation - returns values in seconds - static float GetMaxLatency(u32 sample_rate, u32 buffer_size); + void SetStretchMode(AudioStretchMode mode); + + static std::unique_ptr CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms); protected: - virtual bool OpenDevice() = 0; - virtual void PauseDevice(bool paused) = 0; - virtual void CloseDevice() = 0; - virtual void FramesAvailable() = 0; + AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch); + void BaseInitialize(); - ALWAYS_INLINE static SampleType ApplyVolume(SampleType sample, u32 volume) - { - return s16((s32(sample) * s32(volume)) / 100); - } + void ReadFrames(s16* bData, u32 nSamples); - ALWAYS_INLINE u32 GetBufferSpace() const { return (m_max_samples - m_buffer.GetSize()); } - ALWAYS_INLINE void ReleaseBufferLock(std::unique_lock lock) - { - // lock is released implicitly by destruction - m_buffer_draining_cv.notify_one(); - } - - bool SetBufferSize(u32 buffer_size); - bool IsDeviceOpen() const { return (m_output_sample_rate > 0); } - - void EnsureBuffer(u32 size); - void LockedEmptyBuffers(); - u32 GetSamplesAvailable() const; - u32 GetSamplesAvailableLocked() const; - void ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume); - void DropFrames(u32 count); - - void CreateResampler(); - void DestroyResampler(); - void ResetResampler(); - void InternalSetInputSampleRate(u32 sample_rate); - void ResampleInput(std::unique_lock buffer_lock); - - u32 m_input_sample_rate = 0; - u32 m_output_sample_rate = 0; + u32 m_sample_rate = 0; u32 m_channels = 0; + u32 m_buffer_ms = 0; + u32 m_volume = 0; + + AudioStretchMode m_stretch_mode = AudioStretchMode::Off; + 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, + }; + + void AllocateBuffer(); + void DestroyBuffer(); + + void InternalWriteFrames(s32* bData, u32 nFrames); + + void StretchAllocate(); + void StretchDestroy(); + void StretchWrite(); + void StretchUnderrun(); + void StretchOverrun(); + + float AddAndGetAverageTempo(float val); + void UpdateStretchTempo(); + u32 m_buffer_size = 0; + std::unique_ptr m_buffer; - // volume, 0-100 - u32 m_output_volume = FullVolume; + std::atomic m_rpos{0}; + std::atomic m_wpos{0}; - HeapFIFOQueue m_buffer; - mutable std::mutex m_buffer_mutex; - std::condition_variable m_buffer_draining_cv; - std::vector m_resample_buffer; + std::unique_ptr m_soundtouch; - std::atomic_bool m_underflow_flag{false}; - std::atomic_bool m_buffer_filling{false}; - u32 m_max_samples = 0; + u32 m_target_buffer_size = 0; + u32 m_stretch_reset = STRETCH_RESET_THRESHOLD; - bool m_output_paused = true; - bool m_sync = true; - bool m_wait_for_buffer_fill = false; + u32 m_stretch_ok_count = 0; + float m_nominal_rate = 1.0f; + float m_dynamic_target_usage = 0.0f; - // Resampling - double m_resampler_ratio = 1.0; - void* m_resampler_state = nullptr; - std::mutex m_resampler_mutex; - HeapFIFOQueue m_resampled_buffer; - std::vector m_resample_in_buffer; - std::vector m_resample_out_buffer; -}; \ No newline at end of file + u32 m_average_position = 0; + u32 m_average_available = 0; + u32 m_staging_buffer_pos = 0; + + std::array m_average_fullness = {}; + + // temporary staging buffer, used for timestretching + alignas(16) std::array m_staging_buffer; + + // float buffer, soundtouch only accepts float samples as input + alignas(16) std::array m_float_buffer; +}; + +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/src/util/null_audio_stream.cpp b/src/util/null_audio_stream.cpp deleted file mode 100644 index 3c9c81a92..000000000 --- a/src/util/null_audio_stream.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "null_audio_stream.h" - -NullAudioStream::NullAudioStream() = default; - -NullAudioStream::~NullAudioStream() = default; - -bool NullAudioStream::OpenDevice() -{ - return true; -} - -void NullAudioStream::PauseDevice(bool paused) {} - -void NullAudioStream::CloseDevice() {} - -void NullAudioStream::FramesAvailable() -{ - // drop any buffer as soon as they're available - DropFrames(GetSamplesAvailable()); -} - -std::unique_ptr AudioStream::CreateNullAudioStream() -{ - return std::make_unique(); -} diff --git a/src/util/null_audio_stream.h b/src/util/null_audio_stream.h deleted file mode 100644 index 55ce9a3f8..000000000 --- a/src/util/null_audio_stream.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "audio_stream.h" - -class NullAudioStream final : public AudioStream -{ -public: - NullAudioStream(); - ~NullAudioStream(); - -protected: - bool OpenDevice() override; - void PauseDevice(bool paused) override; - void CloseDevice() override; - void FramesAvailable() override; -}; diff --git a/src/util/util.props b/src/util/util.props index 641511f68..5aba0b6d8 100644 --- a/src/util/util.props +++ b/src/util/util.props @@ -4,13 +4,15 @@ - $(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories) + %(PreprocessorDefinitions);SOUNDTOUCH_FLOAT_SAMPLES;SOUNDTOUCH_ALLOW_SSE + %(PreprocessorDefinitions);SOUNDTOUCH_USE_NEON + $(SolutionDir)dep\soundtouch\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories) - $(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;$(RootBuildDir)libsamplerate\libsamplerate.lib;%(AdditionalDependencies) + $(RootBuildDir)soundtouch\soundtouch.lib;$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;%(AdditionalDependencies) diff --git a/src/util/util.vcxproj b/src/util/util.vcxproj index 135c3362a..13173f653 100644 --- a/src/util/util.vcxproj +++ b/src/util/util.vcxproj @@ -9,7 +9,6 @@ - @@ -38,7 +37,6 @@ - diff --git a/src/util/util.vcxproj.filters b/src/util/util.vcxproj.filters index d2f5a56d3..adcb7d33d 100644 --- a/src/util/util.vcxproj.filters +++ b/src/util/util.vcxproj.filters @@ -8,7 +8,6 @@ - @@ -28,7 +27,6 @@ -