SPU: Add time stretched audio output

This commit is contained in:
Connor McLaughlin 2022-07-28 00:42:41 +10:00
parent f54e32ff01
commit 68b5dd869c
27 changed files with 1165 additions and 808 deletions

View file

@ -102,7 +102,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "dep\fmt\fmt.vcxproj"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "util", "src\util\util.vcxproj", "{57F6206D-F264-4B07-BAF8-11B9BBE1F455}"
ProjectSection(ProjectDependencies) = postProject
{39F0ADFF-3A84-470D-9CF0-CA49E164F2F3} = {39F0ADFF-3A84-470D-9CF0-CA49E164F2F3}
{425D6C99-D1C8-43C2-B8AC-4D7B1D941017} = {425D6C99-D1C8-43C2-B8AC-4D7B1D941017}
{751D9F62-881C-454E-BCE8-CB9CF5F1D22F} = {751D9F62-881C-454E-BCE8-CB9CF5F1D22F}
{EE054E08-3799-4A59-A422-18259C105FFD} = {EE054E08-3799-4A59-A422-18259C105FFD}

View file

@ -13,6 +13,7 @@
struct WindowInfo;
enum class AudioBackend : u8;
enum class AudioStretchMode : u8;
class AudioStream;
class CDImage;
@ -77,7 +78,8 @@ std::optional<std::time_t> GetResourceFileTimestamp(const char* filename);
TinyString TranslateString(const char* context, const char* str, const char* disambiguation = nullptr, int n = -1);
std::string TranslateStdString(const char* context, const char* str, const char* disambiguation = nullptr, int n = -1);
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend);
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms,
u32 latency_ms, AudioStretchMode stretch);
/// Returns the scale of OSD elements.
float GetOSDScale();

View file

@ -275,10 +275,15 @@ void Settings::Load(SettingsInterface& si)
audio_backend =
ParseAudioBackend(si.GetStringValue("Audio", "Backend", GetAudioBackendName(DEFAULT_AUDIO_BACKEND)).c_str())
.value_or(DEFAULT_AUDIO_BACKEND);
audio_output_volume = si.GetIntValue("Audio", "OutputVolume", 100);
audio_fast_forward_volume = si.GetIntValue("Audio", "FastForwardVolume", 100);
audio_buffer_size = si.GetIntValue("Audio", "BufferSize", DEFAULT_AUDIO_BUFFER_SIZE);
audio_resampling = si.GetBoolValue("Audio", "Resampling", true);
audio_stretch_mode =
AudioStream::ParseStretchMode(
si.GetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(DEFAULT_AUDIO_STRETCH_MODE)).c_str())
.value_or(DEFAULT_AUDIO_STRETCH_MODE);
audio_output_latency_ms = si.GetUIntValue("Audio", "OutputLatencyMS", DEFAULT_AUDIO_OUTPUT_LATENCY_MS);
audio_buffer_ms = si.GetUIntValue("Audio", "BufferMS", DEFAULT_AUDIO_BUFFER_MS);
audio_output_volume = si.GetUIntValue("Audio", "OutputVolume", 100);
audio_fast_forward_volume = si.GetUIntValue("Audio", "FastForwardVolume", 100);
audio_output_muted = si.GetBoolValue("Audio", "OutputMuted", false);
audio_sync_enabled = si.GetBoolValue("Audio", "Sync", true);
audio_dump_on_boot = si.GetBoolValue("Audio", "DumpOnBoot", false);
@ -472,10 +477,11 @@ void Settings::Save(SettingsInterface& si) const
si.SetIntValue("CDROM", "SeekSpeedup", cdrom_seek_speedup);
si.SetStringValue("Audio", "Backend", GetAudioBackendName(audio_backend));
si.SetIntValue("Audio", "OutputVolume", audio_output_volume);
si.SetIntValue("Audio", "FastForwardVolume", audio_fast_forward_volume);
si.SetIntValue("Audio", "BufferSize", audio_buffer_size);
si.SetBoolValue("Audio", "Resampling", audio_resampling);
si.SetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(audio_stretch_mode));
si.SetUIntValue("Audio", "BufferMS", audio_buffer_ms);
si.SetUIntValue("Audio", "OutputLatencyMS", audio_output_latency_ms);
si.SetUIntValue("Audio", "OutputVolume", audio_output_volume);
si.SetUIntValue("Audio", "FastForwardVolume", audio_fast_forward_volume);
si.SetBoolValue("Audio", "OutputMuted", audio_output_muted);
si.SetBoolValue("Audio", "Sync", audio_sync_enabled);
si.SetBoolValue("Audio", "DumpOnBoot", audio_dump_on_boot);

View file

@ -3,6 +3,7 @@
#include "common/settings_interface.h"
#include "common/string.h"
#include "types.h"
#include "util/audio_stream.h"
#include <array>
#include <optional>
#include <string>
@ -142,10 +143,11 @@ struct Settings
u32 cdrom_seek_speedup = 1;
AudioBackend audio_backend = DEFAULT_AUDIO_BACKEND;
s32 audio_output_volume = 100;
s32 audio_fast_forward_volume = 100;
u32 audio_buffer_size = DEFAULT_AUDIO_BUFFER_SIZE;
bool audio_resampling = true;
AudioStretchMode audio_stretch_mode = DEFAULT_AUDIO_STRETCH_MODE;
u32 audio_output_latency_ms = DEFAULT_AUDIO_OUTPUT_LATENCY_MS;
u32 audio_buffer_ms = DEFAULT_AUDIO_BUFFER_MS;
u32 audio_output_volume = 100;
u32 audio_fast_forward_volume = 100;
bool audio_output_muted = false;
bool audio_sync_enabled = true;
bool audio_dump_on_boot = false;
@ -400,7 +402,9 @@ struct Settings
static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO;
static constexpr u32 DEFAULT_AUDIO_BUFFER_SIZE = 2048;
static constexpr u32 DEFAULT_AUDIO_BUFFER_MS = 50;
static constexpr u32 DEFAULT_AUDIO_OUTPUT_LATENCY_MS = 20;
static constexpr AudioStretchMode DEFAULT_AUDIO_STRETCH_MODE = AudioStretchMode::TimeStretch;
// Enable console logging by default on Linux platforms.
#if defined(__linux__) && !defined(__ANDROID__)

View file

@ -30,8 +30,7 @@ void SPU::Initialize()
"SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD,
[](void* param, TickCount ticks, TickCount ticks_late) { static_cast<SPU*>(param)->ExecuteTransfer(ticks); }, this,
false);
m_null_audio_stream = AudioStream::CreateNullAudioStream();
m_null_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, Settings::DEFAULT_AUDIO_BUFFER_SIZE);
m_null_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms);
CreateOutputStream();
Reset();
@ -39,22 +38,23 @@ void SPU::Initialize()
void SPU::CreateOutputStream()
{
Log_InfoPrintf("Creating '%s' audio stream, sample rate = %u, channels = %u, buffer size = %u",
Settings::GetAudioBackendName(g_settings.audio_backend), SAMPLE_RATE, NUM_CHANNELS,
g_settings.audio_buffer_size);
Log_InfoPrintf(
"Creating '%s' audio stream, sample rate = %u, channels = %u, buffer = %u, latency = %u, stretching = %s",
Settings::GetAudioBackendName(g_settings.audio_backend), SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms,
g_settings.audio_output_latency_ms, AudioStream::GetStretchModeName(g_settings.audio_stretch_mode));
m_audio_stream = Host::CreateAudioStream(g_settings.audio_backend);
if (!m_audio_stream ||
!m_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_size))
m_audio_stream =
Host::CreateAudioStream(g_settings.audio_backend, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms,
g_settings.audio_output_latency_ms, g_settings.audio_stretch_mode);
if (!m_audio_stream)
{
Host::ReportErrorAsync("Error", "Failed to create or configure audio stream, falling back to null output.");
m_audio_stream.reset();
m_audio_stream = AudioStream::CreateNullAudioStream();
m_audio_stream->Reconfigure(SAMPLE_RATE, SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_size);
m_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, NUM_CHANNELS, g_settings.audio_buffer_ms);
}
m_audio_stream->SetOutputVolume(System::GetAudioOutputVolume());
m_audio_stream->SetPaused(System::IsPaused());
}
void SPU::RecreateOutputStream()
@ -77,7 +77,7 @@ void SPU::Shutdown()
m_tick_event.reset();
m_transfer_event.reset();
m_dump_writer.reset();
m_audio_stream = nullptr;
m_audio_stream.reset();
}
void SPU::Reset()

View file

