forked from suyu/suyu
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
This commit is contained in:
parent
d299f7ed28
commit
6f6af6928f
4 changed files with 219 additions and 0 deletions
|
@ -7,6 +7,7 @@ set(SRCS
|
||||||
hle/source.cpp
|
hle/source.cpp
|
||||||
interpolate.cpp
|
interpolate.cpp
|
||||||
sink_details.cpp
|
sink_details.cpp
|
||||||
|
time_stretch.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
|
@ -21,6 +22,7 @@ set(HEADERS
|
||||||
null_sink.h
|
null_sink.h
|
||||||
sink.h
|
sink.h
|
||||||
sink_details.h
|
sink_details.h
|
||||||
|
time_stretch.h
|
||||||
)
|
)
|
||||||
|
|
||||||
include_directories(../../externals/soundtouch/include)
|
include_directories(../../externals/soundtouch/include)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "audio_core/hle/pipe.h"
|
#include "audio_core/hle/pipe.h"
|
||||||
#include "audio_core/hle/source.h"
|
#include "audio_core/hle/source.h"
|
||||||
#include "audio_core/sink.h"
|
#include "audio_core/sink.h"
|
||||||
|
#include "audio_core/time_stretch.h"
|
||||||
|
|
||||||
namespace DSP {
|
namespace DSP {
|
||||||
namespace HLE {
|
namespace HLE {
|
||||||
|
@ -48,15 +49,29 @@ static std::array<Source, num_sources> sources = {
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unique_ptr<AudioCore::Sink> sink;
|
static std::unique_ptr<AudioCore::Sink> sink;
|
||||||
|
static AudioCore::TimeStretcher time_stretcher;
|
||||||
|
|
||||||
void Init() {
|
void Init() {
|
||||||
DSP::HLE::ResetPipes();
|
DSP::HLE::ResetPipes();
|
||||||
|
|
||||||
for (auto& source : sources) {
|
for (auto& source : sources) {
|
||||||
source.Reset();
|
source.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time_stretcher.Reset();
|
||||||
|
if (sink) {
|
||||||
|
time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shutdown() {
|
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() {
|
bool Tick() {
|
||||||
|
@ -77,6 +92,7 @@ bool Tick() {
|
||||||
|
|
||||||
void SetSink(std::unique_ptr<AudioCore::Sink> sink_) {
|
void SetSink(std::unique_ptr<AudioCore::Sink> sink_) {
|
||||||
sink = std::move(sink_);
|
sink = std::move(sink_);
|
||||||
|
time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace HLE
|
} // namespace HLE
|
||||||
|
|
144
src/audio_core/time_stretch.cpp
Normal file
144
src/audio_core/time_stretch.cpp
Normal file
|
@ -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
|
57
src/audio_core/time_stretch.h
Normal file
57
src/audio_core/time_stretch.h
Normal file
|
@ -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
|
Loading…
Reference in a new issue