summaryrefslogtreecommitdiff
path: root/Source/Core
diff options
context:
space:
mode:
authorSam Belliveau <sam.belliveau@gmail.com>2025-03-02 20:19:20 -0500
committerJordan Woyak <jordan.woyak@gmail.com>2025-03-14 01:22:35 -0500
commitf09ba10daae4c44fc9404aa2cc48076f6edec9a4 (patch)
tree7395da269bfc52f61e412c62c6009f30a5d414dd /Source/Core
parente82f03b825ed4ec35539d93512877cdd2a02294b (diff)
AudioCommon: Added Granular Synthesis
Diffstat (limited to 'Source/Core')
-rw-r--r--Source/Core/AudioCommon/AudioStretcher.cpp85
-rw-r--r--Source/Core/AudioCommon/AudioStretcher.h27
-rw-r--r--Source/Core/AudioCommon/CMakeLists.txt3
-rw-r--r--Source/Core/AudioCommon/Mixer.cpp450
-rw-r--r--Source/Core/AudioCommon/Mixer.h147
-rw-r--r--Source/Core/AudioCommon/OpenALStream.cpp4
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp3
-rw-r--r--Source/Core/Core/Config/MainSettings.h3
-rw-r--r--Source/Core/DolphinLib.props2
-rw-r--r--Source/Core/DolphinLib.vcxproj1
-rw-r--r--Source/Core/DolphinQt/DolphinQt.vcxproj1
-rw-r--r--Source/Core/DolphinQt/Settings/AudioPane.cpp59
-rw-r--r--Source/Core/DolphinQt/Settings/AudioPane.h10
13 files changed, 367 insertions, 428 deletions
diff --git a/Source/Core/AudioCommon/AudioStretcher.cpp b/Source/Core/AudioCommon/AudioStretcher.cpp
deleted file mode 100644
index a5e54b8163..0000000000
--- a/Source/Core/AudioCommon/AudioStretcher.cpp
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright 2017 Dolphin Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "AudioCommon/AudioStretcher.h"
-
-#include <algorithm>
-#include <cmath>
-#include <cstddef>
-
-#include "Common/Logging/Log.h"
-#include "Core/Config/MainSettings.h"
-
-namespace AudioCommon
-{
-AudioStretcher::AudioStretcher(unsigned int sample_rate) : m_sample_rate(sample_rate)
-{
- m_sound_touch.setChannels(2);
- m_sound_touch.setSampleRate(sample_rate);
- m_sound_touch.setPitch(1.0);
- m_sound_touch.setTempo(1.0);
-}
-
-void AudioStretcher::Clear()
-{
- m_sound_touch.clear();
-}
-
-void AudioStretcher::ProcessSamples(const short* in, unsigned int num_in, unsigned int num_out)
-{
- const double time_delta = static_cast<double>(num_out) / m_sample_rate; // seconds
-
- // We were given actual_samples number of samples, and num_samples were requested from us.
- double current_ratio = static_cast<double>(num_in) / static_cast<double>(num_out);
-
- const double max_latency = Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY);
- const double max_backlog = m_sample_rate * max_latency / 1000.0 / m_stretch_ratio;
- const double backlog_fullness = m_sound_touch.numSamples() / max_backlog;
- if (backlog_fullness > 5.0)
- {
- // Too many samples in backlog: Don't push anymore on
- num_in = 0;
- }
-
- // We ideally want the backlog to be about 50% full.
- // This gives some headroom both ways to prevent underflow and overflow.
- // We tweak current_ratio to encourage this.
- constexpr double tweak_time_scale = 0.5; // seconds
- current_ratio *= 1.0 + 2.0 * (backlog_fullness - 0.5) * (time_delta / tweak_time_scale);
-
- // This low-pass filter smoothes out variance in the calculated stretch ratio.
- // The time-scale determines how responsive this filter is.
- constexpr double lpf_time_scale = 1.0; // seconds
- const double lpf_gain = 1.0 - std::exp(-time_delta / lpf_time_scale);
- m_stretch_ratio += lpf_gain * (current_ratio - m_stretch_ratio);
-
- // Place a lower limit of 10% speed. When a game boots up, there will be
- // many silence samples. These do not need to be timestretched.
- m_stretch_ratio = std::max(m_stretch_ratio, 0.1);
- m_sound_touch.setTempo(m_stretch_ratio);
-
- DEBUG_LOG_FMT(AUDIO, "Audio stretching: samples:{}/{} ratio:{} backlog:{} gain: {}", num_in,
- num_out, m_stretch_ratio, backlog_fullness, lpf_gain);
-
- m_sound_touch.putSamples(in, num_in);
-}
-
-void AudioStretcher::GetStretchedSamples(short* out, unsigned int num_out)
-{
- const size_t samples_received = m_sound_touch.receiveSamples(out, num_out);
-
- if (samples_received != 0)
- {
- m_last_stretched_sample[0] = out[samples_received * 2 - 2];
- m_last_stretched_sample[1] = out[samples_received * 2 - 1];
- }
-
- // Perform padding if we've run out of samples.
- for (size_t i = samples_received; i < num_out; i++)
- {
- out[i * 2 + 0] = m_last_stretched_sample[0];
- out[i * 2 + 1] = m_last_stretched_sample[1];
- }
-}
-
-} // namespace AudioCommon
diff --git a/Source/Core/AudioCommon/AudioStretcher.h b/Source/Core/AudioCommon/AudioStretcher.h
deleted file mode 100644
index 9bc67ad893..0000000000
--- a/Source/Core/AudioCommon/AudioStretcher.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2017 Dolphin Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-
-#include <SoundTouch.h>
-
-namespace AudioCommon
-{
-class AudioStretcher
-{
-public:
- explicit AudioStretcher(unsigned int sample_rate);
- void ProcessSamples(const short* in, unsigned int num_in, unsigned int num_out);
- void GetStretchedSamples(short* out, unsigned int num_out);
- void Clear();
-
-private:
- unsigned int m_sample_rate;
- std::array<short, 2> m_last_stretched_sample = {};
- soundtouch::SoundTouch m_sound_touch;
- double m_stretch_ratio = 1.0;
-};
-
-} // namespace AudioCommon
diff --git a/Source/Core/AudioCommon/CMakeLists.txt b/Source/Core/AudioCommon/CMakeLists.txt
index d08a87247b..04ad967234 100644
--- a/Source/Core/AudioCommon/CMakeLists.txt
+++ b/Source/Core/AudioCommon/CMakeLists.txt
@@ -1,8 +1,6 @@
add_library(audiocommon
AudioCommon.cpp
AudioCommon.h
- AudioStretcher.cpp
- AudioStretcher.h
CubebStream.h
Enums.h
Mixer.cpp
@@ -90,7 +88,6 @@ PUBLIC
common
PRIVATE
- SoundTouch
FreeSurround)
if(ENABLE_CUBEB)
diff --git a/Source/Core/AudioCommon/Mixer.cpp b/Source/Core/AudioCommon/Mixer.cpp
index 18d899e3cd..e44197a429 100644
--- a/Source/Core/AudioCommon/Mixer.cpp
+++ b/Source/Core/AudioCommon/Mixer.cpp
@@ -13,8 +13,8 @@
#include "Common/Logging/Log.h"
#include "Common/Swap.h"
#include "Core/Config/MainSettings.h"
-#include "Core/ConfigManager.h"
-#include "VideoCommon/PerformanceMetrics.h"
+#include "Core/Core.h"
+#include "Core/System.h"
static u32 DPL2QualityToFrameBlockSize(AudioCommon::DPL2Quality quality)
{
@@ -31,8 +31,8 @@ static u32 DPL2QualityToFrameBlockSize(AudioCommon::DPL2Quality quality)
}
}
-Mixer::Mixer(unsigned int BackendSampleRate)
- : m_sampleRate(BackendSampleRate), m_stretcher(BackendSampleRate),
+Mixer::Mixer(u32 BackendSampleRate)
+ : m_output_sample_rate(BackendSampleRate),
m_surround_decoder(BackendSampleRate,
DPL2QualityToFrameBlockSize(Config::Get(Config::MAIN_DPL2_QUALITY)))
{
@@ -58,177 +58,80 @@ void Mixer::DoState(PointerWrap& p)
}
// Executed from sound stream thread
-unsigned int Mixer::MixerFifo::Mix(short* samples, unsigned int numSamples,
- bool consider_framelimit, float emulationspeed,
- int timing_variance)
-{
- unsigned int currentSample = 0;
-
- // Cache access in non-volatile variable
- // This is the only function changing the read value, so it's safe to
- // cache it locally although it's written here.
- // The writing pointer will be modified outside, but it will only increase,
- // so we will just ignore new written data while interpolating.
- // Without this cache, the compiler wouldn't be allowed to optimize the
- // interpolation loop.
- u32 indexR = m_indexR.load();
- u32 indexW = m_indexW.load();
-
- // render numleft sample pairs to samples[]
- // advance indexR with sample position
- // remember fractional offset
-
- float aid_sample_rate =
- FIXED_SAMPLE_RATE_DIVIDEND / static_cast<float>(m_input_sample_rate_divisor);
- if (consider_framelimit && emulationspeed > 0.0f)
- {
- float numLeft = static_cast<float>(((indexW - indexR) & INDEX_MASK) / 2);
-
- u32 low_watermark = (FIXED_SAMPLE_RATE_DIVIDEND * timing_variance) /
- (static_cast<u64>(m_input_sample_rate_divisor) * 1000);
- low_watermark = std::min(low_watermark, MAX_SAMPLES / 2);
+void Mixer::MixerFifo::Mix(s16* samples, std::size_t num_samples)
+{
+ constexpr u32 half = 0x80000000;
- m_numLeftI = (numLeft + m_numLeftI * (CONTROL_AVG - 1)) / CONTROL_AVG;
- float offset = (m_numLeftI - low_watermark) * CONTROL_FACTOR;
- if (offset > MAX_FREQ_SHIFT)
- offset = MAX_FREQ_SHIFT;
- if (offset < -MAX_FREQ_SHIFT)
- offset = -MAX_FREQ_SHIFT;
+ const u64 out_sample_rate = m_mixer->m_output_sample_rate;
+ u64 in_sample_rate = FIXED_SAMPLE_RATE_DIVIDEND / m_input_sample_rate_divisor;
- aid_sample_rate = (aid_sample_rate + offset) * emulationspeed;
- }
+ const float emulation_speed = m_mixer->m_config_emulation_speed;
+ if (0 < emulation_speed && emulation_speed != 1.0)
+ in_sample_rate = static_cast<u64>(std::llround(in_sample_rate * emulation_speed));
- const u32 ratio = (u32)(65536.0f * aid_sample_rate / (float)m_mixer->m_sampleRate);
+ const u32 index_jump = (in_sample_rate << GRANULE_BUFFER_FRAC_BITS) / (out_sample_rate);
- s32 lvolume = m_LVolume.load();
- s32 rvolume = m_RVolume.load();
+ const StereoPair volume{m_LVolume.load() / 256.0f, m_RVolume.load() / 256.0f};
- const auto read_buffer = [this](auto index) {
- return m_little_endian ? m_buffer[index] : Common::swap16(m_buffer[index]);
- };
-
- // TODO: consider a higher-quality resampling algorithm.
- for (; currentSample < numSamples * 2 && ((indexW - indexR) & INDEX_MASK) > 2; currentSample += 2)
+ while (num_samples-- > 0)
{
- u32 indexR2 = indexR + 2; // next sample
-
- s16 l1 = read_buffer(indexR & INDEX_MASK); // current
- s16 l2 = read_buffer(indexR2 & INDEX_MASK); // next
- int sampleL = ((l1 << 16) + (l2 - l1) * (u16)m_frac) >> 16;
- sampleL = (sampleL * lvolume) >> 8;
- sampleL += samples[currentSample + 1];
- samples[currentSample + 1] = std::clamp(sampleL, -32767, 32767);
-
- s16 r1 = read_buffer((indexR + 1) & INDEX_MASK); // current
- s16 r2 = read_buffer((indexR2 + 1) & INDEX_MASK); // next
- int sampleR = ((r1 << 16) + (r2 - r1) * (u16)m_frac) >> 16;
- sampleR = (sampleR * rvolume) >> 8;
- sampleR += samples[currentSample];
- samples[currentSample] = std::clamp(sampleR, -32767, 32767);
-
- m_frac += ratio;
- indexR += 2 * (u16)(m_frac >> 16);
- m_frac &= 0xffff;
- }
+ StereoPair sample = Granule::InterpStereoPair(m_front, m_back, m_current_index);
+ sample *= volume;
- // Actual number of samples written to the buffer without padding.
- unsigned int actual_sample_count = currentSample / 2;
+ sample.l += samples[0] + m_quantization_error.l;
+ samples[0] = ToShort(std::lround(sample.l));
+ m_quantization_error.l = std::clamp(sample.l - samples[0], -1.0f, 1.0f);
- // Padding
- short s[2];
- s[0] = read_buffer((indexR - 1) & INDEX_MASK);
- s[1] = read_buffer((indexR - 2) & INDEX_MASK);
- s[0] = (s[0] * rvolume) >> 8;
- s[1] = (s[1] * lvolume) >> 8;
- for (; currentSample < numSamples * 2; currentSample += 2)
- {
- int sampleR = std::clamp(s[0] + samples[currentSample + 0], -32767, 32767);
- int sampleL = std::clamp(s[1] + samples[currentSample + 1], -32767, 32767);
+ sample.r += samples[1] + m_quantization_error.r;
+ samples[1] = ToShort(std::lround(sample.r));
+ m_quantization_error.r = std::clamp(sample.r - samples[1], -1.0f, 1.0f);
- samples[currentSample + 0] = sampleR;
- samples[currentSample + 1] = sampleL;
- }
+ samples += 2;
- // Flush cached variable
- m_indexR.store(indexR);
+ m_current_index += index_jump;
+ if (m_current_index < half)
+ {
+ m_front = m_back;
+ Dequeue(&m_back);
- return actual_sample_count;
+ m_current_index += half;
+ }
+ }
}
-unsigned int Mixer::Mix(short* samples, unsigned int num_samples)
+std::size_t Mixer::Mix(s16* samples, std::size_t num_samples)
{
if (!samples)
return 0;
- memset(samples, 0, num_samples * 2 * sizeof(short));
-
- // TODO: Determine how emulation speed will be used in audio
- // const float emulation_speed = g_perf_metrics.GetSpeed();
- const float emulation_speed = m_config_emulation_speed;
- const int timing_variance = m_config_timing_variance;
- if (m_config_audio_stretch)
- {
- unsigned int available_samples =
- std::min(m_dma_mixer.AvailableSamples(), m_streaming_mixer.AvailableSamples());
-
- ASSERT_MSG(AUDIO, available_samples <= MAX_SAMPLES,
- "Audio stretching would overflow m_scratch_buffer: min({}, {}) -> {} > {} ({})",
- m_dma_mixer.AvailableSamples(), m_streaming_mixer.AvailableSamples(),
- available_samples, MAX_SAMPLES, num_samples);
-
- m_scratch_buffer.fill(0);
-
- m_dma_mixer.Mix(m_scratch_buffer.data(), available_samples, false, emulation_speed,
- timing_variance);
- m_streaming_mixer.Mix(m_scratch_buffer.data(), available_samples, false, emulation_speed,
- timing_variance);
- m_wiimote_speaker_mixer.Mix(m_scratch_buffer.data(), available_samples, false, emulation_speed,
- timing_variance);
- m_skylander_portal_mixer.Mix(m_scratch_buffer.data(), available_samples, false, emulation_speed,
- timing_variance);
- for (auto& mixer : m_gba_mixers)
- {
- mixer.Mix(m_scratch_buffer.data(), available_samples, false, emulation_speed,
- timing_variance);
- }
+ memset(samples, 0, num_samples * 2 * sizeof(s16));
- if (!m_is_stretching)
- {
- m_stretcher.Clear();
- m_is_stretching = true;
- }
- m_stretcher.ProcessSamples(m_scratch_buffer.data(), available_samples, num_samples);
- m_stretcher.GetStretchedSamples(samples, num_samples);
- }
- else
- {
- m_dma_mixer.Mix(samples, num_samples, true, emulation_speed, timing_variance);
- m_streaming_mixer.Mix(samples, num_samples, true, emulation_speed, timing_variance);
- m_wiimote_speaker_mixer.Mix(samples, num_samples, true, emulation_speed, timing_variance);
- m_skylander_portal_mixer.Mix(samples, num_samples, true, emulation_speed, timing_variance);
- for (auto& mixer : m_gba_mixers)
- mixer.Mix(samples, num_samples, true, emulation_speed, timing_variance);
- m_is_stretching = false;
- }
+ m_dma_mixer.Mix(samples, num_samples);
+ m_streaming_mixer.Mix(samples, num_samples);
+ m_wiimote_speaker_mixer.Mix(samples, num_samples);
+ m_skylander_portal_mixer.Mix(samples, num_samples);
+ for (auto& mixer : m_gba_mixers)
+ mixer.Mix(samples, num_samples);
return num_samples;
}
-unsigned int Mixer::MixSurround(float* samples, unsigned int num_samples)
+std::size_t Mixer::MixSurround(float* samples, std::size_t num_samples)
{
if (!num_samples)
return 0;
memset(samples, 0, num_samples * SURROUND_CHANNELS * sizeof(float));
- size_t needed_frames = m_surround_decoder.QueryFramesNeededForSurroundOutput(num_samples);
+ std::size_t needed_frames = m_surround_decoder.QueryFramesNeededForSurroundOutput(num_samples);
- // Mix() may also use m_scratch_buffer internally, but is safe because it alternates reads
- // and writes.
- ASSERT_MSG(AUDIO, needed_frames <= MAX_SAMPLES,
+ constexpr std::size_t max_samples = 0x8000;
+ ASSERT_MSG(AUDIO, needed_frames <= max_samples,
"needed_frames would overflow m_scratch_buffer: {} -> {} > {}", num_samples,
- needed_frames, MAX_SAMPLES);
- size_t available_frames = Mix(m_scratch_buffer.data(), static_cast<u32>(needed_frames));
+ needed_frames, max_samples);
+
+ std::array<s16, max_samples> buffer;
+ std::size_t available_frames = Mix(buffer.data(), static_cast<std::size_t>(needed_frames));
if (available_frames != needed_frames)
{
ERROR_LOG_FMT(AUDIO,
@@ -237,71 +140,58 @@ unsigned int Mixer::MixSurround(float* samples, unsigned int num_samples)
return 0;
}
- m_surround_decoder.PutFrames(m_scratch_buffer.data(), needed_frames);
+ m_surround_decoder.PutFrames(buffer.data(), needed_frames);
m_surround_decoder.ReceiveFrames(samples, num_samples);
return num_samples;
}
-void Mixer::MixerFifo::PushSamples(const short* samples, unsigned int num_samples)
+void Mixer::MixerFifo::PushSamples(const s16* samples, std::size_t num_samples)
{
- // Cache access in non-volatile variable
- // indexR isn't allowed to cache in the audio throttling loop as it
- // needs to get updates to not deadlock.
- u32 indexW = m_indexW.load();
+ while (num_samples-- > 0)
+ {
+ const s16 l = m_little_endian ? samples[1] : Common::swap16(samples[1]);
+ const s16 r = m_little_endian ? samples[0] : Common::swap16(samples[0]);
- // Check if we have enough free space
- // indexW == m_indexR results in empty buffer, so indexR must always be smaller than indexW
- if (num_samples * 2 + ((indexW - m_indexR.load()) & INDEX_MASK) >= MAX_SAMPLES * 2)
- return;
+ m_buffer[m_buffer_index] = StereoPair(l, r);
+ m_buffer_index = (m_buffer_index + 1) & GRANULE_BUFFER_MASK;
+ samples += 2;
- // AyuanX: Actual re-sampling work has been moved to sound thread
- // to alleviate the workload on main thread
- // and we simply store raw data here to make fast mem copy
- int over_bytes = num_samples * 4 - (MAX_SAMPLES * 2 - (indexW & INDEX_MASK)) * sizeof(short);
- if (over_bytes > 0)
- {
- memcpy(&m_buffer[indexW & INDEX_MASK], samples, num_samples * 4 - over_bytes);
- memcpy(&m_buffer[0], samples + (num_samples * 4 - over_bytes) / sizeof(short), over_bytes);
- }
- else
- {
- memcpy(&m_buffer[indexW & INDEX_MASK], samples, num_samples * 4);
+ if (m_buffer_index == 0 || m_buffer_index == m_buffer.size() / 2)
+ Enqueue(Granule(m_buffer, m_buffer_index));
}
-
- m_indexW.fetch_add(num_samples * 2);
}
-void Mixer::PushSamples(const short* samples, unsigned int num_samples)
+void Mixer::PushSamples(const s16* samples, std::size_t num_samples)
{
m_dma_mixer.PushSamples(samples, num_samples);
if (m_log_dsp_audio)
{
- int sample_rate_divisor = m_dma_mixer.GetInputSampleRateDivisor();
+ const s32 sample_rate_divisor = m_dma_mixer.GetInputSampleRateDivisor();
auto volume = m_dma_mixer.GetVolume();
- m_wave_writer_dsp.AddStereoSamplesBE(samples, num_samples, sample_rate_divisor, volume.first,
- volume.second);
+ m_wave_writer_dsp.AddStereoSamplesBE(samples, static_cast<u32>(num_samples),
+ sample_rate_divisor, volume.first, volume.second);
}
}
-void Mixer::PushStreamingSamples(const short* samples, unsigned int num_samples)
+void Mixer::PushStreamingSamples(const s16* samples, std::size_t num_samples)
{
m_streaming_mixer.PushSamples(samples, num_samples);
if (m_log_dtk_audio)
{
- int sample_rate_divisor = m_streaming_mixer.GetInputSampleRateDivisor();
+ const s32 sample_rate_divisor = m_streaming_mixer.GetInputSampleRateDivisor();
auto volume = m_streaming_mixer.GetVolume();
- m_wave_writer_dtk.AddStereoSamplesBE(samples, num_samples, sample_rate_divisor, volume.first,
- volume.second);
+ m_wave_writer_dtk.AddStereoSamplesBE(samples, static_cast<u32>(num_samples),
+ sample_rate_divisor, volume.first, volume.second);
}
}
-void Mixer::PushWiimoteSpeakerSamples(const short* samples, unsigned int num_samples,
- unsigned int sample_rate_divisor)
+void Mixer::PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_samples,
+ u32 sample_rate_divisor)
{
// Max 20 bytes/speaker report, may be 4-bit ADPCM so multiply by 2
- static constexpr u32 MAX_SPEAKER_SAMPLES = 20 * 2;
- std::array<short, MAX_SPEAKER_SAMPLES * 2> samples_stereo;
+ static constexpr std::size_t MAX_SPEAKER_SAMPLES = 20 * 2;
+ std::array<s16, MAX_SPEAKER_SAMPLES * 2> samples_stereo;
ASSERT_MSG(AUDIO, num_samples <= MAX_SPEAKER_SAMPLES,
"num_samples would overflow samples_stereo: {} > {}", num_samples,
@@ -310,7 +200,7 @@ void Mixer::PushWiimoteSpeakerSamples(const short* samples, unsigned int num_sam
{
m_wiimote_speaker_mixer.SetInputSampleRateDivisor(sample_rate_divisor);
- for (unsigned int i = 0; i < num_samples; ++i)
+ for (std::size_t i = 0; i < num_samples; ++i)
{
samples_stereo[i * 2] = samples[i];
samples_stereo[i * 2 + 1] = samples[i];
@@ -320,12 +210,12 @@ void Mixer::PushWiimoteSpeakerSamples(const short* samples, unsigned int num_sam
}
}
-void Mixer::PushSkylanderPortalSamples(const u8* samples, unsigned int num_samples)
+void Mixer::PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples)
{
// Skylander samples are always supplied as 64 bytes, 32 x 16 bit samples
// The portal speaker is 1 channel, so duplicate and play as stereo audio
- static constexpr u32 MAX_PORTAL_SPEAKER_SAMPLES = 32;
- std::array<short, MAX_PORTAL_SPEAKER_SAMPLES * 2> samples_stereo;
+ static constexpr std::size_t MAX_PORTAL_SPEAKER_SAMPLES = 32;
+ std::array<s16, MAX_PORTAL_SPEAKER_SAMPLES * 2> samples_stereo;
ASSERT_MSG(AUDIO, num_samples <= MAX_PORTAL_SPEAKER_SAMPLES,
"num_samples is not less or equal to 32: {} > {}", num_samples,
@@ -333,7 +223,7 @@ void Mixer::PushSkylanderPortalSamples(const u8* samples, unsigned int num_sampl
if (num_samples <= MAX_PORTAL_SPEAKER_SAMPLES)
{
- for (unsigned int i = 0; i < num_samples; ++i)
+ for (std::size_t i = 0; i < num_samples; ++i)
{
s16 sample = static_cast<u16>(samples[i * 2 + 1]) << 8 | static_cast<u16>(samples[i * 2]);
samples_stereo[i * 2] = sample;
@@ -344,37 +234,38 @@ void Mixer::PushSkylanderPortalSamples(const u8* samples, unsigned int num_sampl
}
}
-void Mixer::PushGBASamples(int device_number, const short* samples, unsigned int num_samples)
+void Mixer::PushGBASamples(std::size_t device_number, const s16* samples, std::size_t num_samples)
{
m_gba_mixers[device_number].PushSamples(samples, num_samples);
}
-void Mixer::SetDMAInputSampleRateDivisor(unsigned int rate_divisor)
+void Mixer::SetDMAInputSampleRateDivisor(u32 rate_divisor)
{
m_dma_mixer.SetInputSampleRateDivisor(rate_divisor);
}
-void Mixer::SetStreamInputSampleRateDivisor(unsigned int rate_divisor)
+void Mixer::SetStreamInputSampleRateDivisor(u32 rate_divisor)
{
m_streaming_mixer.SetInputSampleRateDivisor(rate_divisor);
}
-void Mixer::SetGBAInputSampleRateDivisors(int device_number, unsigned int rate_divisor)
+void Mixer::SetGBAInputSampleRateDivisors(std::size_t device_number, u32 rate_divisor)
{
m_gba_mixers[device_number].SetInputSampleRateDivisor(rate_divisor);
}
-void Mixer::SetStreamingVolume(unsigned int lvolume, unsigned int rvolume)
+void Mixer::SetStreamingVolume(u32 lvolume, u32 rvolume)
{
- m_streaming_mixer.SetVolume(lvolume, rvolume);
+ m_streaming_mixer.SetVolume(std::clamp<u32>(lvolume, 0x00, 0xff),
+ std::clamp<u32>(rvolume, 0x00, 0xff));
}
-void Mixer::SetWiimoteSpeakerVolume(unsigned int lvolume, unsigned int rvolume)
+void Mixer::SetWiimoteSpeakerVolume(u32 lvolume, u32 rvolume)
{
m_wiimote_speaker_mixer.SetVolume(lvolume, rvolume);
}
-void Mixer::SetGBAVolume(int device_number, unsigned int lvolume, unsigned int rvolume)
+void Mixer::SetGBAVolume(std::size_t device_number, u32 lvolume, u32 rvolume)
{
m_gba_mixers[device_number].SetVolume(lvolume, rvolume);
}
@@ -456,8 +347,7 @@ void Mixer::StopLogDSPAudio()
void Mixer::RefreshConfig()
{
m_config_emulation_speed = Config::Get(Config::MAIN_EMULATION_SPEED);
- m_config_timing_variance = Config::Get(Config::MAIN_TIMING_VARIANCE);
- m_config_audio_stretch = Config::Get(Config::MAIN_AUDIO_STRETCH);
+ m_audio_fill_gaps = Config::Get(Config::MAIN_AUDIO_FILL_GAPS);
}
void Mixer::MixerFifo::DoState(PointerWrap& p)
@@ -467,17 +357,17 @@ void Mixer::MixerFifo::DoState(PointerWrap& p)
p.Do(m_RVolume);
}
-void Mixer::MixerFifo::SetInputSampleRateDivisor(unsigned int rate_divisor)
+void Mixer::MixerFifo::SetInputSampleRateDivisor(u32 rate_divisor)
{
m_input_sample_rate_divisor = rate_divisor;
}
-unsigned int Mixer::MixerFifo::GetInputSampleRateDivisor() const
+u32 Mixer::MixerFifo::GetInputSampleRateDivisor() const
{
return m_input_sample_rate_divisor;
}
-void Mixer::MixerFifo::SetVolume(unsigned int lvolume, unsigned int rvolume)
+void Mixer::MixerFifo::SetVolume(u32 lvolume, u32 rvolume)
{
m_LVolume.store(lvolume + (lvolume >> 7));
m_RVolume.store(rvolume + (rvolume >> 7));
@@ -488,11 +378,161 @@ std::pair<s32, s32> Mixer::MixerFifo::GetVolume() const
return std::make_pair(m_LVolume.load(), m_RVolume.load());
}
-unsigned int Mixer::MixerFifo::AvailableSamples() const
+void Mixer::MixerFifo::Enqueue(const Granule& granule)
+{
+ const std::size_t head = m_queue_head.load(std::memory_order_relaxed);
+
+ std::size_t next_head = (head + 1) % GRANULE_QUEUE_SIZE;
+ if (next_head == m_queue_tail.load(std::memory_order_acquire))
+ next_head = (head + GRANULE_QUEUE_SIZE / 2) % GRANULE_QUEUE_SIZE;
+
+ m_queue[head] = granule;
+ m_queue_head.store(next_head, std::memory_order_release);
+
+ m_queue_looping.store(false, std::memory_order_relaxed);
+}
+
+void Mixer::MixerFifo::Dequeue(Granule* granule)
+{
+ // import numpy as np
+ // import scipy.signal as signal
+ // window = np.cumsum(signal.windows.dpss(32, 10))[::-1]
+ // window /= window.max()
+ // elements = ", ".join([f"{x:.10f}f" for x in window])
+ // print(f'constexpr std::array<StereoPair, {len(window)}> FADE_WINDOW = {{ {elements} }};')
+ constexpr std::array<float, 32> FADE_WINDOW = {
+ 1.0000000000f, 0.9999999932f, 0.9999998472f, 0.9999982765f, 0.9999870876f, 0.9999278274f,
+ 0.9996794215f, 0.9988227502f, 0.9963278433f, 0.9900772448f, 0.9764215513f, 0.9501402658f,
+ 0.9052392639f, 0.8367449916f, 0.7430540364f, 0.6277889467f, 0.5000000000f, 0.3722110533f,
+ 0.2569459636f, 0.1632550084f, 0.0947607361f, 0.0498597342f, 0.0235784487f, 0.0099227552f,
+ 0.0036721567f, 0.0011772498f, 0.0003205785f, 0.0000721726f, 0.0000129124f, 0.0000017235f,
+ 0.0000001528f, 0.0000000068f};
+
+ const std::size_t tail = m_queue_tail.load(std::memory_order_relaxed);
+ std::size_t next_tail = (tail + 1) % GRANULE_QUEUE_SIZE;
+
+ if (next_tail == m_queue_head.load(std::memory_order_acquire))
+ {
+ // Only fill gaps when running to prevent stutter on pause.
+ const bool is_running = Core::GetState(Core::System::GetInstance()) == Core::State::Running;
+ if (m_mixer->m_audio_fill_gaps && is_running)
+ {
+ next_tail = (tail + GRANULE_QUEUE_SIZE / 2) % GRANULE_QUEUE_SIZE;
+ m_queue_looping.store(true, std::memory_order_relaxed);
+ }
+ else
+ {
+ *granule = Granule();
+ return;
+ }
+ }
+
+ if (m_queue_looping.load(std::memory_order_relaxed))
+ m_queue_fade_index = std::min(m_queue_fade_index + 1, FADE_WINDOW.size() - 1);
+ else
+ m_queue_fade_index = 0;
+
+ *granule = m_queue[tail];
+ *granule *= StereoPair(FADE_WINDOW[m_queue_fade_index]);
+
+ m_queue_tail.store(next_tail, std::memory_order_release);
+}
+
+// Implementation of Granule's constructor
+constexpr Mixer::MixerFifo::Granule::Granule(const GranuleBuffer& input,
+ const std::size_t start_index)
+{
+ // import numpy as np
+ // import scipy.signal as signal
+ // window = np.convolve(np.ones(128), signal.windows.dpss(128 + 1, 4))
+ // window /= (window[:len(window) // 2] + window[len(window) // 2:]).max()
+ // elements = ", ".join([f"{x:.10f}f" for x in window])
+ // print(f'constexpr std::array<StereoPair, GRANULE_BUFFER_SIZE> GRANULE_WINDOW = {{ {elements}
+ // }};')
+ constexpr std::array<float, GRANULE_BUFFER_SIZE> GRANULE_WINDOW = {
+ 0.0000016272f, 0.0000050749f, 0.0000113187f, 0.0000216492f, 0.0000377350f, 0.0000616906f,
+ 0.0000961509f, 0.0001443499f, 0.0002102045f, 0.0002984010f, 0.0004144844f, 0.0005649486f,
+ 0.0007573262f, 0.0010002765f, 0.0013036694f, 0.0016786636f, 0.0021377783f, 0.0026949534f,
+ 0.0033656000f, 0.0041666352f, 0.0051165029f, 0.0062351752f, 0.0075441359f, 0.0090663409f,
+ 0.0108261579f, 0.0128492811f, 0.0151626215f, 0.0177941726f, 0.0207728499f, 0.0241283062f,
+ 0.0278907219f, 0.0320905724f, 0.0367583739f, 0.0419244083f, 0.0476184323f, 0.0538693708f,
+ 0.0607049996f, 0.0681516192f, 0.0762337261f, 0.0849736833f, 0.0943913952f, 0.1045039915f,
+ 0.1153255250f, 0.1268666867f, 0.1391345431f, 0.1521323012f, 0.1658591025f, 0.1803098534f,
+ 0.1954750915f, 0.2113408944f, 0.2278888303f, 0.2450959552f, 0.2629348550f, 0.2813737361f,
+ 0.3003765625f, 0.3199032396f, 0.3399098438f, 0.3603488941f, 0.3811696664f, 0.4023185434f,
+ 0.4237393998f, 0.4453740162f, 0.4671625177f, 0.4890438330f, 0.5109561670f, 0.5328374823f,
+ 0.5546259838f, 0.5762606002f, 0.5976814566f, 0.6188303336f, 0.6396511059f, 0.6600901562f,
+ 0.6800967604f, 0.6996234375f, 0.7186262639f, 0.7370651450f, 0.7549040448f, 0.7721111697f,
+ 0.7886591056f, 0.8045249085f, 0.8196901466f, 0.8341408975f, 0.8478676988f, 0.8608654569f,
+ 0.8731333133f, 0.8846744750f, 0.8954960085f, 0.9056086048f, 0.9150263167f, 0.9237662739f,
+ 0.9318483808f, 0.9392950004f, 0.9461306292f, 0.9523815677f, 0.9580755917f, 0.9632416261f,
+ 0.9679094276f, 0.9721092781f, 0.9758716938f, 0.9792271501f, 0.9822058274f, 0.9848373785f,
+ 0.9871507189f, 0.9891738421f, 0.9909336591f, 0.9924558641f, 0.9937648248f, 0.9948834971f,
+ 0.9958333648f, 0.9966344000f, 0.9973050466f, 0.9978622217f, 0.9983213364f, 0.9986963306f,
+ 0.9989997235f, 0.9992426738f, 0.9994350514f, 0.9995855156f, 0.9997015990f, 0.9997897955f,
+ 0.9998556501f, 0.9999038491f, 0.9999383094f, 0.9999622650f, 0.9999783508f, 0.9999886813f,
+ 0.9999949251f, 0.9999983728f, 0.9999983728f, 0.9999949251f, 0.9999886813f, 0.9999783508f,
+ 0.9999622650f, 0.9999383094f, 0.9999038491f, 0.9998556501f, 0.9997897955f, 0.9997015990f,
+ 0.9995855156f, 0.9994350514f, 0.9992426738f, 0.9989997235f, 0.9986963306f, 0.9983213364f,
+ 0.9978622217f, 0.9973050466f, 0.9966344000f, 0.9958333648f, 0.9948834971f, 0.9937648248f,
+ 0.9924558641f, 0.9909336591f, 0.9891738421f, 0.9871507189f, 0.9848373785f, 0.9822058274f,
+ 0.9792271501f, 0.9758716938f, 0.9721092781f, 0.9679094276f, 0.9632416261f, 0.9580755917f,
+ 0.9523815677f, 0.9461306292f, 0.9392950004f, 0.9318483808f, 0.9237662739f, 0.9150263167f,
+ 0.9056086048f, 0.8954960085f, 0.8846744750f, 0.8731333133f, 0.8608654569f, 0.8478676988f,
+ 0.8341408975f, 0.8196901466f, 0.8045249085f, 0.7886591056f, 0.7721111697f, 0.7549040448f,
+ 0.7370651450f, 0.7186262639f, 0.6996234375f, 0.6800967604f, 0.6600901562f, 0.6396511059f,
+ 0.6188303336f, 0.5976814566f, 0.5762606002f, 0.5546259838f, 0.5328374823f, 0.5109561670f,
+ 0.4890438330f, 0.4671625177f, 0.4453740162f, 0.4237393998f, 0.4023185434f, 0.3811696664f,
+ 0.3603488941f, 0.3399098438f, 0.3199032396f, 0.3003765625f, 0.2813737361f, 0.2629348550f,
+ 0.2450959552f, 0.2278888303f, 0.2113408944f, 0.1954750915f, 0.1803098534f, 0.1658591025f,
+ 0.1521323012f, 0.1391345431f, 0.1268666867f, 0.1153255250f, 0.1045039915f, 0.0943913952f,
+ 0.0849736833f, 0.0762337261f, 0.0681516192f, 0.0607049996f, 0.0538693708f, 0.0476184323f,
+ 0.0419244083f, 0.0367583739f, 0.0320905724f, 0.0278907219f, 0.0241283062f, 0.0207728499f,
+ 0.0177941726f, 0.0151626215f, 0.0128492811f, 0.0108261579f, 0.0090663409f, 0.0075441359f,
+ 0.0062351752f, 0.0051165029f, 0.0041666352f, 0.0033656000f, 0.0026949534f, 0.0021377783f,
+ 0.0016786636f, 0.0013036694f, 0.0010002765f, 0.0007573262f, 0.0005649486f, 0.0004144844f,
+ 0.0002984010f, 0.0002102045f, 0.0001443499f, 0.0000961509f, 0.0000616906f, 0.0000377350f,
+ 0.0000216492f, 0.0000113187f, 0.0000050749f, 0.0000016272f};
+
+ const auto input_middle = input.end() - start_index;
+ std::ranges::rotate_copy(input, input_middle, m_buffer.begin());
+
+ for (std::size_t i = 0; i < m_buffer.size(); ++i)
+ m_buffer[i] *= StereoPair(GRANULE_WINDOW[i]);
+}
+
+Mixer::MixerFifo::StereoPair Mixer::MixerFifo::Granule::InterpStereoPair(const Granule& prev,
+ const Granule& next,
+ const u32 frac)
{
- unsigned int samples_in_fifo = ((m_indexW.load() - m_indexR.load()) & INDEX_MASK) / 2;
- if (samples_in_fifo <= 1)
- return 0; // Mixer::MixerFifo::Mix always keeps one sample in the buffer.
- return (samples_in_fifo - 1) * static_cast<u64>(m_mixer->m_sampleRate) *
- m_input_sample_rate_divisor / FIXED_SAMPLE_RATE_DIVIDEND;
+ const std::size_t prev_index = frac >> Mixer::MixerFifo::GRANULE_BUFFER_FRAC_BITS;
+ const std::size_t next_index = prev_index - (GRANULE_BUFFER_SIZE / 2);
+
+ const u32 frac_t = frac & ((1 << GRANULE_BUFFER_FRAC_BITS) - 1);
+ const float t1 = frac_t / static_cast<float>(1 << GRANULE_BUFFER_FRAC_BITS);
+ const float t2 = t1 * t1;
+ const float t3 = t2 * t1;
+
+ // The Granules are pre-windowed, so we can just add them together
+ StereoPair s0 = prev.m_buffer[(prev_index - 2) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index - 2) & GRANULE_BUFFER_MASK];
+ StereoPair s1 = prev.m_buffer[(prev_index - 1) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index - 1) & GRANULE_BUFFER_MASK];
+ StereoPair s2 = prev.m_buffer[(prev_index + 0) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index + 0) & GRANULE_BUFFER_MASK];
+ StereoPair s3 = prev.m_buffer[(prev_index + 1) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index + 1) & GRANULE_BUFFER_MASK];
+ StereoPair s4 = prev.m_buffer[(prev_index + 2) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index + 2) & GRANULE_BUFFER_MASK];
+ StereoPair s5 = prev.m_buffer[(prev_index + 3) & GRANULE_BUFFER_MASK] +
+ next.m_buffer[(next_index + 3) & GRANULE_BUFFER_MASK];
+
+ s0 *= StereoPair{(+0.0f + 1.0f * t1 - 2.0f * t2 + 1.0f * t3) / 12.0f};
+ s1 *= StereoPair{(+0.0f - 8.0f * t1 + 15.0f * t2 - 7.0f * t3) / 12.0f};
+ s2 *= StereoPair{(+3.0f + 0.0f * t1 - 7.0f * t2 + 4.0f * t3) / 3.0f};
+ s3 *= StereoPair{(+0.0f + 2.0f * t1 + 5.0f * t2 - 4.0f * t3) / 3.0f};
+ s4 *= StereoPair{(+0.0f - 1.0f * t1 - 6.0f * t2 + 7.0f * t3) / 12.0f};
+ s5 *= StereoPair{(+0.0f + 0.0f * t1 + 1.0f * t2 - 1.0f * t3) / 12.0f};
+
+ return s0 + s1 + s2 + s3 + s4 + s5;
}
diff --git a/Source/Core/AudioCommon/Mixer.h b/Source/Core/AudioCommon/Mixer.h
index 5a71f5e8af..9ddec6dba4 100644
--- a/Source/Core/AudioCommon/Mixer.h
+++ b/Source/Core/AudioCommon/Mixer.h
@@ -3,10 +3,12 @@
#pragma once
+#include <algorithm>
#include <array>
#include <atomic>
+#include <bit>
+#include <cmath>
-#include "AudioCommon/AudioStretcher.h"
#include "AudioCommon/SurroundDecoder.h"
#include "AudioCommon/WaveFile.h"
#include "Common/CommonTypes.h"
@@ -17,32 +19,32 @@ class PointerWrap;
class Mixer final
{
public:
- explicit Mixer(unsigned int BackendSampleRate);
+ explicit Mixer(u32 BackendSampleRate);
~Mixer();
void DoState(PointerWrap& p);
// Called from audio threads
- unsigned int Mix(short* samples, unsigned int numSamples);
- unsigned int MixSurround(float* samples, unsigned int num_samples);
+ std::size_t Mix(s16* samples, std::size_t numSamples);
+ std::size_t MixSurround(float* samples, std::size_t num_samples);
// Called from main thread
- void PushSamples(const short* samples, unsigned int num_samples);
- void PushStreamingSamples(const short* samples, unsigned int num_samples);
- void PushWiimoteSpeakerSamples(const short* samples, unsigned int num_samples,
- unsigned int sample_rate_divisor);
- void PushSkylanderPortalSamples(const u8* samples, unsigned int num_samples);
- void PushGBASamples(int device_number, const short* samples, unsigned int num_samples);
+ void PushSamples(const s16* samples, std::size_t num_samples);
+ void PushStreamingSamples(const s16* samples, std::size_t num_samples);
+ void PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_samples,
+ u32 sample_rate_divisor);
+ void PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples);
+ void PushGBASamples(std::size_t device_number, const s16* samples, std::size_t num_samples);
- unsigned int GetSampleRate() const { return m_sampleRate; }
+ u32 GetSampleRate() const { return m_output_sample_rate; }
- void SetDMAInputSampleRateDivisor(unsigned int rate_divisor);
- void SetStreamInputSampleRateDivisor(unsigned int rate_divisor);
- void SetGBAInputSampleRateDivisors(int device_number, unsigned int rate_divisor);
+ void SetDMAInputSampleRateDivisor(u32 rate_divisor);
+ void SetStreamInputSampleRateDivisor(u32 rate_divisor);
+ void SetGBAInputSampleRateDivisors(std::size_t device_number, u32 rate_divisor);
- void SetStreamingVolume(unsigned int lvolume, unsigned int rvolume);
- void SetWiimoteSpeakerVolume(unsigned int lvolume, unsigned int rvolume);
- void SetGBAVolume(int device_number, unsigned int lvolume, unsigned int rvolume);
+ void SetStreamingVolume(u32 lvolume, u32 rvolume);
+ void SetWiimoteSpeakerVolume(u32 lvolume, u32 rvolume);
+ void SetGBAVolume(std::size_t device_number, u32 lvolume, u32 rvolume);
void StartLogDTKAudio(const std::string& filename);
void StopLogDTKAudio();
@@ -54,44 +56,105 @@ public:
static constexpr u64 FIXED_SAMPLE_RATE_DIVIDEND = 54000000 * 2;
private:
- static constexpr u32 MAX_SAMPLES = 1024 * 4; // 128 ms
- static constexpr u32 INDEX_MASK = MAX_SAMPLES * 2 - 1;
- static constexpr int MAX_FREQ_SHIFT = 200; // Per 32000 Hz
- static constexpr float CONTROL_FACTOR = 0.2f;
- static constexpr u32 CONTROL_AVG = 32; // In freq_shift per FIFO size offset
-
- const unsigned int SURROUND_CHANNELS = 6;
+ const std::size_t SURROUND_CHANNELS = 6;
class MixerFifo final
{
+ static constexpr std::size_t GRANULE_QUEUE_SIZE = 20;
+
+ template <typename T>
+ static s16 ToShort(const T x)
+ {
+ return static_cast<s16>(std::clamp<T>(x, static_cast<T>(std::numeric_limits<s16>::min()),
+ static_cast<T>(std::numeric_limits<s16>::max())));
+ }
+ struct StereoPair final
+ {
+ float l = 0.f;
+ float r = 0.f;
+
+ constexpr StereoPair() = default;
+ constexpr explicit StereoPair(float mono) : l(mono), r(mono) {}
+ constexpr StereoPair(float left, float right) : l(left), r(right) {}
+ constexpr StereoPair(s16 left, s16 right) : l(left), r(right) {}
+
+ StereoPair operator+(const StereoPair& other) const
+ {
+ return StereoPair(l + other.l, r + other.r);
+ }
+
+ StereoPair& operator*=(const StereoPair& other)
+ {
+ l *= other.l;
+ r *= other.r;
+ return *this;
+ }
+ };
+
+ static constexpr std::size_t GRANULE_BUFFER_SIZE = 256;
+ static constexpr std::size_t GRANULE_BUFFER_MASK = GRANULE_BUFFER_SIZE - 1;
+ static constexpr std::size_t GRANULE_BUFFER_BITS = std::countr_one(GRANULE_BUFFER_MASK);
+ static constexpr std::size_t GRANULE_BUFFER_FRAC_BITS = 32 - GRANULE_BUFFER_BITS;
+
+ using GranuleBuffer = std::array<StereoPair, GRANULE_BUFFER_SIZE>;
+ class Granule final
+ {
+ public:
+ constexpr Granule() = default;
+ constexpr Granule(const GranuleBuffer& input, std::size_t start_index);
+
+ static StereoPair InterpStereoPair(const Granule& front, const Granule& back, u32 frac);
+
+ Granule& operator*=(const StereoPair& x)
+ {
+ for (auto& sample : m_buffer)
+ sample *= x;
+ return *this;
+ }
+
+ private:
+ GranuleBuffer m_buffer{};
+ };
+
public:
- MixerFifo(Mixer* mixer, unsigned sample_rate_divisor, bool little_endian)
+ MixerFifo(Mixer* mixer, u32 sample_rate_divisor, bool little_endian)
: m_mixer(mixer), m_input_sample_rate_divisor(sample_rate_divisor),
m_little_endian(little_endian)
{
}
void DoState(PointerWrap& p);
- void PushSamples(const short* samples, unsigned int num_samples);
- unsigned int Mix(short* samples, unsigned int numSamples, bool consider_framelimit,
- float emulationspeed, int timing_variance);
- void SetInputSampleRateDivisor(unsigned int rate_divisor);
- unsigned int GetInputSampleRateDivisor() const;
- void SetVolume(unsigned int lvolume, unsigned int rvolume);
+ void PushSamples(const s16* samples, std::size_t num_samples);
+ void Mix(s16* samples, std::size_t num_samples);
+ void SetInputSampleRateDivisor(u32 rate_divisor);
+ u32 GetInputSampleRateDivisor() const;
+ void SetVolume(u32 lvolume, u32 rvolume);
std::pair<s32, s32> GetVolume() const;
- unsigned int AvailableSamples() const;
private:
Mixer* m_mixer;
- unsigned m_input_sample_rate_divisor;
+ u32 m_input_sample_rate_divisor;
bool m_little_endian;
- std::array<short, MAX_SAMPLES * 2> m_buffer{};
- std::atomic<u32> m_indexW{0};
- std::atomic<u32> m_indexR{0};
+
+ std::size_t m_buffer_index = 0;
+ GranuleBuffer m_buffer{};
+
+ u32 m_current_index = 0;
+ Granule m_front, m_back;
+
+ std::array<Granule, GRANULE_QUEUE_SIZE> m_queue;
+ std::atomic<std::size_t> m_queue_head{0};
+ std::atomic<std::size_t> m_queue_tail{0};
+ std::atomic<bool> m_queue_looping{false};
+ std::size_t m_queue_fade_index = 0;
+
+ void Enqueue(const Granule& granule);
+ void Dequeue(Granule* granule);
+
// Volume ranges from 0-256
std::atomic<s32> m_LVolume{256};
std::atomic<s32> m_RVolume{256};
- float m_numLeftI = 0.0f;
- u32 m_frac = 0;
+
+ StereoPair m_quantization_error;
};
void RefreshConfig();
@@ -104,12 +167,9 @@ private:
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true},
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true},
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true}};
- unsigned int m_sampleRate;
+ u32 m_output_sample_rate;
- bool m_is_stretching = false;
- AudioCommon::AudioStretcher m_stretcher;
AudioCommon::SurroundDecoder m_surround_decoder;
- std::array<short, MAX_SAMPLES * 2> m_scratch_buffer{};
WaveFileWriter m_wave_writer_dtk;
WaveFileWriter m_wave_writer_dsp;
@@ -118,8 +178,7 @@ private:
bool m_log_dsp_audio = false;
float m_config_emulation_speed;
- int m_config_timing_variance;
- bool m_config_audio_stretch;
+ bool m_audio_fill_gaps = true;
Config::ConfigChangedCallbackID m_config_changed_callback_id;
};
diff --git a/Source/Core/AudioCommon/OpenALStream.cpp b/Source/Core/AudioCommon/OpenALStream.cpp
index cdd44501ca..31ae9b047e 100644
--- a/Source/Core/AudioCommon/OpenALStream.cpp
+++ b/Source/Core/AudioCommon/OpenALStream.cpp
@@ -293,7 +293,7 @@ void OpenALStream::SoundLoop()
if (use_surround)
{
std::array<float, OAL_MAX_FRAMES * SURROUND_CHANNELS> dpl2;
- u32 rendered_frames = m_mixer->MixSurround(dpl2.data(), min_frames);
+ u32 rendered_frames = static_cast<u32>(m_mixer->MixSurround(dpl2.data(), min_frames));
if (rendered_frames < min_frames)
continue;
@@ -351,7 +351,7 @@ void OpenALStream::SoundLoop()
}
else
{
- u32 rendered_frames = m_mixer->Mix(m_realtime_buffer.data(), min_frames);
+ u32 rendered_frames = static_cast<u32>(m_mixer->Mix(m_realtime_buffer.data(), min_frames));
if (!rendered_frames)
continue;
diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp
index 4159042b2f..6ee56e461a 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -56,8 +56,7 @@ const Info<bool> MAIN_DPL2_DECODER{{System::Main, "Core", "DPL2Decoder"}, false}
const Info<AudioCommon::DPL2Quality> MAIN_DPL2_QUALITY{{System::Main, "Core", "DPL2Quality"},
AudioCommon::GetDefaultDPL2Quality()};
const Info<int> MAIN_AUDIO_LATENCY{{System::Main, "Core", "AudioLatency"}, 20};
-const Info<bool> MAIN_AUDIO_STRETCH{{System::Main, "Core", "AudioStretch"}, false};
-const Info<int> MAIN_AUDIO_STRETCH_LATENCY{{System::Main, "Core", "AudioStretchMaxLatency"}, 80};
+const Info<bool> MAIN_AUDIO_FILL_GAPS{{System::Main, "Core", "AudioFillGaps"}, true};
const Info<std::string> MAIN_MEMCARD_A_PATH{{System::Main, "Core", "MemcardAPath"}, ""};
const Info<std::string> MAIN_MEMCARD_B_PATH{{System::Main, "Core", "MemcardBPath"}, ""};
const Info<std::string>& GetInfoForMemcardPath(ExpansionInterface::Slot slot)
diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h
index 8764a5d91c..748ffac440 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -72,8 +72,7 @@ extern const Info<bool> MAIN_OVERRIDE_REGION_SETTINGS;
extern const Info<bool> MAIN_DPL2_DECODER;
extern const Info<AudioCommon::DPL2Quality> MAIN_DPL2_QUALITY;
extern const Info<int> MAIN_AUDIO_LATENCY;
-extern const Info<bool> MAIN_AUDIO_STRETCH;
-extern const Info<int> MAIN_AUDIO_STRETCH_LATENCY;
+extern const Info<bool> MAIN_AUDIO_FILL_GAPS;
extern const Info<std::string> MAIN_MEMCARD_A_PATH;
extern const Info<std::string> MAIN_MEMCARD_B_PATH;
const Info<std::string>& GetInfoForMemcardPath(ExpansionInterface::Slot slot);
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 2c9437c01c..c8b3b0ed4d 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -2,7 +2,6 @@
<Project>
<ItemGroup>
<ClInclude Include="AudioCommon\AudioCommon.h" />
- <ClInclude Include="AudioCommon\AudioStretcher.h" />
<ClInclude Include="AudioCommon\CubebStream.h" />
<ClInclude Include="AudioCommon\CubebUtils.h" />
<ClInclude Include="AudioCommon\Enums.h" />
@@ -769,7 +768,6 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="AudioCommon\AudioCommon.cpp" />
- <ClCompile Include="AudioCommon\AudioStretcher.cpp" />
<ClCompile Include="AudioCommon\CubebStream.cpp" />
<ClCompile Include="AudioCommon\CubebUtils.cpp" />
<ClCompile Include="AudioCommon\Mixer.cpp" />
diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj
index 781bb082e6..0d4925c47e 100644
--- a/Source/Core/DolphinLib.vcxproj
+++ b/Source/Core/DolphinLib.vcxproj
@@ -56,7 +56,6 @@
<Import Project="$(ExternalsDir)rcheevos\exports.props" />
<Import Project="$(ExternalsDir)SDL\exports.props" />
<Import Project="$(ExternalsDir)SFML\exports.props" />
- <Import Project="$(ExternalsDir)soundtouch\exports.props" />
<Import Project="$(ExternalsDir)spirv_cross\exports.props" />
<Import Project="$(ExternalsDir)tinygltf\exports.props" />
<Import Project="$(ExternalsDir)xxhash\exports.props" />
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index edcdc2516a..41349a4cee 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -482,7 +482,6 @@
<Import Project="$(ExternalsDir)picojson\exports.props" />
<Import Project="$(ExternalsDir)rcheevos\exports.props" />
<Import Project="$(ExternalsDir)SFML\exports.props" />
- <Import Project="$(ExternalsDir)soundtouch\exports.props" />
<Import Project="$(ExternalsDir)xxhash\exports.props" />
<Import Project="$(ExternalsDir)zstd\exports.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
diff --git a/Source/Core/DolphinQt/Settings/AudioPane.cpp b/Source/Core/DolphinQt/Settings/AudioPane.cpp
index c61245375a..b457fe382a 100644
--- a/Source/Core/DolphinQt/Settings/AudioPane.cpp
+++ b/Source/Core/DolphinQt/Settings/AudioPane.cpp
@@ -25,6 +25,7 @@
#include "Core/Core.h"
#include "Core/System.h"
+#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/SettingsWindow.h"
#include "DolphinQt/Settings.h"
@@ -137,43 +138,29 @@ void AudioPane::CreateWidgets()
backend_layout->addRow(dolby_quality_layout);
backend_layout->addRow(m_dolby_quality_latency_label);
- auto* stretching_box = new QGroupBox(tr("Audio Stretching Settings"));
- auto* stretching_layout = new QGridLayout;
- m_stretching_enable = new QCheckBox(tr("Enable Audio Stretching"));
- m_stretching_buffer_slider = new QSlider(Qt::Horizontal);
- m_stretching_buffer_indicator = new QLabel();
- m_stretching_buffer_label = new QLabel(tr("Buffer Size:"));
- stretching_box->setLayout(stretching_layout);
-
- m_stretching_buffer_slider->setMinimum(5);
- m_stretching_buffer_slider->setMaximum(300);
-
- m_stretching_enable->setToolTip(tr("Enables stretching of the audio to match emulation speed."));
- m_stretching_buffer_slider->setToolTip(tr("Size of stretch buffer in milliseconds. "
- "Values too low may cause audio crackling."));
-
- stretching_layout->addWidget(m_stretching_enable, 0, 0, 1, -1);
- stretching_layout->addWidget(m_stretching_buffer_label, 1, 0);
- stretching_layout->addWidget(m_stretching_buffer_slider, 1, 1);
- stretching_layout->addWidget(m_stretching_buffer_indicator, 1, 2);
-
dsp_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto* misc_box = new QGroupBox(tr("Miscellaneous Settings"));
auto* misc_layout = new QGridLayout;
misc_box->setLayout(misc_layout);
- m_speed_up_mute_enable = new QCheckBox(tr("Mute When Disabling Speed Limit"));
- m_speed_up_mute_enable->setToolTip(
+ m_audio_fill_gaps = new ConfigBool(tr("Fill Audio Gaps"), Config::MAIN_AUDIO_FILL_GAPS);
+ m_audio_fill_gaps->SetDescription(
+ tr("Repeat existing audio during lag spikes to prevent stuttering."
+ "<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>"));
+
+ m_speed_up_mute_enable = new ConfigBool(tr("Mute When Disabling Speed Limit"),
+ Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT);
+ m_speed_up_mute_enable->SetDescription(
tr("Mutes the audio when overriding the emulation speed limit (default hotkey: Tab)."));
- misc_layout->addWidget(m_speed_up_mute_enable, 0, 0, 1, 1);
+ misc_layout->addWidget(m_audio_fill_gaps, 0, 0, 1, 1);
+ misc_layout->addWidget(m_speed_up_mute_enable, 1, 0, 1, 1);
auto* const main_vbox_layout = new QVBoxLayout;
main_vbox_layout->addWidget(dsp_box);
main_vbox_layout->addWidget(backend_box);
- main_vbox_layout->addWidget(stretching_box);
main_vbox_layout->addWidget(misc_box);
m_main_layout = new QHBoxLayout;
@@ -192,14 +179,11 @@ void AudioPane::ConnectWidgets()
{
connect(m_latency_spin, &QSpinBox::valueChanged, this, &AudioPane::SaveSettings);
}
- connect(m_stretching_buffer_slider, &QSlider::valueChanged, this, &AudioPane::SaveSettings);
connect(m_dolby_pro_logic, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
connect(m_dolby_quality_slider, &QSlider::valueChanged, this, &AudioPane::SaveSettings);
- connect(m_stretching_enable, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_hle, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_lle, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_interpreter, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
- connect(m_speed_up_mute_enable, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
#ifdef _WIN32
connect(m_wasapi_device_combo, &QComboBox::currentIndexChanged, this, &AudioPane::SaveSettings);
@@ -255,17 +239,6 @@ void AudioPane::LoadSettings()
if (m_latency_control_supported)
m_latency_spin->setValue(Config::Get(Config::MAIN_AUDIO_LATENCY));
- // Stretch
- m_stretching_enable->setChecked(Config::Get(Config::MAIN_AUDIO_STRETCH));
- m_stretching_buffer_label->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_slider->setValue(Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY));
- m_stretching_buffer_slider->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_indicator->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_indicator->setText(tr("%1 ms").arg(m_stretching_buffer_slider->value()));
-
- // Misc
- m_speed_up_mute_enable->setChecked(Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT));
-
#ifdef _WIN32
if (Config::Get(Config::MAIN_WASAPI_DEVICE) == "default")
{
@@ -326,16 +299,8 @@ void AudioPane::SaveSettings()
if (m_latency_control_supported)
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_LATENCY, m_latency_spin->value());
- // Stretch
- Config::SetBaseOrCurrent(Config::MAIN_AUDIO_STRETCH, m_stretching_enable->isChecked());
- Config::SetBaseOrCurrent(Config::MAIN_AUDIO_STRETCH_LATENCY, m_stretching_buffer_slider->value());
- m_stretching_buffer_label->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_slider->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_indicator->setEnabled(m_stretching_enable->isChecked());
- m_stretching_buffer_indicator->setText(
- tr("%1 ms").arg(Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY)));
-
// Misc
+ Config::SetBaseOrCurrent(Config::MAIN_AUDIO_FILL_GAPS, m_audio_fill_gaps->isChecked());
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT,
m_speed_up_mute_enable->isChecked());
diff --git a/Source/Core/DolphinQt/Settings/AudioPane.h b/Source/Core/DolphinQt/Settings/AudioPane.h
index 493b067939..825865eafe 100644
--- a/Source/Core/DolphinQt/Settings/AudioPane.h
+++ b/Source/Core/DolphinQt/Settings/AudioPane.h
@@ -18,6 +18,7 @@ class QRadioButton;
class QSlider;
class QSpinBox;
class SettingsWindow;
+class ConfigBool;
class AudioPane final : public QWidget
{
@@ -71,12 +72,7 @@ private:
QComboBox* m_wasapi_device_combo;
#endif
- // Audio Stretching
- QCheckBox* m_stretching_enable;
- QLabel* m_stretching_buffer_label;
- QSlider* m_stretching_buffer_slider;
- QLabel* m_stretching_buffer_indicator;
-
// Misc Settings
- QCheckBox* m_speed_up_mute_enable;
+ ConfigBool* m_audio_fill_gaps;
+ ConfigBool* m_speed_up_mute_enable;
};