@ -923,9 +923,7 @@ void System::PauseSystem(bool paused)
return;
SetState(paused ? State::Paused : State::Running);
if (!paused)
g_spu.GetOutputStream()->EmptyBuffers();
g_spu.GetOutputStream()->PauseOutput(paused);
g_spu.GetOutputStream()->SetPaused(paused);
if (paused)
{
@ -1179,7 +1177,7 @@ bool System::BootSystem(SystemBootParameters parameters)
// Good to go.
Host::OnSystemStarted();
UpdateSoftwareCursor();
g_spu.GetOutputStream()->PauseOutput(false);
g_spu.GetOutputStream()->SetPaused(false);
// Initial state must be set before loading state.
s_state =
@ -1813,7 +1811,7 @@ bool System::DoLoadState(ByteStream* state, bool force_software_renderer, bool u
if (s_state == State::Starting)
s_state = State::Running;
g_spu.GetOutputStream()->EmptyBuffers();
g_spu.GetOutputStream()->EmptyBuffer();
ResetPerformanceCounters();
ResetThrottler();
return true;
@ -2035,14 +2033,6 @@ void System::ResetThrottler()
void System::Throttle()
{
// Reset the throttler on audio buffer overflow, so we don't end up out of phase.
if (g_spu.GetOutputStream()->DidUnderflow() && s_target_speed >= 1.0f)
{
Log_VerbosePrintf("Audio buffer underflowed, resetting throttler");
ResetThrottler();
return;
}
// Allow variance of up to 40ms either way.
#ifndef __ANDROID__
static constexpr double MAX_VARIANCE_TIME_NS = 40 * 1000000;
@ -2181,7 +2171,8 @@ void System::UpdateSpeedLimiterState()
m_display_all_frames = !m_throttler_enabled || g_settings.display_all_frames;
bool syncing_to_host = false;
if (g_settings.sync_to_host_refresh_rate && g_settings.audio_resampling && target_speed == 1.0f && IsRunning())
if (g_settings.sync_to_host_refresh_rate && (g_settings.audio_stretch_mode != AudioStretchMode::Off) &&
target_speed == 1.0f && IsValid())
{
float host_refresh_rate;
if (g_host_display->GetHostRefreshRate(&host_refresh_rate))
@ -2212,21 +2203,18 @@ void System::UpdateSpeedLimiterState()
UpdateThrottlePeriod();
ResetThrottler();
const u32 input_sample_rate = (target_speed == 0.0f || !g_settings.audio_resampling) ?
SPU::SAMPLE_RATE :
static_cast<u32>(static_cast<float>(SPU::SAMPLE_RATE) * target_speed);
Log_InfoPrintf("Audio input sample rate: %u hz", input_sample_rate);
AudioStream* stream = g_spu.GetOutputStream();
stream->SetInputSampleRate(input_sample_rate);
stream->SetWaitForBufferFill(true);
if (g_settings.audio_fast_forward_volume != g_settings.audio_output_volume)
stream->SetOutputVolume(GetAudioOutputVolume());
stream->SetSync(audio_sync_enabled);
if (audio_sync_enabled)
stream->EmptyBuffers();
// Adjust nominal rate when resampling, or syncing to host.
const bool rate_adjust =
(syncing_to_host || g_settings.audio_stretch_mode == AudioStretchMode::Resample) && target_speed > 0.0f;
stream->SetNominalRate(rate_adjust ? target_speed : 1.0f);
// stream->SetSync(audio_sync_enabled);
// if (audio_sync_enabled)
// stream->EmptyBuffer();
}
g_host_display->SetDisplayMaxFPS(max_display_fps);
@ -3034,8 +3022,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
UpdateOverclock();
}
if (g_settings.audio_backend != old_settings.audio_backend ||
g_settings.audio_buffer_size != old_settings.audio_buffer_size)
if (g_settings.audio_backend != old_settings.audio_backend)
{
if (g_settings.audio_backend != old_settings.audio_backend)
{
@ -3044,7 +3031,15 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
}
g_spu.RecreateOutputStream();
g_spu.GetOutputStream()->PauseOutput(IsPaused());
}
if (g_settings.audio_stretch_mode != old_settings.audio_stretch_mode)
g_spu.GetOutputStream()->SetStretchMode(g_settings.audio_stretch_mode);
if (g_settings.audio_buffer_ms != old_settings.audio_buffer_ms ||
g_settings.audio_output_latency_ms != old_settings.audio_output_latency_ms ||
g_settings.audio_stretch_mode != old_settings.audio_stretch_mode)
{
g_spu.RecreateOutputStream();
UpdateSpeedLimiterState();
}
if (g_settings.emulation_speed != old_settings.emulation_speed)
@ -3169,7 +3164,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_dma.SetHaltTicks(g_settings.dma_halt_ticks);
if (g_settings.audio_backend != old_settings.audio_backend ||
g_settings.audio_buffer_size != old_settings.audio_buffer_size ||
g_settings.video_sync_enabled != old_settings.video_sync_enabled ||
g_settings.audio_sync_enabled != old_settings.audio_sync_enabled ||
g_settings.increase_timer_resolution != old_settings.increase_timer_resolution ||
@ -3177,7 +3171,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_settings.fast_forward_speed != old_settings.fast_forward_speed ||
g_settings.display_max_fps != old_settings.display_max_fps ||
g_settings.display_all_frames != old_settings.display_all_frames ||
g_settings.audio_resampling != old_settings.audio_resampling ||
g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate)
{
UpdateSpeedLimiterState();

View file

@ -20,14 +20,24 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", &Settings::ParseAudioBackend,
&Settings::GetAudioBackendName, Settings::DEFAULT_AUDIO_BACKEND);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToOutput, "Audio", "Sync", true);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferSize, "Audio", "BufferSize",
Settings::DEFAULT_AUDIO_BUFFER_SIZE);
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.stretchMode, "Audio", "StretchMode",
&AudioStream::ParseStretchMode, &AudioStream::GetStretchModeName,
Settings::DEFAULT_AUDIO_STRETCH_MODE);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "Audio", "BufferMS",
Settings::DEFAULT_AUDIO_BUFFER_MS);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "Audio", "OutputLatencyMS",
Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startDumpingOnBoot, "Audio", "DumpOnBoot", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muteCDAudio, "CDROM", "MuteCDAudio", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.resampling, "Audio", "Resampling", true);
connect(m_ui.bufferSize, &QSlider::valueChanged, this, &AudioSettingsWidget::updateBufferingLabel);
updateBufferingLabel();
m_ui.outputLatencyMinimal->setChecked(m_ui.outputLatencyMS->value() == 0);
m_ui.outputLatencyMS->setEnabled(m_ui.outputLatencyMinimal->isChecked());
m_ui.driver->setEnabled(false);
connect(m_ui.bufferMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);
connect(m_ui.outputLatencyMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);
connect(m_ui.outputLatencyMinimal, &QCheckBox::toggled, this, &AudioSettingsWidget::onMinimalOutputLatencyChecked);
updateLatencyLabel();
// for per-game, just use the normal path, since it needs to re-read/apply
if (!dialog->isPerGameSettings())
@ -53,7 +63,7 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent
"lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio "
"output."));
dialog->registerWidgetHelp(
m_ui.bufferSize, tr("Buffer Size"), QStringLiteral("2048"),
m_ui.outputLatencyMS, tr("Output Latency"), QStringLiteral("50 ms"),
tr("The buffer size determines the size of the chunks of audio which will be pulled by the "
"host. Smaller values reduce the output latency, but may cause hitches if the emulation "
"speed is inconsistent. Note that the Cubeb backend uses smaller chunks regardless of "
@ -75,27 +85,31 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent
tr("Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable "
"background music in some games."));
dialog->registerWidgetHelp(
m_ui.resampling, tr("Resampling"), tr("Checked"),
tr("When running outside of 100% speed, resamples audio from the target speed instead of dropping frames. Produces "
m_ui.stretchMode, tr("Stretch Mode"), tr("Time Stretching"),
tr("When running outside of 100% speed, adjusts the tempo on audio instead of dropping frames. Produces "
"much nicer fast forward/slowdown audio at a small cost to performance."));
}
AudioSettingsWidget::~AudioSettingsWidget() = default;
void AudioSettingsWidget::updateBufferingLabel()
void AudioSettingsWidget::updateLatencyLabel()
{
constexpr float step = 128;
const u32 actual_buffer_size =
static_cast<u32>(std::round(static_cast<float>(m_ui.bufferSize->value()) / step) * step);
if (static_cast<u32>(m_ui.bufferSize->value()) != actual_buffer_size)
const u32 output_latency_ms = static_cast<u32>(m_ui.outputLatencyMS->value());
const u32 output_latency_frames = AudioStream::GetBufferSizeForMS(SPU::SAMPLE_RATE, output_latency_ms);
const u32 buffer_ms = static_cast<u32>(m_ui.bufferMS->value());
const u32 buffer_frames = AudioStream::GetBufferSizeForMS(SPU::SAMPLE_RATE, buffer_ms);
if (output_latency_ms > 0)
{
m_ui.bufferSize->setValue(static_cast<int>(actual_buffer_size));
return;
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 frames / %2 ms (%3ms buffer + %5ms output)")
.arg(buffer_frames + output_latency_frames)
.arg(buffer_ms + output_latency_ms)
.arg(buffer_ms)
.arg(output_latency_ms));
}
else
{
m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 frames / %2 ms").arg(buffer_frames).arg(buffer_ms));
}
const float max_latency = AudioStream::GetMaxLatency(SPU::SAMPLE_RATE, actual_buffer_size);
m_ui.bufferingLabel->setText(tr("Maximum Latency: %n frames (%1ms)", "", actual_buffer_size)
.arg(static_cast<double>(max_latency) * 1000.0, 0, 'f', 2));
}
void AudioSettingsWidget::updateVolumeLabel()
@ -104,9 +118,21 @@ void AudioSettingsWidget::updateVolumeLabel()
m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value()));
}
void AudioSettingsWidget::onMinimalOutputLatencyChecked(bool new_value)
{
const u32 value = new_value ? 0u : Settings::DEFAULT_AUDIO_OUTPUT_LATENCY_MS;
m_dialog->setIntSettingValue("Audio", "OutputLatencyMS", value);
QSignalBlocker sb(m_ui.outputLatencyMS);
m_ui.outputLatencyMS->setValue(value);
m_ui.outputLatencyMS->setEnabled(!new_value);
updateLatencyLabel();
}
void AudioSettingsWidget::onOutputVolumeChanged(int new_value)
{
m_dialog->setIntSettingValue("Audio", "OutputVolume", new_value);
// only called for base settings
DebugAssert(!m_dialog->isPerGameSettings());
Host::SetBaseIntSettingValue("Audio", "OutputVolume", new_value);
g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value());
updateVolumeLabel();
@ -114,7 +140,9 @@ void AudioSettingsWidget::onOutputVolumeChanged(int new_value)
void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value)
{
m_dialog->setIntSettingValue("Audio", "FastForwardVolume", new_value);
// only called for base settings
DebugAssert(!m_dialog->isPerGameSettings());
Host::SetBaseIntSettingValue("Audio", "FastForwardVolume", new_value);
g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value);
updateVolumeLabel();
@ -122,7 +150,10 @@ void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value)
void AudioSettingsWidget::onOutputMutedChanged(int new_state)
{
// only called for base settings
DebugAssert(!m_dialog->isPerGameSettings());
const bool muted = (new_state != 0);
m_dialog->setBoolSettingValue("Audio", "OutputMuted", muted);
Host::SetBaseBoolSettingValue("Audio", "OutputMuted", muted);
g_emu_thread->setAudioOutputMuted(muted);
}

View file

