git subrepo clone --branch=sono6good https://github.com/essej/JUCE.git deps/juce

subrepo:
  subdir:   "deps/juce"
  merged:   "b13f9084e"
upstream:
  origin:   "https://github.com/essej/JUCE.git"
  branch:   "sono6good"
  commit:   "b13f9084e"
git-subrepo:
  version:  "0.4.3"
  origin:   "https://github.com/ingydotnet/git-subrepo.git"
  commit:   "2f68596"
This commit is contained in:
essej
2022-04-18 17:51:22 -04:00
parent 63e175fee6
commit 25bd5d8adb
3210 changed files with 1045392 additions and 0 deletions

View File

@ -0,0 +1,89 @@
/*
==============================================================================
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
{
AudioAppComponent::AudioAppComponent()
: deviceManager (defaultDeviceManager),
usingCustomDeviceManager (false)
{
}
AudioAppComponent::AudioAppComponent (AudioDeviceManager& adm)
: deviceManager (adm),
usingCustomDeviceManager (true)
{
}
AudioAppComponent::~AudioAppComponent()
{
// If you hit this then your derived class must call shutdown audio in
// destructor!
jassert (audioSourcePlayer.getCurrentSource() == nullptr);
}
void AudioAppComponent::setAudioChannels (int numInputChannels, int numOutputChannels, const XmlElement* const xml)
{
String audioError;
if (usingCustomDeviceManager && xml == nullptr)
{
auto setup = deviceManager.getAudioDeviceSetup();
if (setup.inputChannels.countNumberOfSetBits() != numInputChannels
|| setup.outputChannels.countNumberOfSetBits() != numOutputChannels)
{
setup.inputChannels.clear();
setup.outputChannels.clear();
setup.inputChannels.setRange (0, numInputChannels, true);
setup.outputChannels.setRange (0, numOutputChannels, true);
audioError = deviceManager.setAudioDeviceSetup (setup, false);
}
}
else
{
audioError = deviceManager.initialise (numInputChannels, numOutputChannels, xml, true);
}
jassert (audioError.isEmpty());
deviceManager.addAudioCallback (&audioSourcePlayer);
audioSourcePlayer.setSource (this);
}
void AudioAppComponent::shutdownAudio()
{
audioSourcePlayer.setSource (nullptr);
deviceManager.removeAudioCallback (&audioSourcePlayer);
// other audio callbacks may still be using the device
if (! usingCustomDeviceManager)
deviceManager.closeAudioDevice();
}
} // namespace juce

View File

@ -0,0 +1,135 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A base class for writing audio apps that stream from the audio i/o devices.
Conveniently combines a Component with an AudioSource to provide a starting
point for your audio applications.
A subclass can inherit from this and implement just a few methods such as
getNextAudioBlock(). The base class provides a basic AudioDeviceManager object
and runs audio through the default output device.
An application should only create one global instance of this object and multiple
classes should not inherit from this.
This class should not be inherited when creating a plug-in as the host will
handle audio streams from hardware devices.
@tags{Audio}
*/
class JUCE_API AudioAppComponent : public Component,
public AudioSource
{
public:
AudioAppComponent();
AudioAppComponent (AudioDeviceManager&);
~AudioAppComponent() override;
/** A subclass should call this from their constructor, to set up the audio. */
void setAudioChannels (int numInputChannels, int numOutputChannels, const XmlElement* const storedSettings = nullptr);
/** Tells the source to prepare for playing.
An AudioSource has two states: prepared and unprepared.
The prepareToPlay() method is guaranteed to be called at least once on an 'unprepared'
source to put it into a 'prepared' state before any calls will be made to getNextAudioBlock().
This callback allows the source to initialise any resources it might need when playing.
Once playback has finished, the releaseResources() method is called to put the stream
back into an 'unprepared' state.
Note that this method could be called more than once in succession without
a matching call to releaseResources(), so make sure your code is robust and
can handle that kind of situation.
@param samplesPerBlockExpected the number of samples that the source
will be expected to supply each time its
getNextAudioBlock() method is called. This
number may vary slightly, because it will be dependent
on audio hardware callbacks, and these aren't
guaranteed to always use a constant block size, so
the source should be able to cope with small variations.
@param sampleRate the sample rate that the output will be used at - this
is needed by sources such as tone generators.
@see releaseResources, getNextAudioBlock
*/
void prepareToPlay (int samplesPerBlockExpected,
double sampleRate) override = 0;
/** Allows the source to release anything it no longer needs after playback has stopped.
This will be called when the source is no longer going to have its getNextAudioBlock()
method called, so it should release any spare memory, etc. that it might have
allocated during the prepareToPlay() call.
Note that there's no guarantee that prepareToPlay() will actually have been called before
releaseResources(), and it may be called more than once in succession, so make sure your
code is robust and doesn't make any assumptions about when it will be called.
@see prepareToPlay, getNextAudioBlock
*/
void releaseResources() override = 0;
/** Called repeatedly to fetch subsequent blocks of audio data.
After calling the prepareToPlay() method, this callback will be made each
time the audio playback hardware (or whatever other destination the audio
data is going to) needs another block of data.
It will generally be called on a high-priority system thread, or possibly even
an interrupt, so be careful not to do too much work here, as that will cause
audio glitches!
@see AudioSourceChannelInfo, prepareToPlay, releaseResources
*/
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override = 0;
/** Shuts down the audio device and clears the audio source.
This method should be called in the destructor of the derived class
otherwise an assertion will be triggered.
*/
void shutdownAudio();
AudioDeviceManager& deviceManager;
private:
//==============================================================================
AudioDeviceManager defaultDeviceManager;
AudioSourcePlayer audioSourcePlayer;
bool usingCustomDeviceManager;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioAppComponent)
};
} // namespace juce

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A component containing controls to let the user change the audio settings of
an AudioDeviceManager object.
Very easy to use - just create one of these and show it to the user.
@see AudioDeviceManager
@tags{Audio}
*/
class JUCE_API AudioDeviceSelectorComponent : public Component,
private ChangeListener,
private Timer
{
public:
//==============================================================================
/** Creates the component.
If your app needs only output channels, you might ask for a maximum of 0 input
channels, and the component won't display any options for choosing the input
channels. And likewise if you're doing an input-only app.
@param deviceManager the device manager that this component should control
@param minAudioInputChannels the minimum number of audio input channels that the application needs
@param maxAudioInputChannels the maximum number of audio input channels that the application needs
@param minAudioOutputChannels the minimum number of audio output channels that the application needs
@param maxAudioOutputChannels the maximum number of audio output channels that the application needs
@param showMidiInputOptions if true, the component will allow the user to select which midi inputs are enabled
@param showMidiOutputSelector if true, the component will let the user choose a default midi output device
@param showChannelsAsStereoPairs if true, channels will be treated as pairs; if false, channels will be
treated as a set of separate mono channels.
@param hideAdvancedOptionsWithButton if true, only the minimum amount of UI components
are shown, with an "advanced" button that shows the rest of them
*/
AudioDeviceSelectorComponent (AudioDeviceManager& deviceManager,
int minAudioInputChannels,
int maxAudioInputChannels,
int minAudioOutputChannels,
int maxAudioOutputChannels,
bool showMidiInputOptions,
bool showMidiOutputSelector,
bool showChannelsAsStereoPairs,
bool hideAdvancedOptionsWithButton);
/** Destructor */
~AudioDeviceSelectorComponent() override;
/** The device manager that this component is controlling */
AudioDeviceManager& deviceManager;
/** Sets the standard height used for items in the panel. */
void setItemHeight (int itemHeight);
/** Returns the standard height used for items in the panel. */
int getItemHeight() const noexcept { return itemHeight; }
/** Returns the ListBox that's being used to show the midi inputs, or nullptr if there isn't one. */
ListBox* getMidiInputSelectorListBox() const noexcept;
//==============================================================================
/** @internal */
void resized() override;
private:
//==============================================================================
void timerCallback() override;
void handleBluetoothButton();
void updateDeviceType();
void updateMidiOutput();
void changeListenerCallback (ChangeBroadcaster*) override;
void updateAllControls();
std::unique_ptr<ComboBox> deviceTypeDropDown;
std::unique_ptr<Label> deviceTypeDropDownLabel;
std::unique_ptr<Component> audioDeviceSettingsComp;
String audioDeviceSettingsCompType;
int itemHeight = 0;
const int minOutputChannels, maxOutputChannels, minInputChannels, maxInputChannels;
const bool showChannelsAsStereoPairs;
const bool hideAdvancedOptionsWithButton;
class MidiInputSelectorComponentListBox;
Array<MidiDeviceInfo> currentMidiOutputs;
std::unique_ptr<MidiInputSelectorComponentListBox> midiInputsList;
std::unique_ptr<ComboBox> midiOutputSelector;
std::unique_ptr<Label> midiInputsLabel, midiOutputLabel;
std::unique_ptr<TextButton> bluetoothButton;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceSelectorComponent)
};
} // namespace juce

View File

