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 1115 additions and 758 deletions

View file

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

View file

@ -13,6 +13,7 @@
struct WindowInfo; struct WindowInfo;
enum class AudioBackend : u8; enum class AudioBackend : u8;
enum class AudioStretchMode : u8;
class AudioStream; class AudioStream;
class CDImage; 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); 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::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. /// Returns the scale of OSD elements.
float GetOSDScale(); float GetOSDScale();

View file

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

View file

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

View file

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

View file

@ -923,9 +923,7 @@ void System::PauseSystem(bool paused)
return; return;
SetState(paused ? State::Paused : State::Running); SetState(paused ? State::Paused : State::Running);
if (!paused) g_spu.GetOutputStream()->SetPaused(paused);
g_spu.GetOutputStream()->EmptyBuffers();
g_spu.GetOutputStream()->PauseOutput(paused);
if (paused) if (paused)
{ {
@ -1179,7 +1177,7 @@ bool System::BootSystem(SystemBootParameters parameters)
// Good to go. // Good to go.
Host::OnSystemStarted(); Host::OnSystemStarted();
UpdateSoftwareCursor(); UpdateSoftwareCursor();
g_spu.GetOutputStream()->PauseOutput(false); g_spu.GetOutputStream()->SetPaused(false);
// Initial state must be set before loading state. // Initial state must be set before loading state.
s_state = s_state =
@ -1813,7 +1811,7 @@ bool System::DoLoadState(ByteStream* state, bool force_software_renderer, bool u
if (s_state == State::Starting) if (s_state == State::Starting)
s_state = State::Running; s_state = State::Running;
g_spu.GetOutputStream()->EmptyBuffers(); g_spu.GetOutputStream()->EmptyBuffer();
ResetPerformanceCounters(); ResetPerformanceCounters();
ResetThrottler(); ResetThrottler();
return true; return true;
@ -2035,14 +2033,6 @@ void System::ResetThrottler()
void System::Throttle() 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. // Allow variance of up to 40ms either way.
#ifndef __ANDROID__ #ifndef __ANDROID__
static constexpr double MAX_VARIANCE_TIME_NS = 40 * 1000000; 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; m_display_all_frames = !m_throttler_enabled || g_settings.display_all_frames;
bool syncing_to_host = false; 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; float host_refresh_rate;
if (g_host_display->GetHostRefreshRate(&host_refresh_rate)) if (g_host_display->GetHostRefreshRate(&host_refresh_rate))
@ -2212,21 +2203,18 @@ void System::UpdateSpeedLimiterState()
UpdateThrottlePeriod(); UpdateThrottlePeriod();
ResetThrottler(); 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(); 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) if (g_settings.audio_fast_forward_volume != g_settings.audio_output_volume)
stream->SetOutputVolume(GetAudioOutputVolume()); stream->SetOutputVolume(GetAudioOutputVolume());
stream->SetSync(audio_sync_enabled); // Adjust nominal rate when resampling, or syncing to host.
if (audio_sync_enabled) const bool rate_adjust =
stream->EmptyBuffers(); (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); g_host_display->SetDisplayMaxFPS(max_display_fps);
@ -3034,8 +3022,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
UpdateOverclock(); UpdateOverclock();
} }
if (g_settings.audio_backend != old_settings.audio_backend || 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.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) 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); g_dma.SetHaltTicks(g_settings.dma_halt_ticks);
if (g_settings.audio_backend != old_settings.audio_backend || 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.video_sync_enabled != old_settings.video_sync_enabled ||
g_settings.audio_sync_enabled != old_settings.audio_sync_enabled || g_settings.audio_sync_enabled != old_settings.audio_sync_enabled ||
g_settings.increase_timer_resolution != old_settings.increase_timer_resolution || 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.fast_forward_speed != old_settings.fast_forward_speed ||
g_settings.display_max_fps != old_settings.display_max_fps || g_settings.display_max_fps != old_settings.display_max_fps ||
g_settings.display_all_frames != old_settings.display_all_frames || 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) g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate)
{ {
UpdateSpeedLimiterState(); UpdateSpeedLimiterState();

View file

@ -20,14 +20,24 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent
SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", &Settings::ParseAudioBackend, SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.audioBackend, "Audio", "Backend", &Settings::ParseAudioBackend,
&Settings::GetAudioBackendName, Settings::DEFAULT_AUDIO_BACKEND); &Settings::GetAudioBackendName, Settings::DEFAULT_AUDIO_BACKEND);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToOutput, "Audio", "Sync", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToOutput, "Audio", "Sync", true);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferSize, "Audio", "BufferSize", SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.stretchMode, "Audio", "StretchMode",
Settings::DEFAULT_AUDIO_BUFFER_SIZE); &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.startDumpingOnBoot, "Audio", "DumpOnBoot", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muteCDAudio, "CDROM", "MuteCDAudio", 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); m_ui.outputLatencyMinimal->setChecked(m_ui.outputLatencyMS->value() == 0);
updateBufferingLabel(); 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 // for per-game, just use the normal path, since it needs to re-read/apply
if (!dialog->isPerGameSettings()) 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 " "lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio "
"output.")); "output."));
dialog->registerWidgetHelp( 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 " 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 " "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 " "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 " tr("Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable "
"background music in some games.")); "background music in some games."));
dialog->registerWidgetHelp( dialog->registerWidgetHelp(
m_ui.resampling, tr("Resampling"), tr("Checked"), m_ui.stretchMode, tr("Stretch Mode"), tr("Time Stretching"),
tr("When running outside of 100% speed, resamples audio from the target speed instead of dropping frames. Produces " 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.")); "much nicer fast forward/slowdown audio at a small cost to performance."));
} }
AudioSettingsWidget::~AudioSettingsWidget() = default; AudioSettingsWidget::~AudioSettingsWidget() = default;
void AudioSettingsWidget::updateBufferingLabel() void AudioSettingsWidget::updateLatencyLabel()
{ {
constexpr float step = 128; const u32 output_latency_ms = static_cast<u32>(m_ui.outputLatencyMS->value());
const u32 actual_buffer_size = const u32 output_latency_frames = AudioStream::GetBufferSizeForMS(SPU::SAMPLE_RATE, output_latency_ms);
static_cast<u32>(std::round(static_cast<float>(m_ui.bufferSize->value()) / step) * step); const u32 buffer_ms = static_cast<u32>(m_ui.bufferMS->value());
if (static_cast<u32>(m_ui.bufferSize->value()) != actual_buffer_size) 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)); m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 frames / %2 ms (%3ms buffer + %5ms output)")
return; .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() void AudioSettingsWidget::updateVolumeLabel()
@ -104,9 +118,21 @@ void AudioSettingsWidget::updateVolumeLabel()
m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value())); 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) 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()); g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value());
updateVolumeLabel(); updateVolumeLabel();
@ -114,7 +140,9 @@ void AudioSettingsWidget::onOutputVolumeChanged(int new_value)
void AudioSettingsWidget::onFastForwardVolumeChanged(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); g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value);
updateVolumeLabel(); updateVolumeLabel();
@ -122,7 +150,10 @@ void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value)
void AudioSettingsWidget::onOutputMutedChanged(int new_state) void AudioSettingsWidget::onOutputMutedChanged(int new_state)
{ {
// only called for base settings
DebugAssert(!m_dialog->isPerGameSettings());
const bool muted = (new_state != 0); const bool muted = (new_state != 0);
m_dialog->setBoolSettingValue("Audio", "OutputMuted", muted); Host::SetBaseBoolSettingValue("Audio", "OutputMuted", muted);
g_emu_thread->setAudioOutputMuted(muted); g_emu_thread->setAudioOutputMuted(muted);
} }