@ -15,8 +15,9 @@ public:
~AudioSettingsWidget();
private Q_SLOTS:
void updateBufferingLabel();
void updateLatencyLabel();
void updateVolumeLabel();
void onMinimalOutputLatencyChecked(bool new_value);
void onOutputVolumeChanged(int new_value);
void onFastForwardVolumeChanged(int new_value);
void onOutputMutedChanged(int new_state);

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>502</width>
<height>312</height>
<width>516</width>
<height>435</height>
</rect>
</property>
<property name="windowTitle">
@ -31,7 +31,21 @@
<property name="title">
<string>Configuration</string>
</property>
<layout class="QFormLayout" name="formLayout">
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Buffer Size:</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="startDumpingOnBoot">
<property name="text">
<string>Start Dumping On Boot</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
@ -39,55 +53,87 @@
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSlider" name="outputLatencyMS">
<property name="maximum">
<number>500</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>20</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="outputLatencyMinimal">
<property name="text">
<string>Minimal</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="driver"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="stretchMode">
<item>
<property name="text">
<string>Off (Noisy)</string>
</property>
</item>
<item>
<property name="text">
<string>Resampling (Pitch Shift)</string>
</property>
</item>
<item>
<property name="text">
<string>Time Stretch (Tempo Change, Best Sound)</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output Latency:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="audioBackend"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Buffer Size:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="bufferSize">
<property name="minimum">
<number>1024</number>
</property>
<property name="maximum">
<number>8192</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="pageStep">
<number>1024</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>1024</number>
<string>Driver:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Stretch Mode:</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</widget>
</item>
<item row="2" column="1">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="syncToOutput">
<property name="text">
<string>Sync To Output</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="bufferingLabel">
<property name="text">
<string>Maximum latency: 0 frames (0.00ms)</string>
@ -97,24 +143,31 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="syncToOutput">
<property name="text">
<string>Sync To Output</string>
<item row="3" column="1">
<widget class="QSlider" name="bufferMS">
<property name="minimum">
<number>15</number>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="resampling">
<property name="text">
<string>Resampling</string>
<property name="maximum">
<number>500</number>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="startDumpingOnBoot">
<property name="text">
<string>Start Dumping On Boot</string>
<property name="singleStep">
<number>1</number>
</property>
<property name="pageStep">
<number>5</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>20</number>
</property>
</widget>
</item>

View file

@ -66,14 +66,6 @@
#include <mmsystem.h>
#endif
namespace FrontendCommon {
#ifdef _WIN32
std::unique_ptr<AudioStream> CreateXAudio2AudioStream();
#endif
} // namespace FrontendCommon
Log_SetChannel(CommonHostInterface);
namespace CommonHost {
@ -148,26 +140,27 @@ void CommonHost::ReleaseHostDisplayResources()
//
}
std::unique_ptr<AudioStream> Host::CreateAudioStream(AudioBackend backend)
std::unique_ptr<AudioStream> Host::CreateAudioStream(AudioBackend backend, u32 sample_rate, u32 channels, u32 buffer_ms,
u32 latency_ms, AudioStretchMode stretch)
{
switch (backend)
{
case AudioBackend::Null:
return AudioStream::CreateNullAudioStream();
return AudioStream::CreateNullStream(sample_rate, channels, buffer_ms);
#ifndef _UWP
case AudioBackend::Cubeb:
return CubebAudioStream::Create();
return CommonHost::CreateCubebAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch);
#endif
#ifdef _WIN32
case AudioBackend::XAudio2:
return FrontendCommon::CreateXAudio2AudioStream();
return CommonHost::CreateXAudio2Stream(sample_rate, channels, buffer_ms, latency_ms, stretch);
#endif
#ifdef WITH_SDL2
case AudioBackend::SDL:
return SDLAudioStream::Create();
return CommonHost::CreateSDLAudioStream(sample_rate, channels, buffer_ms, latency_ms, stretch);
#endif
default:
@ -927,7 +920,7 @@ DEFINE_HOTKEY("AudioMute", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE("Hotke
{
g_settings.audio_output_muted = !g_settings.audio_output_muted;
const s32 volume = System::GetAudioOutputVolume();
g_spu.GetOutputStream()->SetOutputVolume(volume);
// g_spu.GetOutputStream()->SetOutputVolume(volume);
if (g_settings.audio_output_muted)
{
Host::AddKeyedOSDMessage("AudioControlHotkey", Host::TranslateStdString("OSDMessage", "Volume: Muted"), 2.0f);
@ -959,7 +952,7 @@ DEFINE_HOTKEY("AudioVolumeUp", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE("H
const s32 volume = std::min<s32>(System::GetAudioOutputVolume() + 10, 100);
g_settings.audio_output_volume = volume;
g_settings.audio_fast_forward_volume = volume;
g_spu.GetOutputStream()->SetOutputVolume(volume);
// g_spu.GetOutputStream()->SetOutputVolume(volume);
Host::AddKeyedFormattedOSDMessage("AudioControlHotkey", 2.0f, Host::TranslateString("OSDMessage", "Volume: %d%%"),
volume);
}
@ -973,7 +966,7 @@ DEFINE_HOTKEY("AudioVolumeDown", TRANSLATABLE("Hotkeys", "Audio"), TRANSLATABLE(
const s32 volume = std::max<s32>(System::GetAudioOutputVolume() - 10, 0);
g_settings.audio_output_volume = volume;
g_settings.audio_fast_forward_volume = volume;
g_spu.GetOutputStream()->SetOutputVolume(volume);
// g_spu.GetOutputStream()->SetOutputVolume(volume);
Host::AddKeyedFormattedOSDMessage("AudioControlHotkey", 2.0f,
Host::TranslateString("OSDMessage", "Volume: %d%%"), volume);
}

View file

@ -1,9 +1,13 @@
#pragma once
#include "core/system.h"
#include <memory>
#include <mutex>
class SettingsInterface;
class AudioStream;
enum class AudioStretchMode : u8;
namespace CommonHost {
/// Initializes configuration.
void UpdateLogSettings();
@ -25,6 +29,19 @@ void OnGameChanged(const std::string& disc_path, const std::string& game_serial,
void PumpMessagesOnCPUThread();
bool CreateHostDisplayResources();
void ReleaseHostDisplayResources();
#ifdef _WIN32
std::unique_ptr<AudioStream> CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms,
AudioStretchMode stretch);
#endif
#ifdef WITH_SDL2
std::unique_ptr<AudioStream> CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms,
AudioStretchMode stretch);
#endif
#ifndef _UWP
std::unique_ptr<AudioStream> CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms,
AudioStretchMode stretch);
#endif
} // namespace CommonHost
namespace ImGuiManager {

View file

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

View file

@ -1,34 +1,30 @@
#pragma once
#include "cubeb/cubeb.h"
#include "util/audio_stream.h"
#include <cstdint>
class CubebAudioStream final : public AudioStream
struct cubeb;
struct cubeb_stream;
class CubebAudioStream : public AudioStream
{
public:
CubebAudioStream();
CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~CubebAudioStream();
static std::unique_ptr<AudioStream> Create();
protected:
bool IsOpen() const { return m_cubeb_stream != nullptr; }
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
void SetPaused(bool paused) override;
void SetOutputVolume(u32 volume) override;
void DestroyContext();
bool Initialize(u32 latency_ms);
private:
static void LogCallback(const char* fmt, ...);
static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
long nframes);
static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state);
cubeb* m_cubeb_context = nullptr;
cubeb_stream* m_cubeb_stream = nullptr;
bool m_paused = true;
void DestroyContextAndStream();
cubeb* m_context = nullptr;
cubeb_stream* stream = nullptr;
#ifdef _WIN32
bool m_com_initialized_by_us = false;

View file

@ -3152,18 +3152,18 @@ void FullscreenUI::DrawAudioSettingsPage()
"The audio backend determines how frames produced by the emulator are submitted to the host.",
"Audio", "Backend", Settings::DEFAULT_AUDIO_BACKEND, &Settings::ParseAudioBackend,
&Settings::GetAudioBackendName, &Settings::GetAudioBackendDisplayName, AudioBackend::Count);
DrawIntRangeSetting("Buffer Size",
DrawIntRangeSetting("Latency",
"The buffer size determines the size of the chunks of audio which will be pulled by the host.",
"Audio", "BufferSize", Settings::DEFAULT_AUDIO_BUFFER_SIZE, 1024, 8192, "%d Frames");
"Audio", "Latency", Settings::DEFAULT_AUDIO_BUFFER_MS, 10, 500, "%d ms");
DrawToggleSetting("Sync To Output",
"Throttles the emulation speed based on the audio backend pulling audio "
"frames. Enable to reduce the chances of crackling.",
"Audio", "Sync", true);
DrawToggleSetting(
"Resampling",
"When running outside of 100% speed, resamples audio from the target speed instead of dropping frames.", "Audio",
"Resampling", true);
"Time Stretching",
"When running outside of 100% speed, adjusts tempo on audio from the target speed instead of dropping frames.",
"Audio", "TimeStretching", true);
EndMenuButtons();
}

View file

@ -13,6 +13,7 @@
#include "core/host_display.h"
#include "core/host_settings.h"
#include "core/settings.h"
#include "core/spu.h"
#include "core/system.h"
#include "fmt/chrono.h"
#include "fmt/format.h"
@ -23,6 +24,7 @@
#include "imgui_internal.h"
#include "imgui_manager.h"
#include "input_manager.h"
#include "util/audio_stream.h"
#include <atomic>
#include <chrono>
#include <cmath>
@ -172,6 +174,16 @@ void ImGuiManager::DrawPerformanceOverlay()
FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
}
#if 0
{
AudioStream* stream = g_spu.GetOutputStream();
const u32 frames = stream->GetBufferedFramesRelaxed();
text.Clear();
text.Fmt("Audio: {:<4u}f/{:<3u}ms", frames, AudioStream::GetMSForBufferSize(stream->GetSampleRate(), frames));
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
}
#endif
}
if (g_settings.display_show_status_indicators)

View file

