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:
89
deps/juce/modules/juce_audio_utils/gui/juce_AudioAppComponent.cpp
vendored
Normal file
89
deps/juce/modules/juce_audio_utils/gui/juce_AudioAppComponent.cpp
vendored
Normal 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
|
135
deps/juce/modules/juce_audio_utils/gui/juce_AudioAppComponent.h
vendored
Normal file
135
deps/juce/modules/juce_audio_utils/gui/juce_AudioAppComponent.h
vendored
Normal 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
|
1338
deps/juce/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp
vendored
Normal file
1338
deps/juce/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
121
deps/juce/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h
vendored
Normal file
121
deps/juce/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h
vendored
Normal 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
|
826
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnail.cpp
vendored
Normal file
826
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnail.cpp
vendored
Normal 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
|
222
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnail.h
vendored
Normal file
222
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnail.h
vendored
Normal 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
|
158
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailBase.h
vendored
Normal file
158
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailBase.h
vendored
Normal 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
|
197
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailCache.cpp
vendored
Normal file
197
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailCache.cpp
vendored
Normal 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
|
118
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailCache.h
vendored
Normal file
118
deps/juce/modules/juce_audio_utils/gui/juce_AudioThumbnailCache.h
vendored
Normal 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
|
222
deps/juce/modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.cpp
vendored
Normal file
222
deps/juce/modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.cpp
vendored
Normal 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
|
133
deps/juce/modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h
vendored
Normal file
133
deps/juce/modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h
vendored
Normal 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
|
83
deps/juce/modules/juce_audio_utils/gui/juce_BluetoothMidiDevicePairingDialogue.h
vendored
Normal file
83
deps/juce/modules/juce_audio_utils/gui/juce_BluetoothMidiDevicePairingDialogue.h
vendored
Normal 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
|
912
deps/juce/modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.cpp
vendored
Normal file
912
deps/juce/modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.cpp
vendored
Normal 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
|
445
deps/juce/modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.h
vendored
Normal file
445
deps/juce/modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.h
vendored
Normal 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
|
Reference in New Issue
Block a user