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:
		| @@ -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 | ||||||
		Reference in New Issue
	
	Block a user