View file

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

View file

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>502</width> <width>516</width>
<height>312</height> <height>435</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -31,7 +31,21 @@
<property name="title"> <property name="title">
<string>Configuration</string> <string>Configuration</string>
</property> </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"> <item row="0" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
@ -39,55 +53,87 @@
</property> </property>
</widget> </widget>
</item> </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"> <item row="0" column="1">
<widget class="QComboBox" name="audioBackend"/> <widget class="QComboBox" name="audioBackend"/>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>Buffer Size:</string> <string>Driver:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="0">
<widget class="QSlider" name="bufferSize"> <widget class="QLabel" name="label_7">
<property name="minimum"> <property name="text">
<number>1024</number> <string>Stretch Mode:</string>
</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>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="6" column="0" colspan="2">
<spacer name="horizontalSpacer"> <widget class="QCheckBox" name="syncToOutput">
<property name="orientation"> <property name="text">
<enum>Qt::Horizontal</enum> <string>Sync To Output</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property> </property>
</spacer> </widget>
</item> </item>
<item row="2" column="1"> <item row="5" column="0" colspan="2">
<widget class="QLabel" name="bufferingLabel"> <widget class="QLabel" name="bufferingLabel">
<property name="text"> <property name="text">
<string>Maximum latency: 0 frames (0.00ms)</string> <string>Maximum latency: 0 frames (0.00ms)</string>
@ -97,24 +143,31 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="1">
<widget class="QCheckBox" name="syncToOutput"> <widget class="QSlider" name="bufferMS">
<property name="text"> <property name="minimum">
<string>Sync To Output</string> <number>15</number>
</property> </property>
</widget> <property name="maximum">
</item> <number>500</number>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="resampling">
<property name="text">
<string>Resampling</string>
</property> </property>
</widget> <property name="singleStep">
</item> <number>1</number>
<item row="5" column="0" colspan="2"> </property>
<widget class="QCheckBox" name="startDumpingOnBoot"> <property name="pageStep">
<property name="text"> <number>5</number>
<string>Start Dumping On Boot</string> </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> </property>
</widget> </widget>
</item> </item>

View file

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

View file

