fix macOS build (following Projucer changes made in Windows, which removed /Applications/JUCE/modules from its headers). move JUCE headers under source control, so that Windows and macOS can both build against same version of JUCE. remove AUv3 target (I think it's an iOS thing, so it will never work with this macOS fluidsynth dylib).
This commit is contained in:
174
modules/juce_audio_utils/audio_cd/juce_AudioCDBurner.h
Normal file
174
modules/juce_audio_utils/audio_cd/juce_AudioCDBurner.h
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
#if JUCE_USE_CDBURNER || DOXYGEN
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class AudioCDBurner : public ChangeBroadcaster
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Returns a list of available optical drives.
|
||||
|
||||
Use openDevice() to open one of the items from this list.
|
||||
*/
|
||||
static StringArray findAvailableDevices();
|
||||
|
||||
/** Tries to open one of the optical drives.
|
||||
|
||||
The deviceIndex is an index into the array returned by findAvailableDevices().
|
||||
*/
|
||||
static AudioCDBurner* openDevice (const int deviceIndex);
|
||||
|
||||
/** Destructor. */
|
||||
~AudioCDBurner();
|
||||
|
||||
//==============================================================================
|
||||
enum DiskState
|
||||
{
|
||||
unknown, /**< An error condition, if the device isn't responding. */
|
||||
trayOpen, /**< The drive is currently open. Note that a slot-loading drive
|
||||
may seem to be permanently open. */
|
||||
noDisc, /**< The drive has no disk in it. */
|
||||
writableDiskPresent, /**< The drive contains a writeable disk. */
|
||||
readOnlyDiskPresent /**< The drive contains a read-only disk. */
|
||||
};
|
||||
|
||||
/** Returns the current status of the device.
|
||||
|
||||
To get informed when the drive's status changes, attach a ChangeListener to
|
||||
the AudioCDBurner.
|
||||
*/
|
||||
DiskState getDiskState() const;
|
||||
|
||||
/** Returns true if there's a writable disk in the drive. */
|
||||
bool isDiskPresent() const;
|
||||
|
||||
/** Sends an eject signal to the drive.
|
||||
The eject will happen asynchronously, so you can use getDiskState() and
|
||||
waitUntilStateChange() to monitor its progress.
|
||||
*/
|
||||
bool openTray();
|
||||
|
||||
/** Blocks the current thread until the drive's state changes, or until the timeout expires.
|
||||
@returns the device's new state
|
||||
*/
|
||||
DiskState waitUntilStateChange (int timeOutMilliseconds);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the set of possible write speeds that the device can handle.
|
||||
These are as a multiple of 'normal' speed, so e.g. '24x' returns 24, etc.
|
||||
Note that if there's no media present in the drive, this value may be unavailable!
|
||||
@see setWriteSpeed, getWriteSpeed
|
||||
*/
|
||||
Array<int> getAvailableWriteSpeeds() const;
|
||||
|
||||
//==============================================================================
|
||||
/** Tries to enable or disable buffer underrun safety on devices that support it.
|
||||
@returns true if it's now enabled. If the device doesn't support it, this
|
||||
will always return false.
|
||||
*/
|
||||
bool setBufferUnderrunProtection (bool shouldBeEnabled);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the number of free blocks on the disk.
|
||||
|
||||
There are 75 blocks per second, at 44100Hz.
|
||||
*/
|
||||
int getNumAvailableAudioBlocks() const;
|
||||
|
||||
/** Adds a track to be written.
|
||||
|
||||
The source passed-in here will be kept by this object, and it will
|
||||
be used and deleted at some point in the future, either during the
|
||||
burn() method or when this AudioCDBurner object is deleted. Your caller
|
||||
method shouldn't keep a reference to it or use it again after passing
|
||||
it in here.
|
||||
*/
|
||||
bool addAudioTrack (AudioSource* source, int numSamples);
|
||||
|
||||
//==============================================================================
|
||||
/** Receives progress callbacks during a cd-burn operation.
|
||||
@see AudioCDBurner::burn()
|
||||
*/
|
||||
class BurnProgressListener
|
||||
{
|
||||
public:
|
||||
BurnProgressListener() noexcept {}
|
||||
virtual ~BurnProgressListener() {}
|
||||
|
||||
/** Called at intervals to report on the progress of the AudioCDBurner.
|
||||
|
||||
To cancel the burn, return true from this method.
|
||||
*/
|
||||
virtual bool audioCDBurnProgress (float proportionComplete) = 0;
|
||||
};
|
||||
|
||||
/** Runs the burn process.
|
||||
This method will block until the operation is complete.
|
||||
|
||||
@param listener the object to receive callbacks about progress
|
||||
@param ejectDiscAfterwards whether to eject the disk after the burn completes
|
||||
@param performFakeBurnForTesting if true, no data will actually be written to the disk
|
||||
@param writeSpeed one of the write speeds from getAvailableWriteSpeeds(), or
|
||||
0 or less to mean the fastest speed.
|
||||
*/
|
||||
String burn (BurnProgressListener* listener,
|
||||
bool ejectDiscAfterwards,
|
||||
bool performFakeBurnForTesting,
|
||||
int writeSpeed);
|
||||
|
||||
/** If a burn operation is currently in progress, this tells it to stop
|
||||
as soon as possible.
|
||||
|
||||
It's also possible to stop the burn process by returning true from
|
||||
BurnProgressListener::audioCDBurnProgress()
|
||||
*/
|
||||
void abortBurn();
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioCDBurner (const int deviceIndex);
|
||||
|
||||
class Pimpl;
|
||||
friend struct ContainerDeletePolicy<Pimpl>;
|
||||
std::unique_ptr<Pimpl> pimpl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioCDBurner)
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
64
modules/juce_audio_utils/audio_cd/juce_AudioCDReader.cpp
Normal file
64
modules/juce_audio_utils/audio_cd/juce_AudioCDReader.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
#if JUCE_USE_CDREADER
|
||||
|
||||
int AudioCDReader::getNumTracks() const
|
||||
{
|
||||
return trackStartSamples.size() - 1;
|
||||
}
|
||||
|
||||
int AudioCDReader::getPositionOfTrackStart (int trackNum) const
|
||||
{
|
||||
return trackStartSamples [trackNum];
|
||||
}
|
||||
|
||||
const Array<int>& AudioCDReader::getTrackOffsets() const
|
||||
{
|
||||
return trackStartSamples;
|
||||
}
|
||||
|
||||
int AudioCDReader::getCDDBId()
|
||||
{
|
||||
int checksum = 0;
|
||||
const int numTracks = getNumTracks();
|
||||
|
||||
for (int i = 0; i < numTracks; ++i)
|
||||
for (int offset = (trackStartSamples.getUnchecked(i) + 88200) / 44100; offset > 0; offset /= 10)
|
||||
checksum += offset % 10;
|
||||
|
||||
const int length = (trackStartSamples.getLast() - trackStartSamples.getFirst()) / 44100;
|
||||
|
||||
// CCLLLLTT: checksum, length, tracks
|
||||
return ((checksum & 0xff) << 24) | (length << 8) | numTracks;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
179
modules/juce_audio_utils/audio_cd/juce_AudioCDReader.h
Normal file
179
modules/juce_audio_utils/audio_cd/juce_AudioCDReader.h
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
#if JUCE_USE_CDREADER || DOXYGEN
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A type of AudioFormatReader that reads from an audio CD.
|
||||
|
||||
One of these can be used to read a CD as if it's one big audio stream. Use the
|
||||
getPositionOfTrackStart() method to find where the individual tracks are
|
||||
within the stream.
|
||||
|
||||
@see AudioFormatReader
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API AudioCDReader : public AudioFormatReader
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Returns a list of names of Audio CDs currently available for reading.
|
||||
|
||||
If there's a CD drive but no CD in it, this might return an empty list, or
|
||||
possibly a device that can be opened but which has no tracks, depending
|
||||
on the platform.
|
||||
|
||||
@see createReaderForCD
|
||||
*/
|
||||
static StringArray getAvailableCDNames();
|
||||
|
||||
/** Tries to create an AudioFormatReader that can read from an Audio CD.
|
||||
|
||||
@param index the index of one of the available CDs - use getAvailableCDNames()
|
||||
to find out how many there are.
|
||||
@returns a new AudioCDReader object, or nullptr if it couldn't be created. The
|
||||
caller will be responsible for deleting the object returned.
|
||||
*/
|
||||
static AudioCDReader* createReaderForCD (const int index);
|
||||
|
||||
//==============================================================================
|
||||
/** Destructor. */
|
||||
~AudioCDReader();
|
||||
|
||||
/** Implementation of the AudioFormatReader method. */
|
||||
bool readSamples (int** destSamples, int numDestChannels, int startOffsetInDestBuffer,
|
||||
int64 startSampleInFile, int numSamples) override;
|
||||
|
||||
/** Checks whether the CD has been removed from the drive. */
|
||||
bool isCDStillPresent() const;
|
||||
|
||||
/** Returns the total number of tracks (audio + data). */
|
||||
int getNumTracks() const;
|
||||
|
||||
/** Finds the sample offset of the start of a track.
|
||||
@param trackNum the track number, where trackNum = 0 is the first track
|
||||
and trackNum = getNumTracks() means the end of the CD.
|
||||
*/
|
||||
int getPositionOfTrackStart (int trackNum) const;
|
||||
|
||||
/** Returns true if a given track is an audio track.
|
||||
@param trackNum the track number, where 0 is the first track.
|
||||
*/
|
||||
bool isTrackAudio (int trackNum) const;
|
||||
|
||||
/** Returns an array of sample offsets for the start of each track, followed by
|
||||
the sample position of the end of the CD.
|
||||
*/
|
||||
const Array<int>& getTrackOffsets() const;
|
||||
|
||||
/** Refreshes the object's table of contents.
|
||||
|
||||
If the disc has been ejected and a different one put in since this
|
||||
object was created, this will cause it to update its idea of how many tracks
|
||||
there are, etc.
|
||||
*/
|
||||
void refreshTrackLengths();
|
||||
|
||||
/** Enables scanning for indexes within tracks.
|
||||
@see getLastIndex
|
||||
*/
|
||||
void enableIndexScanning (bool enabled);
|
||||
|
||||
/** Returns the index number found during the last read() call.
|
||||
|
||||
Index scanning is turned off by default - turn it on with enableIndexScanning().
|
||||
|
||||
Then when the read() method is called, if it comes across an index within that
|
||||
block, the index number is stored and returned by this method.
|
||||
|
||||
Some devices might not support indexes, of course.
|
||||
|
||||
(If you don't know what CD indexes are, it's unlikely you'll ever need them).
|
||||
|
||||
@see enableIndexScanning
|
||||
*/
|
||||
int getLastIndex() const;
|
||||
|
||||
/** Scans a track to find the position of any indexes within it.
|
||||
@param trackNumber the track to look in, where 0 is the first track on the disc
|
||||
@returns an array of sample positions of any index points found (not including
|
||||
the index that marks the start of the track)
|
||||
*/
|
||||
Array<int> findIndexesInTrack (const int trackNumber);
|
||||
|
||||
/** Returns the CDDB id number for the CD.
|
||||
It's not a great way of identifying a disc, but it's traditional.
|
||||
*/
|
||||
int getCDDBId();
|
||||
|
||||
/** Tries to eject the disk.
|
||||
Ejecting the disk might not actually be possible, e.g. if some other process is using it.
|
||||
*/
|
||||
void ejectDisk();
|
||||
|
||||
//==============================================================================
|
||||
enum
|
||||
{
|
||||
framesPerSecond = 75,
|
||||
samplesPerFrame = 44100 / framesPerSecond
|
||||
};
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Array<int> trackStartSamples;
|
||||
|
||||
#if JUCE_MAC
|
||||
File volumeDir;
|
||||
Array<File> tracks;
|
||||
int currentReaderTrack;
|
||||
std::unique_ptr<AudioFormatReader> reader;
|
||||
AudioCDReader (const File& volume);
|
||||
|
||||
#elif JUCE_WINDOWS
|
||||
bool audioTracks [100];
|
||||
void* handle;
|
||||
MemoryBlock buffer;
|
||||
bool indexingEnabled;
|
||||
int lastIndex, firstFrameInBuffer, samplesInBuffer;
|
||||
AudioCDReader (void* handle);
|
||||
int getIndexAt (int samplePos);
|
||||
|
||||
#elif JUCE_LINUX
|
||||
AudioCDReader();
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioCDReader)
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
91
modules/juce_audio_utils/gui/juce_AudioAppComponent.cpp
Normal file
91
modules/juce_audio_utils/gui/juce_AudioAppComponent.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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)
|
||||
{
|
||||
AudioDeviceManager::AudioDeviceSetup setup;
|
||||
deviceManager.getAudioDeviceSetup (setup);
|
||||
|
||||
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
|
136
modules/juce_audio_utils/gui/juce_AudioAppComponent.h
Normal file
136
modules/juce_audio_utils/gui/juce_AudioAppComponent.h
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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();
|
||||
|
||||
/** 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 'unpreprared'
|
||||
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
|
||||
*/
|
||||
virtual void prepareToPlay (int samplesPerBlockExpected,
|
||||
double sampleRate) = 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
|
||||
*/
|
||||
virtual void releaseResources() = 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
|
||||
*/
|
||||
virtual void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) = 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
|
1197
modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp
Normal file
1197
modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp
Normal file
File diff suppressed because it is too large
Load Diff
123
modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h
Normal file
123
modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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();
|
||||
|
||||
/** 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;
|
||||
/** @internal */
|
||||
void timerCallback() override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
std::unique_ptr<ComboBox> deviceTypeDropDown;
|
||||
std::unique_ptr<Label> deviceTypeDropDownLabel;
|
||||
std::unique_ptr<Component> audioDeviceSettingsComp;
|
||||
String audioDeviceSettingsCompType;
|
||||
int itemHeight;
|
||||
const int minOutputChannels, maxOutputChannels, minInputChannels, maxInputChannels;
|
||||
const bool showChannelsAsStereoPairs;
|
||||
const bool hideAdvancedOptionsWithButton;
|
||||
|
||||
class MidiInputSelectorComponentListBox;
|
||||
friend struct ContainerDeletePolicy<MidiInputSelectorComponentListBox>;
|
||||
std::unique_ptr<MidiInputSelectorComponentListBox> midiInputsList;
|
||||
std::unique_ptr<ComboBox> midiOutputSelector;
|
||||
std::unique_ptr<Label> midiInputsLabel, midiOutputLabel;
|
||||
std::unique_ptr<TextButton> bluetoothButton;
|
||||
|
||||
void handleBluetoothButton();
|
||||
void updateDeviceType();
|
||||
void updateMidiOutput();
|
||||
void changeListenerCallback (ChangeBroadcaster*) override;
|
||||
void updateAllControls();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceSelectorComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
826
modules/juce_audio_utils/gui/juce_AudioThumbnail.cpp
Normal file
826
modules/juce_audio_utils/gui/juce_AudioThumbnail.cpp
Normal file
@ -0,0 +1,826 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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()
|
||||
{
|
||||
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;
|
||||
uint32 lastReaderUseTime = 0;
|
||||
|
||||
void createReader()
|
||||
{
|
||||
if (reader == nullptr && source != nullptr)
|
||||
if (auto* audioFileStream = source->createInputStream())
|
||||
reader.reset (owner.formatManagerToUse.createReaderFor (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)
|
||||
{
|
||||
jassert (MessageManager::getInstance()->currentThreadHasLockedMessageManager());
|
||||
|
||||
numSamplesFinished = 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
source.reset (newSource); // (make sure this isn't done before loadThumb is called)
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
source->initialise (numSamplesFinished);
|
||||
|
||||
totalSamples = source->lengthInSamples;
|
||||
sampleRate = source->sampleRate;
|
||||
numChannels = (int32) source->numChannels;
|
||||
|
||||
createChannels (1 + (int) (totalSamples / samplesPerThumbSample));
|
||||
}
|
||||
|
||||
return sampleRate > 0 && totalSamples > 0;
|
||||
}
|
||||
|
||||
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);
|
||||
window->invalidate();
|
||||
sendChangeMessage();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int AudioThumbnail::getNumChannels() const noexcept
|
||||
{
|
||||
return numChannels;
|
||||
}
|
||||
|
||||
double AudioThumbnail::getTotalLength() const noexcept
|
||||
{
|
||||
return sampleRate > 0 ? (totalSamples / sampleRate) : 0;
|
||||
}
|
||||
|
||||
bool AudioThumbnail::isFullyLoaded() const noexcept
|
||||
{
|
||||
return numSamplesFinished >= totalSamples - samplesPerThumbSample;
|
||||
}
|
||||
|
||||
double AudioThumbnail::getProportionComplete() const noexcept
|
||||
{
|
||||
return jlimit (0.0, 1.0, numSamplesFinished / (double) jmax ((int64) 1, totalSamples));
|
||||
}
|
||||
|
||||
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 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
|
229
modules/juce_audio_utils/gui/juce_AudioThumbnail.h
Normal file
229
modules/juce_audio_utils/gui/juce_AudioThumbnail.h
Normal file
@ -0,0 +1,229 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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();
|
||||
|
||||
//==============================================================================
|
||||
/** 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;
|
||||
|
||||
friend class LevelDataSource;
|
||||
friend class ThumbData;
|
||||
friend class CachedWindow;
|
||||
friend struct ContainerDeletePolicy<LevelDataSource>;
|
||||
friend struct ContainerDeletePolicy<ThumbData>;
|
||||
friend struct ContainerDeletePolicy<CachedWindow>;
|
||||
|
||||
std::unique_ptr<LevelDataSource> source;
|
||||
std::unique_ptr<CachedWindow> window;
|
||||
OwnedArray<ThumbData> channels;
|
||||
|
||||
int32 samplesPerThumbSample = 0;
|
||||
int64 totalSamples = 0, 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
|
159
modules/juce_audio_utils/gui/juce_AudioThumbnailBase.h
Normal file
159
modules/juce_audio_utils/gui/juce_AudioThumbnailBase.h
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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() {}
|
||||
virtual ~AudioThumbnailBase() {}
|
||||
|
||||
//==============================================================================
|
||||
/** 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
|
198
modules/juce_audio_utils/gui/juce_AudioThumbnailCache.cpp
Normal file
198
modules/juce_audio_utils/gui/juce_AudioThumbnailCache.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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 inline 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
|
120
modules/juce_audio_utils/gui/juce_AudioThumbnailCache.h
Normal file
120
modules/juce_audio_utils/gui/juce_AudioThumbnailCache.h
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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;
|
||||
friend struct ContainerDeletePolicy<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
|
227
modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.cpp
Normal file
227
modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.cpp
Normal file
@ -0,0 +1,227 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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), nextSample (0), subSample (0)
|
||||
{
|
||||
setBufferSize (bufferSize);
|
||||
clear();
|
||||
}
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
for (int i = 0; i < levels.size(); ++i)
|
||||
levels.getReference(i) = Range<float>();
|
||||
|
||||
value = Range<float>();
|
||||
subSample = 0;
|
||||
}
|
||||
|
||||
void pushSamples (const float* inputSamples, const int num) noexcept
|
||||
{
|
||||
for (int i = 0; i < num; ++i)
|
||||
pushSample (inputSamples[i]);
|
||||
}
|
||||
|
||||
void pushSample (const float newSample) noexcept
|
||||
{
|
||||
if (--subSample <= 0)
|
||||
{
|
||||
nextSample %= levels.size();
|
||||
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, Range<float>(), newSize - levels.size());
|
||||
|
||||
if (nextSample >= newSize)
|
||||
nextSample = 0;
|
||||
}
|
||||
|
||||
AudioVisualiserComponent& owner;
|
||||
Array<Range<float>> levels;
|
||||
Range<float> value;
|
||||
int nextSample, subSample;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelInfo)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
AudioVisualiserComponent::AudioVisualiserComponent (const int initialNumChannels)
|
||||
: numSamples (1024),
|
||||
inputSamplesPerBlock (256),
|
||||
backgroundColour (Colours::black),
|
||||
waveformColour (Colours::white)
|
||||
{
|
||||
setOpaque (true);
|
||||
setNumChannels (initialNumChannels);
|
||||
setRepaintRate (60);
|
||||
}
|
||||
|
||||
AudioVisualiserComponent::~AudioVisualiserComponent()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioVisualiserComponent::setNumChannels (const 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 (int i = 0; i < channels.size(); ++i)
|
||||
channels.getUnchecked(i)->setBufferSize (newNumSamples);
|
||||
}
|
||||
|
||||
void AudioVisualiserComponent::clear()
|
||||
{
|
||||
for (int i = 0; i < channels.size(); ++i)
|
||||
channels.getUnchecked(i)->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)
|
||||
{
|
||||
const int 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);
|
||||
|
||||
Rectangle<float> r (getLocalBounds().toFloat());
|
||||
const float channelHeight = r.getHeight() / channels.size();
|
||||
|
||||
g.setColour (waveformColour);
|
||||
|
||||
for (int i = 0; i < channels.size(); ++i)
|
||||
{
|
||||
const ChannelInfo& c = *channels.getUnchecked(i);
|
||||
|
||||
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)
|
||||
{
|
||||
const float 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
|
136
modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h
Normal file
136
modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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();
|
||||
|
||||
/** 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;
|
||||
friend struct ChannelInfo;
|
||||
friend struct ContainerDeletePolicy<ChannelInfo>;
|
||||
|
||||
OwnedArray<ChannelInfo> channels;
|
||||
int numSamples, inputSamplesPerBlock;
|
||||
Colour backgroundColour, waveformColour;
|
||||
|
||||
void timerCallback() override;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioVisualiserComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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 iOS and Android. On OSX,
|
||||
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
|
918
modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.cpp
Normal file
918
modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.cpp
Normal file
@ -0,0 +1,918 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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);
|
||||
}
|
||||
|
||||
void paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown) override
|
||||
{
|
||||
owner.drawUpDownButton (g, getWidth(), getHeight(),
|
||||
isMouseOverButton, isButtonDown,
|
||||
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::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;
|
||||
shouldCheckState = true;
|
||||
}
|
||||
|
||||
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 = 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 { getWidth() - blackNoteLength, x, blackNoteLength, w };
|
||||
case verticalKeyboardFacingRight: return { 0, 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, 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, getWidth() - p.y };
|
||||
else
|
||||
p = { 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 = width - 1.0f;
|
||||
x2 = 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 (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, height - 1.0f, x, 1.0f); break;
|
||||
case verticalKeyboardFacingLeft: g.fillRect (0.0f, 0.0f, 1.0f, x); break;
|
||||
case verticalKeyboardFacingRight: g.fillRect (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, w - 2.0f, 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 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 <= 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 - 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() > w);
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MidiKeyboardComponent::handleNoteOn (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
||||
{
|
||||
shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
|
||||
}
|
||||
|
||||
void MidiKeyboardComponent::handleNoteOff (MidiKeyboardState*, int /*midiChannel*/, int /*midiNoteNumber*/, float /*velocity*/)
|
||||
{
|
||||
shouldCheckState = true; // (probably being called from the audio thread, so avoid blocking in here)
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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 : 1.0f;
|
||||
|
||||
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);
|
||||
shouldCheckMousePos = 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; }
|
||||
void MidiKeyboardComponent::mouseDraggedToKey (int, const MouseEvent&) {}
|
||||
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);
|
||||
shouldCheckMousePos = true;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiKeyboardComponent::mouseUp (const MouseEvent& e)
|
||||
{
|
||||
updateNoteUnderMouse (e, false);
|
||||
shouldCheckMousePos = 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 (shouldCheckState)
|
||||
{
|
||||
shouldCheckState = false;
|
||||
|
||||
for (int i = rangeStart; i <= rangeEnd; ++i)
|
||||
{
|
||||
bool isOn = state.isNoteOnForChannels (midiInChannelMask, i);
|
||||
|
||||
if (keysCurrentlyDrawnDown[i] != isOn)
|
||||
{
|
||||
keysCurrentlyDrawnDown.setBit (i, isOn);
|
||||
repaintNote (i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheckMousePos)
|
||||
{
|
||||
for (auto& ms : Desktop::getInstance().getMouseSources())
|
||||
if (ms.getComponentUnderMouse() == this || isParentOf (ms.getComponentUnderMouse()))
|
||||
updateNoteUnderMouse (getLocalPoint (nullptr, ms.getScreenPosition()), ms.isDragging(), ms.getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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
|
443
modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.h
Normal file
443
modules/juce_audio_utils/gui/juce_MidiKeyboardComponent.h
Normal file
@ -0,0 +1,443 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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 MidiKeyboardStateListener,
|
||||
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 horizonal or vertical
|
||||
*/
|
||||
MidiKeyboardComponent (MidiKeyboardState& state,
|
||||
Orientation orientation);
|
||||
|
||||
/** Destructor. */
|
||||
~MidiKeyboardComponent();
|
||||
|
||||
//==============================================================================
|
||||
/** 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);
|
||||
|
||||
//==============================================================================
|
||||
/** 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.
|
||||
@see mouseDownOnKey
|
||||
*/
|
||||
virtual void 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;
|
||||
bool shouldCheckState = false;
|
||||
|
||||
int rangeStart = 0, rangeEnd = 127;
|
||||
float firstKey = 12 * 4.0f;
|
||||
bool canScroll = true, useMousePositionForVelocity = true, shouldCheckMousePos = false;
|
||||
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);
|
||||
|
||||
#if JUCE_CATCH_DEPRECATED_CODE_MISUSE
|
||||
// Note that the parameters for these method have changed
|
||||
virtual int getKeyPosition (int, float, int&, int&) const { return 0; }
|
||||
virtual int drawWhiteNote (int, Graphics&, int, int, int, int, bool, bool, const Colour&, const Colour&) { return 0; }
|
||||
virtual int drawBlackNote (int, Graphics&, int, int, int, int, bool, bool, const Colour&) { return 0; }
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardComponent)
|
||||
};
|
||||
|
||||
} // namespace juce
|
102
modules/juce_audio_utils/juce_audio_utils.cpp
Normal file
102
modules/juce_audio_utils/juce_audio_utils.cpp
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#ifdef JUCE_AUDIO_UTILS_H_INCLUDED
|
||||
/* When you add this cpp file to your project, you mustn't include it in a file where you've
|
||||
already included any other headers - just put it inside a file on its own, possibly with your config
|
||||
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
|
||||
header files that the compiler may be using.
|
||||
*/
|
||||
#error "Incorrect use of JUCE cpp file"
|
||||
#endif
|
||||
|
||||
#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1
|
||||
#define JUCE_CORE_INCLUDE_JNI_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_COM_SMART_PTR 1
|
||||
|
||||
#include "juce_audio_utils.h"
|
||||
#include <juce_gui_extra/juce_gui_extra.h>
|
||||
|
||||
#if JUCE_MAC
|
||||
#import <DiscRecording/DiscRecording.h>
|
||||
#elif JUCE_WINDOWS
|
||||
#if JUCE_USE_CDBURNER
|
||||
/* You'll need the Platform SDK for these headers - if you don't have it and don't
|
||||
need to use CD-burning, then you might just want to set the JUCE_USE_CDBURNER flag
|
||||
to 0, to avoid these includes.
|
||||
*/
|
||||
#include <imapi.h>
|
||||
#include <imapierror.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "gui/juce_AudioDeviceSelectorComponent.cpp"
|
||||
#include "gui/juce_AudioThumbnail.cpp"
|
||||
#include "gui/juce_AudioThumbnailCache.cpp"
|
||||
#include "gui/juce_AudioVisualiserComponent.cpp"
|
||||
#include "gui/juce_MidiKeyboardComponent.cpp"
|
||||
#include "gui/juce_AudioAppComponent.cpp"
|
||||
#include "players/juce_SoundPlayer.cpp"
|
||||
#include "players/juce_AudioProcessorPlayer.cpp"
|
||||
#include "audio_cd/juce_AudioCDReader.cpp"
|
||||
|
||||
#if JUCE_MAC
|
||||
#include "native/juce_mac_BluetoothMidiDevicePairingDialogue.mm"
|
||||
|
||||
#if JUCE_USE_CDREADER
|
||||
#include "native/juce_mac_AudioCDReader.mm"
|
||||
#endif
|
||||
|
||||
#if JUCE_USE_CDBURNER
|
||||
#include "native/juce_mac_AudioCDBurner.mm"
|
||||
#endif
|
||||
|
||||
#elif JUCE_IOS
|
||||
#include "native/juce_ios_BluetoothMidiDevicePairingDialogue.mm"
|
||||
|
||||
#elif JUCE_ANDROID
|
||||
#include "native/juce_android_BluetoothMidiDevicePairingDialogue.cpp"
|
||||
|
||||
#elif JUCE_LINUX
|
||||
#if JUCE_USE_CDREADER
|
||||
#include "native/juce_linux_AudioCDReader.cpp"
|
||||
#endif
|
||||
|
||||
#include "native/juce_linux_BluetoothMidiDevicePairingDialogue.cpp"
|
||||
|
||||
#elif JUCE_WINDOWS
|
||||
#include "native/juce_win_BluetoothMidiDevicePairingDialogue.cpp"
|
||||
|
||||
#if JUCE_USE_CDREADER
|
||||
#include "native/juce_win32_AudioCDReader.cpp"
|
||||
#endif
|
||||
|
||||
#if JUCE_USE_CDBURNER
|
||||
#include "native/juce_win32_AudioCDBurner.cpp"
|
||||
#endif
|
||||
|
||||
#endif
|
88
modules/juce_audio_utils/juce_audio_utils.h
Normal file
88
modules/juce_audio_utils/juce_audio_utils.h
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this module, and is read by
|
||||
the Projucer to automatically generate project code that uses it.
|
||||
For details about the syntax and how to create or use a module, see the
|
||||
JUCE Module Format.txt file.
|
||||
|
||||
|
||||
BEGIN_JUCE_MODULE_DECLARATION
|
||||
|
||||
ID: juce_audio_utils
|
||||
vendor: juce
|
||||
version: 5.3.2
|
||||
name: JUCE extra audio utility classes
|
||||
description: Classes for audio-related GUI and miscellaneous tasks.
|
||||
website: http://www.juce.com/juce
|
||||
license: GPL/Commercial
|
||||
|
||||
dependencies: juce_gui_extra, juce_audio_processors, juce_audio_formats, juce_audio_devices
|
||||
OSXFrameworks: DiscRecording
|
||||
iOSFrameworks: CoreAudioKit
|
||||
|
||||
END_JUCE_MODULE_DECLARATION
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
#define JUCE_AUDIO_UTILS_H_INCLUDED
|
||||
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
#include <juce_audio_devices/juce_audio_devices.h>
|
||||
#include <juce_audio_formats/juce_audio_formats.h>
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
|
||||
//==============================================================================
|
||||
/** Config: JUCE_USE_CDREADER
|
||||
Enables the AudioCDReader class (on supported platforms).
|
||||
*/
|
||||
#ifndef JUCE_USE_CDREADER
|
||||
#define JUCE_USE_CDREADER 0
|
||||
#endif
|
||||
|
||||
/** Config: JUCE_USE_CDBURNER
|
||||
Enables the AudioCDBurner class (on supported platforms).
|
||||
*/
|
||||
#ifndef JUCE_USE_CDBURNER
|
||||
#define JUCE_USE_CDBURNER 0
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
#include "gui/juce_AudioDeviceSelectorComponent.h"
|
||||
#include "gui/juce_AudioThumbnailBase.h"
|
||||
#include "gui/juce_AudioThumbnail.h"
|
||||
#include "gui/juce_AudioThumbnailCache.h"
|
||||
#include "gui/juce_AudioVisualiserComponent.h"
|
||||
#include "gui/juce_MidiKeyboardComponent.h"
|
||||
#include "gui/juce_AudioAppComponent.h"
|
||||
#include "gui/juce_BluetoothMidiDevicePairingDialogue.h"
|
||||
#include "players/juce_SoundPlayer.h"
|
||||
#include "players/juce_AudioProcessorPlayer.h"
|
||||
#include "audio_cd/juce_AudioCDBurner.h"
|
||||
#include "audio_cd/juce_AudioCDReader.h"
|
27
modules/juce_audio_utils/juce_audio_utils.mm
Normal file
27
modules/juce_audio_utils/juce_audio_utils.mm
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "juce_audio_utils.cpp"
|
@ -0,0 +1,509 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
|
||||
METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
|
||||
METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
|
||||
METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
|
||||
METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
|
||||
METHOD (startStopScan, "startStopScan", "(Z)V")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidBluetoothManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager");
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
//==============================================================================
|
||||
struct AndroidBluetoothMidiInterface
|
||||
{
|
||||
static void startStopScan (bool startScanning)
|
||||
{
|
||||
JNIEnv* env = getEnv();
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
|
||||
if (btManager.get() != nullptr)
|
||||
env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.startStopScan, (jboolean) (startScanning ? 1 : 0));
|
||||
}
|
||||
|
||||
static StringArray getBluetoothMidiDevicesNearby()
|
||||
{
|
||||
StringArray retval;
|
||||
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
|
||||
// if this is null then bluetooth is not enabled
|
||||
if (btManager.get() == nullptr)
|
||||
return {};
|
||||
|
||||
jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
|
||||
AndroidBluetoothManager.getMidiBluetoothAddresses);
|
||||
LocalRef<jobjectArray> devices (jDevices);
|
||||
|
||||
const int count = env->GetArrayLength (devices.get());
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
|
||||
retval.add (juceString (string));
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
|
||||
{
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
if (btManager.get() == nullptr)
|
||||
return false;
|
||||
|
||||
jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
|
||||
javaString (bluetoothAddress).get());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
|
||||
{
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
|
||||
if (btManager.get() != nullptr)
|
||||
env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
|
||||
javaString (bluetoothAddress).get());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static String getHumanReadableStringForBluetoothAddress (const String& address)
|
||||
{
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
|
||||
if (btManager.get() == nullptr)
|
||||
return address;
|
||||
|
||||
LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
|
||||
AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
|
||||
javaString (address).get()));
|
||||
|
||||
|
||||
if (string.get() == nullptr)
|
||||
return address;
|
||||
|
||||
return juceString (string);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
enum PairStatus
|
||||
{
|
||||
unpaired = 0,
|
||||
paired = 1,
|
||||
pairing = 2
|
||||
};
|
||||
|
||||
static PairStatus isBluetoothDevicePaired (const String& address)
|
||||
{
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
|
||||
if (btManager.get() == nullptr)
|
||||
return unpaired;
|
||||
|
||||
return static_cast<PairStatus> (env->CallIntMethod (btManager.get(), AndroidBluetoothManager.getBluetoothDeviceStatus,
|
||||
javaString (address).get()));
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct AndroidBluetoothMidiDevice
|
||||
{
|
||||
enum ConnectionStatus
|
||||
{
|
||||
offline,
|
||||
connected,
|
||||
disconnected,
|
||||
connecting,
|
||||
disconnecting
|
||||
};
|
||||
|
||||
AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
|
||||
: name (deviceName), bluetoothAddress (address), connectionStatus (status)
|
||||
{
|
||||
// can't create a device without a valid name and bluetooth address!
|
||||
jassert (! name.isEmpty());
|
||||
jassert (! bluetoothAddress.isEmpty());
|
||||
}
|
||||
|
||||
bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
|
||||
{
|
||||
return bluetoothAddress == other.bluetoothAddress;
|
||||
}
|
||||
|
||||
bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
|
||||
{
|
||||
return ! operator== (other);
|
||||
}
|
||||
|
||||
const String name, bluetoothAddress;
|
||||
ConnectionStatus connectionStatus;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AndroidBluetoothMidiDevicesListBox : public ListBox,
|
||||
private ListBoxModel,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
AndroidBluetoothMidiDevicesListBox()
|
||||
: timerPeriodInMs (1000)
|
||||
{
|
||||
setRowHeight (40);
|
||||
setModel (this);
|
||||
setOutlineThickness (1);
|
||||
startTimer (timerPeriodInMs);
|
||||
}
|
||||
|
||||
void pairDeviceThreadFinished() // callback from PairDeviceThread
|
||||
{
|
||||
updateDeviceList();
|
||||
startTimer (timerPeriodInMs);
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
|
||||
|
||||
int getNumRows() override
|
||||
{
|
||||
return devices.size();
|
||||
}
|
||||
|
||||
void paintListBoxItem (int rowNumber, Graphics& g,
|
||||
int width, int height, bool) override
|
||||
{
|
||||
if (isPositiveAndBelow (rowNumber, devices.size()))
|
||||
{
|
||||
const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
|
||||
const String statusString (getDeviceStatusString (device.connectionStatus));
|
||||
|
||||
g.fillAll (Colours::white);
|
||||
|
||||
const float xmargin = 3.0f;
|
||||
const float ymargin = 3.0f;
|
||||
const float fontHeight = 0.4f * height;
|
||||
const float deviceNameWidth = 0.6f * width;
|
||||
|
||||
g.setFont (fontHeight);
|
||||
|
||||
g.setColour (getDeviceNameFontColour (device.connectionStatus));
|
||||
g.drawText (device.name,
|
||||
Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
|
||||
Justification::topLeft, true);
|
||||
|
||||
g.setColour (getDeviceStatusFontColour (device.connectionStatus));
|
||||
g.drawText (statusString,
|
||||
Rectangle<float> (deviceNameWidth + xmargin, ymargin,
|
||||
width - deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
|
||||
Justification::topRight, true);
|
||||
|
||||
g.setColour (Colours::grey);
|
||||
g.drawHorizontalLine (height - 1, xmargin, width);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
|
||||
{
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::offline)
|
||||
return Colours::grey;
|
||||
|
||||
return Colours::black;
|
||||
}
|
||||
|
||||
static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
|
||||
{
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::offline
|
||||
|| deviceStatus == AndroidBluetoothMidiDevice::connecting
|
||||
|| deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
|
||||
return Colours::grey;
|
||||
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::connected)
|
||||
return Colours::green;
|
||||
|
||||
return Colours::black;
|
||||
}
|
||||
|
||||
static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
|
||||
{
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
|
||||
if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
|
||||
|
||||
// unknown device state!
|
||||
jassertfalse;
|
||||
return "Status unknown";
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void listBoxItemClicked (int row, const MouseEvent&) override
|
||||
{
|
||||
const AndroidBluetoothMidiDevice& device = devices.getReference (row);
|
||||
|
||||
if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
|
||||
disconnectedDeviceClicked (row);
|
||||
|
||||
else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
|
||||
connectedDeviceClicked (row);
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
updateDeviceList();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct PairDeviceThread : public Thread,
|
||||
private AsyncUpdater
|
||||
{
|
||||
PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
|
||||
AndroidBluetoothMidiDevicesListBox& ownerListBox)
|
||||
: Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
|
||||
bluetoothAddress (bluetoothAddressOfDeviceToPair),
|
||||
owner (&ownerListBox)
|
||||
{
|
||||
startThread();
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
if (owner != nullptr)
|
||||
owner->pairDeviceThreadFinished();
|
||||
|
||||
delete this;
|
||||
}
|
||||
|
||||
private:
|
||||
String bluetoothAddress;
|
||||
Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
void disconnectedDeviceClicked (int row)
|
||||
{
|
||||
stopTimer();
|
||||
|
||||
AndroidBluetoothMidiDevice& device = devices.getReference (row);
|
||||
device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
|
||||
updateContent();
|
||||
repaint();
|
||||
|
||||
new PairDeviceThread (device.bluetoothAddress, *this);
|
||||
}
|
||||
|
||||
void connectedDeviceClicked (int row)
|
||||
{
|
||||
AndroidBluetoothMidiDevice& device = devices.getReference (row);
|
||||
device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
|
||||
updateContent();
|
||||
repaint();
|
||||
AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void updateDeviceList()
|
||||
{
|
||||
StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
|
||||
|
||||
Array<AndroidBluetoothMidiDevice> newDevices;
|
||||
|
||||
for (String* address = bluetoothAddresses.begin();
|
||||
address != bluetoothAddresses.end(); ++address)
|
||||
{
|
||||
String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
|
||||
|
||||
DeviceStatus status;
|
||||
switch (AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address))
|
||||
{
|
||||
case AndroidBluetoothMidiInterface::pairing:
|
||||
status = AndroidBluetoothMidiDevice::connecting;
|
||||
break;
|
||||
case AndroidBluetoothMidiInterface::paired:
|
||||
status = AndroidBluetoothMidiDevice::connected;
|
||||
break;
|
||||
default:
|
||||
status = AndroidBluetoothMidiDevice::disconnected;
|
||||
}
|
||||
|
||||
newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
|
||||
}
|
||||
|
||||
devices.swapWith (newDevices);
|
||||
updateContent();
|
||||
repaint();
|
||||
}
|
||||
|
||||
Array<AndroidBluetoothMidiDevice> devices;
|
||||
const int timerPeriodInMs;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class BluetoothMidiSelectorOverlay : public Component
|
||||
{
|
||||
public:
|
||||
BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse,
|
||||
const Rectangle<int>& boundsToUse)
|
||||
: bounds (boundsToUse)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
|
||||
|
||||
AndroidBluetoothMidiInterface::startStopScan (true);
|
||||
|
||||
setAlwaysOnTop (true);
|
||||
setVisible (true);
|
||||
addToDesktop (ComponentPeer::windowHasDropShadow);
|
||||
|
||||
if (bounds.isEmpty())
|
||||
setBounds (0, 0, getParentWidth(), getParentHeight());
|
||||
else
|
||||
setBounds (bounds);
|
||||
|
||||
toFront (true);
|
||||
setOpaque (! bounds.isEmpty());
|
||||
|
||||
addAndMakeVisible (bluetoothDevicesList);
|
||||
enterModalState (true, exitCallback.release(), true);
|
||||
}
|
||||
|
||||
~BluetoothMidiSelectorOverlay()
|
||||
{
|
||||
AndroidBluetoothMidiInterface::startStopScan (false);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (bounds.isEmpty() ? Colours::black.withAlpha (0.6f) : Colours::black);
|
||||
|
||||
g.setColour (Colour (0xffdfdfdf));
|
||||
Rectangle<int> overlayBounds = getOverlayBounds();
|
||||
g.fillRect (overlayBounds);
|
||||
|
||||
g.setColour (Colours::black);
|
||||
g.setFont (16);
|
||||
g.drawText ("Bluetooth MIDI Devices",
|
||||
overlayBounds.removeFromTop (20).reduced (3, 3),
|
||||
Justification::topLeft, true);
|
||||
|
||||
overlayBounds.removeFromTop (2);
|
||||
|
||||
g.setFont (12);
|
||||
g.drawText ("tap to connect/disconnect",
|
||||
overlayBounds.removeFromTop (18).reduced (3, 3),
|
||||
Justification::topLeft, true);
|
||||
}
|
||||
|
||||
void inputAttemptWhenModal() override { exitModalState (0); }
|
||||
void mouseDrag (const MouseEvent&) override {}
|
||||
void mouseDown (const MouseEvent&) override { exitModalState (0); }
|
||||
void resized() override { update(); }
|
||||
void parentSizeChanged() override { update(); }
|
||||
|
||||
private:
|
||||
Rectangle<int> bounds;
|
||||
|
||||
void update()
|
||||
{
|
||||
if (bounds.isEmpty())
|
||||
setBounds (0, 0, getParentWidth(), getParentHeight());
|
||||
else
|
||||
setBounds (bounds);
|
||||
|
||||
bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
|
||||
}
|
||||
|
||||
Rectangle<int> getOverlayBounds() const noexcept
|
||||
{
|
||||
if (bounds.isEmpty())
|
||||
{
|
||||
const int pw = getParentWidth();
|
||||
const int ph = getParentHeight();
|
||||
|
||||
return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
|
||||
jmin (300, ph - 40));
|
||||
}
|
||||
|
||||
return bounds.withZeroOrigin();
|
||||
}
|
||||
|
||||
AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr,
|
||||
Rectangle<int>* btBounds)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackPtr);
|
||||
auto boundsToUse = (btBounds != nullptr ? *btBounds : Rectangle<int> {});
|
||||
|
||||
if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
|
||||
{
|
||||
// If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi.
|
||||
// This is not going to work, boo! The pairing dialogue won't be able to scan for or
|
||||
// find any devices, it will just display an empty list, so don't bother opening it.
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
new BluetoothMidiSelectorOverlay (exitCallback.release(), boundsToUse);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable()
|
||||
{
|
||||
jobject btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
|
||||
return btManager != nullptr;
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#if ! TARGET_IPHONE_SIMULATOR
|
||||
|
||||
#include <CoreAudioKit/CoreAudioKit.h>
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
class BluetoothMidiSelectorOverlay : public Component
|
||||
{
|
||||
public:
|
||||
BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse,
|
||||
const Rectangle<int>& boundsToUse)
|
||||
: bounds (boundsToUse)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
|
||||
|
||||
setAlwaysOnTop (true);
|
||||
setVisible (true);
|
||||
addToDesktop (ComponentPeer::windowHasDropShadow);
|
||||
|
||||
if (bounds.isEmpty())
|
||||
setBounds (0, 0, getParentWidth(), getParentHeight());
|
||||
else
|
||||
setBounds (bounds);
|
||||
|
||||
toFront (true);
|
||||
setOpaque (! bounds.isEmpty());
|
||||
|
||||
controller = [[CABTMIDICentralViewController alloc] init];
|
||||
nativeSelectorComponent.setView ([controller view]);
|
||||
|
||||
addAndMakeVisible (nativeSelectorComponent);
|
||||
|
||||
enterModalState (true, exitCallback.release(), true);
|
||||
}
|
||||
|
||||
~BluetoothMidiSelectorOverlay()
|
||||
{
|
||||
nativeSelectorComponent.setView (nullptr);
|
||||
[controller release];
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (bounds.isEmpty() ? Colours::black.withAlpha (0.5f) : Colours::black);
|
||||
}
|
||||
|
||||
void inputAttemptWhenModal() override { close(); }
|
||||
void mouseDrag (const MouseEvent&) override {}
|
||||
void mouseDown (const MouseEvent&) override { close(); }
|
||||
void resized() override { update(); }
|
||||
void parentSizeChanged() override { update(); }
|
||||
|
||||
private:
|
||||
void update()
|
||||
{
|
||||
if (bounds.isEmpty())
|
||||
{
|
||||
const int pw = getParentWidth();
|
||||
const int ph = getParentHeight();
|
||||
|
||||
nativeSelectorComponent.setBounds (Rectangle<int> (pw, ph)
|
||||
.withSizeKeepingCentre (jmin (400, pw),
|
||||
jmin (450, ph - 40)));
|
||||
}
|
||||
else
|
||||
{
|
||||
nativeSelectorComponent.setBounds (bounds.withZeroOrigin());
|
||||
}
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
exitModalState (0);
|
||||
setVisible (false);
|
||||
}
|
||||
|
||||
CABTMIDICentralViewController* controller;
|
||||
UIViewComponent nativeSelectorComponent;
|
||||
Rectangle<int> bounds;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
|
||||
};
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
|
||||
Rectangle<int>* btBounds)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
|
||||
auto boundsToUse = (btBounds != nullptr ? *btBounds : Rectangle<int> {});
|
||||
|
||||
if (isAvailable())
|
||||
{
|
||||
new BluetoothMidiSelectorOverlay (cb.release(), boundsToUse);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable()
|
||||
{
|
||||
return NSClassFromString ([NSString stringWithUTF8String: "CABTMIDICentralViewController"]) != nil;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
||||
//==============================================================================
|
||||
#else
|
||||
|
||||
namespace juce
|
||||
{
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
|
||||
Rectangle<int>*)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable() { return false; }
|
||||
}
|
||||
|
||||
#endif
|
84
modules/juce_audio_utils/native/juce_linux_AudioCDReader.cpp
Normal file
84
modules/juce_audio_utils/native/juce_linux_AudioCDReader.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
AudioCDReader::AudioCDReader()
|
||||
: AudioFormatReader (0, "CD Audio")
|
||||
{
|
||||
}
|
||||
|
||||
StringArray AudioCDReader::getAvailableCDNames()
|
||||
{
|
||||
StringArray names;
|
||||
return names;
|
||||
}
|
||||
|
||||
AudioCDReader* AudioCDReader::createReaderForCD (const int)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioCDReader::~AudioCDReader()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioCDReader::refreshTrackLengths()
|
||||
{
|
||||
}
|
||||
|
||||
bool AudioCDReader::readSamples (int**, int, int,
|
||||
int64, int)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AudioCDReader::isCDStillPresent() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AudioCDReader::isTrackAudio (int) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AudioCDReader::enableIndexScanning (bool)
|
||||
{
|
||||
}
|
||||
|
||||
int AudioCDReader::getLastIndex() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Array<int> AudioCDReader::findIndexesInTrack (const int)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
|
||||
Rectangle<int>*)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
|
||||
// not implemented on Linux yet!
|
||||
// You should check whether the dialogue is available on your system
|
||||
// using isAvailable() before calling open().
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace juce
|
469
modules/juce_audio_utils/native/juce_mac_AudioCDBurner.mm
Normal file
469
modules/juce_audio_utils/native/juce_mac_AudioCDBurner.mm
Normal file
@ -0,0 +1,469 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
const int kilobytesPerSecond1x = 176;
|
||||
|
||||
struct AudioTrackProducerClass : public ObjCClass <NSObject>
|
||||
{
|
||||
AudioTrackProducerClass() : ObjCClass <NSObject> ("JUCEAudioTrackProducer_")
|
||||
{
|
||||
addIvar<AudioSourceHolder*> ("source");
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
addMethod (@selector (initWithAudioSourceHolder:), initWithAudioSourceHolder, "@@:^v");
|
||||
addMethod (@selector (verifyDataForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:),
|
||||
produceDataForTrack, "I@:@^cIQI^I");
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
addMethod (@selector (cleanupTrackAfterBurn:), cleanupTrackAfterBurn, "v@:@");
|
||||
addMethod (@selector (cleanupTrackAfterVerification:), cleanupTrackAfterVerification, "c@:@");
|
||||
addMethod (@selector (estimateLengthOfTrack:), estimateLengthOfTrack, "Q@:@");
|
||||
addMethod (@selector (prepareTrack:forBurn:toMedia:), prepareTrack, "c@:@@@");
|
||||
addMethod (@selector (prepareTrackForVerification:), prepareTrackForVerification, "c@:@");
|
||||
addMethod (@selector (produceDataForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:),
|
||||
produceDataForTrack, "I@:@^cIQI^I");
|
||||
addMethod (@selector (producePreGapForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:),
|
||||
produceDataForTrack, "I@:@^cIQI^I");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
struct AudioSourceHolder
|
||||
{
|
||||
AudioSourceHolder (AudioSource* s, int numFrames)
|
||||
: source (s), readPosition (0), lengthInFrames (numFrames)
|
||||
{
|
||||
}
|
||||
|
||||
~AudioSourceHolder()
|
||||
{
|
||||
if (source != nullptr)
|
||||
source->releaseResources();
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioSource> source;
|
||||
int readPosition, lengthInFrames;
|
||||
};
|
||||
|
||||
private:
|
||||
static id initWithAudioSourceHolder (id self, SEL, AudioSourceHolder* source)
|
||||
{
|
||||
self = sendSuperclassMessage (self, @selector (init));
|
||||
object_setInstanceVariable (self, "source", source);
|
||||
return self;
|
||||
}
|
||||
|
||||
static AudioSourceHolder* getSource (id self)
|
||||
{
|
||||
return getIvar<AudioSourceHolder*> (self, "source");
|
||||
}
|
||||
|
||||
static void dealloc (id self, SEL)
|
||||
{
|
||||
delete getSource (self);
|
||||
sendSuperclassMessage (self, @selector (dealloc));
|
||||
}
|
||||
|
||||
static void cleanupTrackAfterBurn (id, SEL, DRTrack*) {}
|
||||
static BOOL cleanupTrackAfterVerification (id, SEL, DRTrack*) { return true; }
|
||||
|
||||
static uint64_t estimateLengthOfTrack (id self, SEL, DRTrack*)
|
||||
{
|
||||
return static_cast<uint64_t> (getSource (self)->lengthInFrames);
|
||||
}
|
||||
|
||||
static BOOL prepareTrack (id self, SEL, DRTrack*, DRBurn*, NSDictionary*)
|
||||
{
|
||||
if (AudioSourceHolder* const source = getSource (self))
|
||||
{
|
||||
source->source->prepareToPlay (44100 / 75, 44100);
|
||||
source->readPosition = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static BOOL prepareTrackForVerification (id self, SEL, DRTrack*)
|
||||
{
|
||||
if (AudioSourceHolder* const source = getSource (self))
|
||||
source->source->prepareToPlay (44100 / 75, 44100);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t produceDataForTrack (id self, SEL, DRTrack*, char* buffer,
|
||||
uint32_t bufferLength, uint64_t /*address*/,
|
||||
uint32_t /*blockSize*/, uint32_t* /*flags*/)
|
||||
{
|
||||
if (AudioSourceHolder* const source = getSource (self))
|
||||
{
|
||||
const int numSamples = jmin ((int) bufferLength / 4,
|
||||
(source->lengthInFrames * (44100 / 75)) - source->readPosition);
|
||||
|
||||
if (numSamples > 0)
|
||||
{
|
||||
AudioBuffer<float> tempBuffer (2, numSamples);
|
||||
AudioSourceChannelInfo info (tempBuffer);
|
||||
|
||||
source->source->getNextAudioBlock (info);
|
||||
|
||||
typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> CDSampleFormat;
|
||||
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SourceSampleFormat;
|
||||
|
||||
CDSampleFormat left (buffer, 2);
|
||||
left.convertSamples (SourceSampleFormat (tempBuffer.getReadPointer (0)), numSamples);
|
||||
CDSampleFormat right (buffer + 2, 2);
|
||||
right.convertSamples (SourceSampleFormat (tempBuffer.getReadPointer (1)), numSamples);
|
||||
|
||||
source->readPosition += numSamples;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t> (numSamples * 4);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint32_t producePreGapForTrack (id, SEL, DRTrack*, char* buffer,
|
||||
uint32_t bufferLength, uint64_t /*address*/,
|
||||
uint32_t /*blockSize*/, uint32_t* /*flags*/)
|
||||
{
|
||||
zeromem (buffer, bufferLength);
|
||||
return bufferLength;
|
||||
}
|
||||
|
||||
static BOOL verifyDataForTrack (id, SEL, DRTrack*, const char*,
|
||||
uint32_t /*bufferLength*/, uint64_t /*address*/,
|
||||
uint32_t /*blockSize*/, uint32_t* /*flags*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct OpenDiskDevice
|
||||
{
|
||||
OpenDiskDevice (DRDevice* d)
|
||||
: device (d),
|
||||
tracks ([[NSMutableArray alloc] init]),
|
||||
underrunProtection (true)
|
||||
{
|
||||
}
|
||||
|
||||
~OpenDiskDevice()
|
||||
{
|
||||
[tracks release];
|
||||
}
|
||||
|
||||
void addSourceTrack (AudioSource* source, int numSamples)
|
||||
{
|
||||
if (source != nullptr)
|
||||
{
|
||||
const int numFrames = (numSamples + 587) / 588;
|
||||
|
||||
static AudioTrackProducerClass cls;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
NSObject* producer = [cls.createInstance() performSelector: @selector (initWithAudioSourceHolder:)
|
||||
withObject: (id) new AudioTrackProducerClass::AudioSourceHolder (source, numFrames)];
|
||||
#pragma clang diagnostic pop
|
||||
DRTrack* track = [[DRTrack alloc] initWithProducer: producer];
|
||||
|
||||
{
|
||||
NSMutableDictionary* p = [[track properties] mutableCopy];
|
||||
[p setObject: [DRMSF msfWithFrames: static_cast<UInt32> (numFrames)] forKey: DRTrackLengthKey];
|
||||
[p setObject: [NSNumber numberWithUnsignedShort: 2352] forKey: DRBlockSizeKey];
|
||||
[p setObject: [NSNumber numberWithInt: 0] forKey: DRDataFormKey];
|
||||
[p setObject: [NSNumber numberWithInt: 0] forKey: DRBlockTypeKey];
|
||||
[p setObject: [NSNumber numberWithInt: 0] forKey: DRTrackModeKey];
|
||||
[p setObject: [NSNumber numberWithInt: 0] forKey: DRSessionFormatKey];
|
||||
[track setProperties: p];
|
||||
[p release];
|
||||
}
|
||||
|
||||
[tracks addObject: track];
|
||||
|
||||
[track release];
|
||||
[producer release];
|
||||
}
|
||||
}
|
||||
|
||||
String burn (AudioCDBurner::BurnProgressListener* listener,
|
||||
bool shouldEject, bool peformFakeBurnForTesting, int burnSpeed)
|
||||
{
|
||||
DRBurn* burn = [DRBurn burnForDevice: device];
|
||||
|
||||
if (! [device acquireExclusiveAccess])
|
||||
return "Couldn't open or write to the CD device";
|
||||
|
||||
[device acquireMediaReservation];
|
||||
|
||||
NSMutableDictionary* d = [[burn properties] mutableCopy];
|
||||
[d autorelease];
|
||||
[d setObject: [NSNumber numberWithBool: peformFakeBurnForTesting] forKey: DRBurnTestingKey];
|
||||
[d setObject: [NSNumber numberWithBool: false] forKey: DRBurnVerifyDiscKey];
|
||||
[d setObject: (shouldEject ? DRBurnCompletionActionEject : DRBurnCompletionActionMount) forKey: DRBurnCompletionActionKey];
|
||||
|
||||
if (burnSpeed > 0)
|
||||
[d setObject: [NSNumber numberWithFloat: burnSpeed * kilobytesPerSecond1x] forKey: DRBurnRequestedSpeedKey];
|
||||
|
||||
if (! underrunProtection)
|
||||
[d setObject: [NSNumber numberWithBool: false] forKey: DRBurnUnderrunProtectionKey];
|
||||
|
||||
[burn setProperties: d];
|
||||
|
||||
[burn writeLayout: tracks];
|
||||
|
||||
for (;;)
|
||||
{
|
||||
Thread::sleep (300);
|
||||
float progress = [[[burn status] objectForKey: DRStatusPercentCompleteKey] floatValue];
|
||||
|
||||
if (listener != nullptr && listener->audioCDBurnProgress (progress))
|
||||
{
|
||||
[burn abort];
|
||||
return "User cancelled the write operation";
|
||||
}
|
||||
|
||||
if ([[[burn status] objectForKey: DRStatusStateKey] isEqualTo: DRStatusStateFailed])
|
||||
return "Write operation failed";
|
||||
|
||||
if ([[[burn status] objectForKey: DRStatusStateKey] isEqualTo: DRStatusStateDone])
|
||||
break;
|
||||
|
||||
NSString* err = (NSString*) [[[burn status] objectForKey: DRErrorStatusKey]
|
||||
objectForKey: DRErrorStatusErrorStringKey];
|
||||
if ([err length] > 0)
|
||||
return nsStringToJuce (err);
|
||||
}
|
||||
|
||||
[device releaseMediaReservation];
|
||||
[device releaseExclusiveAccess];
|
||||
return {};
|
||||
}
|
||||
|
||||
DRDevice* device;
|
||||
NSMutableArray* tracks;
|
||||
bool underrunProtection;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AudioCDBurner::Pimpl : public Timer
|
||||
{
|
||||
public:
|
||||
Pimpl (AudioCDBurner& b, int deviceIndex) : owner (b)
|
||||
{
|
||||
if (DRDevice* dev = [[DRDevice devices] objectAtIndex: static_cast<NSUInteger> (deviceIndex)])
|
||||
{
|
||||
device.reset (new OpenDiskDevice (dev));
|
||||
lastState = getDiskState();
|
||||
startTimer (1000);
|
||||
}
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
const DiskState state = getDiskState();
|
||||
|
||||
if (state != lastState)
|
||||
{
|
||||
lastState = state;
|
||||
owner.sendChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
DiskState getDiskState() const
|
||||
{
|
||||
if ([device->device isValid])
|
||||
{
|
||||
NSDictionary* status = [device->device status];
|
||||
NSString* state = [status objectForKey: DRDeviceMediaStateKey];
|
||||
|
||||
if ([state isEqualTo: DRDeviceMediaStateNone])
|
||||
{
|
||||
if ([[status objectForKey: DRDeviceIsTrayOpenKey] boolValue])
|
||||
return trayOpen;
|
||||
|
||||
return noDisc;
|
||||
}
|
||||
|
||||
if ([state isEqualTo: DRDeviceMediaStateMediaPresent])
|
||||
{
|
||||
if ([[[status objectForKey: DRDeviceMediaInfoKey] objectForKey: DRDeviceMediaBlocksFreeKey] intValue] > 0)
|
||||
return writableDiskPresent;
|
||||
|
||||
return readOnlyDiskPresent;
|
||||
}
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
|
||||
bool openTray() { return [device->device isValid] && [device->device ejectMedia]; }
|
||||
|
||||
Array<int> getAvailableWriteSpeeds() const
|
||||
{
|
||||
Array<int> results;
|
||||
|
||||
if ([device->device isValid])
|
||||
for (id kbPerSec in [[[device->device status] objectForKey: DRDeviceMediaInfoKey] objectForKey: DRDeviceBurnSpeedsKey])
|
||||
results.add ([kbPerSec intValue] / kilobytesPerSecond1x);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
bool setBufferUnderrunProtection (const bool shouldBeEnabled)
|
||||
{
|
||||
if ([device->device isValid])
|
||||
{
|
||||
device->underrunProtection = shouldBeEnabled;
|
||||
return shouldBeEnabled && [[[device->device status] objectForKey: DRDeviceCanUnderrunProtectCDKey] boolValue];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int getNumAvailableAudioBlocks() const
|
||||
{
|
||||
return [[[[device->device status] objectForKey: DRDeviceMediaInfoKey]
|
||||
objectForKey: DRDeviceMediaBlocksFreeKey] intValue];
|
||||
}
|
||||
|
||||
std::unique_ptr<OpenDiskDevice> device;
|
||||
|
||||
private:
|
||||
DiskState lastState;
|
||||
AudioCDBurner& owner;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
AudioCDBurner::AudioCDBurner (const int deviceIndex)
|
||||
{
|
||||
pimpl.reset (new Pimpl (*this, deviceIndex));
|
||||
}
|
||||
|
||||
AudioCDBurner::~AudioCDBurner()
|
||||
{
|
||||
}
|
||||
|
||||
AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex)
|
||||
{
|
||||
std::unique_ptr<AudioCDBurner> b (new AudioCDBurner (deviceIndex));
|
||||
|
||||
if (b->pimpl->device == nil)
|
||||
b = nullptr;
|
||||
|
||||
return b.release();
|
||||
}
|
||||
|
||||
StringArray AudioCDBurner::findAvailableDevices()
|
||||
{
|
||||
StringArray s;
|
||||
|
||||
for (NSDictionary* dic in [DRDevice devices])
|
||||
if (NSString* name = [dic valueForKey: DRDeviceProductNameKey])
|
||||
s.add (nsStringToJuce (name));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
AudioCDBurner::DiskState AudioCDBurner::getDiskState() const
|
||||
{
|
||||
return pimpl->getDiskState();
|
||||
}
|
||||
|
||||
bool AudioCDBurner::isDiskPresent() const
|
||||
{
|
||||
return getDiskState() == writableDiskPresent;
|
||||
}
|
||||
|
||||
bool AudioCDBurner::openTray()
|
||||
{
|
||||
return pimpl->openTray();
|
||||
}
|
||||
|
||||
AudioCDBurner::DiskState AudioCDBurner::waitUntilStateChange (int timeOutMilliseconds)
|
||||
{
|
||||
const int64 timeout = Time::currentTimeMillis() + timeOutMilliseconds;
|
||||
DiskState oldState = getDiskState();
|
||||
DiskState newState = oldState;
|
||||
|
||||
while (newState == oldState && Time::currentTimeMillis() < timeout)
|
||||
{
|
||||
newState = getDiskState();
|
||||
Thread::sleep (100);
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
Array<int> AudioCDBurner::getAvailableWriteSpeeds() const
|
||||
{
|
||||
return pimpl->getAvailableWriteSpeeds();
|
||||
}
|
||||
|
||||
bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled)
|
||||
{
|
||||
return pimpl->setBufferUnderrunProtection (shouldBeEnabled);
|
||||
}
|
||||
|
||||
int AudioCDBurner::getNumAvailableAudioBlocks() const
|
||||
{
|
||||
return pimpl->getNumAvailableAudioBlocks();
|
||||
}
|
||||
|
||||
bool AudioCDBurner::addAudioTrack (AudioSource* source, int numSamps)
|
||||
{
|
||||
if ([pimpl->device->device isValid])
|
||||
{
|
||||
pimpl->device->addSourceTrack (source, numSamps);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener,
|
||||
bool ejectDiscAfterwards,
|
||||
bool performFakeBurnForTesting,
|
||||
int writeSpeed)
|
||||
{
|
||||
if ([pimpl->device->device isValid])
|
||||
return pimpl->device->burn (listener, ejectDiscAfterwards, performFakeBurnForTesting, writeSpeed);
|
||||
|
||||
return "Couldn't open or write to the CD device";
|
||||
}
|
||||
|
||||
}
|
267
modules/juce_audio_utils/native/juce_mac_AudioCDReader.mm
Normal file
267
modules/juce_audio_utils/native/juce_mac_AudioCDReader.mm
Normal file
@ -0,0 +1,267 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
namespace CDReaderHelpers
|
||||
{
|
||||
inline const XmlElement* getElementForKey (const XmlElement& xml, const String& key)
|
||||
{
|
||||
forEachXmlChildElementWithTagName (xml, child, "key")
|
||||
if (child->getAllSubText().trim() == key)
|
||||
return child->getNextElement();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static int getIntValueForKey (const XmlElement& xml, const String& key, int defaultValue = -1)
|
||||
{
|
||||
const XmlElement* const block = getElementForKey (xml, key);
|
||||
return block != nullptr ? block->getAllSubText().trim().getIntValue() : defaultValue;
|
||||
}
|
||||
|
||||
// Get the track offsets for a CD given an XmlElement representing its TOC.Plist.
|
||||
// Returns NULL on success, otherwise a const char* representing an error.
|
||||
static const char* getTrackOffsets (XmlDocument& xmlDocument, Array<int>& offsets)
|
||||
{
|
||||
const std::unique_ptr<XmlElement> xml (xmlDocument.getDocumentElement());
|
||||
if (xml == nullptr)
|
||||
return "Couldn't parse XML in file";
|
||||
|
||||
const XmlElement* const dict = xml->getChildByName ("dict");
|
||||
if (dict == nullptr)
|
||||
return "Couldn't get top level dictionary";
|
||||
|
||||
const XmlElement* const sessions = getElementForKey (*dict, "Sessions");
|
||||
if (sessions == nullptr)
|
||||
return "Couldn't find sessions key";
|
||||
|
||||
const XmlElement* const session = sessions->getFirstChildElement();
|
||||
if (session == nullptr)
|
||||
return "Couldn't find first session";
|
||||
|
||||
const int leadOut = getIntValueForKey (*session, "Leadout Block");
|
||||
if (leadOut < 0)
|
||||
return "Couldn't find Leadout Block";
|
||||
|
||||
const XmlElement* const trackArray = getElementForKey (*session, "Track Array");
|
||||
if (trackArray == nullptr)
|
||||
return "Couldn't find Track Array";
|
||||
|
||||
forEachXmlChildElement (*trackArray, track)
|
||||
{
|
||||
const int trackValue = getIntValueForKey (*track, "Start Block");
|
||||
if (trackValue < 0)
|
||||
return "Couldn't find Start Block in the track";
|
||||
|
||||
offsets.add (trackValue * AudioCDReader::samplesPerFrame - 88200);
|
||||
}
|
||||
|
||||
offsets.add (leadOut * AudioCDReader::samplesPerFrame - 88200);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void findDevices (Array<File>& cds)
|
||||
{
|
||||
File volumes ("/Volumes");
|
||||
volumes.findChildFiles (cds, File::findDirectories, false);
|
||||
|
||||
for (int i = cds.size(); --i >= 0;)
|
||||
if (! cds.getReference(i).getChildFile (".TOC.plist").exists())
|
||||
cds.remove (i);
|
||||
}
|
||||
|
||||
struct TrackSorter
|
||||
{
|
||||
static int getCDTrackNumber (const File& file)
|
||||
{
|
||||
return file.getFileName().initialSectionContainingOnly ("0123456789").getIntValue();
|
||||
}
|
||||
|
||||
static int compareElements (const File& first, const File& second)
|
||||
{
|
||||
const int firstTrack = getCDTrackNumber (first);
|
||||
const int secondTrack = getCDTrackNumber (second);
|
||||
|
||||
jassert (firstTrack > 0 && secondTrack > 0);
|
||||
|
||||
return firstTrack - secondTrack;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
StringArray AudioCDReader::getAvailableCDNames()
|
||||
{
|
||||
Array<File> cds;
|
||||
CDReaderHelpers::findDevices (cds);
|
||||
|
||||
StringArray names;
|
||||
|
||||
for (int i = 0; i < cds.size(); ++i)
|
||||
names.add (cds.getReference(i).getFileName());
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
AudioCDReader* AudioCDReader::createReaderForCD (const int index)
|
||||
{
|
||||
Array<File> cds;
|
||||
CDReaderHelpers::findDevices (cds);
|
||||
|
||||
if (cds[index].exists())
|
||||
return new AudioCDReader (cds[index]);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AudioCDReader::AudioCDReader (const File& volume)
|
||||
: AudioFormatReader (0, "CD Audio"),
|
||||
volumeDir (volume),
|
||||
currentReaderTrack (-1)
|
||||
{
|
||||
sampleRate = 44100.0;
|
||||
bitsPerSample = 16;
|
||||
numChannels = 2;
|
||||
usesFloatingPointData = false;
|
||||
|
||||
refreshTrackLengths();
|
||||
}
|
||||
|
||||
AudioCDReader::~AudioCDReader()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioCDReader::refreshTrackLengths()
|
||||
{
|
||||
tracks.clear();
|
||||
trackStartSamples.clear();
|
||||
lengthInSamples = 0;
|
||||
|
||||
volumeDir.findChildFiles (tracks, File::findFiles | File::ignoreHiddenFiles, false, "*.aiff");
|
||||
|
||||
CDReaderHelpers::TrackSorter sorter;
|
||||
tracks.sort (sorter);
|
||||
|
||||
const File toc (volumeDir.getChildFile (".TOC.plist"));
|
||||
|
||||
if (toc.exists())
|
||||
{
|
||||
XmlDocument doc (toc);
|
||||
const char* error = CDReaderHelpers::getTrackOffsets (doc, trackStartSamples);
|
||||
ignoreUnused (error); // could be logged..
|
||||
|
||||
lengthInSamples = trackStartSamples.getLast() - trackStartSamples.getFirst();
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioCDReader::readSamples (int** destSamples, int numDestChannels, int startOffsetInDestBuffer,
|
||||
int64 startSampleInFile, int numSamples)
|
||||
{
|
||||
while (numSamples > 0)
|
||||
{
|
||||
int track = -1;
|
||||
|
||||
for (int i = 0; i < trackStartSamples.size() - 1; ++i)
|
||||
{
|
||||
if (startSampleInFile < trackStartSamples.getUnchecked (i + 1))
|
||||
{
|
||||
track = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track < 0)
|
||||
return false;
|
||||
|
||||
if (track != currentReaderTrack)
|
||||
{
|
||||
reader = nullptr;
|
||||
|
||||
if (FileInputStream* const in = tracks [track].createInputStream())
|
||||
{
|
||||
BufferedInputStream* const bin = new BufferedInputStream (in, 65536, true);
|
||||
|
||||
AiffAudioFormat format;
|
||||
reader.reset (format.createReaderFor (bin, true));
|
||||
|
||||
if (reader == nullptr)
|
||||
currentReaderTrack = -1;
|
||||
else
|
||||
currentReaderTrack = track;
|
||||
}
|
||||
}
|
||||
|
||||
if (reader == nullptr)
|
||||
return false;
|
||||
|
||||
const int startPos = (int) (startSampleInFile - trackStartSamples.getUnchecked (track));
|
||||
const int numAvailable = (int) jmin ((int64) numSamples, reader->lengthInSamples - startPos);
|
||||
|
||||
reader->readSamples (destSamples, numDestChannels, startOffsetInDestBuffer, startPos, numAvailable);
|
||||
|
||||
numSamples -= numAvailable;
|
||||
startSampleInFile += numAvailable;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioCDReader::isCDStillPresent() const
|
||||
{
|
||||
return volumeDir.exists();
|
||||
}
|
||||
|
||||
void AudioCDReader::ejectDisk()
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] unmountAndEjectDeviceAtPath: juceStringToNS (volumeDir.getFullPathName())];
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioCDReader::isTrackAudio (int trackNum) const
|
||||
{
|
||||
return tracks [trackNum].hasFileExtension (".aiff");
|
||||
}
|
||||
|
||||
void AudioCDReader::enableIndexScanning (bool)
|
||||
{
|
||||
// any way to do this on a Mac??
|
||||
}
|
||||
|
||||
int AudioCDReader::getLastIndex() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Array<int> AudioCDReader::findIndexesInTrack (const int /*trackNumber*/)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace juce
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
|
||||
Rectangle<int>*)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
|
||||
// Do not call this on OSX. Instead, you should pair Bluetooth MIDI devices
|
||||
// using the "Audio MIDI Setup" app (located in /Applications/Utilities).
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace juce
|
418
modules/juce_audio_utils/native/juce_win32_AudioCDBurner.cpp
Normal file
418
modules/juce_audio_utils/native/juce_win32_AudioCDBurner.cpp
Normal file
@ -0,0 +1,418 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
namespace CDBurnerHelpers
|
||||
{
|
||||
IDiscRecorder* enumCDBurners (StringArray* list, int indexToOpen, IDiscMaster** master)
|
||||
{
|
||||
CoInitialize (0);
|
||||
|
||||
IDiscMaster* dm;
|
||||
IDiscRecorder* result = nullptr;
|
||||
|
||||
if (SUCCEEDED (CoCreateInstance (CLSID_MSDiscMasterObj, 0,
|
||||
CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
|
||||
IID_IDiscMaster,
|
||||
(void**) &dm)))
|
||||
{
|
||||
if (SUCCEEDED (dm->Open()))
|
||||
{
|
||||
IEnumDiscRecorders* drEnum = nullptr;
|
||||
|
||||
if (SUCCEEDED (dm->EnumDiscRecorders (&drEnum)))
|
||||
{
|
||||
IDiscRecorder* dr = nullptr;
|
||||
DWORD dummy;
|
||||
int index = 0;
|
||||
|
||||
while (drEnum->Next (1, &dr, &dummy) == S_OK)
|
||||
{
|
||||
if (indexToOpen == index)
|
||||
{
|
||||
result = dr;
|
||||
break;
|
||||
}
|
||||
else if (list != nullptr)
|
||||
{
|
||||
BSTR path;
|
||||
|
||||
if (SUCCEEDED (dr->GetPath (&path)))
|
||||
list->add ((const WCHAR*) path);
|
||||
}
|
||||
|
||||
++index;
|
||||
dr->Release();
|
||||
}
|
||||
|
||||
drEnum->Release();
|
||||
}
|
||||
|
||||
if (master == 0)
|
||||
dm->Close();
|
||||
}
|
||||
|
||||
if (master != nullptr)
|
||||
*master = dm;
|
||||
else
|
||||
dm->Release();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class AudioCDBurner::Pimpl : public ComBaseClassHelper <IDiscMasterProgressEvents>,
|
||||
public Timer
|
||||
{
|
||||
public:
|
||||
Pimpl (AudioCDBurner& owner_, IDiscMaster* discMaster_, IDiscRecorder* discRecorder_)
|
||||
: owner (owner_), discMaster (discMaster_), discRecorder (discRecorder_), redbook (0),
|
||||
listener (0), progress (0), shouldCancel (false)
|
||||
{
|
||||
HRESULT hr = discMaster->SetActiveDiscMasterFormat (IID_IRedbookDiscMaster, (void**) &redbook);
|
||||
jassert (SUCCEEDED (hr));
|
||||
hr = discMaster->SetActiveDiscRecorder (discRecorder);
|
||||
//jassert (SUCCEEDED (hr));
|
||||
|
||||
lastState = getDiskState();
|
||||
startTimer (2000);
|
||||
}
|
||||
|
||||
~Pimpl() {}
|
||||
|
||||
void releaseObjects()
|
||||
{
|
||||
discRecorder->Close();
|
||||
if (redbook != nullptr)
|
||||
redbook->Release();
|
||||
discRecorder->Release();
|
||||
discMaster->Release();
|
||||
Release();
|
||||
}
|
||||
|
||||
JUCE_COMRESULT QueryCancel (boolean* pbCancel)
|
||||
{
|
||||
if (listener != nullptr && ! shouldCancel)
|
||||
shouldCancel = listener->audioCDBurnProgress (progress);
|
||||
|
||||
*pbCancel = shouldCancel;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT NotifyBlockProgress (long nCompleted, long nTotal)
|
||||
{
|
||||
progress = nCompleted / (float) nTotal;
|
||||
shouldCancel = listener != nullptr && listener->audioCDBurnProgress (progress);
|
||||
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT NotifyPnPActivity (void) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyAddProgress (long /*nCompletedSteps*/, long /*nTotalSteps*/) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyTrackProgress (long /*nCurrentTrack*/, long /*nTotalTracks*/) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyPreparingBurn (long /*nEstimatedSeconds*/) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyClosingDisc (long /*nEstimatedSeconds*/) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyBurnComplete (HRESULT /*status*/) { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT NotifyEraseComplete (HRESULT /*status*/) { return E_NOTIMPL; }
|
||||
|
||||
class ScopedDiscOpener
|
||||
{
|
||||
public:
|
||||
ScopedDiscOpener (Pimpl& p) : pimpl (p) { pimpl.discRecorder->OpenExclusive(); }
|
||||
~ScopedDiscOpener() { pimpl.discRecorder->Close(); }
|
||||
|
||||
private:
|
||||
Pimpl& pimpl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (ScopedDiscOpener)
|
||||
};
|
||||
|
||||
DiskState getDiskState()
|
||||
{
|
||||
const ScopedDiscOpener opener (*this);
|
||||
|
||||
long type, flags;
|
||||
HRESULT hr = discRecorder->QueryMediaType (&type, &flags);
|
||||
|
||||
if (FAILED (hr))
|
||||
return unknown;
|
||||
|
||||
if (type != 0 && (flags & MEDIA_WRITABLE) != 0)
|
||||
return writableDiskPresent;
|
||||
|
||||
if (type == 0)
|
||||
return noDisc;
|
||||
|
||||
return readOnlyDiskPresent;
|
||||
}
|
||||
|
||||
int getIntProperty (const LPOLESTR name, const int defaultReturn) const
|
||||
{
|
||||
ComSmartPtr<IPropertyStorage> prop;
|
||||
if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
|
||||
return defaultReturn;
|
||||
|
||||
PROPSPEC iPropSpec;
|
||||
iPropSpec.ulKind = PRSPEC_LPWSTR;
|
||||
iPropSpec.lpwstr = name;
|
||||
|
||||
PROPVARIANT iPropVariant;
|
||||
return FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant))
|
||||
? defaultReturn : (int) iPropVariant.lVal;
|
||||
}
|
||||
|
||||
bool setIntProperty (const LPOLESTR name, const int value) const
|
||||
{
|
||||
ComSmartPtr<IPropertyStorage> prop;
|
||||
if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
|
||||
return false;
|
||||
|
||||
PROPSPEC iPropSpec;
|
||||
iPropSpec.ulKind = PRSPEC_LPWSTR;
|
||||
iPropSpec.lpwstr = name;
|
||||
|
||||
PROPVARIANT iPropVariant;
|
||||
if (FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant)))
|
||||
return false;
|
||||
|
||||
iPropVariant.lVal = (long) value;
|
||||
return SUCCEEDED (prop->WriteMultiple (1, &iPropSpec, &iPropVariant, iPropVariant.vt))
|
||||
&& SUCCEEDED (discRecorder->SetRecorderProperties (prop));
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
const DiskState state = getDiskState();
|
||||
|
||||
if (state != lastState)
|
||||
{
|
||||
lastState = state;
|
||||
owner.sendChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
AudioCDBurner& owner;
|
||||
DiskState lastState;
|
||||
IDiscMaster* discMaster;
|
||||
IDiscRecorder* discRecorder;
|
||||
IRedbookDiscMaster* redbook;
|
||||
AudioCDBurner::BurnProgressListener* listener;
|
||||
float progress;
|
||||
bool shouldCancel;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
AudioCDBurner::AudioCDBurner (const int deviceIndex)
|
||||
{
|
||||
IDiscMaster* discMaster = nullptr;
|
||||
IDiscRecorder* discRecorder = CDBurnerHelpers::enumCDBurners (0, deviceIndex, &discMaster);
|
||||
|
||||
if (discRecorder != nullptr)
|
||||
pimpl.reset (new Pimpl (*this, discMaster, discRecorder));
|
||||
}
|
||||
|
||||
AudioCDBurner::~AudioCDBurner()
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl.release()->releaseObjects();
|
||||
}
|
||||
|
||||
StringArray AudioCDBurner::findAvailableDevices()
|
||||
{
|
||||
StringArray devs;
|
||||
CDBurnerHelpers::enumCDBurners (&devs, -1, 0);
|
||||
return devs;
|
||||
}
|
||||
|
||||
AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex)
|
||||
{
|
||||
std::unique_ptr<AudioCDBurner> b (new AudioCDBurner (deviceIndex));
|
||||
|
||||
if (b->pimpl == 0)
|
||||
b = nullptr;
|
||||
|
||||
return b.release();
|
||||
}
|
||||
|
||||
AudioCDBurner::DiskState AudioCDBurner::getDiskState() const
|
||||
{
|
||||
return pimpl->getDiskState();
|
||||
}
|
||||
|
||||
bool AudioCDBurner::isDiskPresent() const
|
||||
{
|
||||
return getDiskState() == writableDiskPresent;
|
||||
}
|
||||
|
||||
bool AudioCDBurner::openTray()
|
||||
{
|
||||
const Pimpl::ScopedDiscOpener opener (*pimpl);
|
||||
return SUCCEEDED (pimpl->discRecorder->Eject());
|
||||
}
|
||||
|
||||
AudioCDBurner::DiskState AudioCDBurner::waitUntilStateChange (int timeOutMilliseconds)
|
||||
{
|
||||
const int64 timeout = Time::currentTimeMillis() + timeOutMilliseconds;
|
||||
DiskState oldState = getDiskState();
|
||||
DiskState newState = oldState;
|
||||
|
||||
while (newState == oldState && Time::currentTimeMillis() < timeout)
|
||||
{
|
||||
newState = getDiskState();
|
||||
Thread::sleep (jmin (250, (int) (timeout - Time::currentTimeMillis())));
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
Array<int> AudioCDBurner::getAvailableWriteSpeeds() const
|
||||
{
|
||||
Array<int> results;
|
||||
const int maxSpeed = pimpl->getIntProperty (L"MaxWriteSpeed", 1);
|
||||
const int speeds[] = { 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 64, 80 };
|
||||
|
||||
for (int i = 0; i < numElementsInArray (speeds); ++i)
|
||||
if (speeds[i] <= maxSpeed)
|
||||
results.add (speeds[i]);
|
||||
|
||||
results.addIfNotAlreadyThere (maxSpeed);
|
||||
return results;
|
||||
}
|
||||
|
||||
bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled)
|
||||
{
|
||||
if (pimpl->getIntProperty (L"BufferUnderrunFreeCapable", 0) == 0)
|
||||
return false;
|
||||
|
||||
pimpl->setIntProperty (L"EnableBufferUnderrunFree", shouldBeEnabled ? -1 : 0);
|
||||
return pimpl->getIntProperty (L"EnableBufferUnderrunFree", 0) != 0;
|
||||
}
|
||||
|
||||
int AudioCDBurner::getNumAvailableAudioBlocks() const
|
||||
{
|
||||
long blocksFree = 0;
|
||||
pimpl->redbook->GetAvailableAudioTrackBlocks (&blocksFree);
|
||||
return blocksFree;
|
||||
}
|
||||
|
||||
String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener, bool ejectDiscAfterwards,
|
||||
bool performFakeBurnForTesting, int writeSpeed)
|
||||
{
|
||||
pimpl->setIntProperty (L"WriteSpeed", writeSpeed > 0 ? writeSpeed : -1);
|
||||
|
||||
pimpl->listener = listener;
|
||||
pimpl->progress = 0;
|
||||
pimpl->shouldCancel = false;
|
||||
|
||||
UINT_PTR cookie;
|
||||
HRESULT hr = pimpl->discMaster->ProgressAdvise ((AudioCDBurner::Pimpl*) pimpl.get(), &cookie);
|
||||
|
||||
hr = pimpl->discMaster->RecordDisc (performFakeBurnForTesting,
|
||||
ejectDiscAfterwards);
|
||||
|
||||
String error;
|
||||
if (hr != S_OK)
|
||||
{
|
||||
const char* e = "Couldn't open or write to the CD device";
|
||||
|
||||
if (hr == IMAPI_E_USERABORT)
|
||||
e = "User cancelled the write operation";
|
||||
else if (hr == IMAPI_E_MEDIUM_NOTPRESENT || hr == IMAPI_E_TRACKOPEN)
|
||||
e = "No Disk present";
|
||||
|
||||
error = e;
|
||||
}
|
||||
|
||||
pimpl->discMaster->ProgressUnadvise (cookie);
|
||||
pimpl->listener = 0;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
bool AudioCDBurner::addAudioTrack (AudioSource* audioSource, int numSamples)
|
||||
{
|
||||
if (audioSource == 0)
|
||||
return false;
|
||||
|
||||
std::unique_ptr<AudioSource> source (audioSource);
|
||||
|
||||
long bytesPerBlock;
|
||||
HRESULT hr = pimpl->redbook->GetAudioBlockSize (&bytesPerBlock);
|
||||
|
||||
const int samplesPerBlock = bytesPerBlock / 4;
|
||||
bool ok = true;
|
||||
|
||||
hr = pimpl->redbook->CreateAudioTrack ((long) numSamples / (bytesPerBlock * 4));
|
||||
|
||||
HeapBlock<byte> buffer (bytesPerBlock);
|
||||
AudioBuffer<float> sourceBuffer (2, samplesPerBlock);
|
||||
int samplesDone = 0;
|
||||
|
||||
source->prepareToPlay (samplesPerBlock, 44100.0);
|
||||
|
||||
while (ok)
|
||||
{
|
||||
{
|
||||
AudioSourceChannelInfo info (&sourceBuffer, 0, samplesPerBlock);
|
||||
sourceBuffer.clear();
|
||||
|
||||
source->getNextAudioBlock (info);
|
||||
}
|
||||
|
||||
buffer.clear (bytesPerBlock);
|
||||
|
||||
typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian,
|
||||
AudioData::Interleaved, AudioData::NonConst> CDSampleFormat;
|
||||
|
||||
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian,
|
||||
AudioData::NonInterleaved, AudioData::Const> SourceSampleFormat;
|
||||
|
||||
CDSampleFormat left (buffer, 2);
|
||||
left.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (0)), samplesPerBlock);
|
||||
CDSampleFormat right (buffer + 2, 2);
|
||||
right.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (1)), samplesPerBlock);
|
||||
|
||||
hr = pimpl->redbook->AddAudioTrackBlocks (buffer, bytesPerBlock);
|
||||
|
||||
if (FAILED (hr))
|
||||
ok = false;
|
||||
|
||||
samplesDone += samplesPerBlock;
|
||||
|
||||
if (samplesDone >= numSamples)
|
||||
break;
|
||||
}
|
||||
|
||||
hr = pimpl->redbook->CloseAudioTrack();
|
||||
return ok && hr == S_OK;
|
||||
}
|
||||
|
||||
} // namespace juce
|
1316
modules/juce_audio_utils/native/juce_win32_AudioCDReader.cpp
Normal file
1316
modules/juce_audio_utils/native/juce_win32_AudioCDReader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
|
||||
Rectangle<int>*)
|
||||
{
|
||||
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
|
||||
// not implemented on Windows yet!
|
||||
// You should check whether the dialogue is available on your system
|
||||
// using isAvailable() before calling open().
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BluetoothMidiDevicePairingDialogue::isAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace juce
|
221
modules/juce_audio_utils/players/juce_AudioProcessorPlayer.cpp
Normal file
221
modules/juce_audio_utils/players/juce_AudioProcessorPlayer.cpp
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
AudioProcessorPlayer::AudioProcessorPlayer (bool doDoublePrecisionProcessing)
|
||||
: isDoublePrecision (doDoublePrecisionProcessing)
|
||||
{
|
||||
}
|
||||
|
||||
AudioProcessorPlayer::~AudioProcessorPlayer()
|
||||
{
|
||||
setProcessor (nullptr);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void AudioProcessorPlayer::setProcessor (AudioProcessor* const processorToPlay)
|
||||
{
|
||||
if (processor != processorToPlay)
|
||||
{
|
||||
if (processorToPlay != nullptr && sampleRate > 0 && blockSize > 0)
|
||||
{
|
||||
processorToPlay->setPlayConfigDetails (numInputChans, numOutputChans, sampleRate, blockSize);
|
||||
|
||||
bool supportsDouble = processorToPlay->supportsDoublePrecisionProcessing() && isDoublePrecision;
|
||||
|
||||
processorToPlay->setProcessingPrecision (supportsDouble ? AudioProcessor::doublePrecision
|
||||
: AudioProcessor::singlePrecision);
|
||||
processorToPlay->prepareToPlay (sampleRate, blockSize);
|
||||
}
|
||||
|
||||
AudioProcessor* oldOne;
|
||||
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
oldOne = isPrepared ? processor : nullptr;
|
||||
processor = processorToPlay;
|
||||
isPrepared = true;
|
||||
}
|
||||
|
||||
if (oldOne != nullptr)
|
||||
oldOne->releaseResources();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProcessorPlayer::setDoublePrecisionProcessing (bool doublePrecision)
|
||||
{
|
||||
if (doublePrecision != isDoublePrecision)
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (processor != nullptr)
|
||||
{
|
||||
processor->releaseResources();
|
||||
|
||||
bool supportsDouble = processor->supportsDoublePrecisionProcessing() && doublePrecision;
|
||||
|
||||
processor->setProcessingPrecision (supportsDouble ? AudioProcessor::doublePrecision
|
||||
: AudioProcessor::singlePrecision);
|
||||
processor->prepareToPlay (sampleRate, blockSize);
|
||||
}
|
||||
|
||||
isDoublePrecision = doublePrecision;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void AudioProcessorPlayer::audioDeviceIOCallback (const float** const inputChannelData,
|
||||
const int numInputChannels,
|
||||
float** const outputChannelData,
|
||||
const int numOutputChannels,
|
||||
const int numSamples)
|
||||
{
|
||||
// these should have been prepared by audioDeviceAboutToStart()...
|
||||
jassert (sampleRate > 0 && blockSize > 0);
|
||||
|
||||
incomingMidi.clear();
|
||||
messageCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
|
||||
int totalNumChans = 0;
|
||||
|
||||
if (numInputChannels > numOutputChannels)
|
||||
{
|
||||
// if there aren't enough output channels for the number of
|
||||
// inputs, we need to create some temporary extra ones (can't
|
||||
// use the input data in case it gets written to)
|
||||
tempBuffer.setSize (numInputChannels - numOutputChannels, numSamples,
|
||||
false, false, true);
|
||||
|
||||
for (int i = 0; i < numOutputChannels; ++i)
|
||||
{
|
||||
channels[totalNumChans] = outputChannelData[i];
|
||||
memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
|
||||
++totalNumChans;
|
||||
}
|
||||
|
||||
for (int i = numOutputChannels; i < numInputChannels; ++i)
|
||||
{
|
||||
channels[totalNumChans] = tempBuffer.getWritePointer (i - numOutputChannels);
|
||||
memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
|
||||
++totalNumChans;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < numInputChannels; ++i)
|
||||
{
|
||||
channels[totalNumChans] = outputChannelData[i];
|
||||
memcpy (channels[totalNumChans], inputChannelData[i], sizeof (float) * (size_t) numSamples);
|
||||
++totalNumChans;
|
||||
}
|
||||
|
||||
for (int i = numInputChannels; i < numOutputChannels; ++i)
|
||||
{
|
||||
channels[totalNumChans] = outputChannelData[i];
|
||||
zeromem (channels[totalNumChans], sizeof (float) * (size_t) numSamples);
|
||||
++totalNumChans;
|
||||
}
|
||||
}
|
||||
|
||||
AudioBuffer<float> buffer (channels, totalNumChans, numSamples);
|
||||
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (processor != nullptr)
|
||||
{
|
||||
const ScopedLock sl2 (processor->getCallbackLock());
|
||||
|
||||
if (! processor->isSuspended())
|
||||
{
|
||||
if (processor->isUsingDoublePrecision())
|
||||
{
|
||||
conversionBuffer.makeCopyOf (buffer, true);
|
||||
processor->processBlock (conversionBuffer, incomingMidi);
|
||||
buffer.makeCopyOf (conversionBuffer, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
processor->processBlock (buffer, incomingMidi);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < numOutputChannels; ++i)
|
||||
FloatVectorOperations::clear (outputChannelData[i], numSamples);
|
||||
}
|
||||
|
||||
void AudioProcessorPlayer::audioDeviceAboutToStart (AudioIODevice* const device)
|
||||
{
|
||||
auto newSampleRate = device->getCurrentSampleRate();
|
||||
auto newBlockSize = device->getCurrentBufferSizeSamples();
|
||||
auto numChansIn = device->getActiveInputChannels().countNumberOfSetBits();
|
||||
auto numChansOut = device->getActiveOutputChannels().countNumberOfSetBits();
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
sampleRate = newSampleRate;
|
||||
blockSize = newBlockSize;
|
||||
numInputChans = numChansIn;
|
||||
numOutputChans = numChansOut;
|
||||
|
||||
messageCollector.reset (sampleRate);
|
||||
channels.calloc (jmax (numChansIn, numChansOut) + 2);
|
||||
|
||||
if (processor != nullptr)
|
||||
{
|
||||
if (isPrepared)
|
||||
processor->releaseResources();
|
||||
|
||||
auto* oldProcessor = processor;
|
||||
setProcessor (nullptr);
|
||||
setProcessor (oldProcessor);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProcessorPlayer::audioDeviceStopped()
|
||||
{
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (processor != nullptr && isPrepared)
|
||||
processor->releaseResources();
|
||||
|
||||
sampleRate = 0.0;
|
||||
blockSize = 0;
|
||||
isPrepared = false;
|
||||
tempBuffer.setSize (1, 1);
|
||||
}
|
||||
|
||||
void AudioProcessorPlayer::handleIncomingMidiMessage (MidiInput*, const MidiMessage& message)
|
||||
{
|
||||
messageCollector.addMessageToQueue (message);
|
||||
}
|
||||
|
||||
} // namespace juce
|
114
modules/juce_audio_utils/players/juce_AudioProcessorPlayer.h
Normal file
114
modules/juce_audio_utils/players/juce_AudioProcessorPlayer.h
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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 AudioIODeviceCallback object which streams audio through an AudioProcessor.
|
||||
|
||||
To use one of these, just make it the callback used by your AudioIODevice, and
|
||||
give it a processor to use by calling setProcessor().
|
||||
|
||||
It's also a MidiInputCallback, so you can connect it to both an audio and midi
|
||||
input to send both streams through the processor.
|
||||
|
||||
@see AudioProcessor, AudioProcessorGraph
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API AudioProcessorPlayer : public AudioIODeviceCallback,
|
||||
public MidiInputCallback
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
AudioProcessorPlayer (bool doDoublePrecisionProcessing = false);
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~AudioProcessorPlayer();
|
||||
|
||||
//==============================================================================
|
||||
/** Sets the processor that should be played.
|
||||
|
||||
The processor that is passed in will not be deleted or owned by this object.
|
||||
To stop anything playing, pass a nullptr to this method.
|
||||
*/
|
||||
void setProcessor (AudioProcessor* processorToPlay);
|
||||
|
||||
/** Returns the current audio processor that is being played. */
|
||||
AudioProcessor* getCurrentProcessor() const noexcept { return processor; }
|
||||
|
||||
/** Returns a midi message collector that you can pass midi messages to if you
|
||||
want them to be injected into the midi stream that is being sent to the
|
||||
processor.
|
||||
*/
|
||||
MidiMessageCollector& getMidiMessageCollector() noexcept { return messageCollector; }
|
||||
|
||||
/** Switch between double and single floating point precisions processing.
|
||||
The audio IO callbacks will still operate in single floating point
|
||||
precision, however, all internal processing including the
|
||||
AudioProcessor will be processed in double floating point precision if
|
||||
the AudioProcessor supports it (see
|
||||
AudioProcessor::supportsDoublePrecisionProcessing()).
|
||||
Otherwise, the processing will remain single precision irrespective of
|
||||
the parameter doublePrecision. */
|
||||
void setDoublePrecisionProcessing (bool doublePrecision);
|
||||
|
||||
/** Returns true if this player processes internally processes the samples with
|
||||
double floating point precision. */
|
||||
inline bool getDoublePrecisionProcessing() { return isDoublePrecision; }
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void audioDeviceIOCallback (const float**, int, float**, int, int) override;
|
||||
/** @internal */
|
||||
void audioDeviceAboutToStart (AudioIODevice*) override;
|
||||
/** @internal */
|
||||
void audioDeviceStopped() override;
|
||||
/** @internal */
|
||||
void handleIncomingMidiMessage (MidiInput*, const MidiMessage&) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioProcessor* processor = nullptr;
|
||||
CriticalSection lock;
|
||||
double sampleRate = 0;
|
||||
int blockSize = 0;
|
||||
bool isPrepared = false, isDoublePrecision = false;
|
||||
|
||||
int numInputChans = 0, numOutputChans = 0;
|
||||
HeapBlock<float*> channels;
|
||||
AudioBuffer<float> tempBuffer;
|
||||
AudioBuffer<double> conversionBuffer;
|
||||
|
||||
MidiBuffer incomingMidi;
|
||||
MidiMessageCollector messageCollector;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioProcessorPlayer)
|
||||
};
|
||||
|
||||
} // namespace juce
|
278
modules/juce_audio_utils/players/juce_SoundPlayer.cpp
Normal file
278
modules/juce_audio_utils/players/juce_SoundPlayer.cpp
Normal file
@ -0,0 +1,278 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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
|
||||
{
|
||||
|
||||
// This is an AudioTransportSource which will own it's assigned source
|
||||
struct AudioSourceOwningTransportSource : public AudioTransportSource
|
||||
{
|
||||
AudioSourceOwningTransportSource (PositionableAudioSource* s, double sampleRate) : source (s)
|
||||
{
|
||||
AudioTransportSource::setSource (s, 0, nullptr, sampleRate);
|
||||
}
|
||||
|
||||
~AudioSourceOwningTransportSource()
|
||||
{
|
||||
setSource (nullptr);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<PositionableAudioSource> source;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioSourceOwningTransportSource)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// An AudioSourcePlayer which will remove itself from the AudioDeviceManager's
|
||||
// callback list once it finishes playing its source
|
||||
struct AutoRemovingTransportSource : public AudioTransportSource,
|
||||
private Timer
|
||||
{
|
||||
AutoRemovingTransportSource (MixerAudioSource& mixerToUse, AudioTransportSource* ts, bool ownSource,
|
||||
int samplesPerBlock, double requiredSampleRate)
|
||||
: mixer (mixerToUse), transportSource (ts, ownSource)
|
||||
{
|
||||
jassert (ts != nullptr);
|
||||
|
||||
setSource (transportSource);
|
||||
|
||||
prepareToPlay (samplesPerBlock, requiredSampleRate);
|
||||
start();
|
||||
|
||||
mixer.addInputSource (this, true);
|
||||
startTimerHz (10);
|
||||
}
|
||||
|
||||
~AutoRemovingTransportSource()
|
||||
{
|
||||
setSource (nullptr);
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (! transportSource->isPlaying())
|
||||
mixer.removeInputSource (this);
|
||||
}
|
||||
|
||||
private:
|
||||
MixerAudioSource& mixer;
|
||||
OptionalScopedPointer<AudioTransportSource> transportSource;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AutoRemovingTransportSource)
|
||||
};
|
||||
|
||||
// An AudioSource which simply outputs a buffer
|
||||
class AudioBufferSource : public PositionableAudioSource
|
||||
{
|
||||
public:
|
||||
AudioBufferSource (AudioBuffer<float>* audioBuffer, bool ownBuffer, bool playOnAllChannels)
|
||||
: buffer (audioBuffer, ownBuffer),
|
||||
playAcrossAllChannels (playOnAllChannels)
|
||||
{}
|
||||
|
||||
//==============================================================================
|
||||
void setNextReadPosition (int64 newPosition) override
|
||||
{
|
||||
jassert (newPosition >= 0);
|
||||
|
||||
if (looping)
|
||||
newPosition = newPosition % static_cast<int64> (buffer->getNumSamples());
|
||||
|
||||
position = jmin (buffer->getNumSamples(), static_cast<int> (newPosition));
|
||||
}
|
||||
|
||||
int64 getNextReadPosition() const override { return static_cast<int64> (position); }
|
||||
int64 getTotalLength() const override { return static_cast<int64> (buffer->getNumSamples()); }
|
||||
|
||||
bool isLooping() const override { return looping; }
|
||||
void setLooping (bool shouldLoop) override { looping = shouldLoop; }
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (int, double) override {}
|
||||
void releaseResources() override {}
|
||||
|
||||
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
|
||||
{
|
||||
bufferToFill.clearActiveBufferRegion();
|
||||
|
||||
const int bufferSize = buffer->getNumSamples();
|
||||
const int samplesNeeded = bufferToFill.numSamples;
|
||||
const int samplesToCopy = jmin (bufferSize - position, samplesNeeded);
|
||||
|
||||
if (samplesToCopy > 0)
|
||||
{
|
||||
int maxInChannels = buffer->getNumChannels();
|
||||
int maxOutChannels = bufferToFill.buffer->getNumChannels();
|
||||
|
||||
if (! playAcrossAllChannels)
|
||||
maxOutChannels = jmin (maxOutChannels, maxInChannels);
|
||||
|
||||
for (int i = 0; i < maxOutChannels; ++i)
|
||||
bufferToFill.buffer->copyFrom (i, bufferToFill.startSample, *buffer,
|
||||
i % maxInChannels, position, samplesToCopy);
|
||||
}
|
||||
|
||||
position += samplesNeeded;
|
||||
|
||||
if (looping)
|
||||
position %= bufferSize;
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
OptionalScopedPointer<AudioBuffer<float>> buffer;
|
||||
int position = 0;
|
||||
bool looping = false, playAcrossAllChannels;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioBufferSource)
|
||||
};
|
||||
|
||||
SoundPlayer::SoundPlayer()
|
||||
: sampleRate (44100.0), bufferSize (512)
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
player.setSource (&mixer);
|
||||
}
|
||||
|
||||
SoundPlayer::~SoundPlayer()
|
||||
{
|
||||
mixer.removeAllInputs();
|
||||
player.setSource (nullptr);
|
||||
}
|
||||
|
||||
void SoundPlayer::play (const File& file)
|
||||
{
|
||||
if (file.existsAsFile())
|
||||
play (formatManager.createReaderFor (file), true);
|
||||
}
|
||||
|
||||
void SoundPlayer::play (const void* resourceData, size_t resourceSize)
|
||||
{
|
||||
if (resourceData != nullptr && resourceSize > 0)
|
||||
{
|
||||
MemoryInputStream* mem = new MemoryInputStream (resourceData, resourceSize, false);
|
||||
play (formatManager.createReaderFor (mem), true);
|
||||
}
|
||||
}
|
||||
|
||||
void SoundPlayer::play (AudioFormatReader* reader, bool deleteWhenFinished)
|
||||
{
|
||||
if (reader != nullptr)
|
||||
play (new AudioFormatReaderSource (reader, deleteWhenFinished), true, reader->sampleRate);
|
||||
}
|
||||
|
||||
void SoundPlayer::play (AudioBuffer<float>* buffer, bool deleteWhenFinished, bool playOnAllOutputChannels)
|
||||
{
|
||||
if (buffer != nullptr)
|
||||
play (new AudioBufferSource (buffer, deleteWhenFinished, playOnAllOutputChannels), true);
|
||||
}
|
||||
|
||||
void SoundPlayer::play (PositionableAudioSource* audioSource, bool deleteWhenFinished, double fileSampleRate)
|
||||
{
|
||||
if (audioSource != nullptr)
|
||||
{
|
||||
AudioTransportSource* transport = dynamic_cast<AudioTransportSource*> (audioSource);
|
||||
|
||||
if (transport == nullptr)
|
||||
{
|
||||
if (deleteWhenFinished)
|
||||
{
|
||||
transport = new AudioSourceOwningTransportSource (audioSource, fileSampleRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
transport = new AudioTransportSource();
|
||||
transport->setSource (audioSource, 0, nullptr, fileSampleRate);
|
||||
deleteWhenFinished = true;
|
||||
}
|
||||
}
|
||||
|
||||
transport->start();
|
||||
transport->prepareToPlay (bufferSize, sampleRate);
|
||||
|
||||
new AutoRemovingTransportSource (mixer, transport, deleteWhenFinished, bufferSize, sampleRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (deleteWhenFinished)
|
||||
delete audioSource;
|
||||
}
|
||||
}
|
||||
|
||||
void SoundPlayer::playTestSound()
|
||||
{
|
||||
auto soundLength = (int) sampleRate;
|
||||
double frequency = 440.0;
|
||||
float amplitude = 0.5f;
|
||||
|
||||
auto phasePerSample = MathConstants<double>::twoPi / (sampleRate / frequency);
|
||||
|
||||
auto* newSound = new AudioBuffer<float> (1, soundLength);
|
||||
|
||||
for (int i = 0; i < soundLength; ++i)
|
||||
newSound->setSample (0, i, amplitude * (float) std::sin (i * phasePerSample));
|
||||
|
||||
newSound->applyGainRamp (0, 0, soundLength / 10, 0.0f, 1.0f);
|
||||
newSound->applyGainRamp (0, soundLength - soundLength / 4, soundLength / 4, 1.0f, 0.0f);
|
||||
|
||||
play (newSound, true, true);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void SoundPlayer::audioDeviceIOCallback (const float** inputChannelData,
|
||||
int numInputChannels,
|
||||
float** outputChannelData,
|
||||
int numOutputChannels,
|
||||
int numSamples)
|
||||
{
|
||||
player.audioDeviceIOCallback (inputChannelData, numInputChannels,
|
||||
outputChannelData, numOutputChannels,
|
||||
numSamples);
|
||||
}
|
||||
|
||||
void SoundPlayer::audioDeviceAboutToStart (AudioIODevice* device)
|
||||
{
|
||||
if (device != nullptr)
|
||||
{
|
||||
sampleRate = device->getCurrentSampleRate();
|
||||
bufferSize = device->getCurrentBufferSizeSamples();
|
||||
}
|
||||
|
||||
player.audioDeviceAboutToStart (device);
|
||||
}
|
||||
|
||||
void SoundPlayer::audioDeviceStopped()
|
||||
{
|
||||
player.audioDeviceStopped();
|
||||
}
|
||||
|
||||
void SoundPlayer::audioDeviceError (const String& errorMessage)
|
||||
{
|
||||
player.audioDeviceError (errorMessage);
|
||||
}
|
||||
|
||||
} // namespace juce
|
137
modules/juce_audio_utils/players/juce_SoundPlayer.h
Normal file
137
modules/juce_audio_utils/players/juce_SoundPlayer.h
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-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 sound player that you can add to the AudioDeviceManager to play
|
||||
simple sounds.
|
||||
|
||||
@see AudioProcessor, AudioProcessorGraph
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API SoundPlayer : public AudioIODeviceCallback
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
SoundPlayer();
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~SoundPlayer();
|
||||
|
||||
//==============================================================================
|
||||
/** Plays a sound from a file. */
|
||||
void play (const File& file);
|
||||
|
||||
/** Convenient method to play sound from a JUCE resource. */
|
||||
void play (const void* resourceData, size_t resourceSize);
|
||||
|
||||
/** Plays the sound from an audio format reader.
|
||||
|
||||
If deleteWhenFinished is true then the format reader will be
|
||||
automatically deleted once the sound has finished playing.
|
||||
*/
|
||||
void play (AudioFormatReader* buffer, bool deleteWhenFinished = false);
|
||||
|
||||
/** Plays the sound from a positionable audio source.
|
||||
|
||||
This will output the sound coming from a positionable audio source.
|
||||
This gives you slightly more control over the sound playback compared
|
||||
to the other playSound methods. For example, if you would like to
|
||||
stop the sound prematurely you can call this method with a
|
||||
TransportAudioSource and then call audioSource->stop. Note that,
|
||||
you must call audioSource->start to start the playback, if your
|
||||
audioSource is a TransportAudioSource.
|
||||
|
||||
The audio device manager will not hold any references to this audio
|
||||
source once the audio source has stopped playing for any reason,
|
||||
for example when the sound has finished playing or when you have
|
||||
called audioSource->stop. Therefore, calling audioSource->start() on
|
||||
a finished audioSource will not restart the sound again. If this is
|
||||
desired simply call playSound with the same audioSource again.
|
||||
|
||||
@param audioSource the audio source to play
|
||||
@param deleteWhenFinished If this is true then the audio source will
|
||||
be deleted once the device manager has finished
|
||||
playing.
|
||||
@param sampleRateOfSource The sample rate of the source. If this is zero, JUCE
|
||||
will assume that the sample rate is the same as the
|
||||
audio output device.
|
||||
*/
|
||||
void play (PositionableAudioSource* audioSource, bool deleteWhenFinished = false,
|
||||
double sampleRateOfSource = 0.0);
|
||||
|
||||
/** Plays the sound from an audio sample buffer.
|
||||
|
||||
This will output the sound contained in an audio sample buffer. If
|
||||
deleteWhenFinished is true then the audio sample buffer will be
|
||||
automatically deleted once the sound has finished playing.
|
||||
|
||||
If playOnAllOutputChannels is true, then if there are more output channels
|
||||
than buffer channels, then the ones that are available will be re-used on
|
||||
multiple outputs so that something is sent to all output channels. If it
|
||||
is false, then the buffer will just be played on the first output channels.
|
||||
*/
|
||||
void play (AudioBuffer<float>* buffer,
|
||||
bool deleteWhenFinished = false,
|
||||
bool playOnAllOutputChannels = false);
|
||||
|
||||
/** Plays a beep through the current audio device.
|
||||
|
||||
This is here to allow the audio setup UI panels to easily include a "test"
|
||||
button so that the user can check where the audio is coming from.
|
||||
*/
|
||||
void playTestSound();
|
||||
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
void audioDeviceIOCallback (const float**, int, float**, int, int) override;
|
||||
/** @internal */
|
||||
void audioDeviceAboutToStart (AudioIODevice*) override;
|
||||
/** @internal */
|
||||
void audioDeviceStopped() override;
|
||||
/** @internal */
|
||||
void audioDeviceError (const String& errorMessage) override;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioFormatManager formatManager;
|
||||
AudioSourcePlayer player;
|
||||
MixerAudioSource mixer;
|
||||
OwnedArray<AudioSource> sources;
|
||||
|
||||
//==============================================================================
|
||||
double sampleRate;
|
||||
int bufferSize;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SoundPlayer)
|
||||
};
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user