From 743f04dc014d7afd3b2a5e53fc317b8ddce06855 Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 7 Oct 2014 15:57:56 +0100 Subject: [PATCH] Refactored the Systhesiser class's voice-stealing methods and gave it a better default voice-stealing algorithm. --- .../synthesisers/juce_Synthesiser.cpp | 67 ++++++++++++++++--- .../synthesisers/juce_Synthesiser.h | 42 +++++++----- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/modules/juce_audio_basics/synthesisers/juce_Synthesiser.cpp b/modules/juce_audio_basics/synthesisers/juce_Synthesiser.cpp index 6a38130f18..6a889b4b32 100644 --- a/modules/juce_audio_basics/synthesisers/juce_Synthesiser.cpp +++ b/modules/juce_audio_basics/synthesisers/juce_Synthesiser.cpp @@ -236,7 +236,7 @@ void Synthesiser::noteOn (const int midiChannel, stopVoice (voice, 1.0f, true); } - startVoice (findFreeVoice (sound, shouldStealNotes), + startVoice (findFreeVoice (sound, midiChannel, midiNoteNumber, shouldStealNotes), sound, midiChannel, midiNoteNumber, velocity); } } @@ -416,7 +416,9 @@ void Synthesiser::handleSoftPedal (int midiChannel, bool /*isDown*/) } //============================================================================== -SynthesiserVoice* Synthesiser::findFreeVoice (SynthesiserSound* soundToPlay, const bool stealIfNoneAvailable) const +SynthesiserVoice* Synthesiser::findFreeVoice (SynthesiserSound* soundToPlay, + int midiChannel, int midiNoteNumber, + const bool stealIfNoneAvailable) const { const ScopedLock sl (lock); @@ -429,25 +431,68 @@ SynthesiserVoice* Synthesiser::findFreeVoice (SynthesiserSound* soundToPlay, con } if (stealIfNoneAvailable) - return findVoiceToSteal (soundToPlay); + return findVoiceToSteal (soundToPlay, midiChannel, midiNoteNumber); return nullptr; } -SynthesiserVoice* Synthesiser::findVoiceToSteal (SynthesiserSound* soundToPlay) const +struct VoiceAgeSorter { - // currently this just steals the one that's been playing the longest, but could be made a bit smarter.. - SynthesiserVoice* oldest = nullptr; + static int compareElements (SynthesiserVoice* v1, SynthesiserVoice* v2) noexcept + { + return v1->wasStartedBefore (*v2) ? 1 : (v2->wasStartedBefore (*v1) ? -1 : 0); + } +}; + +SynthesiserVoice* Synthesiser::findVoiceToSteal (SynthesiserSound* soundToPlay, + int /*midiChannel*/, int midiNoteNumber) const +{ + SynthesiserVoice* bottom = nullptr; + SynthesiserVoice* top = nullptr; + + // this is a list of voices we can steal, sorted by how long they've been running + Array usableVoices; + usableVoices.ensureStorageAllocated (voices.size()); 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; + if (voice->canPlaySound (soundToPlay)) + { + VoiceAgeSorter sorter; + usableVoices.addSorted (sorter, voice); + + const int note = voice->getCurrentlyPlayingNote(); + + if (bottom == nullptr || note < bottom->getCurrentlyPlayingNote()) + bottom = voice; + + if (top == nullptr || note > top->getCurrentlyPlayingNote()) + top = voice; + } } - jassert (oldest != nullptr); - return oldest; + jassert (bottom != nullptr && top != nullptr); + + // The oldest note that's playing with the target pitch playing is ideal.. + for (int i = 0; i < usableVoices.size(); ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice->getCurrentlyPlayingNote() == midiNoteNumber) + return voice; + } + + // ..otherwise, look for the oldest note that isn't the top or bottom note.. + for (int i = 0; i < usableVoices.size(); ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice != bottom && voice != top) + return voice; + } + + // ..otherwise, there's only one or two voices to choose from - we'll return the top one.. + return top; } diff --git a/modules/juce_audio_basics/synthesisers/juce_Synthesiser.h b/modules/juce_audio_basics/synthesisers/juce_Synthesiser.h index a704b5fcd5..6a32cedaa0 100644 --- a/modules/juce_audio_basics/synthesisers/juce_Synthesiser.h +++ b/modules/juce_audio_basics/synthesisers/juce_Synthesiser.h @@ -185,6 +185,11 @@ public: */ virtual void setCurrentPlaybackSampleRate (double newRate); + /** Returns the current target sample rate at which rendering is being done. + Subclasses may need to know this so that they can pitch things correctly. + */ + double getSampleRate() const noexcept { return currentSampleRate; } + /** Returns true if the voice is currently playing a sound which is mapped to the given midi channel. @@ -205,13 +210,6 @@ public: bool wasStartedBefore (const SynthesiserVoice& other) const noexcept; protected: - //============================================================================== - /** Returns the current target sample rate at which rendering is being done. - - This is available for subclasses so they can pitch things correctly. - */ - double getSampleRate() const { return currentSampleRate; } - /** Resets the state of this voice after a sound has finished playing. The subclass must call this when it finishes playing a note and becomes available @@ -275,8 +273,7 @@ class JUCE_API Synthesiser public: //============================================================================== /** Creates a new synthesiser. - - You'll need to add some sounds and voices before it'll make any sound.. + You'll need to add some sounds and voices before it'll make any sound. */ Synthesiser(); @@ -471,6 +468,11 @@ public: int startSample, int numSamples); + /** Returns the current target sample rate at which rendering is being done. + Subclasses may need to know this so that they can pitch things correctly. + */ + double getSampleRate() const noexcept { return sampleRate; } + protected: //============================================================================== /** This is used to control access to the rendering callback and the note trigger methods. */ @@ -489,21 +491,27 @@ protected: virtual void renderVoices (AudioSampleBuffer& outputAudio, int startSample, int numSamples); - /** Searches through the voices to find one that's not currently playing, and which - can play the given sound. + /** Searches through the voices to find one that's not currently playing, and + which can play the given sound. Returns nullptr if all voices are busy and stealing isn't enabled. - This can be overridden to implement custom voice-stealing algorithms. + To implement a custom note-stealing algorithm, you can either override this + method, or (preferably) override findVoiceToSteal(). */ virtual SynthesiserVoice* findFreeVoice (SynthesiserSound* soundToPlay, - const bool stealIfNoneAvailable) const; + int midiChannel, + int midiNoteNumber, + bool stealIfNoneAvailable) const; /** Chooses a voice that is most suitable for being re-used. - The default method returns the one that has been playing for the longest, but - you may want to override this and do something more cunning instead. + The default method will attempt to find the oldest voice that isn't the + bottom or top note being played. If that's not suitable for your synth, + you can override this method and do something more cunning instead. */ - virtual SynthesiserVoice* findVoiceToSteal (SynthesiserSound* soundToPlay) const; + virtual SynthesiserVoice* findVoiceToSteal (SynthesiserSound* soundToPlay, + int midiChannel, + int midiNoteNumber) const; /** Starts a specified voice playing a particular sound. @@ -532,6 +540,8 @@ private: // Note the new parameters for these methods. virtual int findFreeVoice (const bool) const { return 0; } virtual int noteOff (int, int, int) { return 0; } + virtual int findFreeVoice (SynthesiserSound*, const bool) { return 0; } + virtual int findVoiceToSteal (SynthesiserSound*) const { return 0; } #endif JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Synthesiser)