tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_ReadAheadTimeStretcher.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion::inline engine
12{
13
14//==============================================================================
15//==============================================================================
16ReadAheadTimeStretcher::ProcessThread::ProcessThread()
17{
18 thread = std::thread ([this] { process(); });
19}
20
21ReadAheadTimeStretcher::ProcessThread::~ProcessThread()
22{
23 waitingToExitFlag.test_and_set();
24 event.signal();
25 thread.join();
26}
27
28void ReadAheadTimeStretcher::ProcessThread::addInstance (ReadAheadTimeStretcher* instance)
29{
30 breakForAddRemoveInstance.test_and_set();
31
32 const std::unique_lock sl (instancesMutex);
33 instances.emplace_back (instance);
34
35 breakForAddRemoveInstance.clear();
36}
37
38void ReadAheadTimeStretcher::ProcessThread::removeInstance (ReadAheadTimeStretcher* instance)
39{
40 breakForAddRemoveInstance.test_and_set();
41
42 const std::unique_lock sl (instancesMutex);
43 std::erase_if (instances, [&] (auto& i) { return i == instance; });
44
45 breakForAddRemoveInstance.clear();
46}
47
48void ReadAheadTimeStretcher::ProcessThread::flagForProcessing (std::atomic<std::uint64_t>& epoch)
49{
50 epoch = processEpoch.load (std::memory_order_acquire);
51 event.signal();
52}
53
54//==============================================================================
55void ReadAheadTimeStretcher::ProcessThread::process()
56{
57 for (;;)
58 {
59 if (waitingToExitFlag.test (std::memory_order_acquire))
60 return;
61
62 bool anyInstancesProcessed = false;
63
64 {
65 const std::unique_lock sl (instancesMutex);
66 const auto currentEpoch = processEpoch.load (std::memory_order_acquire);
67
68 for (auto instance : instances)
69 {
70 if (waitingToExitFlag.test (std::memory_order_acquire))
71 return;
72
73 if (breakForAddRemoveInstance.test (std::memory_order_acquire))
74 {
75 anyInstancesProcessed = false; // Run again asap
76 break;
77 }
78
79 // Skip unflagged instances
80 if (instance->getEpoch() > currentEpoch)
81 continue;
82
83 if (instance->processNextBlock (false))
84 anyInstancesProcessed = true;
85 }
86 }
87
88 processEpoch.fetch_add (1, std::memory_order_release);
89
90 if (! anyInstancesProcessed)
91 event.wait (-1);
92 }
93}
94
95
96//==============================================================================
97//==============================================================================
98ReadAheadTimeStretcher::ReadAheadTimeStretcher (int numBlocksToReadAhead_)
99 : numBlocksToReadAhead (numBlocksToReadAhead_)
100{
101}
102
104{
105 processThread->removeInstance (this);
106}
107
108void ReadAheadTimeStretcher::initialise (double sourceSampleRate, int samplesPerBlock,
109 int numChannelsToUse, TimeStretcher::Mode mode, TimeStretcher::ElastiqueProOptions proOpts,
110 bool realtime)
111{
112 assert (! stretcher.isInitialised() && "Can only initialise once");
113
114 numSamplesPerOutputBlock = samplesPerBlock;
115 numChannels = numChannelsToUse;
116
117 stretcher.initialise (sourceSampleRate, samplesPerBlock,
118 numChannelsToUse, mode, proOpts,
119 realtime);
120
121 if (! isInitialised())
122 return;
123
124 processThread->addInstance (this);
125 inputFifo.setSize (numChannels, getMaxFramesNeeded());
126 outputFifo.setSize (numChannels, samplesPerBlock * numBlocksToReadAhead);
127}
128
130{
131 return stretcher.isInitialised();
132}
133
135{
136 const std::scoped_lock sl (processMutex);
137
138 inputFifo.reset();
139 outputFifo.reset();
140 newSpeedAndPitchPending.store (true, std::memory_order_release);
141
142 stretcher.reset();
143 tryToSetNewSpeedAndPitch();
144
145 hasBeenReset.store (true, std::memory_order_release);
146}
147
148bool ReadAheadTimeStretcher::setSpeedAndPitch (float speedRatio, float semitonesUp)
149{
150 const bool setSpeed = ! juce::approximatelyEqual (pendingSpeedRatio.exchange (speedRatio, std::memory_order_acq_rel), speedRatio);
151 const bool setPitch = ! juce::approximatelyEqual (pendingSemitonesUp.exchange (semitonesUp, std::memory_order_acq_rel), semitonesUp);
152
153 if (setSpeed || setPitch)
154 newSpeedAndPitchPending.store (true, std::memory_order_release);
155
156 return true;
157}
158
160{
161 // N.B. This should be thread safe as its constant in the stretcher implementations
162 return stretcher.getMaxFramesNeeded();
163}
164
166{
167 const std::scoped_lock sl (processMutex);
168 return stretcher.getFramesNeeded();
169}
170
172{
173 // The logic here is complicated but basically we want to avoid reading too many
174 // samples at once as it's expensive to read and resample them.
175 // However, after the first reset, we do want to provide enough samples to get
176 // the first block out. After that, we want to keep the stretcher stoked with
177 // samples so the background thread can process them
178 return hasBeenReset.load (std::memory_order_acquire) ? getFramesNeeded()
179 : std::min (std::max (getFramesNeeded(), numSamplesPerOutputBlock * 2),
180 getFreeSpace());
181}
182
184{
185 return inputFifo.getNumReady() < getFramesNeeded();
186}
187
189{
190 return inputFifo.getFreeSpace();
191}
192
193int ReadAheadTimeStretcher::pushData (const float* const* inChannels, int numSamples)
194{
195 assert (inputFifo.getFreeSpace() >= numSamples);
196 inputFifo.write (inChannels, numSamples);
197 processThread->flagForProcessing (epoch);
198 hasBeenReset.store (false, std::memory_order_release);
199
200 return numSamples;
201}
202
204{
205 return outputFifo.getNumReady();
206}
207
208int ReadAheadTimeStretcher::popData (float* const* outChannels, int numSamples)
209{
210 assert (numSamples <= numSamplesPerOutputBlock || numSamples <= getNumReady());
211
212 if (outputFifo.getNumReady() <= numSamples)
213 {
214 [[ maybe_unused ]] const int numPopped = processNextBlock (true);
215 assert (numPopped > 0 && "Not enough input frames pushed");
216 }
217
218 const int numToRead = std::min (numSamples, outputFifo.getNumReady());
219 juce::AudioBuffer<float> destBuffer (outChannels, numChannels, numToRead);
220 outputFifo.read (destBuffer, 0);
221 return numToRead;
222}
223
224int ReadAheadTimeStretcher::flush (float* const* outChannels)
225{
226 {
227 AudioScratchBuffer scratchBuffer (numChannels, numSamplesPerOutputBlock);
228 const int numFramesReturned = stretcher.flush ((float **) scratchBuffer.buffer.getArrayOfWritePointers());
229 assert (numFramesReturned <= scratchBuffer.buffer.getNumSamples());
230 assert (outputFifo.getFreeSpace() >= numFramesReturned);
231 outputFifo.write (scratchBuffer.buffer, 0, numFramesReturned);
232 }
233
234 // If enough samples are ready, output these now
235 if (const int numReady = outputFifo.getNumReady(); numReady > 0)
236 {
237 const int numToReturn = std::min (numReady, numSamplesPerOutputBlock);
238 juce::AudioBuffer<float> outputView (outChannels, numChannels, numToReturn);
239 [[maybe_unused]] const bool success = outputFifo.read (outputView, 0);
240 jassert (success);
241
242 return outputView.getNumSamples();
243 }
244
245 return 0;
246}
247
248void ReadAheadTimeStretcher::tryToSetNewSpeedAndPitch() const
249{
250 if (! newSpeedAndPitchPending.exchange (false, std::memory_order_acq_rel))
251 return;
252
253 stretcher.setSpeedAndPitch (pendingSpeedRatio.load (std::memory_order_acquire),
254 pendingSemitonesUp.load (std::memory_order_acquire));
255}
256
257int ReadAheadTimeStretcher::processNextBlock (bool block)
258{
259 if (outputFifo.getFreeSpace() < numSamplesPerOutputBlock)
260 return 0;
261
263
264 if (block)
265 sl = std::unique_lock (processMutex);
266 else
267 sl = std::unique_lock (processMutex, std::try_to_lock);
268
269 if (! sl.owns_lock())
270 return 0;
271
272 // If we were waiting for the lock to be released, there might now be samples ready for us so don't process again
273 if (block)
274 if (outputFifo.getNumReady() >= numSamplesPerOutputBlock)
275 return numSamplesPerOutputBlock;
276
277 if (inputFifo.getNumReady() < stretcher.getFramesNeeded())
278 return 0;
279
280 tryToSetNewSpeedAndPitch();
281 return stretcher.processData (inputFifo, stretcher.getFramesNeeded(), outputFifo);
282}
283
284}
assert
int getMaxFramesNeeded() const
Returns the maximum number of frames that will ever be returned by getFramesNeeded.
void reset()
Resets the TimeStretcher ready for a new set of audio data, maintains mode, speed and pitch ratios.
bool isInitialised() const
Returns true if this has been fully initialised.
int getFramesNeeded() const
Returns the expected number of frames required to generate some output.
void initialise(double sourceSampleRate, int samplesPerBlock, int numChannels, TimeStretcher::Mode, TimeStretcher::ElastiqueProOptions, bool realtime)
Initialises the TimeStretcher ready to perform timestretching.
int getFreeSpace() const
Returns the number of samples that can be pushed to the input, ready to be processed.
int popData(float *const *outChannels, int numSamples)
Retrieves some samples from the output buffer.
int flush(float *const *outChannels)
Flushes the end of the stream when input data is exhausted but there is still output data available.
bool setSpeedAndPitch(float speedRatio, float semitones)
Sets the timestretch speed ratio and semitones pitch shift.
int getNumReady() const
Returns the number of samples ready in the output buffer which can be retrieved without processing th...
int pushData(const float *const *inChannels, int numSamples)
Pushes a number of samples to the instance ready to be time-stretched.
bool requiresMoreFrames() const
Returns true if more frames are required to be pushed in order for a pop operation to succeed.
int getFramesRecomended() const
Returns the number of frames that should be pushed to ensure the background thread has ample to work ...
int getNumSamples() const noexcept
T exchange(T... args)
T is_pointer_v
T join(T... args)
#define jassert(expression)
T load(T... args)
T max(T... args)
T min(T... args)
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
T owns_lock(T... args)
T store(T... args)