@ -1,11 +1,15 @@
#include "sdl_audio_stream.h"
#include "common/assert.h"
#include "common/log.h"
#include "common_host.h"
#include "sdl_initializer.h"
#include <SDL.h>
Log_SetChannel(SDLAudioStream);
SDLAudioStream::SDLAudioStream() = default;
SDLAudioStream::SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch)
: AudioStream(sample_rate, channels, buffer_ms, stretch)
{
}
SDLAudioStream::~SDLAudioStream()
{
@ -13,12 +17,16 @@ SDLAudioStream::~SDLAudioStream()
SDLAudioStream::CloseDevice();
}
std::unique_ptr<SDLAudioStream> SDLAudioStream::Create()
std::unique_ptr<AudioStream> CommonHost::CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms,
u32 latency_ms, AudioStretchMode stretch)
{
return std::make_unique<SDLAudioStream>();
std::unique_ptr<SDLAudioStream> stream(std::make_unique<SDLAudioStream>(sample_rate, channels, buffer_ms, stretch));
if (!stream->OpenDevice(latency_ms))
stream.reset();
return stream;
}
bool SDLAudioStream::OpenDevice()
bool SDLAudioStream::OpenDevice(u32 latency_ms)
{
DebugAssert(!IsOpen());
@ -31,22 +39,15 @@ bool SDLAudioStream::OpenDevice()
}
SDL_AudioSpec spec = {};
spec.freq = m_output_sample_rate;
spec.freq = m_sample_rate;
spec.channels = static_cast<Uint8>(m_channels);
spec.format = AUDIO_S16;
spec.samples = static_cast<Uint16>(m_buffer_size);
spec.samples = static_cast<Uint16>(GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms));
spec.callback = AudioCallback;
spec.userdata = static_cast<void*>(this);
SDL_AudioSpec obtained_spec = {};
#ifdef SDL_AUDIO_ALLOW_SAMPLES_CHANGE
const u32 allowed_change_flags = SDL_AUDIO_ALLOW_SAMPLES_CHANGE;
#else
const u32 allowed_change_flags = 0;
#endif
m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, allowed_change_flags);
m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, SDL_AUDIO_ALLOW_SAMPLES_CHANGE);
if (m_device_id == 0)
{
Log_ErrorPrintf("SDL_OpenAudioDevice() failed: %s", SDL_GetError());
@ -54,25 +55,23 @@ bool SDLAudioStream::OpenDevice()
return false;
}
if (obtained_spec.samples > spec.samples)
{
Log_WarningPrintf("Requested buffer size %u, got buffer size %u. Adjusting to compensate.", spec.samples,
obtained_spec.samples);
Log_DevPrintf("Requested %u frame buffer, got %u frame buffer", spec.samples, obtained_spec.samples);
if (!SetBufferSize(obtained_spec.samples))
{
Log_ErrorPrintf("Failed to set new buffer size of %u", obtained_spec.samples);
CloseDevice();
return false;
}
}
BaseInitialize();
m_volume = 100;
m_paused = false;
SDL_PauseAudioDevice(m_device_id, 0);
return true;
}
void SDLAudioStream::PauseDevice(bool paused)
void SDLAudioStream::SetPaused(bool paused)
{
if (m_paused == paused)
return;
SDL_PauseAudioDevice(m_device_id, paused ? 1 : 0);
m_paused = paused;
}
void SDLAudioStream::CloseDevice()
@ -87,7 +86,13 @@ void SDLAudioStream::AudioCallback(void* userdata, uint8_t* stream, int len)
SDLAudioStream* const this_ptr = static_cast<SDLAudioStream*>(userdata);
const u32 num_frames = len / sizeof(SampleType) / this_ptr->m_channels;
this_ptr->ReadFrames(reinterpret_cast<SampleType*>(stream), num_frames, true);
this_ptr->ReadFrames(reinterpret_cast<SampleType*>(stream), num_frames);
}
void SDLAudioStream::FramesAvailable() {}
void SDLAudioStream::SetOutputVolume(u32 volume)
{
if (m_volume == volume)
return;
Panic("Fixme");
}

View file

