diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 6c1ccc2463..a5db7b76c1 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -57,9 +57,11 @@ target_sources(pcsx2-qt PRIVATE Settings/AdvancedSettingsWidget.cpp Settings/AdvancedSettingsWidget.h Settings/AdvancedSettingsWidget.ui + Settings/AudioExpansionSettingsDialog.ui Settings/AudioSettingsWidget.cpp Settings/AudioSettingsWidget.h Settings/AudioSettingsWidget.ui + Settings/AudioStretchSettingsDialog.ui Settings/BIOSSettingsWidget.cpp Settings/BIOSSettingsWidget.h Settings/BIOSSettingsWidget.ui diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index eaade67454..24bfbc2b98 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -29,6 +29,7 @@ #include "pcsx2/Input/InputManager.h" #include "pcsx2/MTGS.h" #include "pcsx2/PerformanceMetrics.h" +#include "pcsx2/SPU2/spu2.h" #include "pcsx2/VMManager.h" #include "common/Assertions.h" @@ -889,6 +890,38 @@ void EmuThread::endCapture() MTGS::RunOnGSThread(&GSEndCapture); } +void EmuThread::setAudioOutputVolume(int volume, int fast_forward_volume) +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, "setAudioOutputVolume", Qt::QueuedConnection, Q_ARG(int, volume), + Q_ARG(int, fast_forward_volume)); + return; + } + + if (!VMManager::HasValidVM()) + return; + + EmuConfig.SPU2.OutputVolume = static_cast(volume); + EmuConfig.SPU2.FastForwardVolume = static_cast(fast_forward_volume); + SPU2::SetOutputVolume(SPU2::GetResetVolume()); +} + +void EmuThread::setAudioOutputMuted(bool muted) +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, "setAudioOutputMuted", Qt::QueuedConnection, Q_ARG(bool, muted)); + return; + } + + if (!VMManager::HasValidVM()) + return; + + EmuConfig.SPU2.OutputMuted = muted; + SPU2::SetOutputVolume(SPU2::GetResetVolume()); +} + std::optional EmuThread::acquireRenderWindow(bool recreate_window) { // Check if we're wanting to get exclusive fullscreen. This should be safe to read, since we're going to be calling from the GS thread. diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index 05fba5a673..c8ce4c7a8e 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -112,6 +112,8 @@ public Q_SLOTS: void queueSnapshot(quint32 gsdump_frames); void beginCapture(const QString& path); void endCapture(); + void setAudioOutputVolume(int volume, int fast_forward_volume); + void setAudioOutputMuted(bool muted); Q_SIGNALS: bool messageConfirmed(const QString& title, const QString& message); diff --git a/pcsx2-qt/QtUtils.cpp b/pcsx2-qt/QtUtils.cpp index 15c8b6523b..ea30d51dda 100644 --- a/pcsx2-qt/QtUtils.cpp +++ b/pcsx2-qt/QtUtils.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #include "QtUtils.h" @@ -14,11 +14,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -202,6 +204,15 @@ namespace QtUtils } } + 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/pcsx2-qt/QtUtils.h b/pcsx2-qt/QtUtils.h index 6818ee6f93..8a47709e2c 100644 --- a/pcsx2-qt/QtUtils.h +++ b/pcsx2-qt/QtUtils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #pragma once @@ -20,7 +20,9 @@ class QAction; class QComboBox; class QFileInfo; class QFrame; +class QLabel; class QKeyEvent; +class QSlider; class QTableView; class QTreeView; class QVariant; @@ -71,6 +73,9 @@ namespace QtUtils /// 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/pcsx2-qt/Settings/AudioExpansionSettingsDialog.ui b/pcsx2-qt/Settings/AudioExpansionSettingsDialog.ui new file mode 100644 index 0000000000..45a5c0a282 --- /dev/null +++ b/pcsx2-qt/Settings/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/pcsx2-qt/Settings/AudioSettingsWidget.cpp b/pcsx2-qt/Settings/AudioSettingsWidget.cpp index c87d777159..e047217e26 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.cpp +++ b/pcsx2-qt/Settings/AudioSettingsWidget.cpp @@ -1,29 +1,22 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include -#include - -#include "pcsx2/SPU2/Global.h" -#include "pcsx2/SPU2/spu2.h" -#include "pcsx2/VMManager.h" - #include "AudioSettingsWidget.h" #include "QtHost.h" #include "QtUtils.h" #include "SettingWidgetBinder.h" #include "SettingsWindow.h" -static constexpr s32 DEFAULT_SYNCHRONIZATION_MODE = 0; -static constexpr s32 DEFAULT_EXPANSION_MODE = 0; -static constexpr s32 DEFAULT_DPL_DECODING_LEVEL = 0; -static const char* DEFAULT_OUTPUT_MODULE = "cubeb"; -static constexpr s32 DEFAULT_TARGET_LATENCY = 60; -static constexpr s32 DEFAULT_OUTPUT_LATENCY = 20; -static constexpr s32 DEFAULT_VOLUME = 100; -static constexpr s32 DEFAULT_SOUNDTOUCH_SEQUENCE_LENGTH = 30; -static constexpr s32 DEFAULT_SOUNDTOUCH_SEEK_WINDOW = 20; -static constexpr s32 DEFAULT_SOUNDTOUCH_OVERLAP = 10; +#include "ui_AudioExpansionSettingsDialog.h" +#include "ui_AudioStretchSettingsDialog.h" + +#include "pcsx2/Host/AudioStream.h" +#include "pcsx2/SPU2/spu2.h" +#include "pcsx2/VMManager.h" + +#include +#include +#include AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent) : QWidget(parent) @@ -32,317 +25,481 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent SettingsInterface* sif = dialog->getSettingsInterface(); m_ui.setupUi(this); - populateOutputModules(); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.syncMode, "SPU2/Output", "SynchMode", DEFAULT_SYNCHRONIZATION_MODE); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.expansionMode, "SPU2/Output", "SpeakerConfiguration", DEFAULT_EXPANSION_MODE); - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.dplLevel, "SPU2/Output", "DplDecodingLevel", DEFAULT_DPL_DECODING_LEVEL); - connect(m_ui.syncMode, QOverload::of(&QComboBox::currentIndexChanged), this, &AudioSettingsWidget::updateTargetLatencyRange); - connect(m_ui.expansionMode, QOverload::of(&QComboBox::currentIndexChanged), this, &AudioSettingsWidget::expansionModeChanged); - updateTargetLatencyRange(); - expansionModeChanged(); + for (u32 i = 0; i < static_cast(AudioBackend::Count); i++) + m_ui.audioBackend->addItem(QString::fromUtf8(AudioStream::GetBackendDisplayName(static_cast(i)))); - SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.outputModule, "SPU2/Output", "OutputModule", DEFAULT_OUTPUT_MODULE); - SettingWidgetBinder::BindWidgetAndLabelToIntSetting( - //: Measuring unit that will appear after the number selected in its option. Adapt the space depending on your language's rules. - sif, m_ui.targetLatency, m_ui.targetLatencyLabel, tr(" ms"), "SPU2/Output", "Latency", DEFAULT_TARGET_LATENCY); - SettingWidgetBinder::BindWidgetAndLabelToIntSetting( - sif, m_ui.outputLatency, m_ui.outputLatencyLabel, tr(" ms"), "SPU2/Output", "OutputLatency", DEFAULT_OUTPUT_LATENCY); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.outputLatencyMinimal, "SPU2/Output", "OutputLatencyMinimal", false); - connect(m_ui.outputModule, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::outputModuleChanged); - connect(m_ui.backend, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::outputBackendChanged); - connect(m_ui.targetLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels); - connect(m_ui.outputLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels); - connect(m_ui.outputLatencyMinimal, &QCheckBox::checkStateChanged, this, &AudioSettingsWidget::updateLatencyLabels); - connect(m_ui.outputLatencyMinimal, &QCheckBox::checkStateChanged, this, &AudioSettingsWidget::onMinimalOutputLatencyStateChanged); - outputModuleChanged(); - - m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME)); - connect(m_ui.volume, &QSlider::valueChanged, this, &AudioSettingsWidget::volumeChanged); - updateVolumeLabel(); - if (dialog->isPerGameSettings()) + for (u32 i = 0; i < static_cast(AudioExpansionMode::Count); i++) { - connect(m_ui.volume, &QSlider::customContextMenuRequested, this, &AudioSettingsWidget::volumeContextMenuRequested); - m_ui.volume->setContextMenuPolicy(Qt::CustomContextMenu); - if (sif->ContainsValue("SPU2/Mixing", "FinalVolume")) - { - QFont bold_font(m_ui.volume->font()); - bold_font.setBold(true); - m_ui.volumeLabel->setFont(bold_font); - } + m_ui.expansionMode->addItem( + QString::fromUtf8(AudioStream::GetExpansionModeDisplayName(static_cast(i)))); } - SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.sequenceLength, m_ui.sequenceLengthLabel, tr(" ms"), "Soundtouch", - "SequenceLengthMS", DEFAULT_SOUNDTOUCH_SEQUENCE_LENGTH); - SettingWidgetBinder::BindWidgetAndLabelToIntSetting( - sif, m_ui.seekWindowSize, m_ui.seekWindowSizeLabel, tr(" ms"), "Soundtouch", "SeekWindowMS", DEFAULT_SOUNDTOUCH_SEEK_WINDOW); - SettingWidgetBinder::BindWidgetAndLabelToIntSetting( - sif, m_ui.overlap, m_ui.overlapLabel, tr(" ms"), "Soundtouch", "OverlapMS", DEFAULT_SOUNDTOUCH_OVERLAP); - connect(m_ui.resetTimestretchDefaults, &QPushButton::clicked, this, &AudioSettingsWidget::resetTimestretchDefaults); + for (u32 i = 0; i < static_cast(Pcsx2Config::SPU2Options::SPU2SyncMode::Count); i++) + { + m_ui.syncMode->addItem( + QString::fromUtf8(Pcsx2Config::SPU2Options::GetSyncModeDisplayName( + static_cast(i)))); + } - m_ui.label_3b->setVisible(false); - m_ui.dplLevel->setVisible(false); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "SPU2/Output", "Backend", + &AudioStream::ParseBackendName, &AudioStream::GetBackendName, + Pcsx2Config::SPU2Options::DEFAULT_BACKEND); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.expansionMode, "SPU2/Output", "ExpansionMode", + &AudioStream::ParseExpansionMode, &AudioStream::GetExpansionModeName, + AudioStreamParameters::DEFAULT_EXPANSION_MODE); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.syncMode, "SPU2/Output", "SyncMode", + &Pcsx2Config::SPU2Options::ParseSyncMode, &Pcsx2Config::SPU2Options::GetSyncModeName, + Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "SPU2/Output", "BufferMS", + AudioStreamParameters::DEFAULT_BUFFER_MS); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "SPU2/Output", "OutputLatencyMS", + AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.outputLatencyMinimal, "SPU2/Output", "OutputLatencyMinimal", 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.syncMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onSyncModeChanged); + connect(m_ui.stretchSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onStretchSettingsClicked); + onExpansionModeChanged(); + onSyncModeChanged(); + updateDriverNames(); - onMinimalOutputLatencyStateChanged(); - updateLatencyLabels(); + connect(m_ui.bufferMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel); + connect(m_ui.outputLatencyMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel); + connect(m_ui.outputLatencyMinimal, &QCheckBox::checkStateChanged, this, &AudioSettingsWidget::onMinimalOutputLatencyChanged); + onMinimalOutputLatencyChanged(); + updateLatencyLabel(); + // for per-game, just use the normal path, since it needs to re-read/apply + if (!dialog->isPerGameSettings()) + { + m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Output", "OutputVolume", 100)); + m_ui.fastForwardVolume->setValue(m_dialog->getEffectiveIntValue("SPU2/Output", "FastForwardVolume", 100)); + m_ui.muted->setChecked(m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputMuted", false)); + connect(m_ui.volume, &QSlider::valueChanged, this, &AudioSettingsWidget::onOutputVolumeChanged); + connect(m_ui.fastForwardVolume, &QSlider::valueChanged, this, &AudioSettingsWidget::onFastForwardVolumeChanged); + connect(m_ui.muted, &QCheckBox::checkStateChanged, this, &AudioSettingsWidget::onOutputMutedChanged); + updateVolumeLabel(); + } + else + { + SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.volume, m_ui.volumeLabel, tr("%"), "SPU2/Output", "OutputVolume", 100); + SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.fastForwardVolume, m_ui.fastForwardVolumeLabel, tr("%"), "SPU2/Output", "FastForwardVolume", 100); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muted, "SPU2/Output", "OutputMuted", false); + } + connect(m_ui.resetVolume, &QToolButton::clicked, this, [this]() { resetVolume(false); }); + connect(m_ui.resetFastForwardVolume, &QToolButton::clicked, this, [this]() { resetVolume(true); }); + + dialog->registerWidgetHelp( + m_ui.audioBackend, tr("Audio Backend"), QStringLiteral("Cubeb"), + tr("The audio backend determines how frames produced by the emulator are submitted to the host. Cubeb provides the " + "lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio " + "output.")); + dialog->registerWidgetHelp( + m_ui.bufferMS, tr("Buffer Size"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_BUFFER_MS), + tr("Determines the buffer size which the time stretcher will try to keep filled. It effectively selects the " + "average latency, as audio will be stretched/shrunk to keep the buffer size within check.")); + dialog->registerWidgetHelp( + m_ui.outputLatencyMS, tr("Output Latency"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS), + tr("Determines the latency from the buffer to the host audio output. This can be set lower than the target latency " + "to reduce audio delay.")); + dialog->registerWidgetHelp(m_ui.volume, tr("Output Volume"), "100%", + tr("Controls the volume of the audio played on the host.")); + dialog->registerWidgetHelp(m_ui.fastForwardVolume, tr("Fast Forward Volume"), "100%", + tr("Controls the volume of the audio played on the host when fast forwarding.")); + dialog->registerWidgetHelp(m_ui.muted, tr("Mute All Sound"), tr("Unchecked"), + tr("Prevents the emulator from producing any audible sound.")); + 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.expansionSettings, tr("Expansion Settings"), tr("N/A"), + tr("These settings fine-tune the behavior of the FreeSurround-based channel expander.")); dialog->registerWidgetHelp(m_ui.syncMode, tr("Synchronization"), tr("TimeStretch (Recommended)"), tr("When running outside of 100% speed, adjusts the tempo on audio instead of dropping frames. Produces much nicer fast-forward/slowdown audio.")); - - dialog->registerWidgetHelp(m_ui.expansionMode, tr("Expansion"), tr("Stereo (None, Default)"), - tr("Determines how the stereo output from the emulated system is upmixed into a greater number of the output speakers.")); - - //: Cubeb is an audio engine name. Leave as-is. - dialog->registerWidgetHelp(m_ui.outputModule, tr("Output Module"), tr("Cubeb (Cross-platform)"), - tr("Selects the library to be used for audio output.")); - - dialog->registerWidgetHelp(m_ui.backend, tr("Output Backend"), tr("Default"), - tr("When the sound output module supports multiple audio backends, determines the API to be used for audio output to the system.")); - - dialog->registerWidgetHelp(m_ui.outputDevice, tr("Output Device"), tr("Default"), - tr("Determines which audio device to output the sound to.")); - - dialog->registerWidgetHelp(m_ui.targetLatency, tr("Target Latency"), tr("60 ms"), - tr("Determines the buffer size which the time stretcher will try to keep filled. It effectively selects the average latency, as " - "audio will be stretched/shrunk to keep the buffer size within check.")); - dialog->registerWidgetHelp(m_ui.outputLatency, tr("Output Latency"), tr("20 ms"), - tr("Determines the latency from the buffer to the host audio output. This can be set lower than the target latency to reduce audio " - "delay.")); - - dialog->registerWidgetHelp(m_ui.sequenceLength, tr("Sequence Length"), tr("30 ms"), tr("This is the default length of a single processing sequence which determines how the original sound is chopped in the time-stretch algorithm. " - "Larger values mean fewer sequences are used in processing. In principle a larger value sounds better when slowing down the tempo, but worse when increasing the tempo.")); - - //: Seek Window: the region of samples (window) the audio stretching algorithm is allowed to search. - dialog->registerWidgetHelp(m_ui.seekWindowSize, tr("Seek Window Size"), tr("20 ms"), tr("The seeking window is for the algorithm that seeks the best possible overlapping location. " - - "This determines from how wide a sample window the algorithm can use to find an optimal mixing location when the sound sequences are to be linked back together.")); - - dialog->registerWidgetHelp(m_ui.overlap, tr("Overlap"), tr("10 ms"), tr("When the sound sequences are mixed back together to form again a continuous sound stream, this parameter defines how much the ends of the consecutive sequences will overlap with each other.")); - - dialog->registerWidgetHelp(m_ui.volume, tr("Volume"), tr("100%"), - tr("Pre-applies a volume modifier to the game's audio output before forwarding it to your computer.")); + dialog->registerWidgetHelp(m_ui.stretchSettings, tr("Stretch Settings"), tr("N/A"), + tr("These settings fine-tune the behavior of the SoundTouch audio time stretcher when running outside of 100% speed.")); + dialog->registerWidgetHelp(m_ui.resetVolume, tr("Reset Volume"), tr("N/A"), + m_dialog->isPerGameSettings() ? tr("Resets volume back to the global/inherited setting.") : + tr("Resets volume back to the default, i.e. full.")); + dialog->registerWidgetHelp(m_ui.resetFastForwardVolume, tr("Reset Fast Forward Volume"), tr("N/A"), + m_dialog->isPerGameSettings() ? tr("Resets volume back to the global/inherited setting.") : + tr("Resets volume back to the default, i.e. full.")); } AudioSettingsWidget::~AudioSettingsWidget() = default; -void AudioSettingsWidget::expansionModeChanged() +AudioExpansionMode AudioSettingsWidget::getEffectiveExpansionMode() const { - const bool expansion51 = m_dialog->getEffectiveIntValue("SPU2/Output", "SpeakerConfiguration", 0) == 2; - m_ui.dplLevel->setDisabled(!expansion51); + return AudioStream::ParseExpansionMode( + m_dialog->getEffectiveStringValue("SPU2/Output", "ExpansionMode", + AudioStream::GetExpansionModeName(AudioStreamParameters::DEFAULT_EXPANSION_MODE)) + .c_str()) + .value_or(AudioStreamParameters::DEFAULT_EXPANSION_MODE); } -void AudioSettingsWidget::populateOutputModules() +u32 AudioSettingsWidget::getEffectiveExpansionBlockSize() const { - for (const SndOutModule* mod : GetSndOutModules()) - m_ui.outputModule->addItem(qApp->translate("SPU2", mod->GetDisplayName()), QString::fromUtf8(mod->GetIdent())); + const AudioExpansionMode expansion_mode = getEffectiveExpansionMode(); + if (expansion_mode == AudioExpansionMode::Disabled) + return 0; + + const u32 config_block_size = m_dialog->getEffectiveIntValue("SPU2/Output", "ExpandBlockSize", + AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE); + return std::has_single_bit(config_block_size) ? config_block_size : std::bit_ceil(config_block_size); } -void AudioSettingsWidget::outputModuleChanged() +void AudioSettingsWidget::onExpansionModeChanged() { - const std::string module_name(m_dialog->getEffectiveStringValue("SPU2/Output", "OutputModule", DEFAULT_OUTPUT_MODULE)); - const char* const* backend_names = GetOutputModuleBackends(module_name.c_str()); - - const std::string backend_name(m_dialog->getEffectiveStringValue("SPU2/Output", "BackendName", "")); - - QSignalBlocker sb(m_ui.backend); - m_ui.backend->clear(); - - if (m_dialog->isPerGameSettings()) - { - const QString global_backend(QString::fromStdString(Host::GetStringSettingValue("SPU2/Output", "BackendName", ""))); - m_ui.backend->addItem(tr("Use Global Setting [%1]").arg(global_backend.isEmpty() ? tr("Default") : global_backend)); - } - - m_ui.backend->setEnabled(backend_names != nullptr); - m_ui.backend->addItem(tr("Default")); - if (!backend_names || backend_name.empty()) - m_ui.backend->setCurrentIndex(0); - - if (backend_names) - { - for (u32 i = 0; backend_names[i] != nullptr; i++) - { - const int index = m_ui.backend->count(); - m_ui.backend->addItem(QString::fromUtf8(backend_names[i])); - if (backend_name == backend_names[i]) - m_ui.backend->setCurrentIndex(index); - } - } - - updateDevices(); + const AudioExpansionMode expansion_mode = getEffectiveExpansionMode(); + m_ui.expansionSettings->setEnabled(expansion_mode != AudioExpansionMode::Disabled); + updateLatencyLabel(); } -void AudioSettingsWidget::outputBackendChanged() +void AudioSettingsWidget::onSyncModeChanged() { - int index = m_ui.backend->currentIndex(); - if (m_dialog->isPerGameSettings()) + const Pcsx2Config::SPU2Options::SPU2SyncMode sync_mode = + Pcsx2Config::SPU2Options::ParseSyncMode( + m_dialog + ->getEffectiveStringValue("SPU2/Output", "SyncMode", + Pcsx2Config::SPU2Options::GetSyncModeName(Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE)) + .c_str()) + .value_or(Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE); + m_ui.stretchSettings->setEnabled(sync_mode == Pcsx2Config::SPU2Options::SPU2SyncMode::TimeStretch); +} + +AudioBackend AudioSettingsWidget::getEffectiveBackend() const +{ + return AudioStream::ParseBackendName(m_dialog->getEffectiveStringValue("SPU2/Output", "Backend", + AudioStream::GetBackendName(Pcsx2Config::SPU2Options::DEFAULT_BACKEND)) + .c_str()) + .value_or(Pcsx2Config::SPU2Options::DEFAULT_BACKEND); +} + +void AudioSettingsWidget::updateDriverNames() +{ + const AudioBackend backend = getEffectiveBackend(); + const std::vector> names = AudioStream::GetDriverNames(backend); + + m_ui.driver->disconnect(); + m_ui.driver->clear(); + if (names.empty()) { - if (index == 0) - { - m_dialog->setStringSettingValue("SPU2/Output", "BackendName", std::nullopt); - return; - } - - index--; + m_ui.driver->addItem(tr("Default"), QString()); + m_ui.driver->setEnabled(false); } - - if (index == 0) - m_dialog->setStringSettingValue("SPU2/Output", "BackendName", ""); else - m_dialog->setStringSettingValue("SPU2/Output", "BackendName", m_ui.backend->currentText().toUtf8().constData()); + { + m_ui.driver->setEnabled(true); + for (const std::pair& it : names) + m_ui.driver->addItem(QString::fromStdString(it.second), QString::fromStdString(it.first)); - updateDevices(); + SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.driver, "SPU2/Output", "DriverName", + std::move(names.front().first)); + connect(m_ui.driver, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDeviceNames); + } + + updateDeviceNames(); } -void AudioSettingsWidget::updateDevices() +void AudioSettingsWidget::updateDeviceNames() { - const std::string module_name(m_dialog->getEffectiveStringValue("SPU2/Output", "OutputModule", DEFAULT_OUTPUT_MODULE)); - const std::string backend_name(m_dialog->getEffectiveStringValue("SPU2/Output", "BackendName", "")); + const AudioBackend backend = getEffectiveBackend(); + const std::string driver_name = m_dialog->getEffectiveStringValue("SPU2/Output", "DriverName", ""); + const std::string current_device = m_dialog->getEffectiveStringValue("SPU2/Output", "DeviceName", ""); + const std::vector devices = AudioStream::GetOutputDevices(backend, driver_name.c_str()); m_ui.outputDevice->disconnect(); m_ui.outputDevice->clear(); m_output_device_latency = 0; - std::vector devices(GetOutputDeviceList(module_name.c_str(), backend_name.c_str())); if (devices.empty()) { - m_ui.outputDevice->addItem(tr("Default")); + m_ui.outputDevice->addItem(tr("Default"), QString()); m_ui.outputDevice->setEnabled(false); } else { - const std::string current_device(m_dialog->getEffectiveStringValue("SPU2/Output", "DeviceName", "")); - m_ui.outputDevice->setEnabled(true); - for (const SndOutDeviceInfo& devi : devices) + + bool is_known_device = false; + for (const AudioStream::DeviceInfo& di : devices) { - m_ui.outputDevice->addItem(QString::fromStdString(devi.display_name), QString::fromStdString(devi.name)); - if (devi.name == current_device) - m_output_device_latency = devi.minimum_latency_frames; + m_ui.outputDevice->addItem(QString::fromStdString(di.display_name), QString::fromStdString(di.name)); + if (di.name == current_device) + { + m_output_device_latency = di.minimum_latency_frames; + is_known_device = true; + } } - SettingWidgetBinder::BindWidgetToStringSetting( - m_dialog->getSettingsInterface(), m_ui.outputDevice, "SPU2/Output", "DeviceName", std::move(devices.front().name)); + if (!is_known_device) + { + m_ui.outputDevice->addItem(tr("Unknown Device \"%1\"").arg(QString::fromStdString(current_device)), + QString::fromStdString(current_device)); + } + + SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.outputDevice, "SPU2/Output", + "DeviceName", std::move(devices.front().name)); } + + updateLatencyLabel(); } -void AudioSettingsWidget::volumeChanged(int value) +void AudioSettingsWidget::updateLatencyLabel() { - // Nasty, but needed so we don't do a full settings apply and lag while dragging. - if (SettingsInterface* sif = m_dialog->getSettingsInterface()) + const u32 expand_buffer_ms = AudioStream::GetMSForBufferSize(SPU2::SAMPLE_RATE, getEffectiveExpansionBlockSize()); + const u32 config_buffer_ms = m_dialog->getEffectiveIntValue("SPU2/Output", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS); + const u32 config_output_latency_ms = m_dialog->getEffectiveIntValue("SPU2/Output", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); + const bool minimal_output = m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false); + + //: Preserve the %1 variable, adapt the latter ms (and/or any possible spaces in between) to your language's ruleset. + m_ui.outputLatencyLabel->setText(minimal_output ? tr("N/A") : tr("%1 ms").arg(config_output_latency_ms)); + + const u32 output_latency_ms = minimal_output ? AudioStream::GetMSForBufferSize(SPU2::SAMPLE_RATE, m_output_device_latency) : config_output_latency_ms; + if (output_latency_ms > 0) { - if (!m_ui.volumeLabel->font().bold()) + if (expand_buffer_ms > 0) { - QFont bold_font(m_ui.volumeLabel->font()); - bold_font.setBold(true); - m_ui.volumeLabel->setFont(bold_font); + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms buffer + %3 ms expand + %4 ms output)") + .arg(config_buffer_ms + expand_buffer_ms + output_latency_ms) + .arg(config_buffer_ms) + .arg(expand_buffer_ms) + .arg(output_latency_ms)); + } + else + { + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms buffer + %3 ms output)") + .arg(config_buffer_ms + output_latency_ms) + .arg(config_buffer_ms) + .arg(output_latency_ms)); } - - sif->SetIntValue("SPU2/Mixing", "FinalVolume", value); - sif->Save(); - - // There's two separate interfaces - one we're editing, and the active one. - // We need to reload the latter. - g_emu_thread->reloadGameSettings(); } else { - Host::SetBaseIntSettingValue("SPU2/Mixing", "FinalVolume", value); - Host::CommitBaseSettingChanges(); - - // Push through to emu thread since we're not applying. - if (QtHost::IsVMValid()) + if (expand_buffer_ms > 0) { - Host::RunOnCPUThread([]() { - if (!VMManager::HasValidVM()) - return; - - EmuConfig.SPU2.FinalVolume = Host::GetIntSettingValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME); - SPU2::SetOutputVolume(EmuConfig.SPU2.FinalVolume); - }); + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms expand, minimum output latency unknown)") + .arg(expand_buffer_ms + config_buffer_ms) + .arg(expand_buffer_ms)); + } + else + { + m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (minimum output latency unknown)").arg(config_buffer_ms)); } } - - updateVolumeLabel(); -} - -void AudioSettingsWidget::volumeContextMenuRequested(const QPoint& pt) -{ - QMenu menu(m_ui.volume); - m_ui.volume->connect(menu.addAction(qApp->translate("SettingWidgetBinder", "Reset")), &QAction::triggered, this, [this]() { - const s32 global_value = Host::GetBaseIntSettingValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME); - { - QSignalBlocker sb(m_ui.volume); - m_ui.volume->setValue(global_value); - updateVolumeLabel(); - } - - if (m_ui.volumeLabel->font().bold()) - { - QFont orig_font(m_ui.volumeLabel->font()); - orig_font.setBold(false); - m_ui.volumeLabel->setFont(orig_font); - } - - SettingsInterface* sif = m_dialog->getSettingsInterface(); - if (sif->ContainsValue("SPU2/Mixing", "FinalVolume")) - { - sif->DeleteValue("SPU2/Mixing", "FinalVolume"); - sif->Save(); - g_emu_thread->reloadGameSettings(); - } - }); - menu.exec(m_ui.volume->mapToGlobal(pt)); } void AudioSettingsWidget::updateVolumeLabel() { - //: Variable value that indicates a percentage. Preserve the %1 variable, adapt the latter % (and/or any possible spaces) to your language's ruleset. m_ui.volumeLabel->setText(tr("%1%").arg(m_ui.volume->value())); + m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value())); } -void AudioSettingsWidget::updateTargetLatencyRange() +void AudioSettingsWidget::onMinimalOutputLatencyChanged() { - const Pcsx2Config::SPU2Options::SynchronizationMode sync_mode = static_cast( - m_dialog->getIntValue("SPU2/Output", "SynchMode", DEFAULT_SYNCHRONIZATION_MODE).value_or(DEFAULT_SYNCHRONIZATION_MODE)); - - m_ui.targetLatency->setMinimum((sync_mode == Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch) ? - Pcsx2Config::SPU2Options::MIN_LATENCY_TIMESTRETCH : - Pcsx2Config::SPU2Options::MIN_LATENCY); - m_ui.targetLatency->setMaximum(Pcsx2Config::SPU2Options::MAX_LATENCY); + const bool minimal = m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false); + m_ui.outputLatencyMS->setEnabled(!minimal); + updateLatencyLabel(); } -void AudioSettingsWidget::updateLatencyLabels() +void AudioSettingsWidget::onOutputVolumeChanged(int new_value) { - const bool minimal_output = m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false); + // only called for base settings + pxAssert(!m_dialog->isPerGameSettings()); + Host::SetBaseIntSettingValue("SPU2/Output", "OutputVolume", new_value); + Host::CommitBaseSettingChanges(); + g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value()); - //: Preserve the %1 variable, adapt the latter ms (and/or any possible spaces in between) to your language's ruleset. - m_ui.outputLatencyLabel->setText(minimal_output ? tr("N/A") : tr("%1 ms").arg(m_ui.outputLatency->value())); + updateVolumeLabel(); +} - const u32 output_latency_ms = - minimal_output ? (((m_output_device_latency * 1000u) + 47999u) / 48000u) : static_cast(m_ui.outputLatency->value()); - const u32 buffer_ms = static_cast(m_ui.targetLatency->value()); - if (output_latency_ms > 0) +void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value) +{ + // only called for base settings + pxAssert(!m_dialog->isPerGameSettings()); + Host::SetBaseIntSettingValue("SPU2/Output", "FastForwardVolume", new_value); + Host::CommitBaseSettingChanges(); + g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value); + + updateVolumeLabel(); +} + +void AudioSettingsWidget::onOutputMutedChanged(int new_state) +{ + // only called for base settings + pxAssert(!m_dialog->isPerGameSettings()); + + const bool muted = (new_state != 0); + Host::SetBaseBoolSettingValue("SPU2/Output", "OutputMuted", muted); + 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, "SPU2/Output", "ExpandBlockSize", + AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE, 0); + QtUtils::BindLabelToSlider(dlgui.blockSize, dlgui.blockSizeLabel); + SettingWidgetBinder::BindWidgetToFloatSetting(sif, dlgui.circularWrap, "SPU2/Output", "ExpandCircularWrap", + AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP); + QtUtils::BindLabelToSlider(dlgui.circularWrap, dlgui.circularWrapLabel); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.shift, "SPU2/Output", "ExpandShift", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_SHIFT); + QtUtils::BindLabelToSlider(dlgui.shift, dlgui.shiftLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.depth, "SPU2/Output", "ExpandDepth", 10.0f, + AudioStreamParameters::DEFAULT_EXPAND_DEPTH); + QtUtils::BindLabelToSlider(dlgui.depth, dlgui.depthLabel, 10.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.focus, "SPU2/Output", "ExpandFocus", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_FOCUS); + QtUtils::BindLabelToSlider(dlgui.focus, dlgui.focusLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.centerImage, "SPU2/Output", "ExpandCenterImage", 100.0f, + AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE); + QtUtils::BindLabelToSlider(dlgui.centerImage, dlgui.centerImageLabel, 100.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.frontSeparation, "SPU2/Output", "ExpandFrontSeparation", + 10.0f, AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION); + QtUtils::BindLabelToSlider(dlgui.frontSeparation, dlgui.frontSeparationLabel, 10.0f); + SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, dlgui.rearSeparation, "SPU2/Output", "ExpandRearSeparation", 10.0f, + AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION); + QtUtils::BindLabelToSlider(dlgui.rearSeparation, dlgui.rearSeparationLabel, 10.0f); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.lowCutoff, "SPU2/Output", "ExpandLowCutoff", + AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF); + QtUtils::BindLabelToSlider(dlgui.lowCutoff, dlgui.lowCutoffLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.highCutoff, "SPU2/Output", "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("SPU2/Output", "ExpandBlockSize", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_BLOCK_SIZE)); + + m_dialog->setFloatSettingValue("SPU2/Output", "ExpandCircularWrap", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_CIRCULAR_WRAP)); + m_dialog->setFloatSettingValue( + "SPU2/Output", "ExpandShift", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_SHIFT)); + m_dialog->setFloatSettingValue( + "SPU2/Output", "ExpandDepth", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_DEPTH)); + m_dialog->setFloatSettingValue( + "SPU2/Output", "ExpandFocus", + m_dialog->isPerGameSettings() ? std::nullopt : std::optional(AudioStreamParameters::DEFAULT_EXPAND_FOCUS)); + m_dialog->setFloatSettingValue("SPU2/Output", "ExpandCenterImage", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_CENTER_IMAGE)); + m_dialog->setFloatSettingValue("SPU2/Output", "ExpandFrontSeparation", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_FRONT_SEPARATION)); + m_dialog->setFloatSettingValue("SPU2/Output", "ExpandRearSeparation", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_REAR_SEPARATION)); + m_dialog->setIntSettingValue("SPU2/Output", "ExpandLowCutoff", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_EXPAND_LOW_CUTOFF)); + m_dialog->setIntSettingValue("SPU2/Output", "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(); + updateLatencyLabel(); +} + +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, "SPU2/Output", "StretchSequenceLengthMS", + AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH, 0); + QtUtils::BindLabelToSlider(dlgui.sequenceLength, dlgui.sequenceLengthLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.seekWindowSize, "SPU2/Output", "StretchSeekWindowMS", + AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW, 0); + QtUtils::BindLabelToSlider(dlgui.seekWindowSize, dlgui.seekWindowSizeLabel); + SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.overlap, "SPU2/Output", "StretchOverlapMS", + AudioStreamParameters::DEFAULT_STRETCH_OVERLAP, 0); + QtUtils::BindLabelToSlider(dlgui.overlap, dlgui.overlapLabel); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useQuickSeek, "SPU2/Output", "StretchUseQuickSeek", + AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useAAFilter, "SPU2/Output", "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("SPU2/Output", "StretchSequenceLengthMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH)); + m_dialog->setIntSettingValue("SPU2/Output", "StretchSeekWindowMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW)); + m_dialog->setIntSettingValue("SPU2/Output", "StretchOverlapMS", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_OVERLAP)); + m_dialog->setBoolSettingValue("SPU2/Output", "StretchUseQuickSeek", + m_dialog->isPerGameSettings() ? + std::nullopt : + std::optional(AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK)); + m_dialog->setBoolSettingValue("SPU2/Output", "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(); +} + +void AudioSettingsWidget::resetVolume(bool fast_forward) +{ + const char* key = fast_forward ? "FastForwardVolume" : "OutputVolume"; + QSlider* const slider = fast_forward ? m_ui.fastForwardVolume : m_ui.volume; + QLabel* const label = fast_forward ? m_ui.fastForwardVolumeLabel : m_ui.volumeLabel; + + if (m_dialog->isPerGameSettings()) { - m_ui.latencySummary->setText(tr("Average Latency: %1 ms (%2 ms buffer + %3 ms output)") - .arg(buffer_ms + output_latency_ms) - .arg(buffer_ms) - .arg(output_latency_ms)); + m_dialog->removeSettingValue("Audio", key); + + const int value = m_dialog->getEffectiveIntValue("Audio", key, 100); + QSignalBlocker sb(slider); + slider->setValue(value); + label->setText(QStringLiteral("%1%2").arg(value).arg(tr("%"))); + + // remove bold font if it was previously overridden + QFont font(label->font()); + font.setBold(false); + label->setFont(font); } else { - m_ui.latencySummary->setText(tr("Average Latency: %1 ms (minimum output latency unknown)").arg(buffer_ms)); + slider->setValue(100); } } - -void AudioSettingsWidget::onMinimalOutputLatencyStateChanged() -{ - m_ui.outputLatency->setEnabled(!m_dialog->getEffectiveBoolValue("SPU2/Output", "OutputLatencyMinimal", false)); -} - -void AudioSettingsWidget::resetTimestretchDefaults() -{ - m_ui.sequenceLength->setValue(DEFAULT_SOUNDTOUCH_SEQUENCE_LENGTH); - m_ui.seekWindowSize->setValue(DEFAULT_SOUNDTOUCH_SEEK_WINDOW); - m_ui.overlap->setValue(DEFAULT_SOUNDTOUCH_OVERLAP); -} diff --git a/pcsx2-qt/Settings/AudioSettingsWidget.h b/pcsx2-qt/Settings/AudioSettingsWidget.h index 4dab0cb360..25798ade30 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.h +++ b/pcsx2-qt/Settings/AudioSettingsWidget.h @@ -1,11 +1,16 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #pragma once +#include "ui_AudioSettingsWidget.h" + +#include "common/Pcsx2Defs.h" + #include -#include "ui_AudioSettingsWidget.h" +enum class AudioBackend : u8; +enum class AudioExpansionMode : u8; class SettingsWindow; @@ -18,22 +23,28 @@ public: ~AudioSettingsWidget(); private Q_SLOTS: - void expansionModeChanged(); - void outputModuleChanged(); - void outputBackendChanged(); - void updateDevices(); - void volumeChanged(int value); - void volumeContextMenuRequested(const QPoint& pt); - void updateTargetLatencyRange(); - void updateLatencyLabels(); - void onMinimalOutputLatencyStateChanged(); - void resetTimestretchDefaults(); + void onExpansionModeChanged(); + void onSyncModeChanged(); + + void updateDriverNames(); + void updateDeviceNames(); + void updateLatencyLabel(); + void updateVolumeLabel(); + void onMinimalOutputLatencyChanged(); + void onOutputVolumeChanged(int new_value); + void onFastForwardVolumeChanged(int new_value); + void onOutputMutedChanged(int new_state); + + void onExpansionSettingsClicked(); + void onStretchSettingsClicked(); private: - void populateOutputModules(); - void updateVolumeLabel(); + AudioBackend getEffectiveBackend() const; + AudioExpansionMode getEffectiveExpansionMode() const; + u32 getEffectiveExpansionBlockSize() const; + void resetVolume(bool fast_forward); - SettingsWindow* m_dialog; Ui::AudioSettingsWidget m_ui; + SettingsWindow* m_dialog; u32 m_output_device_latency = 0; }; diff --git a/pcsx2-qt/Settings/AudioSettingsWidget.ui b/pcsx2-qt/Settings/AudioSettingsWidget.ui index bdfde1fd8a..185d44a537 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.ui +++ b/pcsx2-qt/Settings/AudioSettingsWidget.ui @@ -6,11 +6,11 @@ 0 0 - 754 - 485 + 523 + 478 - + 0 @@ -23,434 +23,133 @@ 0 - - + + - Timestretch Settings + Configuration - - + + - Sequence Length: - - - - - - - - - 20 - - - 100 - - - 30 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 10 - - - - - - - 30 - - - - - - - - - Seekwindow Size: + Driver: - - - - - 10 - - - 30 - - - 20 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 2 - - - - - - - 20 - - - - - - - - - Overlap: - - - - - - - - - 5 - - - 15 - - - 10 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 1 - - - - - - - 10 - - - - + - + - - - Qt::Horizontal - - - - 40 - 20 - - - + - - - - 120 - 0 - + + + Expansion Settings - - Restore Defaults + + - - - - - - - - - Volume - - - - - - - 0 - 0 - - - - - 40 - 0 - - - - 200 - - - 100 - - - Qt::Vertical - - - QSlider::TicksBothSides - - - 10 - - - - - - - - 40 - 0 - - - - 100% - - - Qt::AlignCenter - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Mixing Settings - - - - - - Synchronization: + + + + 15 - - - - - - - TimeStretch (Recommended) - - - - - Async Mix (Breaks some games!) - - - - - None (Audio can skip.) - - - - - - - - Expansion: + + 500 - - - - - - - Stereo (None, Default) - - - - - Quadraphonic - - - - - Surround 5.1 - - - - - Surround 7.1 - - - - - - - - ProLogic Level: + + 1 - - - - - - - None (Default) - - - - - ProLogic Decoding (basic) - - - - - ProLogic II Decoding (gigaherz) - - - - - - - - Target Latency: + + 5 + + + 50 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBothSides + + + 20 - + - + + + + + + Stretch Settings + + + + + + + + + + + + Buffer Size: + + + + + + + Maximum latency: 0 frames (0.00ms) + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Backend: + + + + + + + 15 200 - - 60 - - Qt::Horizontal + Qt::Orientation::Horizontal - QSlider::TicksBelow + QSlider::TickPosition::TicksBothSides - 200 - - - - - - - 60 ms - - - - - - - - - - - - Output Settings - - - - - - Output Module: - - - - - - - - - - Output Latency: - - - - - - - - - 10 - - - 200 - - 20 - - Qt::Horizontal - - - QSlider::TicksBelow - - - 200 - - 20 ms + 0 ms @@ -463,41 +162,188 @@ - - - - Output Backend: - - + + - - - - - + + - Maximum Latency: - - - Qt::AlignCenter - - - - - - - Output Device: + Output Latency: + + + + Output Device: + + + + + + + Expansion: + + + + + + + Synchronization: + + + + + + + Controls + + + + + + Output Volume: + + + + + + + + + 200 + + + 100 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBothSides + + + 10 + + + + + + + + 0 + 0 + + + + 100% + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Stretch Settings + + + + + + + + + + + + + + 200 + + + 100 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBothSides + + + 10 + + + + + + + + 0 + 0 + + + + 100% + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Stretch Settings + + + + + + + + + + + + Fast Forward Volume: + + + + + + + Mute All Sound + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + - + + + diff --git a/pcsx2-qt/Settings/AudioStretchSettingsDialog.ui b/pcsx2-qt/Settings/AudioStretchSettingsDialog.ui new file mode 100644 index 0000000000..42d50ffce7 --- /dev/null +++ b/pcsx2-qt/Settings/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/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 30f6330d51..42abb7f53f 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -432,6 +432,12 @@ Document + + Document + + + Document + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 2836d7e82d..40a3fa79fc 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -665,6 +665,12 @@ Settings + + Settings + + + Settings + diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index 53353bb3c4..e163156db6 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -247,15 +247,12 @@ set(pcsx2CDVDHeaders set(pcsx2SPU2Sources SPU2/ADSR.cpp SPU2/Debug.cpp - SPU2/DplIIdecoder.cpp SPU2/Dma.cpp SPU2/Mixer.cpp SPU2/spu2.cpp SPU2/ReadInput.cpp SPU2/RegTable.cpp SPU2/Reverb.cpp - SPU2/SndOut.cpp - SPU2/SndOut_Cubeb.cpp SPU2/spu2freeze.cpp SPU2/spu2sys.cpp SPU2/Wavedump_wav.cpp @@ -270,21 +267,12 @@ set(pcsx2SPU2Headers SPU2/Debug.h SPU2/defs.h SPU2/Dma.h - SPU2/Global.h SPU2/interpolate_table.h - SPU2/Mixer.h SPU2/spu2.h SPU2/regs.h - SPU2/SndOut.h SPU2/spdif.h ) -if(WIN32) - list(APPEND pcsx2SPU2Sources - SPU2/SndOut_XAudio2.cpp - ) -endif() - # DEV9 sources set(pcsx2DEV9Sources DEV9/AdapterUtils.cpp diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 50c7f55dc7..c6094bb326 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -3,8 +3,11 @@ #pragma once +#include "Host/AudioStreamTypes.h" + #include "common/Pcsx2Defs.h" #include "common/FPControl.h" + #include #include #include @@ -765,28 +768,22 @@ struct Pcsx2Config struct SPU2Options { - enum class SynchronizationMode + enum class SPU2SyncMode : u8 { + Disabled, TimeStretch, - ASync, - NoSync, + Count }; static constexpr s32 MAX_VOLUME = 200; - - static constexpr s32 MIN_LATENCY = 3; - static constexpr s32 MIN_LATENCY_TIMESTRETCH = 15; - static constexpr s32 MAX_LATENCY = 750; + static constexpr AudioBackend DEFAULT_BACKEND = AudioBackend::Cubeb; + static constexpr SPU2SyncMode DEFAULT_SYNC_MODE = SPU2SyncMode::TimeStretch; - static constexpr s32 MIN_SEQUENCE_LEN = 20; - static constexpr s32 MAX_SEQUENCE_LEN = 100; - static constexpr s32 MIN_SEEKWINDOW = 10; - static constexpr s32 MAX_SEEKWINDOW = 30; - static constexpr s32 MIN_OVERLAP = 5; - static constexpr s32 MAX_OVERLAP = 15; + static std::optional ParseSyncMode(const char* str); + static const char* GetSyncModeName(SPU2SyncMode backend); + static const char* GetSyncModeDisplayName(SPU2SyncMode backend); BITFIELD32() - bool OutputLatencyMinimal : 1; bool DebugEnabled : 1, MsgToConsole : 1, @@ -794,7 +791,6 @@ struct Pcsx2Config MsgVoiceOff : 1, MsgDMA : 1, MsgAutoDMA : 1, - MsgOverruns : 1, MsgCache : 1, AccessLog : 1, DMALog : 1, @@ -805,26 +801,23 @@ struct Pcsx2Config VisualDebugEnabled : 1; BITFIELD_END - SynchronizationMode SynchMode = SynchronizationMode::TimeStretch; + u32 OutputVolume = 100; + u32 FastForwardVolume = 100; + bool OutputMuted = false; - s32 FinalVolume = 100; - s32 Latency = 60; - s32 OutputLatency = 20; - s32 SpeakerConfiguration = 0; - s32 DplDecodingLevel = 0; + AudioBackend Backend = DEFAULT_BACKEND; + SPU2SyncMode SyncMode = DEFAULT_SYNC_MODE; + AudioStreamParameters StreamParameters; - s32 SequenceLenMS = 30; - s32 SeekWindowMS = 20; - s32 OverlapMS = 10; - - std::string OutputModule; - std::string BackendName; + std::string DriverName; std::string DeviceName; SPU2Options(); void LoadSave(SettingsWrapper& wrap); + bool IsTimeStretchEnabled() const { return (SyncMode == SPU2SyncMode::TimeStretch); } + bool operator==(const SPU2Options& right) const; bool operator!=(const SPU2Options& right) const; }; diff --git a/pcsx2/GS/GSCapture.cpp b/pcsx2/GS/GSCapture.cpp index ed9fd7c177..d9579abed6 100644 --- a/pcsx2/GS/GSCapture.cpp +++ b/pcsx2/GS/GSCapture.cpp @@ -8,8 +8,8 @@ #include "GS/Renderers/Common/GSDevice.h" #include "GS/Renderers/Common/GSTexture.h" #include "SPU2/spu2.h" -#include "SPU2/SndOut.h" #include "Host.h" +#include "Host/AudioStream.h" #include "IconsFontAwesome5.h" #include "common/Assertions.h" #include "common/Console.h" @@ -133,7 +133,7 @@ namespace GSCapture { static constexpr u32 NUM_FRAMES_IN_FLIGHT = 3; static constexpr u32 MAX_PENDING_FRAMES = NUM_FRAMES_IN_FLIGHT * 2; - static constexpr u32 AUDIO_BUFFER_SIZE = Common::AlignUpPow2((MAX_PENDING_FRAMES * 48000) / 60, SndOutPacketSize); + static constexpr u32 AUDIO_BUFFER_SIZE = Common::AlignUpPow2((MAX_PENDING_FRAMES * 48000) / 60, AudioStream::CHUNK_SIZE); static constexpr u32 AUDIO_CHANNELS = 2; struct PendingFrame @@ -729,7 +729,7 @@ bool GSCapture::BeginCapture(float fps, GSVector2i recommendedResolution, float // Use packet size for frame if it supports it... but most don't. if (acodec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) - s_audio_frame_size = SndOutPacketSize; + s_audio_frame_size = AudioStream::CHUNK_SIZE; else s_audio_frame_size = s_audio_codec_context->frame_size; if (s_audio_frame_size >= AUDIO_BUFFER_SIZE) @@ -1070,7 +1070,7 @@ void GSCapture::DeliverAudioPacket(const s16* frames) // through and clear them out for the next capture. If we happen to fill the buffer, *then* we'll lock, and check if // the capture has stopped. - static constexpr u32 num_frames = static_cast(SndOutPacketSize); + static constexpr u32 num_frames = AudioStream::CHUNK_SIZE; if ((AUDIO_BUFFER_SIZE - s_audio_buffer_size.load(std::memory_order_acquire)) < num_frames) { @@ -1113,7 +1113,7 @@ bool GSCapture::ProcessAudioPackets(s64 video_pts) while (pending_frames > 0 && (!s_video_codec_context || wrap_av_compare_ts(video_pts, s_video_codec_context->time_base, s_next_audio_pts, s_audio_codec_context->time_base) > 0)) { - pxAssert(pending_frames >= static_cast(SndOutPacketSize)); + pxAssert(pending_frames >= AudioStream::CHUNK_SIZE); // In case the encoder is still using it. if (s_audio_frame_pos == 0) diff --git a/pcsx2/GS/GSCapture.h b/pcsx2/GS/GSCapture.h index 05b0a7605d..658f8f33eb 100644 --- a/pcsx2/GS/GSCapture.h +++ b/pcsx2/GS/GSCapture.h @@ -21,7 +21,7 @@ namespace GSCapture { bool BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string filename); bool DeliverVideoFrame(GSTexture* stex); - void DeliverAudioPacket(const s16* frames); // SndOutPacketSize + void DeliverAudioPacket(const s16* frames); // AudioStream::CHUNK_SIZE void EndCapture(); bool IsCapturing(); diff --git a/pcsx2/Host/CubebAudioStream.cpp b/pcsx2/Host/CubebAudioStream.cpp index 7ed9ecf05e..0e2758b739 100644 --- a/pcsx2/Host/CubebAudioStream.cpp +++ b/pcsx2/Host/CubebAudioStream.cpp @@ -342,15 +342,4 @@ std::vector AudioStream::GetCubebOutputDevices(const ch } return ret; - - for (size_t i = 0; i < devices.count; i++) - { - const cubeb_device_info& di = devices.device[i]; - if (!di.device_id) - continue; - - ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, 0); - } - - return ret; } diff --git a/pcsx2/Hotkeys.cpp b/pcsx2/Hotkeys.cpp index fd2b3a3863..cb757db7dd 100644 --- a/pcsx2/Hotkeys.cpp +++ b/pcsx2/Hotkeys.cpp @@ -44,11 +44,11 @@ static void HotkeyAdjustVolume(s32 fixed, s32 delta) if (!VMManager::HasValidVM()) return; - const s32 current_vol = SPU2::GetOutputVolume(); + const s32 current_vol = static_cast(SPU2::GetOutputVolume()); const s32 new_volume = - std::clamp((fixed >= 0) ? fixed : (current_vol + delta), 0, Pcsx2Config::SPU2Options::MAX_VOLUME); + std::clamp((fixed >= 0) ? fixed : (current_vol + delta), 0, static_cast(Pcsx2Config::SPU2Options::MAX_VOLUME)); if (current_vol != new_volume) - SPU2::SetOutputVolume(new_volume); + SPU2::SetOutputVolume(static_cast(new_volume)); if (new_volume == 0) { @@ -197,7 +197,7 @@ DEFINE_HOTKEY("DecreaseVolume", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_N }) DEFINE_HOTKEY("Mute", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Mute"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) - HotkeyAdjustVolume((SPU2::GetOutputVolume() == 0) ? EmuConfig.SPU2.FinalVolume : 0, 0); + HotkeyAdjustVolume((SPU2::GetOutputVolume() == 0) ? SPU2::GetResetVolume() : 0, 0); }) DEFINE_HOTKEY( "FrameAdvance", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Frame Advance"), [](s32 pressed) { diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index 10931a3692..bd33953cd1 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -10,6 +10,7 @@ #include "CDVD/CDVDdiscReader.h" #include "GameList.h" #include "Host.h" +#include "Host/AudioStream.h" #include "INISettingsInterface.h" #include "ImGui/FullscreenUI.h" #include "ImGui/ImGuiFullscreen.h" @@ -323,6 +324,7 @@ namespace FullscreenUI static void SetSettingsChanged(SettingsInterface* bsi); static bool GetEffectiveBoolSetting(SettingsInterface* bsi, const char* section, const char* key, bool default_value); static s32 GetEffectiveIntSetting(SettingsInterface* bsi, const char* section, const char* key, s32 default_value); + static u32 GetEffectiveUIntSetting(SettingsInterface* bsi, const char* section, const char* key, u32 default_value); static void DoCopyGameSettings(); static void DoClearGameSettings(); static void CopyGlobalControllerSettingsToGame(); @@ -371,6 +373,14 @@ namespace FullscreenUI float default_value, const char* const* options, const float* option_values, size_t option_count, bool translate_options, bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); + template + static void DrawEnumSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, DataType default_value, + std::optional (*from_string_function)(const char* str), + const char* (*to_string_function)(DataType value), + const char* (*to_display_string_function)(DataType value), SizeType option_count, + bool enabled = true, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key, const std::string& runtime_var, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); @@ -1441,6 +1451,18 @@ s32 FullscreenUI::GetEffectiveIntSetting(SettingsInterface* bsi, const char* sec return Host::Internal::GetBaseSettingsLayer()->GetIntValue(section, key, default_value); } +u32 FullscreenUI::GetEffectiveUIntSetting(SettingsInterface* bsi, const char* section, const char* key, u32 default_value) +{ + if (IsEditingGameSettings(bsi)) + { + std::optional value = bsi->GetOptionalUIntValue(section, key, std::nullopt); + if (value.has_value()) + return value.value(); + } + + return Host::Internal::GetBaseSettingsLayer()->GetUIntValue(section, key, default_value); +} + void FullscreenUI::DrawInputBindingButton( SettingsInterface* bsi, InputBindingInfo::Type type, const char* section, const char* name, const char* display_name, const char* icon_name, bool show_type) { @@ -2443,6 +2465,57 @@ void FullscreenUI::DrawFloatListSetting(SettingsInterface* bsi, const char* titl } } +template +void FullscreenUI::DrawEnumSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, + const char* key, DataType default_value, std::optional (*from_string_function)(const char* str), + const char* (*to_string_function)(DataType value), const char* (*to_display_string_function)(DataType value), SizeType option_count, + bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + const bool game_settings = IsEditingGameSettings(bsi); + const std::optional value(bsi->GetOptionalSmallStringValue( + section, key, game_settings ? std::nullopt : std::optional(to_string_function(default_value)))); + + const std::optional typed_value(value.has_value() ? from_string_function(value->c_str()) : std::nullopt); + + if (MenuButtonWithValue(title, summary, + typed_value.has_value() ? to_display_string_function(typed_value.value()) : + FSUI_CSTR("Use Global Setting"), + enabled, height, font, summary_font)) + { + ImGuiFullscreen::ChoiceDialogOptions cd_options; + cd_options.reserve(static_cast(option_count) + 1); + if (game_settings) + cd_options.emplace_back(FSUI_CSTR("Use Global Setting"), !value.has_value()); + for (u32 i = 0; i < static_cast(option_count); i++) + cd_options.emplace_back(to_display_string_function(static_cast(i)), + (typed_value.has_value() && i == static_cast(typed_value.value()))); + OpenChoiceDialog( + title, false, std::move(cd_options), + [section, key, to_string_function, game_settings](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); + if (game_settings) + { + if (index == 0) + bsi->DeleteValue(section, key); + else + bsi->SetStringValue(section, key, to_string_function(static_cast(index - 1))); + } + else + { + bsi->SetStringValue(section, key, to_string_function(static_cast(index))); + } + + SetSettingsChanged(bsi); + } + + CloseChoiceDialog(); + }); + } +} + void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, const char* title, const char* section, const char* key, const std::string& runtime_var, float height /* = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT */, ImFont* font /* = g_large_font */, ImFont* summary_font /* = g_medium_font */) @@ -3851,67 +3924,62 @@ void FullscreenUI::DrawGraphicsSettingsPage() void FullscreenUI::DrawAudioSettingsPage() { - static constexpr const char* synchronization_modes[] = { - FSUI_NSTR("TimeStretch (Recommended)"), - FSUI_NSTR("Async Mix (Breaks some games!)"), - FSUI_NSTR("None (Audio can skip.)"), - }; - static constexpr const char* expansion_modes[] = { - FSUI_NSTR("Stereo (None, Default)"), - FSUI_NSTR("Quadraphonic"), - FSUI_NSTR("Surround 5.1"), - FSUI_NSTR("Surround 7.1"), - }; - static constexpr const char* output_entries[] = { - FSUI_NSTR("No Sound (Emulate SPU2 only)"), - FSUI_NSTR("Cubeb (Cross-platform)"), -#ifdef _WIN32 - FSUI_NSTR("XAudio2"), -#endif - }; - static constexpr const char* output_values[] = { - "nullout", - "cubeb", -#ifdef _WIN32 - "xaudio2", -#endif - }; - static constexpr const char* default_output_module = "cubeb"; - SettingsInterface* bsi = GetEditingSettingsInterface(); BeginMenuButtons(); - MenuHeading(FSUI_CSTR("Runtime Settings")); - DrawIntRangeSetting(bsi, FSUI_ICONSTR(ICON_FA_VOLUME_UP, "Output Volume"), - FSUI_CSTR("Applies a global volume modifier to all sound produced by the game."), "SPU2/Mixing", "FinalVolume", 100, 0, 200, - FSUI_CSTR("%d%%")); + MenuHeading(FSUI_CSTR("Audio Control")); - MenuHeading(FSUI_CSTR("Mixing Settings")); - DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_SYNC_ALT, "Synchronization Mode"), - FSUI_CSTR("Changes when SPU samples are generated relative to system emulation."), "SPU2/Output", "SynchMode", - static_cast(Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch), synchronization_modes, - std::size(synchronization_modes), true); - DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_PF_SPEAKER_ALT, "Expansion Mode"), - FSUI_CSTR("Determines how the stereo output is transformed to greater speaker counts."), "SPU2/Output", "SpeakerConfiguration", 0, - expansion_modes, std::size(expansion_modes), true); + DrawIntRangeSetting(bsi, FSUI_CSTR("Output Volume"), + FSUI_CSTR("Controls the volume of the audio played on the host."), "SPU2/Output", "OutputVolume", 100, + 0, 100, "%d%%"); + DrawIntRangeSetting(bsi, FSUI_CSTR("Fast Forward Volume"), + FSUI_CSTR("Controls the volume of the audio played on the host when fast forwarding."), "SPU2/Output", + "FastForwardVolume", 100, 0, 100, "%d%%"); + DrawToggleSetting(bsi, FSUI_CSTR("Mute All Sound"), + FSUI_CSTR("Prevents the emulator from producing any audible sound."), "SPU2/Output", "OutputMuted", + false); - MenuHeading(FSUI_CSTR("Output Settings")); - DrawStringListSetting(bsi, FSUI_ICONSTR(ICON_FA_PLAY_CIRCLE, "Output Module"), - FSUI_CSTR("Determines which API is used to play back audio samples on the host."), "SPU2/Output", "OutputModule", - default_output_module, output_entries, output_values, std::size(output_entries), true); - DrawIntRangeSetting(bsi, FSUI_ICONSTR(ICON_FA_CLOCK, "Latency"), - FSUI_CSTR("Sets the average output latency when using the cubeb backend."), "SPU2/Output", "Latency", 100, 15, 200, FSUI_CSTR("%d ms (avg)")); + MenuHeading(FSUI_CSTR("Backend Settings")); - MenuHeading(FSUI_CSTR("Timestretch Settings")); - DrawIntRangeSetting(bsi, FSUI_ICONSTR(ICON_FA_RULER_HORIZONTAL, "Sequence Length"), - FSUI_CSTR("Affects how the timestretcher operates when not running at 100% speed."), "Soundtouch", "SequenceLengthMS", 30, 20, 100, - FSUI_CSTR("%d ms")); - DrawIntRangeSetting(bsi, FSUI_ICONSTR(ICON_FA_WINDOW_MAXIMIZE, "Seekwindow Size"), - FSUI_CSTR("Affects how the timestretcher operates when not running at 100% speed."), "Soundtouch", "SeekWindowMS", 20, 10, 30, - FSUI_CSTR("%d ms")); - DrawIntRangeSetting(bsi, FSUI_ICONSTR(ICON_FA_RECEIPT, "Overlap"), - FSUI_CSTR("Affects how the timestretcher operates when not running at 100% speed."), "Soundtouch", "OverlapMS", 20, 5, 15, FSUI_CSTR("%d ms")); + DrawEnumSetting( + bsi, FSUI_CSTR("Audio Backend"), + FSUI_CSTR("The audio backend determines how frames produced by the emulator are submitted to the host."), "SPU2/Output", + "Backend", Pcsx2Config::SPU2Options::DEFAULT_BACKEND, &AudioStream::ParseBackendName, &AudioStream::GetBackendName, + &AudioStream::GetBackendDisplayName, AudioBackend::Count); + DrawEnumSetting(bsi, FSUI_CSTR("Expansion"), + FSUI_CSTR("Determines how audio is expanded from stereo to surround for supported games."), "SPU2/Output", + "ExpansionMode", AudioStreamParameters::DEFAULT_EXPANSION_MODE, &AudioStream::ParseExpansionMode, + &AudioStream::GetExpansionModeName, &AudioStream::GetExpansionModeDisplayName, + AudioExpansionMode::Count); + DrawEnumSetting(bsi, FSUI_CSTR("Synchronization"), + FSUI_CSTR("Changes when SPU samples are generated relative to system emulation."), + "SPU2/Output", "SyncMode", Pcsx2Config::SPU2Options::DEFAULT_SYNC_MODE, + &Pcsx2Config::SPU2Options::ParseSyncMode, &Pcsx2Config::SPU2Options::GetSyncModeName, + &Pcsx2Config::SPU2Options::GetSyncModeDisplayName, Pcsx2Config::SPU2Options::SPU2SyncMode::Count); + DrawIntRangeSetting(bsi, FSUI_CSTR("Buffer Size"), + FSUI_CSTR("Determines the amount of audio buffered before being pulled by the host API."), + "SPU2/Output", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS, 10, 500, "%d ms"); + + const u32 output_latency = + GetEffectiveUIntSetting(bsi, "SPU2/Output", "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("SPU2/Output", "OutputLatencyMS", + output_latency_minimal ? 0 : AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS); + SetSettingsChanged(bsi); + } + if (!output_latency_minimal) + { + DrawIntRangeSetting( + 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."), + "SPU2/Output", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS, 1, 500, "%d ms"); + } EndMenuButtons(); } diff --git a/pcsx2/ImGui/ImGuiManager.cpp b/pcsx2/ImGui/ImGuiManager.cpp index 30dca58743..582b1b969d 100644 --- a/pcsx2/ImGui/ImGuiManager.cpp +++ b/pcsx2/ImGui/ImGuiManager.cpp @@ -489,8 +489,8 @@ ImFont* ImGuiManager::AddFixedFont(float size) bool ImGuiManager::AddIconFonts(float size) { // clang-format off - static constexpr ImWchar range_fa[] = { 0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf023,0xf025,0xf025,0xf027,0xf028,0xf02b,0xf02b,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04b,0xf04c,0xf04e,0xf04e,0xf050,0xf050,0xf052,0xf052,0xf05e,0xf05e,0xf063,0xf063,0xf067,0xf067,0xf06a,0xf06a,0xf06e,0xf06e,0xf071,0xf071,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf084,0xf091,0xf091,0xf0ac,0xf0ad,0xf0b0,0xf0b0,0xf0c5,0xf0c5,0xf0c7,0xf0c8,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf11b,0xf11c,0xf121,0xf121,0xf129,0xf12a,0xf140,0xf140,0xf144,0xf144,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf187,0xf188,0xf191,0xf192,0xf1b3,0xf1b3,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1ea,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf21e,0xf21e,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2bd,0xf2bd,0xf2d0,0xf2d0,0xf2f1,0xf2f2,0xf302,0xf302,0xf3c1,0xf3c1,0xf3fd,0xf3fd,0xf410,0xf410,0xf462,0xf462,0xf466,0xf466,0xf51f,0xf51f,0xf543,0xf543,0xf547,0xf547,0xf54c,0xf54c,0xf553,0xf553,0xf56d,0xf56d,0xf5a2,0xf5a2,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf756,0xf756,0xf794,0xf794,0xf815,0xf815,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 }; - static constexpr ImWchar range_pf[] = { 0x2198,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21f3,0x21f3,0x21f7,0x21f8,0x21fa,0x21fb,0x221a,0x221a,0x227a,0x227f,0x2284,0x2284,0x22bf,0x22c8,0x2349,0x2349,0x235a,0x235e,0x2360,0x2361,0x2364,0x2367,0x237a,0x237b,0x237d,0x237d,0x237f,0x2380,0x23b2,0x23b5,0x23cc,0x23cc,0x23f4,0x23f7,0x2427,0x243a,0x243d,0x243d,0x2443,0x2443,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2605,0x2605,0x2699,0x2699,0x278a,0x278e,0xe001,0xe001,0xff21,0xff3a,0x0,0x0 }; + static constexpr ImWchar range_fa[] = { 0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf023,0xf025,0xf025,0xf027,0xf028,0xf02b,0xf02b,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04b,0xf04c,0xf04e,0xf04e,0xf050,0xf050,0xf052,0xf052,0xf05e,0xf05e,0xf063,0xf063,0xf067,0xf067,0xf06a,0xf06a,0xf06e,0xf06e,0xf071,0xf071,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf084,0xf091,0xf091,0xf0ac,0xf0ad,0xf0b0,0xf0b0,0xf0c5,0xf0c5,0xf0c7,0xf0c8,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf11b,0xf11c,0xf121,0xf121,0xf129,0xf12a,0xf140,0xf140,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf187,0xf188,0xf191,0xf192,0xf1b3,0xf1b3,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1ea,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf21e,0xf21e,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2bd,0xf2bd,0xf2f2,0xf2f2,0xf302,0xf302,0xf3c1,0xf3c1,0xf3fd,0xf3fd,0xf410,0xf410,0xf462,0xf462,0xf466,0xf466,0xf51f,0xf51f,0xf54c,0xf54c,0xf553,0xf553,0xf56d,0xf56d,0xf5a2,0xf5a2,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf756,0xf756,0xf794,0xf794,0xf815,0xf815,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 }; + static constexpr ImWchar range_pf[] = { 0x2198,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21f3,0x21f3,0x21f7,0x21f8,0x21fa,0x21fb,0x221a,0x221a,0x227a,0x227f,0x2284,0x2284,0x22bf,0x22c8,0x2349,0x2349,0x235a,0x235e,0x2360,0x2361,0x2364,0x2366,0x237a,0x237b,0x237d,0x237d,0x237f,0x2380,0x23b2,0x23b5,0x23cc,0x23cc,0x23f4,0x23f7,0x2427,0x243a,0x243d,0x243d,0x2443,0x2443,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2605,0x2605,0x2699,0x2699,0x278a,0x278e,0xe001,0xe001,0xff21,0xff3a,0x0,0x0 }; // clang-format on { diff --git a/pcsx2/ImGui/ImGuiOverlays.cpp b/pcsx2/ImGui/ImGuiOverlays.cpp index b7dd4fac46..5a81630f44 100644 --- a/pcsx2/ImGui/ImGuiOverlays.cpp +++ b/pcsx2/ImGui/ImGuiOverlays.cpp @@ -240,7 +240,8 @@ __ri void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, f if (GSConfig.OsdShowIndicators) { const float target_speed = VMManager::GetTargetSpeed(); - const bool is_normal_speed = (target_speed == EmuConfig.EmulationSpeed.NominalScalar); + const bool is_normal_speed = (target_speed == EmuConfig.EmulationSpeed.NominalScalar || + VMManager::IsTargetSpeedAdjustedToHost()); if (!is_normal_speed) { if (target_speed == EmuConfig.EmulationSpeed.SlomoScalar) // Slow-Motion diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index c5e5b8a8d7..5ccd801a75 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -10,12 +10,13 @@ #include "Config.h" #include "GS.h" #include "CDVD/CDVDcommon.h" +#include "Host.h" +#include "Host/AudioStream.h" #include "SIO/Memcard/MemoryCardFile.h" #include "SIO/Pad/Pad.h" #include "USB/USB.h" #include "fmt/format.h" - #ifdef _WIN32 #include "common/RedtapeWindows.h" #include @@ -1025,10 +1026,42 @@ bool Pcsx2Config::GSOptions::UseHardwareRenderer() const return (Renderer != GSRendererType::Null && Renderer != GSRendererType::SW); } +static constexpr const std::array s_spu2_sync_mode_names = { + "Disabled", + "TimeStretch" +}; +static constexpr const std::array s_spu2_sync_mode_display_names = { + TRANSLATE_NOOP("Pcsx2Config", "Disabled (Noisy)"), + TRANSLATE_NOOP("Pcsx2Config", "TimeStretch (Recommended)"), +}; + +const char* Pcsx2Config::SPU2Options::GetSyncModeName(SPU2SyncMode mode) +{ + return (static_cast(mode) < s_spu2_sync_mode_names.size()) ? s_spu2_sync_mode_names[static_cast(mode)] : ""; +} + +const char* Pcsx2Config::SPU2Options::GetSyncModeDisplayName(SPU2SyncMode mode) +{ + return (static_cast(mode) < s_spu2_sync_mode_display_names.size()) ? + Host::TranslateToCString("Pcsx2Config", s_spu2_sync_mode_display_names[static_cast(mode)]) : + ""; +} + +std::optional Pcsx2Config::SPU2Options::ParseSyncMode(const char* name) +{ + for (u8 i = 0; i < static_cast(SPU2SyncMode::Count); i++) + { + if (std::strcmp(name, s_spu2_sync_mode_names[i]) == 0) + return static_cast(i); + } + + return std::nullopt; +} + + Pcsx2Config::SPU2Options::SPU2Options() { bitset = 0; - OutputModule = "cubeb"; } void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap) @@ -1042,7 +1075,6 @@ void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBoolEx(MsgVoiceOff, "Show_Messages_Voice_Off"); SettingsWrapBitBoolEx(MsgDMA, "Show_Messages_DMA_Transfer"); SettingsWrapBitBoolEx(MsgAutoDMA, "Show_Messages_AutoDMA"); - SettingsWrapBitBoolEx(MsgOverruns, "Show_Messages_Overruns"); SettingsWrapBitBoolEx(MsgCache, "Show_Messages_CacheStats"); SettingsWrapBitBoolEx(AccessLog, "Log_Register_Access"); @@ -1061,7 +1093,6 @@ void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap) MsgVoiceOff = false; MsgDMA = false; MsgAutoDMA = false; - MsgOverruns = false; MsgCache = false; AccessLog = false; DMALog = false; @@ -1071,28 +1102,19 @@ void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap) RegDump = false; } } - { - SettingsWrapSection("SPU2/Mixing"); - - SettingsWrapEntry(FinalVolume); - } { SettingsWrapSection("SPU2/Output"); - - SettingsWrapEntry(OutputModule); - SettingsWrapEntry(BackendName); + SettingsWrapEntry(OutputVolume); + SettingsWrapEntry(FastForwardVolume); + SettingsWrapEntry(OutputMuted); + SettingsWrapParsedEnum(Backend, "Backend", &AudioStream::ParseBackendName, &AudioStream::GetBackendName); + SettingsWrapParsedEnum(SyncMode, "SyncMode", &ParseSyncMode, &GetSyncModeName); + SettingsWrapEntry(DriverName); SettingsWrapEntry(DeviceName); - SettingsWrapEntry(Latency); - SettingsWrapEntry(OutputLatency); - SettingsWrapBitBool(OutputLatencyMinimal); - SynchMode = static_cast(wrap.EntryBitfield(CURRENT_SETTINGS_SECTION, "SynchMode", static_cast(SynchMode), static_cast(SynchMode))); - SettingsWrapEntry(SpeakerConfiguration); - SettingsWrapEntry(DplDecodingLevel); + StreamParameters.LoadSave(wrap, CURRENT_SETTINGS_SECTION); + } } - - // clampy clamp -} bool Pcsx2Config::SPU2Options::operator!=(const SPU2Options& right) const { @@ -1102,21 +1124,12 @@ bool Pcsx2Config::SPU2Options::operator!=(const SPU2Options& right) const bool Pcsx2Config::SPU2Options::operator==(const SPU2Options& right) const { return OpEqu(bitset) && - - OpEqu(SynchMode) && - - OpEqu(FinalVolume) && - OpEqu(Latency) && - OpEqu(OutputLatency) && - OpEqu(SpeakerConfiguration) && - OpEqu(DplDecodingLevel) && - - OpEqu(SequenceLenMS) && - OpEqu(SeekWindowMS) && - OpEqu(OverlapMS) && - - OpEqu(OutputModule) && - OpEqu(BackendName) && + OpEqu(OutputVolume) && + OpEqu(FastForwardVolume) && + OpEqu(OutputMuted) && + OpEqu(Backend) && + OpEqu(StreamParameters) && + OpEqu(DriverName) && OpEqu(DeviceName); } diff --git a/pcsx2/SPU2/ADSR.cpp b/pcsx2/SPU2/ADSR.cpp index f679afb381..5d264f7198 100644 --- a/pcsx2/SPU2/ADSR.cpp +++ b/pcsx2/SPU2/ADSR.cpp @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" +#include "SPU2/defs.h" #include "common/Assertions.h" diff --git a/pcsx2/SPU2/Debug.cpp b/pcsx2/SPU2/Debug.cpp index 7b7de2772c..7b2447bb16 100644 --- a/pcsx2/SPU2/Debug.cpp +++ b/pcsx2/SPU2/Debug.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" +#include "SPU2/Debug.h" +#include "SPU2/regs.h" #include "Config.h" #include "common/Console.h" diff --git a/pcsx2/SPU2/Debug.h b/pcsx2/SPU2/Debug.h index 82e26bd2e7..ce57eb1c54 100644 --- a/pcsx2/SPU2/Debug.h +++ b/pcsx2/SPU2/Debug.h @@ -5,6 +5,8 @@ #include "Config.h" +#include "SPU2/defs.h" + namespace SPU2 { #ifdef PCSX2_DEVBUILD @@ -17,7 +19,6 @@ namespace SPU2 __fi static bool MsgVoiceOff() { return EmuConfig.SPU2.MsgVoiceOff; } __fi static bool MsgDMA() { return EmuConfig.SPU2.MsgDMA; } __fi static bool MsgAutoDMA() { return EmuConfig.SPU2.MsgAutoDMA; } - __fi static bool MsgOverruns() { return EmuConfig.SPU2.MsgOverruns; } __fi static bool MsgCache() { return EmuConfig.SPU2.MsgCache; } __fi static bool AccessLog() { return EmuConfig.SPU2.AccessLog; } @@ -93,7 +94,7 @@ namespace WaveDump extern void Open(); extern void Close(); extern void WriteCore(uint coreidx, CoreSourceType src, s16 left, s16 right); - extern void WriteCore(uint coreidx, CoreSourceType src, const StereoOut16& sample); + extern void WriteCore(uint coreidx, CoreSourceType src, const StereoOut32& sample); } // namespace WaveDump using WaveDump::CoreSrc_DryVoiceMix; diff --git a/pcsx2/SPU2/Dma.cpp b/pcsx2/SPU2/Dma.cpp index fc010eb729..2583ebc91a 100644 --- a/pcsx2/SPU2/Dma.cpp +++ b/pcsx2/SPU2/Dma.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" +#include "SPU2/defs.h" +#include "SPU2/Debug.h" #include "SPU2/Dma.h" #include "SPU2/spu2.h" #include "R3000A.h" diff --git a/pcsx2/SPU2/DplIIdecoder.cpp b/pcsx2/SPU2/DplIIdecoder.cpp deleted file mode 100644 index 2d4720e383..0000000000 --- a/pcsx2/SPU2/DplIIdecoder.cpp +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#include "Global.h" - -// FIXME Not yet used so let's comment it out. -/*static const u8 sLogTable[256] = { - 0x00, 0x3C, 0x60, 0x78, 0x8C, 0x9C, 0xA8, 0xB4, 0xBE, 0xC8, 0xD0, 0xD8, 0xDE, 0xE4, 0xEA, 0xF0, - 0xF6, 0xFA, 0xFE, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x16, 0x1A, 0x1E, 0x20, 0x24, 0x26, 0x2A, 0x2C, - 0x2E, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, 0x4C, 0x4E, 0x50, - 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5A, 0x5C, 0x5E, 0x60, 0x60, 0x62, 0x64, 0x66, 0x66, 0x68, - 0x6A, 0x6A, 0x6C, 0x6E, 0x6E, 0x70, 0x70, 0x72, 0x74, 0x74, 0x76, 0x76, 0x78, 0x7A, 0x7A, 0x7C, - 0x7C, 0x7E, 0x7E, 0x80, 0x80, 0x82, 0x82, 0x84, 0x84, 0x86, 0x86, 0x88, 0x88, 0x8A, 0x8A, 0x8C, - 0x8C, 0x8C, 0x8E, 0x8E, 0x90, 0x90, 0x92, 0x92, 0x92, 0x94, 0x94, 0x96, 0x96, 0x96, 0x98, 0x98, - 0x9A, 0x9A, 0x9A, 0x9C, 0x9C, 0x9C, 0x9E, 0x9E, 0xA0, 0xA0, 0xA0, 0xA2, 0xA2, 0xA2, 0xA4, 0xA4, - 0xA4, 0xA6, 0xA6, 0xA6, 0xA8, 0xA8, 0xA8, 0xAA, 0xAA, 0xAA, 0xAC, 0xAC, 0xAC, 0xAC, 0xAE, 0xAE, - 0xAE, 0xB0, 0xB0, 0xB0, 0xB2, 0xB2, 0xB2, 0xB2, 0xB4, 0xB4, 0xB4, 0xB6, 0xB6, 0xB6, 0xB6, 0xB8, - 0xB8, 0xB8, 0xB8, 0xBA, 0xBA, 0xBA, 0xBC, 0xBC, 0xBC, 0xBC, 0xBE, 0xBE, 0xBE, 0xBE, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC4, 0xC4, 0xC4, 0xC4, 0xC6, 0xC6, 0xC6, 0xC6, 0xC8, - 0xC8, 0xC8, 0xC8, 0xC8, 0xCA, 0xCA, 0xCA, 0xCA, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCE, 0xCE, 0xCE, - 0xCE, 0xCE, 0xD0, 0xD0, 0xD0, 0xD0, 0xD0, 0xD2, 0xD2, 0xD2, 0xD2, 0xD2, 0xD4, 0xD4, 0xD4, 0xD4, - 0xD4, 0xD6, 0xD6, 0xD6, 0xD6, 0xD6, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xDA, 0xDA, 0xDA, 0xDA, - 0xDA, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE, 0xE0, 0xE0, 0xE0, -};*/ - -static float Gfl = 0, Gfr = 0; -static float LMax = 0, RMax = 0; - -static float AccL = 0; -static float AccR = 0; - -constexpr float Scale = 4294967296.0f; // tweak this value to change the overall output volume - -constexpr float GainL = 0.80f * Scale; -constexpr float GainR = 0.80f * Scale; - -constexpr float GainC = 0.75f * Scale; - -constexpr float GainSL = 0.90f * Scale; -constexpr float GainSR = 0.90f * Scale; - -constexpr float GainLFE = 0.90f * Scale; - -constexpr float AddCLR = 0.20f * Scale; // Stereo expansion - -extern void ResetDplIIDecoder() -{ - Gfl = 0; - Gfr = 0; - LMax = 0; - RMax = 0; - AccL = 0; - AccR = 0; -} - -void ProcessDplIISample32(const StereoOut16& src, Stereo51Out32DplII* s) -{ - const float IL = src.Left / static_cast(1 << 16); - const float IR = src.Right / static_cast(1 << 16); - - // Calculate center channel and LFE - const float C = (IL + IR) * 0.5f; - const float SUB = C; // no need to lowpass, the speaker amplifier should take care of it - - float L = IL - C; // Effective L/R data - float R = IR - C; - - // Peak L/R - const float PL = std::abs(L); - const float PR = std::abs(R); - - AccL += (PL - AccL) * 0.1f; - AccR += (PR - AccR) * 0.1f; - - // Calculate power balance - const float Balance = (AccR - AccL); // -1 .. 1 - - // If the power levels are different, then the audio is meant for the front speakers - const float Frontness = std::abs(Balance); - const float Rearness = 1 - Frontness; // And the other way around - - // Equalize the power levels for L/R - const float B = std::min(0.9f, std::max(-0.9f, Balance)); - - const float VL = L / (1 - B); // if B>0, it means R>L, so increase L, else decrease L - const float VR = R / (1 + B); // vice-versa - - // 1.73+1.22 = 2.94; 2.94 = 0.34 = 0.9996; Close enough. - // The range for VL/VR is approximately 0..1, - // But in the cases where VL/VR are > 0.5, Rearness is 0 so it should never overflow. - constexpr float RearScale = 0.34f * 2; - - const float SL = (VR * 1.73f - VL * 1.22f) * RearScale * Rearness; - const float SR = (VR * 1.22f - VL * 1.73f) * RearScale * Rearness; - // Possible experiment: Play with stereo expension levels on rear - - // Adjust the volume of the front speakers based on what we calculated above - L *= Frontness; - R *= Frontness; - - const s32 CX = static_cast(C * AddCLR); - - s->Left = static_cast(L * GainL) + CX; - s->Right = static_cast(R * GainR) + CX; - s->Center = static_cast(C * GainC); - s->LFE = static_cast(SUB * GainLFE); - s->LeftBack = static_cast(SL * GainSL); - s->RightBack = static_cast(SR * GainSR); -} - -void ProcessDplIISample16(const StereoOut16& src, Stereo51Out16DplII* s) -{ - Stereo51Out32DplII ss; - ProcessDplIISample32(src, &ss); - - s->Left = ss.Left >> 16; - s->Right = ss.Right >> 16; - s->Center = ss.Center >> 16; - s->LFE = ss.LFE >> 16; - s->LeftBack = ss.LeftBack >> 16; - s->RightBack = ss.RightBack >> 16; -} - -void ProcessDplSample32(const StereoOut16& src, Stereo51Out32Dpl* s) -{ - const float ValL = src.Left / static_cast(1 << 16); - const float ValR = src.Right / static_cast(1 << 16); - - const float C = (ValL + ValR) * 0.5f; //+15.8 - const float S = (ValL - ValR) * 0.5f; - - const float L = ValL - C; //+15.8 - const float R = ValR - C; - - const float SUB = C; - - const s32 CX = static_cast(C * AddCLR); // +15.16 - - s->Left = static_cast(L * GainL) + CX; // +15.16 = +31, can grow to +32 if (GainL + AddCLR)>255 - s->Right = static_cast(R * GainR) + CX; - s->Center = static_cast(C * GainC); // +15.16 = +31 - s->LFE = static_cast(SUB * GainLFE); - s->LeftBack = static_cast(S * GainSL); - s->RightBack = static_cast(S * GainSR); -} - -void ProcessDplSample16(const StereoOut16& src, Stereo51Out16Dpl* s) -{ - Stereo51Out32Dpl ss; - ProcessDplSample32(src, &ss); - - s->Left = ss.Left >> 16; - s->Right = ss.Right >> 16; - s->Center = ss.Center >> 16; - s->LFE = ss.LFE >> 16; - s->LeftBack = ss.LeftBack >> 16; - s->RightBack = ss.RightBack >> 16; -} diff --git a/pcsx2/SPU2/Global.h b/pcsx2/SPU2/Global.h deleted file mode 100644 index e915347ff2..0000000000 --- a/pcsx2/SPU2/Global.h +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#pragma once - -struct StereoOut16; -struct StereoOut32; -struct StereoOutFloat; - -struct V_Core; - -#include "defs.h" -#include "regs.h" - -#include "Debug.h" -#include "Mixer.h" -#include "SndOut.h" diff --git a/pcsx2/SPU2/Mixer.cpp b/pcsx2/SPU2/Mixer.cpp index 9a7b58fad8..1c8d47358a 100644 --- a/pcsx2/SPU2/Mixer.cpp +++ b/pcsx2/SPU2/Mixer.cpp @@ -1,12 +1,14 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "common/Assertions.h" - -#include "SPU2/Global.h" +#include "Host/AudioStream.h" +#include "SPU2/Debug.h" +#include "SPU2/defs.h" #include "SPU2/spu2.h" #include "SPU2/interpolate_table.h" +#include "common/Assertions.h" + static const s32 tbl_XA_Factor[16][2] = { {0, 0}, @@ -565,16 +567,7 @@ static StereoOut32 DCFilter(StereoOut32 input) { return output; } -// used to throttle the output rate of cache stat reports -static int p_cachestat_counter = 0; - -// Gcc does not want to inline it when lto is enabled because some functions growth too much. -// The function is big enought to see any speed impact. -- Gregory -#ifndef __POSIX__ -__forceinline -#endif - void - Mix() +__forceinline void spu2Mix() { // Note: Playmode 4 is SPDIF, which overrides other inputs. StereoOut32 InputData[2] = @@ -638,17 +631,23 @@ __forceinline Out = ApplyVolume(Out, {0x4fff, 0x4fff}); Out = DCFilter(Out); - // Final clamp, take care not to exceed 16 bits from here on - Out = clamp_mix(Out); - SndBuffer::Write(StereoOut16(Out)); +#ifdef PCSX2_DEVBUILD + // Log final output to wavefile. + WaveDump::WriteCore(1, CoreSrc_External, Out); +#endif + + spu2Output(Out); // Update AutoDMA output positioning OutPos++; if (OutPos >= 0x200) OutPos = 0; - if (IsDevBuild) + if constexpr (IsDevBuild) { + // used to throttle the output rate of cache stat reports + static int p_cachestat_counter = 0; + p_cachestat_counter++; if (p_cachestat_counter > (48000 * 10)) { diff --git a/pcsx2/SPU2/Mixer.h b/pcsx2/SPU2/Mixer.h deleted file mode 100644 index bce89776ee..0000000000 --- a/pcsx2/SPU2/Mixer.h +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#pragma once - -extern void Mix(); diff --git a/pcsx2/SPU2/ReadInput.cpp b/pcsx2/SPU2/ReadInput.cpp index d406f815c7..197c3c6874 100644 --- a/pcsx2/SPU2/ReadInput.cpp +++ b/pcsx2/SPU2/ReadInput.cpp @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "Global.h" #include "Dma.h" #include "IopDma.h" #include "IopHw.h" - -#include "spu2.h" // required for ENABLE_NEW_IOPDMA_SPU2 define +#include "SPU2/Debug.h" +#include "SPU2/defs.h" +#include "SPU2/spu2.h" // Core 0 Input is "SPDIF mode" - Source audio is AC3 compressed. diff --git a/pcsx2/SPU2/RegTable.cpp b/pcsx2/SPU2/RegTable.cpp index f2069ede87..19927374ea 100644 --- a/pcsx2/SPU2/RegTable.cpp +++ b/pcsx2/SPU2/RegTable.cpp @@ -1,7 +1,8 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "Global.h" +#include "SPU2/defs.h" +#include "SPU2/regs.h" #define U16P(x) ((u16*)&(x)) diff --git a/pcsx2/SPU2/Reverb.cpp b/pcsx2/SPU2/Reverb.cpp index 4e2a63a71e..94ab88509d 100644 --- a/pcsx2/SPU2/Reverb.cpp +++ b/pcsx2/SPU2/Reverb.cpp @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "Global.h" +#include "SPU2/defs.h" #include "GS/GSVector.h" #include "common/Console.h" diff --git a/pcsx2/SPU2/ReverbResample.cpp b/pcsx2/SPU2/ReverbResample.cpp index 4fbb256b81..426caf3d99 100644 --- a/pcsx2/SPU2/ReverbResample.cpp +++ b/pcsx2/SPU2/ReverbResample.cpp @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #include "GS/GSVector.h" -#include "Global.h" +#include "SPU2/defs.h" MULTI_ISA_UNSHARED_START diff --git a/pcsx2/SPU2/SndOut.cpp b/pcsx2/SPU2/SndOut.cpp deleted file mode 100644 index eeba83f985..0000000000 --- a/pcsx2/SPU2/SndOut.cpp +++ /dev/null @@ -1,1043 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#include "SPU2/Global.h" -#include "SPU2/spu2.h" -#include "GS/GSCapture.h" -#include "GS/GSVector.h" -#include "Host.h" - -#include "common/Assertions.h" -#include "common/Timer.h" - -#include "SoundTouch.h" - -const StereoOut32 StereoOut32::Empty(0, 0); - -static bool s_audio_capture_active = false; - -//Uncomment the next line to use the old time stretcher -//#define SPU2X_USE_OLD_STRETCHER -//#define SPU2X_HANDLE_STRETCH_OVERRUNS - -namespace -{ - class NullOutModule final : public SndOutModule - { - public: - bool Init() override { return true; } - void Close() override {} - void SetPaused(bool paused) override {} - int GetEmptySampleCount() override { return 0; } - - const char* GetIdent() const override - { - return "nullout"; - } - - const char* GetDisplayName() const override - { - return TRANSLATE_NOOP("SPU2", "No Sound (Emulate SPU2 only)"); - } - - const char* const* GetBackendNames() const override - { - return nullptr; - } - - std::vector GetOutputDeviceList(const char* driver) const override - { - return {}; - } - }; -} // namespace - -static NullOutModule s_NullOut; -static SndOutModule* NullOut = &s_NullOut; -extern SndOutModule* CubebOut; - -#ifdef _WIN32 -extern SndOutModule* XAudio2Out; -#endif - -static SndOutModule* mods[] = - { - NullOut, - CubebOut, -#ifdef _WIN32 - XAudio2Out, -#endif -}; - -static SndOutModule* s_output_module; - -std::span GetSndOutModules() -{ - return mods; -} - -static SndOutModule* FindOutputModule(const char* name) -{ - for (u32 i = 0; i < std::size(mods); i++) - { - if (std::strcmp(mods[i]->GetIdent(), name) == 0) - return mods[i]; - } - - return nullptr; -} - -const char* const* GetOutputModuleBackends(const char* omodid) -{ - if (SndOutModule* mod = FindOutputModule(omodid)) - return mod->GetBackendNames(); - - return nullptr; -} - -SndOutDeviceInfo::SndOutDeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_) - : name(std::move(name_)) - , display_name(std::move(display_name_)) - , minimum_latency_frames(minimum_latency_) -{ -} - -SndOutDeviceInfo::~SndOutDeviceInfo() = default; - -std::vector GetOutputDeviceList(const char* omodid, const char* driver) -{ - std::vector ret; - if (SndOutModule* mod = FindOutputModule(omodid)) - ret = mod->GetOutputDeviceList(driver); - return ret; -} - -namespace SndBuffer -{ - static float s_final_volume = 1.0f; - - static bool s_underrun_freeze = 0; - - // data prediction amount, used to "commit" data that hasn't - // finished timestretch processing. - static s32 s_predict_data = 0; - - // records last buffer status (fill %, range -100 to 100, with 0 being 50% full) - static float s_last_pct = 0; - - static float s_last_emergency_adj = 0.0f; - static float s_cTempo = 1.0f; - static float s_eTempo = 1.0f; - static int s_ss_freeze = 0; - - static std::unique_ptr s_staging_buffer; - static std::unique_ptr s_float_buffer; - - static int s_staging_progress = 0; - - static std::unique_ptr s_output_buffer; - static s32 s_output_buffer_size = 0; - - // TODO: Replace these with proper atomics. - alignas(4) static volatile s32 m_rpos = 0; - alignas(4) static volatile s32 m_wpos = 0; - - static bool CheckUnderrunStatus(int& nSamples, int& quietSampleCount); - - static void soundtouchInit(); - static void soundtouchClearContents(); - static void soundtouchCleanup(); - static void timeStretchWrite(); - static void timeStretchUnderrun(); -#ifdef SPU2X_HANDLE_STRETCH_OVERRUNS - static s32 timeStretchOverrun(); -#endif - - static void PredictDataWrite(int samples); - static float GetStatusPct(); - static void UpdateTempoChangeSoundTouch(); - - static void _WriteSamples(StereoOut16* bData, int nSamples); - static void _WriteSamples_Safe(StereoOut16* bData, int nSamples); - - static void _WriteSamples_Internal(StereoOut16* bData, int nSamples); - static void _DropSamples_Internal(int nSamples); - - static int _GetApproximateDataInBuffer(); -} // namespace SndBuffer - -static int GetAlignedBufferSize(int comp) -{ - return (comp + SndOutPacketSize - 1) & ~(SndOutPacketSize - 1); -} - -s32 SPU2::GetOutputVolume() -{ - return static_cast(std::round(SndBuffer::s_final_volume * 100.0f)); -} - -void SPU2::SetOutputVolume(s32 volume) -{ - SndBuffer::s_final_volume = static_cast(std::clamp(volume, 0, Pcsx2Config::SPU2Options::MAX_VOLUME)) / 100.0f; -} - -// Returns TRUE if there is data to be output, or false if no data -// is available to be copied. -bool SndBuffer::CheckUnderrunStatus(int& nSamples, int& quietSampleCount) -{ - quietSampleCount = 0; - - int data = _GetApproximateDataInBuffer(); - if (s_underrun_freeze) - { - int toFill = s_output_buffer_size / ((EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::NoSync) ? 32 : 400); // TimeStretch and Async off? - toFill = GetAlignedBufferSize(toFill); - - // toFill is now aligned to a SndOutPacket - - if (data < toFill) - { - quietSampleCount = nSamples; - nSamples = 0; - return false; - } - - s_underrun_freeze = false; - if (SPU2::MsgOverruns()) - SPU2::ConLog(" * SPU2 > Underrun compensation (%d packets buffered)\n", toFill / SndOutPacketSize); - s_last_pct = 0.0; // normalize timestretcher - } - else if (data < nSamples) - { - quietSampleCount = nSamples - data; - nSamples = data; - s_underrun_freeze = true; - - if (EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch) // TimeStrech on - timeStretchUnderrun(); - - return nSamples != 0; - } - - return true; -} - -int SndBuffer::_GetApproximateDataInBuffer() -{ - // WARNING: not necessarily 100% up to date by the time it's used, but it will have to do. - return (m_wpos + s_output_buffer_size - m_rpos) % s_output_buffer_size; -} - -void SndBuffer::_WriteSamples_Internal(StereoOut16* bData, int nSamples) -{ - // WARNING: This assumes the write will NOT wrap around, - // and also assumes there's enough free space in the buffer. - - std::memcpy(s_output_buffer.get() + m_wpos, bData, nSamples * sizeof(StereoOut16)); - m_wpos = (m_wpos + nSamples) % s_output_buffer_size; -} - -void SndBuffer::_DropSamples_Internal(int nSamples) -{ - m_rpos = (m_rpos + nSamples) % s_output_buffer_size; -} - -void SndBuffer::_WriteSamples_Safe(StereoOut16* bData, int nSamples) -{ - // WARNING: This code assumes there's only ONE writing process. - if ((s_output_buffer_size - m_wpos) < nSamples) - { - const int b1 = s_output_buffer_size - m_wpos; - const int b2 = nSamples - b1; - - _WriteSamples_Internal(bData, b1); - _WriteSamples_Internal(bData + b1, b2); - } - else - { - _WriteSamples_Internal(bData, nSamples); - } -} - -static __fi StereoOut16 ApplyVolume(StereoOut16 frame, float volume) -{ - // TODO: This could be done with SSE/NEON, but we'd only be processing half our vector width. - // It happens on the audio thread anyway, so no biggie, but someone might want to do it at some point. - return StereoOut16( - static_cast(std::clamp(static_cast(frame.Left) * volume, -32768.0f, 32767.0f)), - static_cast(std::clamp(static_cast(frame.Right) * volume, -32768.0f, 32767.0f))); -} - -// Note: When using with 32 bit output buffers, the user of this function is responsible -// for shifting the values to where they need to be manually. The fixed point depth of -// the sample output is determined by the SndOutVolumeShift, which is the number of bits -// to shift right to get a 16 bit result. -template -void SndBuffer::ReadSamples(T* bData, int nSamples) -{ - // Problem: - // If the SPU2 gets even the least bit out of sync with the SndOut device, - // the readpos of the circular buffer will overtake the writepos, - // leading to a prolonged period of hopscotching read/write accesses (ie, - // lots of staticy crap sound for several seconds). - // - // Fix: - // If the read position overtakes the write position, abort the - // transfer immediately and force the SndOut driver to wait until - // the read buffer has filled up again before proceeding. - // This will cause one brief hiccup that can never exceed the user's - // set buffer length in duration. - - int quietSamples = 0; - if (CheckUnderrunStatus(nSamples, quietSamples)) - { - pxAssume(nSamples <= SndOutPacketSize); - - // WARNING: This code assumes there's only ONE reading process. - int b1 = s_output_buffer_size - m_rpos; - - if (b1 > nSamples) - b1 = nSamples; - - const int b2 = nSamples - b1; - - if (std::is_same_v && s_final_volume == 1.0f) - { - // First part - if (b1 > 0) - std::memcpy(bData, &s_output_buffer[m_rpos], sizeof(StereoOut16) * b1); - - // Second part - if (b2 > 0) - std::memcpy(bData + b1, s_output_buffer.get(), sizeof(StereoOut16) * b2); - } - else - { - // First part - for (int i = 0; i < b1; i++) - bData[i].SetFrom(ApplyVolume(s_output_buffer[i + m_rpos], s_final_volume)); - - // Second part - for (int i = 0; i < b2; i++) - bData[i + b1].SetFrom(ApplyVolume(s_output_buffer[i], s_final_volume)); - } - - _DropSamples_Internal(nSamples); - } - - // If quietSamples != 0 it means we have an underrun... - // Let's just dull out some silence, because that's usually the least - // painful way of dealing with underruns: - if (quietSamples > 0) - std::memset(bData + nSamples, 0, sizeof(T) * quietSamples); -} - -template void SndBuffer::ReadSamples(StereoOut16*, int); -template void SndBuffer::ReadSamples(Stereo21Out16*, int); -template void SndBuffer::ReadSamples(Stereo40Out16*, int); -template void SndBuffer::ReadSamples(Stereo41Out16*, int); -template void SndBuffer::ReadSamples(Stereo51Out16*, int); -template void SndBuffer::ReadSamples(Stereo51Out16Dpl*, int); -template void SndBuffer::ReadSamples(Stereo51Out16DplII*, int); -template void SndBuffer::ReadSamples(Stereo71Out16*, int); - -void SndBuffer::_WriteSamples(StereoOut16* bData, int nSamples) -{ - s_predict_data = 0; - - // Problem: - // If the SPU2 gets out of sync with the SndOut device, the writepos of the - // circular buffer will overtake the readpos, leading to a prolonged period - // of hopscotching read/write accesses (ie, lots of staticy crap sound for - // several seconds). - // - // Compromise: - // When an overrun occurs, we adapt by discarding a portion of the buffer. - // The older portion of the buffer is discarded rather than incoming data, - // so that the overall audio synchronization is better. - - const int free = s_output_buffer_size - _GetApproximateDataInBuffer(); // -1, but the <= handles that - if (free <= nSamples) - { -// Disabled since the lock-free queue can't handle changing the read end from the write thread -#ifdef SPU2X_HANDLE_STRETCH_OVERRUNS - // Buffer overrun! - // Dump samples from the read portion of the buffer instead of dropping - // the newly written stuff. - - s32 comp = 0; - - if (EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch) // TimeStrech on - { - comp = timeStretchOverrun(); - } - else - { - // Toss half the buffer plus whatever's being written anew: - comp = GetAlignedBufferSize((m_size + nSamples) / 16); - if (comp > (m_size - SndOutPacketSize)) - comp = m_size - SndOutPacketSize; - } - - _DropSamples_Internal(comp); - - if (SPU2::MsgOverruns()) - SPU2::ConLog(" * SPU2 > Overrun Compensation (%d packets tossed)\n", comp / SndOutPacketSize); - lastPct = 0.0; // normalize the timestretcher -#else - if (SPU2::MsgOverruns()) - SPU2::ConLog(" * SPU2 > Overrun! 1 packet tossed)\n"); - s_last_pct = 0.0; // normalize the timestretcher - - // Toss the packet because we overran the buffer. - return; -#endif - } - - _WriteSamples_Safe(bData, nSamples); -} - -bool SndBuffer::Init(const char* modname) -{ - s_output_module = FindOutputModule(modname); - if (!s_output_module) - return false; - - // initialize sound buffer - // Buffer actually attempts to run ~50%, so allocate near double what - // the requested latency is: - - m_rpos = 0; - m_wpos = 0; - - const float latencyMS = EmuConfig.SPU2.Latency * 16; - s_output_buffer_size = GetAlignedBufferSize((int)(latencyMS * SampleRate / 1000.0f)); - s_output_buffer = std::make_unique(s_output_buffer_size); - s_underrun_freeze = false; - - s_staging_buffer = std::make_unique(SndOutPacketSize); - s_float_buffer = std::make_unique(SndOutPacketSize * 2); - s_staging_progress = 0; - - soundtouchInit(); // initializes the timestretching - - // initialize module - if (!s_output_module->Init()) - { - Cleanup(); - return false; - } - - return true; -} - -bool SndBuffer::IsOpen() -{ - return (s_output_module != nullptr); -} - -void SndBuffer::Cleanup() -{ - if (s_output_module) - { - s_output_module->Close(); - s_output_module = nullptr; - } - - soundtouchCleanup(); - - s_output_buffer.reset(); - s_staging_buffer.reset(); -} - -void SndBuffer::ClearContents() -{ - soundtouchClearContents(); - s_ss_freeze = 256; //Delays sound output for about 1 second. -} - -void SndBuffer::ResetBuffers() -{ - m_rpos = 0; - m_wpos = 0; -} - -void SPU2::SetOutputPaused(bool paused) -{ - s_output_module->SetPaused(paused); -} - -void SPU2::SetAudioCaptureActive(bool active) -{ - s_audio_capture_active = active; -} - -bool SPU2::IsAudioCaptureActive() -{ - return s_audio_capture_active; -} - -void SndBuffer::Write(StereoOut16 Sample) -{ -#ifdef PCSX2_DEVBUILD - // Log final output to wavefile. - WaveDump::WriteCore(1, CoreSrc_External, Sample); -#endif - - s_staging_buffer[s_staging_progress++] = Sample; - - // If we haven't accumulated a full packet yet, do nothing more: - if (s_staging_progress < SndOutPacketSize) - return; - s_staging_progress = 0; - - // We want to capture audio *before* time stretching. - if (s_audio_capture_active) - GSCapture::DeliverAudioPacket(reinterpret_cast(s_staging_buffer.get())); - - //Don't play anything directly after loading a savestate, avoids static killing your speakers. - if (s_ss_freeze > 0) - { - s_ss_freeze--; - std::memset(s_staging_buffer.get(), 0, sizeof(StereoOut16) * SndOutPacketSize); - } - else - { - if (EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch) - timeStretchWrite(); - else - _WriteSamples(s_staging_buffer.get(), SndOutPacketSize); - } -} - -////////////////////////////////////////////////////////////////////////// -// Time Stretching -////////////////////////////////////////////////////////////////////////// - -static std::unique_ptr pSoundTouch = nullptr; - -void SndBuffer::PredictDataWrite(int samples) -{ - s_predict_data += samples; -} - -// Calculate the buffer status percentage. -// Returns range from -1.0 to 1.0 -// 1.0 = buffer overflow! -// 0.0 = buffer nominal (50% full) -// -1.0 = buffer underflow! -float SndBuffer::GetStatusPct() -{ - // Get the buffer status of the output driver too, so that we can - // obtain a more accurate overall buffer status. - - const int drvempty = s_output_module->GetEmptySampleCount(); // / 2; - - //ConLog( "Data %d >>> driver: %d predict: %d\n", m_data, drvempty, m_predictData ); - - const int data = _GetApproximateDataInBuffer(); - float result = static_cast(data + s_predict_data - drvempty) - (s_output_buffer_size / 16); - result /= (s_output_buffer_size / 16); - return result; -} - - -//Alternative simple tempo adjustment. Based only on the soundtouch buffer state. -//Base algorithm: aim at specific average number of samples at the buffer (by GUI), and adjust tempo simply by current/target. -//An extra mechanism is added to keep adjustment at perfect 1:1 ratio (when emulation speed is stable around 100%) -// to prevent constant stretching/shrinking of packets if possible. -// This mechanism is triggered when the adjustment is close to 1:1 for long enough (defaults to 100 iterations within hys_ok_factor - defaults to 3%). -// 1:1 state is aborted when required adjustment goes beyond hys_bad_factor (defaults to 20%). -// -//To compensate for wide variation of the ratio due to relatively small size of the buffer, -// The required tempo is a running average of STRETCH_AVERAGE_LEN (defaults to 50) last calculations. -// This averaging slows down the respons time of the algorithm, but greatly stablize it towards steady stretching. -// -//Keeping the buffer at required latency: -// This algorithm stabilises when the actual latency is *. While this is just fine at 100% speed, -// it's problematic especially for slow speeds, as the number of actual samples at the buffer gets very small on that case, -// which may lead to underruns (or just too much latency when running very fast). -//To compensate for that, the algorithm has a slowly moving compensation factor which will eventually bring the actual latency to the required one. -//compensationDivider defines how slow this compensation changes. By default it's set to 100, -// which will finalize the compensation after about 200 iterations. -// -// Note, this algorithm is intentionally simplified by not taking extreme actions at extreme scenarios (mostly underruns when speed drops sharply), -// and let's the overrun/underrun protections do what they should (doesn't happen much though in practice, even at big FPS variations). -// -// These params were tested to show good respond and stability, on all audio systems (dsound, wav, port audio, xaudio2), -// even at extreme small latency of 50ms which can handle 50%-100% variations without audible glitches. - -constexpr int targetIPS = 750; - -//Additional performance note: since MAX_STRETCH_AVERAGE_LEN = 128 (or any power of 2), the '%' below -//could be replaced with a faster '&'. The compiler is highly likely to do it since all the values are unsigned. -#define AVERAGING_BUFFER_SIZE 256U -unsigned int AVERAGING_WINDOW = 50 * targetIPS / 750; - - -#define STRETCHER_RESET_THRESHOLD 5 -int gRequestStretcherReset = STRETCHER_RESET_THRESHOLD; -//Adds a value to the running average buffer, and return the new running average. -static float addToAvg(float val) -{ - static float avg_fullness[AVERAGING_BUFFER_SIZE]; - static unsigned int nextAvgPos = 0; - static unsigned int available = 0; // Make sure we're not averaging AVERAGING_WINDOW items if we inserted less. - if (gRequestStretcherReset >= STRETCHER_RESET_THRESHOLD) - available = 0; - - if (available < AVERAGING_BUFFER_SIZE) - available++; - - avg_fullness[nextAvgPos] = val; - nextAvgPos = (nextAvgPos + 1U) % AVERAGING_BUFFER_SIZE; - - const unsigned int actualWindow = std::min(available, AVERAGING_WINDOW); - const unsigned int first = (nextAvgPos - actualWindow + AVERAGING_BUFFER_SIZE) % AVERAGING_BUFFER_SIZE; - - // Possible optimization: if we know that actualWindow hasn't changed since - // last invocation, we could calculate the running average in O(1) instead of O(N) - // by keeping a running sum between invocations, and then - // do "runningSum = runningSum + val - avg_fullness[(first-1)%...]" instead of the following loop. - // Few gotchas: val overwrites first-1, handling actualWindow changes, etc. - // However, this isn't hot code, so unless proven otherwise, we can live with unoptimized code. - float sum = 0; - for (unsigned int i = first; i < first + actualWindow; i++) - { - sum += avg_fullness[i % AVERAGING_BUFFER_SIZE]; - } - sum = sum / actualWindow; - - return sum ? sum : 1; // 1 because that's the 100% perfect speed value -} - -template -static bool IsInRange(const T& val, const T& min, const T& max) -{ - return (min <= val && val <= max); -} - -//actual stretch algorithm implementation -void SndBuffer::UpdateTempoChangeSoundTouch() -{ -#ifndef SPU2X_USE_OLD_STRETCHER - const long targetSamplesReservoir = 48 * EmuConfig.SPU2.Latency; //48000*SndOutLatencyMS/1000 - //base aim at buffer filled % - float baseTargetFullness = static_cast(targetSamplesReservoir); ///(double)m_size;//0.05; - - //state vars - static bool inside_hysteresis; //=false; - static int hys_ok_count; //=0; - static float dynamicTargetFullness; //=baseTargetFullness; - if (gRequestStretcherReset >= STRETCHER_RESET_THRESHOLD) - { - if (SPU2::MsgOverruns()) - SPU2::ConLog("______> stretch: Reset.\n"); - inside_hysteresis = false; - hys_ok_count = 0; - dynamicTargetFullness = baseTargetFullness; - } - - const int data = _GetApproximateDataInBuffer(); - const float bufferFullness = static_cast(data); ///(float)m_size; - - //Algorithm params: (threshold params (hysteresis), etc) - constexpr float hys_ok_factor = 1.04f; - constexpr float hys_bad_factor = 1.2f; - const int hys_min_ok_count = std::clamp((int)(50.0 * (float)targetIPS / 750.0), 2, 100); //consecutive iterations within hys_ok before going to 1:1 mode - const int compensationDivider = std::clamp((int)(100.0 * (float)targetIPS / 750), 15, 150); - - float tempoAdjust = bufferFullness / dynamicTargetFullness; - const float avgerage = addToAvg(tempoAdjust); - tempoAdjust = avgerage; - - // Dampen the adjustment to avoid overshoots (this means the average will compensate to the other side). - // This is different than simply bigger averaging window since bigger window also has bigger "momentum", - // so it's slower to slow down when it gets close to the equilibrium state and can therefore resonate. - // The dampening (sqrt was chosen for no very good reason) manages to mostly prevent that. - tempoAdjust = sqrt(tempoAdjust); - - tempoAdjust = std::clamp(tempoAdjust, 0.05f, 10.0f); - - if (tempoAdjust < 1) - baseTargetFullness /= sqrt(tempoAdjust); // slightly increase latency when running slow. - - dynamicTargetFullness += (baseTargetFullness / tempoAdjust - dynamicTargetFullness) / (double)compensationDivider; - if (IsInRange(tempoAdjust, 0.9f, 1.1f) && IsInRange(dynamicTargetFullness, baseTargetFullness * 0.9f, baseTargetFullness * 1.1f)) - dynamicTargetFullness = baseTargetFullness; - - if (!inside_hysteresis) - { - if (IsInRange(tempoAdjust, 1.0f / hys_ok_factor, hys_ok_factor)) - hys_ok_count++; - else - hys_ok_count = 0; - - if (hys_ok_count >= hys_min_ok_count) - { - inside_hysteresis = true; - if (SPU2::MsgOverruns()) - SPU2::ConLog("======> stretch: None (1:1)\n"); - } - } - else if (!IsInRange(tempoAdjust, 1.0f / hys_bad_factor, hys_bad_factor)) - { - if (SPU2::MsgOverruns()) - SPU2::ConLog("~~~~~~> stretch: Dynamic\n"); - inside_hysteresis = false; - hys_ok_count = 0; - } - - if (inside_hysteresis) - tempoAdjust = 1.0; - - if (SPU2::MsgOverruns()) - { - static int iters = 0; - static u64 last = 0; - - const u64 now = Common::Timer::GetCurrentValue(); - - if (Common::Timer::ConvertValueToSeconds(now - last) > 1.0f) - { //report buffers state and tempo adjust every second - SPU2::ConLog("buffers: %4d ms (%3.0f%%), tempo: %f, comp: %2.3f, iters: %d, (N-IPS:%d -> avg:%d, minokc:%d, div:%d) reset:%d\n", - (int)(data / 48), (double)(100.0 * bufferFullness / baseTargetFullness), (double)tempoAdjust, (double)(dynamicTargetFullness / baseTargetFullness), iters, (int)targetIPS, AVERAGING_WINDOW, hys_min_ok_count, compensationDivider, gRequestStretcherReset); - last = now; - iters = 0; - } - iters++; - } - - pSoundTouch->setTempo(tempoAdjust); - if (gRequestStretcherReset >= STRETCHER_RESET_THRESHOLD) - gRequestStretcherReset = 0; - -#else - - const float statusPct = GetStatusPct(); - const float pctChange = statusPct - s_last_pct; - - float tempoChange; - float emergencyAdj = 0; - float newcee = s_cTempo; // workspace var. for cTempo - - // IMPORTANT! - // If you plan to tweak these values, make sure you're using a release build - // OUTSIDE THE DEBUGGER to test it! The Visual Studio debugger can really cause - // erratic behavior in the audio buffers, and makes the timestretcher seem a - // lot more inconsistent than it really is. - - // We have two factors. - // * Distance from nominal buffer status (50% full) - // * The change from previous update to this update. - - // Prediction based on the buffer change: - // (linear seems to work better here) - - tempoChange = pctChange * 0.75f; - - if (statusPct * tempoChange < 0.0f) - { - // only apply tempo change if it is in synch with the buffer status. - // In other words, if the buffer is high (over 0%), and is decreasing, - // ignore it. It'll just muck things up. - - tempoChange = 0; - } - - // Sudden spikes in framerate can cause the nominal buffer status - // to go critical, in which case we have to enact an emergency - // stretch. The following cubic formulas do that. Values near - // the extremeites give much larger results than those near 0. - // And the value is added only this time, and does not accumulate. - // (otherwise a large value like this would cause problems down the road) - - // Constants: - // Weight - weights the statusPct's "emergency" consideration. - // higher values here will make the buffer perform more drastic - // compensations at the outer edges of the buffer (at -75 or +75% - // or beyond, for example). - - // Range - scales the adjustment to the given range (more or less). - // The actual range is dependent on the weight used, so if you increase - // Weight you'll usually want to decrease Range somewhat to compensate. - - // Prediction based on the buffer fill status: - - constexpr float statusWeight = 2.99f; - constexpr float statusRange = 0.068f; - - // "non-emergency" deadzone: In this area stretching will be strongly discouraged. - // Note: due tot he nature of timestretch latency, it's always a wee bit harder to - // cope with low fps (underruns) than it is high fps (overruns). So to help out a - // little, the low-end portions of this check are less forgiving than the high-sides. - - if (s_cTempo < 0.965f || s_cTempo > 1.060f || - pctChange < -0.38f || pctChange > 0.54f || - statusPct < -0.42f || statusPct > 0.70f || - s_eTempo < 0.89f || s_eTempo > 1.19f) - { - //printf("Emergency stretch: cTempo = %f eTempo = %f pctChange = %f statusPct = %f\n",cTempo,eTempo,pctChange,statusPct); - emergencyAdj = (pow(statusPct * statusWeight, 3.0f) * statusRange); - } - - // Smooth things out by factoring our previous adjustment into this one. - // It helps make the system 'feel' a little smarter by giving it at least - // one packet worth of history to help work off of: - - emergencyAdj = (emergencyAdj * 0.75f) + (s_last_emergency_adj * 0.25f); - - s_last_emergency_adj = emergencyAdj; - s_last_pct = statusPct; - - // Accumulate a fraction of the tempo change into the tempo itself. - // This helps the system run "smarter" to games that run consistently - // fast or slow by altering the base tempo to something closer to the - // game's active speed. In tests most games normalize within 2 seconds - // at 100ms latency, which is pretty good (larger buffers normalize even - // quicker). - - newcee += newcee * (tempoChange + emergencyAdj) * 0.03f; - - // Apply tempoChange as a scale of cTempo. That way the effect is proportional - // to the current tempo. (otherwise tempos rate of change at the extremes would - // be too drastic) - - float newTempo = newcee + (emergencyAdj * s_cTempo); - - // ... and as a final optimization, only stretch if the new tempo is outside - // a nominal threshold. Keep this threshold check small, because it could - // cause some serious side effects otherwise. (enlarging the cTempo check above - // is usually better/safer) - if (newTempo < 0.970f || newTempo > 1.045f) - { - s_cTempo = static_cast(newcee); - - if (newTempo < 0.10f) - newTempo = 0.10f; - else if (newTempo > 10.0f) - newTempo = 10.0f; - - if (s_cTempo < 0.15f) - s_cTempo = 0.15f; - else if (s_cTempo > 7.5f) - s_cTempo = 7.5f; - - pSoundTouch->setTempo(s_eTempo = static_cast(newTempo)); - - /*ConLog("* SPU2: [Nominal %d%%] [Emergency: %d%%] (baseTempo: %d%% ) (newTempo: %d%%) (buffer: %d%%)\n", - //(relation < 0.0) ? "Normalize" : "", - (int)(tempoChange * 100.0 * 0.03), - (int)(emergencyAdj * 100.0), - (int)(cTempo * 100.0), - (int)(newTempo * 100.0), - (int)(statusPct * 100.0) - );*/ - } - else - { - // Nominal operation -- turn off stretching. - // note: eTempo 'slides' toward 1.0 for smoother audio and better - // protection against spikes. - if (s_cTempo != 1.0f) - { - s_cTempo = 1.0f; - s_eTempo = (1.0f + s_eTempo) * 0.5f; - pSoundTouch->setTempo(s_eTempo); - } - else - { - if (s_eTempo != s_cTempo) - pSoundTouch->setTempo(s_eTempo = s_cTempo); - } - } -#endif // SPU2X_USE_OLD_STRETCHER -} - -extern uint TickInterval; -void SndBuffer::UpdateTempoChangeAsyncMixing() -{ - const float statusPct = GetStatusPct(); - - s_last_pct = statusPct; - if (statusPct < -0.1f) - { - TickInterval -= 4; - if (statusPct < -0.3f) - TickInterval = 64; - if (TickInterval < 64) - TickInterval = 64; - //printf("-- %d, %f\n",TickInterval,statusPct); - } - else if (statusPct > 0.2f) - { - TickInterval += 1; - if (TickInterval >= 7000) - TickInterval = 7000; - //printf("++ %d, %f\n",TickInterval,statusPct); - } - else - TickInterval = 768; -} - -void SndBuffer::timeStretchUnderrun() -{ - gRequestStretcherReset++; - // timeStretcher failed it's job. We need to slow down the audio some. - - s_cTempo -= (s_cTempo * 0.12f); - s_eTempo -= (s_eTempo * 0.30f); - if (s_eTempo < 0.1f) - s_eTempo = 0.1f; - // pSoundTouch->setTempo( eTempo ); - //pSoundTouch->setTempoChange(-30); // temporary (until stretcher is called) slow down -} - -#ifdef SPU2X_HANDLE_STRETCH_OVERRUNS - -s32 SndBuffer::timeStretchOverrun() -{ - // If we overran it means the timestretcher failed. We need to speed - // up audio playback. - s_cTempo += s_cTempo * 0.12f; - s_eTempo += s_eTempo * 0.40f; - if (s_eTempo > 7.5f) - s_eTempo = 7.5f; - //pSoundTouch->setTempo( eTempo ); - //pSoundTouch->setTempoChange(30);// temporary (until stretcher is called) speed up - - // Throw out just a little bit (two packets worth) to help - // give the TS some room to work: - gRequestStretcherReset++; - return SndOutPacketSize * 2; -} - -#endif - -static constexpr float S16_TO_FLOAT = 1.0f / 32767.0f; -static constexpr float FLOAT_TO_S16 = 32767.0f; - -static void ConvertPacketToFloat(const StereoOut16* src, float* dst) -{ - static_assert((SndOutPacketSize % 4) == 0); - constexpr u32 iterations = SndOutPacketSize / 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; - } -} - -static void ConvertPacketToInt(StereoOut16* dst, const float* src, uint size) -{ - static_assert((SndOutPacketSize % 4) == 0); - constexpr u32 iterations = SndOutPacketSize / 4; - - const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16); - - for (u32 i = 0; i < iterations; i++) - { - __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); - const __m128i iv1 = _mm_cvtps_epi32(fv1); - const __m128i iv2 = _mm_cvtps_epi32(fv2); - - __m128i iv = _mm_packs_epi32(iv1, iv2); - _mm_store_si128(reinterpret_cast<__m128i*>(dst), iv); - dst += 4; - } -} - -void SndBuffer::timeStretchWrite() -{ - // data prediction helps keep the tempo adjustments more accurate. - // The timestretcher returns packets in belated "clump" form. - // Meaning that most of the time we'll get nothing back, and then - // suddenly we'll get several chunks back at once. Thus we use - // data prediction to make the timestretcher more responsive. - - PredictDataWrite((int)(SndOutPacketSize / s_eTempo)); - ConvertPacketToFloat(s_staging_buffer.get(), s_float_buffer.get()); - - pSoundTouch->putSamples(s_float_buffer.get(), SndOutPacketSize); - - int tempProgress; - while (tempProgress = pSoundTouch->receiveSamples(s_float_buffer.get(), SndOutPacketSize), - tempProgress != 0) - { - // Hint: It's assumed that pSoundTouch will return chunks of 128 bytes (it always does as - // long as the SSE optimizations are enabled), which means we can do our own SSE opts here. - - ConvertPacketToInt(s_staging_buffer.get(), s_float_buffer.get(), tempProgress); - _WriteSamples(s_staging_buffer.get(), tempProgress); - } - - UpdateTempoChangeSoundTouch(); -} - -void SndBuffer::soundtouchInit() -{ - pSoundTouch = std::make_unique(); - pSoundTouch->setSampleRate(SampleRate); - pSoundTouch->setChannels(2); - - pSoundTouch->setSetting(SETTING_USE_QUICKSEEK, 0); - pSoundTouch->setSetting(SETTING_USE_AA_FILTER, 0); - - pSoundTouch->setSetting(SETTING_SEQUENCE_MS, EmuConfig.SPU2.SequenceLenMS); - pSoundTouch->setSetting(SETTING_SEEKWINDOW_MS, EmuConfig.SPU2.SeekWindowMS); - pSoundTouch->setSetting(SETTING_OVERLAP_MS, EmuConfig.SPU2.OverlapMS); - - pSoundTouch->setTempo(1); - - // some timestretch management vars: - - s_cTempo = 1.0; - s_eTempo = 1.0; - s_last_pct = 0; - s_last_emergency_adj = 0; - - s_predict_data = 0; -} - -// reset timestretch management vars, and delay updates a bit: -void SndBuffer::soundtouchClearContents() -{ - if (pSoundTouch == nullptr) - return; - - pSoundTouch->clear(); - pSoundTouch->setTempo(1); - - s_cTempo = 1.0; - s_eTempo = 1.0; - s_last_pct = 0; - s_last_emergency_adj = 0; - - s_predict_data = 0; -} - -void SndBuffer::soundtouchCleanup() -{ - pSoundTouch.reset(); -} diff --git a/pcsx2/SPU2/SndOut.h b/pcsx2/SPU2/SndOut.h deleted file mode 100644 index 3aa9dbeb8b..0000000000 --- a/pcsx2/SPU2/SndOut.h +++ /dev/null @@ -1,334 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#pragma once - -#include -#include -#include -#include - -#include "common/Pcsx2Defs.h" - -// Number of stereo samples per SndOut block. -// All drivers must work in units of this size when communicating with -// SndOut. -static constexpr int SndOutPacketSize = 64; - -// Samplerate of the SPU2. For accurate playback we need to match this -// exactly. Trying to scale samplerates and maintain SPU2's Ts timing accuracy -// is too problematic. :) -extern int SampleRate; - -// Returns a null-terminated list of backends for the specified module. -// nullptr is returned if the specified module does not have multiple backends. -extern const char* const* GetOutputModuleBackends(const char* omodid); - -// Returns a list of output devices and their associated minimum latency. -struct SndOutDeviceInfo -{ - std::string name; - std::string display_name; - u32 minimum_latency_frames; - - SndOutDeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_); - ~SndOutDeviceInfo(); -}; -std::vector GetOutputDeviceList(const char* omodid, const char* driver); - -struct StereoOut16; - -struct Stereo51Out16DplII; -struct Stereo51Out32DplII; - -struct Stereo51Out16Dpl; // similar to DplII but without rear balancing -struct Stereo51Out32Dpl; - -extern void ResetDplIIDecoder(); -extern void ProcessDplIISample16(const StereoOut16& src, Stereo51Out16DplII* s); -extern void ProcessDplIISample32(const StereoOut16& src, Stereo51Out32DplII* s); -extern void ProcessDplSample16(const StereoOut16& src, Stereo51Out16Dpl* s); -extern void ProcessDplSample32(const StereoOut16& src, Stereo51Out32Dpl* s); - -struct StereoOut32 -{ - static const StereoOut32 Empty; - - s32 Left; - s32 Right; - - StereoOut32() = default; - - StereoOut32(s32 left, s32 right) - : Left(left) - , Right(right) - { - } - - StereoOut32 operator*(const int& factor) const - { - return StereoOut32( - Left * factor, - Right * factor); - } - - StereoOut32& operator*=(const int& factor) - { - Left *= factor; - Right *= factor; - return *this; - } - - StereoOut32 operator+(const StereoOut32& right) const - { - return StereoOut32( - Left + right.Left, - Right + right.Right); - } - - StereoOut32 operator/(int src) const - { - return StereoOut32(Left / src, Right / src); - } -}; - -struct StereoOut16 -{ - s16 Left; - s16 Right; - - StereoOut16() = default; - - __fi StereoOut16(const StereoOut32& src) - : Left((s16)src.Left) - , Right((s16)src.Right) - { - } - - __fi StereoOut16(s16 left, s16 right) - : Left(left) - , Right(right) - { - } - - __fi StereoOut16 ApplyVolume(float volume) - { - return StereoOut16( - static_cast(std::clamp(static_cast(Left) * volume, -32768.0f, 32767.0f)), - static_cast(std::clamp(static_cast(Right) * volume, -32768.0f, 32767.0f)) - ); - } - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - } -}; - -struct Stereo21Out16 -{ - s16 Left; - s16 Right; - s16 LFE; - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - LFE = (src.Left + src.Right) >> 1; - } -}; - -struct Stereo40Out16 -{ - s16 Left; - s16 Right; - s16 LeftBack; - s16 RightBack; - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - LeftBack = src.Left; - RightBack = src.Right; - } -}; - -struct Stereo41Out16 -{ - s16 Left; - s16 Right; - s16 LFE; - s16 LeftBack; - s16 RightBack; - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - LFE = (src.Left + src.Right) >> 1; - LeftBack = src.Left; - RightBack = src.Right; - } -}; - -struct Stereo51Out16 -{ - s16 Left; - s16 Right; - s16 Center; - s16 LFE; - s16 LeftBack; - s16 RightBack; - - // Implementation Note: Center and Subwoofer/LFE --> - // This method is simple and sounds nice. It relies on the speaker/soundcard - // systems do to their own low pass / crossover. Manual lowpass is wasted effort - // and can't match solid state results anyway. - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - Center = (src.Left + src.Right) >> 1; - LFE = Center; - LeftBack = src.Left >> 1; - RightBack = src.Right >> 1; - } -}; - -struct Stereo51Out16DplII -{ - s16 Left; - s16 Right; - s16 Center; - s16 LFE; - s16 LeftBack; - s16 RightBack; - - __fi void SetFrom(const StereoOut16& src) - { - ProcessDplIISample16(src, this); - } -}; - -struct Stereo51Out32DplII -{ - s32 Left; - s32 Right; - s32 Center; - s32 LFE; - s32 LeftBack; - s32 RightBack; - - __fi void SetFrom(const StereoOut32& src) - { - ProcessDplIISample32(src, this); - } -}; - -struct Stereo51Out16Dpl -{ - s16 Left; - s16 Right; - s16 Center; - s16 LFE; - s16 LeftBack; - s16 RightBack; - - __fi void SetFrom(const StereoOut16& src) - { - ProcessDplSample16(src, this); - } -}; - -struct Stereo51Out32Dpl -{ - s32 Left; - s32 Right; - s32 Center; - s32 LFE; - s32 LeftBack; - s32 RightBack; - - __fi void SetFrom(const StereoOut32& src) - { - ProcessDplSample32(src, this); - } -}; - -struct Stereo71Out16 -{ - s16 Left; - s16 Right; - s16 Center; - s16 LFE; - s16 LeftBack; - s16 RightBack; - s16 LeftSide; - s16 RightSide; - - __fi void SetFrom(const StereoOut16& src) - { - Left = src.Left; - Right = src.Right; - Center = (src.Left + src.Right) >> 1; - LFE = Center; - LeftBack = src.Left; - RightBack = src.Right; - - LeftSide = src.Left >> 1; - RightSide = src.Right >> 1; - } -}; - -namespace SndBuffer -{ - void UpdateTempoChangeAsyncMixing(); - bool Init(const char* modname); - bool IsOpen(); - void Cleanup(); - void Write(StereoOut16 Sample); - void ClearContents(); - void ResetBuffers(); - - // Note: When using with 32 bit output buffers, the user of this function is responsible - // for shifting the values to where they need to be manually. The fixed point depth of - // the sample output is determined by the SndOutVolumeShift, which is the number of bits - // to shift right to get a 16 bit result. - template - void ReadSamples(T* bData, int nSamples = SndOutPacketSize); -} - -class SndOutModule -{ -public: - virtual ~SndOutModule() = default; - - // Returns a unique identification string for this driver. - // (usually just matches the driver's cpp filename) - virtual const char* GetIdent() const = 0; - - // Returns the full name for this driver, and can be translated. - virtual const char* GetDisplayName() const = 0; - - // Returns a null-terminated list of backends, or nullptr. - virtual const char* const* GetBackendNames() const = 0; - - // Returns a list of output devices and their associated minimum latency. - virtual std::vector GetOutputDeviceList(const char* driver) const = 0; - - virtual bool Init() = 0; - virtual void Close() = 0; - - // Temporarily pauses the stream, preventing it from requesting data. - virtual void SetPaused(bool paused) = 0; - - // Returns the number of empty samples in the output buffer. - // (which is effectively the amount of data played since the last update) - virtual int GetEmptySampleCount() = 0; -}; - -std::span GetSndOutModules(); diff --git a/pcsx2/SPU2/SndOut_Cubeb.cpp b/pcsx2/SPU2/SndOut_Cubeb.cpp deleted file mode 100644 index efd5bdbfa3..0000000000 --- a/pcsx2/SPU2/SndOut_Cubeb.cpp +++ /dev/null @@ -1,429 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#include "SPU2/Global.h" -#include "SPU2/SndOut.h" -#include "Host.h" -#include "IconsFontAwesome5.h" - -#include "common/Assertions.h" -#include "common/Console.h" -#include "common/StringUtil.h" -#include "common/RedtapeWindows.h" -#include "common/ScopedGuard.h" - -#include "cubeb/cubeb.h" - -#ifdef _WIN32 -#include -#endif - -class Cubeb : public SndOutModule -{ -private: - ////////////////////////////////////////////////////////////////////////////////////////// - // Stuff necessary for speaker expansion - class SampleReader - { - public: - virtual ~SampleReader() = default; - virtual void ReadSamples(void* outputBuffer, long frames) = 0; - }; - - template - class ConvertedSampleReader final : public SampleReader - { - u64* const written; - - public: - ConvertedSampleReader() = delete; - - explicit ConvertedSampleReader(u64* pWritten) - : written(pWritten) - { - } - - void ReadSamples(void* outputBuffer, long frames) override - { - T* p1 = static_cast(outputBuffer); - - while (frames > 0) - { - const long frames_to_read = std::min(frames, SndOutPacketSize); - SndBuffer::ReadSamples(p1, frames_to_read); - p1 += frames_to_read; - frames -= frames_to_read; - } - - (*written) += frames; - } - }; - - void DestroyContextAndStream() - { - if (stream) - { - cubeb_stream_stop(stream); - cubeb_stream_destroy(stream); - stream = nullptr; - } - - if (m_context) - { - cubeb_destroy(m_context); - m_context = nullptr; - } - - ActualReader.reset(); - -#ifdef _WIN32 - if (m_COMInitializedByUs) - { - CoUninitialize(); - m_COMInitializedByUs = false; - } -#endif - } - - static void LogCallback(const char* fmt, ...) - { - std::va_list ap; - va_start(ap, fmt); - std::string msg(StringUtil::StdStringFromFormatV(fmt, ap)); - va_end(ap); - Console.WriteLn("(Cubeb): %s", msg.c_str()); - } - - ////////////////////////////////////////////////////////////////////////////////////////// - // Configuration Vars -#ifdef _WIN32 - bool m_COMInitializedByUs = false; -#endif - - ////////////////////////////////////////////////////////////////////////////////////////// - // Instance vars - u64 writtenSoFar = 0; - u64 writtenLastTime = 0; - u64 positionLastTime = 0; - - u32 channels = 0; - cubeb* m_context = nullptr; - cubeb_stream* stream = nullptr; - std::unique_ptr ActualReader; - bool m_paused = false; - - -public: - Cubeb() = default; - - ~Cubeb() - { - DestroyContextAndStream(); - } - - bool Init() override - { - if (stream) - pxFailRel("Cubeb stream already open in Init()"); - -#ifdef _WIN32 - const HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - m_COMInitializedByUs = SUCCEEDED(hr); - if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) - { - Host::ReportErrorAsync("Cubeb Error", "Failed to initialize COM"); - return false; - } -#endif - -#ifdef PCSX2_DEVBUILD - cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback); -#endif - - int rv = cubeb_init(&m_context, "PCSX2", EmuConfig.SPU2.BackendName.empty() ? nullptr : EmuConfig.SPU2.BackendName.c_str()); - if (rv != CUBEB_OK) - { - Host::ReportFormattedErrorAsync("Cubeb Error", "Could not initialize cubeb context: %d", rv); - return false; - } - - switch (EmuConfig.SPU2.SpeakerConfiguration) // speakers = (numSpeakers + 1) *2; ? - { - case 1: - channels = 4; - break; // Quadrafonic - case 2: - channels = 6; - break; // Surround 5.1 - case 3: - channels = 8; - break; // Surround 7.1 - default: - channels = 2; - break; // Stereo - } - - cubeb_channel_layout layout = CUBEB_LAYOUT_UNDEFINED; - switch (channels) - { - case 2: - Console.WriteLn("(Cubeb) Using normal 2 speaker stereo output."); - ActualReader = std::make_unique>(&writtenSoFar); - break; - - case 3: - Console.WriteLn("(Cubeb) 2.1 speaker expansion enabled."); - ActualReader = std::make_unique>(&writtenSoFar); - layout = CUBEB_LAYOUT_STEREO_LFE; - break; - - case 4: - Console.WriteLn("(Cubeb) 4 speaker expansion enabled [quadraphenia]"); - ActualReader = std::make_unique>(&writtenSoFar); - layout = CUBEB_LAYOUT_QUAD; - break; - - case 5: - Console.WriteLn("(Cubeb) 4.1 speaker expansion enabled."); - ActualReader = std::make_unique>(&writtenSoFar); - layout = CUBEB_LAYOUT_QUAD_LFE; - break; - - case 6: - case 7: - switch (EmuConfig.SPU2.DplDecodingLevel) - { - case 1: - Console.WriteLn("(Cubeb) 5.1 speaker expansion with basic ProLogic dematrixing enabled."); - ActualReader = std::make_unique>(&writtenSoFar); // basic Dpl decoder without rear stereo balancing - break; - case 2: - Console.WriteLn("(Cubeb) 5.1 speaker expansion with experimental ProLogicII dematrixing enabled."); - ActualReader = std::make_unique>(&writtenSoFar); //gigas PLII - break; - default: - Console.WriteLn("(Cubeb) 5.1 speaker expansion enabled."); - ActualReader = std::make_unique>(&writtenSoFar); //"normal" stereo upmix - break; - } - channels = 6; // we do not support 7.0 or 6.2 configurations, downgrade to 5.1 - layout = CUBEB_LAYOUT_3F2_LFE; - break; - - default: // anything 8 or more gets the 7.1 treatment! - Console.WriteLn("(Cubeb) 7.1 speaker expansion enabled."); - ActualReader = std::make_unique>(&writtenSoFar); - channels = 8; // we do not support 7.2 or more, downgrade to 7.1 - layout = CUBEB_LAYOUT_3F4_LFE; - break; - } - - cubeb_stream_params params = {}; - params.format = CUBEB_SAMPLE_S16LE; - params.rate = SampleRate; - params.channels = channels; - params.layout = layout; - params.prefs = CUBEB_STREAM_PREF_NONE; - - const u32 requested_latency_frames = static_cast((EmuConfig.SPU2.OutputLatency * SampleRate) / 1000u); - u32 latency_frames = 0; - rv = cubeb_get_min_latency(m_context, ¶ms, &latency_frames); - if (rv == CUBEB_ERROR_NOT_SUPPORTED) - { - Console.WriteLn("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).", - EmuConfig.SPU2.OutputLatency, requested_latency_frames); - latency_frames = requested_latency_frames; - } - else - { - if (rv != CUBEB_OK) - { - Console.Error("(Cubeb) Could not get minimum latency: %d", rv); - DestroyContextAndStream(); - return false; - } - - const float minimum_latency_ms = static_cast(latency_frames * 1000u) / static_cast(SampleRate); - Console.WriteLn("(Cubeb) Minimum latency: %.2f ms (%u audio frames)", minimum_latency_ms, latency_frames); - if (!EmuConfig.SPU2.OutputLatencyMinimal) - { - if (latency_frames > requested_latency_frames) - { - Console.Warning("(Cubeb) Minimum latency is above requested latency: %u vs %u, adjusting to compensate.", - latency_frames, requested_latency_frames); - } - else - { - latency_frames = requested_latency_frames; - } - } - } - - cubeb_devid selected_device = nullptr; - const std::string& selected_device_name = EmuConfig.SPU2.DeviceName; - cubeb_device_collection devices; - if (!selected_device_name.empty()) - { - rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); - if (rv == CUBEB_OK) - { - for (size_t i = 0; i < devices.count; i++) - { - const cubeb_device_info& di = devices.device[i]; - if (di.device_id && selected_device_name == di.device_id) - { - Console.WriteLn("Using output device '%s' (%s).", di.device_id, di.friendly_name ? di.friendly_name : di.device_id); - selected_device = di.devid; - break; - } - } - - if (!selected_device) - { - Host::AddIconOSDMessage("CubebDeviceNotFound", ICON_FA_VOLUME_MUTE, - fmt::format( - TRANSLATE_FS("SPU2", "Requested audio output device '{}' not found, using default."), - selected_device_name), - Host::OSD_WARNING_DURATION); - } - } - else - { - Console.Error("cubeb_enumerate_devices() returned %d, using default device.", rv); - } - } - - char stream_name[32]; - std::snprintf(stream_name, sizeof(stream_name), "%p", this); - - rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, ¶ms, - latency_frames, &Cubeb::DataCallback, &Cubeb::StateCallback, this); - if (rv != CUBEB_OK) - { - Console.Error("(Cubeb) Could not create stream: %d", rv); - DestroyContextAndStream(); - return false; - } - - rv = cubeb_stream_start(stream); - if (rv != CUBEB_OK) - { - Console.Error("(Cubeb) Could not start stream: %d", rv); - DestroyContextAndStream(); - return false; - } - - m_paused = false; - return true; - } - - void Close() override - { - DestroyContextAndStream(); - } - - static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) - { - } - - static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, long nframes) - { - static_cast(user_ptr)->ActualReader->ReadSamples(output_buffer, nframes); - return nframes; - } - - void SetPaused(bool paused) override - { - if (paused == m_paused || !stream) - return; - - const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream); - if (rv != CUBEB_OK) - { - Console.Error("(Cubeb) Could not %s stream: %d", paused ? "pause" : "resume", rv); - return; - } - - m_paused = paused; - } - - int GetEmptySampleCount() override - { - u64 pos; - if (cubeb_stream_get_position(stream, &pos) != CUBEB_OK) - pos = 0; - - const int playedSinceLastTime = (writtenSoFar - writtenLastTime) + (pos - positionLastTime); - writtenLastTime = writtenSoFar; - positionLastTime = pos; - return playedSinceLastTime; - } - - const char* GetIdent() const override - { - return "cubeb"; - } - - const char* GetDisplayName() const override - { - //: Cubeb is an audio engine name. Leave as-is. - return TRANSLATE_NOOP("SPU2", "Cubeb (Cross-platform)"); - } - - const char* const* GetBackendNames() const override - { - return cubeb_get_backend_names(); - } - - std::vector GetOutputDeviceList(const char* driver) const override - { - std::vector ret; - ret.emplace_back(std::string(), "Default", 0u); - - cubeb* context; - int rv = cubeb_init(&context, "PCSX2", (driver && *driver) ? driver : nullptr); - if (rv != CUBEB_OK) - { - Console.Error("(GetOutputDeviceList) cubeb_init() failed: %d", rv); - return ret; - } - - ScopedGuard context_cleanup([context]() { cubeb_destroy(context); }); - - cubeb_device_collection devices; - rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); - if (rv != CUBEB_OK) - { - Console.Error("(GetOutputDeviceList) cubeb_enumerate_devices() failed: %d", rv); - return ret; - } - - ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); }); - - // we need stream parameters to query latency - cubeb_stream_params params = {}; - params.format = CUBEB_SAMPLE_S16LE; - params.rate = SampleRate; - params.channels = 2; - params.layout = CUBEB_LAYOUT_UNDEFINED; - params.prefs = CUBEB_STREAM_PREF_NONE; - - u32 min_latency = 0; - cubeb_get_min_latency(context, ¶ms, &min_latency); - ret[0].minimum_latency_frames = min_latency; - - for (size_t i = 0; i < devices.count; i++) - { - const cubeb_device_info& di = devices.device[i]; - if (!di.device_id) - continue; - - ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id, min_latency); - } - - return ret; - } -}; - -static Cubeb s_Cubeb; -SndOutModule* CubebOut = &s_Cubeb; diff --git a/pcsx2/SPU2/SndOut_XAudio2.cpp b/pcsx2/SPU2/SndOut_XAudio2.cpp deleted file mode 100644 index 2b176e045e..0000000000 --- a/pcsx2/SPU2/SndOut_XAudio2.cpp +++ /dev/null @@ -1,365 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team -// SPDX-License-Identifier: LGPL-3.0+ - -#include "SPU2/Global.h" -#include "Host.h" - -#include "common/Console.h" -#include "common/RedtapeWindows.h" -#include "common/RedtapeWilCom.h" - -#include -#include -#include -#include - -#include -#include -#include - -//#define XAUDIO2_DEBUG - -class XAudio2Mod final : public SndOutModule -{ -private: - static const int PacketsPerBuffer = 8; - static const int MAX_BUFFER_COUNT = 3; - - class BaseStreamingVoice : public IXAudio2VoiceCallback - { - protected: - IXAudio2SourceVoice* pSourceVoice = nullptr; - std::unique_ptr m_buffer; - - const uint m_nBuffers; - const uint m_nChannels; - const uint m_BufferSize; - const uint m_BufferSizeBytes; - - public: - virtual ~BaseStreamingVoice() = default; - - int GetEmptySampleCount() const - { - XAUDIO2_VOICE_STATE state; - pSourceVoice->GetState(&state); - return state.SamplesPlayed & (m_BufferSize - 1); - } - - explicit BaseStreamingVoice(uint numChannels) - : m_nBuffers(2) - , m_nChannels(numChannels) - , m_BufferSize(SndOutPacketSize * m_nChannels * PacketsPerBuffer) - , m_BufferSizeBytes(m_BufferSize * sizeof(s16)) - { - } - - bool Init(IXAudio2* pXAudio2) - { - DWORD chanMask = 0; - switch (m_nChannels) - { - case 1: - chanMask |= SPEAKER_FRONT_CENTER; - break; - case 2: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; - break; - case 3: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_LOW_FREQUENCY; - break; - case 4: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; - break; - case 5: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; - break; - case 6: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_LOW_FREQUENCY; - break; - case 8: - chanMask |= SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT | SPEAKER_LOW_FREQUENCY; - break; - } - - WAVEFORMATEXTENSIBLE wfx{}; - - wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; - wfx.Format.nSamplesPerSec = SampleRate; - wfx.Format.nChannels = m_nChannels; - wfx.Format.wBitsPerSample = 16; - wfx.Format.nBlockAlign = wfx.Format.nChannels * wfx.Format.wBitsPerSample / 8; - wfx.Format.nAvgBytesPerSec = SampleRate * wfx.Format.nBlockAlign; - wfx.Format.cbSize = sizeof(wfx) - sizeof(WAVEFORMATEX); - wfx.Samples.wValidBitsPerSample = 16; - wfx.dwChannelMask = chanMask; - wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; - - const HRESULT hr = pXAudio2->CreateSourceVoice(&pSourceVoice, reinterpret_cast(&wfx), XAUDIO2_VOICE_NOSRC, 1.0f, this); - if (FAILED(hr)) - { - Console.Error("XAudio2 CreateSourceVoice failure: %08X", hr); - return false; - } - - m_buffer = std::make_unique(m_nBuffers * m_BufferSize); - - // Start some buffers. - for (size_t i = 0; i < m_nBuffers; i++) - { - XAUDIO2_BUFFER buf{}; - buf.AudioBytes = m_BufferSizeBytes; - buf.pContext = &m_buffer[i * m_BufferSize]; - buf.pAudioData = static_cast(buf.pContext); - pSourceVoice->SubmitSourceBuffer(&buf); - } - - Start(); - return true; - } - - void Stop() - { - pSourceVoice->Stop(); - } - - void Start() - { - pSourceVoice->Start(0, 0); - } - - protected: - STDMETHOD_(void, OnVoiceProcessingPassStart) - (UINT32) override {} - STDMETHOD_(void, OnVoiceProcessingPassEnd) - () override {} - STDMETHOD_(void, OnStreamEnd) - () override {} - STDMETHOD_(void, OnBufferStart) - (void*) override {} - STDMETHOD_(void, OnLoopEnd) - (void*) override {} - STDMETHOD_(void, OnVoiceError) - (THIS_ void* pBufferContext, HRESULT Error) override {} - }; - - template - class StreamingVoice final : public BaseStreamingVoice - { - public: - StreamingVoice() - : BaseStreamingVoice(sizeof(T) / sizeof(s16)) - { - } - - virtual ~StreamingVoice() override - { - // Must be done here and not BaseStreamingVoice, as else OnBufferEnd will not be callable anymore - // but it will be called by DestroyVoice. - if (pSourceVoice != nullptr) - { - pSourceVoice->Stop(); - pSourceVoice->DestroyVoice(); - } - } - - protected: - STDMETHOD_(void, OnBufferEnd) - (void* context) override - { - T* qb = static_cast(context); - - for (size_t p = 0; p < PacketsPerBuffer; p++, qb += SndOutPacketSize) - SndBuffer::ReadSamples(qb); - - XAUDIO2_BUFFER buf{}; - buf.AudioBytes = m_BufferSizeBytes; - buf.pAudioData = static_cast(context); - buf.pContext = context; - - pSourceVoice->SubmitSourceBuffer(&buf); - } - }; - - wil::unique_couninitialize_call xaudio2CoInitialize; - wil::unique_hmodule xAudio2DLL; - wil::com_ptr_nothrow pXAudio2; - IXAudio2MasteringVoice* pMasteringVoice = nullptr; - std::unique_ptr m_voiceContext; - bool m_paused = false; - -public: - bool Init() override - { - xaudio2CoInitialize = wil::CoInitializeEx_failfast(COINIT_MULTITHREADED); - - xAudio2DLL.reset(LoadLibraryEx(XAUDIO2_DLL, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); - if (xAudio2DLL == nullptr) - { - Console.Error("Could not load %s. Error code: %d" XAUDIO2_DLL_A, GetLastError()); - Close(); - return false; - } - - auto pXAudio2Create = GetProcAddressByFunctionDeclaration(xAudio2DLL.get(), XAudio2Create); - if (pXAudio2Create == nullptr) - { - Console.Error("XAudio2Create not found. Error code: %d", GetLastError()); - Close(); - return false; - } - - HRESULT hr = pXAudio2Create(&pXAudio2, 0, XAUDIO2_DEFAULT_PROCESSOR); - if (FAILED(hr)) - { - Console.Error("Failed to init XAudio2 engine. Error Details: %08X", hr); - Close(); - return false; - } - -#ifdef XAUDIO2_DEBUG - XAUDIO2_DEBUG_CONFIGURATION debugConfig{}; - debugConfig.BreakMask = XAUDIO2_LOG_ERRORS; - pXAudio2->SetDebugConfiguration(&debugConfig, nullptr); -#endif - - // Stereo Expansion was planned to grab the currently configured number of - // Speakers from Windows's audio config. - // This doesn't always work though, so let it be a user configurable option. - - int speakers; - // speakers = (numSpeakers + 1) *2; ? - switch (EmuConfig.SPU2.SpeakerConfiguration) - { - case 1: // Quadrafonic - speakers = 4; - break; - case 2: // Surround 5.1 - speakers = 6; - break; - case 3: // Surround 7.1 - speakers = 8; - break; - default: // Stereo - speakers = 2; - break; - } - - hr = pXAudio2->CreateMasteringVoice(&pMasteringVoice, speakers, SampleRate); - if (FAILED(hr)) - { - Console.Error("Failed creating mastering voice: %08X", hr); - Close(); - return false; - } - - switch (speakers) - { - case 2: - Console.WriteLn("* SPU2 > Using normal 2 speaker stereo output."); - m_voiceContext = std::make_unique>(); - break; - case 3: - Console.WriteLn("* SPU2 > 2.1 speaker expansion enabled."); - m_voiceContext = std::make_unique>(); - break; - case 4: - Console.WriteLn("* SPU2 > 4 speaker expansion enabled [quadraphenia]"); - m_voiceContext = std::make_unique>(); - break; - case 5: - Console.WriteLn("* SPU2 > 4.1 speaker expansion enabled."); - m_voiceContext = std::make_unique>(); - break; - case 6: - case 7: - switch (EmuConfig.SPU2.DplDecodingLevel) - { - case 1: // basic Dpl decoder without rear stereo balancing - Console.WriteLn("* SPU2 > 5.1 speaker expansion with basic ProLogic dematrixing enabled."); - m_voiceContext = std::make_unique>(); - break; - case 2: // gigas PLII - Console.WriteLn("* SPU2 > 5.1 speaker expansion with experimental ProLogicII dematrixing enabled."); - m_voiceContext = std::make_unique>(); - break; - default: // "normal" stereo upmix - Console.WriteLn("* SPU2 > 5.1 speaker expansion enabled."); - m_voiceContext = std::make_unique>(); - break; - } - break; - default: // anything 8 or more gets the 7.1 treatment! - Console.WriteLn("* SPU2 > 7.1 speaker expansion enabled."); - m_voiceContext = std::make_unique>(); - break; - } - - if (!m_voiceContext->Init(pXAudio2.get())) - { - Close(); - return false; - } - - m_paused = false; - return true; - } - - void Close() override - { - m_voiceContext.reset(); - - if (pMasteringVoice != nullptr) - { - pMasteringVoice->DestroyVoice(); - pMasteringVoice = nullptr; - } - - pXAudio2.reset(); - xAudio2DLL.reset(); - xaudio2CoInitialize.reset(); - } - - int GetEmptySampleCount() override - { - if (m_voiceContext == nullptr) - return 0; - return m_voiceContext->GetEmptySampleCount(); - } - - void SetPaused(bool paused) override - { - if (m_voiceContext == nullptr || m_paused == paused) - return; - - if (paused) - m_voiceContext->Stop(); - else - m_voiceContext->Start(); - - m_paused = paused; - } - - const char* GetIdent() const override - { - return "xaudio2"; - } - - const char* GetDisplayName() const override - { - //: XAudio2 is an audio engine name. Leave as-is. - return TRANSLATE_NOOP("SPU2", "XAudio2"); - } - - const char* const* GetBackendNames() const override - { - return nullptr; - } - - std::vector GetOutputDeviceList(const char* driver) const override - { - return {}; - } -} static XA2; - -SndOutModule* XAudio2Out = &XA2; diff --git a/pcsx2/SPU2/Wavedump_wav.cpp b/pcsx2/SPU2/Wavedump_wav.cpp index d8b4da1455..ad7b72fbcc 100644 --- a/pcsx2/SPU2/Wavedump_wav.cpp +++ b/pcsx2/SPU2/Wavedump_wav.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" +#include "SPU2/Debug.h" +#include "SPU2/spu2.h" #include "pcsx2/Config.h" #include "fmt/format.h" @@ -30,8 +31,6 @@ namespace WaveDump void Open() { - if (!IsDevBuild) - return; if (!SPU2::WaveLog()) return; @@ -40,10 +39,10 @@ namespace WaveDump for (int srcidx = 0; srcidx < CoreSrc_Count; srcidx++) { m_CoreWav[cidx][srcidx].reset(); - + std::string wavfilename(Path::Combine(EmuFolders::Logs, fmt::format("spu2x-Core{}d-{}.wav", cidx, m_tbl_CoreOutputTypeNames[srcidx]))); m_CoreWav[cidx][srcidx] = std::make_unique(); - if (!m_CoreWav[cidx][srcidx]->Open(wavfilename.c_str(), SampleRate, 2)) + if (!m_CoreWav[cidx][srcidx]->Open(wavfilename.c_str(), SPU2::GetConsoleSampleRate(), 2)) { Console.Error(fmt::format("Failed to open '{}'. Wave Log for this core source disabled.", wavfilename)); m_CoreWav[cidx][srcidx].reset(); @@ -54,8 +53,6 @@ namespace WaveDump void Close() { - if (!IsDevBuild) - return; for (uint cidx = 0; cidx < 2; cidx++) { for (int srcidx = 0; srcidx < CoreSrc_Count; srcidx++) @@ -63,17 +60,22 @@ namespace WaveDump } } - void WriteCore(uint coreidx, CoreSourceType src, const StereoOut16& sample) + void WriteCore(uint coreidx, CoreSourceType src, const StereoOut32& sample) { - if (!IsDevBuild) + if (!m_CoreWav[coreidx][src]) return; - if (m_CoreWav[coreidx][src] != nullptr) - m_CoreWav[coreidx][src]->WriteFrames(reinterpret_cast(&sample), 1); + + const s16 frame[] = {static_cast(clamp_mix(sample.Left)), static_cast(clamp_mix(sample.Right))}; + m_CoreWav[coreidx][src]->WriteFrames(frame, 1); } void WriteCore(uint coreidx, CoreSourceType src, s16 left, s16 right) { - WriteCore(coreidx, src, StereoOut16(left, right)); + if (!m_CoreWav[coreidx][src]) + return; + + const s16 frame[] = {left, right}; + m_CoreWav[coreidx][src]->WriteFrames(frame, 1); } } // namespace WaveDump diff --git a/pcsx2/SPU2/defs.h b/pcsx2/SPU2/defs.h index 29dd372188..adbf67d424 100644 --- a/pcsx2/SPU2/defs.h +++ b/pcsx2/SPU2/defs.h @@ -3,10 +3,6 @@ #pragma once -#include "SPU2/Mixer.h" -#include "SPU2/SndOut.h" -#include "SPU2/Global.h" - #include "GS/MultiISA.h" #include @@ -23,10 +19,54 @@ extern const std::array regtable; #define spu2Rs16(mmem) (*(s16*)((s8*)spu2regs + ((mmem)&0x1fff))) #define spu2Ru16(mmem) (*(u16*)((s8*)spu2regs + ((mmem)&0x1fff))) +struct StereoOut32 +{ + static const StereoOut32 Empty; + + s32 Left; + s32 Right; + + StereoOut32() = default; + + StereoOut32(s32 left, s32 right) + : Left(left) + , Right(right) + { + } + + StereoOut32 operator*(const int& factor) const + { + return StereoOut32( + Left * factor, + Right * factor); + } + + StereoOut32& operator*=(const int& factor) + { + Left *= factor; + Right *= factor; + return *this; + } + + StereoOut32 operator+(const StereoOut32& right) const + { + return StereoOut32( + Left + right.Left, + Right + right.Right); + } + + StereoOut32 operator/(int src) const + { + return StereoOut32(Left / src, Right / src); + } +}; + extern s16* GetMemPtr(u32 addr); extern s16 spu2M_Read(u32 addr); extern void spu2M_Write(u32 addr, s16 value); extern void spu2M_Write(u32 addr, u16 value); +extern void spu2Mix(); +extern void spu2Output(StereoOut32 out); static __forceinline s16 SignExtend16(u16 v) { diff --git a/pcsx2/SPU2/spu2.cpp b/pcsx2/SPU2/spu2.cpp index c0f8f70263..f5873019b2 100644 --- a/pcsx2/SPU2/spu2.cpp +++ b/pcsx2/SPU2/spu2.cpp @@ -1,31 +1,41 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" -#include "SPU2/Debug.h" #include "SPU2/spu2.h" +#include "SPU2/defs.h" +#include "SPU2/Debug.h" #include "SPU2/Dma.h" +#include "Host/AudioStream.h" +#include "Host.h" #include "GS/GSCapture.h" #include "MTGS.h" #include "R3000A.h" +#include "VMManager.h" + +#include "common/Error.h" + +const StereoOut32 StereoOut32::Empty(0, 0); namespace SPU2 { - static void InitSndBuffer(); + static void CreateOutputStream(); static void UpdateSampleRate(); + static float GetNominalRate(); static void InternalReset(bool psxmode); } // namespace SPU2 -static double s_device_sample_rate_multiplier = 1.0; -static bool s_psxmode = false; - -int SampleRate = 48000; - u32 lClocks = 0; -s32 SPU2::GetConsoleSampleRate() +static bool s_audio_capture_active = false; +static bool s_psxmode = false; + +static std::unique_ptr s_output_stream; +static std::array s_current_chunk; +static u32 s_current_chunk_pos; + +u32 SPU2::GetConsoleSampleRate() { - return s_psxmode ? 44100 : 48000; + return s_psxmode ? PSX_SAMPLE_RATE : SAMPLE_RATE; } // -------------------------------------------------------------------------------------- @@ -85,38 +95,36 @@ void SPU2writeDMA7Mem(u16* pMem, u32 size) Cores[1].DoDMAwrite(pMem, size); } -void SPU2::InitSndBuffer() +void SPU2::CreateOutputStream() { - Console.WriteLn("Initializing SndBuffer at sample rate of %u...", SampleRate); - if (SndBuffer::Init(EmuConfig.SPU2.OutputModule.c_str())) - return; + // Persist volume through stream recreates. + const u32 volume = s_output_stream ? s_output_stream->GetOutputVolume() : GetResetVolume(); + const u32 sample_rate = GetConsoleSampleRate(); + s_output_stream.reset(); - if (SampleRate != GetConsoleSampleRate()) + Error error; + s_output_stream = AudioStream::CreateStream(EmuConfig.SPU2.Backend, sample_rate, EmuConfig.SPU2.StreamParameters, + EmuConfig.SPU2.DriverName.c_str(), EmuConfig.SPU2.DeviceName.c_str(), EmuConfig.SPU2.IsTimeStretchEnabled(), &error); + if (!s_output_stream) { - // It'll get stretched instead.. - const int original_sample_rate = SampleRate; - Console.Error("Failed to init SPU2 at adjusted sample rate %u, trying console rate.", SampleRate); - SampleRate = GetConsoleSampleRate(); - if (SndBuffer::Init(EmuConfig.SPU2.OutputModule.c_str())) - return; + Host::ReportErrorAsync("Error", + fmt::format("Failed to create or configure audio stream, falling back to null output. The error was:\n{}", + error.GetDescription())); - SampleRate = original_sample_rate; + s_output_stream = AudioStream::CreateNullStream(sample_rate, EmuConfig.SPU2.StreamParameters.buffer_ms); } - // just use nullout - if (!SndBuffer::Init("nullout")) - pxFailRel("Failed to initialize nullout."); + s_output_stream->SetOutputVolume(volume); + s_output_stream->SetNominalRate(GetNominalRate()); + s_output_stream->SetPaused(VMManager::GetState() == VMState::Paused); } void SPU2::UpdateSampleRate() { - const int new_sample_rate = static_cast(std::round(static_cast(GetConsoleSampleRate()) * s_device_sample_rate_multiplier)); - if (SampleRate == new_sample_rate) + if (s_output_stream && s_output_stream->GetSampleRate() == GetConsoleSampleRate()) return; - SndBuffer::Cleanup(); - SampleRate = new_sample_rate; - InitSndBuffer(); + CreateOutputStream(); // Can't be capturing when the sample rate changes. if (IsAudioCaptureActive()) @@ -126,8 +134,48 @@ void SPU2::UpdateSampleRate() } } +u32 SPU2::GetOutputVolume() +{ + return s_output_stream->GetOutputVolume(); +} + +void SPU2::SetOutputVolume(u32 volume) +{ + s_output_stream->SetOutputVolume(volume); +} + +u32 SPU2::GetResetVolume() +{ + return EmuConfig.SPU2.OutputMuted ? 0 : + ((VMManager::GetTargetSpeed() != 1.0f) ? + EmuConfig.SPU2.FastForwardVolume : + EmuConfig.SPU2.OutputVolume); +} + +float SPU2::GetNominalRate() +{ + // Adjust nominal rate when syncing to host. + return VMManager::IsTargetSpeedAdjustedToHost() ? VMManager::GetTargetSpeed() : 1.0f; +} + +void SPU2::SetOutputPaused(bool paused) +{ + s_output_stream->SetPaused(paused); +} + +void SPU2::SetAudioCaptureActive(bool active) +{ + s_audio_capture_active = active; +} + +bool SPU2::IsAudioCaptureActive() +{ + return s_audio_capture_active; +} + void SPU2::InternalReset(bool psxmode) { + s_current_chunk_pos = 0; s_psxmode = psxmode; if (!s_psxmode) { @@ -151,18 +199,19 @@ void SPU2::Reset(bool psxmode) void SPU2::OnTargetSpeedChanged() { - if (EmuConfig.SPU2.SynchMode != Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch) - SndBuffer::ResetBuffers(); -} - -void SPU2::SetDeviceSampleRateMultiplier(double multiplier) -{ - if (s_device_sample_rate_multiplier == multiplier) + if (!s_output_stream) return; - s_device_sample_rate_multiplier = multiplier; - if (SndBuffer::IsOpen()) - UpdateSampleRate(); + if (!s_output_stream->IsStretchEnabled()) + { + s_output_stream->EmptyBuffer(); + s_current_chunk_pos = 0; + } + + s_output_stream->SetNominalRate(GetNominalRate()); + + if (EmuConfig.SPU2.OutputVolume != EmuConfig.SPU2.FastForwardVolume && !EmuConfig.SPU2.OutputMuted) + s_output_stream->SetOutputVolume(GetResetVolume()); } bool SPU2::Open() @@ -182,13 +231,11 @@ bool SPU2::Open() InternalReset(false); - SampleRate = static_cast(std::round(static_cast(GetConsoleSampleRate()) * s_device_sample_rate_multiplier)); - InitSndBuffer(); + CreateOutputStream(); #ifdef PCSX2_DEVBUILD WaveDump::Open(); #endif - SetOutputVolume(EmuConfig.SPU2.FinalVolume); return true; } @@ -196,7 +243,7 @@ void SPU2::Close() { FileLog("[%10d] SPU2 Close\n", Cycles); - SndBuffer::Cleanup(); + s_output_stream.reset(); #ifdef PCSX2_DEVBUILD WaveDump::Close(); @@ -212,6 +259,44 @@ bool SPU2::IsRunningPSXMode() return s_psxmode; } +void SPU2::CheckForConfigChanges(const Pcsx2Config& old_config) +{ + const Pcsx2Config::SPU2Options& opts = EmuConfig.SPU2; + const Pcsx2Config::SPU2Options& oldopts = old_config.SPU2; + + // No need to reinit for volume change. + if ((opts.OutputVolume != oldopts.OutputVolume && VMManager::GetTargetSpeed() == 1.0f) || + (opts.FastForwardVolume != oldopts.FastForwardVolume && VMManager::GetTargetSpeed() != 1.0f) || + opts.OutputMuted != oldopts.OutputMuted) + { + SetOutputVolume(GetResetVolume()); + } + + // Things which require re-initialzing the output. + if (opts.Backend != oldopts.Backend || + opts.StreamParameters != oldopts.StreamParameters || + opts.DriverName != oldopts.DriverName || + opts.DeviceName != oldopts.DeviceName) + { + CreateOutputStream(); + } + else if (opts.IsTimeStretchEnabled() != oldopts.IsTimeStretchEnabled()) + { + s_output_stream->SetStretchEnabled(opts.IsTimeStretchEnabled()); + } + +#ifdef PCSX2_DEVBUILD + // AccessLog controls file output. + if (opts.AccessLog != oldopts.AccessLog) + { + if (AccessLog()) + OpenFileLog(); + else + CloseFileLog(); + } +#endif +} + void SPU2async() { TimeUpdate(psxRegs.cycle); @@ -327,47 +412,18 @@ s32 SPU2freeze(FreezeAction mode, freezeData* data) return 0; } -void SPU2::CheckForConfigChanges(const Pcsx2Config& old_config) +__forceinline void spu2Output(StereoOut32 out) { - if (EmuConfig.SPU2 == old_config.SPU2) - return; - - const Pcsx2Config::SPU2Options& opts = EmuConfig.SPU2; - const Pcsx2Config::SPU2Options& oldopts = old_config.SPU2; - - // No need to reinit for volume change. - if (opts.FinalVolume != oldopts.FinalVolume) - SetOutputVolume(opts.FinalVolume); - - // Wipe buffer out when changing sync mode, so e.g. TS->none doesn't have a huge delay. - if (opts.SynchMode != oldopts.SynchMode) - SndBuffer::ResetBuffers(); - - // Things which require re-initialzing the output. - if (opts.Latency != oldopts.Latency || - opts.OutputLatency != oldopts.OutputLatency || - opts.OutputLatencyMinimal != oldopts.OutputLatencyMinimal || - opts.OutputModule != oldopts.OutputModule || - opts.BackendName != oldopts.BackendName || - opts.DeviceName != oldopts.DeviceName || - opts.SpeakerConfiguration != oldopts.SpeakerConfiguration || - opts.DplDecodingLevel != oldopts.DplDecodingLevel || - opts.SequenceLenMS != oldopts.SequenceLenMS || - opts.SeekWindowMS != oldopts.SeekWindowMS || - opts.OverlapMS != oldopts.OverlapMS) + // Final clamp, take care not to exceed 16 bits from here on + s_current_chunk[s_current_chunk_pos++] = static_cast(clamp_mix(out.Left)); + s_current_chunk[s_current_chunk_pos++] = static_cast(clamp_mix(out.Right)); + if (s_current_chunk_pos == s_current_chunk.size()) { - SndBuffer::Cleanup(); - InitSndBuffer(); - } + s_current_chunk_pos = 0; -#ifdef PCSX2_DEVBUILD - // AccessLog controls file output. - if (opts.AccessLog != oldopts.AccessLog) - { - if (AccessLog()) - OpenFileLog(); - else - CloseFileLog(); + s_output_stream->WriteChunk(s_current_chunk.data()); + + if (SPU2::IsAudioCaptureActive()) [[unlikely]] + GSCapture::DeliverAudioPacket(s_current_chunk.data()); } -#endif } diff --git a/pcsx2/SPU2/spu2.h b/pcsx2/SPU2/spu2.h index 9c50e91ec3..c51f8ca924 100644 --- a/pcsx2/SPU2/spu2.h +++ b/pcsx2/SPU2/spu2.h @@ -1,16 +1,25 @@ -// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #pragma once #include "SaveState.h" #include "IopCounters.h" -#include + +#include struct Pcsx2Config; +class AudioStream; + namespace SPU2 { +/// PS2/Native Sample Rate. +static constexpr u32 SAMPLE_RATE = 48000; + +/// PSX Mode Sample Rate. +static constexpr u32 PSX_SAMPLE_RATE = 44100; + /// Open/close, call at VM startup/shutdown. bool Open(); void Close(); @@ -22,10 +31,13 @@ void Reset(bool psxmode); void CheckForConfigChanges(const Pcsx2Config& old_config); /// Returns the current output volume, irrespective of the configuration. -s32 GetOutputVolume(); +u32 GetOutputVolume(); /// Directly updates the output volume without going through the configuration. -void SetOutputVolume(s32 volume); +void SetOutputVolume(u32 volume); + +/// Returns the volume that we would reset the output to on startup. +u32 GetResetVolume(); /// Pauses/resumes the output stream. void SetOutputPaused(bool paused); @@ -33,14 +45,11 @@ void SetOutputPaused(bool paused); /// Clears output buffers in no-sync mode, prevents long delays after fast forwarding. void OnTargetSpeedChanged(); -/// Adjusts the premultiplier on the output sample rate. Used for syncing to host refresh rate. -void SetDeviceSampleRateMultiplier(double multiplier); - /// Returns true if we're currently running in PSX mode. bool IsRunningPSXMode(); /// Returns the current sample rate the SPU2 is operating at. -s32 GetConsoleSampleRate(); +u32 GetConsoleSampleRate(); /// Tells SPU2 to forward audio packets to GSCapture. void SetAudioCaptureActive(bool active); diff --git a/pcsx2/SPU2/spu2freeze.cpp b/pcsx2/SPU2/spu2freeze.cpp index 6587f0eccf..9b1c1b2b8d 100644 --- a/pcsx2/SPU2/spu2freeze.cpp +++ b/pcsx2/SPU2/spu2freeze.cpp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ -#include "SPU2/Global.h" +#include "SPU2/defs.h" #include "SPU2/spu2.h" // hopefully temporary, until I resolve lClocks depdendency #include "IopMem.h" @@ -110,8 +110,6 @@ s32 SPU2Savestate::ThawIt(DataBlock& spud) } else { - SndBuffer::ClearContents(); - memcpy(spu2regs, spud.unkregs, sizeof(spud.unkregs)); memcpy(_spu2mem, spud.mem, sizeof(spud.mem)); diff --git a/pcsx2/SPU2/spu2sys.cpp b/pcsx2/SPU2/spu2sys.cpp index c44585e8d3..7efcd57fe3 100644 --- a/pcsx2/SPU2/spu2sys.cpp +++ b/pcsx2/SPU2/spu2sys.cpp @@ -11,8 +11,10 @@ #include "IopDma.h" #include "IopHw.h" #include "R3000A.h" +#include "SPU2/Debug.h" +#include "SPU2/defs.h" #include "SPU2/Dma.h" -#include "SPU2/Global.h" +#include "SPU2/regs.h" #include "SPU2/spu2.h" #include "common/Console.h" @@ -214,10 +216,10 @@ void V_Voice::Stop() ADSR.Phase = V_ADSR::PHASE_STOPPED; } -uint TickInterval = 768; -static const int SanityInterval = 4800; +static constexpr uint TickInterval = 768; +static constexpr int SanityInterval = 4800; -__forceinline bool StartQueuedVoice(uint coreidx, uint voiceidx) +__forceinline static bool StartQueuedVoice(uint coreidx, uint voiceidx) { V_Voice& vc(Cores[coreidx].Voices[voiceidx]); @@ -275,11 +277,6 @@ __forceinline void TimeUpdate(u32 cClocks) lClocks = cClocks - dClocks; } - if (EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::ASync) - SndBuffer::UpdateTempoChangeAsyncMixing(); - else - TickInterval = 768; // Reset to default, in case the user hotswitched from async to something else. - //Update Mixing Progress while (dClocks >= TickInterval) { @@ -307,10 +304,8 @@ __forceinline void TimeUpdate(u32 cClocks) if(Cores[c].KeyOn & (1 << v)) if(StartQueuedVoice(c, v)) Cores[c].KeyOn &= ~(1 << v); - // Note: IOP does not use MMX regs, so no need to save them. - //SaveMMXRegs(); - Mix(); - //RestoreMMXRegs(); + + spu2Mix(); } //Update DMA4 interrupt delay counter diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 43b155ccaa..1c98d52b6e 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -186,6 +186,7 @@ static LimiterModeType s_limiter_mode = LimiterModeType::Nominal; static s64 s_limiter_ticks_per_frame = 0; static u64 s_limiter_frame_start = 0; static float s_target_speed = 0.0f; +static bool s_target_speed_synced_to_host = false; static bool s_use_vsync_for_timing = false; // Used to track play time. We use a monotonic timer here, in case of clock changes. @@ -1988,7 +1989,7 @@ double VMManager::AdjustToHostRefreshRate(float frame_rate, float target_speed) { if (!EmuConfig.EmulationSpeed.SyncToHostRefreshRate || target_speed != 1.0f) { - SPU2::SetDeviceSampleRateMultiplier(1.0); + s_target_speed_synced_to_host = false; s_use_vsync_for_timing = false; return target_speed; } @@ -1997,23 +1998,19 @@ double VMManager::AdjustToHostRefreshRate(float frame_rate, float target_speed) if (!GSGetHostRefreshRate(&host_refresh_rate)) { Console.Warning("Cannot sync to host refresh since the query failed."); - SPU2::SetDeviceSampleRateMultiplier(1.0); + s_target_speed_synced_to_host = false; s_use_vsync_for_timing = false; return target_speed; } - const double ratio = host_refresh_rate / frame_rate; + const float ratio = host_refresh_rate / frame_rate; const bool syncing_to_host = (ratio >= 0.95f && ratio <= 1.05f); + s_target_speed_synced_to_host = syncing_to_host; s_use_vsync_for_timing = (syncing_to_host && !EmuConfig.GS.SkipDuplicateFrames && EmuConfig.GS.VsyncEnable != VsyncMode::Off); Console.WriteLn("Refresh rate: Host=%fhz Guest=%fhz Ratio=%f - %s %s", host_refresh_rate, frame_rate, ratio, syncing_to_host ? "can sync" : "can't sync", s_use_vsync_for_timing ? "and using vsync for pacing" : "and using sleep for pacing"); - if (!syncing_to_host) - return target_speed; - - target_speed *= ratio; - SPU2::SetDeviceSampleRateMultiplier(ratio); - return target_speed; + return syncing_to_host ? ratio : target_speed; } float VMManager::GetTargetSpeedForLimiterMode(LimiterModeType mode) @@ -2063,6 +2060,11 @@ void VMManager::UpdateTargetSpeed() } } +bool VMManager::IsTargetSpeedAdjustedToHost() +{ + return s_target_speed_synced_to_host; +} + float VMManager::GetFrameRate() { return GetVerticalFrequency(); @@ -2928,9 +2930,6 @@ void VMManager::EnforceAchievementsChallengeModeSettings() EmuConfig.Speedhacks.EECycleRate = std::max(EmuConfig.Speedhacks.EECycleRate, 0); EmuConfig.Speedhacks.EECycleSkip = 0; - - // Async mix breaks games. - EmuConfig.SPU2.SynchMode = Pcsx2Config::SPU2Options::SynchronizationMode::TimeStretch; } void VMManager::LogUnsafeSettingsToConsole(const std::string& messages) @@ -2973,11 +2972,6 @@ void VMManager::WarnAboutUnsafeSettings() append(ICON_FA_TACHOMETER_ALT, TRANSLATE_SV("VMManager", "Cycle rate/skip is not at default, this may crash or make games run too slow.")); } - if (EmuConfig.SPU2.SynchMode == Pcsx2Config::SPU2Options::SynchronizationMode::ASync) - { - append(ICON_FA_VOLUME_MUTE, - TRANSLATE_SV("VMManager", "Audio is using async mix, expect desynchronization in FMVs.")); - } if (EmuConfig.GS.UpscaleMultiplier < 1.0f) append(ICON_FA_TV, TRANSLATE_SV("VMManager", "Upscale multiplier is below native, this will break rendering.")); if (EmuConfig.GS.HWMipmap != HWMipmapLevel::Automatic) diff --git a/pcsx2/VMManager.h b/pcsx2/VMManager.h index 9afa447093..bfe02083d0 100644 --- a/pcsx2/VMManager.h +++ b/pcsx2/VMManager.h @@ -150,6 +150,9 @@ namespace VMManager /// EmuConfig.EmulationSpeed without going through the usual config apply. void UpdateTargetSpeed(); + /// Returns true if the target speed is being synchronized with the host's refresh rate. + bool IsTargetSpeedAdjustedToHost(); + /// Returns the current frame rate of the virtual machine. float GetFrameRate(); diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index 9adb41b85b..60279c4414 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -250,11 +250,7 @@ - - - - @@ -603,13 +599,10 @@ - - - diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters index 98dd9d7924..11181437ff 100644 --- a/pcsx2/pcsx2.vcxproj.filters +++ b/pcsx2/pcsx2.vcxproj.filters @@ -830,9 +830,6 @@ System\Ps2\SPU2 - - System\Ps2\SPU2 - System\Ps2\SPU2 @@ -842,9 +839,6 @@ System\Ps2\SPU2 - - System\Ps2\SPU2 - System\Ps2\SPU2 @@ -1115,9 +1109,6 @@ System\Ps2\GS\Renderers\Direct3D11 - - System\Ps2\SPU2 - System\Ps2\GS\Renderers\Vulkan @@ -1244,9 +1235,6 @@ System\Ps2\SPU2 - - System\Ps2\SPU2 - Tools @@ -1730,24 +1718,15 @@ System\Ps2\SPU2 - - System\Ps2\SPU2 - System\Ps2\SPU2 System\Ps2\SPU2 - - System\Ps2\SPU2 - System\Ps2\SPU2 - - System\Ps2\SPU2 - System\Ps2\SPU2