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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_BufferedFileReader.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 namespace engine
12{
13
15 juce::TimeSliceThread& timeSliceThread,
16 int samplesToBuffer)
17 : juce::AudioFormatReader (nullptr, sourceReader->getFormatName()),
18 source (sourceReader), thread (timeSliceThread),
19 isFullyBuffering (samplesToBuffer < 0)
20{
23
24 sampleRate = source->sampleRate;
25 lengthInSamples = source->lengthInSamples;
26 numChannels = source->numChannels;
27 metadataValues = source->metadataValues;
28 bitsPerSample = 32;
30
31 const size_t totalNumSlotsRequired = 1 + (size_t (lengthInSamples) / samplesPerBlock);
32 assert (totalNumSlotsRequired <= std::numeric_limits<int>::max());
33 numBlocksToBuffer = samplesToBuffer > -1 ? static_cast<size_t> (1 + (samplesToBuffer / samplesPerBlock))
34 : totalNumSlotsRequired;
35
36 slots = std::vector<std::atomic<BufferedBlock*>> (totalNumSlotsRequired);
37 std::fill (slots.begin(), slots.end(), nullptr);
38
39 slotsInUse = std::vector<std::atomic<bool>> (totalNumSlotsRequired);
40 std::fill (slotsInUse.begin(), slotsInUse.end(), false);
41
42 for (size_t i = 0; i < numBlocksToBuffer; ++i)
43 {
44 // The following code makes the assumption that the pointers are at least 8-bit aligned
45 static_assert (alignof (BufferedBlock*) >= 8);
47
48 // Check the least significant bit is actually 0
49 assert (! std::bitset<sizeof (BufferedBlock*)> (size_t (blocks.back().get()))[0]);
50 }
51
52 timeSliceThread.addTimeSliceClient (this);
53}
54
59
60void BufferedFileReader::setReadTimeout (int timeoutMilliseconds) noexcept
61{
62 timeoutMs = timeoutMilliseconds;
63}
64
66{
67 return isFullyBuffering
68 && numBlocksBuffered == numBlocksToBuffer;
69}
70
71bool BufferedFileReader::readSamples (int* const* destSamples, int numDestChannels, int startOffsetInDestBuffer,
72 juce::int64 startSampleInFile, int numSamples)
73{
74 // If a cached block can't be found, update the read position and possibly wait for it
75 nextReadPosition = startSampleInFile;
76
77 auto startTime = juce::Time::getMillisecondCounter();
78 clearSamplesBeyondAvailableLength (destSamples, numDestChannels, startOffsetInDestBuffer,
79 startSampleInFile, numSamples, lengthInSamples);
80
81 bool allSamplesRead = true;
82 bool hasNotified = false;
83
84 while (numSamples > 0)
85 {
86 {
87 auto ssa = ScopedSlotAccess::fromPosition (*this, startSampleInFile);
88
89 if (auto block = ssa.getBlock())
90 {
91 jassert (block->range.contains (startSampleInFile));
92
93 // This isn't exact but will be ok for finding the oldest block
94 block->lastUseTime = startTime;
95
96 auto offset = (int) (startSampleInFile - block->range.getStart());
97 auto numToDo = std::min (numSamples, (int) (block->range.getEnd() - startSampleInFile));
98
99 for (int j = 0; j < numDestChannels; ++j)
100 {
101 if (auto dest = (float*) destSamples[j])
102 {
103 dest += startOffsetInDestBuffer;
104
105 if (j < (int) numChannels)
106 juce::FloatVectorOperations::copy (dest, block->buffer.getReadPointer (j, offset), numToDo);
107 else
108 juce::FloatVectorOperations::clear (dest, numToDo);
109 }
110 }
111
112 startOffsetInDestBuffer += numToDo;
113 startSampleInFile += numToDo;
114 numSamples -= numToDo;
115
116 allSamplesRead = allSamplesRead && block->allSamplesRead;
117
118 // Use a continue here rather an an else to avoid keeping the ScopedSlotAccess in scope
119 continue;
120 }
121 }
122
123 if (! std::exchange (hasNotified, true))
124 thread.moveToFrontOfQueue (this);
125
126 // If the timeout has expired, clear the dest buffer and return
127 if (timeoutMs >= 0 && juce::Time::getMillisecondCounter() >= startTime + (juce::uint32) timeoutMs)
128 {
129 for (int j = 0; j < numDestChannels; ++j)
130 if (auto dest = (float*) destSamples[j])
131 juce::FloatVectorOperations::clear (dest + startOffsetInDestBuffer, numSamples);
132
133 allSamplesRead = false;
134 break;
135 }
136 else
137 {
138 // Otherwise wait and try again
140 }
141 }
142
143 return allSamplesRead;
144}
145
146BufferedFileReader::BufferedBlock::BufferedBlock (juce::AudioFormatReader& reader)
147 : buffer ((int) reader.numChannels, samplesPerBlock)
148{
149}
150
151void BufferedFileReader::BufferedBlock::update (juce::AudioFormatReader& reader, juce::Range<juce::int64> newSampleRange, size_t currentSlotIndex)
152{
153 assert (newSampleRange.getEnd() <= reader.lengthInSamples);
154 const int numSamples = (int) newSampleRange.getLength();
155 range = newSampleRange;
156 buffer.setSize ((int) reader.numChannels,
157 numSamples,
158 false, false, true);
159 allSamplesRead = reader.read (&buffer, 0, numSamples, (int) range.getStart(), true, true);
160 assert (slotIndex == static_cast<int> (currentSlotIndex));
161 slotIndex = static_cast<int> (currentSlotIndex);
162 lastUseTime = juce::Time::getMillisecondCounter();
163 DBG(slotIndex);
164}
165
166BufferedFileReader::ScopedSlotAccess::ScopedSlotAccess (BufferedFileReader& reader_, size_t slotIndex_)
167 : reader (reader_), slotIndex (slotIndex_)
168{
169 assert (slotIndex < reader.slots.size());
170
171 reader.markSlotUseState (slotIndex, true);
172 block = reader.slots[slotIndex];
173}
174
175BufferedFileReader::ScopedSlotAccess::~ScopedSlotAccess()
176{
177 block = nullptr;
178 reader.markSlotUseState (slotIndex, false);
179}
180
181BufferedFileReader::ScopedSlotAccess BufferedFileReader::ScopedSlotAccess::fromPosition (BufferedFileReader& reader, juce::int64 position)
182{
183 return { reader, static_cast<size_t> (position / (float) samplesPerBlock) };
184}
185
186void BufferedFileReader::ScopedSlotAccess::setBlock (BufferedBlock* blockToReferTo)
187{
188 if (block)
189 block->slotIndex = -1;
190
191 block = blockToReferTo;
192
193 if (block)
194 block->slotIndex = static_cast<int> (slotIndex);
195
196 reader.slots[slotIndex] = block;
197}
198
199int BufferedFileReader::useTimeSlice()
200{
201 for (;;)
202 {
203 switch (readNextBufferChunk())
204 {
205 case PositionStatus::positionChangedByAudioThread: break;
206 case PositionStatus::nextChunkScheduled: return 1;
207 case PositionStatus::blocksFull: return 5;
208 case PositionStatus::fullyLoaded: return 100;
209 }
210 }
211}
212
213BufferedFileReader::PositionStatus BufferedFileReader::readNextBufferChunk()
214{
215 if (isFullyBuffered())
216 return PositionStatus::fullyLoaded;
217
218 // First find the slot the audio thread is trying to read
219 // If that needs reading, set the next-slot-to-read to this
220 // If it's already read, use the current next-slot-to-read value
221 const auto currentReadPosition = nextReadPosition.load();
222 const auto currentSlotIndex = getSlotIndexFromSamplePosition (currentReadPosition);
223
224 // Check if the slot being read by the audio thread is buffered
225 {
226 // Take exclusive control of the current slot
227 ScopedSlotAccess currentSlot (*this, currentSlotIndex);
228
229 if (auto currentBlockInCurrentSlot = currentSlot.getBlock())
230 {
231 // If the slot has a valid block, it should have the correct range
232 assert (currentBlockInCurrentSlot->range == getSlotRange (currentSlotIndex));
233
234 // If the block contains invalid data, we need to re-read this
235 if (! currentBlockInCurrentSlot->allSamplesRead)
236 nextSlotScheduled = currentSlotIndex;
237 }
238 }
239
240 const auto slotToReadIndex = nextSlotScheduled.load();
241 bool readScheduledSlot = true;
242
243 {
244 // Take exclusive control of the scheduled slot
245 ScopedSlotAccess scheduledSlot (*this, slotToReadIndex);
246
247 // If that block is inthe correct slot with all the samples read, don't re-read
248 if (auto currentBlockInScheduledSlot = scheduledSlot.getBlock())
249 if (currentBlockInScheduledSlot->allSamplesRead)
250 readScheduledSlot = false;
251 }
252
253 // Read the next scheduled slot if its out of date
254 if (readScheduledSlot)
255 {
256 // This can be done without taking any exclusive access as the
257 // blocks won't move around and their time stamps are atomic
259 BufferedBlock* blockToUse = nullptr;
260
261 for (size_t i = 0; i < blocks.size(); ++i)
262 {
263 auto& block = blocks[i];
264 const auto useTime = block->lastUseTime.load (std::memory_order_relaxed);
265
266 if (useTime > oldestTime)
267 continue;
268
269 blockToUse = block.get();
270 oldestTime = useTime;
271
272 const int curentBlockSlotIndex = block->slotIndex.load (std::memory_order_relaxed);
273
274 // If the block is free, we can simply use it
275 if (curentBlockSlotIndex < 0)
276 break;
277 }
278
279 assert (blockToUse != nullptr);
280 const int blockToUseSlotIndex = blockToUse->slotIndex.load (std::memory_order_relaxed);
281 ScopedSlotAccess desiredSlot (*this, slotToReadIndex);
282
283 if (blockToUseSlotIndex < 0)
284 {
285 // If the block isn't in use, just set its slot
286 desiredSlot.setBlock (blockToUse);
287 }
288 else
289 {
290 // Take exclusive control of the slot the oldest block is in
291 ScopedSlotAccess slotWithOldestBlock (*this, static_cast<size_t> (blockToUseSlotIndex));
292 assert (blockToUse == slotWithOldestBlock.getBlock());
293
294 // Swap the blocks
295 slotWithOldestBlock.setBlock (nullptr);
296 desiredSlot.setBlock (blockToUse);
297 }
298
299 // Update the block's data
300 const juce::Range<juce::int64> slotSampleRange = getSlotRange (slotToReadIndex);
301 blockToUse->update (*source, slotSampleRange, slotToReadIndex);
302
303 // Increment the number of blocks in use if this was a fresh block
304 if (blockToUseSlotIndex < 0 && blockToUse->allSamplesRead)
305 ++numBlocksBuffered;
306 }
307
308 // If all the slots are in use, free the oldest slot from the current read position so a future one can be queued up
309//ddd if (numBlocksBuffered == blocks.size() && slotToReadIndex > 0)
310// {
311// for (auto& block : blocks)
312// {
313// // This won't change during this function
314// const auto blockSlotIndex = block->slotIndex.load();
315//
316// if (blockSlotIndex < 0)
317// continue;
318//
319// if (blockSlotIndex < int (slotToReadIndex) - 1)
320// {
321// ScopedSlotAccess slotAccess (*this, size_t (blockSlotIndex));
322// slotAccess.setBlock (nullptr);
323// --numBlocksBuffered;
324// }
325// }
326//
327// return PositionStatus::nextChunkScheduled;
328// }
329
330 // If the read position hasn't changed by the audio thread let the thread reschedule us
331 if (nextReadPosition == currentReadPosition)
332 {
333 assert (blocks.size() > 0);
334 const auto startSlot = std::max (size_t (0), getSlotIndexFromSamplePosition (currentReadPosition));
335 nextSlotScheduled = (startSlot + 1) % slots.size();
336
337 return PositionStatus::nextChunkScheduled;
338 }
339
340 // Otherwise, the audio thread has changed the position so read the block asap
341 return PositionStatus::positionChangedByAudioThread;
342}
343
344size_t BufferedFileReader::getSlotIndexFromSamplePosition (juce::int64 samplePos)
345{
346 return static_cast<size_t> (samplePos / (float) samplesPerBlock);
347}
348
349juce::Range<juce::int64> BufferedFileReader::getSlotRange (size_t slotIndex)
350{
351 const juce::int64 slotStartSamplePos = static_cast<juce::int64> (slotIndex * samplesPerBlock);
352 const juce::int64 slotEndSamplePos = std::min (slotStartSamplePos + samplesPerBlock, lengthInSamples);
353
354 return { slotStartSamplePos, slotEndSamplePos };
355}
356
357void BufferedFileReader::markSlotUseState (size_t slotIndex, bool isInUse)
358{
359 assert (slotIndex < slotsInUse.size());
360 auto& slotInUseState = slotsInUse[slotIndex];
361 auto expected = ! isInUse;
362
363 for (;;)
364 {
365 if (slotInUseState.compare_exchange_weak (expected, isInUse))
366 break;
367
368 expected = ! isInUse; // Update this as it might have been changed by the cmp call!
369 }
370}
371
372}} // namespace tracktion { inline namespace engine
assert
T back(T... args)
T begin(T... args)
bool read(float *const *destChannels, int numDestChannels, int64 startSampleInSource, int numSamplesToRead)
StringPairArray metadataValues
static void clearSamplesBeyondAvailableLength(int *const *destChannels, int numDestChannels, int startOffsetInDestBuffer, int64 startSampleInFile, int &numSamples, int64 fileLengthInSamples)
unsigned int bitsPerSample
unsigned int numChannels
constexpr ValueType getEnd() const noexcept
constexpr ValueType getLength() const noexcept
static void JUCE_CALLTYPE yield()
void removeTimeSliceClient(TimeSliceClient *clientToRemove)
void addTimeSliceClient(TimeSliceClient *clientToAdd, int millisecondsBeforeStarting=0)
void moveToFrontOfQueue(TimeSliceClient *clientToMove)
static uint32 getMillisecondCounter() noexcept
bool isFullyBuffered() const
Returns true if this has been initialised to buffer the whole file once that is complete,...
void setReadTimeout(int timeoutMilliseconds) noexcept
Sets a number of milliseconds that the reader can block for in its readSamples() method before giving...
BufferedFileReader(juce::AudioFormatReader *sourceReader, juce::TimeSliceThread &timeSliceThread, int samplesToBuffer)
Creates a reader.
T end(T... args)
T exchange(T... args)
T fill(T... args)
T is_pointer_v
#define jassert(expression)
#define DBG(textToWrite)
typedef int
T load(T... args)
typedef float
T max(T... args)
T min(T... args)
unsigned int uint32
long long int64
T push_back(T... args)
T size(T... args)
typedef size_t