@ -5,19 +5,18 @@
class SDLAudioStream final : public AudioStream
{
public:
SDLAudioStream();
SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~SDLAudioStream();
static std::unique_ptr<SDLAudioStream> Create();
void SetPaused(bool paused) override;
void SetOutputVolume(u32 volume) override;
bool OpenDevice(u32 latency_ms);
void CloseDevice();
protected:
ALWAYS_INLINE bool IsOpen() const { return (m_device_id != 0); }
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
static void AudioCallback(void* userdata, uint8_t* stream, int len);
u32 m_device_id = 0;

View file

@ -1,6 +1,7 @@
#include "xaudio2_audio_stream.h"
#include "common/assert.h"
#include "common/log.h"
#include "common_host.h"
#include <VersionHelpers.h>
#include <xaudio2.h>
Log_SetChannel(XAudio2AudioStream);
@ -9,12 +10,15 @@ Log_SetChannel(XAudio2AudioStream);
#pragma comment(lib, "xaudio2.lib")
#endif
XAudio2AudioStream::XAudio2AudioStream() = default;
XAudio2AudioStream::XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch)
: AudioStream(sample_rate, channels, buffer_ms, stretch)
{
}
XAudio2AudioStream::~XAudio2AudioStream()
{
if (IsOpen())
XAudio2AudioStream::CloseDevice();
CloseDevice();
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
if (m_xaudio2_library)
@ -25,8 +29,20 @@ XAudio2AudioStream::~XAudio2AudioStream()
#endif
}
bool XAudio2AudioStream::Initialize()
std::unique_ptr<AudioStream> CommonHost::CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms,
u32 latency_ms, AudioStretchMode stretch)
{
std::unique_ptr<XAudio2AudioStream> stream(
std::make_unique<XAudio2AudioStream>(sample_rate, channels, buffer_ms, stretch));
if (!stream->OpenDevice(latency_ms))
stream.reset();
return stream;
}
bool XAudio2AudioStream::OpenDevice(u32 latency_ms)
{
DebugAssert(!IsOpen());
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
m_xaudio2_library = LoadLibraryW(XAUDIO2_DLL_W);
if (!m_xaudio2_library)
@ -36,13 +52,6 @@ bool XAudio2AudioStream::Initialize()
}
#endif
return true;
}
bool XAudio2AudioStream::OpenDevice()
{
DebugAssert(!IsOpen());
HRESULT hr;
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
using PFNXAUDIO2CREATE =
@ -70,7 +79,7 @@ bool XAudio2AudioStream::OpenDevice()
return false;
}
hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_channels, m_output_sample_rate, 0, nullptr);
hr = m_xaudio->CreateMasteringVoice(&m_mastering_voice, m_channels, m_sample_rate, 0, nullptr);
if (FAILED(hr))
{
Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr);
@ -79,10 +88,10 @@ bool XAudio2AudioStream::OpenDevice()
WAVEFORMATEX wf = {};
wf.cbSize = sizeof(wf);
wf.nAvgBytesPerSec = m_output_sample_rate * m_channels * sizeof(s16);
wf.nAvgBytesPerSec = m_sample_rate * m_channels * sizeof(s16);
wf.nBlockAlign = static_cast<WORD>(sizeof(s16) * m_channels);
wf.nChannels = static_cast<WORD>(m_channels);
wf.nSamplesPerSec = m_output_sample_rate;
wf.nSamplesPerSec = m_sample_rate;
wf.wBitsPerSample = sizeof(s16) * 8;
wf.wFormatTag = WAVE_FORMAT_PCM;
hr = m_xaudio->CreateSourceVoice(&m_source_voice, &wf, 0, 1.0f, this);
@ -99,13 +108,27 @@ bool XAudio2AudioStream::OpenDevice()
return false;
}
m_enqueue_buffer_size = std::max<u32>(INTERNAL_BUFFER_SIZE, GetBufferSizeForMS(m_sample_rate, latency_ms));
Log_DevPrintf("Allocating %u buffers of %u frames", NUM_BUFFERS, m_enqueue_buffer_size);
for (u32 i = 0; i < NUM_BUFFERS; i++)
m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels);
m_enqueue_buffers[i] = std::make_unique<SampleType[]>(m_enqueue_buffer_size * m_channels);
BaseInitialize();
m_volume = 100;
m_paused = false;
hr = m_source_voice->Start(0, 0);
if (FAILED(hr))
{
Log_ErrorPrintf("Start() failed: %08X", hr);
return false;
}
EnqueueBuffer();
return true;
}
void XAudio2AudioStream::PauseDevice(bool paused)
void XAudio2AudioStream::SetPaused(bool paused)
{
if (m_paused == paused)
return;
@ -124,6 +147,9 @@ void XAudio2AudioStream::PauseDevice(bool paused)
}
m_paused = paused;
if (!m_buffer_enqueued)
EnqueueBuffer();
}
void XAudio2AudioStream::CloseDevice()
@ -139,29 +165,20 @@ void XAudio2AudioStream::CloseDevice()
m_source_voice = nullptr;
m_mastering_voice = nullptr;
m_xaudio.Reset();
m_buffers = {};
m_enqueue_buffers = {};
m_current_buffer = 0;
m_paused = true;
}
void XAudio2AudioStream::FramesAvailable()
{
if (!m_buffer_enqueued)
{
m_buffer_enqueued = true;
EnqueueBuffer();
}
}
void XAudio2AudioStream::EnqueueBuffer()
{
SampleType* samples = m_buffers[m_current_buffer].get();
ReadFrames(samples, m_buffer_size, false);
SampleType* samples = m_enqueue_buffers[m_current_buffer].get();
ReadFrames(samples, m_enqueue_buffer_size);
const XAUDIO2_BUFFER buf = {
static_cast<UINT32>(0), // flags
static_cast<UINT32>(sizeof(s16) * m_channels * m_buffer_size), // bytes
reinterpret_cast<const BYTE*>(samples) // data
static_cast<UINT32>(0), // flags
static_cast<UINT32>(sizeof(s16) * m_channels * m_enqueue_buffer_size), // bytes
reinterpret_cast<const BYTE*>(samples) // data
};
HRESULT hr = m_source_voice->SubmitSourceBuffer(&buf, nullptr);
@ -173,10 +190,14 @@ void XAudio2AudioStream::EnqueueBuffer()
void XAudio2AudioStream::SetOutputVolume(u32 volume)
{
AudioStream::SetOutputVolume(volume);
HRESULT hr = m_mastering_voice->SetVolume(static_cast<float>(m_output_volume) / 100.0f);
HRESULT hr = m_mastering_voice->SetVolume(static_cast<float>(m_volume) / 100.0f);
if (FAILED(hr))
{
Log_ErrorPrintf("SetVolume() failed: %08X", hr);
return;
}
m_volume = volume;
}
void __stdcall XAudio2AudioStream::OnVoiceProcessingPassStart(UINT32 BytesRequired) {}
@ -195,16 +216,3 @@ void __stdcall XAudio2AudioStream::OnBufferEnd(void* pBufferContext)
void __stdcall XAudio2AudioStream::OnLoopEnd(void* pBufferContext) {}
void __stdcall XAudio2AudioStream::OnVoiceError(void* pBufferContext, HRESULT Error) {}
namespace FrontendCommon {
std::unique_ptr<AudioStream> CreateXAudio2AudioStream()
{
std::unique_ptr<XAudio2AudioStream> stream = std::make_unique<XAudio2AudioStream>();
if (!stream->Initialize())
return {};
return stream;
}
} // namespace FrontendCommon

View file

@ -14,45 +14,42 @@
class XAudio2AudioStream final : public AudioStream, private IXAudio2VoiceCallback
{
public:
XAudio2AudioStream();
XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~XAudio2AudioStream();
bool Initialize();
void SetPaused(bool paused) override;
void SetOutputVolume(u32 volume) override;
protected:
bool OpenDevice(u32 latency_ms);
void CloseDevice();
void EnqueueBuffer();
private:
enum : u32
{
NUM_BUFFERS = 2
NUM_BUFFERS = 2,
INTERNAL_BUFFER_SIZE = 512,
};
ALWAYS_INLINE bool IsOpen() const { return static_cast<bool>(m_xaudio); }
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
// Inherited via IXAudio2VoiceCallback
virtual void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override;
virtual void __stdcall OnVoiceProcessingPassEnd(void) override;
virtual void __stdcall OnStreamEnd(void) override;
virtual void __stdcall OnBufferStart(void* pBufferContext) override;
virtual void __stdcall OnBufferEnd(void* pBufferContext) override;
virtual void __stdcall OnLoopEnd(void* pBufferContext) override;
virtual void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override;
void EnqueueBuffer();
void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override;
void __stdcall OnVoiceProcessingPassEnd(void) override;
void __stdcall OnStreamEnd(void) override;
void __stdcall OnBufferStart(void* pBufferContext) override;
void __stdcall OnBufferEnd(void* pBufferContext) override;
void __stdcall OnLoopEnd(void* pBufferContext) override;
void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override;
Microsoft::WRL::ComPtr<IXAudio2> m_xaudio;
IXAudio2MasteringVoice* m_mastering_voice = nullptr;
IXAudio2SourceVoice* m_source_voice = nullptr;
std::array<std::unique_ptr<SampleType[]>, NUM_BUFFERS> m_buffers;
std::array<std::unique_ptr<SampleType[]>, NUM_BUFFERS> m_enqueue_buffers;
u32 m_enqueue_buffer_size = 0;
u32 m_current_buffer = 0;
bool m_buffer_enqueued = false;
bool m_paused = true;
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
HMODULE m_xaudio2_library = {};

View file

@ -27,8 +27,6 @@ add_library(util
iso_reader.h
jit_code_buffer.cpp
jit_code_buffer.h
null_audio_stream.cpp
null_audio_stream.h
memory_arena.cpp
memory_arena.h
page_fault_handler.cpp
@ -44,4 +42,4 @@ add_library(util
target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_link_libraries(util PUBLIC common simpleini)
target_link_libraries(util PRIVATE libchdr samplerate zlib)
target_link_libraries(util PRIVATE libchdr samplerate zlib soundtouch)

View file

@ -1,387 +1,615 @@
#include "audio_stream.h"
#include "assert.h"
#include "SoundTouch.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/log.h"
#include "samplerate.h"
#include "common/make_array.h"
#include "common/timer.h"
#include <algorithm>
#include <cmath>
#include <cstring>
Log_SetChannel(AudioStream);
AudioStream::AudioStream() = default;
#if defined(_M_ARM64)
#include <arm64_neon.h>
#elif defined(__aarch64__)
#include <arm_neon.h>
#elif defined(_M_IX86) || defined(_M_AMD64)
#include <emmintrin.h>
#endif
static constexpr bool LOG_TIMESTRETCH_STATS = false;
AudioStream::AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch)
: m_sample_rate(sample_rate), m_channels(channels), m_buffer_ms(buffer_ms), m_stretch_mode(stretch)
{
}
AudioStream::~AudioStream()
{
DestroyResampler();
DestroyBuffer();
}
bool AudioStream::Reconfigure(u32 input_sample_rate /* = DefaultInputSampleRate */,
u32 output_sample_rate /* = DefaultOutputSampleRate */, u32 channels /* = 1 */,
u32 buffer_size /* = DefaultBufferSize */)
std::unique_ptr<AudioStream> AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms)
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_Lock(m_resampler_mutex);
return std::unique_ptr<AudioStream>(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off));
}
DestroyResampler();
if (IsDeviceOpen())
CloseDevice();
u32 AudioStream::GetAlignedBufferSize(u32 size)
{
static_assert(Common::IsPow2(CHUNK_SIZE));
return Common::AlignUpPow2(size, CHUNK_SIZE);
}
m_output_sample_rate = output_sample_rate;
m_channels = channels;
m_buffer_size = buffer_size;
m_buffer_filling.store(m_wait_for_buffer_fill);
m_output_paused = true;
u32 AudioStream::GetBufferSizeForMS(u32 sample_rate, u32 ms)
{
return GetAlignedBufferSize((ms * sample_rate) / 1000u);
}
if (!SetBufferSize(buffer_size))
return false;
u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size)
{
buffer_size = GetAlignedBufferSize(buffer_size);
return (buffer_size * 1000u) / sample_rate;
}
if (!OpenDevice())
static constexpr const auto s_stretch_mode_names = make_array("None", "Resample", "TimeStretch");
const char* AudioStream::GetStretchModeName(AudioStretchMode mode)
{
return (static_cast<u32>(mode) < s_stretch_mode_names.size()) ? s_stretch_mode_names[static_cast<u32>(mode)] : "";
}
std::optional<AudioStretchMode> AudioStream::ParseStretchMode(const char* name)
{
for (u8 i = 0; i < static_cast<u8>(AudioStretchMode::Count); i++)
{
LockedEmptyBuffers();
m_buffer_size = 0;
m_output_sample_rate = 0;
m_channels = 0;
return false;
if (std::strcmp(name, s_stretch_mode_names[i]) == 0)
return static_cast<AudioStretchMode>(i);
}
CreateResampler();
InternalSetInputSampleRate(input_sample_rate);
return true;
return std::nullopt;
}
void AudioStream::SetInputSampleRate(u32 sample_rate)
u32 AudioStream::GetBufferedFramesRelaxed() const
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
InternalSetInputSampleRate(sample_rate);
const u32 rpos = m_rpos.load(std::memory_order_relaxed);
const u32 wpos = m_wpos.load(std::memory_order_relaxed);
return (wpos + m_buffer_size - rpos) % m_buffer_size;
}
void AudioStream::SetWaitForBufferFill(bool enabled)
void AudioStream::ReadFrames(s16* bData, u32 nFrames)
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
m_wait_for_buffer_fill = enabled;
if (enabled && m_buffer.IsEmpty())
m_buffer_filling.store(true);
const u32 available_frames = GetBufferedFramesRelaxed();
u32 frames_to_read = nFrames;
u32 silence_frames = 0;
if (m_filling)
{
u32 toFill = m_buffer_size / ((m_stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400);
toFill = GetAlignedBufferSize(toFill);
if (available_frames < toFill)
{
silence_frames = nFrames;
frames_to_read = 0;
}
else
{
m_filling = false;
Log_VerbosePrintf("Underrun compensation done (%d frames buffered)", toFill);
}
}
else if (available_frames < nFrames)
{
silence_frames = nFrames - available_frames;
frames_to_read = available_frames;
m_filling = true;
if (m_stretch_mode == AudioStretchMode::TimeStretch)
StretchUnderrun();
}
if (frames_to_read > 0)
{
u32 rpos = m_rpos.load(std::memory_order_acquire);
u32 end = m_buffer_size - rpos;
if (end > frames_to_read)
end = frames_to_read;
// towards the end of the buffer
if (end > 0)
{
std::memcpy(bData, &m_buffer[rpos], sizeof(s32) * end);
rpos += end;
rpos = (rpos == m_buffer_size) ? 0 : rpos;
}
// after wrapping around
const u32 start = frames_to_read - end;
if (start > 0)
{
std::memcpy(&bData[end * 2], &m_buffer[0], sizeof(s32) * start);
rpos = start;
}
m_rpos.store(rpos, std::memory_order_release);
}
// TODO: Bring back the crappy resampler?
if (silence_frames > 0)
std::memset(bData + frames_to_read, 0, sizeof(s32) * silence_frames);
}
void AudioStream::InternalSetInputSampleRate(u32 sample_rate)
void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples)
{
if (m_input_sample_rate == sample_rate)
const u32 free = m_buffer_size - GetBufferedFramesRelaxed();
if (free <= nSamples)
{
if (m_stretch_mode == AudioStretchMode::TimeStretch)
{
StretchOverrun();
}
else
{
Log_DebugPrintf("Buffer overrun, chunk dropped");
return;
}
}
u32 wpos = m_wpos.load(std::memory_order_acquire);
// wrapping around the end of the buffer?
if ((m_buffer_size - wpos) <= nSamples)
{
// needs to be written in two parts
const u32 end = m_buffer_size - wpos;
const u32 start = nSamples - end;
// start is zero when this chunk reaches exactly the end
std::memcpy(&m_buffer[wpos], bData, end * sizeof(s32));
if (start > 0)
std::memcpy(&m_buffer[0], bData + end, start * sizeof(s32));
wpos = start;
}
else
{
// no split
std::memcpy(&m_buffer[wpos], bData, nSamples * sizeof(s32));
wpos += nSamples;
}
m_wpos.store(wpos, std::memory_order_release);
}
void AudioStream::BaseInitialize()
{
AllocateBuffer();
StretchAllocate();
}
void AudioStream::AllocateBuffer()
{
// use a larger buffer when time stretching, since we need more input
const u32 multplier =
(m_stretch_mode == AudioStretchMode::TimeStretch) ? 16 : ((m_stretch_mode == AudioStretchMode::Off) ? 1 : 2);
m_buffer_size = GetAlignedBufferSize(((m_buffer_ms * multplier) * m_sample_rate) / 1000);
m_target_buffer_size = GetAlignedBufferSize((m_sample_rate * m_buffer_ms) / 1000u);
m_buffer = std::unique_ptr<s32[]>(new s32[m_buffer_size]);
Log_DevPrintf("Allocated buffer of %u frames for buffer of %u ms [stretch %s, target size %u].", m_buffer_size,
m_buffer_ms, GetStretchModeName(m_stretch_mode), m_target_buffer_size);
}
void AudioStream::DestroyBuffer()
{
m_buffer.reset();
m_buffer_size = 0;
m_wpos.store(0, std::memory_order_release);
m_rpos.store(0, std::memory_order_release);
}
void AudioStream::EmptyBuffer()
{
if (m_stretch_mode != AudioStretchMode::Off)
{
m_soundtouch->clear();
if (m_stretch_mode == AudioStretchMode::TimeStretch)
m_soundtouch->setTempo(m_nominal_rate);
}
m_wpos.store(m_rpos.load(std::memory_order_acquire), std::memory_order_release);
}
void AudioStream::SetNominalRate(float tempo)
{
m_nominal_rate = tempo;
if (m_stretch_mode == AudioStretchMode::Resample)
m_soundtouch->setRate(tempo);
}
void AudioStream::SetStretchMode(AudioStretchMode mode)
{
if (m_stretch_mode == mode)
return;
m_input_sample_rate = sample_rate;
m_resampler_ratio = static_cast<double>(m_output_sample_rate) / static_cast<double>(sample_rate);
src_set_ratio(static_cast<SRC_STATE*>(m_resampler_state), m_resampler_ratio);
ResetResampler();
// can't resize the buffers while paused
bool paused = m_paused;
if (!paused)
SetPaused(true);
DestroyBuffer();
StretchDestroy();
m_stretch_mode = mode;
AllocateBuffer();
if (m_stretch_mode != AudioStretchMode::Off)
StretchAllocate();
if (!paused)
SetPaused(false);
}
void AudioStream::SetPaused(bool paused)
{
m_paused = paused;
}
void AudioStream::SetOutputVolume(u32 volume)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
m_output_volume = volume;
}
void AudioStream::PauseOutput(bool paused)
{
if (m_output_paused == paused)
return;
PauseDevice(paused);
m_output_paused = paused;
// Empty buffers on pause.
if (paused)
EmptyBuffers();
}
void AudioStream::Shutdown()
{
if (!IsDeviceOpen())
return;
CloseDevice();
EmptyBuffers();
m_buffer_size = 0;
m_output_sample_rate = 0;
m_channels = 0;
m_output_paused = true;
m_volume = volume;
}
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
{
m_buffer_mutex.lock();
const u32 requested_frames = std::min(*num_frames, m_buffer_size);
EnsureBuffer(requested_frames * m_channels);
*buffer_ptr = m_buffer.GetWritePointer();
*num_frames = std::min(m_buffer_size, m_buffer.GetContiguousSpace() / m_channels);
// TODO: Write directly to buffer when not using stretching.
*buffer_ptr = reinterpret_cast<s16*>(&m_staging_buffer[m_staging_buffer_pos]);
*num_frames = CHUNK_SIZE - m_staging_buffer_pos;
}
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
{
Assert(num_frames <= m_buffer_size);
const u32 num_samples = num_frames * m_channels;
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
EnsureBuffer(num_samples);
m_buffer.PushRange(frames, num_samples);
}
FramesAvailable();
Panic("not implemented");
}
void AudioStream::EndWrite(u32 num_frames)
{
m_buffer.AdvanceTail(num_frames * m_channels);
if (m_buffer_filling.load())
{
if ((m_buffer.GetSize() / m_channels) >= m_buffer_size)
m_buffer_filling.store(false);
}
m_buffer_mutex.unlock();
FramesAvailable();
}
float AudioStream::GetMaxLatency(u32 sample_rate, u32 buffer_size)
{
return (static_cast<float>(buffer_size) / static_cast<float>(sample_rate));
}
bool AudioStream::SetBufferSize(u32 buffer_size)
{
const u32 buffer_size_in_samples = buffer_size * m_channels;
const u32 max_samples = buffer_size_in_samples * 2u;
if (max_samples > m_buffer.GetCapacity())
return false;
m_buffer_size = buffer_size;
m_max_samples = max_samples;
return true;
}
u32 AudioStream::GetSamplesAvailable() const
{
// TODO: Use atomic loads
u32 available_samples;
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
available_samples = m_buffer.GetSize();
}
return available_samples / m_channels;
}
u32 AudioStream::GetSamplesAvailableLocked() const
{
return m_buffer.GetSize() / m_channels;
}
void AudioStream::ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume)
{
const u32 total_samples = num_frames * m_channels;
u32 samples_copied = 0;
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
if (!m_buffer_filling.load())
{
if (m_input_sample_rate == m_output_sample_rate)
{
samples_copied = std::min(m_buffer.GetSize(), total_samples);
if (samples_copied > 0)
m_buffer.PopRange(samples, samples_copied);
ReleaseBufferLock(std::move(buffer_lock));
}
else
{
if (m_resampled_buffer.GetSize() < total_samples)
ResampleInput(std::move(buffer_lock));
else
ReleaseBufferLock(std::move(buffer_lock));
samples_copied = std::min(m_resampled_buffer.GetSize(), total_samples);
if (samples_copied > 0)
m_resampled_buffer.PopRange(samples, samples_copied);
}
}
else
{
ReleaseBufferLock(std::move(buffer_lock));
}
if (samples_copied < total_samples)
{
if (samples_copied > 0)
{
m_resample_buffer.resize(samples_copied);
std::memcpy(m_resample_buffer.data(), samples, sizeof(SampleType) * samples_copied);
// super basic resampler - spread the input samples evenly across the output samples. will sound like ass and have
// aliasing, but better than popping by inserting silence.
const u32 increment =
static_cast<u32>(65536.0f * (static_cast<float>(samples_copied / m_channels) / static_cast<float>(num_frames)));
SampleType* out_ptr = samples;
const SampleType* resample_ptr = m_resample_buffer.data();
const u32 copy_stride = sizeof(SampleType) * m_channels;
u32 resample_subpos = 0;
for (u32 i = 0; i < num_frames; i++)
{
std::memcpy(out_ptr, resample_ptr, copy_stride);
out_ptr += m_channels;
resample_subpos += increment;
resample_ptr += (resample_subpos >> 16) * m_channels;
resample_subpos %= 65536u;
}
Log_VerbosePrintf("Audio buffer underflow, resampled %u frames to %u", samples_copied / m_channels, num_frames);
m_underflow_flag.store(true);
}
else
{
// read nothing, so zero-fill
std::memset(samples, 0, sizeof(SampleType) * total_samples);
Log_VerbosePrintf("Audio buffer underflow with no samples, added %u frames silence", num_frames);
m_underflow_flag.store(true);
}
m_buffer_filling.store(m_wait_for_buffer_fill);
}
if (apply_volume && m_output_volume != FullVolume)
{
SampleType* current_ptr = samples;
const SampleType* end_ptr = samples + (num_frames * m_channels);
while (current_ptr != end_ptr)
{
*current_ptr = ApplyVolume(*current_ptr, m_output_volume);
current_ptr++;
}
}
}
void AudioStream::EnsureBuffer(u32 size)
{
DebugAssert(size <= (m_buffer_size * m_channels));
if (GetBufferSpace() >= size)
// don't bother committing anything when muted
if (m_volume == 0)
return;
if (m_sync)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex, std::adopt_lock);
m_buffer_draining_cv.wait(lock, [this, size]() { return GetBufferSpace() >= size; });
lock.release();
}
m_staging_buffer_pos += num_frames;
DebugAssert(m_staging_buffer_pos <= CHUNK_SIZE);
if (m_staging_buffer_pos < CHUNK_SIZE)
return;
m_staging_buffer_pos = 0;
if (m_stretch_mode != AudioStretchMode::Off)
StretchWrite();
else
InternalWriteFrames(m_staging_buffer.data(), CHUNK_SIZE);
}
static constexpr float S16_TO_FLOAT = 1.0f / 32767.0f;
static constexpr float FLOAT_TO_S16 = 32767.0f;
#if defined(_M_ARM64) || defined(__aarch64__)
static void S16ChunkToFloat(const s32* src, float* dst)
{
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
const float32x4_t S16_TO_FLOAT_V = vdupq_n_f32(S16_TO_FLOAT);
for (u32 i = 0; i < iterations; i++)
{
m_buffer.Remove(size);
const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src));
src += 4;
int32x4_t iv1 = vreinterpretq_s32_s16(vzip1q_s16(sv, sv)); // [0, 0, 1, 1, 2, 2, 3, 3]
int32x4_t iv2 = vreinterpretq_s32_s16(vzip2q_s16(sv, sv)); // [4, 4, 5, 5, 6, 6, 7, 7]
iv1 = vshrq_n_s32(iv1, 16); // [0, 1, 2, 3]
iv2 = vshrq_n_s32(iv2, 16); // [4, 5, 6, 7]
float32x4_t fv1 = vcvtq_f32_s32(iv1); // [f0, f1, f2, f3]
float32x4_t fv2 = vcvtq_f32_s32(iv2); // [f4, f5, f6, f7]
fv1 = vmulq_f32(fv1, S16_TO_FLOAT_V);
fv2 = vmulq_f32(fv2, S16_TO_FLOAT_V);
vst1q_f32(dst + 0, fv1);
vst1q_f32(dst + 4, fv2);
dst += 8;
}
}
void AudioStream::DropFrames(u32 count)
static void FloatChunkToS16(s32* dst, const float* src, uint size)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
m_buffer.Remove(count);
}
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
void AudioStream::EmptyBuffers()
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
LockedEmptyBuffers();
}
const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16);
void AudioStream::LockedEmptyBuffers()
{
m_buffer.Clear();
m_underflow_flag.store(false);
m_buffer_filling.store(m_wait_for_buffer_fill);
ResetResampler();
}
void AudioStream::CreateResampler()
{
m_resampler_state = src_new(SRC_SINC_MEDIUM_QUALITY, static_cast<int>(m_channels), nullptr);
if (!m_resampler_state)
Panic("Failed to allocate resampler");
}
void AudioStream::DestroyResampler()
{
if (m_resampler_state)
for (u32 i = 0; i < iterations; i++)
{
src_delete(static_cast<SRC_STATE*>(m_resampler_state));
m_resampler_state = nullptr;
float32x4_t fv1 = vld1q_s32(src + 0);
float32x4_t fv2 = vld1q_s32(src + 4);
src += 8;
fv1 = vmulq_f32(fv1, FLOAT_TO_S16_V);
fv2 = vmulq_f32(fv2, FLOAT_TO_S16_V);
int32x4_t iv1 = vcvtq_s32_f32(fv1);
int32x4_t iv2 = vcvtq_s32_f32(fv2);
int16x8_t iv = vcombine_s16(vqmovn_s32(iv1), vqmovn_s32(iv2));
vst1q_s32(dst, vreinterpretq_s32_s16(iv));
dst += 4;
}
}
void AudioStream::ResetResampler()
#elif defined(_M_IX86) || defined(_M_AMD64)
static void S16ChunkToFloat(const s32* src, float* dst)
{
m_resampled_buffer.Clear();
m_resample_in_buffer.clear();
m_resample_out_buffer.clear();
src_reset(static_cast<SRC_STATE*>(m_resampler_state));
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
const __m128 S16_TO_FLOAT_V = _mm_set1_ps(S16_TO_FLOAT);
for (u32 i = 0; i < iterations; i++)
{
const __m128i sv = _mm_load_si128(reinterpret_cast<const __m128i*>(src));
src += 4;
__m128i iv1 = _mm_unpacklo_epi16(sv, sv); // [0, 0, 1, 1, 2, 2, 3, 3]
__m128i iv2 = _mm_unpackhi_epi16(sv, sv); // [4, 4, 5, 5, 6, 6, 7, 7]
iv1 = _mm_srai_epi32(iv1, 16); // [0, 1, 2, 3]
iv2 = _mm_srai_epi32(iv2, 16); // [4, 5, 6, 7]
__m128 fv1 = _mm_cvtepi32_ps(iv1); // [f0, f1, f2, f3]
__m128 fv2 = _mm_cvtepi32_ps(iv2); // [f4, f5, f6, f7]
fv1 = _mm_mul_ps(fv1, S16_TO_FLOAT_V);
fv2 = _mm_mul_ps(fv2, S16_TO_FLOAT_V);
_mm_store_ps(dst + 0, fv1);
_mm_store_ps(dst + 4, fv2);
dst += 8;
}
}
void AudioStream::ResampleInput(std::unique_lock<std::mutex> buffer_lock)
static void FloatChunkToS16(s32* dst, const float* src, uint size)
{
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
const u32 input_space_from_output = (m_resampled_buffer.GetSpace() * m_output_sample_rate) / m_input_sample_rate;
u32 remaining = std::min(m_buffer.GetSize(), input_space_from_output);
if (m_resample_in_buffer.size() < remaining)
const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16);
for (u32 i = 0; i < iterations; i++)
{
remaining -= static_cast<u32>(m_resample_in_buffer.size());
m_resample_in_buffer.reserve(m_resample_in_buffer.size() + remaining);
while (remaining > 0)
__m128 fv1 = _mm_load_ps(src + 0);
__m128 fv2 = _mm_load_ps(src + 4);
src += 8;
fv1 = _mm_mul_ps(fv1, FLOAT_TO_S16_V);
fv2 = _mm_mul_ps(fv2, FLOAT_TO_S16_V);
__m128i iv1 = _mm_cvtps_epi32(fv1);
__m128i iv2 = _mm_cvtps_epi32(fv2);
__m128i iv = _mm_packs_epi32(iv1, iv2);
_mm_store_si128(reinterpret_cast<__m128i*>(dst), iv);
dst += 4;
}
}
#else
static void S16ChunkToFloat(const s32* src, float* dst)
{
for (uint i = 0; i < AudioStream::CHUNK_SIZE; ++i)
{
*(dst++) = static_cast<float>(static_cast<s16>((u32)*src)) / 32767.0f;
*(dst++) = static_cast<float>(static_cast<s16>(((u32)*src) >> 16)) / 32767.0f;
src++;
}
}
static void FloatChunkToS16(s32* dst, const float* src, uint size)
{
for (uint i = 0; i < size; ++i)
{
const s16 left = static_cast<s16>((*(src++) * 32767.0f));
const s16 right = static_cast<s16>((*(src++) * 32767.0f));
*(dst++) = (static_cast<u32>(left) & 0xFFFFu) | (static_cast<u32>(right) << 16);
}
}
#endif
// Time stretching algorithm based on PCSX2 implementation.
template<class T>
ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max)
{
return (min <= val && val <= max);
}
void AudioStream::StretchAllocate()
{
if (m_stretch_mode == AudioStretchMode::Off)
return;
m_soundtouch = std::make_unique<soundtouch::SoundTouch>();
m_soundtouch->setSampleRate(m_sample_rate);
m_soundtouch->setChannels(m_channels);
m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, 0);
m_soundtouch->setSetting(SETTING_USE_AA_FILTER, 0);
m_soundtouch->setSetting(SETTING_SEQUENCE_MS, 30);
m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, 20);
m_soundtouch->setSetting(SETTING_OVERLAP_MS, 10);
if (m_stretch_mode == AudioStretchMode::Resample)
m_soundtouch->setRate(m_nominal_rate);
else
m_soundtouch->setTempo(m_nominal_rate);
m_stretch_reset = STRETCH_RESET_THRESHOLD;
m_stretch_inactive = false;
m_stretch_ok_count = 0;
m_dynamic_target_usage = 0.0f;
m_average_position = 0;
m_average_available = 0;
m_staging_buffer_pos = 0;
}
void AudioStream::StretchDestroy()
{
m_soundtouch.reset();
}
void AudioStream::StretchWrite()
{
S16ChunkToFloat(m_staging_buffer.data(), m_float_buffer.data());
m_soundtouch->putSamples(m_float_buffer.data(), CHUNK_SIZE);
int tempProgress;
while (tempProgress = m_soundtouch->receiveSamples((float*)m_float_buffer.data(), CHUNK_SIZE), tempProgress != 0)
{
FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress);
InternalWriteFrames(m_staging_buffer.data(), tempProgress);
}
if (m_stretch_mode == AudioStretchMode::TimeStretch)
UpdateStretchTempo();
}
float AudioStream::AddAndGetAverageTempo(float val)
{
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
m_average_available = 0;
if (m_average_available < AVERAGING_BUFFER_SIZE)
m_average_available++;
m_average_fullness[m_average_position] = val;
m_average_position = (m_average_position + 1U) % AVERAGING_BUFFER_SIZE;
const u32 actual_window = std::min<u32>(m_average_available, AVERAGING_WINDOW);
const u32 first_index = (m_average_position - actual_window + AVERAGING_BUFFER_SIZE) % AVERAGING_BUFFER_SIZE;
float sum = 0;
for (u32 i = first_index; i < first_index + actual_window; i++)
sum += m_average_fullness[i % AVERAGING_BUFFER_SIZE];
sum = sum / actual_window;
return (sum != 0.0f) ? sum : 1.0f;
}
void AudioStream::UpdateStretchTempo()
{
static constexpr float MIN_TEMPO = 0.05f;
static constexpr float MAX_TEMPO = 50.0f;
// Which range we will run in 1:1 mode for.
static constexpr float INACTIVE_GOOD_FACTOR = 1.04f;
static constexpr float INACTIVE_BAD_FACTOR = 1.2f;
static constexpr u32 INACTIVE_MIN_OK_COUNT = 50;
static constexpr u32 COMPENSATION_DIVIDER = 100;
float base_target_usage = static_cast<float>(m_target_buffer_size) * m_nominal_rate;
// state vars
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
{
Log_VerbosePrintf("___ Stretcher is being reset.");
m_stretch_inactive = false;
m_stretch_ok_count = 0;
m_dynamic_target_usage = base_target_usage;
}
const u32 ibuffer_usage = GetBufferedFramesRelaxed();
float buffer_usage = static_cast<float>(ibuffer_usage);
float tempo = buffer_usage / m_dynamic_target_usage;
tempo = AddAndGetAverageTempo(tempo);
// Dampening when we get close to target.
if (tempo < 2.0f)
tempo = std::sqrt(tempo);
tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO);
if (tempo < 1.0f)
base_target_usage /= std::sqrt(tempo);
m_dynamic_target_usage +=
static_cast<float>(base_target_usage / tempo - m_dynamic_target_usage) / static_cast<float>(COMPENSATION_DIVIDER);
if (IsInRange(tempo, 0.9f, 1.1f) &&
IsInRange(m_dynamic_target_usage, base_target_usage * 0.9f, base_target_usage * 1.1f))
{
m_dynamic_target_usage = base_target_usage;
}
if (!m_stretch_inactive)
{
if (IsInRange(tempo, 1.0f / INACTIVE_GOOD_FACTOR, INACTIVE_GOOD_FACTOR))
m_stretch_ok_count++;
else
m_stretch_ok_count = 0;
if (m_stretch_ok_count >= INACTIVE_MIN_OK_COUNT)
{
const u32 read_len = std::min(m_buffer.GetContiguousSize(), remaining);
const size_t old_pos = m_resample_in_buffer.size();
m_resample_in_buffer.resize(m_resample_in_buffer.size() + read_len);
src_short_to_float_array(m_buffer.GetReadPointer(), m_resample_in_buffer.data() + old_pos,
static_cast<int>(read_len));
m_buffer.Remove(read_len);
remaining -= read_len;
Log_VerbosePrintf("=== Stretcher is now inactive.");
m_stretch_inactive = true;
}
}
ReleaseBufferLock(std::move(buffer_lock));
const u32 potential_output_size =
(static_cast<u32>(m_resample_in_buffer.size()) * m_input_sample_rate) / m_output_sample_rate;
const u32 output_size = std::min(potential_output_size, m_resampled_buffer.GetSpace());
m_resample_out_buffer.resize(output_size);
SRC_DATA sd = {};
sd.data_in = m_resample_in_buffer.data();
sd.data_out = m_resample_out_buffer.data();
sd.input_frames = static_cast<u32>(m_resample_in_buffer.size()) / m_channels;
sd.output_frames = output_size / m_channels;
sd.src_ratio = m_resampler_ratio;
const int error = src_process(static_cast<SRC_STATE*>(m_resampler_state), &sd);
if (error)
else if (!IsInRange(tempo, 1.0f / INACTIVE_BAD_FACTOR, INACTIVE_BAD_FACTOR))
{
Log_ErrorPrintf("Resampler error %d", error);
m_resample_in_buffer.clear();
m_resample_out_buffer.clear();
return;
Log_VerbosePrintf("~~~ Stretcher is now active @ tempo %f.", tempo);
m_stretch_inactive = false;
m_stretch_ok_count = 0;
}
m_resample_in_buffer.erase(m_resample_in_buffer.begin(),
m_resample_in_buffer.begin() + (static_cast<u32>(sd.input_frames_used) * m_channels));
if (m_stretch_inactive)
tempo = m_nominal_rate;
const float* write_ptr = m_resample_out_buffer.data();
remaining = static_cast<u32>(sd.output_frames_gen) * m_channels;
while (remaining > 0)
if constexpr (LOG_TIMESTRETCH_STATS)
{
const u32 samples_to_write = std::min(m_resampled_buffer.GetContiguousSpace(), remaining);
src_float_to_short_array(write_ptr, m_resampled_buffer.GetWritePointer(), static_cast<int>(samples_to_write));
m_resampled_buffer.AdvanceTail(samples_to_write);
write_ptr += samples_to_write;
remaining -= samples_to_write;
static int iterations = 0;
static u64 last_log_time = 0;
const u64 now = Common::Timer::GetCurrentValue();
if (Common::Timer::ConvertValueToSeconds(now - last_log_time) > 1.0f)
{
Log_VerbosePrintf("buffers: %4u ms (%3.0f%%), tempo: %f, comp: %2.3f, iters: %d, reset:%d",
(ibuffer_usage * 1000u) / m_sample_rate, 100.0f * buffer_usage / base_target_usage, tempo,
m_dynamic_target_usage / base_target_usage, iterations, m_stretch_reset);
last_log_time = now;
iterations = 0;
}
iterations++;
}
m_resample_out_buffer.erase(m_resample_out_buffer.begin(),
m_resample_out_buffer.begin() + (static_cast<u32>(sd.output_frames_gen) * m_channels));
m_soundtouch->setTempo(tempo);
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
m_stretch_reset = 0;
}
void AudioStream::StretchUnderrun()
{
// Didn't produce enough frames in time.
m_stretch_reset++;
}
void AudioStream::StretchOverrun()
{
// Produced more frames than can fit in the buffer.
m_stretch_reset++;
// Drop two packets to give the time stretcher a bit more time to slow things down.
const u32 discard = CHUNK_SIZE * 2;
m_rpos.store((m_rpos.load(std::memory_order_acquire) + discard) % m_buffer_size, std::memory_order_release);
}

