From 6f6af6928fdff8c807e4a4d03cfd8906e0c7c7cd Mon Sep 17 00:00:00 2001 From: Maribel Date: Sun, 15 May 2016 03:04:03 +0100 Subject: AudioCore: Implement time stretcher (#1737) * AudioCore: Implement time stretcher * fixup! AudioCore: Implement time stretcher * fixup! fixup! AudioCore: Implement time stretcher * fixup! fixup! fixup! AudioCore: Implement time stretcher * fixup! fixup! fixup! fixup! AudioCore: Implement time stretcher * fixup! fixup! fixup! fixup! fixup! AudioCore: Implement time stretcher --- src/audio_core/time_stretch.cpp | 144 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/audio_core/time_stretch.cpp (limited to 'src/audio_core/time_stretch.cpp') 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 +#include +#include + +#include + +#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(native_sample_rate); +}; + +std::vector 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 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->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(sample_rate); + impl->soundtouch.setRate(static_cast(native_sample_rate) / impl->sample_rate); +} + +void TimeStretcher::AddSamples(const s16* buffer, size_t num_samples) { + impl->soundtouch.putSamples(buffer, static_cast(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 duration = now - impl->frame_timer; + + const double expected_time = static_cast(impl->samples_queued) / static_cast(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(MIN_DELAY_TIME * impl->sample_rate); + const size_t max_sample_delay = static_cast(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 TimeStretcher::GetSamples() { + uint available = impl->soundtouch.numSamples(); + + std::vector output(static_cast(available) * 2); + + impl->soundtouch.receiveSamples(output.data(), available); + + return output; +} + +} // namespace AudioCore -- cgit v1.2.3