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