summaryrefslogtreecommitdiffstats
path: root/src/audio_core
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio_core')
-rw-r--r--src/audio_core/CMakeLists.txt4
-rw-r--r--src/audio_core/hle/dsp.cpp75
-rw-r--r--src/audio_core/hle/dsp.h2
-rw-r--r--src/audio_core/hle/mixers.cpp201
-rw-r--r--src/audio_core/hle/mixers.h63
-rw-r--r--src/audio_core/hle/source.cpp4
-rw-r--r--src/audio_core/time_stretch.cpp144
-rw-r--r--src/audio_core/time_stretch.h57
8 files changed, 538 insertions, 12 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 13b5e400e..a72a907ef 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -3,10 +3,12 @@ set(SRCS
codec.cpp
hle/dsp.cpp
hle/filter.cpp
+ hle/mixers.cpp
hle/pipe.cpp
hle/source.cpp
interpolate.cpp
sink_details.cpp
+ time_stretch.cpp
)
set(HEADERS
@@ -15,12 +17,14 @@ set(HEADERS
hle/common.h
hle/dsp.h
hle/filter.h
+ hle/mixers.h
hle/pipe.h
hle/source.h
interpolate.h
null_sink.h
sink.h
sink_details.h
+ time_stretch.h
)
include_directories(../../externals/soundtouch/include)
diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp
index 0cdbdb06a..0640e1eff 100644
--- a/src/audio_core/hle/dsp.cpp
+++ b/src/audio_core/hle/dsp.cpp
@@ -6,13 +6,17 @@
#include <memory>
#include "audio_core/hle/dsp.h"
+#include "audio_core/hle/mixers.h"
#include "audio_core/hle/pipe.h"
#include "audio_core/hle/source.h"
#include "audio_core/sink.h"
+#include "audio_core/time_stretch.h"
namespace DSP {
namespace HLE {
+// Region management
+
std::array<SharedMemory, 2> g_regions;
static size_t CurrentRegionIndex() {
@@ -40,43 +44,96 @@ static SharedMemory& WriteRegion() {
return g_regions[1 - CurrentRegionIndex()];
}
+// Audio processing and mixing
+
static std::array<Source, num_sources> sources = {
Source(0), Source(1), Source(2), Source(3), Source(4), Source(5),
Source(6), Source(7), Source(8), Source(9), Source(10), Source(11),
Source(12), Source(13), Source(14), Source(15), Source(16), Source(17),
Source(18), Source(19), Source(20), Source(21), Source(22), Source(23)
};
+static Mixers mixers;
+
+static StereoFrame16 GenerateCurrentFrame() {
+ SharedMemory& read = ReadRegion();
+ SharedMemory& write = WriteRegion();
+
+ std::array<QuadFrame32, 3> intermediate_mixes = {};
+
+ // Generate intermediate mixes
+ for (size_t i = 0; i < num_sources; i++) {
+ write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]);
+ for (size_t mix = 0; mix < 3; mix++) {
+ sources[i].MixInto(intermediate_mixes[mix], mix);
+ }
+ }
+
+ // Generate final mix
+ write.dsp_status = mixers.Tick(read.dsp_configuration, read.intermediate_mix_samples, write.intermediate_mix_samples, intermediate_mixes);
+
+ StereoFrame16 output_frame = mixers.GetOutput();
+
+ // Write current output frame to the shared memory region
+ for (size_t samplei = 0; samplei < output_frame.size(); samplei++) {
+ for (size_t channeli = 0; channeli < output_frame[0].size(); channeli++) {
+ write.final_samples.pcm16[samplei][channeli] = s16_le(output_frame[samplei][channeli]);
+ }
+ }
+
+ return output_frame;
+}
+
+// Audio output
static std::unique_ptr<AudioCore::Sink> sink;
+static AudioCore::TimeStretcher time_stretcher;
+
+static void OutputCurrentFrame(const StereoFrame16& frame) {
+ time_stretcher.AddSamples(&frame[0][0], frame.size());
+ sink->EnqueueSamples(time_stretcher.Process(sink->SamplesInQueue()));
+}
+
+// Public Interface
void Init() {
DSP::HLE::ResetPipes();
+
for (auto& source : sources) {
source.Reset();
}
+
+ mixers.Reset();
+
+ time_stretcher.Reset();
+ if (sink) {
+ time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
+ }
}
void Shutdown() {
+ time_stretcher.Flush();
+ while (true) {
+ std::vector<s16> residual_audio = time_stretcher.Process(sink->SamplesInQueue());
+ if (residual_audio.empty())
+ break;
+ sink->EnqueueSamples(residual_audio);
+ }
}
bool Tick() {
- SharedMemory& read = ReadRegion();
- SharedMemory& write = WriteRegion();
+ StereoFrame16 current_frame = {};
- std::array<QuadFrame32, 3> intermediate_mixes = {};
+ // TODO: Check dsp::DSP semaphore (which indicates emulated application has finished writing to shared memory region)
+ current_frame = GenerateCurrentFrame();
- for (size_t i = 0; i < num_sources; i++) {
- write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]);
- for (size_t mix = 0; mix < 3; mix++) {
- sources[i].MixInto(intermediate_mixes[mix], mix);
- }
- }
+ OutputCurrentFrame(current_frame);
return true;
}
void SetSink(std::unique_ptr<AudioCore::Sink> sink_) {
sink = std::move(sink_);
+ time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
}
} // namespace HLE
diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h
index f6e53f68f..9275cd7de 100644
--- a/src/audio_core/hle/dsp.h
+++ b/src/audio_core/hle/dsp.h
@@ -428,7 +428,7 @@ ASSERT_DSP_STRUCT(DspStatus, 32);
/// Final mixed output in PCM16 stereo format, what you hear out of the speakers.
/// When the application writes to this region it has no effect.
struct FinalMixSamples {
- s16_le pcm16[2 * samples_per_frame];
+ s16_le pcm16[samples_per_frame][2];
};
ASSERT_DSP_STRUCT(FinalMixSamples, 640);
diff --git a/src/audio_core/hle/mixers.cpp b/src/audio_core/hle/mixers.cpp
new file mode 100644
index 000000000..18335f7f0
--- /dev/null
+++ b/src/audio_core/hle/mixers.cpp
@@ -0,0 +1,201 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstddef>
+
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/dsp.h"
+#include "audio_core/hle/mixers.h"
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "common/math_util.h"
+
+namespace DSP {
+namespace HLE {
+
+void Mixers::Reset() {
+ current_frame.fill({});
+ state = {};
+}
+
+DspStatus Mixers::Tick(DspConfiguration& config,
+ const IntermediateMixSamples& read_samples,
+ IntermediateMixSamples& write_samples,
+ const std::array<QuadFrame32, 3>& input)
+{
+ ParseConfig(config);
+
+ AuxReturn(read_samples);
+ AuxSend(write_samples, input);
+
+ MixCurrentFrame();
+
+ return GetCurrentStatus();
+}
+
+void Mixers::ParseConfig(DspConfiguration& config) {
+ if (!config.dirty_raw) {
+ return;
+ }
+
+ if (config.mixer1_enabled_dirty) {
+ config.mixer1_enabled_dirty.Assign(0);
+ state.mixer1_enabled = config.mixer1_enabled != 0;
+ LOG_TRACE(Audio_DSP, "mixers mixer1_enabled = %hu", config.mixer1_enabled);
+ }
+
+ if (config.mixer2_enabled_dirty) {
+ config.mixer2_enabled_dirty.Assign(0);
+ state.mixer2_enabled = config.mixer2_enabled != 0;
+ LOG_TRACE(Audio_DSP, "mixers mixer2_enabled = %hu", config.mixer2_enabled);
+ }
+
+ if (config.volume_0_dirty) {
+ config.volume_0_dirty.Assign(0);
+ state.intermediate_mixer_volume[0] = config.volume[0];
+ LOG_TRACE(Audio_DSP, "mixers volume[0] = %f", config.volume[0]);
+ }
+
+ if (config.volume_1_dirty) {
+ config.volume_1_dirty.Assign(0);
+ state.intermediate_mixer_volume[1] = config.volume[1];
+ LOG_TRACE(Audio_DSP, "mixers volume[1] = %f", config.volume[1]);
+ }
+
+ if (config.volume_2_dirty) {
+ config.volume_2_dirty.Assign(0);
+ state.intermediate_mixer_volume[2] = config.volume[2];
+ LOG_TRACE(Audio_DSP, "mixers volume[2] = %f", config.volume[2]);
+ }
+
+ if (config.output_format_dirty) {
+ config.output_format_dirty.Assign(0);
+ state.output_format = config.output_format;
+ LOG_TRACE(Audio_DSP, "mixers output_format = %zu", static_cast<size_t>(config.output_format));
+ }
+
+ if (config.headphones_connected_dirty) {
+ config.headphones_connected_dirty.Assign(0);
+ // Do nothing.
+ // (Note: Whether headphones are connected does affect coefficients used for surround sound.)
+ LOG_TRACE(Audio_DSP, "mixers headphones_connected=%hu", config.headphones_connected);
+ }
+
+ if (config.dirty_raw) {
+ LOG_DEBUG(Audio_DSP, "mixers remaining_dirty=%x", config.dirty_raw);
+ }
+
+ config.dirty_raw = 0;
+}
+
+static s16 ClampToS16(s32 value) {
+ return static_cast<s16>(MathUtil::Clamp(value, -32768, 32767));
+}
+
+static std::array<s16, 2> AddAndClampToS16(const std::array<s16, 2>& a, const std::array<s16, 2>& b) {
+ return {
+ ClampToS16(static_cast<s32>(a[0]) + static_cast<s32>(b[0])),
+ ClampToS16(static_cast<s32>(a[1]) + static_cast<s32>(b[1]))
+ };
+}
+
+void Mixers::DownmixAndMixIntoCurrentFrame(float gain, const QuadFrame32& samples) {
+ // TODO(merry): Limiter. (Currently we're performing final mixing assuming a disabled limiter.)
+
+ switch (state.output_format) {
+ case OutputFormat::Mono:
+ std::transform(current_frame.begin(), current_frame.end(), samples.begin(), current_frame.begin(),
+ [gain](const std::array<s16, 2>& accumulator, const std::array<s32, 4>& sample) -> std::array<s16, 2> {
+ // Downmix to mono
+ s16 mono = ClampToS16(static_cast<s32>((gain * sample[0] + gain * sample[1] + gain * sample[2] + gain * sample[3]) / 2));
+ // Mix into current frame
+ return AddAndClampToS16(accumulator, { mono, mono });
+ });
+ return;
+
+ case OutputFormat::Surround:
+ // TODO(merry): Implement surround sound.
+ // fallthrough
+
+ case OutputFormat::Stereo:
+ std::transform(current_frame.begin(), current_frame.end(), samples.begin(), current_frame.begin(),
+ [gain](const std::array<s16, 2>& accumulator, const std::array<s32, 4>& sample) -> std::array<s16, 2> {
+ // Downmix to stereo
+ s16 left = ClampToS16(static_cast<s32>(gain * sample[0] + gain * sample[2]));
+ s16 right = ClampToS16(static_cast<s32>(gain * sample[1] + gain * sample[3]));
+ // Mix into current frame
+ return AddAndClampToS16(accumulator, { left, right });
+ });
+ return;
+ }
+
+ UNREACHABLE_MSG("Invalid output_format %zu", static_cast<size_t>(state.output_format));
+}
+
+void Mixers::AuxReturn(const IntermediateMixSamples& read_samples) {
+ // NOTE: read_samples.mix{1,2}.pcm32 annoyingly have their dimensions in reverse order to QuadFrame32.
+
+ if (state.mixer1_enabled) {
+ for (size_t sample = 0; sample < samples_per_frame; sample++) {
+ for (size_t channel = 0; channel < 4; channel++) {
+ state.intermediate_mix_buffer[1][sample][channel] = read_samples.mix1.pcm32[channel][sample];
+ }
+ }
+ }
+
+ if (state.mixer2_enabled) {
+ for (size_t sample = 0; sample < samples_per_frame; sample++) {
+ for (size_t channel = 0; channel < 4; channel++) {
+ state.intermediate_mix_buffer[2][sample][channel] = read_samples.mix2.pcm32[channel][sample];
+ }
+ }
+ }
+}
+
+void Mixers::AuxSend(IntermediateMixSamples& write_samples, const std::array<QuadFrame32, 3>& input) {
+ // NOTE: read_samples.mix{1,2}.pcm32 annoyingly have their dimensions in reverse order to QuadFrame32.
+
+ state.intermediate_mix_buffer[0] = input[0];
+
+ if (state.mixer1_enabled) {
+ for (size_t sample = 0; sample < samples_per_frame; sample++) {
+ for (size_t channel = 0; channel < 4; channel++) {
+ write_samples.mix1.pcm32[channel][sample] = input[1][sample][channel];
+ }
+ }
+ } else {
+ state.intermediate_mix_buffer[1] = input[1];
+ }
+
+ if (state.mixer2_enabled) {
+ for (size_t sample = 0; sample < samples_per_frame; sample++) {
+ for (size_t channel = 0; channel < 4; channel++) {
+ write_samples.mix2.pcm32[channel][sample] = input[2][sample][channel];
+ }
+ }
+ } else {
+ state.intermediate_mix_buffer[2] = input[2];
+ }
+}
+
+void Mixers::MixCurrentFrame() {
+ current_frame.fill({});
+
+ for (size_t mix = 0; mix < 3; mix++) {
+ DownmixAndMixIntoCurrentFrame(state.intermediate_mixer_volume[mix], state.intermediate_mix_buffer[mix]);
+ }
+
+ // TODO(merry): Compressor. (We currently assume a disabled compressor.)
+}
+
+DspStatus Mixers::GetCurrentStatus() const {
+ DspStatus status;
+ status.unknown = 0;
+ status.dropped_frames = 0;
+ return status;
+}
+
+} // namespace HLE
+} // namespace DSP
diff --git a/src/audio_core/hle/mixers.h b/src/audio_core/hle/mixers.h
new file mode 100644
index 000000000..b52952eb5
--- /dev/null
+++ b/src/audio_core/hle/mixers.h
@@ -0,0 +1,63 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/dsp.h"
+
+namespace DSP {
+namespace HLE {
+
+class Mixers final {
+public:
+ Mixers() {
+ Reset();
+ }
+
+ void Reset();
+
+ DspStatus Tick(DspConfiguration& config,
+ const IntermediateMixSamples& read_samples,
+ IntermediateMixSamples& write_samples,
+ const std::array<QuadFrame32, 3>& input);
+
+ StereoFrame16 GetOutput() const {
+ return current_frame;
+ }
+
+private:
+ StereoFrame16 current_frame = {};
+
+ using OutputFormat = DspConfiguration::OutputFormat;
+
+ struct {
+ std::array<float, 3> intermediate_mixer_volume = {};
+
+ bool mixer1_enabled = false;
+ bool mixer2_enabled = false;
+ std::array<QuadFrame32, 3> intermediate_mix_buffer = {};
+
+ OutputFormat output_format = OutputFormat::Stereo;
+
+ } state;
+
+ /// INTERNAL: Update our internal state based on the current config.
+ void ParseConfig(DspConfiguration& config);
+ /// INTERNAL: Read samples from shared memory that have been modified by the ARM11.
+ void AuxReturn(const IntermediateMixSamples& read_samples);
+ /// INTERNAL: Write samples to shared memory for the ARM11 to modify.
+ void AuxSend(IntermediateMixSamples& write_samples, const std::array<QuadFrame32, 3>& input);
+ /// INTERNAL: Mix current_frame.
+ void MixCurrentFrame();
+ /// INTERNAL: Downmix from quadraphonic to stereo based on status.output_format and accumulate into current_frame.
+ void DownmixAndMixIntoCurrentFrame(float gain, const QuadFrame32& samples);
+ /// INTERNAL: Generate DspStatus based on internal state.
+ DspStatus GetCurrentStatus() const;
+};
+
+} // namespace HLE
+} // namespace DSP
diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp
index daaf6e3f3..30552fe26 100644
--- a/src/audio_core/hle/source.cpp
+++ b/src/audio_core/hle/source.cpp
@@ -126,13 +126,13 @@ void Source::ParseConfig(SourceConfiguration::Configuration& config, const s16_l
if (config.simple_filter_dirty) {
config.simple_filter_dirty.Assign(0);
state.filters.Configure(config.simple_filter);
- LOG_TRACE(Audio_DSP, "source_id=%zu simple filter update");
+ LOG_TRACE(Audio_DSP, "source_id=%zu simple filter update", source_id);
}
if (config.biquad_filter_dirty) {
config.biquad_filter_dirty.Assign(0);
state.filters.Configure(config.biquad_filter);
- LOG_TRACE(Audio_DSP, "source_id=%zu biquad filter update");
+ LOG_TRACE(Audio_DSP, "source_id=%zu biquad filter update", source_id);
}
if (config.interpolation_dirty) {
diff --git a/src/audio_core/time_stretch.cpp b/src/audio_core/time_stretch.cpp
new file mode 100644
index 000000000..ea38f40d0
--- /dev/null
+++ b/src/audio_core/time_stretch.cpp
@@ -0,0 +1,144 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <chrono>
+#include <cmath>
+#include <vector>
+
+#include <SoundTouch.h>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/time_stretch.h"
+
+#include "common/common_types.h"
+#include "common/logging/log.h"
+#include "common/math_util.h"
+
+using steady_clock = std::chrono::steady_clock;
+
+namespace AudioCore {
+
+constexpr double MIN_RATIO = 0.1;
+constexpr double MAX_RATIO = 100.0;
+
+static double ClampRatio(double ratio) {
+ return MathUtil::Clamp(ratio, MIN_RATIO, MAX_RATIO);
+}
+
+constexpr double MIN_DELAY_TIME = 0.05; // Units: seconds
+constexpr double MAX_DELAY_TIME = 0.25; // Units: seconds
+constexpr size_t DROP_FRAMES_SAMPLE_DELAY = 16000; // Units: samples
+
+constexpr double SMOOTHING_FACTOR = 0.007;
+
+struct TimeStretcher::Impl {
+ soundtouch::SoundTouch soundtouch;
+
+ steady_clock::time_point frame_timer = steady_clock::now();
+ size_t samples_queued = 0;
+
+ double smoothed_ratio = 1.0;
+
+ double sample_rate = static_cast<double>(native_sample_rate);
+};
+
+std::vector<s16> TimeStretcher::Process(size_t samples_in_queue) {
+ // This is a very simple algorithm without any fancy control theory. It works and is stable.
+
+ double ratio = CalculateCurrentRatio();
+ ratio = CorrectForUnderAndOverflow(ratio, samples_in_queue);
+ impl->smoothed_ratio = (1.0 - SMOOTHING_FACTOR) * impl->smoothed_ratio + SMOOTHING_FACTOR * ratio;
+ impl->smoothed_ratio = ClampRatio(impl->smoothed_ratio);
+
+ // SoundTouch's tempo definition the inverse of our ratio definition.
+ impl->soundtouch.setTempo(1.0 / impl->smoothed_ratio);
+
+ std::vector<s16> samples = GetSamples();
+ if (samples_in_queue >= DROP_FRAMES_SAMPLE_DELAY) {
+ samples.clear();
+ LOG_DEBUG(Audio, "Dropping frames!");
+ }
+ return samples;
+}
+
+TimeStretcher::TimeStretcher() : impl(std::make_unique<Impl>()) {
+ impl->soundtouch.setPitch(1.0);
+ impl->soundtouch.setChannels(2);
+ impl->soundtouch.setSampleRate(native_sample_rate);
+ Reset();
+}
+
+TimeStretcher::~TimeStretcher() {
+ impl->soundtouch.clear();
+}
+
+void TimeStretcher::SetOutputSampleRate(unsigned int sample_rate) {
+ impl->sample_rate = static_cast<double>(sample_rate);
+ impl->soundtouch.setRate(static_cast<double>(native_sample_rate) / impl->sample_rate);
+}
+
+void TimeStretcher::AddSamples(const s16* buffer, size_t num_samples) {
+ impl->soundtouch.putSamples(buffer, static_cast<uint>(num_samples));
+ impl->samples_queued += num_samples;
+}
+
+void TimeStretcher::Flush() {
+ impl->soundtouch.flush();
+}
+
+void TimeStretcher::Reset() {
+ impl->soundtouch.setTempo(1.0);
+ impl->soundtouch.clear();
+ impl->smoothed_ratio = 1.0;
+ impl->frame_timer = steady_clock::now();
+ impl->samples_queued = 0;
+ SetOutputSampleRate(native_sample_rate);
+}
+
+double TimeStretcher::CalculateCurrentRatio() {
+ const steady_clock::time_point now = steady_clock::now();
+ const std::chrono::duration<double> duration = now - impl->frame_timer;
+
+ const double expected_time = static_cast<double>(impl->samples_queued) / static_cast<double>(native_sample_rate);
+ const double actual_time = duration.count();
+
+ double ratio;
+ if (expected_time != 0) {
+ ratio = ClampRatio(actual_time / expected_time);
+ } else {
+ ratio = impl->smoothed_ratio;
+ }
+
+ impl->frame_timer = now;
+ impl->samples_queued = 0;
+
+ return ratio;
+}
+
+double TimeStretcher::CorrectForUnderAndOverflow(double ratio, size_t sample_delay) const {
+ const size_t min_sample_delay = static_cast<size_t>(MIN_DELAY_TIME * impl->sample_rate);
+ const size_t max_sample_delay = static_cast<size_t>(MAX_DELAY_TIME * impl->sample_rate);
+
+ if (sample_delay < min_sample_delay) {
+ // Make the ratio bigger.
+ ratio = ratio > 1.0 ? ratio * ratio : sqrt(ratio);
+ } else if (sample_delay > max_sample_delay) {
+ // Make the ratio smaller.
+ ratio = ratio > 1.0 ? sqrt(ratio) : ratio * ratio;
+ }
+
+ return ClampRatio(ratio);
+}
+
+std::vector<s16> TimeStretcher::GetSamples() {
+ uint available = impl->soundtouch.numSamples();
+
+ std::vector<s16> output(static_cast<size_t>(available) * 2);
+
+ impl->soundtouch.receiveSamples(output.data(), available);
+
+ return output;
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/time_stretch.h b/src/audio_core/time_stretch.h
new file mode 100644
index 000000000..1fde3f72a
--- /dev/null
+++ b/src/audio_core/time_stretch.h
@@ -0,0 +1,57 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+#include "common/common_types.h"
+
+namespace AudioCore {
+
+class TimeStretcher final {
+public:
+ TimeStretcher();
+ ~TimeStretcher();
+
+ /**
+ * Set sample rate for the samples that Process returns.
+ * @param sample_rate The sample rate.
+ */
+ void SetOutputSampleRate(unsigned int sample_rate);
+
+ /**
+ * Add samples to be processed.
+ * @param sample_buffer Buffer of samples in interleaved stereo PCM16 format.
+ * @param num_sample Number of samples.
+ */
+ void AddSamples(const s16* sample_buffer, size_t num_samples);
+
+ /// Flush audio remaining in internal buffers.
+ void Flush();
+
+ /// Resets internal state and clears buffers.
+ void Reset();
+
+ /**
+ * Does audio stretching and produces the time-stretched samples.
+ * Timer calculations use sample_delay to determine how much of a margin we have.
+ * @param sample_delay How many samples are buffered downstream of this module and haven't been played yet.
+ * @return Samples to play in interleaved stereo PCM16 format.
+ */
+ std::vector<s16> Process(size_t sample_delay);
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> impl;
+
+ /// INTERNAL: ratio = wallclock time / emulated time
+ double CalculateCurrentRatio();
+ /// INTERNAL: If we have too many or too few samples downstream, nudge ratio in the appropriate direction.
+ double CorrectForUnderAndOverflow(double ratio, size_t sample_delay) const;
+ /// INTERNAL: Gets the time-stretched samples from SoundTouch.
+ std::vector<s16> GetSamples();
+};
+
+} // namespace AudioCore