summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/audio_core/CMakeLists.txt2
-rw-r--r--src/audio_core/hle/dsp.cpp16
-rw-r--r--src/audio_core/time_stretch.cpp144
-rw-r--r--src/audio_core/time_stretch.h57
4 files changed, 219 insertions, 0 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 13b5e400e..eba0a5697 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -7,6 +7,7 @@ set(SRCS
7 hle/source.cpp 7 hle/source.cpp
8 interpolate.cpp 8 interpolate.cpp
9 sink_details.cpp 9 sink_details.cpp
10 time_stretch.cpp
10 ) 11 )
11 12
12set(HEADERS 13set(HEADERS
@@ -21,6 +22,7 @@ set(HEADERS
21 null_sink.h 22 null_sink.h
22 sink.h 23 sink.h
23 sink_details.h 24 sink_details.h
25 time_stretch.h
24 ) 26 )
25 27
26include_directories(../../externals/soundtouch/include) 28include_directories(../../externals/soundtouch/include)
diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp
index 0cdbdb06a..5113ad8ca 100644
--- a/src/audio_core/hle/dsp.cpp
+++ b/src/audio_core/hle/dsp.cpp
@@ -9,6 +9,7 @@
9#include "audio_core/hle/pipe.h" 9#include "audio_core/hle/pipe.h"
10#include "audio_core/hle/source.h" 10#include "audio_core/hle/source.h"
11#include "audio_core/sink.h" 11#include "audio_core/sink.h"
12#include "audio_core/time_stretch.h"
12 13
13namespace DSP { 14namespace DSP {
14namespace HLE { 15namespace HLE {
@@ -48,15 +49,29 @@ static std::array<Source, num_sources> sources = {
48}; 49};
49 50
50static std::unique_ptr<AudioCore::Sink> sink; 51static std::unique_ptr<AudioCore::Sink> sink;
52static AudioCore::TimeStretcher time_stretcher;
51 53
52void Init() { 54void Init() {
53 DSP::HLE::ResetPipes(); 55 DSP::HLE::ResetPipes();
56
54 for (auto& source : sources) { 57 for (auto& source : sources) {
55 source.Reset(); 58 source.Reset();
56 } 59 }
60
61 time_stretcher.Reset();
62 if (sink) {
63 time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
64 }
57} 65}
58 66
59void Shutdown() { 67void Shutdown() {
68 time_stretcher.Flush();
69 while (true) {
70 std::vector<s16> residual_audio = time_stretcher.Process(sink->SamplesInQueue());
71 if (residual_audio.empty())
72 break;
73 sink->EnqueueSamples(residual_audio);
74 }
60} 75}
61 76
62bool Tick() { 77bool Tick() {
@@ -77,6 +92,7 @@ bool Tick() {
77 92
78void SetSink(std::unique_ptr<AudioCore::Sink> sink_) { 93void SetSink(std::unique_ptr<AudioCore::Sink> sink_) {
79 sink = std::move(sink_); 94 sink = std::move(sink_);
95 time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
80} 96}
81 97
82} // namespace HLE 98} // namespace HLE
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 @@
1// Copyright 2016 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <chrono>
6#include <cmath>
7#include <vector>
8
9#include <SoundTouch.h>
10
11#include "audio_core/audio_core.h"
12#include "audio_core/time_stretch.h"
13
14#include "common/common_types.h"
15#include "common/logging/log.h"
16#include "common/math_util.h"
17
18using steady_clock = std::chrono::steady_clock;
19
20namespace AudioCore {
21
22constexpr double MIN_RATIO = 0.1;
23constexpr double MAX_RATIO = 100.0;
24
25static double ClampRatio(double ratio) {
26 return MathUtil::Clamp(ratio, MIN_RATIO, MAX_RATIO);
27}
28
29constexpr double MIN_DELAY_TIME = 0.05; // Units: seconds
30constexpr double MAX_DELAY_TIME = 0.25; // Units: seconds
31constexpr size_t DROP_FRAMES_SAMPLE_DELAY = 16000; // Units: samples
32
33constexpr double SMOOTHING_FACTOR = 0.007;
34
35struct TimeStretcher::Impl {
36 soundtouch::SoundTouch soundtouch;
37
38 steady_clock::time_point frame_timer = steady_clock::now();
39 size_t samples_queued = 0;
40
41 double smoothed_ratio = 1.0;
42
43 double sample_rate = static_cast<double>(native_sample_rate);
44};
45
46std::vector<s16> TimeStretcher::Process(size_t samples_in_queue) {
47 // This is a very simple algorithm without any fancy control theory. It works and is stable.
48
49 double ratio = CalculateCurrentRatio();
50 ratio = CorrectForUnderAndOverflow(ratio, samples_in_queue);
51 impl->smoothed_ratio = (1.0 - SMOOTHING_FACTOR) * impl->smoothed_ratio + SMOOTHING_FACTOR * ratio;
52 impl->smoothed_ratio = ClampRatio(impl->smoothed_ratio);
53
54 // SoundTouch's tempo definition the inverse of our ratio definition.
55 impl->soundtouch.setTempo(1.0 / impl->smoothed_ratio);
56
57 std::vector<s16> samples = GetSamples();
58 if (samples_in_queue >= DROP_FRAMES_SAMPLE_DELAY) {
59 samples.clear();
60 LOG_DEBUG(Audio, "Dropping frames!");
61 }
62 return samples;
63}
64
65TimeStretcher::TimeStretcher() : impl(std::make_unique<Impl>()) {
66 impl->soundtouch.setPitch(1.0);
67 impl->soundtouch.setChannels(2);
68 impl->soundtouch.setSampleRate(native_sample_rate);
69 Reset();
70}
71
72TimeStretcher::~TimeStretcher() {
73 impl->soundtouch.clear();
74}
75
76void TimeStretcher::SetOutputSampleRate(unsigned int sample_rate) {
77 impl->sample_rate = static_cast<double>(sample_rate);
78 impl->soundtouch.setRate(static_cast<double>(native_sample_rate) / impl->sample_rate);
79}
80
81void TimeStretcher::AddSamples(const s16* buffer, size_t num_samples) {
82 impl->soundtouch.putSamples(buffer, static_cast<uint>(num_samples));
83 impl->samples_queued += num_samples;
84}
85
86void TimeStretcher::Flush() {
87 impl->soundtouch.flush();
88}
89
90void TimeStretcher::Reset() {
91 impl->soundtouch.setTempo(1.0);
92 impl->soundtouch.clear();
93 impl->smoothed_ratio = 1.0;
94 impl->frame_timer = steady_clock::now();
95 impl->samples_queued = 0;
96 SetOutputSampleRate(native_sample_rate);
97}
98
99double TimeStretcher::CalculateCurrentRatio() {
100 const steady_clock::time_point now = steady_clock::now();
101 const std::chrono::duration<double> duration = now - impl->frame_timer;
102
103 const double expected_time = static_cast<double>(impl->samples_queued) / static_cast<double>(native_sample_rate);
104 const double actual_time = duration.count();
105
106 double ratio;
107 if (expected_time != 0) {
108 ratio = ClampRatio(actual_time / expected_time);
109 } else {
110 ratio = impl->smoothed_ratio;
111 }
112
113 impl->frame_timer = now;
114 impl->samples_queued = 0;
115
116 return ratio;
117}
118
119double TimeStretcher::CorrectForUnderAndOverflow(double ratio, size_t sample_delay) const {
120 const size_t min_sample_delay = static_cast<size_t>(MIN_DELAY_TIME * impl->sample_rate);
121 const size_t max_sample_delay = static_cast<size_t>(MAX_DELAY_TIME * impl->sample_rate);
122
123 if (sample_delay < min_sample_delay) {
124 // Make the ratio bigger.
125 ratio = ratio > 1.0 ? ratio * ratio : sqrt(ratio);
126 } else if (sample_delay > max_sample_delay) {
127 // Make the ratio smaller.
128 ratio = ratio > 1.0 ? sqrt(ratio) : ratio * ratio;
129 }
130
131 return ClampRatio(ratio);
132}
133
134std::vector<s16> TimeStretcher::GetSamples() {
135 uint available = impl->soundtouch.numSamples();
136
137 std::vector<s16> output(static_cast<size_t>(available) * 2);
138
139 impl->soundtouch.receiveSamples(output.data(), available);
140
141 return output;
142}
143
144} // 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 @@
1// Copyright 2016 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <cstddef>
6#include <memory>
7#include <vector>
8
9#include "common/common_types.h"
10
11namespace AudioCore {
12
13class TimeStretcher final {
14public:
15 TimeStretcher();
16 ~TimeStretcher();
17
18 /**
19 * Set sample rate for the samples that Process returns.
20 * @param sample_rate The sample rate.
21 */
22 void SetOutputSampleRate(unsigned int sample_rate);
23
24 /**
25 * Add samples to be processed.
26 * @param sample_buffer Buffer of samples in interleaved stereo PCM16 format.
27 * @param num_sample Number of samples.
28 */
29 void AddSamples(const s16* sample_buffer, size_t num_samples);
30
31 /// Flush audio remaining in internal buffers.
32 void Flush();
33
34 /// Resets internal state and clears buffers.
35 void Reset();
36
37 /**
38 * Does audio stretching and produces the time-stretched samples.
39 * Timer calculations use sample_delay to determine how much of a margin we have.
40 * @param sample_delay How many samples are buffered downstream of this module and haven't been played yet.
41 * @return Samples to play in interleaved stereo PCM16 format.
42 */
43 std::vector<s16> Process(size_t sample_delay);
44
45private:
46 struct Impl;
47 std::unique_ptr<Impl> impl;
48
49 /// INTERNAL: ratio = wallclock time / emulated time
50 double CalculateCurrentRatio();
51 /// INTERNAL: If we have too many or too few samples downstream, nudge ratio in the appropriate direction.
52 double CorrectForUnderAndOverflow(double ratio, size_t sample_delay) const;
53 /// INTERNAL: Gets the time-stretched samples from SoundTouch.
54 std::vector<s16> GetSamples();
55};
56
57} // namespace AudioCore