1 #include "engine/bufferscalers/enginebufferscalest.h"
2
3 #include "moc_enginebufferscalest.cpp"
4
5 // Fixes redefinition warnings from SoundTouch.
6 #include <soundtouch/SoundTouch.h>
7
8 #include "control/controlobject.h"
9 #include "engine/engineobject.h"
10 #include "engine/readaheadmanager.h"
11 #include "track/keyutils.h"
12 #include "util/math.h"
13 #include "util/sample.h"
14
15 using namespace soundtouch;
16
17 namespace {
18
19 // Due to filtering and oversampling, SoundTouch is some samples behind.
20 // The value below was experimental identified using a saw signal and SoundTouch 1.8
21 // at a speed of 1.0
22 // 0.918 (upscaling 44.1 kHz to 48 kHz) will produce an additional offset of 3 Frames
23 // 0.459 (upscaling 44.1 kHz to 96 kHz) will produce an additional offset of 18 Frames
24 // (Rubberband does not suffer this issue)
25 const SINT kSeekOffsetFrames = 519;
26
27 } // namespace
28
EngineBufferScaleST(ReadAheadManager * pReadAheadManager)29 EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager *pReadAheadManager)
30 : m_pReadAheadManager(pReadAheadManager),
31 m_pSoundTouch(std::make_unique<soundtouch::SoundTouch>()),
32 m_bBackwards(false) {
33 m_pSoundTouch->setChannels(getOutputSignal().getChannelCount());
34 m_pSoundTouch->setRate(m_dBaseRate);
35 m_pSoundTouch->setPitch(1.0);
36 m_pSoundTouch->setSetting(SETTING_USE_QUICKSEEK, 1);
37 // Initialize the internal buffers to prevent re-allocations
38 // in the real-time thread.
39 onSampleRateChanged();
40
41 }
42
~EngineBufferScaleST()43 EngineBufferScaleST::~EngineBufferScaleST() {
44 }
45
setScaleParameters(double base_rate,double * pTempoRatio,double * pPitchRatio)46 void EngineBufferScaleST::setScaleParameters(double base_rate,
47 double* pTempoRatio,
48 double* pPitchRatio) {
49
50 // Negative speed means we are going backwards. pitch does not affect
51 // the playback direction.
52 m_bBackwards = *pTempoRatio < 0;
53
54 // It's an error to pass a rate or tempo smaller than MIN_SEEK_SPEED to
55 // SoundTouch (see definition of MIN_SEEK_SPEED for more details).
56 double speed_abs = fabs(*pTempoRatio);
57 if (speed_abs > MAX_SEEK_SPEED) {
58 speed_abs = MAX_SEEK_SPEED;
59 } else if (speed_abs < MIN_SEEK_SPEED) {
60 speed_abs = 0;
61 }
62
63 // Let the caller know if we clamped their value.
64 *pTempoRatio = m_bBackwards ? -speed_abs : speed_abs;
65
66 // Include baserate in rate_abs so that we do samplerate conversion as part
67 // of rate adjustment.
68 if (speed_abs != m_dTempoRatio) {
69 // Note: A rate of zero would make Soundtouch crash,
70 // this is caught in scaleBuffer()
71 m_pSoundTouch->setTempo(speed_abs);
72 m_dTempoRatio = speed_abs;
73 }
74 if (base_rate != m_dBaseRate) {
75 m_pSoundTouch->setRate(base_rate);
76 m_dBaseRate = base_rate;
77 }
78
79 if (*pPitchRatio != m_dPitchRatio) {
80 // Note: pitch ratio must be positive
81 double pitch = fabs(*pPitchRatio);
82 if (pitch > 0.0) {
83 m_pSoundTouch->setPitch(pitch);
84 }
85 m_dPitchRatio = *pPitchRatio;
86 }
87
88 // NOTE(rryan) : There used to be logic here that clear()'d when the player
89 // changed direction. I removed it because this is handled by EngineBuffer.
90 }
91
onSampleRateChanged()92 void EngineBufferScaleST::onSampleRateChanged() {
93 buffer_back.clear();
94 if (!getOutputSignal().isValid()) {
95 return;
96 }
97 m_pSoundTouch->setSampleRate(getOutputSignal().getSampleRate());
98 const auto bufferSize = getOutputSignal().frames2samples(kSeekOffsetFrames);
99 if (bufferSize > buffer_back.size()) {
100 // grow buffer
101 buffer_back = mixxx::SampleBuffer(bufferSize);
102 }
103 // Setting the tempo to a very low value will force SoundTouch
104 // to preallocate buffers large enough to (almost certainly)
105 // avoid memory reallocations during playback.
106 m_pSoundTouch->setTempo(0.1);
107 m_pSoundTouch->putSamples(buffer_back.data(), kSeekOffsetFrames);
108 m_pSoundTouch->clear();
109 m_pSoundTouch->setTempo(m_dTempoRatio);
110 }
111
clear()112 void EngineBufferScaleST::clear() {
113 m_pSoundTouch->clear();
114
115 // compensate seek offset for a rate of 1.0
116 SampleUtil::clear(buffer_back.data(), buffer_back.size());
117 m_pSoundTouch->putSamples(buffer_back.data(), kSeekOffsetFrames);
118 }
119
scaleBuffer(CSAMPLE * pOutputBuffer,SINT iOutputBufferSize)120 double EngineBufferScaleST::scaleBuffer(
121 CSAMPLE* pOutputBuffer,
122 SINT iOutputBufferSize) {
123 if (m_dBaseRate == 0.0 || m_dTempoRatio == 0.0 || m_dPitchRatio == 0.0) {
124 SampleUtil::clear(pOutputBuffer, iOutputBufferSize);
125 // No actual samples/frames have been read from the
126 // unscaled input buffer!
127 return 0.0;
128 }
129
130 SINT total_received_frames = 0;
131 SINT total_read_frames = 0;
132
133 SINT remaining_frames = getOutputSignal().samples2frames(iOutputBufferSize);
134 CSAMPLE* read = pOutputBuffer;
135 bool last_read_failed = false;
136 while (remaining_frames > 0) {
137 SINT received_frames = m_pSoundTouch->receiveSamples(
138 read, remaining_frames);
139 DEBUG_ASSERT(remaining_frames >= received_frames);
140 remaining_frames -= received_frames;
141 total_received_frames += received_frames;
142 read += getOutputSignal().frames2samples(received_frames);
143
144 if (remaining_frames > 0) {
145 SINT iAvailSamples = m_pReadAheadManager->getNextSamples(
146 // The value doesn't matter here. All that matters is we
147 // are going forward or backward.
148 (m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
149 buffer_back.data(),
150 buffer_back.size());
151 SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples);
152
153 if (iAvailFrames > 0) {
154 last_read_failed = false;
155 total_read_frames += iAvailFrames;
156 m_pSoundTouch->putSamples(buffer_back.data(), iAvailFrames);
157 } else {
158 if (last_read_failed) {
159 m_pSoundTouch->flush();
160 break; // exit loop after failure
161 }
162 last_read_failed = true;
163 }
164 }
165 }
166
167 // framesRead is interpreted as the total number of virtual sample frames
168 // consumed to produce the scaled buffer. Due to this, we do not take into
169 // account directionality or starting point.
170 // NOTE(rryan): Why no m_dPitchAdjust here? SoundTouch implements pitch
171 // shifting as a tempo shift of (1/m_dPitchAdjust) and a rate shift of
172 // (*m_dPitchAdjust) so these two cancel out.
173 double framesRead = m_dBaseRate * m_dTempoRatio * total_received_frames;
174
175 return framesRead;
176 }
177