@@ -7,9 +7,12 @@ | |||||
#include "SFZDebug.h" | #include "SFZDebug.h" | ||||
#include <stdarg.h> | #include <stdarg.h> | ||||
namespace sfzero | |||||
{ | |||||
#ifdef DEBUG | #ifdef DEBUG | ||||
void sfzero::dbgprintf(const char *msg, ...) | |||||
void dbgprintf(const char *msg, ...) | |||||
{ | { | ||||
va_list args; | va_list args; | ||||
@@ -22,3 +25,5 @@ void sfzero::dbgprintf(const char *msg, ...) | |||||
} | } | ||||
#endif // JUCE_DEBUG | #endif // JUCE_DEBUG | ||||
} |
@@ -6,16 +6,19 @@ | |||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZEG.h" | #include "SFZEG.h" | ||||
namespace sfzero | |||||
{ | |||||
static const float fastReleaseTime = 0.01f; | static const float fastReleaseTime = 0.01f; | ||||
sfzero::EG::EG() | |||||
EG::EG() | |||||
: segment_(), sampleRate_(0), exponentialDecay_(false), level_(0), slope_(0), samplesUntilNextSegment_(0), segmentIsExponential_(false) | : 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) | const EGParameters *velMod) | ||||
{ | { | ||||
parameters_ = *newParameters; | parameters_ = *newParameters; | ||||
@@ -41,7 +44,7 @@ void sfzero::EG::startNote(const EGParameters *newParameters, float floatVelocit | |||||
startDelay(); | startDelay(); | ||||
} | } | ||||
void sfzero::EG::nextSegment() | |||||
void EG::nextSegment() | |||||
{ | { | ||||
switch (segment_) | 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; | segment_ = Release; | ||||
samplesUntilNextSegment_ = static_cast<int>(fastReleaseTime * sampleRate_); | samplesUntilNextSegment_ = static_cast<int>(fastReleaseTime * sampleRate_); | ||||
@@ -82,7 +85,7 @@ void sfzero::EG::fastRelease() | |||||
segmentIsExponential_ = false; | segmentIsExponential_ = false; | ||||
} | } | ||||
void sfzero::EG::startDelay() | |||||
void EG::startDelay() | |||||
{ | { | ||||
if (parameters_.delay <= 0) | if (parameters_.delay <= 0) | ||||
{ | { | ||||
@@ -98,7 +101,7 @@ void sfzero::EG::startDelay() | |||||
} | } | ||||
} | } | ||||
void sfzero::EG::startAttack() | |||||
void EG::startAttack() | |||||
{ | { | ||||
if (parameters_.attack <= 0) | if (parameters_.attack <= 0) | ||||
{ | { | ||||
@@ -114,7 +117,7 @@ void sfzero::EG::startAttack() | |||||
} | } | ||||
} | } | ||||
void sfzero::EG::startHold() | |||||
void EG::startHold() | |||||
{ | { | ||||
if (parameters_.hold <= 0) | if (parameters_.hold <= 0) | ||||
{ | { | ||||
@@ -131,7 +134,7 @@ void sfzero::EG::startHold() | |||||
} | } | ||||
} | } | ||||
void sfzero::EG::startDecay() | |||||
void EG::startDecay() | |||||
{ | { | ||||
if (parameters_.decay <= 0) | if (parameters_.decay <= 0) | ||||
{ | { | ||||
@@ -170,7 +173,7 @@ void sfzero::EG::startDecay() | |||||
} | } | ||||
} | } | ||||
void sfzero::EG::startSustain() | |||||
void EG::startSustain() | |||||
{ | { | ||||
if (parameters_.sustain <= 0) | if (parameters_.sustain <= 0) | ||||
{ | { | ||||
@@ -186,7 +189,7 @@ void sfzero::EG::startSustain() | |||||
} | } | ||||
} | } | ||||
void sfzero::EG::startRelease() | |||||
void EG::startRelease() | |||||
{ | { | ||||
float release = parameters_.release; | 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; | |||||
} |
@@ -10,11 +10,14 @@ | |||||
#include "water/memory/MemoryBlock.h" | #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; | water::MemoryBlock contents; | ||||
bool ok = file.loadFileAsData(contents); | bool ok = file.loadFileAsData(contents); | ||||
@@ -28,15 +31,15 @@ void sfzero::Reader::read(const water::File &file) | |||||
read(static_cast<const char *>(contents.getData()), static_cast<int>(contents.getSize())); | read(static_cast<const char *>(contents.getData()), static_cast<int>(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 *p = text; | ||||
const char *end = text + length; | const char *end = text + length; | ||||
char c = 0; | char c = 0; | ||||
sfzero::Region curGroup; | |||||
sfzero::Region curRegion; | |||||
sfzero::Region *buildingRegion = nullptr; | |||||
Region curGroup; | |||||
Region curRegion; | |||||
Region *buildingRegion = nullptr; | |||||
bool inControl = false; | bool inControl = false; | ||||
water::String defaultPath; | water::String defaultPath; | ||||
@@ -108,7 +111,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) | |||||
error("Unterminated tag"); | error("Unterminated tag"); | ||||
goto fatalError; | goto fatalError; | ||||
} | } | ||||
sfzero::StringSlice tag(tagStart, p - 1); | |||||
StringSlice tag(tagStart, p - 1); | |||||
if (tag == "region") | if (tag == "region") | ||||
{ | { | ||||
if (buildingRegion && (buildingRegion == &curRegion)) | if (buildingRegion && (buildingRegion == &curRegion)) | ||||
@@ -176,7 +179,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) | |||||
error("Malformed parameter"); | error("Malformed parameter"); | ||||
goto nextElement; | goto nextElement; | ||||
} | } | ||||
sfzero::StringSlice opcode(parameterStart, p - 1); | |||||
StringSlice opcode(parameterStart, p - 1); | |||||
if (inControl) | if (inControl) | ||||
{ | { | ||||
if (opcode == "default_path") | if (opcode == "default_path") | ||||
@@ -259,7 +262,7 @@ void sfzero::Reader::read(const char *text, unsigned int length) | |||||
} | } | ||||
else if (opcode == "trigger") | else if (opcode == "trigger") | ||||
{ | { | ||||
buildingRegion->trigger = static_cast<sfzero::Region::Trigger>(triggerValue(value)); | |||||
buildingRegion->trigger = static_cast<Region::Trigger>(triggerValue(value)); | |||||
} | } | ||||
else if (opcode == "group") | 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"; | bool modeIsSupported = value == "no_loop" || value == "one_shot" || value == "loop_continuous"; | ||||
if (modeIsSupported) | if (modeIsSupported) | ||||
{ | { | ||||
buildingRegion->loop_mode = static_cast<sfzero::Region::LoopMode>(loopModeValue(value)); | |||||
buildingRegion->loop_mode = static_cast<Region::LoopMode>(loopModeValue(value)); | |||||
} | } | ||||
else | 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. | // Check for DOS-style line ending. | ||||
char lineEndChar = *p++; | char lineEndChar = *p++; | ||||
@@ -445,7 +448,7 @@ const char *sfzero::Reader::handleLineEnd(const char *p) | |||||
return 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. | // Paths are kind of funny to parse because they can contain whitespace. | ||||
const char *p = pIn; | const char *p = pIn; | ||||
@@ -497,7 +500,7 @@ const char *sfzero::Reader::readPathInto(water::String *pathOut, const char *pIn | |||||
return p; | return p; | ||||
} | } | ||||
int sfzero::Reader::keyValue(const water::String &str) | |||||
int Reader::keyValue(const water::String &str) | |||||
{ | { | ||||
auto chars = str.toRawUTF8(); | auto chars = str.toRawUTF8(); | ||||
@@ -542,56 +545,58 @@ int sfzero::Reader::keyValue(const water::String &str) | |||||
return result; | return result; | ||||
} | } | ||||
int sfzero::Reader::triggerValue(const water::String &str) | |||||
int Reader::triggerValue(const water::String &str) | |||||
{ | { | ||||
if (str == "release") | if (str == "release") | ||||
{ | { | ||||
return sfzero::Region::release; | |||||
return Region::release; | |||||
} | } | ||||
if (str == "first") | if (str == "first") | ||||
{ | { | ||||
return sfzero::Region::first; | |||||
return Region::first; | |||||
} | } | ||||
if (str == "legato") | 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") | if (str == "no_loop") | ||||
{ | { | ||||
return sfzero::Region::no_loop; | |||||
return Region::no_loop; | |||||
} | } | ||||
if (str == "one_shot") | if (str == "one_shot") | ||||
{ | { | ||||
return sfzero::Region::one_shot; | |||||
return Region::one_shot; | |||||
} | } | ||||
if (str == "loop_continuous") | if (str == "loop_continuous") | ||||
{ | { | ||||
return sfzero::Region::loop_continuous; | |||||
return Region::loop_continuous; | |||||
} | } | ||||
if (str == "loop_sustain") | 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; | *newRegion = *region; | ||||
sound_->addRegion(newRegion); | sound_->addRegion(newRegion); | ||||
} | } | ||||
void sfzero::Reader::error(const water::String &message) | |||||
void Reader::error(const water::String &message) | |||||
{ | { | ||||
water::String fullMessage = message; | water::String fullMessage = message; | ||||
fullMessage += " (line " + water::String(line_) + ")."; | fullMessage += " (line " + water::String(line_) + ")."; | ||||
sound_->addError(fullMessage); | sound_->addError(fullMessage); | ||||
} | } | ||||
} |
@@ -4,10 +4,14 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZRegion.h" | #include "SFZRegion.h" | ||||
#include "SFZSample.h" | #include "SFZSample.h" | ||||
void sfzero::EGParameters::clear() | |||||
namespace sfzero | |||||
{ | |||||
void EGParameters::clear() | |||||
{ | { | ||||
delay = 0.0; | delay = 0.0; | ||||
start = 0.0; | start = 0.0; | ||||
@@ -18,15 +22,15 @@ void sfzero::EGParameters::clear() | |||||
release = 0.0; | release = 0.0; | ||||
} | } | ||||
void sfzero::EGParameters::clearMod() | |||||
void EGParameters::clearMod() | |||||
{ | { | ||||
// Clear for velocity or other modification. | // Clear for velocity or other modification. | ||||
delay = start = attack = hold = decay = sustain = release = 0.0; | 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)); | memset(this, 0, sizeof(*this)); | ||||
hikey = 127; | hikey = 127; | ||||
@@ -41,7 +45,7 @@ void sfzero::Region::clear() | |||||
ampeg_veltrack.clearMod(); | ampeg_veltrack.clearMod(); | ||||
} | } | ||||
void sfzero::Region::clearForSF2() | |||||
void Region::clearForSF2() | |||||
{ | { | ||||
clear(); | clear(); | ||||
pitch_keycenter = -1; | pitch_keycenter = -1; | ||||
@@ -56,7 +60,7 @@ void sfzero::Region::clearForSF2() | |||||
ampeg.release = -12000.0; | ampeg.release = -12000.0; | ||||
} | } | ||||
void sfzero::Region::clearForRelativeSF2() | |||||
void Region::clearForRelativeSF2() | |||||
{ | { | ||||
clear(); | clear(); | ||||
pitch_keytrack = 0; | pitch_keytrack = 0; | ||||
@@ -64,7 +68,7 @@ void sfzero::Region::clearForRelativeSF2() | |||||
ampeg.sustain = 0.0; | ampeg.sustain = 0.0; | ||||
} | } | ||||
void sfzero::Region::addForSF2(sfzero::Region *other) | |||||
void Region::addForSF2(Region *other) | |||||
{ | { | ||||
offset += other->offset; | offset += other->offset; | ||||
end += other->end; | end += other->end; | ||||
@@ -84,7 +88,7 @@ void sfzero::Region::addForSF2(sfzero::Region *other) | |||||
ampeg.release += other->ampeg.release; | ampeg.release += other->ampeg.release; | ||||
} | } | ||||
void sfzero::Region::sf2ToSFZ() | |||||
void Region::sf2ToSFZ() | |||||
{ | { | ||||
// EG times need to be converted from timecents to seconds. | // EG times need to be converted from timecents to seconds. | ||||
ampeg.delay = timecents2Secs(static_cast<int>(ampeg.delay)); | ampeg.delay = timecents2Secs(static_cast<int>(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); | water::String info = water::String::formatted("%d - %d, vel %d - %d", lokey, hikey, lovel, hivel); | ||||
if (sample) | if (sample) | ||||
@@ -143,4 +147,6 @@ water::String sfzero::Region::dump() | |||||
return info; | return info; | ||||
} | } | ||||
float sfzero::Region::timecents2Secs(int timecents) { return static_cast<float>(pow(2.0, timecents / 1200.0)); } | |||||
float Region::timecents2Secs(int timecents) { return static_cast<float>(pow(2.0, timecents / 1200.0)); } | |||||
} |
@@ -90,6 +90,7 @@ struct Region | |||||
static float timecents2Secs(int timecents); | static float timecents2Secs(int timecents); | ||||
}; | }; | ||||
} | } | ||||
#endif // SFZREGION_H_INCLUDED | #endif // SFZREGION_H_INCLUDED |
@@ -4,11 +4,15 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZSample.h" | #include "SFZSample.h" | ||||
#include "SFZDebug.h" | #include "SFZDebug.h" | ||||
namespace sfzero | |||||
{ | |||||
#if 0 | #if 0 | ||||
bool sfzero::Sample::load(water::AudioFormatManager *formatManager) | |||||
bool Sample::load(water::AudioFormatManager *formatManager) | |||||
{ | { | ||||
water::AudioFormatReader *reader = formatManager->createReaderFor(file_); | water::AudioFormatReader *reader = formatManager->createReaderFor(file_); | ||||
@@ -37,31 +41,31 @@ bool sfzero::Sample::load(water::AudioFormatManager *formatManager) | |||||
} | } | ||||
#endif | #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; | buffer_ = newBuffer; | ||||
sampleLength_ = buffer_->getNumSamples(); | sampleLength_ = buffer_->getNumSamples(); | ||||
} | } | ||||
water::AudioSampleBuffer *sfzero::Sample::detachBuffer() | |||||
water::AudioSampleBuffer *Sample::detachBuffer() | |||||
{ | { | ||||
water::AudioSampleBuffer *result = buffer_; | water::AudioSampleBuffer *result = buffer_; | ||||
buffer_ = nullptr; | buffer_ = nullptr; | ||||
return result; | return result; | ||||
} | } | ||||
water::String sfzero::Sample::dump() { return file_.getFullPathName() + "\n"; } | |||||
water::String Sample::dump() { return file_.getFullPathName() + "\n"; } | |||||
#ifdef DEBUG | #ifdef DEBUG | ||||
void sfzero::Sample::checkIfZeroed(const char *where) | |||||
void Sample::checkIfZeroed(const char *where) | |||||
{ | { | ||||
if (buffer_ == nullptr) | if (buffer_ == nullptr) | ||||
{ | { | ||||
sfzero::dbgprintf("SFZSample::checkIfZeroed(%s): no buffer!", where); | |||||
dbgprintf("SFZSample::checkIfZeroed(%s): no buffer!", where); | |||||
return; | return; | ||||
} | } | ||||
@@ -81,12 +85,13 @@ void sfzero::Sample::checkIfZeroed(const char *where) | |||||
} | } | ||||
if (nonzero > 0) | 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 | else | ||||
{ | { | ||||
sfzero::dbgprintf("Buffer zeroed at %s! (%lu zeros)", where, zero); | |||||
dbgprintf("Buffer zeroed at %s! (%lu zeros)", where, zero); | |||||
} | } | ||||
} | } | ||||
#endif // JUCE_DEBUG | #endif // JUCE_DEBUG | ||||
} |
@@ -4,13 +4,17 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZSound.h" | #include "SFZSound.h" | ||||
#include "SFZReader.h" | #include "SFZReader.h" | ||||
#include "SFZRegion.h" | #include "SFZRegion.h" | ||||
#include "SFZSample.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(); | int numRegions = regions_.size(); | ||||
@@ -20,21 +24,21 @@ sfzero::Sound::~Sound() | |||||
regions_.set(i, nullptr); | regions_.set(i, nullptr); | ||||
} | } | ||||
for (water::HashMap<water::String, sfzero::Sample *>::Iterator i(samples_); i.next();) | |||||
for (water::HashMap<water::String, Sample *>::Iterator i(samples_); i.next();) | |||||
{ | { | ||||
delete i.getValue(); | 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. | // Just say yes; we can't truly know unless we're told the velocity as well. | ||||
return true; | 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('\\', '/'); | path = path.replaceCharacter('\\', '/'); | ||||
defaultPath = defaultPath.replaceCharacter('\\', '/'); | defaultPath = defaultPath.replaceCharacter('\\', '/'); | ||||
@@ -49,18 +53,18 @@ sfzero::Sample *sfzero::Sound::addSample(water::String path, water::String defau | |||||
sampleFile = defaultDir.getChildFile(path); | sampleFile = defaultDir.getChildFile(path); | ||||
} | } | ||||
water::String samplePath = sampleFile.getFullPathName(); | water::String samplePath = sampleFile.getFullPathName(); | ||||
sfzero::Sample *sample = samples_[samplePath]; | |||||
Sample *sample = samples_[samplePath]; | |||||
if (sample == nullptr) | if (sample == nullptr) | ||||
{ | { | ||||
sample = new sfzero::Sample(sampleFile); | |||||
sample = new Sample(sampleFile); | |||||
samples_.set(samplePath, sample); | samples_.set(samplePath, sample); | ||||
} | } | ||||
return 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)) | 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_); | 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) | if (progressVar) | ||||
{ | { | ||||
@@ -86,9 +90,9 @@ void sfzero::Sound::loadSamples(water::AudioFormatManager* formatManager, double | |||||
} | } | ||||
double numSamplesLoaded = 1.0, numSamples = samples_.size(); | double numSamplesLoaded = 1.0, numSamples = samples_.size(); | ||||
for (water::HashMap<water::String, sfzero::Sample *>::Iterator i(samples_); i.next();) | |||||
for (water::HashMap<water::String, Sample *>::Iterator i(samples_); i.next();) | |||||
{ | { | ||||
sfzero::Sample *sample = i.getValue(); | |||||
Sample *sample = i.getValue(); | |||||
#if 0 | #if 0 | ||||
bool ok = sample->load(formatManager); | bool ok = sample->load(formatManager); | ||||
if (!ok) | 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(); | int numRegions = regions_.size(); | ||||
for (int i = 0; i < numRegions; ++i) | for (int i = 0; i < numRegions; ++i) | ||||
{ | { | ||||
sfzero::Region *region = regions_[i]; | |||||
Region *region = regions_[i]; | |||||
if (region->matches(note, velocity, trigger)) | if (region->matches(note, velocity, trigger)) | ||||
{ | { | ||||
return region; | return region; | ||||
@@ -130,19 +134,19 @@ sfzero::Region *sfzero::Sound::getRegionFor(int note, int velocity, sfzero::Regi | |||||
return nullptr; | 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; | water::String info; | ||||
auto &errors = getErrors(); | auto &errors = getErrors(); | ||||
@@ -184,7 +188,7 @@ water::String sfzero::Sound::dump() | |||||
if (samples_.size() > 0) | if (samples_.size() > 0) | ||||
{ | { | ||||
info << samples_.size() << " samples: \n"; | info << samples_.size() << " samples: \n"; | ||||
for (water::HashMap<water::String, sfzero::Sample *>::Iterator i(samples_); i.next();) | |||||
for (water::HashMap<water::String, Sample *>::Iterator i(samples_); i.next();) | |||||
{ | { | ||||
info << i.getValue()->dump(); | info << i.getValue()->dump(); | ||||
} | } | ||||
@@ -195,3 +199,5 @@ water::String sfzero::Sound::dump() | |||||
} | } | ||||
return info; | return info; | ||||
} | } | ||||
} |
@@ -9,23 +9,13 @@ | |||||
#include "SFZRegion.h" | #include "SFZRegion.h" | ||||
#include "water/containers/HashMap.h" | #include "water/containers/HashMap.h" | ||||
#include "water/memory/ReferenceCountedObject.h" | |||||
#include "water/synthesisers/Synthesiser.h" | |||||
#include "water/text/StringArray.h" | #include "water/text/StringArray.h" | ||||
#include "CarlaJuceUtils.hpp" | |||||
#include "CarlaThread.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 | namespace sfzero | ||||
{ | { | ||||
@@ -4,29 +4,32 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZSynth.h" | #include "SFZSynth.h" | ||||
#include "SFZSound.h" | #include "SFZSound.h" | ||||
#include "SFZVoice.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; | int i; | ||||
const water::ScopedLock locker(lock); | |||||
const CarlaMutexLocker locker(lock); | |||||
int midiVelocity = static_cast<int>(velocity * 127); | int midiVelocity = static_cast<int>(velocity * 127); | ||||
// First, stop any currently-playing sounds in the group. | // First, stop any currently-playing sounds in the group. | ||||
//*** Currently, this only pays attention to the first matching region. | //*** Currently, this only pays attention to the first matching region. | ||||
int group = 0; | int group = 0; | ||||
sfzero::Sound *sound = dynamic_cast<sfzero::Sound *>(getSound(0)); | |||||
Sound *sound = dynamic_cast<Sound *>(getSound(0)); | |||||
if (sound) | if (sound) | ||||
{ | { | ||||
sfzero::Region *region = sound->getRegionFor(midiNoteNumber, midiVelocity); | |||||
Region *region = sound->getRegionFor(midiNoteNumber, midiVelocity); | |||||
if (region) | if (region) | ||||
{ | { | ||||
group = region->group; | group = region->group; | ||||
@@ -36,7 +39,7 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) | |||||
{ | { | ||||
for (i = voices.size(); --i >= 0;) | for (i = voices.size(); --i >= 0;) | ||||
{ | { | ||||
sfzero::Voice *voice = dynamic_cast<sfzero::Voice *>(voices.getUnchecked(i)); | |||||
Voice *voice = dynamic_cast<Voice *>(voices.getUnchecked(i)); | |||||
if (voice == nullptr) | if (voice == nullptr) | ||||
{ | { | ||||
continue; | continue; | ||||
@@ -53,7 +56,7 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) | |||||
bool anyNotesPlaying = false; | bool anyNotesPlaying = false; | ||||
for (i = voices.size(); --i >= 0;) | for (i = voices.size(); --i >= 0;) | ||||
{ | { | ||||
sfzero::Voice *voice = dynamic_cast<sfzero::Voice *>(voices.getUnchecked(i)); | |||||
Voice *voice = dynamic_cast<Voice *>(voices.getUnchecked(i)); | |||||
if (voice == nullptr) | if (voice == nullptr) | ||||
{ | { | ||||
continue; | continue; | ||||
@@ -78,17 +81,17 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) | |||||
} | } | ||||
// Play *all* matching regions. | // 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) | if (sound) | ||||
{ | { | ||||
int numRegions = sound->getNumRegions(); | int numRegions = sound->getNumRegions(); | ||||
for (i = 0; i < numRegions; ++i) | for (i = 0; i < numRegions; ++i) | ||||
{ | { | ||||
sfzero::Region *region = sound->regionAt(i); | |||||
Region *region = sound->regionAt(i); | |||||
if (region->matches(midiNoteNumber, midiVelocity, trigger)) | if (region->matches(midiNoteNumber, midiVelocity, trigger)) | ||||
{ | { | ||||
sfzero::Voice *voice = | |||||
dynamic_cast<sfzero::Voice *>(findFreeVoice(sound, midiNoteNumber, midiChannel, isNoteStealingEnabled())); | |||||
Voice *voice = | |||||
dynamic_cast<Voice *>(findFreeVoice(sound, midiNoteNumber, midiChannel, isNoteStealingEnabled())); | |||||
if (voice) | if (voice) | ||||
{ | { | ||||
voice->setRegion(region); | voice->setRegion(region); | ||||
@@ -100,23 +103,21 @@ void sfzero::Synth::noteOn(int midiChannel, int midiNoteNumber, float velocity) | |||||
noteVelocities_[midiNoteNumber] = midiVelocity; | 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); | Synthesiser::noteOff(midiChannel, midiNoteNumber, velocity, allowTailOff); | ||||
// Start release region. | // Start release region. | ||||
sfzero::Sound *sound = dynamic_cast<sfzero::Sound *>(getSound(0)); | |||||
Sound *sound = dynamic_cast<Sound *>(getSound(0)); | |||||
if (sound) | if (sound) | ||||
{ | { | ||||
sfzero::Region *region = sound->getRegionFor(midiNoteNumber, noteVelocities_[midiNoteNumber], sfzero::Region::release); | |||||
Region *region = sound->getRegionFor(midiNoteNumber, noteVelocities_[midiNoteNumber], Region::release); | |||||
if (region) | if (region) | ||||
{ | { | ||||
sfzero::Voice *voice = dynamic_cast<sfzero::Voice *>(findFreeVoice(sound, midiNoteNumber, midiChannel, false)); | |||||
Voice *voice = dynamic_cast<Voice *>(findFreeVoice(sound, midiNoteNumber, midiChannel, false)); | |||||
if (voice) | if (voice) | ||||
{ | { | ||||
// Synthesiser is too locked-down (ivars are private rt protected), so | // 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; | int numUsed = 0; | ||||
#if 0 | |||||
for (int i = voices.size(); --i >= 0;) | for (int i = voices.size(); --i >= 0;) | ||||
{ | { | ||||
if (voices.getUnchecked(i)->getCurrentlyPlayingNote() >= 0) | if (voices.getUnchecked(i)->getCurrentlyPlayingNote() >= 0) | ||||
@@ -141,11 +140,11 @@ int sfzero::Synth::numVoicesUsed() | |||||
numUsed += 1; | numUsed += 1; | ||||
} | } | ||||
} | } | ||||
#endif | |||||
return numUsed; | return numUsed; | ||||
} | } | ||||
water::String sfzero::Synth::voiceInfoString() | |||||
water::String Synth::voiceInfoString() | |||||
{ | { | ||||
enum | enum | ||||
{ | { | ||||
@@ -154,10 +153,10 @@ water::String sfzero::Synth::voiceInfoString() | |||||
water::StringArray lines; | water::StringArray lines; | ||||
int numUsed = 0, numShown = 0; | int numUsed = 0, numShown = 0; | ||||
#if 0 | |||||
for (int i = voices.size(); --i >= 0;) | for (int i = voices.size(); --i >= 0;) | ||||
{ | { | ||||
sfzero::Voice *voice = dynamic_cast<sfzero::Voice *>(voices.getUnchecked(i)); | |||||
Voice *voice = dynamic_cast<Voice *>(voices.getUnchecked(i)); | |||||
if (voice->getCurrentlyPlayingNote() < 0) | if (voice->getCurrentlyPlayingNote() < 0) | ||||
{ | { | ||||
continue; | continue; | ||||
@@ -169,7 +168,9 @@ water::String sfzero::Synth::voiceInfoString() | |||||
} | } | ||||
lines.add(voice->infoString()); | lines.add(voice->infoString()); | ||||
} | } | ||||
#endif | |||||
lines.insert(0, "voices used: " + water::String(numUsed)); | lines.insert(0, "voices used: " + water::String(numUsed)); | ||||
return lines.joinIntoString("\n"); | return lines.joinIntoString("\n"); | ||||
} | } | ||||
} |
@@ -4,22 +4,13 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#ifndef SFZSYNTH_H_INCLUDED | #ifndef SFZSYNTH_H_INCLUDED | ||||
#define SFZSYNTH_H_INCLUDED | #define SFZSYNTH_H_INCLUDED | ||||
#include "SFZCommon.h" | #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 | namespace sfzero | ||||
{ | { | ||||
@@ -4,6 +4,7 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#include "SFZDebug.h" | #include "SFZDebug.h" | ||||
#include "SFZRegion.h" | #include "SFZRegion.h" | ||||
#include "SFZSample.h" | #include "SFZSample.h" | ||||
@@ -14,23 +15,26 @@ | |||||
#include <cmath> | #include <cmath> | ||||
namespace sfzero | |||||
{ | |||||
static const float globalGain = -1.0; | 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), | : 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) | sourceSamplePosition_(0), sampleEnd_(0), loopStart_(0), loopEnd_(0), numLoops_(0), curVelocity_(0) | ||||
{ | { | ||||
ampeg_.setExponentialDecay(true); | ampeg_.setExponentialDecay(true); | ||||
} | } | ||||
sfzero::Voice::~Voice() {} | |||||
Voice::~Voice() {} | |||||
bool sfzero::Voice::canPlaySound(water::SynthesiserSound *sound) { return dynamic_cast<sfzero::Sound *>(sound) != nullptr; } | |||||
bool Voice::canPlaySound(water::SynthesiserSound *sound) { return dynamic_cast<Sound *>(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) | int currentPitchWheelPosition) | ||||
{ | { | ||||
sfzero::Sound *sound = dynamic_cast<sfzero::Sound *>(soundIn); | |||||
Sound *sound = dynamic_cast<Sound *>(soundIn); | |||||
if (sound == nullptr) | 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; | double adjustedPan = (region_->pan + 100.0) / 200.0; | ||||
noteGainLeft_ *= static_cast<float>(sqrt(1.0 - adjustedPan)); | noteGainLeft_ *= static_cast<float>(sqrt(1.0 - adjustedPan)); | ||||
noteGainRight_ *= static_cast<float>(sqrt(adjustedPan)); | noteGainRight_ *= static_cast<float>(sqrt(adjustedPan)); | ||||
#if 0 | |||||
ampeg_.startNote(®ion_->ampeg, floatVelocity, getSampleRate(), ®ion_->ampeg_veltrack); | ampeg_.startNote(®ion_->ampeg, floatVelocity, getSampleRate(), ®ion_->ampeg_veltrack); | ||||
#endif | |||||
// Offset/end. | // Offset/end. | ||||
sourceSamplePosition_ = static_cast<double>(region_->offset); | sourceSamplePosition_ = static_cast<double>(region_->offset); | ||||
@@ -89,19 +91,19 @@ void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::Sy | |||||
// Loop. | // Loop. | ||||
loopStart_ = loopEnd_ = 0; | 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()) | if (region_->sample->getLoopStart() < region_->sample->getLoopEnd()) | ||||
{ | { | ||||
loopMode = sfzero::Region::loop_continuous; | |||||
loopMode = Region::loop_continuous; | |||||
} | } | ||||
else | 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) | if (region_->loop_start < region_->loop_end) | ||||
{ | { | ||||
@@ -117,7 +119,7 @@ void sfzero::Voice::startNote(int midiNoteNumber, float floatVelocity, water::Sy | |||||
numLoops_ = 0; | numLoops_ = 0; | ||||
} | } | ||||
void sfzero::Voice::stopNote(float /*velocity*/, bool allowTailOff) | |||||
void Voice::stopNote(float /*velocity*/, bool allowTailOff) | |||||
{ | { | ||||
if (!allowTailOff || (region_ == nullptr)) | if (!allowTailOff || (region_ == nullptr)) | ||||
{ | { | ||||
@@ -125,20 +127,20 @@ void sfzero::Voice::stopNote(float /*velocity*/, bool allowTailOff) | |||||
return; | return; | ||||
} | } | ||||
if (region_->loop_mode != sfzero::Region::one_shot) | |||||
if (region_->loop_mode != Region::one_shot) | |||||
{ | { | ||||
ampeg_.noteOff(); | ampeg_.noteOff(); | ||||
} | } | ||||
if (region_->loop_mode == sfzero::Region::loop_sustain) | |||||
if (region_->loop_mode == Region::loop_sustain) | |||||
{ | { | ||||
// Continue playing, but stop looping. | // Continue playing, but stop looping. | ||||
loopEnd_ = loopStart_; | loopEnd_ = loopStart_; | ||||
} | } | ||||
} | } | ||||
void sfzero::Voice::stopNoteForGroup() | |||||
void Voice::stopNoteForGroup() | |||||
{ | { | ||||
if (region_->off_mode == sfzero::Region::fast) | |||||
if (region_->off_mode == Region::fast) | |||||
{ | { | ||||
ampeg_.fastRelease(); | 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) | if (region_ == nullptr) | ||||
{ | { | ||||
@@ -160,8 +162,8 @@ void sfzero::Voice::pitchWheelMoved(int newValue) | |||||
calcPitchRatio(); | 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) | if (region_ == nullptr) | ||||
{ | { | ||||
@@ -265,17 +267,17 @@ void sfzero::Voice::renderNextBlock(water::AudioSampleBuffer &outputBuffer, int | |||||
ampeg_.setSamplesUntilNextSegment(samplesUntilNextAmpSegment); | 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"}; | const char *egSegmentNames[] = {"delay", "attack", "hold", "decay", "sustain", "release", "done"}; | ||||
@@ -294,7 +296,7 @@ water::String sfzero::Voice::infoString() | |||||
return info; | return info; | ||||
} | } | ||||
void sfzero::Voice::calcPitchRatio() | |||||
void Voice::calcPitchRatio() | |||||
{ | { | ||||
double note = curMidiNote_; | double note = curMidiNote_; | ||||
@@ -316,23 +318,21 @@ void sfzero::Voice::calcPitchRatio() | |||||
} | } | ||||
double targetFreq = fractionalMidiNoteInHz(adjustedPitch); | double targetFreq = fractionalMidiNoteInHz(adjustedPitch); | ||||
double naturalFreq = water::MidiMessage::getMidiNoteInHertz(region_->pitch_keycenter); | double naturalFreq = water::MidiMessage::getMidiNoteInHertz(region_->pitch_keycenter); | ||||
#if 0 | |||||
pitchRatio_ = (targetFreq * region_->sample->getSampleRate()) / (naturalFreq * getSampleRate()); | pitchRatio_ = (targetFreq * region_->sample->getSampleRate()) / (naturalFreq * getSampleRate()); | ||||
#endif | |||||
} | } | ||||
void sfzero::Voice::killNote() | |||||
void Voice::killNote() | |||||
{ | { | ||||
region_ = nullptr; | region_ = nullptr; | ||||
#if 0 | |||||
clearCurrentNote(); | 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. | // Like MidiMessage::getMidiNoteInHertz(), but with a float note. | ||||
note -= 69; | note -= 69; | ||||
// Now 0 = A | // Now 0 = A | ||||
return freqOfA * pow(2.0, note / 12.0); | return freqOfA * pow(2.0, note / 12.0); | ||||
} | } | ||||
} |
@@ -4,29 +4,17 @@ | |||||
* Forked from https://github.com/stevefolta/SFZero | * Forked from https://github.com/stevefolta/SFZero | ||||
* For license info please see the LICENSE file distributed with this source code | * For license info please see the LICENSE file distributed with this source code | ||||
*************************************************************************************/ | *************************************************************************************/ | ||||
#ifndef SFZVOICE_H_INCLUDED | #ifndef SFZVOICE_H_INCLUDED | ||||
#define SFZVOICE_H_INCLUDED | #define SFZVOICE_H_INCLUDED | ||||
#include "SFZEG.h" | #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 | namespace sfzero | ||||
{ | { | ||||
struct Region; | struct Region; | ||||
class Voice : public water::SynthesiserVoice | class Voice : public water::SynthesiserVoice | ||||
@@ -47,7 +35,7 @@ public: | |||||
bool isPlayingOneShot(); | bool isPlayingOneShot(); | ||||
int getGroup(); | int getGroup(); | ||||
water::uint64 getOffBy(); | |||||
water::int64 getOffBy(); | |||||
// Set the region to be used by the next startNote(). | // Set the region to be used by the next startNote(). | ||||
void setRegion(Region *nextRegion); | void setRegion(Region *nextRegion); | ||||
@@ -0,0 +1,240 @@ | |||||
/* | |||||
============================================================================== | |||||
This file is part of the JUCE library. | |||||
Copyright (c) 2016 ROLI Ltd. | |||||
Copyright (C) 2018 Filipe Coelho <falktx@falktx.com> | |||||
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<HeapBlock<uint32>&&> (other.heapAllocation)), | |||||
allocatedSize (other.allocatedSize), | |||||
highestBit (other.highestBit) | |||||
{ | |||||
std::memcpy (preallocated, other.preallocated, sizeof (preallocated)); | |||||
} | |||||
BigInteger& BigInteger::operator= (BigInteger&& other) noexcept | |||||
{ | |||||
heapAllocation = static_cast<HeapBlock<uint32>&&> (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; | |||||
} | |||||
} |
@@ -0,0 +1,118 @@ | |||||
/* | |||||
============================================================================== | |||||
This file is part of the Water library. | |||||
Copyright (c) 2016 ROLI Ltd. | |||||
Copyright (C) 2018 Filipe Coelho <falktx@falktx.com> | |||||
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<uint32> 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 |
@@ -0,0 +1,642 @@ | |||||
/* | |||||
============================================================================== | |||||
This file is part of the JUCE library. | |||||
Copyright (c) 2016 ROLI Ltd. | |||||
Copyright (C) 2018 Filipe Coelho <falktx@falktx.com> | |||||
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<SynthesiserVoice*> 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; | |||||
} | |||||
} |
@@ -0,0 +1,626 @@ | |||||
/* | |||||
============================================================================== | |||||
This file is part of the Water library. | |||||
Copyright (c) 2016 ROLI Ltd. | |||||
Copyright (C) 2018 Filipe Coelho <falktx@falktx.com> | |||||
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<SynthesiserSound> 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<SynthesiserVoice> voices; | |||||
ReferenceCountedArray<SynthesiserSound> 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 |
@@ -1,7 +1,7 @@ | |||||
/* | /* | ||||
* Cross-platform C++ library for Carla, based on Juce v4 | * Cross-platform C++ library for Carla, based on Juce v4 | ||||
* Copyright (C) 2015 ROLI Ltd. | |||||
* Copyright (C) 2017 Filipe Coelho <falktx@falktx.com> | |||||
* Copyright (C) 2015-2016 ROLI Ltd. | |||||
* Copyright (C) 2017-2018 Filipe Coelho <falktx@falktx.com> | |||||
* | * | ||||
* This program is free software; you can redistribute it and/or | * This program is free software; you can redistribute it and/or | ||||
* modify it under the terms of the GNU General Public License as | * 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/FileOutputStream.cpp" | ||||
#include "files/TemporaryFile.cpp" | #include "files/TemporaryFile.cpp" | ||||
#include "maths/BigInteger.cpp" | |||||
#include "maths/Random.cpp" | #include "maths/Random.cpp" | ||||
#include "memory/MemoryBlock.cpp" | #include "memory/MemoryBlock.cpp" | ||||
@@ -65,6 +67,8 @@ HINSTANCE water_getCurrentModuleInstanceHandle() noexcept | |||||
#include "streams/MemoryOutputStream.cpp" | #include "streams/MemoryOutputStream.cpp" | ||||
#include "streams/OutputStream.cpp" | #include "streams/OutputStream.cpp" | ||||
#include "synthesisers/Synthesiser.cpp" | |||||
#include "text/CharacterFunctions.cpp" | #include "text/CharacterFunctions.cpp" | ||||
#include "text/Identifier.cpp" | #include "text/Identifier.cpp" | ||||
#include "text/StringArray.cpp" | #include "text/StringArray.cpp" | ||||
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* Cross-platform C++ library for Carla, based on Juce v4 | * 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 <falktx@falktx.com> | * Copyright (C) 2017-2018 Filipe Coelho <falktx@falktx.com> | ||||
* | * | ||||
* This program is free software; you can redistribute it and/or | * This program is free software; you can redistribute it and/or | ||||