|
|
|
/*
|
|
|
|
==============================================================================
|
|
|
|
|
|
|
|
This file is part of the JUCE library.
|
|
|
|
Copyright (c) 2013 - Raw Material Software Ltd.
|
|
|
|
|
|
|
|
Permission is granted to use this software under the terms of either:
|
|
|
|
a) the GPL v2 (or any later version)
|
|
|
|
b) the Affero GPL v3
|
|
|
|
|
|
|
|
Details of these licenses can be found at: www.gnu.org/licenses
|
|
|
|
|
|
|
|
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
To release a closed-source product which uses JUCE, commercial licenses are
|
|
|
|
available: visit www.juce.com for more information.
|
|
|
|
|
|
|
|
==============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
SynthesiserSound::SynthesiserSound() {}
|
|
|
|
SynthesiserSound::~SynthesiserSound() {}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
SynthesiserVoice::SynthesiserVoice()
|
|
|
|
: currentSampleRate (44100.0),
|
|
|
|
currentlyPlayingNote (-1),
|
|
|
|
noteOnTime (0),
|
|
|
|
keyIsDown (false),
|
|
|
|
sostenutoPedalDown (false)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
SynthesiserVoice::~SynthesiserVoice()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SynthesiserVoice::isPlayingChannel (const int midiChannel) const
|
|
|
|
{
|
|
|
|
return currentlyPlayingSound != nullptr
|
|
|
|
&& currentlyPlayingSound->appliesToChannel (midiChannel);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SynthesiserVoice::setCurrentPlaybackSampleRate (const double newRate)
|
|
|
|
{
|
|
|
|
currentSampleRate = newRate;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SynthesiserVoice::clearCurrentNote()
|
|
|
|
{
|
|
|
|
currentlyPlayingNote = -1;
|
|
|
|
currentlyPlayingSound = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SynthesiserVoice::aftertouchChanged (int) {}
|
|
|
|
|
|
|
|
bool SynthesiserVoice::wasStartedBefore (const SynthesiserVoice& other) const noexcept
|
|
|
|
{
|
|
|
|
return noteOnTime < other.noteOnTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
Synthesiser::Synthesiser()
|
|
|
|
: sampleRate (0),
|
|
|
|
lastNoteOnCounter (0),
|
|
|
|
shouldStealNotes (true)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < numElementsInArray (lastPitchWheelValues); ++i)
|
|
|
|
lastPitchWheelValues[i] = 0x2000;
|
|
|
|
}
|
|
|
|
|
|
|
|
Synthesiser::~Synthesiser()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
SynthesiserVoice* Synthesiser::getVoice (const int index) const
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
return voices [index];
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::clearVoices()
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
voices.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
SynthesiserVoice* Synthesiser::addVoice (SynthesiserVoice* const newVoice)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
return voices.add (newVoice);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::removeVoice (const int index)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
voices.remove (index);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::clearSounds()
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
sounds.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
SynthesiserSound* Synthesiser::addSound (const SynthesiserSound::Ptr& newSound)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
return sounds.add (newSound);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::removeSound (const int index)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
sounds.remove (index);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::setNoteStealingEnabled (const bool shouldSteal)
|
|
|
|
{
|
|
|
|
shouldStealNotes = shouldSteal;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void Synthesiser::setCurrentPlaybackSampleRate (const double newRate)
|
|
|
|
{
|
|
|
|
if (sampleRate != newRate)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
allNotesOff (0, false);
|
|
|
|
|
|
|
|
sampleRate = newRate;
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
voices.getUnchecked (i)->setCurrentPlaybackSampleRate (newRate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::renderNextBlock (AudioSampleBuffer& outputBuffer, const MidiBuffer& midiData,
|
|
|
|
int startSample, int numSamples)
|
|
|
|
{
|
|
|
|
// must set the sample rate before using this!
|
|
|
|
jassert (sampleRate != 0);
|
|
|
|
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
MidiBuffer::Iterator midiIterator (midiData);
|
|
|
|
midiIterator.setNextSamplePosition (startSample);
|
|
|
|
MidiMessage m (0xf4, 0.0);
|
|
|
|
|
|
|
|
while (numSamples > 0)
|
|
|
|
{
|
|
|
|
int midiEventPos;
|
|
|
|
const bool useEvent = midiIterator.getNextEvent (m, midiEventPos)
|
|
|
|
&& midiEventPos < startSample + numSamples;
|
|
|
|
|
|
|
|
const int numThisTime = useEvent ? midiEventPos - startSample
|
|
|
|
: numSamples;
|
|
|
|
|
|
|
|
if (numThisTime > 0)
|
|
|
|
{
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
voices.getUnchecked (i)->renderNextBlock (outputBuffer, startSample, numThisTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (useEvent)
|
|
|
|
handleMidiEvent (m);
|
|
|
|
|
|
|
|
startSample += numThisTime;
|
|
|
|
numSamples -= numThisTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleMidiEvent (const MidiMessage& m)
|
|
|
|
{
|
|
|
|
if (m.isNoteOn())
|
|
|
|
{
|
|
|
|
noteOn (m.getChannel(), m.getNoteNumber(), m.getFloatVelocity());
|
|
|
|
}
|
|
|
|
else if (m.isNoteOff())
|
|
|
|
{
|
|
|
|
noteOff (m.getChannel(), m.getNoteNumber(), true);
|
|
|
|
}
|
|
|
|
else if (m.isAllNotesOff() || m.isAllSoundOff())
|
|
|
|
{
|
|
|
|
allNotesOff (m.getChannel(), true);
|
|
|
|
}
|
|
|
|
else if (m.isPitchWheel())
|
|
|
|
{
|
|
|
|
const int channel = m.getChannel();
|
|
|
|
const int wheelPos = m.getPitchWheelValue();
|
|
|
|
lastPitchWheelValues [channel - 1] = wheelPos;
|
|
|
|
|
|
|
|
handlePitchWheel (channel, wheelPos);
|
|
|
|
}
|
|
|
|
else if (m.isAftertouch())
|
|
|
|
{
|
|
|
|
handleAftertouch (m.getChannel(), m.getNoteNumber(), m.getAfterTouchValue());
|
|
|
|
}
|
|
|
|
else if (m.isController())
|
|
|
|
{
|
|
|
|
handleController (m.getChannel(), m.getControllerNumber(), m.getControllerValue());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
void Synthesiser::noteOn (const int midiChannel,
|
|
|
|
const int midiNoteNumber,
|
|
|
|
const float velocity)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = sounds.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserSound* const sound = sounds.getUnchecked(i);
|
|
|
|
|
|
|
|
if (sound->appliesToNote (midiNoteNumber)
|
|
|
|
&& sound->appliesToChannel (midiChannel))
|
|
|
|
{
|
|
|
|
// If hitting a note that's still ringing, stop it first (it could be
|
|
|
|
// still playing because of the sustain or sostenuto pedal).
|
|
|
|
for (int j = voices.size(); --j >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (j);
|
|
|
|
|
|
|
|
if (voice->getCurrentlyPlayingNote() == midiNoteNumber
|
|
|
|
&& voice->isPlayingChannel (midiChannel))
|
|
|
|
stopVoice (voice, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
startVoice (findFreeVoice (sound, shouldStealNotes),
|
|
|
|
sound, midiChannel, midiNoteNumber, velocity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::startVoice (SynthesiserVoice* const voice,
|
|
|
|
SynthesiserSound* const sound,
|
|
|
|
const int midiChannel,
|
|
|
|
const int midiNoteNumber,
|
|
|
|
const float velocity)
|
|
|
|
{
|
|
|
|
if (voice != nullptr && sound != nullptr)
|
|
|
|
{
|
|
|
|
if (voice->currentlyPlayingSound != nullptr)
|
|
|
|
voice->stopNote (false);
|
|
|
|
|
|
|
|
voice->startNote (midiNoteNumber, velocity, sound,
|
|
|
|
lastPitchWheelValues [midiChannel - 1]);
|
|
|
|
|
|
|
|
voice->currentlyPlayingNote = midiNoteNumber;
|
|
|
|
voice->noteOnTime = ++lastNoteOnCounter;
|
|
|
|
voice->currentlyPlayingSound = sound;
|
|
|
|
voice->keyIsDown = true;
|
|
|
|
voice->sostenutoPedalDown = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::stopVoice (SynthesiserVoice* voice, const bool allowTailOff)
|
|
|
|
{
|
|
|
|
jassert (voice != nullptr);
|
|
|
|
|
|
|
|
voice->stopNote (allowTailOff);
|
|
|
|
|
|
|
|
// the subclass MUST call clearCurrentNote() if it's not tailing off! RTFM for stopNote()!
|
|
|
|
jassert (allowTailOff || (voice->getCurrentlyPlayingNote() < 0 && voice->getCurrentlyPlayingSound() == 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::noteOff (const int midiChannel,
|
|
|
|
const int midiNoteNumber,
|
|
|
|
const bool allowTailOff)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->getCurrentlyPlayingNote() == midiNoteNumber)
|
|
|
|
{
|
|
|
|
if (SynthesiserSound* const sound = voice->getCurrentlyPlayingSound())
|
|
|
|
{
|
|
|
|
if (sound->appliesToNote (midiNoteNumber)
|
|
|
|
&& sound->appliesToChannel (midiChannel))
|
|
|
|
{
|
|
|
|
voice->keyIsDown = false;
|
|
|
|
|
|
|
|
if (! (sustainPedalsDown [midiChannel] || voice->sostenutoPedalDown))
|
|
|
|
stopVoice (voice, allowTailOff);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::allNotesOff (const int midiChannel, const bool allowTailOff)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel))
|
|
|
|
voice->stopNote (allowTailOff);
|
|
|
|
}
|
|
|
|
|
|
|
|
sustainPedalsDown.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handlePitchWheel (const int midiChannel, const int wheelValue)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel))
|
|
|
|
voice->pitchWheelMoved (wheelValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleController (const int midiChannel,
|
|
|
|
const int controllerNumber,
|
|
|
|
const int controllerValue)
|
|
|
|
{
|
|
|
|
switch (controllerNumber)
|
|
|
|
{
|
|
|
|
case 0x40: handleSustainPedal (midiChannel, controllerValue >= 64); break;
|
|
|
|
case 0x42: handleSostenutoPedal (midiChannel, controllerValue >= 64); break;
|
|
|
|
case 0x43: handleSoftPedal (midiChannel, controllerValue >= 64); break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel))
|
|
|
|
voice->controllerMoved (controllerNumber, controllerValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleAftertouch (int midiChannel, int midiNoteNumber, int aftertouchValue)
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->getCurrentlyPlayingNote() == midiNoteNumber
|
|
|
|
&& (midiChannel <= 0 || voice->isPlayingChannel (midiChannel)))
|
|
|
|
voice->aftertouchChanged (aftertouchValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleSustainPedal (int midiChannel, bool isDown)
|
|
|
|
{
|
|
|
|
jassert (midiChannel > 0 && midiChannel <= 16);
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
if (isDown)
|
|
|
|
{
|
|
|
|
sustainPedalsDown.setBit (midiChannel);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->isPlayingChannel (midiChannel) && ! voice->keyIsDown)
|
|
|
|
stopVoice (voice, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
sustainPedalsDown.clearBit (midiChannel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleSostenutoPedal (int midiChannel, bool isDown)
|
|
|
|
{
|
|
|
|
jassert (midiChannel > 0 && midiChannel <= 16);
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = voices.size(); --i >= 0;)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->isPlayingChannel (midiChannel))
|
|
|
|
{
|
|
|
|
if (isDown)
|
|
|
|
voice->sostenutoPedalDown = true;
|
|
|
|
else if (voice->sostenutoPedalDown)
|
|
|
|
stopVoice (voice, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Synthesiser::handleSoftPedal (int midiChannel, bool /*isDown*/)
|
|
|
|
{
|
|
|
|
(void) midiChannel;
|
|
|
|
jassert (midiChannel > 0 && midiChannel <= 16);
|
|
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
SynthesiserVoice* Synthesiser::findFreeVoice (SynthesiserSound* soundToPlay, const bool stealIfNoneAvailable) const
|
|
|
|
{
|
|
|
|
const ScopedLock sl (lock);
|
|
|
|
|
|
|
|
for (int i = 0; i < voices.size(); ++i)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->getCurrentlyPlayingNote() < 0 && voice->canPlaySound (soundToPlay))
|
|
|
|
return voice;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stealIfNoneAvailable)
|
|
|
|
return findVoiceToSteal (soundToPlay);
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
SynthesiserVoice* Synthesiser::findVoiceToSteal (SynthesiserSound* soundToPlay) const
|
|
|
|
{
|
|
|
|
// currently this just steals the one that's been playing the longest, but could be made a bit smarter..
|
|
|
|
SynthesiserVoice* oldest = nullptr;
|
|
|
|
|
|
|
|
for (int i = 0; i < voices.size(); ++i)
|
|
|
|
{
|
|
|
|
SynthesiserVoice* const voice = voices.getUnchecked (i);
|
|
|
|
|
|
|
|
if (voice->canPlaySound (soundToPlay)
|
|
|
|
&& (oldest == nullptr || voice->wasStartedBefore (*oldest)))
|
|
|
|
oldest = voice;
|
|
|
|
}
|
|
|
|
|
|
|
|
jassert (oldest != nullptr);
|
|
|
|
return oldest;
|
|
|
|
}
|