2 ==============================================================================
4 This file is part of the JUCE library - "Jules' Utility Class Extensions"
5 Copyright 2004-11 by Raw Material Software Ltd.
7 ------------------------------------------------------------------------------
9 JUCE can be redistributed and/or modified under the terms of the GNU General
10 Public License (Version 2), as published by the Free Software Foundation.
11 A copy of the license is included in the JUCE distribution, or can be found
12 online at www.gnu.org/licenses.
14 JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 ------------------------------------------------------------------------------
20 To release a closed-source product which uses JUCE, commercial licenses are
21 available: visit www.rawmaterialsoftware.com/juce for more information.
23 ==============================================================================
26 #include "../../core/juce_StandardHeader.h"
30 #include "juce_AudioThumbnail.h"
31 #include "juce_AudioThumbnailCache.h"
32 #include "../../events/juce_MessageManager.h"
33 #include "../../io/streams/juce_BufferedInputStream.h"
36 //==============================================================================
37 struct AudioThumbnail::MinMaxValue
39 MinMaxValue() noexcept
45 inline void set (const char newMin
, const char newMax
) noexcept
51 inline char getMinValue() const noexcept
{ return values
[0]; }
52 inline char getMaxValue() const noexcept
{ return values
[1]; }
54 inline void setFloat (const float newMin
, const float newMax
) noexcept
56 values
[0] = (char) jlimit (-128, 127, roundFloatToInt (newMin
* 127.0f
));
57 values
[1] = (char) jlimit (-128, 127, roundFloatToInt (newMax
* 127.0f
));
59 if (values
[0] == values
[1])
68 inline bool isNonZero() const noexcept
70 return values
[1] > values
[0];
73 inline int getPeak() const noexcept
75 return jmax (std::abs ((int) values
[0]),
76 std::abs ((int) values
[1]));
79 inline void read (InputStream
& input
) { input
.read (values
, 2); }
80 inline void write (OutputStream
& output
) { output
.write (values
, 2); }
86 //==============================================================================
87 class AudioThumbnail::LevelDataSource
: public TimeSliceClient
90 LevelDataSource (AudioThumbnail
& owner_
, AudioFormatReader
* newReader
, int64 hash
)
91 : lengthInSamples (0), numSamplesFinished (0), sampleRate (0), numChannels (0),
92 hashCode (hash
), owner (owner_
), reader (newReader
)
96 LevelDataSource (AudioThumbnail
& owner_
, InputSource
* source_
)
97 : lengthInSamples (0), numSamplesFinished (0), sampleRate (0), numChannels (0),
98 hashCode (source_
->hashCode()), owner (owner_
), source (source_
)
104 owner
.cache
.removeTimeSliceClient (this);
107 enum { timeBeforeDeletingReader
= 1000 };
109 void initialise (int64 numSamplesFinished_
)
111 const ScopedLock
sl (readerLock
);
113 numSamplesFinished
= numSamplesFinished_
;
117 if (reader
!= nullptr)
119 lengthInSamples
= reader
->lengthInSamples
;
120 numChannels
= reader
->numChannels
;
121 sampleRate
= reader
->sampleRate
;
123 if (lengthInSamples
<= 0 || isFullyLoaded())
126 owner
.cache
.addTimeSliceClient (this);
130 void getLevels (int64 startSample
, int numSamples
, Array
<float>& levels
)
132 const ScopedLock
sl (readerLock
);
134 if (reader
== nullptr)
138 if (reader
!= nullptr)
139 owner
.cache
.addTimeSliceClient (this);
142 if (reader
!= nullptr)
145 reader
->readMaxLevels (startSample
, numSamples
, l
[0], l
[1], l
[2], l
[3]);
148 levels
.addArray ((const float*) l
, 4);
152 void releaseResources()
154 const ScopedLock
sl (readerLock
);
162 if (reader
!= nullptr && source
!= nullptr)
168 bool justFinished
= false;
171 const ScopedLock
sl (readerLock
);
175 if (reader
!= nullptr)
177 if (! readNextBlock())
185 owner
.cache
.storeThumb (owner
, hashCode
);
187 return timeBeforeDeletingReader
;
190 bool isFullyLoaded() const noexcept
192 return numSamplesFinished
>= lengthInSamples
;
195 inline int sampleToThumbSample (const int64 originalSample
) const noexcept
197 return (int) (originalSample
/ owner
.samplesPerThumbSample
);
200 int64 lengthInSamples
, numSamplesFinished
;
206 AudioThumbnail
& owner
;
207 ScopedPointer
<InputSource
> source
;
208 ScopedPointer
<AudioFormatReader
> reader
;
209 CriticalSection readerLock
;
213 if (reader
== nullptr && source
!= nullptr)
215 InputStream
* audioFileStream
= source
->createInputStream();
217 if (audioFileStream
!= nullptr)
218 reader
= owner
.formatManagerToUse
.createReaderFor (audioFileStream
);
224 jassert (reader
!= nullptr);
226 if (! isFullyLoaded())
228 const int numToDo
= (int) jmin (256 * (int64
) owner
.samplesPerThumbSample
, lengthInSamples
- numSamplesFinished
);
232 int64 startSample
= numSamplesFinished
;
234 const int firstThumbIndex
= sampleToThumbSample (startSample
);
235 const int lastThumbIndex
= sampleToThumbSample (startSample
+ numToDo
);
236 const int numThumbSamps
= lastThumbIndex
- firstThumbIndex
;
238 HeapBlock
<MinMaxValue
> levelData (numThumbSamps
* 2);
239 MinMaxValue
* levels
[2] = { levelData
, levelData
+ numThumbSamps
};
241 for (int i
= 0; i
< numThumbSamps
; ++i
)
243 float lowestLeft
, highestLeft
, lowestRight
, highestRight
;
245 reader
->readMaxLevels ((firstThumbIndex
+ i
) * owner
.samplesPerThumbSample
, owner
.samplesPerThumbSample
,
246 lowestLeft
, highestLeft
, lowestRight
, highestRight
);
248 levels
[0][i
].setFloat (lowestLeft
, highestLeft
);
249 levels
[1][i
].setFloat (lowestRight
, highestRight
);
253 const ScopedUnlock
su (readerLock
);
254 owner
.setLevels (levels
, firstThumbIndex
, 2, numThumbSamps
);
257 numSamplesFinished
+= numToDo
;
261 return isFullyLoaded();
265 //==============================================================================
266 class AudioThumbnail::ThumbData
269 ThumbData (const int numThumbSamples
)
272 ensureSize (numThumbSamples
);
275 inline MinMaxValue
* getData (const int thumbSampleIndex
) noexcept
277 jassert (thumbSampleIndex
< data
.size());
278 return data
.getRawDataPointer() + thumbSampleIndex
;
281 int getSize() const noexcept
286 void getMinMax (int startSample
, int endSample
, MinMaxValue
& result
) const noexcept
288 if (startSample
>= 0)
290 endSample
= jmin (endSample
, data
.size() - 1);
295 while (startSample
<= endSample
)
297 const MinMaxValue
& v
= data
.getReference (startSample
);
299 if (v
.getMinValue() < mn
) mn
= v
.getMinValue();
300 if (v
.getMaxValue() > mx
) mx
= v
.getMaxValue();
315 void write (const MinMaxValue
* const source
, const int startIndex
, const int numValues
)
319 if (startIndex
+ numValues
> data
.size())
320 ensureSize (startIndex
+ numValues
);
322 MinMaxValue
* const dest
= getData (startIndex
);
324 for (int i
= 0; i
< numValues
; ++i
)
328 void resetPeak() noexcept
333 int getPeak() noexcept
337 for (int i
= 0; i
< data
.size(); ++i
)
339 const int peak
= data
[i
].getPeak();
340 if (peak
> peakLevel
)
349 Array
<MinMaxValue
> data
;
352 void ensureSize (const int thumbSamples
)
354 const int extraNeeded
= thumbSamples
- data
.size();
356 data
.insertMultiple (-1, MinMaxValue(), extraNeeded
);
360 //==============================================================================
361 class AudioThumbnail::CachedWindow
365 : cachedStart (0), cachedTimePerPixel (0),
366 numChannelsCached (0), numSamplesCached (0),
367 cacheNeedsRefilling (true)
373 cacheNeedsRefilling
= true;
376 void drawChannel (Graphics
& g
, const Rectangle
<int>& area
,
377 const double startTime
, const double endTime
,
378 const int channelNum
, const float verticalZoomFactor
,
379 const double sampleRate
, const int numChannels
, const int samplesPerThumbSample
,
380 LevelDataSource
* levelData
, const OwnedArray
<ThumbData
>& channels
)
382 refillCache (area
.getWidth(), startTime
, endTime
, sampleRate
,
383 numChannels
, samplesPerThumbSample
, levelData
, channels
);
385 if (isPositiveAndBelow (channelNum
, numChannelsCached
))
387 const Rectangle
<int> clip (g
.getClipBounds().getIntersection (area
.withWidth (jmin (numSamplesCached
, area
.getWidth()))));
389 if (! clip
.isEmpty())
391 const float topY
= (float) area
.getY();
392 const float bottomY
= (float) area
.getBottom();
393 const float midY
= (topY
+ bottomY
) * 0.5f
;
394 const float vscale
= verticalZoomFactor
* (bottomY
- topY
) / 256.0f
;
396 const MinMaxValue
* cacheData
= getData (channelNum
, clip
.getX() - area
.getX());
399 for (int w
= clip
.getWidth(); --w
>= 0;)
401 if (cacheData
->isNonZero())
402 g
.drawVerticalLine (x
, jmax (midY
- cacheData
->getMaxValue() * vscale
- 0.3f
, topY
),
403 jmin (midY
- cacheData
->getMinValue() * vscale
+ 0.3f
, bottomY
));
413 Array
<MinMaxValue
> data
;
414 double cachedStart
, cachedTimePerPixel
;
415 int numChannelsCached
, numSamplesCached
;
416 bool cacheNeedsRefilling
;
418 void refillCache (const int numSamples
, double startTime
, const double endTime
,
419 const double sampleRate
, const int numChannels
, const int samplesPerThumbSample
,
420 LevelDataSource
* levelData
, const OwnedArray
<ThumbData
>& channels
)
422 const double timePerPixel
= (endTime
- startTime
) / numSamples
;
424 if (numSamples
<= 0 || timePerPixel
<= 0.0 || sampleRate
<= 0)
430 if (numSamples
== numSamplesCached
431 && numChannelsCached
== numChannels
432 && startTime
== cachedStart
433 && timePerPixel
== cachedTimePerPixel
434 && ! cacheNeedsRefilling
)
439 numSamplesCached
= numSamples
;
440 numChannelsCached
= numChannels
;
441 cachedStart
= startTime
;
442 cachedTimePerPixel
= timePerPixel
;
443 cacheNeedsRefilling
= false;
445 ensureSize (numSamples
);
447 if (timePerPixel
* sampleRate
<= samplesPerThumbSample
&& levelData
!= nullptr)
449 int sample
= roundToInt (startTime
* sampleRate
);
453 for (i
= 0; i
< numSamples
; ++i
)
455 const int nextSample
= roundToInt ((startTime
+ timePerPixel
) * sampleRate
);
459 if (sample
>= levelData
->lengthInSamples
)
462 levelData
->getLevels (sample
, jmax (1, nextSample
- sample
), levels
);
464 const int numChans
= jmin (levels
.size() / 2, numChannelsCached
);
466 for (int chan
= 0; chan
< numChans
; ++chan
)
467 getData (chan
, i
)->setFloat (levels
.getUnchecked (chan
* 2),
468 levels
.getUnchecked (chan
* 2 + 1));
471 startTime
+= timePerPixel
;
475 numSamplesCached
= i
;
479 jassert (channels
.size() == numChannelsCached
);
481 for (int channelNum
= 0; channelNum
< numChannelsCached
; ++channelNum
)
483 ThumbData
* channelData
= channels
.getUnchecked (channelNum
);
484 MinMaxValue
* cacheData
= getData (channelNum
, 0);
486 const double timeToThumbSampleFactor
= sampleRate
/ (double) samplesPerThumbSample
;
488 startTime
= cachedStart
;
489 int sample
= roundToInt (startTime
* timeToThumbSampleFactor
);
491 for (int i
= numSamples
; --i
>= 0;)
493 const int nextSample
= roundToInt ((startTime
+ timePerPixel
) * timeToThumbSampleFactor
);
495 channelData
->getMinMax (sample
, nextSample
, *cacheData
);
498 startTime
+= timePerPixel
;
505 MinMaxValue
* getData (const int channelNum
, const int cacheIndex
) noexcept
507 jassert (isPositiveAndBelow (channelNum
, numChannelsCached
) && isPositiveAndBelow (cacheIndex
, data
.size()));
509 return data
.getRawDataPointer() + channelNum
* numSamplesCached
513 void ensureSize (const int numSamples
)
515 const int itemsRequired
= numSamples
* numChannelsCached
;
517 if (data
.size() < itemsRequired
)
518 data
.insertMultiple (-1, MinMaxValue(), itemsRequired
- data
.size());
522 //==============================================================================
523 AudioThumbnail::AudioThumbnail (const int originalSamplesPerThumbnailSample
,
524 AudioFormatManager
& formatManagerToUse_
,
525 AudioThumbnailCache
& cacheToUse
)
526 : formatManagerToUse (formatManagerToUse_
),
528 window (new CachedWindow()),
529 samplesPerThumbSample (originalSamplesPerThumbnailSample
),
536 AudioThumbnail::~AudioThumbnail()
541 void AudioThumbnail::clear()
547 void AudioThumbnail::clearChannelData()
549 const ScopedLock
sl (lock
);
550 window
->invalidate();
552 totalSamples
= numSamplesFinished
= 0;
559 void AudioThumbnail::reset (int newNumChannels
, double newSampleRate
, int64 totalSamplesInSource
)
563 numChannels
= newNumChannels
;
564 sampleRate
= newSampleRate
;
565 totalSamples
= totalSamplesInSource
;
567 createChannels (1 + (int) (totalSamplesInSource
/ samplesPerThumbSample
));
570 void AudioThumbnail::createChannels (const int length
)
572 while (channels
.size() < numChannels
)
573 channels
.add (new ThumbData (length
));
576 //==============================================================================
577 void AudioThumbnail::loadFrom (InputStream
& rawInput
)
581 BufferedInputStream
input (rawInput
, 4096);
583 if (input
.readByte() != 'j' || input
.readByte() != 'a' || input
.readByte() != 't' || input
.readByte() != 'm')
586 samplesPerThumbSample
= input
.readInt();
587 totalSamples
= input
.readInt64(); // Total number of source samples.
588 numSamplesFinished
= input
.readInt64(); // Number of valid source samples that have been read into the thumbnail.
589 int32 numThumbnailSamples
= input
.readInt(); // Number of samples in the thumbnail data.
590 numChannels
= input
.readInt(); // Number of audio channels.
591 sampleRate
= input
.readInt(); // Source sample rate.
592 input
.skipNextBytes (16); // (reserved)
594 createChannels (numThumbnailSamples
);
596 for (int i
= 0; i
< numThumbnailSamples
; ++i
)
597 for (int chan
= 0; chan
< numChannels
; ++chan
)
598 channels
.getUnchecked(chan
)->getData(i
)->read (input
);
601 void AudioThumbnail::saveTo (OutputStream
& output
) const
603 const ScopedLock
sl (lock
);
605 const int numThumbnailSamples
= channels
.size() == 0 ? 0 : channels
.getUnchecked(0)->getSize();
607 output
.write ("jatm", 4);
608 output
.writeInt (samplesPerThumbSample
);
609 output
.writeInt64 (totalSamples
);
610 output
.writeInt64 (numSamplesFinished
);
611 output
.writeInt (numThumbnailSamples
);
612 output
.writeInt (numChannels
);
613 output
.writeInt ((int) sampleRate
);
614 output
.writeInt64 (0);
615 output
.writeInt64 (0);
617 for (int i
= 0; i
< numThumbnailSamples
; ++i
)
618 for (int chan
= 0; chan
< numChannels
; ++chan
)
619 channels
.getUnchecked(chan
)->getData(i
)->write (output
);
622 //==============================================================================
623 bool AudioThumbnail::setDataSource (LevelDataSource
* newSource
)
625 jassert (MessageManager::getInstance()->currentThreadHasLockedMessageManager());
627 numSamplesFinished
= 0;
629 if (cache
.loadThumb (*this, newSource
->hashCode
) && isFullyLoaded())
631 source
= newSource
; // (make sure this isn't done before loadThumb is called)
633 source
->lengthInSamples
= totalSamples
;
634 source
->sampleRate
= sampleRate
;
635 source
->numChannels
= numChannels
;
636 source
->numSamplesFinished
= numSamplesFinished
;
640 source
= newSource
; // (make sure this isn't done before loadThumb is called)
642 const ScopedLock
sl (lock
);
643 source
->initialise (numSamplesFinished
);
645 totalSamples
= source
->lengthInSamples
;
646 sampleRate
= source
->sampleRate
;
647 numChannels
= source
->numChannels
;
649 createChannels (1 + (int) (totalSamples
/ samplesPerThumbSample
));
652 return sampleRate
> 0 && totalSamples
> 0;
655 bool AudioThumbnail::setSource (InputSource
* const newSource
)
659 return newSource
!= nullptr && setDataSource (new LevelDataSource (*this, newSource
));
662 void AudioThumbnail::setReader (AudioFormatReader
* newReader
, int64 hash
)
666 if (newReader
!= nullptr)
667 setDataSource (new LevelDataSource (*this, newReader
, hash
));
670 int64
AudioThumbnail::getHashCode() const
672 return source
== nullptr ? 0 : source
->hashCode
;
675 void AudioThumbnail::addBlock (const int64 startSample
, const AudioSampleBuffer
& incoming
,
676 int startOffsetInBuffer
, int numSamples
)
678 jassert (startSample
>= 0);
680 const int firstThumbIndex
= (int) (startSample
/ samplesPerThumbSample
);
681 const int lastThumbIndex
= (int) ((startSample
+ numSamples
+ (samplesPerThumbSample
- 1)) / samplesPerThumbSample
);
682 const int numToDo
= lastThumbIndex
- firstThumbIndex
;
686 const int numChans
= jmin (channels
.size(), incoming
.getNumChannels());
688 const HeapBlock
<MinMaxValue
> thumbData (numToDo
* numChans
);
689 const HeapBlock
<MinMaxValue
*> thumbChannels (numChans
);
691 for (int chan
= 0; chan
< numChans
; ++chan
)
693 const float* const sourceData
= incoming
.getSampleData (chan
, startOffsetInBuffer
);
694 MinMaxValue
* const dest
= thumbData
+ numToDo
* chan
;
695 thumbChannels
[chan
] = dest
;
697 for (int i
= 0; i
< numToDo
; ++i
)
700 const int start
= i
* samplesPerThumbSample
;
701 findMinAndMax (sourceData
+ start
, jmin (samplesPerThumbSample
, numSamples
- start
), low
, high
);
702 dest
[i
].setFloat (low
, high
);
706 setLevels (thumbChannels
, firstThumbIndex
, numChans
, numToDo
);
710 void AudioThumbnail::setLevels (const MinMaxValue
* const* values
, int thumbIndex
, int numChans
, int numValues
)
712 const ScopedLock
sl (lock
);
714 for (int i
= jmin (numChans
, channels
.size()); --i
>= 0;)
715 channels
.getUnchecked(i
)->write (values
[i
], thumbIndex
, numValues
);
717 const int64 start
= thumbIndex
* (int64
) samplesPerThumbSample
;
718 const int64 end
= (thumbIndex
+ numValues
) * (int64
) samplesPerThumbSample
;
720 if (numSamplesFinished
>= start
&& end
> numSamplesFinished
)
721 numSamplesFinished
= end
;
723 totalSamples
= jmax (numSamplesFinished
, totalSamples
);
724 window
->invalidate();
728 //==============================================================================
729 int AudioThumbnail::getNumChannels() const noexcept
734 double AudioThumbnail::getTotalLength() const noexcept
736 return sampleRate
> 0 ? (totalSamples
/ sampleRate
) : 0;
739 bool AudioThumbnail::isFullyLoaded() const noexcept
741 return numSamplesFinished
>= totalSamples
- samplesPerThumbSample
;
744 int64
AudioThumbnail::getNumSamplesFinished() const noexcept
746 return numSamplesFinished
;
749 float AudioThumbnail::getApproximatePeak() const
753 for (int i
= channels
.size(); --i
>= 0;)
754 peak
= jmax (peak
, channels
.getUnchecked(i
)->getPeak());
756 return jlimit (0, 127, peak
) / 127.0f
;
759 void AudioThumbnail::getApproximateMinMax (const double startTime
, const double endTime
, const int channelIndex
,
760 float& minValue
, float& maxValue
) const noexcept
763 const ThumbData
* const data
= channels
[channelIndex
];
765 if (data
!= nullptr && sampleRate
> 0)
767 const int firstThumbIndex
= (int) ((startTime
* sampleRate
) / samplesPerThumbSample
);
768 const int lastThumbIndex
= (int) (((endTime
* sampleRate
) + samplesPerThumbSample
- 1) / samplesPerThumbSample
);
770 data
->getMinMax (jmax (0, firstThumbIndex
), lastThumbIndex
, result
);
773 minValue
= result
.getMinValue() / 128.0f
;
774 maxValue
= result
.getMaxValue() / 128.0f
;
777 void AudioThumbnail::drawChannel (Graphics
& g
, const Rectangle
<int>& area
, double startTime
,
778 double endTime
, int channelNum
, float verticalZoomFactor
)
780 const ScopedLock
sl (lock
);
782 window
->drawChannel (g
, area
, startTime
, endTime
, channelNum
, verticalZoomFactor
,
783 sampleRate
, numChannels
, samplesPerThumbSample
, source
, channels
);
786 void AudioThumbnail::drawChannels (Graphics
& g
, const Rectangle
<int>& area
, double startTimeSeconds
,
787 double endTimeSeconds
, float verticalZoomFactor
)
789 for (int i
= 0; i
< numChannels
; ++i
)
791 const int y1
= roundToInt ((i
* area
.getHeight()) / numChannels
);
792 const int y2
= roundToInt (((i
+ 1) * area
.getHeight()) / numChannels
);
794 drawChannel (g
, Rectangle
<int> (area
.getX(), area
.getY() + y1
, area
.getWidth(), y2
- y1
),
795 startTimeSeconds
, endTimeSeconds
, i
, verticalZoomFactor
);