Browse Source
AudioCore: Implement time stretcher (#1737)
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 stretcherpull/15/merge
committed by
bunnei
4 changed files with 219 additions and 0 deletions
-
2src/audio_core/CMakeLists.txt
-
16src/audio_core/hle/dsp.cpp
-
144src/audio_core/time_stretch.cpp
-
57src/audio_core/time_stretch.h
@ -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
|
|||
@ -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 |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue