// Copyright (c) 2019, Paul Ferrand // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // 1. Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "Synth.h" #include "AtomicGuard.h" #include "Config.h" #include "Debug.h" #include "MidiState.h" #include "ScopedFTZ.h" #include "StringViewHelpers.h" #include "absl/algorithm/container.h" #include #include #include #include using namespace std::literals; sfz::Synth::Synth() { for (int i = 0; i < config::numVoices; ++i) voices.push_back(std::make_unique(midiState)); voiceViewArray.reserve(config::numVoices); } void sfz::Synth::callback(absl::string_view header, const std::vector& members) { switch (hash(header)) { case hash("global"): // We shouldn't have multiple global headers in file // But apparently some instruments do not really care :) // ASSERT(!hasGlobal); globalOpcodes = members; handleGlobalOpcodes(members); hasGlobal = true; break; case hash("control"): // We shouldn't have multiple control headers in file ASSERT(!hasControl) hasControl = true; handleControlOpcodes(members); break; case hash("master"): masterOpcodes = members; numMasters++; break; case hash("group"): groupOpcodes = members; numGroups++; break; case hash("region"): buildRegion(members); break; case hash("curve"): // TODO: implement curves numCurves++; break; case hash("effect"): // TODO: implement effects break; default: std::cerr << "Unknown header: " << header << '\n'; } } void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { auto lastRegion = std::make_unique(midiState); auto parseOpcodes = [&](const auto& opcodes) { for (auto& opcode : opcodes) { const auto unknown = absl::c_find_if(unknownOpcodes, [&](absl::string_view sv) { return sv.compare(opcode.opcode) == 0; }); if (unknown != unknownOpcodes.end()) { continue; } if (!lastRegion->parseOpcode(opcode)) unknownOpcodes.insert(opcode.opcode); } }; parseOpcodes(globalOpcodes); parseOpcodes(masterOpcodes); parseOpcodes(groupOpcodes); parseOpcodes(regionOpcodes); regions.push_back(std::move(lastRegion)); } void sfz::Synth::clear() { for (auto &voice: voices) voice->reset(); for (auto& list: noteActivationLists) list.clear(); for (auto& list: ccActivationLists) list.clear(); regions.clear(); filePool.clear(); hasGlobal = false; hasControl = false; numGroups = 0; numMasters = 0; numCurves = 0; fileTicket = -1; defaultSwitch = absl::nullopt; for (auto& state : midiState.cc) state = 0; ccNames.clear(); globalOpcodes.clear(); masterOpcodes.clear(); groupOpcodes.clear(); } void sfz::Synth::handleGlobalOpcodes(const std::vector& members) { for (auto& member : members) { switch (hash(member.opcode)) { case hash("sw_default"): setValueFromOpcode(member, defaultSwitch, Default::keyRange); break; } } } void sfz::Synth::handleControlOpcodes(const std::vector& members) { for (auto& member : members) { switch (hash(member.opcode)) { case hash("Set_cc"): [[fallthrough]]; case hash("set_cc"): if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter)) setValueFromOpcode(member, midiState.cc[*member.parameter], Default::ccRange); break; case hash("Label_cc"): [[fallthrough]]; case hash("label_cc"): if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter)) ccNames.emplace_back(*member.parameter, member.value); break; case hash("Default_path"): [[fallthrough]]; case hash("default_path"): { auto stringPath = std::string(member.value.begin(), member.value.end()); auto newPath = fs::path(stringPath); if (fs::exists(newPath)) rootDirectory = newPath; break; } default: // Unsupported control opcode ASSERTFALSE; } } } void addEndpointsToVelocityCurve(sfz::Region& region) { if (region.velocityPoints.size() > 0) { absl::c_sort(region.velocityPoints, [](auto& lhs, auto& rhs) { return lhs.first < rhs.first; }); if (region.ampVeltrack > 0) { if (region.velocityPoints.back().first != sfz::Default::velocityRange.getEnd()) region.velocityPoints.push_back(std::make_pair(127, 1.0f)); if (region.velocityPoints.front().first != sfz::Default::velocityRange.getStart()) region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair(0, 0.0f)); } else { if (region.velocityPoints.front().first != sfz::Default::velocityRange.getEnd()) region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair(127, 0.0f)); if (region.velocityPoints.back().first != sfz::Default::velocityRange.getStart()) region.velocityPoints.push_back(std::make_pair(0, 1.0f)); } } } bool sfz::Synth::loadSfzFile(const fs::path& filename) { AtomicDisabler callbackDisabler { canEnterCallback }; while (inCallback) { std::this_thread::sleep_for(1ms); } clear(); auto parserReturned = sfz::Parser::loadSfzFile(filename); if (!parserReturned) return false; if (regions.empty()) return false; filePool.setRootDirectory(this->rootDirectory); auto lastRegion = regions.end() - 1; auto currentRegion = regions.begin(); while (currentRegion <= lastRegion) { auto region = currentRegion->get(); if (!region->isGenerator()) { auto fileInformation = filePool.getFileInformation(region->sample, region->offset + region->offsetRandom); if (!fileInformation) { DBG("Removing the region with sample " << region->sample); std::iter_swap(currentRegion, lastRegion); lastRegion--; continue; } region->sampleEnd = std::min(region->sampleEnd, fileInformation->end); region->loopRange.shrinkIfSmaller(fileInformation->loopBegin, fileInformation->loopEnd); region->preloadedData = fileInformation->preloadedData; region->sampleRate = fileInformation->sampleRate; } for (auto note = 0; note < 128; note++) { if (region->keyRange.containsWithEnd(note) || region->keyswitchRange.containsWithEnd(note)) noteActivationLists[note].push_back(region); } for (auto cc = 0; cc < 128; cc++) { if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc)) ccActivationLists[cc].push_back(region); } // Defaults for (int ccIndex = 1; ccIndex < 128; ccIndex++) region->registerCC(region->channelRange.getStart(), ccIndex, midiState.cc[ccIndex]); if (defaultSwitch) { region->registerNoteOn(region->channelRange.getStart(), *defaultSwitch, 127, 1.0); region->registerNoteOff(region->channelRange.getStart(), *defaultSwitch, 0, 1.0); } addEndpointsToVelocityCurve(*region); region->registerPitchWheel(region->channelRange.getStart(), 0); region->registerAftertouch(region->channelRange.getStart(), 0); region->registerTempo(2.0f); currentRegion++; } DBG("Removed " << regions.size() - std::distance(regions.begin(), lastRegion) - 1 << " out of " << regions.size() << " regions."); regions.resize(std::distance(regions.begin(), lastRegion) + 1); return parserReturned; } sfz::Voice* sfz::Synth::findFreeVoice() noexcept { auto freeVoice = absl::c_find_if(voices, [](const auto& voice) { return voice->isFree(); }); if (freeVoice != voices.end()) return freeVoice->get(); // Find voices that can be stolen DBG("No free voice, trying to steal"); voiceViewArray.clear(); for (auto& voice : voices) if (voice->canBeStolen()) voiceViewArray.push_back(voice.get()); absl::c_sort(voices, [](const auto& lhs, const auto& rhs) { return lhs->getSourcePosition() > rhs->getSourcePosition(); }); for (auto* voice : voiceViewArray) { DBG("Average voice power: " << voice->getMeanSquaredAverage()); if (voice->getMeanSquaredAverage() < config::voiceStealingThreshold) { DBG("Stealing voice..."); voice->reset(); return voice; } } DBG("Voices are overloaded, can't start a new note"); return {}; } void sfz::Synth::getNumActiveVoices() const noexcept { auto activeVoices { 0 }; for (const auto& voice : voices) { if (!voice->isFree()) activeVoices++; } } void sfz::Synth::garbageCollect() noexcept { for (auto& voice : voices) { voice->garbageCollect(); } } void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { AtomicDisabler callbackDisabler { canEnterCallback }; while (inCallback) { std::this_thread::sleep_for(1ms); } this->samplesPerBlock = samplesPerBlock; this->tempBuffer.resize(samplesPerBlock); for (auto& voice : voices) voice->setSamplesPerBlock(samplesPerBlock); } void sfz::Synth::setSampleRate(float sampleRate) noexcept { AtomicDisabler callbackDisabler { canEnterCallback }; while (inCallback) { std::this_thread::sleep_for(1ms); } this->sampleRate = sampleRate; for (auto& voice : voices) voice->setSampleRate(sampleRate); } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { ScopedFTZ ftz; buffer.fill(0.0f); if (!canEnterCallback) return; AtomicGuard callbackGuard { inCallback }; auto tempSpan = AudioSpan(tempBuffer).first(buffer.getNumFrames()); for (auto& voice : voices) { voice->renderBlock(tempSpan); buffer.add(tempSpan); } } void sfz::Synth::noteOn(int delay, int channel, int noteNumber, uint8_t velocity) noexcept { ASSERT(noteNumber < 128); ASSERT(noteNumber >= 0); midiState.noteOn(noteNumber, velocity); if (!canEnterCallback) return; AtomicGuard callbackGuard { inCallback }; auto randValue = randNoteDistribution(Random::randomGenerator); for (auto& region : noteActivationLists[noteNumber]) { 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, delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn); if (!region->isGenerator()) { voice->expectFileData(fileTicket); filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++); } } } } void sfz::Synth::noteOff(int delay, int channel, int noteNumber, uint8_t velocity [[maybe_unused]]) noexcept { ASSERT(noteNumber < 128); ASSERT(noteNumber >= 0); if (!canEnterCallback) return; AtomicGuard callbackGuard { inCallback }; // FIXME: Some keyboards (e.g. Casio PX5S) can send a real note-off velocity. In this case, do we have a // way in sfz to specify that a release trigger should NOT use the note-on velocity? // auto replacedVelocity = (velocity == 0 ? sfz::getNoteVelocity(noteNumber) : velocity); auto replacedVelocity = midiState.getNoteVelocity(noteNumber); auto randValue = randNoteDistribution(Random::randomGenerator); for (auto& voice : voices) voice->registerNoteOff(delay, channel, noteNumber, replacedVelocity); for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(channel, noteNumber, replacedVelocity, randValue)) { auto voice = findFreeVoice(); if (voice == nullptr) continue; voice->startVoice(region, delay, channel, noteNumber, replacedVelocity, Voice::TriggerType::NoteOff); if (!region->isGenerator()) { voice->expectFileData(fileTicket); filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++); } } } } void sfz::Synth::cc(int delay, int channel, int ccNumber, uint8_t ccValue) noexcept { ASSERT(ccNumber < 128); ASSERT(ccNumber >= 0); if (!canEnterCallback) return; AtomicGuard callbackGuard { inCallback }; for (auto& voice : voices) voice->registerCC(delay, channel, ccNumber, ccValue); midiState.cc[ccNumber] = ccValue; for (auto& region : ccActivationLists[ccNumber]) { if (region->registerCC(channel, ccNumber, ccValue)) { auto voice = findFreeVoice(); if (voice == nullptr) continue; voice->startVoice(region, delay, channel, ccNumber, ccValue, Voice::TriggerType::CC); if (!region->isGenerator()) { voice->expectFileData(fileTicket); filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++); } } } } int sfz::Synth::getNumRegions() const noexcept { return static_cast(regions.size()); } int sfz::Synth::getNumGroups() const noexcept { return numGroups; } int sfz::Synth::getNumMasters() const noexcept { return numMasters; } int sfz::Synth::getNumCurves() const noexcept { return numCurves; } const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept { return (size_t)idx < regions.size() ? regions[idx].get() : nullptr; } std::set sfz::Synth::getUnknownOpcodes() const noexcept { return unknownOpcodes; } size_t sfz::Synth::getNumPreloadedSamples() const noexcept { return filePool.getNumPreloadedSamples(); }