View file

@ -1,13 +1,26 @@
#pragma once
#include "common/fifo_queue.h"
#include "common/types.h"
#include <array>
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <vector>
#include <optional>
// Uses signed 16-bits samples.
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4324) // warning C4324: structure was padded due to alignment specifier
#endif
namespace soundtouch {
class SoundTouch;
}
enum class AudioStretchMode : u8
{
Off,
Resample,
TimeStretch,
Count
};
class AudioStream
{
@ -16,111 +29,116 @@ public:
enum : u32
{
DefaultInputSampleRate = 44100,
DefaultOutputSampleRate = 44100,
DefaultBufferSize = 2048,
MaxSamples = 32768,
FullVolume = 100
CHUNK_SIZE = 64,
MAX_CHANNELS = 2
};
AudioStream();
public:
virtual ~AudioStream();
u32 GetOutputSampleRate() const { return m_output_sample_rate; }
u32 GetChannels() const { return m_channels; }
u32 GetBufferSize() const { return m_buffer_size; }
s32 GetOutputVolume() const { return m_output_volume; }
bool IsSyncing() const { return m_sync; }
static u32 GetAlignedBufferSize(u32 size);
static u32 GetBufferSizeForMS(u32 sample_rate, u32 ms);
static u32 GetMSForBufferSize(u32 sample_rate, u32 buffer_size);
bool Reconfigure(u32 input_sample_rate = DefaultInputSampleRate, u32 output_sample_rate = DefaultOutputSampleRate,
u32 channels = 1, u32 buffer_size = DefaultBufferSize);
void SetSync(bool enable) { m_sync = enable; }
static const char* GetStretchModeName(AudioStretchMode mode);
static std::optional<AudioStretchMode> ParseStretchMode(const char* name);
void SetInputSampleRate(u32 sample_rate);
void SetWaitForBufferFill(bool enabled);
ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
ALWAYS_INLINE u32 GetChannels() const { return m_channels; }
ALWAYS_INLINE u32 GetBufferSize() const { return m_buffer_size; }
ALWAYS_INLINE u32 GetTargetBufferSize() const { return m_target_buffer_size; }
ALWAYS_INLINE u32 GetOutputVolume() const { return m_volume; }
ALWAYS_INLINE float GetNominalTempo() const { return m_nominal_rate; }
ALWAYS_INLINE bool IsPaused() const { return m_paused; }
u32 GetBufferedFramesRelaxed() const;
/// Temporarily pauses the stream, preventing it from requesting data.
virtual void SetPaused(bool paused);
virtual void SetOutputVolume(u32 volume);
void PauseOutput(bool paused);
void EmptyBuffers();
void Shutdown();
void BeginWrite(SampleType** buffer_ptr, u32* num_frames);
void WriteFrames(const SampleType* frames, u32 num_frames);
void EndWrite(u32 num_frames);
bool DidUnderflow()
{
bool expected = true;
return m_underflow_flag.compare_exchange_strong(expected, false);
}
void EmptyBuffer();
static std::unique_ptr<AudioStream> CreateNullAudioStream();
/// Nominal rate is used for both resampling and timestretching, input samples are assumed to be this amount faster
/// than the sample rate.
void SetNominalRate(float tempo);
// Latency computation - returns values in seconds
static float GetMaxLatency(u32 sample_rate, u32 buffer_size);
void SetStretchMode(AudioStretchMode mode);
static std::unique_ptr<AudioStream> CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms);
protected:
virtual bool OpenDevice() = 0;
virtual void PauseDevice(bool paused) = 0;
virtual void CloseDevice() = 0;
virtual void FramesAvailable() = 0;
AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
void BaseInitialize();
ALWAYS_INLINE static SampleType ApplyVolume(SampleType sample, u32 volume)
{
return s16((s32(sample) * s32(volume)) / 100);
}
void ReadFrames(s16* bData, u32 nSamples);
ALWAYS_INLINE u32 GetBufferSpace() const { return (m_max_samples - m_buffer.GetSize()); }
ALWAYS_INLINE void ReleaseBufferLock(std::unique_lock<std::mutex> lock)
{
// lock is released implicitly by destruction
m_buffer_draining_cv.notify_one();
}
bool SetBufferSize(u32 buffer_size);
bool IsDeviceOpen() const { return (m_output_sample_rate > 0); }
void EnsureBuffer(u32 size);
void LockedEmptyBuffers();
u32 GetSamplesAvailable() const;
u32 GetSamplesAvailableLocked() const;
void ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume);
void DropFrames(u32 count);
void CreateResampler();
void DestroyResampler();
void ResetResampler();
void InternalSetInputSampleRate(u32 sample_rate);
void ResampleInput(std::unique_lock<std::mutex> buffer_lock);
u32 m_input_sample_rate = 0;
u32 m_output_sample_rate = 0;
u32 m_sample_rate = 0;
u32 m_channels = 0;
u32 m_buffer_ms = 0;
u32 m_volume = 0;
AudioStretchMode m_stretch_mode = AudioStretchMode::Off;
bool m_stretch_inactive = false;
bool m_filling = false;
bool m_paused = false;
private:
enum : u32
{
AVERAGING_BUFFER_SIZE = 256,
AVERAGING_WINDOW = 50,
STRETCH_RESET_THRESHOLD = 5,
TARGET_IPS = 691,
};
void AllocateBuffer();
void DestroyBuffer();
void InternalWriteFrames(s32* bData, u32 nFrames);
void StretchAllocate();
void StretchDestroy();
void StretchWrite();
void StretchUnderrun();
void StretchOverrun();
float AddAndGetAverageTempo(float val);
void UpdateStretchTempo();
u32 m_buffer_size = 0;
std::unique_ptr<s32[]> m_buffer;
// volume, 0-100
u32 m_output_volume = FullVolume;
std::atomic<u32> m_rpos{0};
std::atomic<u32> m_wpos{0};
HeapFIFOQueue<SampleType, MaxSamples> m_buffer;
mutable std::mutex m_buffer_mutex;
std::condition_variable m_buffer_draining_cv;
std::vector<SampleType> m_resample_buffer;
std::unique_ptr<soundtouch::SoundTouch> m_soundtouch;
std::atomic_bool m_underflow_flag{false};
std::atomic_bool m_buffer_filling{false};
u32 m_max_samples = 0;
u32 m_target_buffer_size = 0;
u32 m_stretch_reset = STRETCH_RESET_THRESHOLD;
bool m_output_paused = true;
bool m_sync = true;
bool m_wait_for_buffer_fill = false;
u32 m_stretch_ok_count = 0;
float m_nominal_rate = 1.0f;
float m_dynamic_target_usage = 0.0f;
// Resampling
double m_resampler_ratio = 1.0;
void* m_resampler_state = nullptr;
std::mutex m_resampler_mutex;
HeapFIFOQueue<SampleType, MaxSamples> m_resampled_buffer;
std::vector<float> m_resample_in_buffer;
std::vector<float> m_resample_out_buffer;
u32 m_average_position = 0;
u32 m_average_available = 0;
u32 m_staging_buffer_pos = 0;
std::array<float, AVERAGING_BUFFER_SIZE> m_average_fullness = {};
// temporary staging buffer, used for timestretching
alignas(16) std::array<s32, CHUNK_SIZE> m_staging_buffer;
// float buffer, soundtouch only accepts float samples as input
alignas(16) std::array<float, CHUNK_SIZE * MAX_CHANNELS> m_float_buffer;
};
#ifdef _MSC_VER
#pragma warning(pop)
#endif

