diff --git a/CMakeLists.txt b/CMakeLists.txt index ab5a2861..29686524 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,9 +72,8 @@ set(COMMON_SOURCES sources/Synth.cpp sources/FilePool.cpp sources/Region.cpp + sources/Voice.cpp sources/FloatEnvelopes.cpp - # sources/LinearEnvelope.cpp - # sources/ADSREnvelope.cpp sources/Parser.cpp ) diff --git a/sources/Region.cpp b/sources/Region.cpp index 2a97704b..e17b0171 100644 --- a/sources/Region.cpp +++ b/sources/Region.cpp @@ -546,4 +546,40 @@ void sfz::Region::registerTempo(float secondsPerQuarter) bpmSwitched = true; else bpmSwitched = false; +} + +float sfz::Region::getBasePitchVariation(int noteNumber, uint8_t velocity) noexcept +{ + auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center + pitchVariationInCents += tune; // sample tuning + pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose + pitchVariationInCents += velocity / 127 * pitchVeltrack; // track velocity + pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes + return centsFactor(pitchVariationInCents); +} +float sfz::Region::getBaseGain() noexcept +{ + float baseGaindB { volume }; + baseGaindB += gainDistribution(Random::randomGenerator); + return db2mag(baseGaindB); +} +uint32_t sfz::Region::getOffset() noexcept +{ + return offset + offsetDistribution(Random::randomGenerator); +} +uint32_t sfz::Region::getDelay() noexcept +{ + return delay + delayDistribution(Random::randomGenerator); +} + +uint32_t sfz::Region::trueSampleEnd() const noexcept +{ + return min(sampleEnd, loopRange.getEnd()); +} +bool sfz::Region::canUsePreloadedData() const noexcept +{ + if (preloadedData == nullptr) + return false; + + return trueSampleEnd() < static_cast(preloadedData->getNumFrames()); } \ No newline at end of file diff --git a/sources/Region.h b/sources/Region.h index 1977f969..8441de4d 100644 --- a/sources/Region.h +++ b/sources/Region.h @@ -2,7 +2,6 @@ #include "CCMap.h" #include "Defaults.h" #include "EGDescription.h" -#include "Helpers.h" #include "Opcode.h" #include "StereoBuffer.h" #include @@ -31,44 +30,14 @@ struct Region { void registerAftertouch(int channel, uint8_t aftertouch); void registerTempo(float secondsPerQuarter); bool isStereo() const noexcept; - float getBasePitchVariation(int noteNumber, uint8_t velocity) noexcept - { - auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center - pitchVariationInCents += tune; // sample tuning - pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose - pitchVariationInCents += velocity / 127 * pitchVeltrack; // track velocity - if (pitchRandom > 0) - pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes - return centsFactor(pitchVariationInCents); - } - float getBaseGain() - { - float baseGaindB { volume }; - baseGaindB += gainDistribution(Random::randomGenerator); - return db2mag(baseGaindB); - } - uint32_t getOffset() - { - return offset + offsetDistribution(Random::randomGenerator); - } - uint32_t getDelay() - { - return delay + delayDistribution(Random::randomGenerator); - } - - uint32_t trueSampleEnd() - { - return min(sampleEnd, loopRange.getEnd()); - } - bool canUsePreloadedData() - { - if (preloadedData == nullptr) - return false; - - return trueSampleEnd() < static_cast(preloadedData->getNumFrames()); - } - + float getBasePitchVariation(int noteNumber, uint8_t velocity) noexcept; + float getBaseGain() noexcept; + uint32_t getOffset() noexcept; + uint32_t getDelay() noexcept; + uint32_t trueSampleEnd() const noexcept; + bool canUsePreloadedData() const noexcept; bool parseOpcode(const Opcode& opcode); + // Sound source: sample playback std::string sample {}; // Sample float delay { Default::delay }; // delay diff --git a/sources/Synth.cpp b/sources/Synth.cpp index 31356133..591de443 100644 --- a/sources/Synth.cpp +++ b/sources/Synth.cpp @@ -4,6 +4,12 @@ #include #include +sfz::Synth::Synth() +{ + for (int i = 0; i < config::numVoices; ++i) + voices.push_back(std::make_unique(ccState)); +} + void sfz::Synth::callback(std::string_view header, const std::vector& members) { switch (hash(header)) { @@ -167,3 +173,115 @@ bool sfz::Synth::loadSfzFile(const std::filesystem::path& filename) regions.resize(std::distance(regions.begin(), lastRegion) + 1); return parserReturned; } + +sfz::Voice* sfz::Synth::findFreeVoice() +{ + auto freeVoice = absl::c_find_if(voices, [](const auto& voice) { return voice->isFree(); }); + if (freeVoice == voices.end()) { + DBG("Voices are overloaded, can't start a new note"); + return {}; + } + return freeVoice->get(); +} + +void sfz::Synth::getNumActiveVoices() const +{ + auto activeVoices { 0 }; + for (const auto& voice : voices) { + if (!voice->isFree()) + activeVoices++; + } +} + +void sfz::Synth::garbageCollect() +{ + for (auto& voice : voices) { + voice->garbageCollect(); + } +} + +void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) +{ + DBG("[Synth] Samples per block set to " << samplesPerBlock); + this->samplesPerBlock = samplesPerBlock; + this->tempBuffer.resize(samplesPerBlock); + for (auto& voice : voices) + voice->setSamplesPerBlock(samplesPerBlock); +} + +void sfz::Synth::setSampleRate(float sampleRate) +{ + DBG("[Synth] Sample rate set to " << sampleRate); + this->sampleRate = sampleRate; + for (auto& voice : voices) + voice->setSampleRate(sampleRate); +} + +void sfz::Synth::renderBlock(StereoSpan buffer) +{ + ScopedFTZ ftz; + buffer.fill(0.0f); + StereoSpan tempSpan { tempBuffer, buffer.size() }; + for (auto& voice : voices) { + voice->renderBlock(tempSpan); + buffer.add(tempSpan); + } +} + +void sfz::Synth::noteOn(int delay, int channel, int noteNumber, uint8_t velocity) +{ + auto randValue = randNoteDistribution(Random::randomGenerator); + + for (auto& region : regions) { + if (region->registerNoteOn(channel, noteNumber, velocity, randValue)) { + for (auto& voice : voices) { + if (voice->checkOffGroup(delay, region->group)) + noteOff(delay, voice->getTriggerChannel(), voice->getTriggerNumber(), 0); + } + + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); + } + } +} + +void sfz::Synth::noteOff(int delay, int channel, int noteNumber, uint8_t velocity) +{ + auto randValue = randNoteDistribution(Random::randomGenerator); + for (auto& voice : voices) + voice->registerNoteOff(delay, channel, noteNumber, velocity); + + for (auto& region : regions) { + if (region->registerNoteOff(channel, noteNumber, velocity, randValue)) { + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOff); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); + } + } +} + +void sfz::Synth::cc(int delay, int channel, int ccNumber, uint8_t ccValue) +{ + for (auto& voice : voices) + voice->registerCC(delay, channel, ccNumber, ccValue); + + ccState[ccNumber] = ccValue; + + for (auto& region : regions) { + if (region->registerCC(channel, ccNumber, ccValue)) { + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region.get(), delay, channel, ccNumber, ccValue, Voice::TriggerType::CC); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); + } + } +} \ No newline at end of file diff --git a/sources/Synth.h b/sources/Synth.h index e9bcd4fe..dae03f90 100644 --- a/sources/Synth.h +++ b/sources/Synth.h @@ -19,16 +19,7 @@ namespace sfz { class Synth : public Parser { public: - Synth() - { - for (int i = 0; i < config::numVoices; ++i) - voices.push_back(std::make_unique(ccState)); - } - - ~Synth() - { - - } + Synth(); bool loadSfzFile(const std::filesystem::path& file) final; int getNumRegions() const noexcept { return static_cast(regions.size()); } @@ -39,124 +30,23 @@ public: auto getUnknownOpcodes() { return unknownOpcodes; } size_t getNumPreloadedSamples() { return filePool.getNumPreloadedSamples(); } - void setSamplesPerBlock(int samplesPerBlock) - { - DBG("[Synth] Samples per block set to " << samplesPerBlock); - this->samplesPerBlock = samplesPerBlock; - this->tempBuffer.resize(samplesPerBlock); - for (auto& voice : voices) - voice->setSamplesPerBlock(samplesPerBlock); - } - - void setSampleRate(float sampleRate) - { - DBG("[Synth] Sample rate set to " << sampleRate); - this->sampleRate = sampleRate; - for (auto& voice : voices) - voice->setSampleRate(sampleRate); - } - - void renderBlock(StereoSpan buffer) - { - ScopedFTZ ftz; - buffer.fill(0.0f); - StereoSpan tempSpan { tempBuffer, buffer.size() }; - for (auto& voice : voices) { - voice->renderBlock(tempSpan); - buffer.add(tempSpan); - } - } - - void noteOn(int delay, int channel, int noteNumber, uint8_t velocity) - { - auto randValue = randNoteDistribution(Random::randomGenerator); - - for (auto& region : regions) { - if (region->registerNoteOn(channel, noteNumber, velocity, randValue)) { - for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) - noteOff(delay, voice->getTriggerChannel(), voice->getTriggerNumber(), 0); - } - - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn); - filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); - } - } - } - - void noteOff(int delay, int channel, int noteNumber, uint8_t velocity) - { - auto randValue = randNoteDistribution(Random::randomGenerator); - for (auto& voice : voices) - voice->registerNoteOff(delay, channel, noteNumber, velocity); - - for (auto& region : regions) { - if (region->registerNoteOff(channel, noteNumber, velocity, randValue)) { - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOff); - filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); - } - } - } - - void cc(int delay, int channel, int ccNumber, uint8_t ccValue) - { - for (auto& voice : voices) - voice->registerCC(delay, channel, ccNumber, ccValue); - - ccState[ccNumber] = ccValue; - - for (auto& region : regions) { - if (region->registerCC(channel, ccNumber, ccValue)) { - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region.get(), delay, channel, ccNumber, ccValue, Voice::TriggerType::CC); - filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); - } - } - } + void setSamplesPerBlock(int samplesPerBlock); + void setSampleRate(float sampleRate); + void renderBlock(StereoSpan buffer); + void noteOn(int delay, int channel, int noteNumber, uint8_t velocity); + void noteOff(int delay, int channel, int noteNumber, uint8_t velocity); + void cc(int delay, int channel, int ccNumber, uint8_t ccValue); void pitchWheel(int delay, int channel, int pitch); void aftertouch(int delay, int channel, uint8_t aftertouch); void tempo(int delay, float secondsPerQuarter); - void getNumActiveVoices() const - { - auto activeVoices { 0 }; - for (const auto& voice : voices) { - if (!voice->isFree()) - activeVoices++; - } - } - void garbageCollect() - { - for (auto& voice : voices) { - voice->garbageCollect(); - } - } + void getNumActiveVoices() const; + void garbageCollect(); protected: void callback(std::string_view header, const std::vector& members) final; private: - Voice* findFreeVoice() - { - auto freeVoice = absl::c_find_if(voices, [](const auto& voice) { return voice->isFree(); }); - if (freeVoice == voices.end()) { - DBG("Voices are overloaded, can't start a new note"); - return {}; - } - return freeVoice->get(); - } - bool hasGlobal { false }; bool hasControl { false }; int numGroups { 0 }; @@ -165,12 +55,15 @@ private: void clear(); void handleGlobalOpcodes(const std::vector& members); void handleControlOpcodes(const std::vector& members); + void buildRegion(const std::vector& regionOpcodes); + std::vector globalOpcodes; std::vector masterOpcodes; std::vector groupOpcodes; FilePool filePool; CCValueArray ccState; + Voice* findFreeVoice(); std::vector ccNames; std::optional defaultSwitch; std::set unknownOpcodes; @@ -179,7 +72,6 @@ private: std::vector> voices; std::array noteActivationLists; std::array ccActivationLists; - void buildRegion(const std::vector& regionOpcodes); StereoBuffer tempBuffer { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock }; diff --git a/sources/Voice.cpp b/sources/Voice.cpp new file mode 100644 index 00000000..3ed3cf4d --- /dev/null +++ b/sources/Voice.cpp @@ -0,0 +1,259 @@ +#include "Voice.h" +#include "SIMDHelpers.h" +#include "SfzHelpers.h" + +sfz::Voice::Voice(const CCValueArray& ccState) + : ccState(ccState) +{ +} + +void sfz::Voice::startVoice(Region* region, int delay, int channel, int number, uint8_t value, sfz::Voice::TriggerType triggerType) +{ + this->triggerType = triggerType; + triggerNumber = number; + triggerChannel = channel; + triggerValue = value; + + this->region = region; + + ASSERT(delay >= 0); + if (delay < 0) + delay = 0; + + DBG("Starting voice with " << region->sample); + + state = State::playing; + speedRatio = static_cast(region->sampleRate / this->sampleRate); + pitchRatio = region->getBasePitchVariation(number, value); + baseGain = region->getBaseGain(); + sourcePosition = region->getOffset(); + initialDelay = delay + region->getDelay(); + baseFrequency = midiNoteFrequency(number) * pitchRatio; + prepareEGEnvelope(delay, value); +} + +void sfz::Voice::prepareEGEnvelope(int delay, uint8_t velocity) +{ + auto secondsToSamples = [this](auto timeInSeconds) { + return static_cast(timeInSeconds * sampleRate); + }; + + egEnvelope.reset( + secondsToSamples(region->amplitudeEG.getAttack(ccState, velocity)), + secondsToSamples(region->amplitudeEG.getRelease(ccState, velocity)), + normalizePercents(region->amplitudeEG.getSustain(ccState, velocity)), + delay + secondsToSamples(region->amplitudeEG.getDelay(ccState, velocity)), + secondsToSamples(region->amplitudeEG.getDecay(ccState, velocity)), + secondsToSamples(region->amplitudeEG.getHold(ccState, velocity)), + normalizePercents(region->amplitudeEG.getStart(ccState, velocity))); +} + +void sfz::Voice::setFileData(std::unique_ptr> file) +{ + fileData = std::move(file); + dataReady.store(true); +} + +bool sfz::Voice::isFree() const +{ + return (region == nullptr); +} + +void sfz::Voice::registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity [[maybe_unused]]) +{ + if (region == nullptr) + return; + + if (state != State::playing) + return; + + if (triggerChannel == channel && triggerNumber == noteNumber) { + noteIsOff = true; + + if (region->loopMode == SfzLoopMode::one_shot) + return; + + if (ccState[64] < 63) + egEnvelope.startRelease(delay); + } +} + +void sfz::Voice::registerCC(int delay, int channel [[maybe_unused]], int ccNumber, uint8_t ccValue) +{ + if (ccNumber == 64 && noteIsOff && ccValue < 63) + egEnvelope.startRelease(delay); +} + +void sfz::Voice::registerPitchWheel(int delay [[maybe_unused]], int channel [[maybe_unused]], int pitch [[maybe_unused]]) +{ + +} + +void sfz::Voice::registerAftertouch(int delay [[maybe_unused]], int channel [[maybe_unused]], uint8_t aftertouch [[maybe_unused]]) +{ + +} + +void sfz::Voice::registerTempo(int delay [[maybe_unused]], float secondsPerQuarter [[maybe_unused]]) +{ + +} + +void sfz::Voice::setSampleRate(float sampleRate) +{ + this->sampleRate = sampleRate; +} + +void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) +{ + this->samplesPerBlock = samplesPerBlock; + tempBuffer1.resize(samplesPerBlock); + tempBuffer2.resize(samplesPerBlock); + indexBuffer.resize(samplesPerBlock); + tempSpan1 = absl::MakeSpan(tempBuffer1); + tempSpan2 = absl::MakeSpan(tempBuffer2); + indexSpan = absl::MakeSpan(indexBuffer); +} + +void sfz::Voice::renderBlock(StereoSpan buffer) +{ + const auto numSamples = buffer.size(); + ASSERT(static_cast(numSamples) <= samplesPerBlock); + buffer.fill(0.0f); + + if (state == State::idle || region == nullptr) + return; + + if (region->isGenerator()) + fillWithGenerator(buffer); + else + fillWithData(buffer); + + buffer.applyGain(baseGain); + + auto envelopeSpan = tempSpan1.first(numSamples); + egEnvelope.getBlock(envelopeSpan); + buffer.applyGain(envelopeSpan); + + if (!egEnvelope.isSmoothing()) + reset(); +} + +void sfz::Voice::fillWithData(StereoSpan buffer) +{ + auto source { [&]() { + if (region->canUsePreloadedData() || !dataReady) + return StereoSpan(*region->preloadedData); + else + return StereoSpan(*fileData); + }() }; + + auto indices = indexSpan.first(buffer.size()); + auto jumps = tempSpan1.first(buffer.size()); + auto leftCoeffs = tempSpan1.first(buffer.size()); + auto rightCoeffs = tempSpan2.first(buffer.size()); + + ::fill(jumps, pitchRatio * speedRatio); + + if (region->shouldLoop() && region->trueSampleEnd() <= source.size()) { + floatPosition = ::loopingSFZIndex( + jumps, + leftCoeffs, + rightCoeffs, + indices, + floatPosition, + region->trueSampleEnd() - 1, + region->loopRange.getStart()); + } else { + floatPosition = ::saturatingSFZIndex( + jumps, + leftCoeffs, + rightCoeffs, + indices, + floatPosition, + source.size() - 1); + } + + auto ind = indices.data(); + auto leftCoeff = leftCoeffs.data(); + auto rightCoeff = rightCoeffs.data(); + auto left = buffer.left().data(); + auto right = buffer.right().data(); + while (ind < indices.end()) { + *left = source.left()[*ind] * (*leftCoeff) + source.left()[*ind + 1] * (*rightCoeff); + *right = source.right()[*ind] * (*leftCoeff) + source.right()[*ind + 1] * (*rightCoeff); + left++; + right++; + ind++; + leftCoeff++; + rightCoeff++; + } + + if (!region->shouldLoop() && (floatPosition + 1.01) > source.size()) { + DBG("Releasing " << region->sample); + egEnvelope.startRelease(buffer.size()); + } +} + +void sfz::Voice::fillWithGenerator(StereoSpan buffer) +{ + if (region->sample != "*sine") + return; + + float step = baseFrequency * twoPi / sampleRate; + phase = ::linearRamp(tempSpan1, phase, step); + + ::sin(tempSpan1.first(buffer.size()), buffer.left()); + absl::c_copy(buffer.left(), buffer.right().begin()); + + sourcePosition += buffer.size(); +} + +bool sfz::Voice::checkOffGroup(int delay [[maybe_unused]], uint32_t group) noexcept +{ + if (region != nullptr && triggerType == TriggerType::NoteOn && region->offBy && *region->offBy == group) { + // TODO: release + return true; + } + + return false; +} + +int sfz::Voice::getTriggerNumber() const +{ + return triggerNumber; +} + +int sfz::Voice::getTriggerChannel() const +{ + return triggerNumber; +} + +uint8_t sfz::Voice::getTriggerValue() const +{ + return triggerNumber; +} + +sfz::Voice::TriggerType sfz::Voice::getTriggerType() const +{ + return triggerType; +} + +void sfz::Voice::reset() +{ + dataReady.store(false); + state = State::idle; + if (region != nullptr) { + DBG("Reset voice with sample " << region->sample); + } + sourcePosition = 0; + floatPosition = 0.0f; + region = nullptr; + noteIsOff = false; +} + +void sfz::Voice::garbageCollect() +{ + if (state == State::idle && region == nullptr) + fileData.reset(); +} diff --git a/sources/Voice.h b/sources/Voice.h index 07ba9b37..3ce66e38 100644 --- a/sources/Voice.h +++ b/sources/Voice.h @@ -1,268 +1,50 @@ #pragma once #include "ADSREnvelope.h" -#include "Defaults.h" #include "Globals.h" #include "Region.h" -#include "SIMDHelpers.h" -#include "SfzHelpers.h" #include "StereoBuffer.h" #include "StereoSpan.h" -#include "absl/types/span.h" +#include #include -#include namespace sfz { class Voice { public: Voice() = delete; - Voice(const CCValueArray& ccState) - : ccState(ccState) - { - } + Voice(const CCValueArray& ccState); enum class TriggerType { NoteOn, NoteOff, CC }; - void startVoice(Region* region, int delay, int channel, int number, uint8_t value, TriggerType triggerType) - { - this->triggerType = triggerType; - triggerNumber = number; - triggerChannel = channel; - triggerValue = value; - - this->region = region; - - ASSERT(delay >= 0); - if (delay < 0) - delay = 0; - - DBG("Starting voice with " << region->sample); - - state = State::playing; - speedRatio = static_cast(region->sampleRate / this->sampleRate); - pitchRatio = region->getBasePitchVariation(number, value); - baseGain = region->getBaseGain(); - sourcePosition = region->getOffset(); - initialDelay = delay + region->getDelay(); - baseFrequency = midiNoteFrequency(number) * pitchRatio; - prepareEGEnvelope(delay, value); - } - - void prepareEGEnvelope(int delay, uint8_t velocity) - { - auto secondsToSamples = [this](auto timeInSeconds) { - return static_cast(timeInSeconds * sampleRate); - }; - - egEnvelope.reset( - secondsToSamples(region->amplitudeEG.getAttack(ccState, velocity)), - secondsToSamples(region->amplitudeEG.getRelease(ccState, velocity)), - normalizePercents(region->amplitudeEG.getSustain(ccState, velocity)), - delay + secondsToSamples(region->amplitudeEG.getDelay(ccState, velocity)), - secondsToSamples(region->amplitudeEG.getDecay(ccState, velocity)), - secondsToSamples(region->amplitudeEG.getHold(ccState, velocity)), - normalizePercents(region->amplitudeEG.getStart(ccState, velocity))); - } - - void setFileData(std::unique_ptr> file) - { - fileData = std::move(file); - dataReady.store(true); - } - - bool isFree() const - { - return (region == nullptr); - } - - void registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity [[maybe_unused]]) - { - if (region == nullptr) - return; - - if (state != State::playing) - return; - - if (triggerChannel == channel && triggerNumber == noteNumber) { - noteIsOff = true; - - if (region->loopMode == SfzLoopMode::one_shot) - return; - - if (ccState[64] < 63) - egEnvelope.startRelease(delay); - } - } - - void registerCC(int delay [[maybe_unused]], int channel [[maybe_unused]], int ccNumber [[maybe_unused]], uint8_t ccValue [[maybe_unused]]) - { - if (ccNumber == 64 && noteIsOff && ccValue < 63) - egEnvelope.startRelease(delay); - } + void setSampleRate(float sampleRate); + void setSamplesPerBlock(int samplesPerBlock); + + void startVoice(Region* region, int delay, int channel, int number, uint8_t value, TriggerType triggerType); + void setFileData(std::unique_ptr> file); + void registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity); + void registerCC(int delay, int channel, int ccNumber, uint8_t ccValue); void registerPitchWheel(int delay, int channel, int pitch); void registerAftertouch(int delay, int channel, uint8_t aftertouch); void registerTempo(int delay, float secondsPerQuarter); + bool checkOffGroup(int delay [[maybe_unused]], uint32_t group) noexcept; - void setSampleRate(float sampleRate) - { - this->sampleRate = sampleRate; - } + void renderBlock(StereoSpan buffer); - void setSamplesPerBlock(int samplesPerBlock) - { - this->samplesPerBlock = samplesPerBlock; - tempBuffer1.resize(samplesPerBlock); - tempBuffer2.resize(samplesPerBlock); - indexBuffer.resize(samplesPerBlock); - tempSpan1 = absl::MakeSpan(tempBuffer1); - tempSpan2 = absl::MakeSpan(tempBuffer2); - indexSpan = absl::MakeSpan(indexBuffer); - } - - void renderBlock(StereoSpan buffer) - { - const auto numSamples = buffer.size(); - ASSERT(static_cast(numSamples) <= samplesPerBlock); - buffer.fill(0.0f); - - if (state == State::idle || region == nullptr) - return; - - if (region->isGenerator()) - fillWithGenerator(buffer); - else - fillWithData(buffer); - - // buffer.applyGain(baseGain); - - auto envelopeSpan = tempSpan1.first(numSamples); - egEnvelope.getBlock(envelopeSpan); - // buffer.applyGain(envelopeSpan); - - if (!egEnvelope.isSmoothing()) - reset(); - } - - void fillWithData(StereoSpan buffer) - { - auto source { [&]() { - if (region->canUsePreloadedData() || !dataReady) - return StereoSpan(*region->preloadedData); - else - return StereoSpan(*fileData); - }()}; - - auto indices = indexSpan.first(buffer.size()); - auto jumps = tempSpan1.first(buffer.size()); - auto leftCoeffs = tempSpan1.first(buffer.size()); - auto rightCoeffs = tempSpan2.first(buffer.size()); - - ::fill(jumps, pitchRatio * speedRatio); - if (region->shouldLoop() && region->trueSampleEnd() <= source.size()) { - floatPosition = ::loopingSFZIndex( - jumps, - leftCoeffs, - rightCoeffs, - indices, - floatPosition, - region->trueSampleEnd() - 1, - region->loopRange.getStart()); - } else { - floatPosition = ::saturatingSFZIndex( - jumps, - leftCoeffs, - rightCoeffs, - indices, - floatPosition, - source.size() - 1); - } - - auto ind = indices.data(); - auto leftCoeff = leftCoeffs.data(); - auto rightCoeff = rightCoeffs.data(); - auto left = buffer.left().data(); - auto right = buffer.right().data(); - while (ind < indices.end()) { - *left = source.left()[*ind] * (*leftCoeff) + source.left()[*ind + 1] * (*rightCoeff); - *right = source.right()[*ind] * (*leftCoeff) + source.right()[*ind + 1] * (*rightCoeff); - left++; - right++; - ind++; - leftCoeff++; - rightCoeff++; - } - - if (!region->shouldLoop() && (floatPosition + 1.01) > source.size()) { - DBG("Releasing " << region->sample); - egEnvelope.startRelease(buffer.size()); - } - } - - void fillWithGenerator(StereoSpan buffer) - { - if (region->sample != "*sine") - return; - - float step = baseFrequency * twoPi / sampleRate; - phase = ::linearRamp(tempSpan1, phase, step); - ::sin(tempSpan1.first(buffer.size()), buffer.left()); - absl::c_copy(buffer.left(), buffer.right().begin()); - - sourcePosition += buffer.size(); - } - - bool checkOffGroup(int delay [[maybe_unused]], uint32_t group) noexcept - { - if (region != nullptr && triggerType == TriggerType::NoteOn && region->offBy && *region->offBy == group) { - // TODO: release - return true; - } - - return false; - } - - int getTriggerNumber() const - { - return triggerNumber; - } - - int getTriggerChannel() const - { - return triggerNumber; - } - - uint8_t getTriggerValue() const - { - return triggerNumber; - } - - TriggerType getTriggerType() const - { - return triggerType; - } - - void reset() - { - dataReady.store(false); - state = State::idle; - if (region != nullptr) { - DBG("Reset voice with sample " << region->sample); - } - sourcePosition = 0; - floatPosition = 0.0f; - region = nullptr; - noteIsOff = false; - } - - void garbageCollect() - { - if (state == State::idle && region == nullptr) - fileData.reset(); - } + bool isFree() const; + int getTriggerNumber() const; + int getTriggerChannel() const; + uint8_t getTriggerValue() const; + TriggerType getTriggerType() const; + void reset(); + void garbageCollect(); private: + void fillWithData(StereoSpan buffer); + void fillWithGenerator(StereoSpan buffer); + void prepareEGEnvelope(int delay, uint8_t velocity); + Region* region { nullptr }; enum class State { @@ -289,7 +71,6 @@ private: uint32_t initialDelay { 0 }; std::atomic dataReady { false }; - // std::unique_ptr, std::function*)>> fileData { nullptr }; std::unique_ptr> fileData { nullptr }; Buffer tempBuffer1;