#pragma once #include "FilePool.h" #include "Parser.h" #include "Region.h" #include "SfzHelpers.h" #include "Helpers.h" #include "StereoSpan.h" #include "absl/types/span.h" #include #include #include #include #include #include #include using namespace std::literals; namespace sfz { class Synth : public Parser { public: Synth() { for (int i = 0; i < config::numVoices; ++i) voices.push_back(std::make_unique(ccState)); garbageCollectionThread = std::thread(&Synth::garbageCollection, this); } ~Synth() { threadsShouldQuit = true; garbageCollectionThread.join(); } bool loadSfzFile(const std::filesystem::path& file) final; int getNumRegions() const noexcept { return static_cast(regions.size()); } int getNumGroups() const noexcept { return numGroups; } int getNumMasters() const noexcept { return numMasters; } int getNumCurves() const noexcept { return numCurves; } const Region* getRegionView(int idx) const noexcept { return (size_t)idx < regions.size() ? regions[idx].get() : nullptr; } 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 = getUniform(); 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 = getUniform(); 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 pitchWheel(int delay, int channel, int pitch); void aftertouch(int delay, int channel, uint8_t aftertouch); void tempo(int delay, float secondsPerQuarter); 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(); } void garbageCollection() { while (!threadsShouldQuit) { auto activeVoices { 0 }; for (auto& voice : voices) { voice->garbageCollect(); if (!voice->isFree()) activeVoices++; } DBG("Active voices:" << activeVoices << " | Stray buffers: " << FilePool::getFileBuffers()); std::this_thread::sleep_for(1s); } } bool hasGlobal { false }; bool hasControl { false }; int numGroups { 0 }; int numMasters { 0 }; int numCurves { 0 }; void clear(); void handleGlobalOpcodes(const std::vector& members); void handleControlOpcodes(const std::vector& members); std::vector globalOpcodes; std::vector masterOpcodes; std::vector groupOpcodes; FilePool filePool; CCValueArray ccState; std::vector ccNames; std::optional defaultSwitch; std::set unknownOpcodes; using RegionPtrVector = std::vector; std::vector> regions; std::vector> voices; std::array noteActivationLists; std::array ccActivationLists; void buildRegion(const std::vector& regionOpcodes); StereoBuffer tempBuffer { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; std::random_device rd {}; std::mt19937 randomGenerator { rd() }; std::uniform_real_distribution randomDistribution { 0, 1 }; bool threadsShouldQuit { false }; std::thread garbageCollectionThread; float getUniform() { return randomDistribution(randomGenerator); } LEAK_DETECTOR(Synth); }; }