@ -0,0 +1,826 @@
/*
==============================================================================
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<float> newRange) noexcept
{
// Workaround for an ndk armeabi compiler bug which crashes on signed saturation
#if JUCE_ANDROID
Range<float> 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<Range<float>>& 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<InputSource> source;
std::unique_ptr<AudioFormatReader> reader;
CriticalSection readerLock;
std::atomic<uint32> lastReaderUseTime { 0 };
void createReader()
{
if (reader == nullptr && source != nullptr)
if (auto* audioFileStream = source->createInputStream())
reader.reset (owner.formatManagerToUse.createReaderFor (std::unique_ptr<InputStream> (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<MinMaxValue> levelData ((unsigned int) numThumbSamps * numChannels);
HeapBlock<MinMaxValue*> levels (numChannels);
for (int i = 0; i < (int) numChannels; ++i)
levels[i] = levelData + i * numThumbSamps;
HeapBlock<Range<float>> 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<MinMaxValue> 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<int>& 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<ThumbData>& 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<float> 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<float> (x, top, 1.0f, bottom - top));
}
x += 1.0f;
++cacheData;
}
g.fillRectList (waveform);
}
}
}
private:
Array<MinMaxValue> 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<ThumbData>& 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<Range<float>> 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<float>& 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<MinMaxValue> thumbData (numToDo * numChans);
const HeapBlock<MinMaxValue*> 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<int>& 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<int>& 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

View File

@ -0,0 +1,222 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Makes it easy to quickly draw scaled views of the waveform shape of an
audio file.
To use this class, just create an AudioThumbnail class for the file you want
to draw, call setSource to tell it which file or resource to use, then call
drawChannel() to draw it.
The class will asynchronously scan the wavefile to create its scaled-down view,
so you should make your UI repaint itself as this data comes in. To do this, the
AudioThumbnail is a ChangeBroadcaster, and will broadcast a message when its
listeners should repaint themselves.
The thumbnail stores an internal low-res version of the wave data, and this can
be loaded and saved to avoid having to scan the file again.
@see AudioThumbnailCache, AudioThumbnailBase
@tags{Audio}
*/
class JUCE_API AudioThumbnail : public AudioThumbnailBase
{
public:
//==============================================================================
/** Creates an audio thumbnail.
@param sourceSamplesPerThumbnailSample when creating a stored, low-res version
of the audio data, this is the scale at which it should be done. (This
number is the number of original samples that will be averaged for each
low-res sample)
@param formatManagerToUse the audio format manager that is used to open the file
@param cacheToUse an instance of an AudioThumbnailCache - this provides a background
thread and storage that is used to by the thumbnail, and the cache
object can be shared between multiple thumbnails
*/
AudioThumbnail (int sourceSamplesPerThumbnailSample,
AudioFormatManager& formatManagerToUse,
AudioThumbnailCache& cacheToUse);
/** Destructor. */
~AudioThumbnail() override;
//==============================================================================
/** Clears and resets the thumbnail. */
void clear() override;
/** Specifies the file or stream that contains the audio file.
For a file, just call
@code
setSource (new FileInputSource (file))
@endcode
You can pass a nullptr in here to clear the thumbnail.
The source that is passed in will be deleted by this object when it is no longer needed.
@returns true if the source could be opened as a valid audio file, false if this failed for
some reason.
*/
bool setSource (InputSource* newSource) override;
/** Gives the thumbnail an AudioFormatReader to use directly.
This will start parsing the audio in a background thread (unless the hash code
can be looked-up successfully in the thumbnail cache). Note that the reader
object will be held by the thumbnail and deleted later when no longer needed.
The thumbnail will actually keep hold of this reader until you clear the thumbnail
or change the input source, so the file will be held open for all this time. If
you don't want the thumbnail to keep a file handle open continuously, you
should use the setSource() method instead, which will only open the file when
it needs to.
*/
void setReader (AudioFormatReader* newReader, int64 hashCode) override;
/** Resets the thumbnail, ready for adding data with the specified format.
If you're going to generate a thumbnail yourself, call this before using addBlock()
to add the data.
*/
void reset (int numChannels, double sampleRate, int64 totalSamplesInSource = 0) override;
/** Adds a block of level data to the thumbnail.
Call reset() before using this, to tell the thumbnail about the data format.
*/
void addBlock (int64 sampleNumberInSource, const AudioBuffer<float>& newData,
int startOffsetInBuffer, int numSamples) override;
//==============================================================================
/** Reloads the low res thumbnail data from an input stream.
This is not an audio file stream! It takes a stream of thumbnail data that would
previously have been created by the saveTo() method.
@see saveTo
*/
bool loadFrom (InputStream& input) override;
/** Saves the low res thumbnail data to an output stream.
The data that is written can later be reloaded using loadFrom().
@see loadFrom
*/
void saveTo (OutputStream& output) const override;
//==============================================================================
/** Returns the number of channels in the file. */
int getNumChannels() const noexcept override;
/** Returns the length of the audio file, in seconds. */
double getTotalLength() const noexcept override;
/** Draws the waveform for a channel.
The waveform will be drawn within the specified rectangle, where startTime
and endTime specify the times within the audio file that should be positioned
at the left and right edges of the rectangle.
The waveform will be scaled vertically so that a full-volume sample will fill
the rectangle vertically, but you can also specify an extra vertical scale factor
with the verticalZoomFactor parameter.
*/
void drawChannel (Graphics& g,
const Rectangle<int>& area,
double startTimeSeconds,
double endTimeSeconds,
int channelNum,
float verticalZoomFactor) override;
/** Draws the waveforms for all channels in the thumbnail.
This will call drawChannel() to render each of the thumbnail's channels, stacked
above each other within the specified area.
@see drawChannel
*/
void drawChannels (Graphics& g,
const Rectangle<int>& area,
double startTimeSeconds,
double endTimeSeconds,
float verticalZoomFactor) override;
/** Returns true if the low res preview is fully generated. */
bool isFullyLoaded() const noexcept override;
/** Returns a value between 0 and 1 to indicate the progress towards loading the entire file. */
double getProportionComplete() const noexcept;
/** Returns the number of samples that have been set in the thumbnail. */
int64 getNumSamplesFinished() const noexcept override;
/** Returns the highest level in the thumbnail.
Note that because the thumb only stores low-resolution data, this isn't
an accurate representation of the highest value, it's only a rough approximation.
*/
float getApproximatePeak() const override;
/** Reads the approximate min and max levels from a section of the thumbnail.
The lowest and highest samples are returned in minValue and maxValue, but obviously
because the thumb only stores low-resolution data, these numbers will only be a rough
approximation of the true values.
*/
void getApproximateMinMax (double startTime, double endTime, int channelIndex,
float& minValue, float& maxValue) const noexcept override;
/** Returns the hash code that was set by setSource() or setReader(). */
int64 getHashCode() const override;
private:
//==============================================================================
AudioFormatManager& formatManagerToUse;
AudioThumbnailCache& cache;
class LevelDataSource;
struct MinMaxValue;
class ThumbData;
class CachedWindow;
std::unique_ptr<LevelDataSource> source;
std::unique_ptr<CachedWindow> window;
OwnedArray<ThumbData> channels;
int32 samplesPerThumbSample = 0;
std::atomic<int64> totalSamples { 0 };
int64 numSamplesFinished = 0;
int32 numChannels = 0;
double sampleRate = 0;
CriticalSection lock;
void clearChannelData();
bool setDataSource (LevelDataSource* newSource);
void setLevels (const MinMaxValue* const* values, int thumbIndex, int numChans, int numValues);
void createChannels (int length);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioThumbnail)
};
} // namespace juce

View File

@ -0,0 +1,158 @@
/*
==============================================================================
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
{
class AudioThumbnailCache;
//==============================================================================
/**
Provides a base for classes that can store and draw scaled views of an
audio waveform.
Typically, you'll want to use the derived class AudioThumbnail, which provides
a concrete implementation.
@see AudioThumbnail, AudioThumbnailCache
@tags{Audio}
*/
class JUCE_API AudioThumbnailBase : public ChangeBroadcaster,
public AudioFormatWriter::ThreadedWriter::IncomingDataReceiver
{
public:
//==============================================================================
AudioThumbnailBase() = default;
~AudioThumbnailBase() override = default;
//==============================================================================
/** Clears and resets the thumbnail. */
virtual void clear() = 0;
/** Specifies the file or stream that contains the audio file.
For a file, just call
@code
setSource (new FileInputSource (file))
@endcode
You can pass a nullptr in here to clear the thumbnail.
The source that is passed in will be deleted by this object when it is no longer needed.
@returns true if the source could be opened as a valid audio file, false if this failed for
some reason.
*/
virtual bool setSource (InputSource* newSource) = 0;
/** Gives the thumbnail an AudioFormatReader to use directly.
This will start parsing the audio in a background thread (unless the hash code
can be looked-up successfully in the thumbnail cache). Note that the reader
object will be held by the thumbnail and deleted later when no longer needed.
The thumbnail will actually keep hold of this reader until you clear the thumbnail
or change the input source, so the file will be held open for all this time. If
you don't want the thumbnail to keep a file handle open continuously, you
should use the setSource() method instead, which will only open the file when
it needs to.
*/
virtual void setReader (AudioFormatReader* newReader, int64 hashCode) = 0;
//==============================================================================
/** Reloads the low res thumbnail data from an input stream.
This is not an audio file stream! It takes a stream of thumbnail data that would
previously have been created by the saveTo() method.
@see saveTo
*/
virtual bool loadFrom (InputStream& input) = 0;
/** Saves the low res thumbnail data to an output stream.
The data that is written can later be reloaded using loadFrom().
@see loadFrom
*/
virtual void saveTo (OutputStream& output) const = 0;
//==============================================================================
/** Returns the number of channels in the file. */
virtual int getNumChannels() const noexcept = 0;
/** Returns the length of the audio file, in seconds. */
virtual double getTotalLength() const noexcept = 0;
/** Draws the waveform for a channel.
The waveform will be drawn within the specified rectangle, where startTime
and endTime specify the times within the audio file that should be positioned
at the left and right edges of the rectangle.
The waveform will be scaled vertically so that a full-volume sample will fill
the rectangle vertically, but you can also specify an extra vertical scale factor
with the verticalZoomFactor parameter.
*/
virtual void drawChannel (Graphics& g,
const Rectangle<int>& area,
double startTimeSeconds,
double endTimeSeconds,
int channelNum,
float verticalZoomFactor) = 0;
/** Draws the waveforms for all channels in the thumbnail.
This will call drawChannel() to render each of the thumbnail's channels, stacked
above each other within the specified area.
@see drawChannel
*/
virtual void drawChannels (Graphics& g,
const Rectangle<int>& area,
double startTimeSeconds,
double endTimeSeconds,
float verticalZoomFactor) = 0;
/** Returns true if the low res preview is fully generated. */
virtual bool isFullyLoaded() const noexcept = 0;
/** Returns the number of samples that have been set in the thumbnail. */
virtual int64 getNumSamplesFinished() const noexcept = 0;
/** Returns the highest level in the thumbnail.
Note that because the thumb only stores low-resolution data, this isn't
an accurate representation of the highest value, it's only a rough approximation.
*/
virtual float getApproximatePeak() const = 0;
/** Reads the approximate min and max levels from a section of the thumbnail.
The lowest and highest samples are returned in minValue and maxValue, but obviously
because the thumb only stores low-resolution data, these numbers will only be a rough
approximation of the true values.
*/
virtual void getApproximateMinMax (double startTime, double endTime, int channelIndex,
float& minValue, float& maxValue) const noexcept = 0;
/** Returns the hash code that was set by setSource() or setReader(). */
virtual int64 getHashCode() const = 0;
};
} // namespace juce