@ -1,9 +1,13 @@
#pragma once #pragma once
#include "core/system.h" #include "core/system.h"
#include <memory>
#include <mutex> #include <mutex>
class SettingsInterface; class SettingsInterface;
class AudioStream;
enum class AudioStretchMode : u8;
namespace CommonHost { namespace CommonHost {
/// Initializes configuration. /// Initializes configuration.
void UpdateLogSettings(); void UpdateLogSettings();
@ -25,6 +29,19 @@ void OnGameChanged(const std::string& disc_path, const std::string& game_serial,
void PumpMessagesOnCPUThread(); void PumpMessagesOnCPUThread();
bool CreateHostDisplayResources(); bool CreateHostDisplayResources();
void ReleaseHostDisplayResources(); 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 CommonHost
namespace ImGuiManager { namespace ImGuiManager {

View file

@ -1,6 +1,11 @@
#include "cubeb_audio_stream.h" #include "cubeb_audio_stream.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/log.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); Log_SetChannel(CubebAudioStream);
#ifdef _WIN32 #ifdef _WIN32
@ -9,154 +14,188 @@ Log_SetChannel(CubebAudioStream);
#pragma comment(lib, "Ole32.lib") #pragma comment(lib, "Ole32.lib")
#endif #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() CubebAudioStream::~CubebAudioStream()
{ {
if (IsOpen()) DestroyContextAndStream();
CubebAudioStream::CloseDevice(); }
void CubebAudioStream::LogCallback(const char* fmt, ...)
{
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());
} }
bool CubebAudioStream::OpenDevice() void CubebAudioStream::DestroyContextAndStream()
{ {
Assert(!IsOpen()); 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 #ifdef _WIN32
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
m_com_initialized_by_us = SUCCEEDED(hr); 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; return false;
} }
#endif #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) 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; return false;
} }
cubeb_stream_params params = {}; cubeb_stream_params params = {};
params.format = CUBEB_SAMPLE_S16LE; params.format = CUBEB_SAMPLE_S16LE;
params.rate = m_output_sample_rate; params.rate = m_sample_rate;
params.channels = m_channels; params.channels = m_channels;
params.layout = CUBEB_LAYOUT_UNDEFINED; params.layout = CUBEB_LAYOUT_UNDEFINED;
params.prefs = CUBEB_STREAM_PREF_PERSIST; params.prefs = CUBEB_STREAM_PREF_NONE;
u32 latency_frames = 0; u32 latency_frames = GetBufferSizeForMS(m_sample_rate, (latency_ms == 0) ? m_buffer_ms : latency_ms);
rv = cubeb_get_min_latency(m_cubeb_context, &params, &latency_frames); u32 min_latency_frames = 0;
rv = cubeb_get_min_latency(m_context, &params, &min_latency_frames);
if (rv == CUBEB_ERROR_NOT_SUPPORTED) if (rv == CUBEB_ERROR_NOT_SUPPORTED)
{ {
Log_WarningPrintf("Cubeb backend does not support latency queries, using buffer size of %u.", m_buffer_size); Log_DevPrintf("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).",
latency_frames = m_buffer_size; m_buffer_ms, latency_frames);
} }
else else
{ {
if (rv != CUBEB_OK) if (rv != CUBEB_OK)
{ {
Log_ErrorPrintf("Could not get minimum latency: %d", rv); Log_ErrorPrintf("(Cubeb) Could not get minimum latency: %d", rv);
DestroyContext(); DestroyContextAndStream();
return false; return false;
} }
Log_InfoPrintf("Minimum latency in frames: %u", latency_frames); const u32 minimum_latency_ms = GetMSForBufferSize(m_sample_rate, min_latency_frames);
if (latency_frames > m_buffer_size) 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, // use minimum
m_buffer_size); latency_frames = min_latency_frames;
if (!SetBufferSize(latency_frames))
{
Log_ErrorPrintf("Failed to set new buffer size of %u frames", latency_frames);
DestroyContext();
return false;
}
} }
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;
} }
} }
BaseInitialize();
m_volume = 100;
m_paused = false;
char stream_name[32]; char stream_name[32];
std::snprintf(stream_name, sizeof(stream_name), "AudioStream_%p", this); std::snprintf(stream_name, sizeof(stream_name), "%p", this);
rv = cubeb_stream_init(m_cubeb_context, &m_cubeb_stream, stream_name, nullptr, nullptr, nullptr, &params, rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, &params, latency_frames,
latency_frames, DataCallback, StateCallback, this); &CubebAudioStream::DataCallback, StateCallback, this);
if (rv != CUBEB_OK) if (rv != CUBEB_OK)
{ {
Log_ErrorPrintf("Could not create stream: %d", rv); Log_ErrorPrintf("(Cubeb) Could not create stream: %d", rv);
DestroyContext(); DestroyContextAndStream();
return false; return false;
} }
cubeb_stream_set_volume(m_cubeb_stream, static_cast<float>(m_output_volume) / 100.0f); rv = cubeb_stream_start(stream);
return true;
}
void CubebAudioStream::PauseDevice(bool paused)
{
if (paused == m_paused)
return;
int rv = paused ? cubeb_stream_stop(m_cubeb_stream) : cubeb_stream_start(m_cubeb_stream);
if (rv != CUBEB_OK) if (rv != CUBEB_OK)
{ {
Log_ErrorPrintf("cubeb_stream_%s failed: %d", paused ? "stop" : "start", rv); Log_ErrorPrintf("(Cubeb) Could not start stream: %d", rv);
return; DestroyContextAndStream();
return false;
} }
m_paused = paused; return true;
} }
void CubebAudioStream::CloseDevice() void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state)
{ {
Assert(IsOpen()); // noop
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 CubebAudioStream::DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
long nframes) long nframes)
{ {
CubebAudioStream* const this_ptr = static_cast<CubebAudioStream*>(user_ptr); static_cast<CubebAudioStream*>(user_ptr)->ReadFrames(static_cast<s16*>(output_buffer), static_cast<u32>(nframes));
this_ptr->ReadFrames(reinterpret_cast<SampleType*>(output_buffer), static_cast<u32>(nframes), false);
return nframes; return nframes;
} }
void CubebAudioStream::StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) {} void CubebAudioStream::SetPaused(bool paused)
{
if (paused == m_paused || !stream)
return;
void CubebAudioStream::FramesAvailable() {} const int rv = paused ? cubeb_stream_stop(stream) : cubeb_stream_start(stream);
if (rv != CUBEB_OK)
{
Log_ErrorPrintf("Could not %s stream: %d", paused ? "pause" : "resume", rv);
return;
}
void CubebAudioStream::SetOutputVolume(u32 volume) m_paused = paused;
{
AudioStream::SetOutputVolume(volume);
cubeb_stream_set_volume(m_cubeb_stream, static_cast<float>(m_output_volume) / 100.0f);
} }
void CubebAudioStream::DestroyContext() void CubebAudioStream::SetOutputVolume(u32 volume)
{ {
cubeb_destroy(m_cubeb_context); if (volume == m_volume)
m_cubeb_context = nullptr; return;
#ifdef _WIN32 int rv = cubeb_stream_set_volume(stream, static_cast<float>(volume) / 100.0f);
if (m_com_initialized_by_us) if (rv != CUBEB_OK)
CoUninitialize(); {
#endif Log_ErrorPrintf("cubeb_stream_set_volume() failed: %d", rv);
return;
}
m_volume = volume;
} }
std::unique_ptr<AudioStream> CubebAudioStream::Create() std::unique_ptr<AudioStream> CommonHost::CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms,
u32 latency_ms, AudioStretchMode stretch)
{ {
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 #pragma once
#include "cubeb/cubeb.h"
#include "util/audio_stream.h" #include "util/audio_stream.h"
#include <cstdint> #include <cstdint>
class CubebAudioStream final : public AudioStream struct cubeb;
struct cubeb_stream;
class CubebAudioStream : public AudioStream
{ {
public: public:
CubebAudioStream(); CubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~CubebAudioStream(); ~CubebAudioStream();
static std::unique_ptr<AudioStream> Create(); void SetPaused(bool paused) override;
protected:
bool IsOpen() const { return m_cubeb_stream != nullptr; }
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
void SetOutputVolume(u32 volume) 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, static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer,
long nframes); long nframes);
static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state);
cubeb* m_cubeb_context = nullptr; void DestroyContextAndStream();
cubeb_stream* m_cubeb_stream = nullptr;
bool m_paused = true; cubeb* m_context = nullptr;
cubeb_stream* stream = nullptr;
#ifdef _WIN32 #ifdef _WIN32
bool m_com_initialized_by_us = false; 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.", "The audio backend determines how frames produced by the emulator are submitted to the host.",
"Audio", "Backend", Settings::DEFAULT_AUDIO_BACKEND, &Settings::ParseAudioBackend, "Audio", "Backend", Settings::DEFAULT_AUDIO_BACKEND, &Settings::ParseAudioBackend,
&Settings::GetAudioBackendName, &Settings::GetAudioBackendDisplayName, AudioBackend::Count); &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.", "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", DrawToggleSetting("Sync To Output",
"Throttles the emulation speed based on the audio backend pulling audio " "Throttles the emulation speed based on the audio backend pulling audio "
"frames. Enable to reduce the chances of crackling.", "frames. Enable to reduce the chances of crackling.",
"Audio", "Sync", true); "Audio", "Sync", true);
DrawToggleSetting( DrawToggleSetting(
"Resampling", "Time Stretching",
"When running outside of 100% speed, resamples audio from the target speed instead of dropping frames.", "Audio", "When running outside of 100% speed, adjusts tempo on audio from the target speed instead of dropping frames.",
"Resampling", true); "Audio", "TimeStretching", true);
EndMenuButtons(); EndMenuButtons();
} }

