diff --git a/sources/ADSREnvelope.cpp b/sources/ADSREnvelope.cpp index c456b660..d1a0bea9 100644 --- a/sources/ADSREnvelope.cpp +++ b/sources/ADSREnvelope.cpp @@ -199,6 +199,11 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept } } +template +bool ADSREnvelope::isSmoothing() noexcept +{ + return currentState != State::Done; +} template void ADSREnvelope::startRelease(int releaseDelay) noexcept @@ -207,10 +212,4 @@ void ADSREnvelope::startRelease(int releaseDelay) noexcept this->releaseDelay = releaseDelay; } -template -constexpr bool ADSREnvelope::isSmoothing() noexcept -{ - return currentState != State::Done; -} - } diff --git a/sources/ADSREnvelope.h b/sources/ADSREnvelope.h index c4d965c4..e2a929cf 100644 --- a/sources/ADSREnvelope.h +++ b/sources/ADSREnvelope.h @@ -12,7 +12,7 @@ public: Type getNextValue() noexcept; void getBlock(absl::Span output) noexcept; void startRelease(int releaseDelay) noexcept; - constexpr bool isSmoothing() noexcept; + bool isSmoothing() noexcept; private: enum class State { diff --git a/sources/Buffer.h b/sources/Buffer.h index 5c6fc527..2e16579f 100644 --- a/sources/Buffer.h +++ b/sources/Buffer.h @@ -53,8 +53,7 @@ public: _alignedEnd = normalEnd + Alignment - endMisalignment; else _alignedEnd = normalEnd; - - DBG("Buffer resized (" << newSize << ") at: " << this); + return true; } diff --git a/sources/Globals.h b/sources/Globals.h index f3d54bf6..ea52b1b6 100644 --- a/sources/Globals.h +++ b/sources/Globals.h @@ -5,7 +5,7 @@ namespace sfz namespace config { - constexpr double defaultSampleRate { 48000 }; + constexpr float defaultSampleRate { 48000 }; constexpr int defaultSamplesPerBlock { 1024 }; constexpr int preloadSize { 8192 }; constexpr int numChannels { 2 }; @@ -13,9 +13,10 @@ namespace config constexpr int numLoadingThreads { 4 }; constexpr int centPerSemitone { 100 }; constexpr float virtuallyZero { 0.00005f }; - constexpr double fastReleaseDuration { 0.01 }; + constexpr float fastReleaseDuration { 0.01 }; constexpr char defineCharacter { '$' }; constexpr int oversamplingFactor { 2 }; + constexpr float A440 { 440.0 }; } // namespace config } // namespace sfz diff --git a/sources/Helpers.h b/sources/Helpers.h index cc8e2cfd..d4823059 100644 --- a/sources/Helpers.h +++ b/sources/Helpers.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include inline void trimInPlace(std::string_view& s) { @@ -74,4 +75,43 @@ inline constexpr T min(T op1, T op2, T op3, T op4) { return std::min(op1, std::m #define ASSERTFALSE #define ASSERT(expression) #define DBG(ostream) -#endif \ No newline at end of file +#endif + +template +inline constexpr Type db2pow(Type in) +{ + return std::pow(static_cast(10.0), in * static_cast(0.1)); +} + +template +inline constexpr Type pow2db(Type in) +{ + return static_cast(10.0) * std::log10(in); +} + +template +inline constexpr Type db2mag(Type in) +{ + return std::pow(static_cast(10.0), in * static_cast(0.05)); +} + +template +inline constexpr Type mag2db(Type in) +{ + return static_cast(20.0) * std::log10(in); +} + +namespace Random +{ + static inline std::random_device randomDevice; + static inline std::mt19937 randomGenerator { randomDevice() }; +} + +inline float midiNoteFrequency(const int noteNumber) +{ + return 440.0f * std::pow(2.0f, (noteNumber - 69) / 12.0f); +} + +constexpr double pi() { return std::acos(-1); } +constexpr double twoPi() { return 2*pi(); } +constexpr double piTwo() { return pi()/2; } \ No newline at end of file diff --git a/sources/Main.cpp b/sources/Main.cpp index 7f99893c..4072ea45 100644 --- a/sources/Main.cpp +++ b/sources/Main.cpp @@ -64,19 +64,19 @@ int process(jack_nframes_t numFrames, void* arg [[maybe_unused]]) switch (midi::status(event.buffer[0])) { case midi::noteOff: - DBG("[MIDI] Note " << event.buffer[1] << " OFF at time " << event.time); - synth->noteOff(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + DBG("[MIDI] Note " << +event.buffer[1] << " OFF at time " << event.time); + synth->noteOff(event.time, midi::channel(event.buffer[0]) + 1, event.buffer[1], event.buffer[2]); break; case midi::noteOn: - DBG("[MIDI] Note " << event.buffer[1] << " ON at time " << event.time); - synth->noteOn(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + DBG("[MIDI] Note " << +event.buffer[1] << " ON at time " << event.time); + synth->noteOn(event.time, midi::channel(event.buffer[0]) + 1, event.buffer[1], event.buffer[2]); break; case midi::polyphonicPressure: DBG("[MIDI] Polyphonic pressure on at time " << event.time); break; case midi::controlChange: - DBG("[MIDI] CC " << event.buffer[1] << " at time " << event.time); - synth->cc(event.time, midi::channel(event.buffer[0]), event.buffer[1], event.buffer[2]); + DBG("[MIDI] CC " << +event.buffer[1] << " at time " << event.time); + synth->cc(event.time, midi::channel(event.buffer[0]) + 1, event.buffer[1], event.buffer[2]); break; case midi::programChange: DBG("[MIDI] Program change at time " << event.time); @@ -100,7 +100,6 @@ int process(jack_nframes_t numFrames, void* arg [[maybe_unused]]) }; synth->renderBlock(output); - return 0; } diff --git a/sources/Region.cpp b/sources/Region.cpp index 0253056f..d491b71c 100644 --- a/sources/Region.cpp +++ b/sources/Region.cpp @@ -1,6 +1,8 @@ #include "Region.h" #include "Helpers.h" #include "absl/strings/str_replace.h" +#include +#include bool sfz::Region::parseOpcode(const Opcode& opcode) { @@ -14,12 +16,14 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) break; case hash("delay_random"): setValueFromOpcode(opcode, delayRandom, Default::delayRange); + delayDistribution.param( std::uniform_real_distribution::param_type(0, delayRandom) ); break; case hash("offset"): setValueFromOpcode(opcode, offset, Default::offsetRange); break; case hash("offset_random"): setValueFromOpcode(opcode, offsetRandom, Default::offsetRange); + offsetDistribution.param( std::uniform_int_distribution::param_type(0, offsetRandom) ); break; case hash("end"): setValueFromOpcode(opcode, sampleEnd, Default::sampleEndRange); @@ -250,6 +254,7 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) break; case hash("amp_random"): setValueFromOpcode(opcode, ampRandom, Default::ampRandomRange); + gainDistribution.param(std::uniform_real_distribution::param_type(-ampRandom, ampRandom)); break; case hash("amp_velcurve_"): if (opcode.parameter && Default::ccRange.containsWithEnd(*opcode.parameter)) { diff --git a/sources/Region.h b/sources/Region.h index 336cbe34..6f1a4c9f 100644 --- a/sources/Region.h +++ b/sources/Region.h @@ -1,4 +1,6 @@ #pragma once +#include "Helpers.h" +#include #include #include #include @@ -8,6 +10,7 @@ #include "Defaults.h" #include "CCMap.h" #include +#include namespace sfz { @@ -31,6 +34,26 @@ struct Region void registerAftertouch(int channel, uint8_t aftertouch); void registerTempo(float secondsPerQuarter); bool isStereo() const noexcept; + 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 parseOpcode(const Opcode& opcode); // Sound source: sample playback std::string sample {}; // Sample @@ -126,6 +149,10 @@ private: int activeNotesInRange { -1 }; int sequenceCounter { 0 }; + + std::uniform_real_distribution gainDistribution { -sfz::Default::ampRandom, sfz::Default::ampRandom }; + std::uniform_real_distribution delayDistribution { 0, sfz::Default::delayRandom }; + std::uniform_int_distribution offsetDistribution { 0, sfz::Default::offsetRandom }; }; } // namespace sfz \ No newline at end of file diff --git a/sources/StereoSpan.h b/sources/StereoSpan.h index c1485c47..40197339 100644 --- a/sources/StereoSpan.h +++ b/sources/StereoSpan.h @@ -41,6 +41,13 @@ public: { } + StereoSpan(StereoBuffer& buffer) + : numFrames(buffer.getNumFrames()) + , leftBuffer(buffer.getSpan(Channel::left)) + , rightBuffer(buffer.getSpan(Channel::right)) + { + } + StereoSpan(StereoBuffer& buffer, size_t numFrames) : numFrames(numFrames) , leftBuffer(buffer.getSpan(Channel::left).first(numFrames)) @@ -55,7 +62,7 @@ public: { } - StereoSpan(StereoSpan& span) + StereoSpan(const StereoSpan& span) : numFrames(span.size()) , leftBuffer(span.left()) , rightBuffer(span.right()) @@ -75,6 +82,12 @@ public: ::fill(rightBuffer, value); } + void applyGain(absl::Span gain) noexcept + { + ::applyGain(gain, leftBuffer); + ::applyGain(gain, rightBuffer); + } + void readInterleaved(absl::Span input) noexcept { ASSERT(input.size() <= static_cast(numChannels * numFrames)); @@ -93,32 +106,32 @@ public: ::add(buffer.right(), rightBuffer); } - absl::Span left() + absl::Span left() const { return leftBuffer; } - absl::Span right() + absl::Span right() const { return rightBuffer; } - StereoSpan first(size_t length) + StereoSpan first(size_t length) const { return { leftBuffer.first(length), rightBuffer.first(length) }; } - StereoSpan last(size_t length) + StereoSpan last(size_t length) const { return { leftBuffer.last(length), rightBuffer.last(length) }; } - StereoSpan subspan(size_t pos, size_t length = absl::Span::npos) + StereoSpan subspan(size_t pos, size_t length = absl::Span::npos) const { return { leftBuffer.subspan(pos, length), rightBuffer.subspan(pos, length) }; } - size_t size() + size_t size() const { return numFrames; } diff --git a/sources/Synth.h b/sources/Synth.h index 3a816b01..774ad652 100644 --- a/sources/Synth.h +++ b/sources/Synth.h @@ -18,7 +18,7 @@ public: Synth() { for (int i = 0; i < config::numVoices; ++i) - voices.push_back(std::make_unique()); + voices.push_back(std::make_unique(ccState)); } bool loadSfzFile(const std::filesystem::path& file) final; int getNumRegions() const noexcept { return static_cast(regions.size()); } @@ -34,12 +34,16 @@ public: DBG("[Synth] Samples per block set to " << samplesPerBlock); this->samplesPerBlock = samplesPerBlock; this->tempBuffer.resize(samplesPerBlock); + for (auto & voice: voices) + voice->setSamplesPerBlock(samplesPerBlock); } - void setSampleRate(double sampleRate) + 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) @@ -59,7 +63,6 @@ public: for (auto& region : regions) { if (region->registerNoteOn(channel, noteNumber, velocity, randValue)) { - for (auto& voice: voices) { if (voice->checkOffGroup(delay, region->group)) @@ -70,7 +73,8 @@ public: if (voice == nullptr) continue; - voice->startVoice(region.get(), channel, noteNumber, velocity, Voice::TriggerType::NoteOn); + voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); } } } @@ -87,7 +91,8 @@ public: if (voice == nullptr) continue; - voice->startVoice(region.get(), channel, noteNumber, velocity, Voice::TriggerType::NoteOff); + voice->startVoice(region.get(), delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOff); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); } } } @@ -96,13 +101,16 @@ public: 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(), channel, ccNumber, ccValue, Voice::TriggerType::NoteOff); + voice->startVoice(region.get(), delay, channel, ccNumber, ccValue, Voice::TriggerType::CC); + filePool.enqueueLoading(voice, region->sample, region->trueSampleEnd()); } } } diff --git a/sources/Voice.h b/sources/Voice.h index 6dce2e9c..f9908b0a 100644 --- a/sources/Voice.h +++ b/sources/Voice.h @@ -1,8 +1,12 @@ #pragma once +#include "ADSREnvelope.h" #include "StereoBuffer.h" #include "StereoSpan.h" #include "Globals.h" #include "Region.h" +#include "absl/types/span.h" +#include "SIMDHelpers.h" +#include "SfzHelpers.h" #include #include @@ -11,20 +15,58 @@ namespace sfz class Voice { public: - enum class TriggerType { NoteOn, NoteOff, CC }; - void startVoice(Region* region, int channel, int number, uint8_t value, TriggerType triggerType) - { + Voice() = delete; + Voice(const CCValueArray &ccState) + : ccState(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); + baseGain = region->getBaseGain(); + sourcePosition = region->getOffset(); + initialDelay = delay + region->getDelay(); + baseFrequency = midiNoteFrequency(number); + 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); + fileData.reset(file.release()); dataReady.store(true); } @@ -33,27 +75,86 @@ public: return (region == nullptr); } - void registerNoteOff(int delay [[maybe_unused]], int channel [[maybe_unused]], int noteNumber [[maybe_unused]], uint8_t velocity [[maybe_unused]]) + void registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity[[maybe_unused]]) { - + if (state == State::playing && triggerChannel == channel && triggerNumber == noteNumber) + egEnvelope.startRelease(delay); } - void registerCC(int delay [[maybe_unused]], int channel [[maybe_unused]], int ccNumber [[maybe_unused]], uint8_t ccValue [[maybe_unused]]) + + void registerCC(int delay[[maybe_unused]], int channel[[maybe_unused]], int ccNumber[[maybe_unused]], uint8_t ccValue[[maybe_unused]]) { - } + void registerPitchWheel(int delay, int channel, int pitch); void registerAftertouch(int delay, int channel, uint8_t aftertouch); void registerTempo(int delay, float secondsPerQuarter); - void prepareToPlay(int samplesPerBlock, double sampleRate); - void renderBlock(StereoSpan buffer) + void setSampleRate(float sampleRate) { - buffer.fill(0.0f); + this->sampleRate = sampleRate; } - bool checkOffGroup(int delay [[maybe_unused]], uint32_t group) noexcept + void setSamplesPerBlock(int samplesPerBlock) { - if (region != nullptr && triggerType == TriggerType::NoteOn && region->offBy && *region->offBy == group ) + this->samplesPerBlock = samplesPerBlock; + tempBuffer1.resize(samplesPerBlock); + tempBuffer2.resize(samplesPerBlock); + tempSpan1 = absl::MakeSpan(tempBuffer1); + tempSpan2 = absl::MakeSpan(tempBuffer2); + } + + 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); + + egEnvelope.getBlock(tempSpan1.first(numSamples)); + buffer.applyGain(tempSpan1); + + if (!egEnvelope.isSmoothing()) + reset(); + } + + void fillWithData(StereoSpan buffer) + { + StereoSpan source([&]() -> StereoBuffer & { + if (dataReady) + return *fileData; + else + return *region->preloadedData; + }()); + + const auto actualBlockSize = min(buffer.size(), source.size() - sourcePosition); + buffer.add(source.subspan(sourcePosition, actualBlockSize)); + sourcePosition += actualBlockSize; + if (sourcePosition == source.size()) + egEnvelope.startRelease(buffer.size()); + } + + void fillWithGenerator(StereoSpan buffer) + { + if (region->sample != "*sine") + return; + + float step = baseFrequency * twoPi() / sampleRate; + ::linearRamp(tempSpan1.first(buffer.size()), sourcePosition * step, step); + ::sin(tempSpan1, 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; @@ -82,24 +183,54 @@ public: return triggerType; } - void reset() { + if (region != nullptr) + { + DBG("Reset voice with sample " << region->sample); + } + region = nullptr; + state = State::idle; dataReady.store(false); } + private: - Region* region; + Region *region; + + enum class State + { + idle, + playing, + release + }; + State state; TriggerType triggerType; int triggerNumber; int triggerChannel; uint8_t triggerValue; + float speedRatio{1.0}; + float pitchRatio{1.0}; + float baseGain{1.0}; + float baseFrequency{440.0}; + + uint32_t sourcePosition; + uint32_t initialDelay; + std::atomic dataReady; std::unique_ptr> fileData; - int samplesPerBlock { config::defaultSamplesPerBlock }; - double sampleRate { config::defaultSampleRate }; + Buffer tempBuffer1; + Buffer tempBuffer2; + absl::Span tempSpan1{absl::MakeSpan(tempBuffer1)}; + absl::Span tempSpan2{absl::MakeSpan(tempBuffer2)}; + + int samplesPerBlock{config::defaultSamplesPerBlock}; + double sampleRate{config::defaultSampleRate}; + + const CCValueArray &ccState; + ADSREnvelope egEnvelope; }; -} \ No newline at end of file +} // namespace sfz \ No newline at end of file