View File

@ -0,0 +1,197 @@
/*
==============================================================================
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
{
class AudioThumbnailCache::ThumbnailCacheEntry
{
public:
ThumbnailCacheEntry (const int64 hashCode)
: hash (hashCode),
lastUsed (Time::getMillisecondCounter())
{
}
ThumbnailCacheEntry (InputStream& in)
: hash (in.readInt64()),
lastUsed (0)
{
const int64 len = in.readInt64();
in.readIntoMemoryBlock (data, (ssize_t) len);
}
void write (OutputStream& out)
{
out.writeInt64 (hash);
out.writeInt64 ((int64) data.getSize());
out << data;
}
int64 hash;
uint32 lastUsed;
MemoryBlock data;
private:
JUCE_LEAK_DETECTOR (ThumbnailCacheEntry)
};
//==============================================================================
AudioThumbnailCache::AudioThumbnailCache (const int maxNumThumbs)
: thread ("thumb cache"),
maxNumThumbsToStore (maxNumThumbs)
{
jassert (maxNumThumbsToStore > 0);
thread.startThread (2);
}
AudioThumbnailCache::~AudioThumbnailCache()
{
}
AudioThumbnailCache::ThumbnailCacheEntry* AudioThumbnailCache::findThumbFor (const int64 hash) const
{
for (int i = thumbs.size(); --i >= 0;)
if (thumbs.getUnchecked(i)->hash == hash)
return thumbs.getUnchecked(i);
return nullptr;
}
int AudioThumbnailCache::findOldestThumb() const
{
int oldest = 0;
uint32 oldestTime = Time::getMillisecondCounter() + 1;
for (int i = thumbs.size(); --i >= 0;)
{
const ThumbnailCacheEntry* const te = thumbs.getUnchecked(i);
if (te->lastUsed < oldestTime)
{
oldest = i;
oldestTime = te->lastUsed;
}
}
return oldest;
}
bool AudioThumbnailCache::loadThumb (AudioThumbnailBase& thumb, const int64 hashCode)
{
const ScopedLock sl (lock);
if (ThumbnailCacheEntry* te = findThumbFor (hashCode))
{
te->lastUsed = Time::getMillisecondCounter();
MemoryInputStream in (te->data, false);
thumb.loadFrom (in);
return true;
}
return loadNewThumb (thumb, hashCode);
}
void AudioThumbnailCache::storeThumb (const AudioThumbnailBase& thumb,
const int64 hashCode)
{
const ScopedLock sl (lock);
ThumbnailCacheEntry* te = findThumbFor (hashCode);
if (te == nullptr)
{
te = new ThumbnailCacheEntry (hashCode);
if (thumbs.size() < maxNumThumbsToStore)
thumbs.add (te);
else
thumbs.set (findOldestThumb(), te);
}
{
MemoryOutputStream out (te->data, false);
thumb.saveTo (out);
}
saveNewlyFinishedThumbnail (thumb, hashCode);
}
void AudioThumbnailCache::clear()
{
const ScopedLock sl (lock);
thumbs.clear();
}
void AudioThumbnailCache::removeThumb (const int64 hashCode)
{
const ScopedLock sl (lock);
for (int i = thumbs.size(); --i >= 0;)
if (thumbs.getUnchecked(i)->hash == hashCode)
thumbs.remove (i);
}
static int getThumbnailCacheFileMagicHeader() noexcept
{
return (int) ByteOrder::littleEndianInt ("ThmC");
}
bool AudioThumbnailCache::readFromStream (InputStream& source)
{
if (source.readInt() != getThumbnailCacheFileMagicHeader())
return false;
const ScopedLock sl (lock);
clear();
int numThumbnails = jmin (maxNumThumbsToStore, source.readInt());
while (--numThumbnails >= 0 && ! source.isExhausted())
thumbs.add (new ThumbnailCacheEntry (source));
return true;
}
void AudioThumbnailCache::writeToStream (OutputStream& out)
{
const ScopedLock sl (lock);
out.writeInt (getThumbnailCacheFileMagicHeader());
out.writeInt (thumbs.size());
for (int i = 0; i < thumbs.size(); ++i)
thumbs.getUnchecked(i)->write (out);
}
void AudioThumbnailCache::saveNewlyFinishedThumbnail (const AudioThumbnailBase&, int64)
{
}
bool AudioThumbnailCache::loadNewThumb (AudioThumbnailBase&, int64)
{
return false;
}
} // namespace juce

View File

@ -0,0 +1,118 @@
/*
==============================================================================
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
{
//==============================================================================
/**
An instance of this class is used to manage multiple AudioThumbnail objects.
The cache runs a single background thread that is shared by all the thumbnails
that need it, and it maintains a set of low-res previews in memory, to avoid
having to re-scan audio files too often.
@see AudioThumbnail
@tags{Audio}
*/
class JUCE_API AudioThumbnailCache
{
public:
//==============================================================================
/** Creates a cache object.
The maxNumThumbsToStore parameter lets you specify how many previews should
be kept in memory at once.
*/
explicit AudioThumbnailCache (int maxNumThumbsToStore);
/** Destructor. */
virtual ~AudioThumbnailCache();
//==============================================================================
/** Clears out any stored thumbnails. */
void clear();
/** Reloads the specified thumb if this cache contains the appropriate stored
data.
This is called automatically by the AudioThumbnail class, so you shouldn't
normally need to call it directly.
*/
bool loadThumb (AudioThumbnailBase& thumb, int64 hashCode);
/** Stores the cachable data from the specified thumb in this cache.
This is called automatically by the AudioThumbnail class, so you shouldn't
normally need to call it directly.
*/
void storeThumb (const AudioThumbnailBase& thumb, int64 hashCode);
/** Tells the cache to forget about the thumb with the given hashcode. */
void removeThumb (int64 hashCode);
//==============================================================================
/** Attempts to re-load a saved cache of thumbnails from a stream.
The cache data must have been written by the writeToStream() method.
This will replace all currently-loaded thumbnails with the new data.
*/
bool readFromStream (InputStream& source);
/** Writes all currently-loaded cache data to a stream.
The resulting data can be re-loaded with readFromStream().
*/
void writeToStream (OutputStream& stream);
/** Returns the thread that client thumbnails can use. */
TimeSliceThread& getTimeSliceThread() noexcept { return thread; }
protected:
/** This can be overridden to provide a custom callback for saving thumbnails
once they have finished being loaded.
*/
virtual void saveNewlyFinishedThumbnail (const AudioThumbnailBase&, int64 hashCode);
/** This can be overridden to provide a custom callback for loading thumbnails
from pre-saved files to save the cache the trouble of having to create them.
*/
virtual bool loadNewThumb (AudioThumbnailBase&, int64 hashCode);
private:
//==============================================================================
TimeSliceThread thread;
class ThumbnailCacheEntry;
OwnedArray<ThumbnailCacheEntry> thumbs;
CriticalSection lock;
int maxNumThumbsToStore;
ThumbnailCacheEntry* findThumbFor (int64 hash) const;
int findOldestThumb() const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioThumbnailCache)
};
} // namespace juce

View File