View file

@ -13,6 +13,7 @@
#include "core/host_display.h" #include "core/host_display.h"
#include "core/host_settings.h" #include "core/host_settings.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/spu.h"
#include "core/system.h" #include "core/system.h"
#include "fmt/chrono.h" #include "fmt/chrono.h"
#include "fmt/format.h" #include "fmt/format.h"
@ -23,6 +24,7 @@
#include "imgui_internal.h" #include "imgui_internal.h"
#include "imgui_manager.h" #include "imgui_manager.h"
#include "input_manager.h" #include "input_manager.h"
#include "util/audio_stream.h"
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@ -172,6 +174,16 @@ void ImGuiManager::DrawPerformanceOverlay()
FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime()); FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255)); 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) if (g_settings.display_show_status_indicators)

View file

@ -1,11 +1,15 @@
#include "sdl_audio_stream.h" #include "sdl_audio_stream.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/log.h" #include "common/log.h"
#include "common_host.h"
#include "sdl_initializer.h" #include "sdl_initializer.h"
#include <SDL.h> #include <SDL.h>
Log_SetChannel(SDLAudioStream); 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() SDLAudioStream::~SDLAudioStream()
{ {
@ -13,12 +17,16 @@ SDLAudioStream::~SDLAudioStream()
SDLAudioStream::CloseDevice(); 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()); DebugAssert(!IsOpen());
@ -31,22 +39,15 @@ bool SDLAudioStream::OpenDevice()
} }
SDL_AudioSpec spec = {}; SDL_AudioSpec spec = {};
spec.freq = m_output_sample_rate; spec.freq = m_sample_rate;
spec.channels = static_cast<Uint8>(m_channels); spec.channels = static_cast<Uint8>(m_channels);
spec.format = AUDIO_S16; 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.callback = AudioCallback;
spec.userdata = static_cast<void*>(this); spec.userdata = static_cast<void*>(this);
SDL_AudioSpec obtained_spec = {}; SDL_AudioSpec obtained_spec = {};
m_device_id = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained_spec, SDL_AUDIO_ALLOW_SAMPLES_CHANGE);
#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);
if (m_device_id == 0) if (m_device_id == 0)
{ {
Log_ErrorPrintf("SDL_OpenAudioDevice() failed: %s", SDL_GetError()); Log_ErrorPrintf("SDL_OpenAudioDevice() failed: %s", SDL_GetError());
@ -54,25 +55,23 @@ bool SDLAudioStream::OpenDevice()
return false; return false;
} }
if (obtained_spec.samples > spec.samples) Log_DevPrintf("Requested %u frame buffer, got %u frame buffer", spec.samples, obtained_spec.samples);
{
Log_WarningPrintf("Requested buffer size %u, got buffer size %u. Adjusting to compensate.", spec.samples, BaseInitialize();
obtained_spec.samples); m_volume = 100;
m_paused = false;
if (!SetBufferSize(obtained_spec.samples)) SDL_PauseAudioDevice(m_device_id, 0);
{
Log_ErrorPrintf("Failed to set new buffer size of %u", obtained_spec.samples);
CloseDevice();
return false;
}
}
return true; 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); SDL_PauseAudioDevice(m_device_id, paused ? 1 : 0);
m_paused = paused;
} }
void SDLAudioStream::CloseDevice() 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); SDLAudioStream* const this_ptr = static_cast<SDLAudioStream*>(userdata);
const u32 num_frames = len / sizeof(SampleType) / this_ptr->m_channels; 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 class SDLAudioStream final : public AudioStream
{ {
public: public:
SDLAudioStream(); SDLAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~SDLAudioStream(); ~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: protected:
ALWAYS_INLINE bool IsOpen() const { return (m_device_id != 0); } 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); static void AudioCallback(void* userdata, uint8_t* stream, int len);
u32 m_device_id = 0; u32 m_device_id = 0;

View file

@ -1,6 +1,7 @@
#include "xaudio2_audio_stream.h" #include "xaudio2_audio_stream.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/log.h" #include "common/log.h"
#include "common_host.h"
#include <VersionHelpers.h> #include <VersionHelpers.h>
#include <xaudio2.h> #include <xaudio2.h>
Log_SetChannel(XAudio2AudioStream); Log_SetChannel(XAudio2AudioStream);
@ -9,12 +10,15 @@ Log_SetChannel(XAudio2AudioStream);
#pragma comment(lib, "xaudio2.lib") #pragma comment(lib, "xaudio2.lib")
#endif #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() XAudio2AudioStream::~XAudio2AudioStream()
{ {
if (IsOpen()) if (IsOpen())
XAudio2AudioStream::CloseDevice(); CloseDevice();
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
if (m_xaudio2_library) if (m_xaudio2_library)
@ -25,8 +29,20 @@ XAudio2AudioStream::~XAudio2AudioStream()
#endif #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) #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
m_xaudio2_library = LoadLibraryW(XAUDIO2_DLL_W); m_xaudio2_library = LoadLibraryW(XAUDIO2_DLL_W);
if (!m_xaudio2_library) if (!m_xaudio2_library)
@ -36,13 +52,6 @@ bool XAudio2AudioStream::Initialize()
} }
#endif #endif
return true;
}
bool XAudio2AudioStream::OpenDevice()
{
DebugAssert(!IsOpen());
HRESULT hr; HRESULT hr;
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
using PFNXAUDIO2CREATE = using PFNXAUDIO2CREATE =
@ -70,7 +79,7 @@ bool XAudio2AudioStream::OpenDevice()
return false; 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)) if (FAILED(hr))
{ {
Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr); Log_ErrorPrintf("CreateMasteringVoice() failed: %08X", hr);
@ -79,10 +88,10 @@ bool XAudio2AudioStream::OpenDevice()
WAVEFORMATEX wf = {}; WAVEFORMATEX wf = {};
wf.cbSize = sizeof(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.nBlockAlign = static_cast<WORD>(sizeof(s16) * m_channels);
wf.nChannels = static_cast<WORD>(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.wBitsPerSample = sizeof(s16) * 8;
wf.wFormatTag = WAVE_FORMAT_PCM; wf.wFormatTag = WAVE_FORMAT_PCM;
hr = m_xaudio->CreateSourceVoice(&m_source_voice, &wf, 0, 1.0f, this); hr = m_xaudio->CreateSourceVoice(&m_source_voice, &wf, 0, 1.0f, this);
@ -99,13 +108,27 @@ bool XAudio2AudioStream::OpenDevice()
return false; 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++) 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; return true;
} }
void XAudio2AudioStream::PauseDevice(bool paused) void XAudio2AudioStream::SetPaused(bool paused)
{ {
if (m_paused == paused) if (m_paused == paused)
return; return;
@ -124,6 +147,9 @@ void XAudio2AudioStream::PauseDevice(bool paused)
} }
m_paused = paused; m_paused = paused;
if (!m_buffer_enqueued)
EnqueueBuffer();
} }
void XAudio2AudioStream::CloseDevice() void XAudio2AudioStream::CloseDevice()
@ -139,29 +165,20 @@ void XAudio2AudioStream::CloseDevice()
m_source_voice = nullptr; m_source_voice = nullptr;
m_mastering_voice = nullptr; m_mastering_voice = nullptr;
m_xaudio.Reset(); m_xaudio.Reset();
m_buffers = {}; m_enqueue_buffers = {};
m_current_buffer = 0; m_current_buffer = 0;
m_paused = true; m_paused = true;
} }
void XAudio2AudioStream::FramesAvailable()
{
if (!m_buffer_enqueued)
{
m_buffer_enqueued = true;
EnqueueBuffer();
}
}
void XAudio2AudioStream::EnqueueBuffer() void XAudio2AudioStream::EnqueueBuffer()
{ {
SampleType* samples = m_buffers[m_current_buffer].get(); SampleType* samples = m_enqueue_buffers[m_current_buffer].get();
ReadFrames(samples, m_buffer_size, false); ReadFrames(samples, m_enqueue_buffer_size);
const XAUDIO2_BUFFER buf = { const XAUDIO2_BUFFER buf = {
static_cast<UINT32>(0), // flags static_cast<UINT32>(0), // flags
static_cast<UINT32>(sizeof(s16) * m_channels * m_buffer_size), // bytes static_cast<UINT32>(sizeof(s16) * m_channels * m_enqueue_buffer_size), // bytes
reinterpret_cast<const BYTE*>(samples) // data reinterpret_cast<const BYTE*>(samples) // data
}; };
HRESULT hr = m_source_voice->SubmitSourceBuffer(&buf, nullptr); HRESULT hr = m_source_voice->SubmitSourceBuffer(&buf, nullptr);
@ -173,10 +190,14 @@ void XAudio2AudioStream::EnqueueBuffer()
void XAudio2AudioStream::SetOutputVolume(u32 volume) void XAudio2AudioStream::SetOutputVolume(u32 volume)
{ {
AudioStream::SetOutputVolume(volume); HRESULT hr = m_mastering_voice->SetVolume(static_cast<float>(m_volume) / 100.0f);
HRESULT hr = m_mastering_voice->SetVolume(static_cast<float>(m_output_volume) / 100.0f);
if (FAILED(hr)) if (FAILED(hr))
{
Log_ErrorPrintf("SetVolume() failed: %08X", hr); Log_ErrorPrintf("SetVolume() failed: %08X", hr);
return;
}
m_volume = volume;
} }
void __stdcall XAudio2AudioStream::OnVoiceProcessingPassStart(UINT32 BytesRequired) {} 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::OnLoopEnd(void* pBufferContext) {}
void __stdcall XAudio2AudioStream::OnVoiceError(void* pBufferContext, HRESULT Error) {} 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 class XAudio2AudioStream final : public AudioStream, private IXAudio2VoiceCallback
{ {
public: public:
XAudio2AudioStream(); XAudio2AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
~XAudio2AudioStream(); ~XAudio2AudioStream();
bool Initialize(); void SetPaused(bool paused) override;
void SetOutputVolume(u32 volume) override; void SetOutputVolume(u32 volume) override;
protected: bool OpenDevice(u32 latency_ms);
void CloseDevice();
void EnqueueBuffer();
private:
enum : u32 enum : u32
{ {
NUM_BUFFERS = 2 NUM_BUFFERS = 2,
INTERNAL_BUFFER_SIZE = 512,
}; };
ALWAYS_INLINE bool IsOpen() const { return static_cast<bool>(m_xaudio); } 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 // Inherited via IXAudio2VoiceCallback
virtual void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override; void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) override;
virtual void __stdcall OnVoiceProcessingPassEnd(void) override; void __stdcall OnVoiceProcessingPassEnd(void) override;
virtual void __stdcall OnStreamEnd(void) override; void __stdcall OnStreamEnd(void) override;
virtual void __stdcall OnBufferStart(void* pBufferContext) override; void __stdcall OnBufferStart(void* pBufferContext) override;
virtual void __stdcall OnBufferEnd(void* pBufferContext) override; void __stdcall OnBufferEnd(void* pBufferContext) override;
virtual void __stdcall OnLoopEnd(void* pBufferContext) override; void __stdcall OnLoopEnd(void* pBufferContext) override;
virtual void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override; void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override;
void EnqueueBuffer();
Microsoft::WRL::ComPtr<IXAudio2> m_xaudio; Microsoft::WRL::ComPtr<IXAudio2> m_xaudio;
IXAudio2MasteringVoice* m_mastering_voice = nullptr; IXAudio2MasteringVoice* m_mastering_voice = nullptr;
IXAudio2SourceVoice* m_source_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; u32 m_current_buffer = 0;
bool m_buffer_enqueued = false; bool m_buffer_enqueued = false;
bool m_paused = true;
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
HMODULE m_xaudio2_library = {}; HMODULE m_xaudio2_library = {};

View file

@ -27,8 +27,6 @@ add_library(util
iso_reader.h iso_reader.h
jit_code_buffer.cpp jit_code_buffer.cpp
jit_code_buffer.h jit_code_buffer.h
null_audio_stream.cpp
null_audio_stream.h
memory_arena.cpp memory_arena.cpp
memory_arena.h memory_arena.h
page_fault_handler.cpp page_fault_handler.cpp
@ -44,4 +42,4 @@ add_library(util
target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_link_libraries(util PUBLIC common simpleini) 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 "audio_stream.h"
#include "assert.h" #include "SoundTouch.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/log.h" #include "common/log.h"
#include "samplerate.h" #include "common/make_array.h"
#include "common/timer.h"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cstring> #include <cstring>
Log_SetChannel(AudioStream); 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() AudioStream::~AudioStream()
{ {
DestroyResampler(); DestroyBuffer();
} }
bool AudioStream::Reconfigure(u32 input_sample_rate /* = DefaultInputSampleRate */, std::unique_ptr<AudioStream> AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms)
u32 output_sample_rate /* = DefaultOutputSampleRate */, u32 channels /* = 1 */,
u32 buffer_size /* = DefaultBufferSize */)
{ {
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex); return std::unique_ptr<AudioStream>(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off));
std::unique_lock<std::mutex> resampler_Lock(m_resampler_mutex); }
u32 AudioStream::GetAlignedBufferSize(u32 size)
{
static_assert(Common::IsPow2(CHUNK_SIZE));
return Common::AlignUpPow2(size, CHUNK_SIZE);
}
u32 AudioStream::GetBufferSizeForMS(u32 sample_rate, u32 ms)
{
return GetAlignedBufferSize((ms * sample_rate) / 1000u);
}
DestroyResampler(); u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size)
if (IsDeviceOpen()) {
CloseDevice(); buffer_size = GetAlignedBufferSize(buffer_size);
return (buffer_size * 1000u) / sample_rate;
}
m_output_sample_rate = output_sample_rate; static constexpr const auto s_stretch_mode_names = make_array("None", "Resample", "TimeStretch");
m_channels = channels;
m_buffer_size = buffer_size;
m_buffer_filling.store(m_wait_for_buffer_fill);
m_output_paused = true;
if (!SetBufferSize(buffer_size)) const char* AudioStream::GetStretchModeName(AudioStretchMode mode)
return false; {
return (static_cast<u32>(mode) < s_stretch_mode_names.size()) ? s_stretch_mode_names[static_cast<u32>(mode)] : "";
}
if (!OpenDevice()) std::optional<AudioStretchMode> AudioStream::ParseStretchMode(const char* name)
{
for (u8 i = 0; i < static_cast<u8>(AudioStretchMode::Count); i++)
{ {
LockedEmptyBuffers(); if (std::strcmp(name, s_stretch_mode_names[i]) == 0)
m_buffer_size = 0; return static_cast<AudioStretchMode>(i);
m_output_sample_rate = 0;
m_channels = 0;
return false;
} }
CreateResampler(); return std::nullopt;
InternalSetInputSampleRate(input_sample_rate); }
return true; u32 AudioStream::GetBufferedFramesRelaxed() const
{
const u32 rpos = m_rpos.load(std::memory_order_relaxed);
const u32 wpos = m_wpos.load(std::memory_order_relaxed);
return (wpos + m_buffer_size - rpos) % m_buffer_size;
} }
void AudioStream::SetInputSampleRate(u32 sample_rate) void AudioStream::ReadFrames(s16* bData, u32 nFrames)
{ {
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex); const u32 available_frames = GetBufferedFramesRelaxed();
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex); 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);
InternalSetInputSampleRate(sample_rate); 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::SetWaitForBufferFill(bool enabled) void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples)
{ {
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex); const u32 free = m_buffer_size - GetBufferedFramesRelaxed();
m_wait_for_buffer_fill = enabled; if (free <= nSamples)
if (enabled && m_buffer.IsEmpty()) {
m_buffer_filling.store(true); 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::InternalSetInputSampleRate(u32 sample_rate) void AudioStream::BaseInitialize()
{ {
if (m_input_sample_rate == sample_rate) AllocateBuffer();
return; StretchAllocate();
}
m_input_sample_rate = sample_rate; void AudioStream::AllocateBuffer()
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); // use a larger buffer when time stretching, since we need more input
ResetResampler(); 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::SetOutputVolume(u32 volume) void AudioStream::DestroyBuffer()
{ {
std::unique_lock<std::mutex> lock(m_buffer_mutex); m_buffer.reset();
m_output_volume = volume; m_buffer_size = 0;
m_wpos.store(0, std::memory_order_release);
m_rpos.store(0, std::memory_order_release);
} }
void AudioStream::PauseOutput(bool paused) void AudioStream::EmptyBuffer()
{ {
if (m_output_paused == paused) if (m_stretch_mode != AudioStretchMode::Off)
return; {
m_soundtouch->clear();
if (m_stretch_mode == AudioStretchMode::TimeStretch)
m_soundtouch->setTempo(m_nominal_rate);
}
PauseDevice(paused); m_wpos.store(m_rpos.load(std::memory_order_acquire), std::memory_order_release);
m_output_paused = paused; }
// Empty buffers on pause. void AudioStream::SetNominalRate(float tempo)
if (paused) {
EmptyBuffers(); m_nominal_rate = tempo;
if (m_stretch_mode == AudioStretchMode::Resample)
m_soundtouch->setRate(tempo);
} }
void AudioStream::Shutdown() void AudioStream::SetStretchMode(AudioStretchMode mode)
{ {
if (!IsDeviceOpen()) if (m_stretch_mode == mode)
return; return;
CloseDevice(); // can't resize the buffers while paused
EmptyBuffers(); bool paused = m_paused;
m_buffer_size = 0; if (!paused)
m_output_sample_rate = 0; SetPaused(true);
m_channels = 0;
m_output_paused = true;
}
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames) DestroyBuffer();
{ StretchDestroy();
m_buffer_mutex.lock(); m_stretch_mode = mode;
const u32 requested_frames = std::min(*num_frames, m_buffer_size); AllocateBuffer();
EnsureBuffer(requested_frames * m_channels); if (m_stretch_mode != AudioStretchMode::Off)
StretchAllocate();
*buffer_ptr = m_buffer.GetWritePointer(); if (!paused)
*num_frames = std::min(m_buffer_size, m_buffer.GetContiguousSpace() / m_channels); SetPaused(false);
} }
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames) void AudioStream::SetPaused(bool paused)
{ {
Assert(num_frames <= m_buffer_size); m_paused = paused;
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(); void AudioStream::SetOutputVolume(u32 volume)
{
m_volume = volume;
} }
void AudioStream::EndWrite(u32 num_frames) void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
{ {
m_buffer.AdvanceTail(num_frames * m_channels); // TODO: Write directly to buffer when not using stretching.
if (m_buffer_filling.load()) *buffer_ptr = reinterpret_cast<s16*>(&m_staging_buffer[m_staging_buffer_pos]);
{ *num_frames = CHUNK_SIZE - m_staging_buffer_pos;
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) void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
{ {
return (static_cast<float>(buffer_size) / static_cast<float>(sample_rate)); Panic("not implemented");
} }
bool AudioStream::SetBufferSize(u32 buffer_size) void AudioStream::EndWrite(u32 num_frames)
{ {
const u32 buffer_size_in_samples = buffer_size * m_channels; // don't bother committing anything when muted
const u32 max_samples = buffer_size_in_samples * 2u; if (m_volume == 0)
if (max_samples > m_buffer.GetCapacity()) return;
return false;
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;
m_buffer_size = buffer_size; if (m_stretch_mode != AudioStretchMode::Off)
m_max_samples = max_samples; StretchWrite();
return true; else
InternalWriteFrames(m_staging_buffer.data(), CHUNK_SIZE);
} }
u32 AudioStream::GetSamplesAvailable() const 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)
{ {
// TODO: Use atomic loads static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
u32 available_samples; 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++)
{ {
std::unique_lock<std::mutex> lock(m_buffer_mutex); const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src));
available_samples = m_buffer.GetSize(); 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;
} }
return available_samples / m_channels;
} }
u32 AudioStream::GetSamplesAvailableLocked() const static void FloatChunkToS16(s32* dst, const float* src, uint size)
{ {
return m_buffer.GetSize() / m_channels; static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
} constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
void AudioStream::ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume) const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16);
{
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)); for (u32 i = 0; i < iterations; i++)
}
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)); 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;
} }
}
if (samples_copied < total_samples) #elif defined(_M_IX86) || defined(_M_AMD64)
{
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); static void S16ChunkToFloat(const s32* src, float* dst)
} {
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);
if (apply_volume && m_output_volume != FullVolume) for (u32 i = 0; i < iterations; i++)
{ {
SampleType* current_ptr = samples; const __m128i sv = _mm_load_si128(reinterpret_cast<const __m128i*>(src));
const SampleType* end_ptr = samples + (num_frames * m_channels); src += 4;
while (current_ptr != end_ptr)
{ __m128i iv1 = _mm_unpacklo_epi16(sv, sv); // [0, 0, 1, 1, 2, 2, 3, 3]
*current_ptr = ApplyVolume(*current_ptr, m_output_volume); __m128i iv2 = _mm_unpackhi_epi16(sv, sv); // [4, 4, 5, 5, 6, 6, 7, 7]
current_ptr++; 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::EnsureBuffer(u32 size) static void FloatChunkToS16(s32* dst, const float* src, uint size)
{ {
DebugAssert(size <= (m_buffer_size * m_channels)); static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
if (GetBufferSpace() >= size) constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
return;
if (m_sync) const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16);
for (u32 i = 0; i < iterations; i++)
{ {
std::unique_lock<std::mutex> lock(m_buffer_mutex, std::adopt_lock); __m128 fv1 = _mm_load_ps(src + 0);
m_buffer_draining_cv.wait(lock, [this, size]() { return GetBufferSpace() >= size; }); __m128 fv2 = _mm_load_ps(src + 4);
lock.release(); 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 }
#else
static void S16ChunkToFloat(const s32* src, float* dst)
{
for (uint i = 0; i < AudioStream::CHUNK_SIZE; ++i)
{ {
m_buffer.Remove(size); *(dst++) = static_cast<float>(static_cast<s16>((u32)*src)) / 32767.0f;
*(dst++) = static_cast<float>(static_cast<s16>(((u32)*src) >> 16)) / 32767.0f;
src++;
} }
} }
void AudioStream::DropFrames(u32 count) static void FloatChunkToS16(s32* dst, const float* src, uint size)
{ {
std::unique_lock<std::mutex> lock(m_buffer_mutex); for (uint i = 0; i < size; ++i)
m_buffer.Remove(count); {
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.
void AudioStream::EmptyBuffers() template<class T>
ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max)
{ {
std::unique_lock<std::mutex> lock(m_buffer_mutex); return (min <= val && val <= max);
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
LockedEmptyBuffers();
} }
void AudioStream::LockedEmptyBuffers() void AudioStream::StretchAllocate()
{ {
m_buffer.Clear(); if (m_stretch_mode == AudioStretchMode::Off)
m_underflow_flag.store(false); return;
m_buffer_filling.store(m_wait_for_buffer_fill);
ResetResampler(); 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::CreateResampler() void AudioStream::StretchDestroy()
{ {
m_resampler_state = src_new(SRC_SINC_MEDIUM_QUALITY, static_cast<int>(m_channels), nullptr); m_soundtouch.reset();
if (!m_resampler_state)
Panic("Failed to allocate resampler");
} }
void AudioStream::DestroyResampler() void AudioStream::StretchWrite()
{ {
if (m_resampler_state) 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)
{ {
src_delete(static_cast<SRC_STATE*>(m_resampler_state)); FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress);
m_resampler_state = nullptr; InternalWriteFrames(m_staging_buffer.data(), tempProgress);
} }
if (m_stretch_mode == AudioStretchMode::TimeStretch)
UpdateStretchTempo();
} }
void AudioStream::ResetResampler() float AudioStream::AddAndGetAverageTempo(float val)
{ {
m_resampled_buffer.Clear(); if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
m_resample_in_buffer.clear(); m_average_available = 0;
m_resample_out_buffer.clear(); if (m_average_available < AVERAGING_BUFFER_SIZE)
src_reset(static_cast<SRC_STATE*>(m_resampler_state)); 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::ResampleInput(std::unique_lock<std::mutex> buffer_lock) void AudioStream::UpdateStretchTempo()
{ {
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex); 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;
const u32 input_space_from_output = (m_resampled_buffer.GetSpace() * m_output_sample_rate) / m_input_sample_rate; // state vars
u32 remaining = std::min(m_buffer.GetSize(), input_space_from_output); if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
if (m_resample_in_buffer.size() < remaining)
{ {
remaining -= static_cast<u32>(m_resample_in_buffer.size()); Log_VerbosePrintf("___ Stretcher is being reset.");
m_resample_in_buffer.reserve(m_resample_in_buffer.size() + remaining); m_stretch_inactive = false;
while (remaining > 0) m_stretch_ok_count = 0;
{ m_dynamic_target_usage = base_target_usage;
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;
}
} }
ReleaseBufferLock(std::move(buffer_lock)); 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);
const u32 potential_output_size = tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO);
(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 = {}; if (tempo < 1.0f)
sd.data_in = m_resample_in_buffer.data(); base_target_usage /= std::sqrt(tempo);
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); m_dynamic_target_usage +=
if (error) 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))
{ {
Log_ErrorPrintf("Resampler error %d", error); m_dynamic_target_usage = base_target_usage;
m_resample_in_buffer.clear();
m_resample_out_buffer.clear();
return;
} }
m_resample_in_buffer.erase(m_resample_in_buffer.begin(), if (!m_stretch_inactive)
m_resample_in_buffer.begin() + (static_cast<u32>(sd.input_frames_used) * m_channels)); {
if (IsInRange(tempo, 1.0f / INACTIVE_GOOD_FACTOR, INACTIVE_GOOD_FACTOR))
m_stretch_ok_count++;
else
m_stretch_ok_count = 0;
const float* write_ptr = m_resample_out_buffer.data(); if (m_stretch_ok_count >= INACTIVE_MIN_OK_COUNT)
remaining = static_cast<u32>(sd.output_frames_gen) * m_channels; {
while (remaining > 0) Log_VerbosePrintf("=== Stretcher is now inactive.");
m_stretch_inactive = true;
}
}
else if (!IsInRange(tempo, 1.0f / INACTIVE_BAD_FACTOR, INACTIVE_BAD_FACTOR))
{
Log_VerbosePrintf("~~~ Stretcher is now active @ tempo %f.", tempo);
m_stretch_inactive = false;
m_stretch_ok_count = 0;
}
if (m_stretch_inactive)
tempo = m_nominal_rate;
if constexpr (LOG_TIMESTRETCH_STATS)
{ {
const u32 samples_to_write = std::min(m_resampled_buffer.GetContiguousSpace(), remaining); static int iterations = 0;
src_float_to_short_array(write_ptr, m_resampled_buffer.GetWritePointer(), static_cast<int>(samples_to_write)); static u64 last_log_time = 0;
m_resampled_buffer.AdvanceTail(samples_to_write);
write_ptr += samples_to_write; const u64 now = Common::Timer::GetCurrentValue();
remaining -= samples_to_write;
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 #pragma once
#include "common/fifo_queue.h"
#include "common/types.h" #include "common/types.h"
#include <array>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <memory> #include <memory>
#include <mutex> #include <optional>
#include <vector>
// 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 class AudioStream
{ {
@ -16,111 +29,116 @@ public:
enum : u32 enum : u32
{ {
DefaultInputSampleRate = 44100, CHUNK_SIZE = 64,
DefaultOutputSampleRate = 44100, MAX_CHANNELS = 2
DefaultBufferSize = 2048,
MaxSamples = 32768,
FullVolume = 100
}; };
AudioStream(); public:
virtual ~AudioStream(); virtual ~AudioStream();
u32 GetOutputSampleRate() const { return m_output_sample_rate; } static u32 GetAlignedBufferSize(u32 size);
u32 GetChannels() const { return m_channels; } static u32 GetBufferSizeForMS(u32 sample_rate, u32 ms);
u32 GetBufferSize() const { return m_buffer_size; } static u32 GetMSForBufferSize(u32 sample_rate, u32 buffer_size);
s32 GetOutputVolume() const { return m_output_volume; }
bool IsSyncing() const { return m_sync; }
bool Reconfigure(u32 input_sample_rate = DefaultInputSampleRate, u32 output_sample_rate = DefaultOutputSampleRate, static const char* GetStretchModeName(AudioStretchMode mode);
u32 channels = 1, u32 buffer_size = DefaultBufferSize); static std::optional<AudioStretchMode> ParseStretchMode(const char* name);
void SetSync(bool enable) { m_sync = enable; }
void SetInputSampleRate(u32 sample_rate); ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
void SetWaitForBufferFill(bool enabled); 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; }
virtual void SetOutputVolume(u32 volume); u32 GetBufferedFramesRelaxed() const;
void PauseOutput(bool paused); /// Temporarily pauses the stream, preventing it from requesting data.
void EmptyBuffers(); virtual void SetPaused(bool paused);
void Shutdown(); virtual void SetOutputVolume(u32 volume);
void BeginWrite(SampleType** buffer_ptr, u32* num_frames); void BeginWrite(SampleType** buffer_ptr, u32* num_frames);
void WriteFrames(const SampleType* frames, u32 num_frames); void WriteFrames(const SampleType* frames, u32 num_frames);
void EndWrite(u32 num_frames); void EndWrite(u32 num_frames);
bool DidUnderflow() void EmptyBuffer();
{
bool expected = true;
return m_underflow_flag.compare_exchange_strong(expected, false);
}
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 void SetStretchMode(AudioStretchMode mode);
static float GetMaxLatency(u32 sample_rate, u32 buffer_size);
static std::unique_ptr<AudioStream> CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms);
protected: protected:
virtual bool OpenDevice() = 0; AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
virtual void PauseDevice(bool paused) = 0; void BaseInitialize();
virtual void CloseDevice() = 0;
virtual void FramesAvailable() = 0;
ALWAYS_INLINE static SampleType ApplyVolume(SampleType sample, u32 volume) void ReadFrames(s16* bData, u32 nSamples);
{
return s16((s32(sample) * s32(volume)) / 100);
}
ALWAYS_INLINE u32 GetBufferSpace() const { return (m_max_samples - m_buffer.GetSize()); } u32 m_sample_rate = 0;
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_channels = 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; u32 m_buffer_size = 0;
std::unique_ptr<s32[]> m_buffer;
std::atomic<u32> m_rpos{0};
std::atomic<u32> m_wpos{0};
std::unique_ptr<soundtouch::SoundTouch> m_soundtouch;
u32 m_target_buffer_size = 0;
u32 m_stretch_reset = STRETCH_RESET_THRESHOLD;
u32 m_stretch_ok_count = 0;
float m_nominal_rate = 1.0f;
float m_dynamic_target_usage = 0.0f;
u32 m_average_position = 0;
u32 m_average_available = 0;
u32 m_staging_buffer_pos = 0;
std::array<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;
};
// volume, 0-100 #ifdef _MSC_VER
u32 m_output_volume = FullVolume; #pragma warning(pop)
#endif
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::atomic_bool m_underflow_flag{false};
std::atomic_bool m_buffer_filling{false};
u32 m_max_samples = 0;
bool m_output_paused = true;
bool m_sync = true;
bool m_wait_for_buffer_fill = false;
// 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;
};

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> <ItemDefinitionGroup>
<ClCompile> <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> </ClCompile>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<Link> <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> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
</Project> </Project>

View file

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

View file

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