View file

@ -1,25 +0,0 @@
#include "null_audio_stream.h"
NullAudioStream::NullAudioStream() = default;
NullAudioStream::~NullAudioStream() = default;
bool NullAudioStream::OpenDevice()
{
return true;
}
void NullAudioStream::PauseDevice(bool paused) {}
void NullAudioStream::CloseDevice() {}
void NullAudioStream::FramesAvailable()
{
// drop any buffer as soon as they're available
DropFrames(GetSamplesAvailable());
}
std::unique_ptr<AudioStream> AudioStream::CreateNullAudioStream()
{
return std::make_unique<NullAudioStream>();
}

View file

@ -1,15 +0,0 @@
#pragma once
#include "audio_stream.h"
class NullAudioStream final : public AudioStream
{
public:
NullAudioStream();
~NullAudioStream();
protected:
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
};

View file

@ -4,13 +4,15 @@
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>%(PreprocessorDefinitions);SOUNDTOUCH_FLOAT_SAMPLES;SOUNDTOUCH_ALLOW_SSE</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(Platform)'=='ARM64'">%(PreprocessorDefinitions);SOUNDTOUCH_USE_NEON</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(SolutionDir)dep\soundtouch\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;$(RootBuildDir)libsamplerate\libsamplerate.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(RootBuildDir)soundtouch\soundtouch.lib;$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>