@ -0,0 +1,222 @@
/*
==============================================================================
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 AudioVisualiserComponent::ChannelInfo
{
ChannelInfo (AudioVisualiserComponent& o, int bufferSize) : owner (o)
{
setBufferSize (bufferSize);
clear();
}
void clear() noexcept
{
levels.fill ({});
value = {};
subSample = 0;
}
void pushSamples (const float* inputSamples, int num) noexcept
{
for (int i = 0; i < num; ++i)
pushSample (inputSamples[i]);
}
void pushSample (float newSample) noexcept
{
if (--subSample <= 0)
{
if (++nextSample == levels.size())
nextSample = 0;
levels.getReference (nextSample) = value;
subSample = owner.getSamplesPerBlock();
value = Range<float> (newSample, newSample);
}
else
{
value = value.getUnionWith (newSample);
}
}
void setBufferSize (int newSize)
{
levels.removeRange (newSize, levels.size());
levels.insertMultiple (-1, {}, newSize - levels.size());
if (nextSample >= newSize)
nextSample = 0;
}
AudioVisualiserComponent& owner;
Array<Range<float>> levels;
Range<float> value;
std::atomic<int> nextSample { 0 }, subSample { 0 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelInfo)
};
//==============================================================================
AudioVisualiserComponent::AudioVisualiserComponent (int initialNumChannels)
: numSamples (1024),
inputSamplesPerBlock (256),
backgroundColour (Colours::black),
waveformColour (Colours::white)
{
setOpaque (true);
setNumChannels (initialNumChannels);
setRepaintRate (60);
}
AudioVisualiserComponent::~AudioVisualiserComponent()
{
}
void AudioVisualiserComponent::setNumChannels (int numChannels)
{
channels.clear();
for (int i = 0; i < numChannels; ++i)
channels.add (new ChannelInfo (*this, numSamples));
}
void AudioVisualiserComponent::setBufferSize (int newNumSamples)
{
numSamples = newNumSamples;
for (auto* c : channels)
c->setBufferSize (newNumSamples);
}
void AudioVisualiserComponent::clear()
{
for (auto* c : channels)
c->clear();
}
void AudioVisualiserComponent::pushBuffer (const float** d, int numChannels, int num)
{
numChannels = jmin (numChannels, channels.size());
for (int i = 0; i < numChannels; ++i)
channels.getUnchecked(i)->pushSamples (d[i], num);
}
void AudioVisualiserComponent::pushBuffer (const AudioBuffer<float>& buffer)
{
pushBuffer (buffer.getArrayOfReadPointers(),
buffer.getNumChannels(),
buffer.getNumSamples());
}
void AudioVisualiserComponent::pushBuffer (const AudioSourceChannelInfo& buffer)
{
auto numChannels = jmin (buffer.buffer->getNumChannels(), channels.size());
for (int i = 0; i < numChannels; ++i)
channels.getUnchecked(i)->pushSamples (buffer.buffer->getReadPointer (i, buffer.startSample),
buffer.numSamples);
}
void AudioVisualiserComponent::pushSample (const float* d, int numChannels)
{
numChannels = jmin (numChannels, channels.size());
for (int i = 0; i < numChannels; ++i)
channels.getUnchecked(i)->pushSample (d[i]);
}
void AudioVisualiserComponent::setSamplesPerBlock (int newSamplesPerPixel) noexcept
{
inputSamplesPerBlock = newSamplesPerPixel;
}
void AudioVisualiserComponent::setRepaintRate (int frequencyInHz)
{
startTimerHz (frequencyInHz);
}
void AudioVisualiserComponent::timerCallback()
{
repaint();
}
void AudioVisualiserComponent::setColours (Colour bk, Colour fg) noexcept
{
backgroundColour = bk;
waveformColour = fg;
repaint();
}
void AudioVisualiserComponent::paint (Graphics& g)
{
g.fillAll (backgroundColour);
auto r = getLocalBounds().toFloat();
auto channelHeight = r.getHeight() / (float) channels.size();
g.setColour (waveformColour);
for (auto* c : channels)
paintChannel (g, r.removeFromTop (channelHeight),
c->levels.begin(), c->levels.size(), c->nextSample);
}
void AudioVisualiserComponent::getChannelAsPath (Path& path, const Range<float>* levels,
int numLevels, int nextSample)
{
path.preallocateSpace (4 * numLevels + 8);
for (int i = 0; i < numLevels; ++i)
{
auto level = -(levels[(nextSample + i) % numLevels].getEnd());
if (i == 0)
path.startNewSubPath (0.0f, level);
else
path.lineTo ((float) i, level);
}
for (int i = numLevels; --i >= 0;)
path.lineTo ((float) i, -(levels[(nextSample + i) % numLevels].getStart()));
path.closeSubPath();
}
void AudioVisualiserComponent::paintChannel (Graphics& g, Rectangle<float> area,
const Range<float>* levels, int numLevels, int nextSample)
{
Path p;
getChannelAsPath (p, levels, numLevels, nextSample);
g.fillPath (p, AffineTransform::fromTargetPoints (0.0f, -1.0f, area.getX(), area.getY(),
0.0f, 1.0f, area.getX(), area.getBottom(),
(float) numLevels, -1.0f, area.getRight(), area.getY()));
}
} // namespace juce

View File

@ -0,0 +1,133 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A simple component that can be used to show a scrolling waveform of audio data.
This is a handy way to get a quick visualisation of some audio data. Just create
one of these, set its size and oversampling rate, and then feed it with incoming
data by calling one of its pushBuffer() or pushSample() methods.
You can override its paint method for more customised views, but it's only designed
as a quick-and-dirty class for simple tasks, so please don't send us feature requests
for fancy additional features that you'd like it to support! If you're building a
real-world app that requires more powerful waveform display, you'll probably want to
create your own component instead.
@tags{Audio}
*/
class JUCE_API AudioVisualiserComponent : public Component,
private Timer
{
public:
/** Creates a visualiser with the given number of channels. */
AudioVisualiserComponent (int initialNumChannels);
/** Destructor. */
~AudioVisualiserComponent() override;
/** Changes the number of channels that the visualiser stores. */
void setNumChannels (int numChannels);
/** Changes the number of samples that the visualiser keeps in its history.
Note that this value refers to the number of averaged sample blocks, and each
block is calculated as the peak of a number of incoming audio samples. To set
the number of incoming samples per block, use setSamplesPerBlock().
*/
void setBufferSize (int bufferSize);
/** */
void setSamplesPerBlock (int newNumInputSamplesPerBlock) noexcept;
/** */
int getSamplesPerBlock() const noexcept { return inputSamplesPerBlock; }
/** Clears the contents of the buffers. */
void clear();
/** Pushes a buffer of channels data.
The number of channels provided here is expected to match the number of channels
that this AudioVisualiserComponent has been told to use.
*/
void pushBuffer (const AudioBuffer<float>& bufferToPush);
/** Pushes a buffer of channels data.
The number of channels provided here is expected to match the number of channels
that this AudioVisualiserComponent has been told to use.
*/
void pushBuffer (const AudioSourceChannelInfo& bufferToPush);
/** Pushes a buffer of channels data.
The number of channels provided here is expected to match the number of channels
that this AudioVisualiserComponent has been told to use.
*/
void pushBuffer (const float** channelData, int numChannels, int numSamples);
/** Pushes a single sample (per channel).
The number of channels provided here is expected to match the number of channels
that this AudioVisualiserComponent has been told to use.
*/
void pushSample (const float* samplesForEachChannel, int numChannels);
/** Sets the colours used to paint the */
void setColours (Colour backgroundColour, Colour waveformColour) noexcept;
/** Sets the frequency at which the component repaints itself. */
void setRepaintRate (int frequencyInHz);
/** Draws a channel of audio data in the given bounds.
The default implementation just calls getChannelAsPath() and fits this into the given
area. You may want to override this to draw things differently.
*/
virtual void paintChannel (Graphics&, Rectangle<float> bounds,
const Range<float>* levels, int numLevels, int nextSample);
/** Creates a path which contains the waveform shape of a given set of range data.
The path is normalised so that -1 and +1 are its upper and lower bounds, and it
goes from 0 to numLevels on the X axis.
*/
void getChannelAsPath (Path& result, const Range<float>* levels, int numLevels, int nextSample);
//==============================================================================
/** @internal */
void paint (Graphics&) override;
private:
struct ChannelInfo;
OwnedArray<ChannelInfo> channels;
int numSamples, inputSamplesPerBlock;
Colour backgroundColour, waveformColour;
void timerCallback() override;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioVisualiserComponent)
};
} // namespace juce

View File

@ -0,0 +1,83 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Opens a Bluetooth MIDI pairing dialogue that allows the user to view and
connect to Bluetooth MIDI devices that are currently found nearby.
The dialogue will ignore non-MIDI Bluetooth devices.
Only after a Bluetooth MIDI device has been paired will its MIDI ports
be available through JUCE's MidiInput and MidiOutput classes.
This dialogue is currently only available on macOS targetting versions 10.11+,
iOS and Android. When targeting older versions of macOS you should instead
pair Bluetooth MIDI devices using the "Audio MIDI Setup" app (located in
/Applications/Utilities). On Windows, you should use the system settings. On
Linux, Bluetooth MIDI devices are currently not supported.
@tags{Audio}
*/
class JUCE_API BluetoothMidiDevicePairingDialogue
{
public:
/** Opens the Bluetooth MIDI pairing dialogue, if it is available.
@param exitCallback A callback which will be called when the modal
bluetooth dialog is closed.
@param btWindowBounds The bounds of the bluetooth window that will
be opened. The dialog itself is opened by the OS so cannot
be customised by JUCE.
@return true if the dialogue was opened, false on error.
@see ModalComponentManager::Callback
*/
static bool open (ModalComponentManager::Callback* exitCallback = nullptr,
Rectangle<int>* btWindowBounds = nullptr);
/** Checks if a Bluetooth MIDI pairing dialogue is available on this
platform.
On iOS, this will be true for iOS versions 8.0 and higher.
On Android, this will be true only for Android SDK versions 23 and
higher, and additionally only if the device itself supports MIDI
over Bluetooth.
On desktop platforms, this will typically be false as the bluetooth
pairing is not done inside the app but by other means.
@return true if the Bluetooth MIDI pairing dialogue is available,
false otherwise.
*/
static bool isAvailable();
};
} // namespace juce

View File

