/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct AudioThumbnail::MinMaxValue { MinMaxValue() noexcept { values[0] = 0; values[1] = 0; } inline void set (const int8 newMin, const int8 newMax) noexcept { values[0] = newMin; values[1] = newMax; } inline int8 getMinValue() const noexcept { return values[0]; } inline int8 getMaxValue() const noexcept { return values[1]; } inline void setFloat (Range newRange) noexcept { // Workaround for an ndk armeabi compiler bug which crashes on signed saturation #if JUCE_ANDROID Range limitedRange (jlimit (-1.0f, 1.0f, newRange.getStart()), jlimit (-1.0f, 1.0f, newRange.getEnd())); values[0] = (int8) (limitedRange.getStart() * 127.0f); values[1] = (int8) (limitedRange.getEnd() * 127.0f); #else values[0] = (int8) jlimit (-128, 127, roundToInt (newRange.getStart() * 127.0f)); values[1] = (int8) jlimit (-128, 127, roundToInt (newRange.getEnd() * 127.0f)); #endif if (values[0] == values[1]) { if (values[1] == 127) values[0]--; else values[1]++; } } inline bool isNonZero() const noexcept { return values[1] > values[0]; } inline int getPeak() const noexcept { return jmax (std::abs ((int) values[0]), std::abs ((int) values[1])); } inline void read (InputStream& input) { input.read (values, 2); } inline void write (OutputStream& output) { output.write (values, 2); } private: int8 values[2]; }; //============================================================================== class AudioThumbnail::LevelDataSource : public TimeSliceClient { public: LevelDataSource (AudioThumbnail& thumb, AudioFormatReader* newReader, int64 hash) : hashCode (hash), owner (thumb), reader (newReader) { } LevelDataSource (AudioThumbnail& thumb, InputSource* src) : hashCode (src->hashCode()), owner (thumb), source (src) { } ~LevelDataSource() override { owner.cache.getTimeSliceThread().removeTimeSliceClient (this); } enum { timeBeforeDeletingReader = 3000 }; void initialise (int64 samplesFinished) { const ScopedLock sl (readerLock); numSamplesFinished = samplesFinished; createReader(); if (reader != nullptr) { lengthInSamples = reader->lengthInSamples; numChannels = reader->numChannels; sampleRate = reader->sampleRate; if (lengthInSamples <= 0 || isFullyLoaded()) reader.reset(); else owner.cache.getTimeSliceThread().addTimeSliceClient (this); } } void getLevels (int64 startSample, int numSamples, Array>& levels) { const ScopedLock sl (readerLock); if (reader == nullptr) { createReader(); if (reader != nullptr) { lastReaderUseTime = Time::getMillisecondCounter(); owner.cache.getTimeSliceThread().addTimeSliceClient (this); } } if (reader != nullptr) { if (levels.size() < (int) reader->numChannels) levels.insertMultiple (0, {}, (int) reader->numChannels - levels.size()); reader->readMaxLevels (startSample, numSamples, levels.getRawDataPointer(), (int) reader->numChannels); lastReaderUseTime = Time::getMillisecondCounter(); } } void releaseResources() { const ScopedLock sl (readerLock); reader.reset(); } int useTimeSlice() override { if (isFullyLoaded()) { if (reader != nullptr && source != nullptr) { if (Time::getMillisecondCounter() > lastReaderUseTime + timeBeforeDeletingReader) releaseResources(); else return 200; } return -1; } bool justFinished = false; { const ScopedLock sl (readerLock); createReader(); if (reader != nullptr) { if (! readNextBlock()) return 0; justFinished = true; } } if (justFinished) owner.cache.storeThumb (owner, hashCode); return 200; } bool isFullyLoaded() const noexcept { return numSamplesFinished >= lengthInSamples; } inline int sampleToThumbSample (const int64 originalSample) const noexcept { return (int) (originalSample / owner.samplesPerThumbSample); } int64 lengthInSamples = 0, numSamplesFinished = 0; double sampleRate = 0; unsigned int numChannels = 0; int64 hashCode = 0; private: AudioThumbnail& owner; std::unique_ptr source; std::unique_ptr reader; CriticalSection readerLock; std::atomic lastReaderUseTime { 0 }; void createReader() { if (reader == nullptr && source != nullptr) if (auto* audioFileStream = source->createInputStream()) reader.reset (owner.formatManagerToUse.createReaderFor (std::unique_ptr (audioFileStream))); } bool readNextBlock() { jassert (reader != nullptr); if (! isFullyLoaded()) { auto numToDo = (int) jmin (256 * (int64) owner.samplesPerThumbSample, lengthInSamples - numSamplesFinished); if (numToDo > 0) { auto startSample = numSamplesFinished; auto firstThumbIndex = sampleToThumbSample (startSample); auto lastThumbIndex = sampleToThumbSample (startSample + numToDo); auto numThumbSamps = lastThumbIndex - firstThumbIndex; HeapBlock levelData ((unsigned int) numThumbSamps * numChannels); HeapBlock levels (numChannels); for (int i = 0; i < (int) numChannels; ++i) levels[i] = levelData + i * numThumbSamps; HeapBlock> levelsRead (numChannels); for (int i = 0; i < numThumbSamps; ++i) { reader->readMaxLevels ((firstThumbIndex + i) * owner.samplesPerThumbSample, owner.samplesPerThumbSample, levelsRead, (int) numChannels); for (int j = 0; j < (int) numChannels; ++j) levels[j][i].setFloat (levelsRead[j]); } { const ScopedUnlock su (readerLock); owner.setLevels (levels, firstThumbIndex, (int) numChannels, numThumbSamps); } numSamplesFinished += numToDo; lastReaderUseTime = Time::getMillisecondCounter(); } } return isFullyLoaded(); } }; //============================================================================== class AudioThumbnail::ThumbData { public: ThumbData (int numThumbSamples) { ensureSize (numThumbSamples); } inline MinMaxValue* getData (int thumbSampleIndex) noexcept { jassert (thumbSampleIndex < data.size()); return data.getRawDataPointer() + thumbSampleIndex; } int getSize() const noexcept { return data.size(); } void getMinMax (int startSample, int endSample, MinMaxValue& result) const noexcept { if (startSample >= 0) { endSample = jmin (endSample, data.size() - 1); int8 mx = -128; int8 mn = 127; while (startSample <= endSample) { auto& v = data.getReference (startSample); if (v.getMinValue() < mn) mn = v.getMinValue(); if (v.getMaxValue() > mx) mx = v.getMaxValue(); ++startSample; } if (mn <= mx) { result.set (mn, mx); return; } } result.set (1, 0); } void write (const MinMaxValue* values, int startIndex, int numValues) { resetPeak(); if (startIndex + numValues > data.size()) ensureSize (startIndex + numValues); auto* dest = getData (startIndex); for (int i = 0; i < numValues; ++i) dest[i] = values[i]; } void resetPeak() noexcept { peakLevel = -1; } int getPeak() noexcept { if (peakLevel < 0) { for (auto& s : data) { auto peak = s.getPeak(); if (peak > peakLevel) peakLevel = peak; } } return peakLevel; } private: Array data; int peakLevel = -1; void ensureSize (int thumbSamples) { auto extraNeeded = thumbSamples - data.size(); if (extraNeeded > 0) data.insertMultiple (-1, MinMaxValue(), extraNeeded); } }; //============================================================================== class AudioThumbnail::CachedWindow { public: CachedWindow() {} void invalidate() { cacheNeedsRefilling = true; } void drawChannel (Graphics& g, const Rectangle& area, const double startTime, const double endTime, const int channelNum, const float verticalZoomFactor, const double rate, const int numChans, const int sampsPerThumbSample, LevelDataSource* levelData, const OwnedArray& chans) { if (refillCache (area.getWidth(), startTime, endTime, rate, numChans, sampsPerThumbSample, levelData, chans) && isPositiveAndBelow (channelNum, numChannelsCached)) { auto clip = g.getClipBounds().getIntersection (area.withWidth (jmin (numSamplesCached, area.getWidth()))); if (! clip.isEmpty()) { auto topY = (float) area.getY(); auto bottomY = (float) area.getBottom(); auto midY = (topY + bottomY) * 0.5f; auto vscale = verticalZoomFactor * (bottomY - topY) / 256.0f; auto* cacheData = getData (channelNum, clip.getX() - area.getX()); RectangleList waveform; waveform.ensureStorageAllocated (clip.getWidth()); auto x = (float) clip.getX(); for (int w = clip.getWidth(); --w >= 0;) { if (cacheData->isNonZero()) { auto top = jmax (midY - cacheData->getMaxValue() * vscale - 0.3f, topY); auto bottom = jmin (midY - cacheData->getMinValue() * vscale + 0.3f, bottomY); waveform.addWithoutMerging (Rectangle (x, top, 1.0f, bottom - top)); } x += 1.0f; ++cacheData; } g.fillRectList (waveform); } } } private: Array data; double cachedStart = 0, cachedTimePerPixel = 0; int numChannelsCached = 0, numSamplesCached = 0; bool cacheNeedsRefilling = true; bool refillCache (int numSamples, double startTime, double endTime, double rate, int numChans, int sampsPerThumbSample, LevelDataSource* levelData, const OwnedArray& chans) { auto timePerPixel = (endTime - startTime) / numSamples; if (numSamples <= 0 || timePerPixel <= 0.0 || rate <= 0) { invalidate(); return false; } if (numSamples == numSamplesCached && numChannelsCached == numChans && startTime == cachedStart && timePerPixel == cachedTimePerPixel && ! cacheNeedsRefilling) { return ! cacheNeedsRefilling; } numSamplesCached = numSamples; numChannelsCached = numChans; cachedStart = startTime; cachedTimePerPixel = timePerPixel; cacheNeedsRefilling = false; ensureSize (numSamples); if (timePerPixel * rate <= sampsPerThumbSample && levelData != nullptr) { auto sample = roundToInt (startTime * rate); Array> levels; int i; for (i = 0; i < numSamples; ++i) { auto nextSample = roundToInt ((startTime + timePerPixel) * rate); if (sample >= 0) { if (sample >= levelData->lengthInSamples) { for (int chan = 0; chan < numChannelsCached; ++chan) *getData (chan, i) = MinMaxValue(); } else { levelData->getLevels (sample, jmax (1, nextSample - sample), levels); auto totalChans = jmin (levels.size(), numChannelsCached); for (int chan = 0; chan < totalChans; ++chan) getData (chan, i)->setFloat (levels.getReference (chan)); } } startTime += timePerPixel; sample = nextSample; } numSamplesCached = i; } else { jassert (chans.size() == numChannelsCached); for (int channelNum = 0; channelNum < numChannelsCached; ++channelNum) { ThumbData* channelData = chans.getUnchecked (channelNum); MinMaxValue* cacheData = getData (channelNum, 0); auto timeToThumbSampleFactor = rate / (double) sampsPerThumbSample; startTime = cachedStart; auto sample = roundToInt (startTime * timeToThumbSampleFactor); for (int i = numSamples; --i >= 0;) { auto nextSample = roundToInt ((startTime + timePerPixel) * timeToThumbSampleFactor); channelData->getMinMax (sample, nextSample, *cacheData); ++cacheData; startTime += timePerPixel; sample = nextSample; } } } return true; } MinMaxValue* getData (const int channelNum, const int cacheIndex) noexcept { jassert (isPositiveAndBelow (channelNum, numChannelsCached) && isPositiveAndBelow (cacheIndex, data.size())); return data.getRawDataPointer() + channelNum * numSamplesCached + cacheIndex; } void ensureSize (const int numSamples) { auto itemsRequired = numSamples * numChannelsCached; if (data.size() < itemsRequired) data.insertMultiple (-1, MinMaxValue(), itemsRequired - data.size()); } }; //============================================================================== AudioThumbnail::AudioThumbnail (const int originalSamplesPerThumbnailSample, AudioFormatManager& formatManager, AudioThumbnailCache& cacheToUse) : formatManagerToUse (formatManager), cache (cacheToUse), window (new CachedWindow()), samplesPerThumbSample (originalSamplesPerThumbnailSample) { } AudioThumbnail::~AudioThumbnail() { clear(); } void AudioThumbnail::clear() { source.reset(); const ScopedLock sl (lock); clearChannelData(); } void AudioThumbnail::clearChannelData() { window->invalidate(); channels.clear(); totalSamples = numSamplesFinished = 0; numChannels = 0; sampleRate = 0; sendChangeMessage(); } void AudioThumbnail::reset (int newNumChannels, double newSampleRate, int64 totalSamplesInSource) { clear(); const ScopedLock sl (lock); numChannels = newNumChannels; sampleRate = newSampleRate; totalSamples = totalSamplesInSource; createChannels (1 + (int) (totalSamplesInSource / samplesPerThumbSample)); } void AudioThumbnail::createChannels (const int length) { while (channels.size() < numChannels) channels.add (new ThumbData (length)); } //============================================================================== bool AudioThumbnail::loadFrom (InputStream& rawInput) { BufferedInputStream input (rawInput, 4096); if (input.readByte() != 'j' || input.readByte() != 'a' || input.readByte() != 't' || input.readByte() != 'm') return false; const ScopedLock sl (lock); clearChannelData(); samplesPerThumbSample = input.readInt(); totalSamples = input.readInt64(); // Total number of source samples. numSamplesFinished = input.readInt64(); // Number of valid source samples that have been read into the thumbnail. int32 numThumbnailSamples = input.readInt(); // Number of samples in the thumbnail data. numChannels = input.readInt(); // Number of audio channels. sampleRate = input.readInt(); // Source sample rate. input.skipNextBytes (16); // (reserved) createChannels (numThumbnailSamples); for (int i = 0; i < numThumbnailSamples; ++i) for (int chan = 0; chan < numChannels; ++chan) channels.getUnchecked(chan)->getData(i)->read (input); return true; } void AudioThumbnail::saveTo (OutputStream& output) const { const ScopedLock sl (lock); const int numThumbnailSamples = channels.size() == 0 ? 0 : channels.getUnchecked(0)->getSize(); output.write ("jatm", 4); output.writeInt (samplesPerThumbSample); output.writeInt64 (totalSamples); output.writeInt64 (numSamplesFinished); output.writeInt (numThumbnailSamples); output.writeInt (numChannels); output.writeInt ((int) sampleRate); output.writeInt64 (0); output.writeInt64 (0); for (int i = 0; i < numThumbnailSamples; ++i) for (int chan = 0; chan < numChannels; ++chan) channels.getUnchecked(chan)->getData(i)->write (output); } //============================================================================== bool AudioThumbnail::setDataSource (LevelDataSource* newSource) { JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED numSamplesFinished = 0; auto wasSuccessful = [&] { return sampleRate > 0 && totalSamples > 0; }; if (cache.loadThumb (*this, newSource->hashCode) && isFullyLoaded()) { source.reset (newSource); // (make sure this isn't done before loadThumb is called) source->lengthInSamples = totalSamples; source->sampleRate = sampleRate; source->numChannels = (unsigned int) numChannels; source->numSamplesFinished = numSamplesFinished; return wasSuccessful(); } source.reset (newSource); const ScopedLock sl (lock); source->initialise (numSamplesFinished); totalSamples = source->lengthInSamples; sampleRate = source->sampleRate; numChannels = (int32) source->numChannels; createChannels (1 + (int) (totalSamples / samplesPerThumbSample)); return wasSuccessful(); } bool AudioThumbnail::setSource (InputSource* const newSource) { clear(); return newSource != nullptr && setDataSource (new LevelDataSource (*this, newSource)); } void AudioThumbnail::setReader (AudioFormatReader* newReader, int64 hash) { clear(); if (newReader != nullptr) setDataSource (new LevelDataSource (*this, newReader, hash)); } int64 AudioThumbnail::getHashCode() const { return source == nullptr ? 0 : source->hashCode; } void AudioThumbnail::addBlock (int64 startSample, const AudioBuffer& incoming, int startOffsetInBuffer, int numSamples) { jassert (startSample >= 0 && startOffsetInBuffer >= 0 && startOffsetInBuffer + numSamples <= incoming.getNumSamples()); auto firstThumbIndex = (int) (startSample / samplesPerThumbSample); auto lastThumbIndex = (int) ((startSample + numSamples + (samplesPerThumbSample - 1)) / samplesPerThumbSample); auto numToDo = lastThumbIndex - firstThumbIndex; if (numToDo > 0) { auto numChans = jmin (channels.size(), incoming.getNumChannels()); const HeapBlock thumbData (numToDo * numChans); const HeapBlock thumbChannels (numChans); for (int chan = 0; chan < numChans; ++chan) { auto* sourceData = incoming.getReadPointer (chan, startOffsetInBuffer); auto* dest = thumbData + numToDo * chan; thumbChannels [chan] = dest; for (int i = 0; i < numToDo; ++i) { auto start = i * samplesPerThumbSample; dest[i].setFloat (FloatVectorOperations::findMinAndMax (sourceData + start, jmin (samplesPerThumbSample, numSamples - start))); } } setLevels (thumbChannels, firstThumbIndex, numChans, numToDo); } } void AudioThumbnail::setLevels (const MinMaxValue* const* values, int thumbIndex, int numChans, int numValues) { const ScopedLock sl (lock); for (int i = jmin (numChans, channels.size()); --i >= 0;) channels.getUnchecked(i)->write (values[i], thumbIndex, numValues); auto start = thumbIndex * (int64) samplesPerThumbSample; auto end = (thumbIndex + numValues) * (int64) samplesPerThumbSample; if (numSamplesFinished >= start && end > numSamplesFinished) numSamplesFinished = end; totalSamples = jmax (numSamplesFinished, totalSamples.load()); window->invalidate(); sendChangeMessage(); } //============================================================================== int AudioThumbnail::getNumChannels() const noexcept { return numChannels; } double AudioThumbnail::getTotalLength() const noexcept { return sampleRate > 0 ? ((double) totalSamples / sampleRate) : 0.0; } bool AudioThumbnail::isFullyLoaded() const noexcept { return numSamplesFinished >= totalSamples - samplesPerThumbSample; } double AudioThumbnail::getProportionComplete() const noexcept { return jlimit (0.0, 1.0, (double) numSamplesFinished / (double) jmax ((int64) 1, totalSamples.load())); } int64 AudioThumbnail::getNumSamplesFinished() const noexcept { return numSamplesFinished; } float AudioThumbnail::getApproximatePeak() const { const ScopedLock sl (lock); int peak = 0; for (auto* c : channels) peak = jmax (peak, c->getPeak()); return (float) jlimit (0, 127, peak) / 127.0f; } void AudioThumbnail::getApproximateMinMax (double startTime, double endTime, int channelIndex, float& minValue, float& maxValue) const noexcept { const ScopedLock sl (lock); MinMaxValue result; auto* data = channels [channelIndex]; if (data != nullptr && sampleRate > 0) { auto firstThumbIndex = (int) ((startTime * sampleRate) / samplesPerThumbSample); auto lastThumbIndex = (int) (((endTime * sampleRate) + samplesPerThumbSample - 1) / samplesPerThumbSample); data->getMinMax (jmax (0, firstThumbIndex), lastThumbIndex, result); } minValue = result.getMinValue() / 128.0f; maxValue = result.getMaxValue() / 128.0f; } void AudioThumbnail::drawChannel (Graphics& g, const Rectangle& area, double startTime, double endTime, int channelNum, float verticalZoomFactor) { const ScopedLock sl (lock); window->drawChannel (g, area, startTime, endTime, channelNum, verticalZoomFactor, sampleRate, numChannels, samplesPerThumbSample, source.get(), channels); } void AudioThumbnail::drawChannels (Graphics& g, const Rectangle& area, double startTimeSeconds, double endTimeSeconds, float verticalZoomFactor) { for (int i = 0; i < numChannels; ++i) { auto y1 = roundToInt ((i * area.getHeight()) / numChannels); auto y2 = roundToInt (((i + 1) * area.getHeight()) / numChannels); drawChannel (g, { area.getX(), area.getY() + y1, area.getWidth(), y2 - y1 }, startTimeSeconds, endTimeSeconds, i, verticalZoomFactor); } } } // namespace juce