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