@ -0,0 +1,912 @@
/*
==============================================================================
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
{
static const uint8 whiteNotes[] = { 0, 2, 4, 5, 7, 9, 11 };
static const uint8 blackNotes[] = { 1, 3, 6, 8, 10 };
struct MidiKeyboardComponent::UpDownButton : public Button
{
UpDownButton (MidiKeyboardComponent& c, int d)
: Button ({}), owner (c), delta (d)
{
}
void clicked() override
{
auto note = owner.getLowestVisibleKey();
if (delta < 0)
note = (note - 1) / 12;
else
note = note / 12 + 1;
owner.setLowestVisibleKey (note * 12);
}
using Button::clicked;
void paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
{
owner.drawUpDownButton (g, getWidth(), getHeight(),
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown,
delta > 0);
}
private:
MidiKeyboardComponent& owner;
const int delta;
JUCE_DECLARE_NON_COPYABLE (UpDownButton)
};
//==============================================================================
MidiKeyboardComponent::MidiKeyboardComponent (MidiKeyboardState& s, Orientation o)
: state (s), orientation (o)
{
scrollDown.reset (new UpDownButton (*this, -1));
scrollUp .reset (new UpDownButton (*this, 1));
addChildComponent (scrollDown.get());
addChildComponent (scrollUp.get());
// initialise with a default set of qwerty key-mappings..
int note = 0;
for (char c : "awsedftgyhujkolp;")
setKeyPressForNote (KeyPress (c, 0, 0), note++);
mouseOverNotes.insertMultiple (0, -1, 32);
mouseDownNotes.insertMultiple (0, -1, 32);
colourChanged();
setWantsKeyboardFocus (true);
state.addListener (this);
startTimerHz (20);
}
MidiKeyboardComponent::~MidiKeyboardComponent()
{
state.removeListener (this);
}
//==============================================================================
void MidiKeyboardComponent::setKeyWidth (float widthInPixels)
{
jassert (widthInPixels > 0);
if (keyWidth != widthInPixels) // Prevent infinite recursion if the width is being computed in a 'resized()' call-back
{
keyWidth = widthInPixels;
resized();
}
}
void MidiKeyboardComponent::setScrollButtonWidth (int widthInPixels)
{
jassert (widthInPixels > 0);
if (scrollButtonWidth != widthInPixels)
{
scrollButtonWidth = widthInPixels;
resized();
}
}
void MidiKeyboardComponent::setOrientation (Orientation newOrientation)
{
if (orientation != newOrientation)
{
orientation = newOrientation;
resized();
}
}
void MidiKeyboardComponent::setAvailableRange (int lowestNote, int highestNote)
{
jassert (lowestNote >= 0 && lowestNote <= 127);
jassert (highestNote >= 0 && highestNote <= 127);
jassert (lowestNote <= highestNote);
if (rangeStart != lowestNote || rangeEnd != highestNote)
{
rangeStart = jlimit (0, 127, lowestNote);
rangeEnd = jlimit (0, 127, highestNote);
firstKey = jlimit ((float) rangeStart, (float) rangeEnd, firstKey);
resized();
}
}
void MidiKeyboardComponent::setLowestVisibleKey (int noteNumber)
{
setLowestVisibleKeyFloat ((float) noteNumber);
}
void MidiKeyboardComponent::setLowestVisibleKeyFloat (float noteNumber)
{
noteNumber = jlimit ((float) rangeStart, (float) rangeEnd, noteNumber);
if (noteNumber != firstKey)
{
bool hasMoved = (((int) firstKey) != (int) noteNumber);
firstKey = noteNumber;
if (hasMoved)
sendChangeMessage();
resized();
}
}
void MidiKeyboardComponent::setScrollButtonsVisible (bool newCanScroll)
{
if (canScroll != newCanScroll)
{
canScroll = newCanScroll;
resized();
}
}
void MidiKeyboardComponent::setScrollButtonsWidth (int width)
{
if (scrollButtonWidth != width) {
scrollButtonWidth = width;
resized();
}
}
void MidiKeyboardComponent::colourChanged()
{
setOpaque (findColour (whiteNoteColourId).isOpaque());
repaint();
}
//==============================================================================
void MidiKeyboardComponent::setMidiChannel (int midiChannelNumber)
{
jassert (midiChannelNumber > 0 && midiChannelNumber <= 16);
if (midiChannel != midiChannelNumber)
{
resetAnyKeysInUse();
midiChannel = jlimit (1, 16, midiChannelNumber);
}
}
void MidiKeyboardComponent::setMidiChannelsToDisplay (int midiChannelMask)
{
midiInChannelMask = midiChannelMask;
noPendingUpdates.store (false);
}
void MidiKeyboardComponent::setVelocity (float v, bool useMousePosition)
{
velocity = jlimit (0.0f, 1.0f, v);
useMousePositionForVelocity = useMousePosition;
}
//==============================================================================
Range<float> MidiKeyboardComponent::getKeyPosition (int midiNoteNumber, float targetKeyWidth) const
{
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
static const float notePos[] = { 0.0f, 1 - blackNoteWidthRatio * 0.6f,
1.0f, 2 - blackNoteWidthRatio * 0.4f,
2.0f,
3.0f, 4 - blackNoteWidthRatio * 0.7f,
4.0f, 5 - blackNoteWidthRatio * 0.5f,
5.0f, 6 - blackNoteWidthRatio * 0.3f,
6.0f };
auto octave = midiNoteNumber / 12;
auto note = midiNoteNumber % 12;
auto start = (float) octave * 7.0f * targetKeyWidth + notePos[note] * targetKeyWidth;
auto width = MidiMessage::isMidiNoteBlack (note) ? blackNoteWidthRatio * targetKeyWidth : targetKeyWidth;
return { start, start + width };
}
Range<float> MidiKeyboardComponent::getKeyPos (int midiNoteNumber) const
{
return getKeyPosition (midiNoteNumber, keyWidth)
- xOffset
- getKeyPosition (rangeStart, keyWidth).getStart();
}
Rectangle<float> MidiKeyboardComponent::getRectangleForKey (int note) const
{
jassert (note >= rangeStart && note <= rangeEnd);
auto pos = getKeyPos (note);
auto x = pos.getStart();
auto w = pos.getLength();
if (MidiMessage::isMidiNoteBlack (note))
{
auto blackNoteLength = getBlackNoteLength();
switch (orientation)
{
case horizontalKeyboard: return { x, 0, w, blackNoteLength };
case verticalKeyboardFacingLeft: return { (float) getWidth() - blackNoteLength, x, blackNoteLength, w };
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, blackNoteLength, w };
default: jassertfalse; break;
}
}
else
{
switch (orientation)
{
case horizontalKeyboard: return { x, 0, w, (float) getHeight() };
case verticalKeyboardFacingLeft: return { 0, x, (float) getWidth(), w };
case verticalKeyboardFacingRight: return { 0, (float) getHeight() - x - w, (float) getWidth(), w };
default: jassertfalse; break;
}
}
return {};
}
float MidiKeyboardComponent::getKeyStartPosition (int midiNoteNumber) const
{
return getKeyPos (midiNoteNumber).getStart();
}
float MidiKeyboardComponent::getTotalKeyboardWidth() const noexcept
{
return getKeyPos (rangeEnd).getEnd();
}
int MidiKeyboardComponent::getNoteAtPosition (Point<float> p)
{
float v;
return xyToNote (p, v);
}
int MidiKeyboardComponent::xyToNote (Point<float> pos, float& mousePositionVelocity)
{
if (! reallyContains (pos.toInt(), false))
return -1;
auto p = pos;
if (orientation != horizontalKeyboard)
{
p = { p.y, p.x };
if (orientation == verticalKeyboardFacingLeft)
p = { p.x, (float) getWidth() - p.y };
else
p = { (float) getHeight() - p.x, p.y };
}
return remappedXYToNote (p + Point<float> (xOffset, 0), mousePositionVelocity);
}
int MidiKeyboardComponent::remappedXYToNote (Point<float> pos, float& mousePositionVelocity) const
{
auto blackNoteLength = getBlackNoteLength();
if (pos.getY() < blackNoteLength)
{
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 5; ++i)
{
auto note = octaveStart + blackNotes[i];
if (note >= rangeStart && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
mousePositionVelocity = jmax (0.0f, pos.y / blackNoteLength);
return note;
}
}
}
}
}
for (int octaveStart = 12 * (rangeStart / 12); octaveStart <= rangeEnd; octaveStart += 12)
{
for (int i = 0; i < 7; ++i)
{
auto note = octaveStart + whiteNotes[i];
if (note >= rangeStart && note <= rangeEnd)
{
if (getKeyPos (note).contains (pos.x - xOffset))
{
auto whiteNoteLength = (orientation == horizontalKeyboard) ? getHeight() : getWidth();
mousePositionVelocity = jmax (0.0f, pos.y / (float) whiteNoteLength);
return note;
}
}
}
}
mousePositionVelocity = 0;
return -1;
}
//==============================================================================
void MidiKeyboardComponent::repaintNote (int noteNum)
{
if (noteNum >= rangeStart && noteNum <= rangeEnd)
repaint (getRectangleForKey (noteNum).getSmallestIntegerContainer());
}
void MidiKeyboardComponent::paint (Graphics& g)
{
g.fillAll (findColour (whiteNoteColourId));
auto lineColour = findColour (keySeparatorLineColourId);
auto textColour = findColour (textLabelColourId);
for (int octave = 0; octave < 128; octave += 12)
{
for (int white = 0; white < 7; ++white)
{
auto noteNum = octave + whiteNotes[white];
if (noteNum >= rangeStart && noteNum <= rangeEnd)
drawWhiteNote (noteNum, g, getRectangleForKey (noteNum),
state.isNoteOnForChannels (midiInChannelMask, noteNum),
mouseOverNotes.contains (noteNum), lineColour, textColour);
}
}
float x1 = 0.0f, y1 = 0.0f, x2 = 0.0f, y2 = 0.0f;
auto width = getWidth();
auto height = getHeight();
if (orientation == verticalKeyboardFacingLeft)
{
x1 = (float) width - 1.0f;
x2 = (float) width - 5.0f;
}
else if (orientation == verticalKeyboardFacingRight)
x2 = 5.0f;
else
y2 = 5.0f;
auto x = getKeyPos (rangeEnd).getEnd();
auto shadowCol = findColour (shadowColourId);
if (! shadowCol.isTransparent())
{
g.setGradientFill (ColourGradient (shadowCol, x1, y1, shadowCol.withAlpha (0.0f), x2, y2, false));
switch (orientation)
{
case horizontalKeyboard: g.fillRect (0.0f, 0.0f, x, 5.0f); break;
case verticalKeyboardFacingLeft: g.fillRect ((float) width - 5.0f, 0.0f, 5.0f, x); break;
case verticalKeyboardFacingRight: g.fillRect (0.0f, 0.0f, 5.0f, x); break;
default: break;
}
}
if (! lineColour.isTransparent())
{
g.setColour (lineColour);
switch (orientation)
{
case horizontalKeyboard: g.fillRect (0.0f, (float) height - 1.0f, x, 1.0f); break;
case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, x); break;
case verticalKeyboardFacingRight: g.fillRect ((float) width - 1.0f, 0.0f, 1.0f, x); break;
default: break;
}
}
auto blackNoteColour = findColour (blackNoteColourId);
for (int octave = 0; octave < 128; octave += 12)
{
for (int black = 0; black < 5; ++black)
{
auto noteNum = octave + blackNotes[black];
if (noteNum >= rangeStart && noteNum <= rangeEnd)
drawBlackNote (noteNum, g, getRectangleForKey (noteNum),
state.isNoteOnForChannels (midiInChannelMask, noteNum),
mouseOverNotes.contains (noteNum), blackNoteColour);
}
}
}
void MidiKeyboardComponent::drawWhiteNote (int midiNoteNumber, Graphics& g, Rectangle<float> area,
bool isDown, bool isOver, Colour lineColour, Colour textColour)
{
auto c = Colours::transparentWhite;
if (isDown) c = findColour (keyDownOverlayColourId);
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
g.setColour (c);
g.fillRect (area);
auto text = getWhiteNoteText (midiNoteNumber);
if (text.isNotEmpty())
{
auto fontHeight = jmin (12.0f, keyWidth * 0.9f);
g.setColour (textColour);
g.setFont (Font (fontHeight).withHorizontalScale (0.8f));
switch (orientation)
{
case horizontalKeyboard: g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f), Justification::centredBottom, false); break;
case verticalKeyboardFacingLeft: g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false); break;
case verticalKeyboardFacingRight: g.drawText (text, area.reduced (2.0f), Justification::centredRight, false); break;
default: break;
}
}
if (! lineColour.isTransparent())
{
g.setColour (lineColour);
switch (orientation)
{
case horizontalKeyboard: g.fillRect (area.withWidth (1.0f)); break;
case verticalKeyboardFacingLeft: g.fillRect (area.withHeight (1.0f)); break;
case verticalKeyboardFacingRight: g.fillRect (area.removeFromBottom (1.0f)); break;
default: break;
}
if (midiNoteNumber == rangeEnd)
{
switch (orientation)
{
case horizontalKeyboard: g.fillRect (area.expanded (1.0f, 0).removeFromRight (1.0f)); break;
case verticalKeyboardFacingLeft: g.fillRect (area.expanded (0, 1.0f).removeFromBottom (1.0f)); break;
case verticalKeyboardFacingRight: g.fillRect (area.expanded (0, 1.0f).removeFromTop (1.0f)); break;
default: break;
}
}
}
}
void MidiKeyboardComponent::drawBlackNote (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area,
bool isDown, bool isOver, Colour noteFillColour)
{
auto c = noteFillColour;
if (isDown) c = c.overlaidWith (findColour (keyDownOverlayColourId));
if (isOver) c = c.overlaidWith (findColour (mouseOverKeyOverlayColourId));
g.setColour (c);
g.fillRect (area);
if (isDown)
{
g.setColour (noteFillColour);
g.drawRect (area);
}
else
{
g.setColour (c.brighter());
auto sideIndent = 1.0f / 8.0f;
auto topIndent = 7.0f / 8.0f;
auto w = area.getWidth();
auto h = area.getHeight();
switch (orientation)
{
case horizontalKeyboard: g.fillRect (area.reduced (w * sideIndent, 0).removeFromTop (h * topIndent)); break;
case verticalKeyboardFacingLeft: g.fillRect (area.reduced (0, h * sideIndent).removeFromRight (w * topIndent)); break;
case verticalKeyboardFacingRight: g.fillRect (area.reduced (0, h * sideIndent).removeFromLeft (w * topIndent)); break;
default: break;
}
}
}
void MidiKeyboardComponent::setOctaveForMiddleC (int octaveNum)
{
octaveNumForMiddleC = octaveNum;
repaint();
}
String MidiKeyboardComponent::getWhiteNoteText (int midiNoteNumber)
{
if (midiNoteNumber % 12 == 0)
return MidiMessage::getMidiNoteName (midiNoteNumber, true, true, octaveNumForMiddleC);
return {};
}
void MidiKeyboardComponent::drawUpDownButton (Graphics& g, int w, int h,
bool mouseOver,
bool buttonDown,
bool movesOctavesUp)
{
g.fillAll (findColour (upDownButtonBackgroundColourId));
float angle = 0;
switch (orientation)
{
case horizontalKeyboard: angle = movesOctavesUp ? 0.0f : 0.5f; break;
case verticalKeyboardFacingLeft: angle = movesOctavesUp ? 0.25f : 0.75f; break;
case verticalKeyboardFacingRight: angle = movesOctavesUp ? 0.75f : 0.25f; break;
default: jassertfalse; break;
}
Path path;
path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * angle, 0.5f, 0.5f));
g.setColour (findColour (upDownButtonArrowColourId)
.withAlpha (buttonDown ? 1.0f : (mouseOver ? 0.6f : 0.4f)));
g.fillPath (path, path.getTransformToScaleToFit (1.0f, 1.0f, (float) w - 2.0f, (float) h - 2.0f, true));
}
void MidiKeyboardComponent::setBlackNoteLengthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteLengthRatio != ratio)
{
blackNoteLengthRatio = ratio;
resized();
}
}
float MidiKeyboardComponent::getBlackNoteLength() const noexcept
{
auto whiteNoteLength = orientation == horizontalKeyboard ? getHeight() : getWidth();
return (float) whiteNoteLength * blackNoteLengthRatio;
}
void MidiKeyboardComponent::setBlackNoteWidthProportion (float ratio) noexcept
{
jassert (ratio >= 0.0f && ratio <= 1.0f);
if (blackNoteWidthRatio != ratio)
{
blackNoteWidthRatio = ratio;
resized();
}
}
void MidiKeyboardComponent::resized()
{
auto w = getWidth();
auto h = getHeight();
if (w > 0 && h > 0)
{
if (orientation != horizontalKeyboard)
std::swap (w, h);
auto kx2 = getKeyPos (rangeEnd).getEnd();
if ((int) firstKey != rangeStart)
{
auto kx1 = getKeyPos (rangeStart).getStart();
if (kx2 - kx1 <= (float) w)
{
firstKey = (float) rangeStart;
sendChangeMessage();
repaint();
}
}
scrollDown->setVisible (canScroll && firstKey > (float) rangeStart);
xOffset = 0;
if (canScroll)
{
auto scrollButtonW = jmin (scrollButtonWidth, w / 2);
auto r = getLocalBounds();
if (orientation == horizontalKeyboard)
{
scrollDown->setBounds (r.removeFromLeft (scrollButtonW));
scrollUp ->setBounds (r.removeFromRight (scrollButtonW));
}
else if (orientation == verticalKeyboardFacingLeft)
{
scrollDown->setBounds (r.removeFromTop (scrollButtonW));
scrollUp ->setBounds (r.removeFromBottom (scrollButtonW));
}
else
{
scrollDown->setBounds (r.removeFromBottom (scrollButtonW));
scrollUp ->setBounds (r.removeFromTop (scrollButtonW));
}
auto endOfLastKey = getKeyPos (rangeEnd).getEnd();
float mousePositionVelocity;
auto spaceAvailable = w;
auto lastStartKey = remappedXYToNote ({ endOfLastKey - (float) spaceAvailable, 0 }, mousePositionVelocity) + 1;
if (lastStartKey >= 0 && ((int) firstKey) > lastStartKey)
{
firstKey = (float) jlimit (rangeStart, rangeEnd, lastStartKey);
sendChangeMessage();
}
xOffset = getKeyPos ((int) firstKey).getStart();
}
else
{
firstKey = (float) rangeStart;
}
scrollUp->setVisible (canScroll && getKeyPos (rangeEnd).getStart() > (float) w);
repaint();
}
}
//==============================================================================
void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
{
noPendingUpdates.store (false);
}
void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
{
noPendingUpdates.store (false);
}
//==============================================================================
void MidiKeyboardComponent::resetAnyKeysInUse()
{
if (! keysPressed.isZero())
{
for (int i = 128; --i >= 0;)
if (keysPressed[i])
state.noteOff (midiChannel, i, 0.0f);
keysPressed.clear();
}
for (int i = mouseDownNotes.size(); --i >= 0;)
{
auto noteDown = mouseDownNotes.getUnchecked(i);
if (noteDown >= 0)
{
state.noteOff (midiChannel, noteDown, 0.0f);
mouseDownNotes.set (i, -1);
}
mouseOverNotes.set (i, -1);
}
}
void MidiKeyboardComponent::updateNoteUnderMouse (const MouseEvent& e, bool isDown)
{
updateNoteUnderMouse (e.getEventRelativeTo (this).position, isDown, e.source.getIndex());
}
void MidiKeyboardComponent::updateNoteUnderMouse (Point<float> pos, bool isDown, int fingerNum)
{
float mousePositionVelocity = 0.0f;
auto newNote = xyToNote (pos, mousePositionVelocity);
auto oldNote = mouseOverNotes.getUnchecked (fingerNum);
auto oldNoteDown = mouseDownNotes.getUnchecked (fingerNum);
auto eventVelocity = useMousePositionForVelocity ? mousePositionVelocity * velocity : velocity;
if (oldNote != newNote)
{
repaintNote (oldNote);
repaintNote (newNote);
mouseOverNotes.set (fingerNum, newNote);
}
if (isDown)
{
if (newNote != oldNoteDown)
{
if (oldNoteDown >= 0)
{
mouseDownNotes.set (fingerNum, -1);
if (! mouseDownNotes.contains (oldNoteDown))
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
}
if (newNote >= 0 && ! mouseDownNotes.contains (newNote))
{
state.noteOn (midiChannel, newNote, eventVelocity);
mouseDownNotes.set (fingerNum, newNote);
}
}
}
else if (oldNoteDown >= 0)
{
mouseDownNotes.set (fingerNum, -1);
if (! mouseDownNotes.contains (oldNoteDown))
state.noteOff (midiChannel, oldNoteDown, eventVelocity);
}
}
void MidiKeyboardComponent::mouseMove (const MouseEvent& e)
{
updateNoteUnderMouse (e, false);
}
void MidiKeyboardComponent::mouseDrag (const MouseEvent& e)
{
float mousePositionVelocity;
auto newNote = xyToNote (e.position, mousePositionVelocity);
if (newNote >= 0 && mouseDraggedToKey (newNote, e))
updateNoteUnderMouse (e, true);
}
bool MidiKeyboardComponent::mouseDownOnKey (int, const MouseEvent&) { return true; }
bool MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) { return true; }
void MidiKeyboardComponent::mouseUpOnKey (int, const MouseEvent&) {}
void MidiKeyboardComponent::mouseDown (const MouseEvent& e)
{
float mousePositionVelocity;
auto newNote = xyToNote (e.position, mousePositionVelocity);
if (newNote >= 0 && mouseDownOnKey (newNote, e))
updateNoteUnderMouse (e, true);
}
void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
{
updateNoteUnderMouse (e, false);
float mousePositionVelocity;
auto note = xyToNote (e.position, mousePositionVelocity);
if (note >= 0)
mouseUpOnKey (note, e);
}
void MidiKeyboardComponent::mouseEnter (const MouseEvent& e)
{
updateNoteUnderMouse (e, false);
}
void MidiKeyboardComponent::mouseExit (const MouseEvent& e)
{
updateNoteUnderMouse (e, false);
}
void MidiKeyboardComponent::mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel)
{
auto amount = (orientation == horizontalKeyboard && wheel.deltaX != 0)
? wheel.deltaX : (orientation == verticalKeyboardFacingLeft ? wheel.deltaY
: -wheel.deltaY);
setLowestVisibleKeyFloat (firstKey - amount * keyWidth);
}
void MidiKeyboardComponent::timerCallback()
{
if (noPendingUpdates.exchange (true))
return;
for (int i = rangeStart; i <= rangeEnd; ++i)
{
bool isOn = state.isNoteOnForChannels (midiInChannelMask, i);
if (keysCurrentlyDrawnDown[i] != isOn)
{
keysCurrentlyDrawnDown.setBit (i, isOn);
repaintNote (i);
}
}
}
//==============================================================================
void MidiKeyboardComponent::clearKeyMappings()
{
resetAnyKeysInUse();
keyPressNotes.clear();
keyPresses.clear();
}
void MidiKeyboardComponent::setKeyPressForNote (const KeyPress& key, int midiNoteOffsetFromC)
{
removeKeyPressForNote (midiNoteOffsetFromC);
keyPressNotes.add (midiNoteOffsetFromC);
keyPresses.add (key);
}
void MidiKeyboardComponent::removeKeyPressForNote (int midiNoteOffsetFromC)
{
for (int i = keyPressNotes.size(); --i >= 0;)
{
if (keyPressNotes.getUnchecked (i) == midiNoteOffsetFromC)
{
keyPressNotes.remove (i);
keyPresses.remove (i);
}
}
}
void MidiKeyboardComponent::setKeyPressBaseOctave (int newOctaveNumber)
{
jassert (newOctaveNumber >= 0 && newOctaveNumber <= 10);
keyMappingOctave = newOctaveNumber;
}
bool MidiKeyboardComponent::keyStateChanged (bool /*isKeyDown*/)
{
bool keyPressUsed = false;
for (int i = keyPresses.size(); --i >= 0;)
{
auto note = 12 * keyMappingOctave + keyPressNotes.getUnchecked (i);
if (keyPresses.getReference(i).isCurrentlyDown())
{
if (! keysPressed[note])
{
keysPressed.setBit (note);
state.noteOn (midiChannel, note, velocity);
keyPressUsed = true;
}
}
else
{
if (keysPressed[note])
{
keysPressed.clearBit (note);
state.noteOff (midiChannel, note, 0.0f);
keyPressUsed = true;
}
}
}
return keyPressUsed;
}
bool MidiKeyboardComponent::keyPressed (const KeyPress& key)
{
return keyPresses.contains (key);
}
void MidiKeyboardComponent::focusLost (FocusChangeType)
{
resetAnyKeysInUse();
}
} // namespace juce