View file

@ -9,7 +9,6 @@
<ClInclude Include="ini_settings_interface.h" />
<ClInclude Include="iso_reader.h" />
<ClInclude Include="jit_code_buffer.h" />
<ClInclude Include="null_audio_stream.h" />
<ClInclude Include="pbp_types.h" />
<ClInclude Include="memory_arena.h" />
<ClInclude Include="page_fault_handler.h" />
@ -38,7 +37,6 @@
<ClCompile Include="iso_reader.cpp" />
<ClCompile Include="jit_code_buffer.cpp" />
<ClCompile Include="cd_subchannel_replacement.cpp" />
<ClCompile Include="null_audio_stream.cpp" />
<ClCompile Include="shiftjis.cpp" />
<ClCompile Include="memory_arena.cpp" />
<ClCompile Include="page_fault_handler.cpp" />

View file

@ -8,7 +8,6 @@
<ClInclude Include="iso_reader.h" />
<ClInclude Include="cd_image.h" />
<ClInclude Include="cd_subchannel_replacement.h" />
<ClInclude Include="null_audio_stream.h" />
<ClInclude Include="wav_writer.h" />
<ClInclude Include="cd_image_hasher.h" />
<ClInclude Include="shiftjis.h" />
@ -28,7 +27,6 @@
<ClCompile Include="cd_image_bin.cpp" />
<ClCompile Include="iso_reader.cpp" />
<ClCompile Include="cd_subchannel_replacement.cpp" />
<ClCompile Include="null_audio_stream.cpp" />
<ClCompile Include="cd_image_chd.cpp" />
<ClCompile Include="wav_writer.cpp" />
<ClCompile Include="cd_image_hasher.cpp" />