diff --git a/source/modules/sfzero/sfzero/SFZDebug.cpp b/source/modules/sfzero/sfzero/SFZDebug.cpp index d80bcf293..e2476bb5f 100644 --- a/source/modules/sfzero/sfzero/SFZDebug.cpp +++ b/source/modules/sfzero/sfzero/SFZDebug.cpp @@ -7,9 +7,12 @@ #include "SFZDebug.h" #include +namespace sfzero +{ + #ifdef DEBUG -void sfzero::dbgprintf(const char *msg, ...) +void dbgprintf(const char *msg, ...) { va_list args; @@ -22,3 +25,5 @@ void sfzero::dbgprintf(const char *msg, ...) } #endif // JUCE_DEBUG + +} diff --git a/source/modules/sfzero/sfzero/SFZEG.cpp b/source/modules/sfzero/sfzero/SFZEG.cpp index 70cdf6c83..d4d3ef5c2 100644 --- a/source/modules/sfzero/sfzero/SFZEG.cpp +++ b/source/modules/sfzero/sfzero/SFZEG.cpp @@ -6,16 +6,19 @@ *************************************************************************************/ #include "SFZEG.h" +namespace sfzero +{ + static const float fastReleaseTime = 0.01f; -sfzero::EG::EG() +EG::EG() : segment_(), sampleRate_(0), exponentialDecay_(false), level_(0), slope_(0), samplesUntilNextSegment_(0), segmentIsExponential_(false) { } -void sfzero::EG::setExponentialDecay(bool newExponentialDecay) { exponentialDecay_ = newExponentialDecay; } +void EG::setExponentialDecay(bool newExponentialDecay) { exponentialDecay_ = newExponentialDecay; } -void sfzero::EG::startNote(const EGParameters *newParameters, float floatVelocity, double newSampleRate, +void EG::startNote(const EGParameters *newParameters, float floatVelocity, double newSampleRate, const EGParameters *velMod) { parameters_ = *newParameters; @@ -41,7 +44,7 @@ void sfzero::EG::startNote(const EGParameters *newParameters, float floatVelocit startDelay(); } -void sfzero::EG::nextSegment() +void EG::nextSegment() { switch (segment_) { @@ -72,9 +75,9 @@ void sfzero::EG::nextSegment() } } -void sfzero::EG::noteOff() { startRelease(); } +void EG::noteOff() { startRelease(); } -void sfzero::EG::fastRelease() +void EG::fastRelease() { segment_ = Release; samplesUntilNextSegment_ = static_cast(fastReleaseTime * sampleRate_); @@ -82,7 +85,7 @@ void sfzero::EG::fastRelease() segmentIsExponential_ = false; } -void sfzero::EG::startDelay() +void EG::startDelay() { if (parameters_.delay <= 0) { @@ -98,7 +101,7 @@ void sfzero::EG::startDelay() } } -void sfzero::EG::startAttack() +void EG::startAttack() { if (parameters_.attack <= 0) { @@ -114,7 +117,7 @@ void sfzero::EG::startAttack() } } -void sfzero::EG::startHold() +void EG::startHold() { if (parameters_.hold <= 0) { @@ -131,7 +134,7 @@ void sfzero::EG::startHold() } } -void sfzero::EG::startDecay() +void EG::startDecay() { if (parameters_.decay <= 0) { @@ -170,7 +173,7 @@ void sfzero::EG::startDecay() } } -void sfzero::EG::startSustain() +void EG::startSustain() { if (parameters_.sustain <= 0) { @@ -186,7 +189,7 @@ void sfzero::EG::startSustain() } } -void sfzero::EG::startRelease() +void EG::startRelease() { float release = parameters_.release; @@ -212,4 +215,6 @@ void sfzero::EG::startRelease() } } -const float sfzero::EG::BottomLevel = 0.001f; +const float EG::BottomLevel = 0.001f; + +} diff --git a/source/modules/sfzero/sfzero/SFZReader.cpp b/source/modules/sfzero/sfzero/SFZReader.cpp index 05b71c848..b213ca495 100644 --- a/source/modules/sfzero/sfzero/SFZReader.cpp +++ b/source/modules/sfzero/sfzero/SFZReader.cpp @@ -10,11 +10,14 @@ #include "water/memory/MemoryBlock.h" -sfzero::Reader::Reader(sfzero::Sound *soundIn) : sound_(soundIn), line_(1) {} +namespace sfzero +{ + +Reader::Reader(Sound *soundIn) : sound_(soundIn), line_(1) {} -sfzero::Reader::~Reader() {} +Reader::~Reader() {} -void sfzero::Reader::read(const water::File &file) +void Reader::read(const water::File &file) { water::MemoryBlock contents; bool ok = file.loadFileAsData(contents); @@ -28,15 +31,15 @@ void sfzero::Reader::read(const water::File &file) read(static_cast(contents.getData()), static_cast(contents.getSize())); } -void sfzero::Reader::read(const char *text, unsigned int length) +void Reader::read(const char *text, unsigned int length) { const char *p = text; const char *end = text + length; char c = 0; - sfzero::Region curGroup; - sfzero::Region curRegion; - sfzero::Region *buildingRegion = nullptr; + Region curGroup; + Region curRegion; + Region *buildingRegion = nullptr; bool inControl = false; water::String defaultPath; @@ -108,7 +111,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) error("Unterminated tag"); goto fatalError; } - sfzero::StringSlice tag(tagStart, p - 1); + StringSlice tag(tagStart, p - 1); if (tag == "region") { if (buildingRegion && (buildingRegion == &curRegion)) @@ -176,7 +179,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) error("Malformed parameter"); goto nextElement; } - sfzero::StringSlice opcode(parameterStart, p - 1); + StringSlice opcode(parameterStart, p - 1); if (inControl) { if (opcode == "default_path") @@ -259,7 +262,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) } else if (opcode == "trigger") { - buildingRegion->trigger = static_cast(triggerValue(value)); + buildingRegion->trigger = static_cast(triggerValue(value)); } else if (opcode == "group") { @@ -290,7 +293,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) bool modeIsSupported = value == "no_loop" || value == "one_shot" || value == "loop_continuous"; if (modeIsSupported) { - buildingRegion->loop_mode = static_cast(loopModeValue(value)); + buildingRegion->loop_mode = static_cast(loopModeValue(value)); } else { @@ -432,7 +435,7 @@ fatalError: } } -const char *sfzero::Reader::handleLineEnd(const char *p) +const char *Reader::handleLineEnd(const char *p) { // Check for DOS-style line ending. char lineEndChar = *p++; @@ -445,7 +448,7 @@ const char *sfzero::Reader::handleLineEnd(const char *p) return p; } -const char *sfzero::Reader::readPathInto(water::String *pathOut, const char *pIn, const char *endIn) +const char *Reader::readPathInto(water::String *pathOut, const char *pIn, const char *endIn) { // Paths are kind of funny to parse because they can contain whitespace. const char *p = pIn; @@ -497,7 +500,7 @@ const char *sfzero::Reader::readPathInto(water::String *pathOut, const char *pIn return p; } -int sfzero::Reader::keyValue(const water::String &str) +int Reader::keyValue(const water::String &str) { auto chars = str.toRawUTF8(); @@ -542,56 +545,58 @@ int sfzero::Reader::keyValue(const water::String &str) return result; } -int sfzero::Reader::triggerValue(const water::String &str) +int Reader::triggerValue(const water::String &str) { if (str == "release") { - return sfzero::Region::release; + return Region::release; } if (str == "first") { - return sfzero::Region::first; + return Region::first; } if (str == "legato") { - return sfzero::Region::legato; + return Region::legato; } - return sfzero::Region::attack; + return Region::attack; } -int sfzero::Reader::loopModeValue(const water::String &str) +int Reader::loopModeValue(const water::String &str) { if (str == "no_loop") { - return sfzero::Region::no_loop; + return Region::no_loop; } if (str == "one_shot") { - return sfzero::Region::one_shot; + return Region::one_shot; } if (str == "loop_continuous") { - return sfzero::Region::loop_continuous; + return Region::loop_continuous; } if (str == "loop_sustain") { - return sfzero::Region::loop_sustain; + return Region::loop_sustain; } - return sfzero::Region::sample_loop; + return Region::sample_loop; } -void sfzero::Reader::finishRegion(sfzero::Region *region) +void Reader::finishRegion(Region *region) { - sfzero::Region *newRegion = new sfzero::Region(); + Region *newRegion = new Region(); *newRegion = *region; sound_->addRegion(newRegion); } -void sfzero::Reader::error(const water::String &message) +void Reader::error(const water::String &message) { water::String fullMessage = message; fullMessage += " (line " + water::String(line_) + ")."; sound_->addError(fullMessage); } + +} diff --git a/source/modules/sfzero/sfzero/SFZRegion.cpp b/source/modules/sfzero/sfzero/SFZRegion.cpp index 318bd611d..53db32911 100644 --- a/source/modules/sfzero/sfzero/SFZRegion.cpp +++ b/source/modules/sfzero/sfzero/SFZRegion.cpp @@ -4,10 +4,14 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #include "SFZRegion.h" #include "SFZSample.h" -void sfzero::EGParameters::clear() +namespace sfzero +{ + +void EGParameters::clear() { delay = 0.0; start = 0.0; @@ -18,15 +22,15 @@ void sfzero::EGParameters::clear() release = 0.0; } -void sfzero::EGParameters::clearMod() +void EGParameters::clearMod() { // Clear for velocity or other modification. delay = start = attack = hold = decay = sustain = release = 0.0; } -sfzero::Region::Region() { clear(); } +Region::Region() { clear(); } -void sfzero::Region::clear() +void Region::clear() { memset(this, 0, sizeof(*this)); hikey = 127; @@ -41,7 +45,7 @@ void sfzero::Region::clear() ampeg_veltrack.clearMod(); } -void sfzero::Region::clearForSF2() +void Region::clearForSF2() { clear(); pitch_keycenter = -1; @@ -56,7 +60,7 @@ void sfzero::Region::clearForSF2() ampeg.release = -12000.0; } -void sfzero::Region::clearForRelativeSF2() +void Region::clearForRelativeSF2() { clear(); pitch_keytrack = 0; @@ -64,7 +68,7 @@ void sfzero::Region::clearForRelativeSF2() ampeg.sustain = 0.0; } -void sfzero::Region::addForSF2(sfzero::Region *other) +void Region::addForSF2(Region *other) { offset += other->offset; end += other->end; @@ -84,7 +88,7 @@ void sfzero::Region::addForSF2(sfzero::Region *other) ampeg.release += other->ampeg.release; } -void sfzero::Region::sf2ToSFZ() +void Region::sf2ToSFZ() { // EG times need to be converted from timecents to seconds. ampeg.delay = timecents2Secs(static_cast(ampeg.delay)); @@ -132,7 +136,7 @@ void sfzero::Region::sf2ToSFZ() } } -water::String sfzero::Region::dump() +water::String Region::dump() { water::String info = water::String::formatted("%d - %d, vel %d - %d", lokey, hikey, lovel, hivel); if (sample) @@ -143,4 +147,6 @@ water::String sfzero::Region::dump() return info; } -float sfzero::Region::timecents2Secs(int timecents) { return static_cast(pow(2.0, timecents / 1200.0)); } +float Region::timecents2Secs(int timecents) { return static_cast(pow(2.0, timecents / 1200.0)); } + +} diff --git a/source/modules/sfzero/sfzero/SFZRegion.h b/source/modules/sfzero/sfzero/SFZRegion.h index 3c7c6b297..dcd67333c 100644 --- a/source/modules/sfzero/sfzero/SFZRegion.h +++ b/source/modules/sfzero/sfzero/SFZRegion.h @@ -90,6 +90,7 @@ struct Region static float timecents2Secs(int timecents); }; + } #endif // SFZREGION_H_INCLUDED diff --git a/source/modules/sfzero/sfzero/SFZSample.cpp b/source/modules/sfzero/sfzero/SFZSample.cpp index 3e4861db1..0492f97d4 100644 --- a/source/modules/sfzero/sfzero/SFZSample.cpp +++ b/source/modules/sfzero/sfzero/SFZSample.cpp @@ -4,11 +4,15 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #include "SFZSample.h" #include "SFZDebug.h" +namespace sfzero +{ + #if 0 -bool sfzero::Sample::load(water::AudioFormatManager *formatManager) +bool Sample::load(water::AudioFormatManager *formatManager) { water::AudioFormatReader *reader = formatManager->createReaderFor(file_); @@ -37,31 +41,31 @@ bool sfzero::Sample::load(water::AudioFormatManager *formatManager) } #endif -sfzero::Sample::~Sample() { delete buffer_; } +Sample::~Sample() { delete buffer_; } -water::String sfzero::Sample::getShortName() { return (file_.getFileName()); } +water::String Sample::getShortName() { return (file_.getFileName()); } -void sfzero::Sample::setBuffer(water::AudioSampleBuffer *newBuffer) +void Sample::setBuffer(water::AudioSampleBuffer *newBuffer) { buffer_ = newBuffer; sampleLength_ = buffer_->getNumSamples(); } -water::AudioSampleBuffer *sfzero::Sample::detachBuffer() +water::AudioSampleBuffer *Sample::detachBuffer() { water::AudioSampleBuffer *result = buffer_; buffer_ = nullptr; return result; } -water::String sfzero::Sample::dump() { return file_.getFullPathName() + "\n"; } +water::String Sample::dump() { return file_.getFullPathName() + "\n"; } #ifdef DEBUG -void sfzero::Sample::checkIfZeroed(const char *where) +void Sample::checkIfZeroed(const char *where) { if (buffer_ == nullptr) { - sfzero::dbgprintf("SFZSample::checkIfZeroed(%s): no buffer!", where); + dbgprintf("SFZSample::checkIfZeroed(%s): no buffer!", where); return; } @@ -81,12 +85,13 @@ void sfzero::Sample::checkIfZeroed(const char *where) } if (nonzero > 0) { - sfzero::dbgprintf("Buffer not zeroed at %s (%lu vs. %lu).", where, nonzero, zero); + dbgprintf("Buffer not zeroed at %s (%lu vs. %lu).", where, nonzero, zero); } else { - sfzero::dbgprintf("Buffer zeroed at %s! (%lu zeros)", where, zero); + dbgprintf("Buffer zeroed at %s! (%lu zeros)", where, zero); } } - #endif // JUCE_DEBUG + +} diff --git a/source/modules/sfzero/sfzero/SFZSound.cpp b/source/modules/sfzero/sfzero/SFZSound.cpp index 7007f1b6d..79c52ccce 100644 --- a/source/modules/sfzero/sfzero/SFZSound.cpp +++ b/source/modules/sfzero/sfzero/SFZSound.cpp @@ -4,13 +4,17 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #include "SFZSound.h" #include "SFZReader.h" #include "SFZRegion.h" #include "SFZSample.h" -sfzero::Sound::Sound(const water::File &fileIn) : file_(fileIn) {} -sfzero::Sound::~Sound() +namespace sfzero +{ + +Sound::Sound(const water::File &fileIn) : file_(fileIn) {} +Sound::~Sound() { int numRegions = regions_.size(); @@ -20,21 +24,21 @@ sfzero::Sound::~Sound() regions_.set(i, nullptr); } - for (water::HashMap::Iterator i(samples_); i.next();) + for (water::HashMap::Iterator i(samples_); i.next();) { delete i.getValue(); } } -bool sfzero::Sound::appliesToNote(int /*midiNoteNumber*/) +bool Sound::appliesToNote(int /*midiNoteNumber*/) { // Just say yes; we can't truly know unless we're told the velocity as well. return true; } -bool sfzero::Sound::appliesToChannel(int /*midiChannel*/) { return true; } -void sfzero::Sound::addRegion(sfzero::Region *region) { regions_.add(region); } -sfzero::Sample *sfzero::Sound::addSample(water::String path, water::String defaultPath) +bool Sound::appliesToChannel(int /*midiChannel*/) { return true; } +void Sound::addRegion(Region *region) { regions_.add(region); } +Sample *Sound::addSample(water::String path, water::String defaultPath) { path = path.replaceCharacter('\\', '/'); defaultPath = defaultPath.replaceCharacter('\\', '/'); @@ -49,18 +53,18 @@ sfzero::Sample *sfzero::Sound::addSample(water::String path, water::String defau sampleFile = defaultDir.getChildFile(path); } water::String samplePath = sampleFile.getFullPathName(); - sfzero::Sample *sample = samples_[samplePath]; + Sample *sample = samples_[samplePath]; if (sample == nullptr) { - sample = new sfzero::Sample(sampleFile); + sample = new Sample(sampleFile); samples_.set(samplePath, sample); } return sample; } -void sfzero::Sound::addError(const water::String &message) { errors_.add(message); } +void Sound::addError(const water::String &message) { errors_.add(message); } -void sfzero::Sound::addUnsupportedOpcode(const water::String &opcode) +void Sound::addUnsupportedOpcode(const water::String &opcode) { if (!unsupportedOpcodes_.contains(opcode)) { @@ -71,14 +75,14 @@ void sfzero::Sound::addUnsupportedOpcode(const water::String &opcode) } } -void sfzero::Sound::loadRegions() +void Sound::loadRegions() { - sfzero::Reader reader(this); + Reader reader(this); reader.read(file_); } -void sfzero::Sound::loadSamples(water::AudioFormatManager* formatManager, double* progressVar, CarlaThread* thread) +void Sound::loadSamples(water::AudioFormatManager* formatManager, double* progressVar, CarlaThread* thread) { if (progressVar) { @@ -86,9 +90,9 @@ void sfzero::Sound::loadSamples(water::AudioFormatManager* formatManager, double } double numSamplesLoaded = 1.0, numSamples = samples_.size(); - for (water::HashMap::Iterator i(samples_); i.next();) + for (water::HashMap::Iterator i(samples_); i.next();) { - sfzero::Sample *sample = i.getValue(); + Sample *sample = i.getValue(); #if 0 bool ok = sample->load(formatManager); if (!ok) @@ -114,13 +118,13 @@ void sfzero::Sound::loadSamples(water::AudioFormatManager* formatManager, double } } -sfzero::Region *sfzero::Sound::getRegionFor(int note, int velocity, sfzero::Region::Trigger trigger) +Region *Sound::getRegionFor(int note, int velocity, Region::Trigger trigger) { int numRegions = regions_.size(); for (int i = 0; i < numRegions; ++i) { - sfzero::Region *region = regions_[i]; + Region *region = regions_[i]; if (region->matches(note, velocity, trigger)) { return region; @@ -130,19 +134,19 @@ sfzero::Region *sfzero::Sound::getRegionFor(int note, int velocity, sfzero::Regi return nullptr; } -int sfzero::Sound::getNumRegions() { return regions_.size(); } +int Sound::getNumRegions() { return regions_.size(); } -sfzero::Region *sfzero::Sound::regionAt(int index) { return regions_[index]; } +Region *Sound::regionAt(int index) { return regions_[index]; } -int sfzero::Sound::numSubsounds() { return 1; } +int Sound::numSubsounds() { return 1; } -water::String sfzero::Sound::subsoundName(int /*whichSubsound*/) { return water::String(); } +water::String Sound::subsoundName(int /*whichSubsound*/) { return water::String(); } -void sfzero::Sound::useSubsound(int /*whichSubsound*/) {} +void Sound::useSubsound(int /*whichSubsound*/) {} -int sfzero::Sound::selectedSubsound() { return 0; } +int Sound::selectedSubsound() { return 0; } -water::String sfzero::Sound::dump() +water::String Sound::dump() { water::String info; auto &errors = getErrors(); @@ -184,7 +188,7 @@ water::String sfzero::Sound::dump() if (samples_.size() > 0) { info << samples_.size() << " samples: \n"; - for (water::HashMap::Iterator i(samples_); i.next();) + for (water::HashMap::Iterator i(samples_); i.next();) { info << i.getValue()->dump(); } @@ -195,3 +199,5 @@ water::String sfzero::Sound::dump() } return info; } + +} diff --git a/source/modules/sfzero/sfzero/SFZSound.h b/source/modules/sfzero/sfzero/SFZSound.h index 7a5e68c2c..15b2c894d 100644 --- a/source/modules/sfzero/sfzero/SFZSound.h +++ b/source/modules/sfzero/sfzero/SFZSound.h @@ -9,23 +9,13 @@ #include "SFZRegion.h" + #include "water/containers/HashMap.h" -#include "water/memory/ReferenceCountedObject.h" +#include "water/synthesisers/Synthesiser.h" #include "water/text/StringArray.h" -#include "CarlaJuceUtils.hpp" #include "CarlaThread.hpp" -#if 1 -namespace water { -class SynthesiserSound { -public: - virtual bool appliesToNote(int midiNoteNumber) = 0; - virtual bool appliesToChannel(int midiChannel) = 0; -}; -} -#endif - namespace sfzero { diff --git a/source/modules/sfzero/sfzero/SFZSynth.cpp b/source/modules/sfzero/sfzero/SFZSynth.cpp index cd4af3b68..31b6c5586 100644 --- a/source/modules/sfzero/sfzero/SFZSynth.cpp +++ b/source/modules/sfzero/sfzero/SFZSynth.cpp @@ -4,29 +4,32 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #include "SFZSynth.h" #include "SFZSound.h" #include "SFZVoice.h" -sfzero::Synth::Synth() : Synthesiser() {} +namespace sfzero +{ + +Synth::Synth() : Synthesiser() {} -#if 0 -void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) +void Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) { int i; - const water::ScopedLock locker(lock); + const CarlaMutexLocker locker(lock); int midiVelocity = static_cast(velocity * 127); // First, stop any currently-playing sounds in the group. //*** Currently, this only pays attention to the first matching region. int group = 0; - sfzero::Sound *sound = dynamic_cast(getSound(0)); + Sound *sound = dynamic_cast(getSound(0)); if (sound) { - sfzero::Region *region = sound->getRegionFor(midiNoteNumber, midiVelocity); + Region *region = sound->getRegionFor(midiNoteNumber, midiVelocity); if (region) { group = region->group; @@ -36,7 +39,7 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) { for (i = voices.size(); --i >= 0;) { - sfzero::Voice *voice = dynamic_cast(voices.getUnchecked(i)); + Voice *voice = dynamic_cast(voices.getUnchecked(i)); if (voice == nullptr) { continue; @@ -53,7 +56,7 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) bool anyNotesPlaying = false; for (i = voices.size(); --i >= 0;) { - sfzero::Voice *voice = dynamic_cast(voices.getUnchecked(i)); + Voice *voice = dynamic_cast(voices.getUnchecked(i)); if (voice == nullptr) { continue; @@ -78,17 +81,17 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) } // Play *all* matching regions. - sfzero::Region::Trigger trigger = (anyNotesPlaying ? sfzero::Region::legato : sfzero::Region::first); + Region::Trigger trigger = (anyNotesPlaying ? Region::legato : Region::first); if (sound) { int numRegions = sound->getNumRegions(); for (i = 0; i < numRegions; ++i) { - sfzero::Region *region = sound->regionAt(i); + Region *region = sound->regionAt(i); if (region->matches(midiNoteNumber, midiVelocity, trigger)) { - sfzero::Voice *voice = - dynamic_cast(findFreeVoice(sound, midiNoteNumber, midiChannel, isNoteStealingEnabled())); + Voice *voice = + dynamic_cast(findFreeVoice(sound, midiNoteNumber, midiChannel, isNoteStealingEnabled())); if (voice) { voice->setRegion(region); @@ -100,23 +103,21 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) noteVelocities_[midiNoteNumber] = midiVelocity; } -#endif -#if 0 -void sfzero::Synth::noteOff(int midiChannel, int midiNoteNumber, float velocity, bool allowTailOff) +void Synth::noteOff(int midiChannel, int midiNoteNumber, float velocity, bool allowTailOff) { - const water::ScopedLock locker(lock); + const CarlaMutexLocker locker(lock); Synthesiser::noteOff(midiChannel, midiNoteNumber, velocity, allowTailOff); // Start release region. - sfzero::Sound *sound = dynamic_cast(getSound(0)); + Sound *sound = dynamic_cast(getSound(0)); if (sound) { - sfzero::Region *region = sound->getRegionFor(midiNoteNumber, noteVelocities_[midiNoteNumber], sfzero::Region::release); + Region *region = sound->getRegionFor(midiNoteNumber, noteVelocities_[midiNoteNumber], Region::release); if (region) { - sfzero::Voice *voice = dynamic_cast(findFreeVoice(sound, midiNoteNumber, midiChannel, false)); + Voice *voice = dynamic_cast(findFreeVoice(sound, midiNoteNumber, midiChannel, false)); if (voice) { // Synthesiser is too locked-down (ivars are private rt protected), so @@ -127,13 +128,11 @@ void sfzero::Synth::noteOff(int midiChannel, int midiNoteNumber, float velocity, } } } -#endif -int sfzero::Synth::numVoicesUsed() +int Synth::numVoicesUsed() { int numUsed = 0; -#if 0 for (int i = voices.size(); --i >= 0;) { if (voices.getUnchecked(i)->getCurrentlyPlayingNote() >= 0) @@ -141,11 +140,11 @@ int sfzero::Synth::numVoicesUsed() numUsed += 1; } } -#endif + return numUsed; } -water::String sfzero::Synth::voiceInfoString() +water::String Synth::voiceInfoString() { enum { @@ -154,10 +153,10 @@ water::String sfzero::Synth::voiceInfoString() water::StringArray lines; int numUsed = 0, numShown = 0; -#if 0 + for (int i = voices.size(); --i >= 0;) { - sfzero::Voice *voice = dynamic_cast(voices.getUnchecked(i)); + Voice *voice = dynamic_cast(voices.getUnchecked(i)); if (voice->getCurrentlyPlayingNote() < 0) { continue; @@ -169,7 +168,9 @@ water::String sfzero::Synth::voiceInfoString() } lines.add(voice->infoString()); } -#endif + lines.insert(0, "voices used: " + water::String(numUsed)); return lines.joinIntoString("\n"); } + +} diff --git a/source/modules/sfzero/sfzero/SFZSynth.h b/source/modules/sfzero/sfzero/SFZSynth.h index 617ec8523..34aab6820 100644 --- a/source/modules/sfzero/sfzero/SFZSynth.h +++ b/source/modules/sfzero/sfzero/SFZSynth.h @@ -4,22 +4,13 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #ifndef SFZSYNTH_H_INCLUDED #define SFZSYNTH_H_INCLUDED #include "SFZCommon.h" -#include "CarlaJuceUtils.hpp" - -#if 1 -namespace water { -class Synthesiser { -public: - virtual void noteOn(int midiChannel, int midiNoteNumber, float velocity) = 0; - virtual void noteOff(int midiChannel, int midiNoteNumber, float velocity, bool allowTailOff) = 0; -}; -} -#endif +#include "water/synthesisers/Synthesiser.h" namespace sfzero { diff --git a/source/modules/sfzero/sfzero/SFZVoice.cpp b/source/modules/sfzero/sfzero/SFZVoice.cpp index 55b70c9c8..01b6aba2f 100644 --- a/source/modules/sfzero/sfzero/SFZVoice.cpp +++ b/source/modules/sfzero/sfzero/SFZVoice.cpp @@ -4,6 +4,7 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #include "SFZDebug.h" #include "SFZRegion.h" #include "SFZSample.h" @@ -14,23 +15,26 @@ #include +namespace sfzero +{ + static const float globalGain = -1.0; -sfzero::Voice::Voice() +Voice::Voice() : region_(nullptr), trigger_(0), curMidiNote_(0), curPitchWheel_(0), pitchRatio_(0), noteGainLeft_(0), noteGainRight_(0), sourceSamplePosition_(0), sampleEnd_(0), loopStart_(0), loopEnd_(0), numLoops_(0), curVelocity_(0) { ampeg_.setExponentialDecay(true); } -sfzero::Voice::~Voice() {} +Voice::~Voice() {} -bool sfzero::Voice::canPlaySound(water::SynthesiserSound *sound) { return dynamic_cast(sound) != nullptr; } +bool Voice::canPlaySound(water::SynthesiserSound *sound) { return dynamic_cast(sound) != nullptr; } -void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::SynthesiserSound *soundIn, +void Voice::startNote(int midiNoteNumber, float floatVelocity, water::SynthesiserSound *soundIn, int currentPitchWheelPosition) { - sfzero::Sound *sound = dynamic_cast(soundIn); + Sound *sound = dynamic_cast(soundIn); if (sound == nullptr) { @@ -75,9 +79,7 @@ void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::Sy double adjustedPan = (region_->pan + 100.0) / 200.0; noteGainLeft_ *= static_cast(sqrt(1.0 - adjustedPan)); noteGainRight_ *= static_cast(sqrt(adjustedPan)); -#if 0 ampeg_.startNote(®ion_->ampeg, floatVelocity, getSampleRate(), ®ion_->ampeg_veltrack); -#endif // Offset/end. sourceSamplePosition_ = static_cast(region_->offset); @@ -89,19 +91,19 @@ void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::Sy // Loop. loopStart_ = loopEnd_ = 0; - sfzero::Region::LoopMode loopMode = region_->loop_mode; - if (loopMode == sfzero::Region::sample_loop) + Region::LoopMode loopMode = region_->loop_mode; + if (loopMode == Region::sample_loop) { if (region_->sample->getLoopStart() < region_->sample->getLoopEnd()) { - loopMode = sfzero::Region::loop_continuous; + loopMode = Region::loop_continuous; } else { - loopMode = sfzero::Region::no_loop; + loopMode = Region::no_loop; } } - if ((loopMode != sfzero::Region::no_loop) && (loopMode != sfzero::Region::one_shot)) + if ((loopMode != Region::no_loop) && (loopMode != Region::one_shot)) { if (region_->loop_start < region_->loop_end) { @@ -117,7 +119,7 @@ void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::Sy numLoops_ = 0; } -void sfzero::Voice::stopNote(float /*velocity*/, bool allowTailOff) +void Voice::stopNote(float /*velocity*/, bool allowTailOff) { if (!allowTailOff || (region_ == nullptr)) { @@ -125,20 +127,20 @@ void sfzero::Voice::stopNote(float /*velocity*/, bool allowTailOff) return; } - if (region_->loop_mode != sfzero::Region::one_shot) + if (region_->loop_mode != Region::one_shot) { ampeg_.noteOff(); } - if (region_->loop_mode == sfzero::Region::loop_sustain) + if (region_->loop_mode == Region::loop_sustain) { // Continue playing, but stop looping. loopEnd_ = loopStart_; } } -void sfzero::Voice::stopNoteForGroup() +void Voice::stopNoteForGroup() { - if (region_->off_mode == sfzero::Region::fast) + if (region_->off_mode == Region::fast) { ampeg_.fastRelease(); } @@ -148,8 +150,8 @@ void sfzero::Voice::stopNoteForGroup() } } -void sfzero::Voice::stopNoteQuick() { ampeg_.fastRelease(); } -void sfzero::Voice::pitchWheelMoved(int newValue) +void Voice::stopNoteQuick() { ampeg_.fastRelease(); } +void Voice::pitchWheelMoved(int newValue) { if (region_ == nullptr) { @@ -160,8 +162,8 @@ void sfzero::Voice::pitchWheelMoved(int newValue) calcPitchRatio(); } -void sfzero::Voice::controllerMoved(int /*controllerNumber*/, int /*newValue*/) { /***/} -void sfzero::Voice::renderNextBlock(water::AudioSampleBuffer &outputBuffer, int startSample, int numSamples) +void Voice::controllerMoved(int /*controllerNumber*/, int /*newValue*/) { /***/} +void Voice::renderNextBlock(water::AudioSampleBuffer &outputBuffer, int startSample, int numSamples) { if (region_ == nullptr) { @@ -265,17 +267,17 @@ void sfzero::Voice::renderNextBlock(water::AudioSampleBuffer &outputBuffer, int ampeg_.setSamplesUntilNextSegment(samplesUntilNextAmpSegment); } -bool sfzero::Voice::isPlayingNoteDown() { return region_ && region_->trigger != sfzero::Region::release; } +bool Voice::isPlayingNoteDown() { return region_ && region_->trigger != Region::release; } -bool sfzero::Voice::isPlayingOneShot() { return region_ && region_->loop_mode == sfzero::Region::one_shot; } +bool Voice::isPlayingOneShot() { return region_ && region_->loop_mode == Region::one_shot; } -int sfzero::Voice::getGroup() { return region_ ? region_->group : 0; } +int Voice::getGroup() { return region_ ? region_->group : 0; } -water::uint64 sfzero::Voice::getOffBy() { return region_ ? region_->off_by : 0; } +water::int64 Voice::getOffBy() { return region_ ? region_->off_by : 0; } -void sfzero::Voice::setRegion(sfzero::Region *nextRegion) { region_ = nextRegion; } +void Voice::setRegion(Region *nextRegion) { region_ = nextRegion; } -water::String sfzero::Voice::infoString() +water::String Voice::infoString() { const char *egSegmentNames[] = {"delay", "attack", "hold", "decay", "sustain", "release", "done"}; @@ -294,7 +296,7 @@ water::String sfzero::Voice::infoString() return info; } -void sfzero::Voice::calcPitchRatio() +void Voice::calcPitchRatio() { double note = curMidiNote_; @@ -316,23 +318,21 @@ void sfzero::Voice::calcPitchRatio() } double targetFreq = fractionalMidiNoteInHz(adjustedPitch); double naturalFreq = water::MidiMessage::getMidiNoteInHertz(region_->pitch_keycenter); -#if 0 pitchRatio_ = (targetFreq * region_->sample->getSampleRate()) / (naturalFreq * getSampleRate()); -#endif } -void sfzero::Voice::killNote() +void Voice::killNote() { region_ = nullptr; -#if 0 clearCurrentNote(); -#endif } -double sfzero::Voice::fractionalMidiNoteInHz(double note, double freqOfA) +double Voice::fractionalMidiNoteInHz(double note, double freqOfA) { // Like MidiMessage::getMidiNoteInHertz(), but with a float note. note -= 69; // Now 0 = A return freqOfA * pow(2.0, note / 12.0); } + +} diff --git a/source/modules/sfzero/sfzero/SFZVoice.h b/source/modules/sfzero/sfzero/SFZVoice.h index 723a18878..30db60d6b 100644 --- a/source/modules/sfzero/sfzero/SFZVoice.h +++ b/source/modules/sfzero/sfzero/SFZVoice.h @@ -4,29 +4,17 @@ * Forked from https://github.com/stevefolta/SFZero * For license info please see the LICENSE file distributed with this source code *************************************************************************************/ + #ifndef SFZVOICE_H_INCLUDED #define SFZVOICE_H_INCLUDED #include "SFZEG.h" -#include "CarlaJuceUtils.hpp" - -#if 1 -namespace water { -class SynthesiserVoice { -public: - virtual bool canPlaySound(water::SynthesiserSound *sound) = 0; - virtual void startNote(int midiNoteNumber, float velocity, water::SynthesiserSound *sound, int currentPitchWheelPosition) = 0; - virtual void stopNote(float velocity, bool allowTailOff) = 0; - virtual void pitchWheelMoved(int newValue) = 0; - virtual void controllerMoved(int controllerNumber, int newValue) = 0; - virtual void renderNextBlock(water::AudioSampleBuffer &outputBuffer, int startSample, int numSamples) = 0; -}; -} -#endif +#include "water/synthesisers/Synthesiser.h" namespace sfzero { + struct Region; class Voice : public water::SynthesiserVoice @@ -47,7 +35,7 @@ public: bool isPlayingOneShot(); int getGroup(); - water::uint64 getOffBy(); + water::int64 getOffBy(); // Set the region to be used by the next startNote(). void setRegion(Region *nextRegion); diff --git a/source/modules/water/maths/BigInteger.cpp b/source/modules/water/maths/BigInteger.cpp new file mode 100644 index 000000000..89942476e --- /dev/null +++ b/source/modules/water/maths/BigInteger.cpp @@ -0,0 +1,240 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2016 ROLI Ltd. + Copyright (C) 2018 Filipe Coelho + + Permission is granted to use this software under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license/ + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, + OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + + ============================================================================== +*/ + +#include "BigInteger.h" + +namespace water +{ + +inline uint32 bitToMask (const int bit) noexcept { return (uint32) 1 << (bit & 31); } +inline size_t bitToIndex (const int bit) noexcept { return (size_t) (bit >> 5); } +inline size_t sizeNeededToHold (int highestBit) noexcept { return (size_t) (highestBit >> 5) + 1; } + +int findHighestSetBit (uint32 n) noexcept +{ + jassert (n != 0); // (the built-in functions may not work for n = 0) + + #if defined(__GNUC__) || defined(__clang__) + return 31 - __builtin_clz (n); + #elif _MSVC_VER + unsigned long highest; + _BitScanReverse (&highest, n); + return (int) highest; + #else + n |= (n >> 1); + n |= (n >> 2); + n |= (n >> 4); + n |= (n >> 8); + n |= (n >> 16); + return countNumberOfBits (n >> 1); + #endif +} + + +//============================================================================== +BigInteger::BigInteger() noexcept + : allocatedSize (numPreallocatedInts), + highestBit (-1) +{ + for (int i = 0; i < numPreallocatedInts; ++i) + preallocated[i] = 0; +} + +BigInteger::BigInteger (const int32 value) noexcept + : allocatedSize (numPreallocatedInts), + highestBit (31) +{ + preallocated[0] = (uint32) std::abs (value); + + for (int i = 1; i < numPreallocatedInts; ++i) + preallocated[i] = 0; + + highestBit = getHighestBit(); +} + +BigInteger::BigInteger (const uint32 value) noexcept + : allocatedSize (numPreallocatedInts), + highestBit (31) +{ + preallocated[0] = value; + + for (int i = 1; i < numPreallocatedInts; ++i) + preallocated[i] = 0; + + highestBit = getHighestBit(); +} + +BigInteger::BigInteger (int64 value) noexcept + : allocatedSize (numPreallocatedInts), + highestBit (63) +{ + if (value < 0) + value = -value; + + preallocated[0] = (uint32) value; + preallocated[1] = (uint32) (value >> 32); + + for (int i = 2; i < numPreallocatedInts; ++i) + preallocated[i] = 0; + + highestBit = getHighestBit(); +} + +#if WATER_COMPILER_SUPPORTS_MOVE_SEMANTICS +BigInteger::BigInteger (BigInteger&& other) noexcept + : heapAllocation (static_cast&&> (other.heapAllocation)), + allocatedSize (other.allocatedSize), + highestBit (other.highestBit) +{ + std::memcpy (preallocated, other.preallocated, sizeof (preallocated)); +} + +BigInteger& BigInteger::operator= (BigInteger&& other) noexcept +{ + heapAllocation = static_cast&&> (other.heapAllocation); + std::memcpy (preallocated, other.preallocated, sizeof (preallocated)); + allocatedSize = other.allocatedSize; + highestBit = other.highestBit; + return *this; +} +#endif + +BigInteger::~BigInteger() noexcept +{ +} + +uint32* BigInteger::getValues() const noexcept +{ + CARLA_SAFE_ASSERT_RETURN(heapAllocation != nullptr || allocatedSize <= numPreallocatedInts, nullptr); + + return heapAllocation != nullptr ? heapAllocation + : (uint32*) preallocated; +} + +uint32* BigInteger::ensureSize (const size_t numVals) noexcept +{ + if (numVals <= allocatedSize) + return getValues(); + + size_t oldSize = allocatedSize; + allocatedSize = ((numVals + 2) * 3) / 2; + + if (heapAllocation == nullptr) + { + CARLA_SAFE_ASSERT_RETURN(heapAllocation.calloc (allocatedSize), nullptr); + std::memcpy (heapAllocation, preallocated, sizeof (uint32) * numPreallocatedInts); + } + else + { + CARLA_SAFE_ASSERT_RETURN(heapAllocation.realloc (allocatedSize), nullptr); + + for (uint32* values = heapAllocation; oldSize < allocatedSize; ++oldSize) + values[oldSize] = 0; + } + + return heapAllocation; +} + +//============================================================================== +bool BigInteger::operator[] (const int bit) const noexcept +{ + CARLA_SAFE_ASSERT_RETURN(bit <= highestBit && bit >= 0, false); + + if (const uint32* const values = getValues()) + return (values [bitToIndex (bit)] & bitToMask (bit)) != 0; + + return false; +} + +//============================================================================== +void BigInteger::clear() noexcept +{ + heapAllocation.free(); + allocatedSize = numPreallocatedInts; + highestBit = -1; + + for (int i = 0; i < numPreallocatedInts; ++i) + preallocated[i] = 0; +} + +bool BigInteger::setBit (const int bit) noexcept +{ + CARLA_SAFE_ASSERT_RETURN(bit >= 0, false); + + if (bit > highestBit) + { + if (ensureSize (sizeNeededToHold (bit)) == nullptr) + return false; + + highestBit = bit; + } + + if (uint32* const values = getValues()) + return values [bitToIndex (bit)] |= bitToMask (bit); + + return false; +} + +bool BigInteger::setBit (const int bit, const bool shouldBeSet) noexcept +{ + if (shouldBeSet) + { + return setBit (bit); + } + else + { + clearBit (bit); + return true; + } +} + +bool BigInteger::clearBit (const int bit) noexcept +{ + CARLA_SAFE_ASSERT_RETURN(bit <= highestBit && bit >= 0, false); + + uint32* const values = getValues(); + CARLA_SAFE_ASSERT_RETURN(values != nullptr, false); + + values [bitToIndex (bit)] &= ~bitToMask (bit); + + if (bit == highestBit) + highestBit = getHighestBit(); + + return true; +} + +int BigInteger::getHighestBit() const noexcept +{ + const uint32* values = getValues(); + CARLA_SAFE_ASSERT_RETURN(values != nullptr, -1); + + for (int i = (int) bitToIndex (highestBit); i >= 0; --i) + if (uint32 n = values[i]) + return findHighestSetBit (n) + (i << 5); + + return -1; +} + +} diff --git a/source/modules/water/maths/BigInteger.h b/source/modules/water/maths/BigInteger.h new file mode 100644 index 000000000..1b5d2fd44 --- /dev/null +++ b/source/modules/water/maths/BigInteger.h @@ -0,0 +1,118 @@ +/* + ============================================================================== + + This file is part of the Water library. + Copyright (c) 2016 ROLI Ltd. + Copyright (C) 2018 Filipe Coelho + + Permission is granted to use this software under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license/ + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, + OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + + ============================================================================== +*/ + +#ifndef WATER_BIGINTEGER_H_INCLUDED +#define WATER_BIGINTEGER_H_INCLUDED + +#include "../memory/HeapBlock.h" + +#include "CarlaJuceUtils.hpp" + +namespace water { + +//============================================================================== +/** + An arbitrarily large integer class. + + A BigInteger can be used in a similar way to a normal integer, but has no size + limit (except for memory and performance constraints). + + Negative values are possible, but the value isn't stored as 2s-complement, so + be careful if you use negative values and look at the values of individual bits. +*/ +class BigInteger +{ +public: + //============================================================================== + /** Creates an empty BigInteger */ + BigInteger() noexcept; + + /** Creates a BigInteger containing an integer value in its low bits. + The low 32 bits of the number are initialised with this value. + */ + BigInteger (uint32 value) noexcept; + + /** Creates a BigInteger containing an integer value in its low bits. + The low 32 bits of the number are initialised with the absolute value + passed in, and its sign is set to reflect the sign of the number. + */ + BigInteger (int32 value) noexcept; + + /** Creates a BigInteger containing an integer value in its low bits. + The low 64 bits of the number are initialised with the absolute value + passed in, and its sign is set to reflect the sign of the number. + */ + BigInteger (int64 value) noexcept; + + #if WATER_COMPILER_SUPPORTS_MOVE_SEMANTICS + BigInteger (BigInteger&&) noexcept; + BigInteger& operator= (BigInteger&&) noexcept; + #endif + + /** Destructor. */ + ~BigInteger() noexcept; + + //============================================================================== + /** Returns the value of a specified bit in the number. + If the index is out-of-range, the result will be false. + */ + bool operator[] (int bit) const noexcept; + + //============================================================================== + /** Resets the value to 0. */ + void clear() noexcept; + + /** Clears a particular bit in the number. */ + bool clearBit (int bitNumber) noexcept; + + /** Sets a specified bit to 1. */ + bool setBit (int bitNumber) noexcept; + + /** Sets or clears a specified bit. */ + bool setBit (int bitNumber, bool shouldBeSet) noexcept; + + //============================================================================== + /** Returns the index of the highest set bit in the number. + If the value is zero, this will return -1. + */ + int getHighestBit() const noexcept; + +private: + //============================================================================== + enum { numPreallocatedInts = 4 }; + HeapBlock heapAllocation; + uint32 preallocated[numPreallocatedInts]; + size_t allocatedSize; + int highestBit; + + uint32* getValues() const noexcept; + uint32* ensureSize (size_t) noexcept; + + CARLA_LEAK_DETECTOR (BigInteger) +}; + +} + +#endif // WATER_BIGINTEGER_H_INCLUDED diff --git a/source/modules/water/synthesisers/Synthesiser.cpp b/source/modules/water/synthesisers/Synthesiser.cpp new file mode 100644 index 000000000..5b41a1136 --- /dev/null +++ b/source/modules/water/synthesisers/Synthesiser.cpp @@ -0,0 +1,642 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2016 ROLI Ltd. + Copyright (C) 2018 Filipe Coelho + + Permission is granted to use this software under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license/ + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, + OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + + ============================================================================== +*/ + +#include "Synthesiser.h" + +#include "../buffers/AudioSampleBuffer.h" +#include "../containers/Array.h" +#include "../midi/MidiBuffer.h" +#include "../midi/MidiMessage.h" + +namespace water { + +SynthesiserSound::SynthesiserSound() {} +SynthesiserSound::~SynthesiserSound() {} + +//============================================================================== +SynthesiserVoice::SynthesiserVoice() + : currentSampleRate (44100.0), + currentlyPlayingNote (-1), + currentPlayingMidiChannel (0), + noteOnTime (0), + keyIsDown (false), + sustainPedalDown (false), + sostenutoPedalDown (false) +{ +} + +SynthesiserVoice::~SynthesiserVoice() +{ +} + +bool SynthesiserVoice::isPlayingChannel (const int midiChannel) const +{ + return currentPlayingMidiChannel == midiChannel; +} + +void SynthesiserVoice::setCurrentPlaybackSampleRate (const double newRate) +{ + currentSampleRate = newRate; +} + +bool SynthesiserVoice::isVoiceActive() const +{ + return getCurrentlyPlayingNote() >= 0; +} + +void SynthesiserVoice::clearCurrentNote() +{ + currentlyPlayingNote = -1; + currentlyPlayingSound = nullptr; + currentPlayingMidiChannel = 0; +} + +void SynthesiserVoice::aftertouchChanged (int) {} +void SynthesiserVoice::channelPressureChanged (int) {} + +bool SynthesiserVoice::wasStartedBefore (const SynthesiserVoice& other) const noexcept +{ + return noteOnTime < other.noteOnTime; +} + + +//============================================================================== +Synthesiser::Synthesiser() + : sampleRate (0), + lastNoteOnCounter (0), + minimumSubBlockSize (32), + subBlockSubdivisionIsStrict (false), + shouldStealNotes (true) +{ + for (int i = 0; i < numElementsInArray (lastPitchWheelValues); ++i) + lastPitchWheelValues[i] = 0x2000; +} + +Synthesiser::~Synthesiser() +{ +} + +//============================================================================== +SynthesiserVoice* Synthesiser::getVoice (const int index) const +{ + const CarlaMutexLocker sl (lock); + return voices [index]; +} + +void Synthesiser::clearVoices() +{ + const CarlaMutexLocker sl (lock); + voices.clear(); +} + +SynthesiserVoice* Synthesiser::addVoice (SynthesiserVoice* const newVoice) +{ + const CarlaMutexLocker sl (lock); + newVoice->setCurrentPlaybackSampleRate (sampleRate); + return voices.add (newVoice); +} + +void Synthesiser::removeVoice (const int index) +{ + const CarlaMutexLocker sl (lock); + voices.remove (index); +} + +void Synthesiser::clearSounds() +{ + const CarlaMutexLocker sl (lock); + sounds.clear(); +} + +SynthesiserSound* Synthesiser::addSound (const SynthesiserSound::Ptr& newSound) +{ + const CarlaMutexLocker sl (lock); + return sounds.add (newSound); +} + +void Synthesiser::removeSound (const int index) +{ + const CarlaMutexLocker sl (lock); + sounds.remove (index); +} + +void Synthesiser::setNoteStealingEnabled (const bool shouldSteal) +{ + shouldStealNotes = shouldSteal; +} + +void Synthesiser::setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict) noexcept +{ + jassert (numSamples > 0); // it wouldn't make much sense for this to be less than 1 + minimumSubBlockSize = numSamples; + subBlockSubdivisionIsStrict = shouldBeStrict; +} + +//============================================================================== +void Synthesiser::setCurrentPlaybackSampleRate (const double newRate) +{ + if (sampleRate != newRate) + { + const CarlaMutexLocker sl (lock); + + allNotesOff (0, false); + + sampleRate = newRate; + + for (int i = voices.size(); --i >= 0;) + voices.getUnchecked (i)->setCurrentPlaybackSampleRate (newRate); + } +} + +void Synthesiser::processNextBlock (AudioSampleBuffer& outputAudio, + const MidiBuffer& midiData, + int startSample, + int numSamples) +{ + // must set the sample rate before using this! + jassert (sampleRate != 0); + const int targetChannels = outputAudio.getNumChannels(); + + MidiBuffer::Iterator midiIterator (midiData); + midiIterator.setNextSamplePosition (startSample); + + bool firstEvent = true; + int midiEventPos; + MidiMessage m; + + const CarlaMutexLocker sl (lock); + + while (numSamples > 0) + { + if (! midiIterator.getNextEvent (m, midiEventPos)) + { + if (targetChannels > 0) + renderVoices (outputAudio, startSample, numSamples); + + return; + } + + const int samplesToNextMidiMessage = midiEventPos - startSample; + + if (samplesToNextMidiMessage >= numSamples) + { + if (targetChannels > 0) + renderVoices (outputAudio, startSample, numSamples); + + handleMidiEvent (m); + break; + } + + if (samplesToNextMidiMessage < ((firstEvent && ! subBlockSubdivisionIsStrict) ? 1 : minimumSubBlockSize)) + { + handleMidiEvent (m); + continue; + } + + firstEvent = false; + + if (targetChannels > 0) + renderVoices (outputAudio, startSample, samplesToNextMidiMessage); + + handleMidiEvent (m); + startSample += samplesToNextMidiMessage; + numSamples -= samplesToNextMidiMessage; + } + + while (midiIterator.getNextEvent (m, midiEventPos)) + handleMidiEvent (m); +} + +void Synthesiser::renderVoices (AudioSampleBuffer& buffer, int startSample, int numSamples) +{ + for (int i = voices.size(); --i >= 0;) + voices.getUnchecked (i)->renderNextBlock (buffer, startSample, numSamples); +} + +void Synthesiser::handleMidiEvent (const MidiMessage& m) +{ + const int channel = m.getChannel(); + + if (m.isNoteOn()) + { + noteOn (channel, m.getNoteNumber(), m.getFloatVelocity()); + } + else if (m.isNoteOff()) + { + noteOff (channel, m.getNoteNumber(), m.getFloatVelocity(), true); + } + else if (m.isAllNotesOff() || m.isAllSoundOff()) + { + allNotesOff (channel, true); + } + else if (m.isPitchWheel()) + { + const int wheelPos = m.getPitchWheelValue(); + lastPitchWheelValues [channel - 1] = wheelPos; + handlePitchWheel (channel, wheelPos); + } + else if (m.isAftertouch()) + { + handleAftertouch (channel, m.getNoteNumber(), m.getAfterTouchValue()); + } + else if (m.isChannelPressure()) + { + handleChannelPressure (channel, m.getChannelPressureValue()); + } + else if (m.isController()) + { + handleController (channel, m.getControllerNumber(), m.getControllerValue()); + } + else if (m.isProgramChange()) + { + handleProgramChange (channel, m.getProgramChangeNumber()); + } +} + +//============================================================================== +void Synthesiser::noteOn (const int midiChannel, + const int midiNoteNumber, + const float velocity) +{ + const CarlaMutexLocker 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, 1.0f, true); + } + + startVoice (findFreeVoice (sound, midiChannel, midiNoteNumber, 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 (0.0f, false); + + voice->currentlyPlayingNote = midiNoteNumber; + voice->currentPlayingMidiChannel = midiChannel; + voice->noteOnTime = ++lastNoteOnCounter; + voice->currentlyPlayingSound = sound; + voice->keyIsDown = true; + voice->sostenutoPedalDown = false; + voice->sustainPedalDown = sustainPedalsDown[midiChannel]; + + voice->startNote (midiNoteNumber, velocity, sound, + lastPitchWheelValues [midiChannel - 1]); + } +} + +void Synthesiser::stopVoice (SynthesiserVoice* voice, float velocity, const bool allowTailOff) +{ + jassert (voice != nullptr); + + voice->stopNote (velocity, 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 float velocity, + const bool allowTailOff) +{ + const CarlaMutexLocker sl (lock); + + for (int i = voices.size(); --i >= 0;) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if (voice->getCurrentlyPlayingNote() == midiNoteNumber + && voice->isPlayingChannel (midiChannel)) + { + if (SynthesiserSound* const sound = voice->getCurrentlyPlayingSound()) + { + if (sound->appliesToNote (midiNoteNumber) + && sound->appliesToChannel (midiChannel)) + { + CARLA_SAFE_ASSERT_RETURN (! voice->keyIsDown || voice->sustainPedalDown == sustainPedalsDown [midiChannel],); + + voice->keyIsDown = false; + + if (! (voice->sustainPedalDown || voice->sostenutoPedalDown)) + stopVoice (voice, velocity, allowTailOff); + } + } + } + } +} + +void Synthesiser::allNotesOff (const int midiChannel, const bool allowTailOff) +{ + const CarlaMutexLocker sl (lock); + + for (int i = voices.size(); --i >= 0;) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel)) + voice->stopNote (1.0f, allowTailOff); + } + + sustainPedalsDown.clear(); +} + +void Synthesiser::handlePitchWheel (const int midiChannel, const int wheelValue) +{ + const CarlaMutexLocker 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 CarlaMutexLocker 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 CarlaMutexLocker 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::handleChannelPressure (int midiChannel, int channelPressureValue) +{ + const CarlaMutexLocker sl (lock); + + for (int i = voices.size(); --i >= 0;) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel)) + voice->channelPressureChanged (channelPressureValue); + } +} + +void Synthesiser::handleSustainPedal (int midiChannel, bool isDown) +{ + jassert (midiChannel > 0 && midiChannel <= 16); + const CarlaMutexLocker sl (lock); + + if (isDown) + { + sustainPedalsDown.setBit (midiChannel); + + for (int i = voices.size(); --i >= 0;) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if (voice->isPlayingChannel (midiChannel) && voice->isKeyDown()) + voice->sustainPedalDown = true; + } + } + else + { + for (int i = voices.size(); --i >= 0;) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if (voice->isPlayingChannel (midiChannel)) + { + voice->sustainPedalDown = false; + + if (! voice->isKeyDown()) + stopVoice (voice, 1.0f, true); + } + } + + sustainPedalsDown.clearBit (midiChannel); + } +} + +void Synthesiser::handleSostenutoPedal (int midiChannel, bool isDown) +{ + jassert (midiChannel > 0 && midiChannel <= 16); + const CarlaMutexLocker 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, 1.0f, true); + } + } +} + +void Synthesiser::handleSoftPedal (int midiChannel, bool /*isDown*/) +{ + ignoreUnused (midiChannel); + jassert (midiChannel > 0 && midiChannel <= 16); +} + +void Synthesiser::handleProgramChange (int midiChannel, int programNumber) +{ + ignoreUnused (midiChannel, programNumber); + jassert (midiChannel > 0 && midiChannel <= 16); +} + +//============================================================================== +SynthesiserVoice* Synthesiser::findFreeVoice (SynthesiserSound* soundToPlay, + int midiChannel, int midiNoteNumber, + const bool stealIfNoneAvailable) const +{ + const CarlaMutexLocker sl (lock); + + for (int i = 0; i < voices.size(); ++i) + { + SynthesiserVoice* const voice = voices.getUnchecked (i); + + if ((! voice->isVoiceActive()) && voice->canPlaySound (soundToPlay)) + return voice; + } + + if (stealIfNoneAvailable) + return findVoiceToSteal (soundToPlay, midiChannel, midiNoteNumber); + + return nullptr; +} + +struct VoiceAgeSorter +{ + 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 +{ + // This voice-stealing algorithm applies the following heuristics: + // - Re-use the oldest notes first + // - Protect the lowest & topmost notes, even if sustained, but not if they've been released. + + // apparently you are trying to render audio without having any voices... + jassert (voices.size() > 0); + + // These are the voices we want to protect (ie: only steal if unavoidable) + SynthesiserVoice* low = nullptr; // Lowest sounding note, might be sustained, but NOT in release phase + SynthesiserVoice* top = nullptr; // Highest sounding note, might be sustained, but NOT in release phase + + // 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)) + { + jassert (voice->isVoiceActive()); // We wouldn't be here otherwise + + VoiceAgeSorter sorter; + usableVoices.addSorted (sorter, voice); + + if (! voice->isPlayingButReleased()) // Don't protect released notes + { + const int note = voice->getCurrentlyPlayingNote(); + + if (low == nullptr || note < low->getCurrentlyPlayingNote()) + low = voice; + + if (top == nullptr || note > top->getCurrentlyPlayingNote()) + top = voice; + } + } + } + + // Eliminate pathological cases (ie: only 1 note playing): we always give precedence to the lowest note(s) + if (top == low) + top = nullptr; + + const int numUsableVoices = usableVoices.size(); + + // The oldest note that's playing with the target pitch is ideal.. + for (int i = 0; i < numUsableVoices; ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice->getCurrentlyPlayingNote() == midiNoteNumber) + return voice; + } + + // Oldest voice that has been released (no finger on it and not held by sustain pedal) + for (int i = 0; i < numUsableVoices; ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice != low && voice != top && voice->isPlayingButReleased()) + return voice; + } + + // Oldest voice that doesn't have a finger on it: + for (int i = 0; i < numUsableVoices; ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice != low && voice != top && ! voice->isKeyDown()) + return voice; + } + + // Oldest voice that isn't protected + for (int i = 0; i < numUsableVoices; ++i) + { + SynthesiserVoice* const voice = usableVoices.getUnchecked (i); + + if (voice != low && voice != top) + return voice; + } + + // We've only got "protected" voices now: lowest note takes priority + jassert (low != nullptr); + + // Duophonic synth: give priority to the bass note: + if (top != nullptr) + return top; + + return low; +} + +} diff --git a/source/modules/water/synthesisers/Synthesiser.h b/source/modules/water/synthesisers/Synthesiser.h new file mode 100644 index 000000000..148a3094e --- /dev/null +++ b/source/modules/water/synthesisers/Synthesiser.h @@ -0,0 +1,626 @@ +/* + ============================================================================== + + This file is part of the Water library. + Copyright (c) 2016 ROLI Ltd. + Copyright (C) 2018 Filipe Coelho + + Permission is granted to use this software under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license/ + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, + OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + + ============================================================================== +*/ + +#ifndef WATER_SYNTHESISER_H_INCLUDED +#define WATER_SYNTHESISER_H_INCLUDED + +#include "../buffers/AudioSampleBuffer.h" +#include "../containers/OwnedArray.h" +#include "../containers/ReferenceCountedArray.h" +#include "../maths/BigInteger.h" + +#include "CarlaJuceUtils.hpp" +#include "CarlaMutex.hpp" + +namespace water { + +//============================================================================== +/** + Describes one of the sounds that a Synthesiser can play. + + A synthesiser can contain one or more sounds, and a sound can choose which + midi notes and channels can trigger it. + + The SynthesiserSound is a passive class that just describes what the sound is - + the actual audio rendering for a sound is done by a SynthesiserVoice. This allows + more than one SynthesiserVoice to play the same sound at the same time. + + @see Synthesiser, SynthesiserVoice +*/ +class SynthesiserSound : public ReferenceCountedObject +{ +protected: + //============================================================================== + SynthesiserSound(); + +public: + /** Destructor. */ + virtual ~SynthesiserSound(); + + //============================================================================== + /** Returns true if this sound should be played when a given midi note is pressed. + + The Synthesiser will use this information when deciding which sounds to trigger + for a given note. + */ + virtual bool appliesToNote (int midiNoteNumber) = 0; + + /** Returns true if the sound should be triggered by midi events on a given channel. + + The Synthesiser will use this information when deciding which sounds to trigger + for a given note. + */ + virtual bool appliesToChannel (int midiChannel) = 0; + + /** The class is reference-counted, so this is a handy pointer class for it. */ + typedef ReferenceCountedObjectPtr Ptr; + + +private: + //============================================================================== + CARLA_LEAK_DETECTOR (SynthesiserSound) +}; + + +//============================================================================== +/** + Represents a voice that a Synthesiser can use to play a SynthesiserSound. + + A voice plays a single sound at a time, and a synthesiser holds an array of + voices so that it can play polyphonically. + + @see Synthesiser, SynthesiserSound +*/ +class SynthesiserVoice +{ +public: + //============================================================================== + /** Creates a voice. */ + SynthesiserVoice(); + + /** Destructor. */ + virtual ~SynthesiserVoice(); + + //============================================================================== + /** Returns the midi note that this voice is currently playing. + Returns a value less than 0 if no note is playing. + */ + int getCurrentlyPlayingNote() const noexcept { return currentlyPlayingNote; } + + /** Returns the sound that this voice is currently playing. + Returns nullptr if it's not playing. + */ + SynthesiserSound::Ptr getCurrentlyPlayingSound() const noexcept { return currentlyPlayingSound; } + + /** Must return true if this voice object is capable of playing the given sound. + + If there are different classes of sound, and different classes of voice, a voice can + choose which ones it wants to take on. + + A typical implementation of this method may just return true if there's only one type + of voice and sound, or it might check the type of the sound object passed-in and + see if it's one that it understands. + */ + virtual bool canPlaySound (SynthesiserSound*) = 0; + + /** Called to start a new note. + This will be called during the rendering callback, so must be fast and thread-safe. + */ + virtual void startNote (int midiNoteNumber, + float velocity, + SynthesiserSound* sound, + int currentPitchWheelPosition) = 0; + + /** Called to stop a note. + + This will be called during the rendering callback, so must be fast and thread-safe. + + The velocity indicates how quickly the note was released - 0 is slowly, 1 is quickly. + + If allowTailOff is false or the voice doesn't want to tail-off, then it must stop all + sound immediately, and must call clearCurrentNote() to reset the state of this voice + and allow the synth to reassign it another sound. + + If allowTailOff is true and the voice decides to do a tail-off, then it's allowed to + begin fading out its sound, and it can stop playing until it's finished. As soon as it + finishes playing (during the rendering callback), it must make sure that it calls + clearCurrentNote(). + */ + virtual void stopNote (float velocity, bool allowTailOff) = 0; + + /** Returns true if this voice is currently busy playing a sound. + By default this just checks the getCurrentlyPlayingNote() value, but can + be overridden for more advanced checking. + */ + virtual bool isVoiceActive() const; + + /** Called to let the voice know that the pitch wheel has been moved. + This will be called during the rendering callback, so must be fast and thread-safe. + */ + virtual void pitchWheelMoved (int newPitchWheelValue) = 0; + + /** Called to let the voice know that a midi controller has been moved. + This will be called during the rendering callback, so must be fast and thread-safe. + */ + virtual void controllerMoved (int controllerNumber, int newControllerValue) = 0; + + /** Called to let the voice know that the aftertouch has changed. + This will be called during the rendering callback, so must be fast and thread-safe. + */ + virtual void aftertouchChanged (int newAftertouchValue); + + /** Called to let the voice know that the channel pressure has changed. + This will be called during the rendering callback, so must be fast and thread-safe. + */ + virtual void channelPressureChanged (int newChannelPressureValue); + + //============================================================================== + /** Renders the next block of data for this voice. + + The output audio data must be added to the current contents of the buffer provided. + Only the region of the buffer between startSample and (startSample + numSamples) + should be altered by this method. + + If the voice is currently silent, it should just return without doing anything. + + If the sound that the voice is playing finishes during the course of this rendered + block, it must call clearCurrentNote(), to tell the synthesiser that it has finished. + + The size of the blocks that are rendered can change each time it is called, and may + involve rendering as little as 1 sample at a time. In between rendering callbacks, + the voice's methods will be called to tell it about note and controller events. + */ + virtual void renderNextBlock (AudioSampleBuffer& outputBuffer, + int startSample, + int numSamples) = 0; + + /** Changes the voice's reference sample rate. + + The rate is set so that subclasses know the output rate and can set their pitch + accordingly. + + This method is called by the synth, and subclasses can access the current rate with + the currentSampleRate member. + */ + virtual void setCurrentPlaybackSampleRate (double newRate); + + /** Returns true if the voice is currently playing a sound which is mapped to the given + midi channel. + + If it's not currently playing, this will return false. + */ + virtual bool isPlayingChannel (int midiChannel) const; + + /** 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 key that triggered this voice is still held down. + Note that the voice may still be playing after the key was released (e.g because the + sostenuto pedal is down). + */ + bool isKeyDown() const noexcept { return keyIsDown; } + + /** Returns true if the sustain pedal is currently active for this voice. */ + bool isSustainPedalDown() const noexcept { return sustainPedalDown; } + + /** Returns true if the sostenuto pedal is currently active for this voice. */ + bool isSostenutoPedalDown() const noexcept { return sostenutoPedalDown; } + + /** Returns true if a voice is sounding in its release phase **/ + bool isPlayingButReleased() const noexcept + { + return isVoiceActive() && ! (isKeyDown() || isSostenutoPedalDown() || isSustainPedalDown()); + } + + /** Returns true if this voice started playing its current note before the other voice did. */ + bool wasStartedBefore (const SynthesiserVoice& other) const noexcept; + +protected: + /** 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 + to play new ones. + + It must either call it in the stopNote() method, or if the voice is tailing off, + then it should call it later during the renderNextBlock method, as soon as it + finishes its tail-off. + + It can also be called at any time during the render callback if the sound happens + to have finished, e.g. if it's playing a sample and the sample finishes. + */ + void clearCurrentNote(); + + +private: + //============================================================================== + friend class Synthesiser; + + double currentSampleRate; + int currentlyPlayingNote, currentPlayingMidiChannel; + uint32 noteOnTime; + SynthesiserSound::Ptr currentlyPlayingSound; + bool keyIsDown, sustainPedalDown, sostenutoPedalDown; + + AudioSampleBuffer tempBuffer; + + CARLA_LEAK_DETECTOR (SynthesiserVoice) +}; + + +//============================================================================== +/** + Base class for a musical device that can play sounds. + + To create a synthesiser, you'll need to create a subclass of SynthesiserSound + to describe each sound available to your synth, and a subclass of SynthesiserVoice + which can play back one of these sounds. + + Then you can use the addVoice() and addSound() methods to give the synthesiser a + set of sounds, and a set of voices it can use to play them. If you only give it + one voice it will be monophonic - the more voices it has, the more polyphony it'll + have available. + + Then repeatedly call the renderNextBlock() method to produce the audio. Any midi + events that go in will be scanned for note on/off messages, and these are used to + start and stop the voices playing the appropriate sounds. + + While it's playing, you can also cause notes to be triggered by calling the noteOn(), + noteOff() and other controller methods. + + Before rendering, be sure to call the setCurrentPlaybackSampleRate() to tell it + what the target playback rate is. This value is passed on to the voices so that + they can pitch their output correctly. +*/ +class Synthesiser +{ +public: + //============================================================================== + /** Creates a new synthesiser. + You'll need to add some sounds and voices before it'll make any sound. + */ + Synthesiser(); + + /** Destructor. */ + virtual ~Synthesiser(); + + //============================================================================== + /** Deletes all voices. */ + void clearVoices(); + + /** Returns the number of voices that have been added. */ + int getNumVoices() const noexcept { return voices.size(); } + + /** Returns one of the voices that have been added. */ + SynthesiserVoice* getVoice (int index) const; + + /** Adds a new voice to the synth. + + All the voices should be the same class of object and are treated equally. + + The object passed in will be managed by the synthesiser, which will delete + it later on when no longer needed. The caller should not retain a pointer to the + voice. + */ + SynthesiserVoice* addVoice (SynthesiserVoice* newVoice); + + /** Deletes one of the voices. */ + void removeVoice (int index); + + //============================================================================== + /** Deletes all sounds. */ + void clearSounds(); + + /** Returns the number of sounds that have been added to the synth. */ + int getNumSounds() const noexcept { return sounds.size(); } + + /** Returns one of the sounds. */ + SynthesiserSound* getSound (int index) const noexcept { return sounds [index]; } + + /** Adds a new sound to the synthesiser. + + The object passed in is reference counted, so will be deleted when the + synthesiser and all voices are no longer using it. + */ + SynthesiserSound* addSound (const SynthesiserSound::Ptr& newSound); + + /** Removes and deletes one of the sounds. */ + void removeSound (int index); + + //============================================================================== + /** If set to true, then the synth will try to take over an existing voice if + it runs out and needs to play another note. + + The value of this boolean is passed into findFreeVoice(), so the result will + depend on the implementation of this method. + */ + void setNoteStealingEnabled (bool shouldStealNotes); + + /** Returns true if note-stealing is enabled. + @see setNoteStealingEnabled + */ + bool isNoteStealingEnabled() const noexcept { return shouldStealNotes; } + + //============================================================================== + /** Triggers a note-on event. + + The default method here will find all the sounds that want to be triggered by + this note/channel. For each sound, it'll try to find a free voice, and use the + voice to start playing the sound. + + Subclasses might want to override this if they need a more complex algorithm. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + The midiChannel parameter is the channel, between 1 and 16 inclusive. + */ + virtual void noteOn (int midiChannel, + int midiNoteNumber, + float velocity); + + /** Triggers a note-off event. + + This will turn off any voices that are playing a sound for the given note/channel. + + If allowTailOff is true, the voices will be allowed to fade out the notes gracefully + (if they can do). If this is false, the notes will all be cut off immediately. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + The midiChannel parameter is the channel, between 1 and 16 inclusive. + */ + virtual void noteOff (int midiChannel, + int midiNoteNumber, + float velocity, + bool allowTailOff); + + /** Turns off all notes. + + This will turn off any voices that are playing a sound on the given midi channel. + + If midiChannel is 0 or less, then all voices will be turned off, regardless of + which channel they're playing. Otherwise it represents a valid midi channel, from + 1 to 16 inclusive. + + If allowTailOff is true, the voices will be allowed to fade out the notes gracefully + (if they can do). If this is false, the notes will all be cut off immediately. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + */ + virtual void allNotesOff (int midiChannel, + bool allowTailOff); + + /** Sends a pitch-wheel message to any active voices. + + This will send a pitch-wheel message to any voices that are playing sounds on + the given midi channel. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + @param midiChannel the midi channel, from 1 to 16 inclusive + @param wheelValue the wheel position, from 0 to 0x3fff, as returned by MidiMessage::getPitchWheelValue() + */ + virtual void handlePitchWheel (int midiChannel, + int wheelValue); + + /** Sends a midi controller message to any active voices. + + This will send a midi controller message to any voices that are playing sounds on + the given midi channel. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + @param midiChannel the midi channel, from 1 to 16 inclusive + @param controllerNumber the midi controller type, as returned by MidiMessage::getControllerNumber() + @param controllerValue the midi controller value, between 0 and 127, as returned by MidiMessage::getControllerValue() + */ + virtual void handleController (int midiChannel, + int controllerNumber, + int controllerValue); + + /** Sends an aftertouch message. + + This will send an aftertouch message to any voices that are playing sounds on + the given midi channel and note number. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + @param midiChannel the midi channel, from 1 to 16 inclusive + @param midiNoteNumber the midi note number, 0 to 127 + @param aftertouchValue the aftertouch value, between 0 and 127, + as returned by MidiMessage::getAftertouchValue() + */ + virtual void handleAftertouch (int midiChannel, int midiNoteNumber, int aftertouchValue); + + /** Sends a channel pressure message. + + This will send a channel pressure message to any voices that are playing sounds on + the given midi channel. + + This method will be called automatically according to the midi data passed into + renderNextBlock(), but may be called explicitly too. + + @param midiChannel the midi channel, from 1 to 16 inclusive + @param channelPressureValue the pressure value, between 0 and 127, as returned + by MidiMessage::getChannelPressureValue() + */ + virtual void handleChannelPressure (int midiChannel, int channelPressureValue); + + /** Handles a sustain pedal event. */ + virtual void handleSustainPedal (int midiChannel, bool isDown); + + /** Handles a sostenuto pedal event. */ + virtual void handleSostenutoPedal (int midiChannel, bool isDown); + + /** Can be overridden to handle soft pedal events. */ + virtual void handleSoftPedal (int midiChannel, bool isDown); + + /** Can be overridden to handle an incoming program change message. + The base class implementation of this has no effect, but you may want to make your + own synth react to program changes. + */ + virtual void handleProgramChange (int midiChannel, + int programNumber); + + //============================================================================== + /** Tells the synthesiser what the sample rate is for the audio it's being used to render. + + This value is propagated to the voices so that they can use it to render the correct + pitches. + */ + virtual void setCurrentPlaybackSampleRate (double sampleRate); + + /** Creates the next block of audio output. + + This will process the next numSamples of data from all the voices, and add that output + to the audio block supplied, starting from the offset specified. Note that the + data will be added to the current contents of the buffer, so you should clear it + before calling this method if necessary. + + The midi events in the inputMidi buffer are parsed for note and controller events, + and these are used to trigger the voices. Note that the startSample offset applies + both to the audio output buffer and the midi input buffer, so any midi events + with timestamps outside the specified region will be ignored. + */ + inline void renderNextBlock (AudioSampleBuffer& outputAudio, + const MidiBuffer& inputMidi, + int startSample, + int numSamples) + { processNextBlock (outputAudio, inputMidi, startSample, 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; } + + /** Sets a minimum limit on the size to which audio sub-blocks will be divided when rendering. + + When rendering, the audio blocks that are passed into renderNextBlock() will be split up + into smaller blocks that lie between all the incoming midi messages, and it is these smaller + sub-blocks that are rendered with multiple calls to renderVoices(). + + Obviously in a pathological case where there are midi messages on every sample, then + renderVoices() could be called once per sample and lead to poor performance, so this + setting allows you to set a lower limit on the block size. + + The default setting is 32, which means that midi messages are accurate to about < 1ms + accuracy, which is probably fine for most purposes, but you may want to increase or + decrease this value for your synth. + + If shouldBeStrict is true, the audio sub-blocks will strictly never be smaller than numSamples. + + If shouldBeStrict is false (default), the first audio sub-block in the buffer is allowed + to be smaller, to make sure that the first MIDI event in a buffer will always be sample-accurate + (this can sometimes help to avoid quantisation or phasing issues). + */ + void setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict = false) noexcept; + +protected: + //============================================================================== + /** This is used to control access to the rendering callback and the note trigger methods. */ + CarlaMutex lock; + + OwnedArray voices; + ReferenceCountedArray sounds; + + /** The last pitch-wheel values for each midi channel. */ + int lastPitchWheelValues [16]; + + /** Renders the voices for the given range. + By default this just calls renderNextBlock() on each voice, but you may need + to override it to handle custom cases. + */ + 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. + + Returns nullptr if all voices are busy and stealing isn't enabled. + + To implement a custom note-stealing algorithm, you can either override this + method, or (preferably) override findVoiceToSteal(). + */ + virtual SynthesiserVoice* findFreeVoice (SynthesiserSound* soundToPlay, + int midiChannel, + int midiNoteNumber, + bool stealIfNoneAvailable) const; + + /** Chooses a voice that is most suitable for being re-used. + 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, + int midiChannel, + int midiNoteNumber) const; + + /** Starts a specified voice playing a particular sound. + You'll probably never need to call this, it's used internally by noteOn(), but + may be needed by subclasses for custom behaviours. + */ + void startVoice (SynthesiserVoice* voice, + SynthesiserSound* sound, + int midiChannel, + int midiNoteNumber, + float velocity); + + /** Stops a given voice. + You should never need to call this, it's used internally by noteOff, but is protected + in case it's useful for some custom subclasses. It basically just calls through to + SynthesiserVoice::stopNote(), and has some assertions to sanity-check a few things. + */ + void stopVoice (SynthesiserVoice*, float velocity, bool allowTailOff); + + /** Can be overridden to do custom handling of incoming midi events. */ + virtual void handleMidiEvent (const MidiMessage&); + +private: + //============================================================================== + void processNextBlock (AudioSampleBuffer& outputAudio, + const MidiBuffer& inputMidi, + int startSample, + int numSamples); + //============================================================================== + double sampleRate; + uint32 lastNoteOnCounter; + int minimumSubBlockSize; + bool subBlockSubdivisionIsStrict; + bool shouldStealNotes; + BigInteger sustainPedalsDown; + + CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Synthesiser) +}; + +} + +#endif // WATER_SYNTHESISER_H_INCLUDED diff --git a/source/modules/water/water.cpp b/source/modules/water/water.cpp index f2ef36658..a7d10e12b 100644 --- a/source/modules/water/water.cpp +++ b/source/modules/water/water.cpp @@ -1,7 +1,7 @@ /* * Cross-platform C++ library for Carla, based on Juce v4 - * Copyright (C) 2015 ROLI Ltd. - * Copyright (C) 2017 Filipe Coelho + * Copyright (C) 2015-2016 ROLI Ltd. + * Copyright (C) 2017-2018 Filipe Coelho * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -45,6 +45,8 @@ HINSTANCE water_getCurrentModuleInstanceHandle() noexcept #include "files/FileOutputStream.cpp" #include "files/TemporaryFile.cpp" +#include "maths/BigInteger.cpp" + #include "maths/Random.cpp" #include "memory/MemoryBlock.cpp" @@ -65,6 +67,8 @@ HINSTANCE water_getCurrentModuleInstanceHandle() noexcept #include "streams/MemoryOutputStream.cpp" #include "streams/OutputStream.cpp" +#include "synthesisers/Synthesiser.cpp" + #include "text/CharacterFunctions.cpp" #include "text/Identifier.cpp" #include "text/StringArray.cpp" diff --git a/source/modules/water/water.h b/source/modules/water/water.h index 59b7c534e..668da7794 100644 --- a/source/modules/water/water.h +++ b/source/modules/water/water.h @@ -1,6 +1,6 @@ /* * Cross-platform C++ library for Carla, based on Juce v4 - * Copyright (C) 2015 ROLI Ltd. + * Copyright (C) 2015-2016 ROLI Ltd. * Copyright (C) 2017-2018 Filipe Coelho * * This program is free software; you can redistribute it and/or