View File

@ -0,0 +1,445 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A component that displays a piano keyboard, whose notes can be clicked on.
This component will mimic a physical midi keyboard, showing the current state of
a MidiKeyboardState object. When the on-screen keys are clicked on, it will play these
notes by calling the noteOn() and noteOff() methods of its MidiKeyboardState object.
Another feature is that the computer keyboard can also be used to play notes. By
default it maps the top two rows of a standard qwerty keyboard to the notes, but
these can be remapped if needed. It will only respond to keypresses when it has
the keyboard focus, so to disable this feature you can call setWantsKeyboardFocus (false).
The component is also a ChangeBroadcaster, so if you want to be informed when the
keyboard is scrolled, you can register a ChangeListener for callbacks.
@see MidiKeyboardState
@tags{Audio}
*/
class JUCE_API MidiKeyboardComponent : public Component,
public MidiKeyboardState::Listener,
public ChangeBroadcaster,
private Timer
{
public:
//==============================================================================
/** The direction of the keyboard.
@see setOrientation
*/
enum Orientation
{
horizontalKeyboard,
verticalKeyboardFacingLeft,
verticalKeyboardFacingRight,
};
/** Creates a MidiKeyboardComponent.
@param state the midi keyboard model that this component will represent
@param orientation whether the keyboard is horizontal or vertical
*/
MidiKeyboardComponent (MidiKeyboardState& state,
Orientation orientation);
/** Destructor. */
~MidiKeyboardComponent() override;
//==============================================================================
/** Changes the velocity used in midi note-on messages that are triggered by clicking
on the component.
Values are 0 to 1.0, where 1.0 is the heaviest.
@see setMidiChannel
*/
void setVelocity (float velocity, bool useMousePositionForVelocity);
/** Changes the midi channel number that will be used for events triggered by clicking
on the component.
The channel must be between 1 and 16 (inclusive). This is the channel that will be
passed on to the MidiKeyboardState::noteOn() method when the user clicks the component.
Although this is the channel used for outgoing events, the component can display
incoming events from more than one channel - see setMidiChannelsToDisplay()
@see setVelocity
*/
void setMidiChannel (int midiChannelNumber);
/** Returns the midi channel that the keyboard is using for midi messages.
@see setMidiChannel
*/
int getMidiChannel() const noexcept { return midiChannel; }
/** Sets a mask to indicate which incoming midi channels should be represented by
key movements.
The mask is a set of bits, where bit 0 = midi channel 1, bit 1 = midi channel 2, etc.
If the MidiKeyboardState has a key down for any of the channels whose bits are set
in this mask, the on-screen keys will also go down.
By default, this mask is set to 0xffff (all channels displayed).
@see setMidiChannel
*/
void setMidiChannelsToDisplay (int midiChannelMask);
/** Returns the current set of midi channels represented by the component.
This is the value that was set with setMidiChannelsToDisplay().
*/
int getMidiChannelsToDisplay() const noexcept { return midiInChannelMask; }
//==============================================================================
/** Changes the width used to draw the white keys. */
void setKeyWidth (float widthInPixels);
/** Returns the width that was set by setKeyWidth(). */
float getKeyWidth() const noexcept { return keyWidth; }
/** Changes the width used to draw the buttons that scroll the keyboard up/down in octaves. */
void setScrollButtonWidth (int widthInPixels);
/** Returns the width that was set by setScrollButtonWidth(). */
int getScrollButtonWidth() const noexcept { return scrollButtonWidth; }
/** Changes the keyboard's current direction. */
void setOrientation (Orientation newOrientation);
/** Returns the keyboard's current direction. */
Orientation getOrientation() const noexcept { return orientation; }
/** Sets the range of midi notes that the keyboard will be limited to.
By default the range is 0 to 127 (inclusive), but you can limit this if you
only want a restricted set of the keys to be shown.
Note that the values here are inclusive and must be between 0 and 127.
*/
void setAvailableRange (int lowestNote,
int highestNote);
/** Returns the first note in the available range.
@see setAvailableRange
*/
int getRangeStart() const noexcept { return rangeStart; }
/** Returns the last note in the available range.
@see setAvailableRange
*/
int getRangeEnd() const noexcept { return rangeEnd; }
/** If the keyboard extends beyond the size of the component, this will scroll
it to show the given key at the start.
Whenever the keyboard's position is changed, this will use the ChangeBroadcaster
base class to send a callback to any ChangeListeners that have been registered.
*/
void setLowestVisibleKey (int noteNumber);
/** Returns the number of the first key shown in the component.
@see setLowestVisibleKey
*/
int getLowestVisibleKey() const noexcept { return (int) firstKey; }
/** Sets the length of the black notes as a proportion of the white note length. */
void setBlackNoteLengthProportion (float ratio) noexcept;
/** Returns the length of the black notes as a proportion of the white note length. */
float getBlackNoteLengthProportion() const noexcept { return blackNoteLengthRatio; }
/** Returns the absolute length of the black notes.
This will be their vertical or horizontal length, depending on the keyboard's orientation.
*/
float getBlackNoteLength() const noexcept;
/** Sets the width of the black notes as a proportion of the white note width. */
void setBlackNoteWidthProportion (float ratio) noexcept;
/** Returns the width of the black notes as a proportion of the white note width. */
float getBlackNoteWidthProportion() const noexcept { return blackNoteWidthRatio; }
/** Returns the absolute width of the black notes.
This will be their vertical or horizontal width, depending on the keyboard's orientation.
*/
float getBlackNoteWidth() const noexcept { return keyWidth * blackNoteWidthRatio; }
/** If set to true, then scroll buttons will appear at either end of the keyboard
if there are too many notes to fit them all in the component at once.
*/
void setScrollButtonsVisible (bool canScroll);
/** Sets width of scroll buttons */
void setScrollButtonsWidth (int width);
/** Returns width of scroll buttons */
int getScrollButtonsWidth () const { return scrollButtonWidth; }
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the keyboard.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
whiteNoteColourId = 0x1005000,
blackNoteColourId = 0x1005001,
keySeparatorLineColourId = 0x1005002,
mouseOverKeyOverlayColourId = 0x1005003, /**< This colour will be overlaid on the normal note colour. */
keyDownOverlayColourId = 0x1005004, /**< This colour will be overlaid on the normal note colour. */
textLabelColourId = 0x1005005,
upDownButtonBackgroundColourId = 0x1005006,
upDownButtonArrowColourId = 0x1005007,
shadowColourId = 0x1005008
};
/** Returns the position within the component of the left-hand edge of a key.
Depending on the keyboard's orientation, this may be a horizontal or vertical
distance, in either direction.
*/
float getKeyStartPosition (int midiNoteNumber) const;
/** Returns the total width needed to fit all the keys in the available range. */
float getTotalKeyboardWidth() const noexcept;
/** Returns the key at a given coordinate. */
int getNoteAtPosition (Point<float> position);
//==============================================================================
/** Deletes all key-mappings.
@see setKeyPressForNote
*/
void clearKeyMappings();
/** Maps a key-press to a given note.
@param key the key that should trigger the note
@param midiNoteOffsetFromC how many semitones above C the triggered note should
be. The actual midi note that gets played will be
this value + (12 * the current base octave). To change
the base octave, see setKeyPressBaseOctave()
*/
void setKeyPressForNote (const KeyPress& key,
int midiNoteOffsetFromC);
/** Removes any key-mappings for a given note.
For a description of what the note number means, see setKeyPressForNote().
*/
void removeKeyPressForNote (int midiNoteOffsetFromC);
/** Changes the base note above which key-press-triggered notes are played.
The set of key-mappings that trigger notes can be moved up and down to cover
the entire scale using this method.
The value passed in is an octave number between 0 and 10 (inclusive), and
indicates which C is the base note to which the key-mapped notes are
relative.
*/
void setKeyPressBaseOctave (int newOctaveNumber);
/** This sets the octave number which is shown as the octave number for middle C.
This affects only the default implementation of getWhiteNoteText(), which
passes this octave number to MidiMessage::getMidiNoteName() in order to
get the note text. See MidiMessage::getMidiNoteName() for more info about
the parameter.
By default this value is set to 3.
@see getOctaveForMiddleC
*/
void setOctaveForMiddleC (int octaveNumForMiddleC);
/** This returns the value set by setOctaveForMiddleC().
@see setOctaveForMiddleC
*/
int getOctaveForMiddleC() const noexcept { return octaveNumForMiddleC; }
//==============================================================================
/** @internal */
void paint (Graphics&) override;
/** @internal */
void resized() override;
/** @internal */
void mouseMove (const MouseEvent&) override;
/** @internal */
void mouseDrag (const MouseEvent&) override;
/** @internal */
void mouseDown (const MouseEvent&) override;
/** @internal */
void mouseUp (const MouseEvent&) override;
/** @internal */
void mouseEnter (const MouseEvent&) override;
/** @internal */
void mouseExit (const MouseEvent&) override;
/** @internal */
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) override;
/** @internal */
void timerCallback() override;
/** @internal */
bool keyStateChanged (bool isKeyDown) override;
/** @internal */
bool keyPressed (const KeyPress&) override;
/** @internal */
void focusLost (FocusChangeType) override;
/** @internal */
void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override;
/** @internal */
void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override;
/** @internal */
void colourChanged() override;
protected:
//==============================================================================
/** Draws a white note in the given rectangle.
isOver indicates whether the mouse is over the key, isDown indicates whether the key is
currently pressed down.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawWhiteNote (int midiNoteNumber,
Graphics& g, Rectangle<float> area,
bool isDown, bool isOver,
Colour lineColour, Colour textColour);
/** Draws a black note in the given rectangle.
isOver indicates whether the mouse is over the key, isDown indicates whether the key is
currently pressed down.
When doing this, be sure to note the keyboard's orientation.
*/
virtual void drawBlackNote (int midiNoteNumber,
Graphics& g, Rectangle<float> area,
bool isDown, bool isOver,
Colour noteFillColour);
/** Allows text to be drawn on the white notes.
By default this is used to label the C in each octave, but could be used for other things.
@see setOctaveForMiddleC
*/
virtual String getWhiteNoteText (int midiNoteNumber);
/** Draws the up and down buttons that scroll the keyboard up/down in octaves. */
virtual void drawUpDownButton (Graphics& g, int w, int h,
bool isMouseOver,
bool isButtonPressed,
bool movesOctavesUp);
/** Callback when the mouse is clicked on a key.
You could use this to do things like handle right-clicks on keys, etc.
Return true if you want the click to trigger the note, or false if you
want to handle it yourself and not have the note played.
@see mouseDraggedToKey
*/
virtual bool mouseDownOnKey (int midiNoteNumber, const MouseEvent& e);
/** Callback when the mouse is dragged from one key onto another.
Return true if you want the drag to trigger the new note, or false if you
want to handle it yourself and not have the note played.
@see mouseDownOnKey
*/
virtual bool mouseDraggedToKey (int midiNoteNumber, const MouseEvent& e);
/** Callback when the mouse is released from a key.
@see mouseDownOnKey
*/
virtual void mouseUpOnKey (int midiNoteNumber, const MouseEvent& e);
/** Calculates the position of a given midi-note.
This can be overridden to create layouts with custom key-widths.
@param midiNoteNumber the note to find
@param keyWidth the desired width in pixels of one key - see setKeyWidth()
@returns the start and length of the key along the axis of the keyboard
*/
virtual Range<float> getKeyPosition (int midiNoteNumber, float keyWidth) const;
/** Returns the rectangle for a given key if within the displayable range */
Rectangle<float> getRectangleForKey (int midiNoteNumber) const;
private:
//==============================================================================
struct UpDownButton;
MidiKeyboardState& state;
float blackNoteLengthRatio = 0.7f;
float blackNoteWidthRatio = 0.7f;
float xOffset = 0;
float keyWidth = 16.0f;
int scrollButtonWidth = 12;
Orientation orientation;
int midiChannel = 1, midiInChannelMask = 0xffff;
float velocity = 1.0f;
Array<int> mouseOverNotes, mouseDownNotes;
BigInteger keysPressed, keysCurrentlyDrawnDown;
std::atomic<bool> noPendingUpdates { true };
int rangeStart = 0, rangeEnd = 127;
float firstKey = 12 * 4.0f;
bool canScroll = true, useMousePositionForVelocity = true;
std::unique_ptr<Button> scrollDown, scrollUp;
Array<KeyPress> keyPresses;
Array<int> keyPressNotes;
int keyMappingOctave = 6, octaveNumForMiddleC = 3;
Range<float> getKeyPos (int midiNoteNumber) const;
int xyToNote (Point<float>, float& mousePositionVelocity);
int remappedXYToNote (Point<float>, float& mousePositionVelocity) const;
void resetAnyKeysInUse();
void updateNoteUnderMouse (Point<float>, bool isDown, int fingerNum);
void updateNoteUnderMouse (const MouseEvent&, bool isDown);
void repaintNote (int midiNoteNumber);
void setLowestVisibleKeyFloat (float noteNumber);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardComponent)
};
} // namespace juce