1217 lines
40 KiB
C++
1217 lines
40 KiB
C++
// SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
|
// license. You should have receive a LICENSE.md file along with the code.
|
|
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
|
|
|
#include "Synth.h"
|
|
#include "Config.h"
|
|
#include "Debug.h"
|
|
#include "Macros.h"
|
|
#include "MidiState.h"
|
|
#include "ScopedFTZ.h"
|
|
#include "StringViewHelpers.h"
|
|
#include "pugixml.hpp"
|
|
#include "absl/algorithm/container.h"
|
|
#include "absl/memory/memory.h"
|
|
#include "absl/strings/str_replace.h"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <utility>
|
|
|
|
sfz::Synth::Synth()
|
|
: Synth(config::numVoices)
|
|
{
|
|
}
|
|
|
|
sfz::Synth::Synth(int numVoices)
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
parser.setListener(this);
|
|
effectFactory.registerStandardEffectTypes();
|
|
effectBuses.reserve(5); // sufficient room for main and fx1-4
|
|
resetVoices(numVoices);
|
|
}
|
|
|
|
sfz::Synth::~Synth()
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
for (auto& voice : voices)
|
|
voice->reset();
|
|
|
|
resources.filePool.emptyFileLoadingQueues();
|
|
}
|
|
|
|
void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<Opcode>& members)
|
|
{
|
|
switch (hash(header)) {
|
|
case hash("global"):
|
|
globalOpcodes = members;
|
|
groupOpcodes.clear();
|
|
masterOpcodes.clear();
|
|
handleGlobalOpcodes(members);
|
|
break;
|
|
case hash("control"):
|
|
defaultPath = ""; // Always reset on a new control header
|
|
handleControlOpcodes(members);
|
|
break;
|
|
case hash("master"):
|
|
masterOpcodes = members;
|
|
groupOpcodes.clear();
|
|
numMasters++;
|
|
break;
|
|
case hash("group"):
|
|
groupOpcodes = members;
|
|
handleGroupOpcodes(members, masterOpcodes);
|
|
numGroups++;
|
|
break;
|
|
case hash("region"):
|
|
buildRegion(members);
|
|
break;
|
|
case hash("curve"):
|
|
resources.curves.addCurveFromHeader(members);
|
|
break;
|
|
case hash("effect"):
|
|
handleEffectOpcodes(members);
|
|
break;
|
|
default:
|
|
std::cerr << "Unknown header: " << header << '\n';
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::onParseError(const SourceRange& range, const std::string& message)
|
|
{
|
|
const auto relativePath = range.start.filePath->lexically_relative(parser.originalDirectory());
|
|
std::cerr << "Parse error in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n';
|
|
}
|
|
|
|
void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& message)
|
|
{
|
|
const auto relativePath = range.start.filePath->lexically_relative(parser.originalDirectory());
|
|
std::cerr << "Parse warning in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n';
|
|
}
|
|
|
|
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
|
{
|
|
auto lastRegion = absl::make_unique<Region>(resources.midiState, defaultPath);
|
|
|
|
auto parseOpcodes = [&](const std::vector<Opcode>& 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.emplace_back(opcode.opcode);
|
|
}
|
|
};
|
|
|
|
parseOpcodes(globalOpcodes);
|
|
parseOpcodes(masterOpcodes);
|
|
parseOpcodes(groupOpcodes);
|
|
parseOpcodes(regionOpcodes);
|
|
|
|
if (octaveOffset != 0 || noteOffset != 0)
|
|
lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset);
|
|
|
|
regions.push_back(std::move(lastRegion));
|
|
}
|
|
|
|
void sfz::Synth::clear()
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
for (auto& voice : voices)
|
|
voice->reset();
|
|
for (auto& list : noteActivationLists)
|
|
list.clear();
|
|
for (auto& list : ccActivationLists)
|
|
list.clear();
|
|
|
|
regions.clear();
|
|
effectBuses.clear();
|
|
effectBuses.emplace_back(new EffectBus);
|
|
effectBuses[0]->setGainToMain(1.0);
|
|
effectBuses[0]->setSamplesPerBlock(samplesPerBlock);
|
|
effectBuses[0]->setSampleRate(sampleRate);
|
|
effectBuses[0]->clearInputs(samplesPerBlock);
|
|
resources.clear();
|
|
numGroups = 0;
|
|
numMasters = 0;
|
|
defaultSwitch = absl::nullopt;
|
|
defaultPath = "";
|
|
resources.midiState.reset();
|
|
ccLabels.clear();
|
|
keyLabels.clear();
|
|
keyswitchLabels.clear();
|
|
globalOpcodes.clear();
|
|
masterOpcodes.clear();
|
|
groupOpcodes.clear();
|
|
unknownOpcodes.clear();
|
|
groupMaxPolyphony.clear();
|
|
groupMaxPolyphony.push_back(config::maxVoices);
|
|
modificationTime = fs::file_time_type::min();
|
|
}
|
|
|
|
void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
|
|
{
|
|
for (auto& rawMember : members) {
|
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
|
|
|
switch (member.lettersOnlyHash) {
|
|
case hash("sw_default"):
|
|
setValueFromOpcode(member, defaultSwitch, Default::keyRange);
|
|
break;
|
|
case hash("volume"):
|
|
// FIXME : Probably best not to mess with this and let the host control the volume
|
|
// setValueFromOpcode(member, volume, Default::volumeRange);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers)
|
|
{
|
|
absl::optional<unsigned> groupIdx;
|
|
unsigned maxPolyphony { config::maxVoices };
|
|
|
|
const auto parseOpcode = [&](const Opcode& rawMember) {
|
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup);
|
|
|
|
switch (member.lettersOnlyHash) {
|
|
case hash("group"):
|
|
setValueFromOpcode(member, groupIdx, Default::groupRange);
|
|
break;
|
|
case hash("polyphony"):
|
|
setValueFromOpcode(member, maxPolyphony, Range<unsigned>(0, config::maxVoices));
|
|
break;
|
|
}
|
|
};
|
|
|
|
for (auto& member : masterMembers)
|
|
parseOpcode(member);
|
|
|
|
for (auto& member : members)
|
|
parseOpcode(member);
|
|
|
|
if (groupIdx)
|
|
setGroupPolyphony(*groupIdx, maxPolyphony);
|
|
}
|
|
|
|
void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
|
{
|
|
for (auto& rawMember : members) {
|
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeControl);
|
|
|
|
switch (member.lettersOnlyHash) {
|
|
case hash("set_cc&"):
|
|
if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) {
|
|
const auto ccValue = readOpcode(member.value, Default::midi7Range);
|
|
if (ccValue)
|
|
resources.midiState.ccEvent(0, member.parameters.back(), normalizeCC(*ccValue));
|
|
}
|
|
break;
|
|
case hash("set_hdcc&"):
|
|
if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) {
|
|
const auto ccValue = readOpcode(member.value, Default::normalizedRange);
|
|
if (ccValue)
|
|
resources.midiState.ccEvent(0, member.parameters.back(), *ccValue);
|
|
}
|
|
break;
|
|
case hash("label_cc&"):
|
|
if (Default::ccNumberRange.containsWithEnd(member.parameters.back()))
|
|
ccLabels.emplace_back(member.parameters.back(), std::string(member.value));
|
|
break;
|
|
case hash("label_key&"):
|
|
if (Default::keyRange.containsWithEnd(member.parameters.back()))
|
|
keyLabels.emplace_back(member.parameters.back(), std::string(member.value));
|
|
break;
|
|
case hash("default_path"):
|
|
defaultPath = absl::StrReplaceAll(trim(member.value), { { "\\", "/" } });
|
|
DBG("Changing default sample path to " << defaultPath);
|
|
break;
|
|
case hash("note_offset"):
|
|
setValueFromOpcode(member, noteOffset, Default::noteOffsetRange);
|
|
break;
|
|
case hash("octave_offset"):
|
|
setValueFromOpcode(member, octaveOffset, Default::octaveOffsetRange);
|
|
break;
|
|
default:
|
|
// Unsupported control opcode
|
|
DBG("Unsupported control opcode: " << member.opcode);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::handleEffectOpcodes(const std::vector<Opcode>& rawMembers)
|
|
{
|
|
absl::string_view busName = "main";
|
|
|
|
auto getOrCreateBus = [this](unsigned index) -> EffectBus& {
|
|
if (index + 1 > effectBuses.size())
|
|
effectBuses.resize(index + 1);
|
|
EffectBusPtr& bus = effectBuses[index];
|
|
if (!bus) {
|
|
bus.reset(new EffectBus);
|
|
bus->setSampleRate(sampleRate);
|
|
bus->setSamplesPerBlock(samplesPerBlock);
|
|
bus->clearInputs(samplesPerBlock);
|
|
}
|
|
return *bus;
|
|
};
|
|
|
|
std::vector<Opcode> members;
|
|
members.reserve(rawMembers.size());
|
|
for (const Opcode& opcode : rawMembers)
|
|
members.push_back(opcode.cleanUp(kOpcodeScopeEffect));
|
|
|
|
for (const Opcode& opcode : members) {
|
|
switch (opcode.lettersOnlyHash) {
|
|
case hash("bus"):
|
|
busName = opcode.value;
|
|
break;
|
|
|
|
// note(jpc): gain opcodes are linear volumes in % units
|
|
|
|
case hash("directtomain"):
|
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
|
getOrCreateBus(0).setGainToMain(*valueOpt / 100);
|
|
break;
|
|
|
|
case hash("fx&tomain"): // fx&tomain
|
|
if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses)
|
|
break;
|
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
|
getOrCreateBus(opcode.parameters.front()).setGainToMain(*valueOpt / 100);
|
|
break;
|
|
|
|
case hash("fx&tomix"): // fx&tomix
|
|
if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses)
|
|
break;
|
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
|
getOrCreateBus(opcode.parameters.front()).setGainToMix(*valueOpt / 100);
|
|
break;
|
|
}
|
|
}
|
|
|
|
unsigned busIndex;
|
|
if (busName.empty() || busName == "main")
|
|
busIndex = 0;
|
|
else if (busName.size() > 2 && busName.substr(0, 2) == "fx" && absl::SimpleAtoi(busName.substr(2), &busIndex) && busIndex >= 1 && busIndex <= config::maxEffectBuses) {
|
|
// an effect bus fxN, with N usually in [1,4]
|
|
} else {
|
|
DBG("Unsupported effect bus: " << busName);
|
|
return;
|
|
}
|
|
|
|
// create the effect and add it
|
|
EffectBus& bus = getOrCreateBus(busIndex);
|
|
auto fx = effectFactory.makeEffect(members);
|
|
fx->setSampleRate(sampleRate);
|
|
fx->setSamplesPerBlock(samplesPerBlock);
|
|
bus.addEffect(std::move(fx));
|
|
}
|
|
|
|
void addEndpointsToVelocityCurve(sfz::Region& region)
|
|
{
|
|
if (region.velocityPoints.size() > 0) {
|
|
const auto velocityStart = sfz::Default::velocityRange.getStart();
|
|
const auto velocityEnd = sfz::Default::velocityRange.getEnd();
|
|
absl::c_sort(region.velocityPoints, [](const std::pair<float, float>& lhs, const std::pair<float, float>& rhs) { return lhs.first < rhs.first; });
|
|
if (region.ampVeltrack > 0) {
|
|
if (region.velocityPoints.front().first != velocityStart)
|
|
region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair(velocityStart, velocityStart));
|
|
if (region.velocityPoints.back().first != velocityEnd)
|
|
region.velocityPoints.push_back(std::make_pair(velocityEnd, velocityEnd));
|
|
} else {
|
|
if (region.velocityPoints.front().first != velocityEnd)
|
|
region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair(velocityEnd, velocityStart));
|
|
if (region.velocityPoints.back().first != velocityStart)
|
|
region.velocityPoints.push_back(std::make_pair(velocityStart, velocityEnd));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool sfz::Synth::loadSfzFile(const fs::path& file)
|
|
{
|
|
clear();
|
|
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
parser.parseFile(file);
|
|
if (parser.getErrorCount() > 0)
|
|
return false;
|
|
|
|
if (regions.empty())
|
|
return false;
|
|
|
|
finalizeSfzLoad();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool sfz::Synth::loadSfzString(const fs::path& path, absl::string_view text)
|
|
{
|
|
clear();
|
|
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
parser.parseString(path, text);
|
|
if (parser.getErrorCount() > 0)
|
|
return false;
|
|
|
|
if (regions.empty())
|
|
return false;
|
|
|
|
finalizeSfzLoad();
|
|
|
|
return true;
|
|
}
|
|
|
|
void sfz::Synth::finalizeSfzLoad()
|
|
{
|
|
resources.filePool.setRootDirectory(parser.originalDirectory());
|
|
|
|
auto currentRegion = regions.begin();
|
|
auto lastRegion = regions.rbegin();
|
|
auto removeCurrentRegion = [¤tRegion, &lastRegion]() {
|
|
if (currentRegion->get() == nullptr)
|
|
return;
|
|
|
|
DBG("Removing the region with sample " << currentRegion->get()->sampleId);
|
|
std::iter_swap(currentRegion, lastRegion);
|
|
++lastRegion;
|
|
};
|
|
|
|
size_t maxFilters { 0 };
|
|
size_t maxEQs { 0 };
|
|
|
|
while (currentRegion < lastRegion.base()) {
|
|
auto region = currentRegion->get();
|
|
|
|
if (!region->oscillator && !region->isGenerator()) {
|
|
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
|
removeCurrentRegion();
|
|
continue;
|
|
}
|
|
|
|
const auto fileInformation = resources.filePool.getFileInformation(region->sampleId);
|
|
if (!fileInformation) {
|
|
removeCurrentRegion();
|
|
continue;
|
|
}
|
|
|
|
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end);
|
|
|
|
if (fileInformation->loopBegin != Default::loopRange.getStart() && fileInformation->loopEnd != Default::loopRange.getEnd()) {
|
|
if (region->loopRange.getStart() == Default::loopRange.getStart())
|
|
region->loopRange.setStart(fileInformation->loopBegin);
|
|
|
|
if (region->loopRange.getEnd() == Default::loopRange.getEnd())
|
|
region->loopRange.setEnd(fileInformation->loopEnd);
|
|
|
|
if (!region->loopMode)
|
|
region->loopMode = SfzLoopMode::loop_continuous;
|
|
}
|
|
|
|
if (region->loopRange.getEnd() == Default::loopRange.getEnd())
|
|
region->loopRange.setEnd(region->sampleEnd);
|
|
|
|
if (fileInformation->numChannels == 2)
|
|
region->hasStereoSample = true;
|
|
|
|
// TODO: adjust with LFO targets
|
|
const auto maxOffset = [region]() {
|
|
uint64_t sumOffsetCC = region->offset + region->offsetRandom;
|
|
for (const auto& offsets : region->offsetCC)
|
|
sumOffsetCC += offsets.data;
|
|
return Default::offsetCCRange.clamp(sumOffsetCC);
|
|
}();
|
|
|
|
if (!resources.filePool.preloadFile(region->sampleId, maxOffset))
|
|
removeCurrentRegion();
|
|
} else if (region->oscillator && !region->isGenerator()) {
|
|
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
|
removeCurrentRegion();
|
|
continue;
|
|
}
|
|
|
|
if (!resources.wavePool.createFileWave(resources.filePool, std::string(region->sampleId.filename()))) {
|
|
removeCurrentRegion();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (region->keyswitchLabel && region->keyswitch)
|
|
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
|
|
|
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
|
while (groupMaxPolyphony.size() <= region->group)
|
|
groupMaxPolyphony.push_back(config::maxVoices);
|
|
|
|
for (auto note = 0; note < 128; note++) {
|
|
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note)))
|
|
noteActivationLists[note].push_back(region);
|
|
}
|
|
|
|
for (int cc = 0; cc < config::numCCs; cc++) {
|
|
if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc))
|
|
ccActivationLists[cc].push_back(region);
|
|
}
|
|
|
|
// Defaults
|
|
for (int cc = 0; cc < config::numCCs; cc++) {
|
|
region->registerCC(cc, resources.midiState.getCCValue(cc));
|
|
}
|
|
|
|
if (defaultSwitch) {
|
|
region->registerNoteOn(*defaultSwitch, 1.0f, 1.0f);
|
|
region->registerNoteOff(*defaultSwitch, 0.0f, 1.0f);
|
|
}
|
|
|
|
// Set the default frequencies on equalizers if needed
|
|
if (region->equalizers.size() > 0
|
|
&& region->equalizers[0].frequency == Default::eqFrequencyUnset) {
|
|
region->equalizers[0].frequency = Default::eqFrequency1;
|
|
if (region->equalizers.size() > 1
|
|
&& region->equalizers[1].frequency == Default::eqFrequencyUnset) {
|
|
region->equalizers[1].frequency = Default::eqFrequency2;
|
|
if (region->equalizers.size() > 2
|
|
&& region->equalizers[2].frequency == Default::eqFrequencyUnset) {
|
|
region->equalizers[2].frequency = Default::eqFrequency3;
|
|
}
|
|
}
|
|
}
|
|
|
|
addEndpointsToVelocityCurve(*region);
|
|
region->registerPitchWheel(0);
|
|
region->registerAftertouch(0);
|
|
region->registerTempo(2.0f);
|
|
maxFilters = max(maxFilters, region->filters.size());
|
|
maxEQs = max(maxEQs, region->equalizers.size());
|
|
|
|
++currentRegion;
|
|
}
|
|
const auto remainingRegions = std::distance(regions.begin(), lastRegion.base());
|
|
DBG("Removing " << (regions.size() - remainingRegions) << " out of " << regions.size() << " regions");
|
|
regions.resize(remainingRegions);
|
|
modificationTime = checkModificationTime();
|
|
|
|
for (auto& voice : voices) {
|
|
voice->setMaxFiltersPerVoice(maxFilters);
|
|
voice->setMaxEQsPerVoice(maxEQs);
|
|
}
|
|
}
|
|
|
|
sfz::Voice* sfz::Synth::findFreeVoice() noexcept
|
|
{
|
|
auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr<Voice>& voice) {
|
|
return voice->isFree();
|
|
});
|
|
if (freeVoice != voices.end())
|
|
return freeVoice->get();
|
|
|
|
// Start of the voice stealing algorithm
|
|
absl::c_sort(voiceViewArray, voiceOrdering);
|
|
|
|
const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) {
|
|
return sum + v->getAverageEnvelope();
|
|
});
|
|
const auto envThreshold = sumEnvelope
|
|
/ static_cast<float>(voiceViewArray.size()) * config::stealingEnvelopeCoeff;
|
|
const auto ageThreshold = voiceViewArray.front()->getAge() * config::stealingAgeCoeff;
|
|
|
|
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
|
|
const auto killVoice = [&] (Voice* v) {
|
|
renderVoiceToOutputs(*v, *tempSpan);
|
|
v->reset();
|
|
};
|
|
|
|
Voice* returnedVoice = voiceViewArray.front();
|
|
unsigned idx = 0;
|
|
while (idx < voiceViewArray.size()) {
|
|
const auto refIdx = idx;
|
|
const auto ref = voiceViewArray[idx];
|
|
idx++;
|
|
|
|
if (ref->getAge() < ageThreshold) {
|
|
unsigned killIdx = 1;
|
|
while (killIdx < voiceViewArray.size()
|
|
&& sisterVoices(returnedVoice, voiceViewArray[killIdx])) {
|
|
killVoice(voiceViewArray[killIdx]);
|
|
killIdx++;
|
|
}
|
|
// std::cout << "Went too far, picking the oldest voice and killing "
|
|
// << killIdx << " voices" << '\n';
|
|
killVoice(returnedVoice);
|
|
break;
|
|
}
|
|
|
|
float maxEnvelope = ref->getAverageEnvelope();
|
|
while (idx < voiceViewArray.size()
|
|
&& sisterVoices(ref, voiceViewArray[idx])) {
|
|
maxEnvelope = max(maxEnvelope, voiceViewArray[idx]->getAverageEnvelope());
|
|
idx++;
|
|
}
|
|
|
|
if (maxEnvelope < envThreshold) {
|
|
returnedVoice = ref;
|
|
// std::cout << "Killing " << idx - refIdx << " voices" << '\n';
|
|
for (unsigned j = refIdx; j < idx; j++) {
|
|
killVoice(voiceViewArray[j]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
assert(returnedVoice->isFree());
|
|
return returnedVoice;
|
|
}
|
|
|
|
int sfz::Synth::getNumActiveVoices() const noexcept
|
|
{
|
|
auto activeVoices = 0;
|
|
for (const auto& voice : voices) {
|
|
if (!voice->isFree())
|
|
activeVoices++;
|
|
}
|
|
return activeVoices;
|
|
}
|
|
|
|
void sfz::Synth::garbageCollect() noexcept
|
|
{
|
|
}
|
|
|
|
void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
|
{
|
|
ASSERT(samplesPerBlock < config::maxBlockSize);
|
|
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
this->samplesPerBlock = samplesPerBlock;
|
|
for (auto& voice : voices)
|
|
voice->setSamplesPerBlock(samplesPerBlock);
|
|
|
|
resources.setSamplesPerBlock(samplesPerBlock);
|
|
|
|
for (auto& bus : effectBuses) {
|
|
if (bus)
|
|
bus->setSamplesPerBlock(samplesPerBlock);
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
this->sampleRate = sampleRate;
|
|
for (auto& voice : voices)
|
|
voice->setSampleRate(sampleRate);
|
|
|
|
resources.setSampleRate(sampleRate);
|
|
|
|
for (auto& bus : effectBuses) {
|
|
if (bus)
|
|
bus->setSampleRate(sampleRate);
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept
|
|
{
|
|
const Region* region = voice.getRegion();
|
|
voice.renderBlock(tempSpan);
|
|
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
|
if (auto& bus = effectBuses[i]) {
|
|
float addGain = region->getGainToEffectBus(i);
|
|
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|
{
|
|
ScopedFTZ ftz;
|
|
CallbackBreakdown callbackBreakdown;
|
|
|
|
{ // Silence buffer
|
|
ScopedTiming logger { callbackBreakdown.renderMethod };
|
|
buffer.fill(0.0f);
|
|
}
|
|
|
|
if (freeWheeling)
|
|
resources.filePool.waitForBackgroundLoading();
|
|
|
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
size_t numFrames = buffer.getNumFrames();
|
|
auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
|
auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
|
auto rampSpan = resources.bufferPool.getBuffer(numFrames);
|
|
if (!tempSpan || !tempMixSpan || !rampSpan) {
|
|
DBG("[sfizz] Could not get a temporary buffer; exiting callback... ");
|
|
return;
|
|
}
|
|
|
|
int numActiveVoices { 0 };
|
|
{ // Main render block
|
|
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
|
|
tempSpan->fill(0.0f);
|
|
tempMixSpan->fill(0.0f);
|
|
resources.filePool.cleanupPromises();
|
|
|
|
// Ramp out whatever is in the buffer at this point; should only be killed voice data
|
|
linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames));
|
|
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
|
if (auto& bus = effectBuses[i]) {
|
|
bus->applyGain(rampSpan->data(), numFrames);
|
|
}
|
|
}
|
|
|
|
for (auto& voice : voices) {
|
|
if (voice->isFree())
|
|
continue;
|
|
|
|
numActiveVoices++;
|
|
renderVoiceToOutputs(*voice, *tempSpan);
|
|
callbackBreakdown.data += voice->getLastDataDuration();
|
|
callbackBreakdown.amplitude += voice->getLastAmplitudeDuration();
|
|
callbackBreakdown.filters += voice->getLastFilterDuration();
|
|
callbackBreakdown.panning += voice->getLastPanningDuration();
|
|
}
|
|
}
|
|
|
|
{ // Apply effect buses
|
|
// -- note(jpc) there is always a "main" bus which is initially empty.
|
|
// without any <effect>, the signal is just going to flow through it.
|
|
ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration };
|
|
|
|
for (auto& bus : effectBuses) {
|
|
if (bus) {
|
|
bus->process(numFrames);
|
|
bus->mixOutputsTo(buffer, *tempMixSpan, numFrames);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the Mix output (fxNtomix opcodes)
|
|
// -- note(jpc) the purpose of the Mix output is not known.
|
|
// perhaps it's designed as extension point for custom processing?
|
|
// as default behavior, it adds itself to the Main signal.
|
|
buffer.add(*tempMixSpan);
|
|
|
|
// Apply the master volume
|
|
buffer.applyGain(db2mag(volume));
|
|
|
|
{ // Clear events and advance midi time
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
resources.midiState.advanceTime(buffer.getNumFrames());
|
|
}
|
|
|
|
callbackBreakdown.dispatch = dispatchDuration;
|
|
resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames);
|
|
|
|
// Reset the dispatch counter
|
|
dispatchDuration = Duration(0);
|
|
|
|
{ // Clear for the next run
|
|
ScopedTiming logger { callbackBreakdown.effects };
|
|
for (auto& bus : effectBuses) {
|
|
if (bus)
|
|
bus->clearInputs(numFrames);
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
ASSERT(!hasNanInf(buffer.getConstSpan(0)));
|
|
ASSERT(!hasNanInf(buffer.getConstSpan(1)));
|
|
ASSERT(isValidAudio(buffer.getConstSpan(0)));
|
|
ASSERT(isValidAudio(buffer.getConstSpan(1)));
|
|
#endif
|
|
}
|
|
|
|
void sfz::Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept
|
|
{
|
|
ASSERT(noteNumber < 128);
|
|
ASSERT(noteNumber >= 0);
|
|
const auto normalizedVelocity = normalizeVelocity(velocity);
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
resources.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity);
|
|
|
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
noteOnDispatch(delay, noteNumber, normalizedVelocity);
|
|
}
|
|
|
|
void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
|
|
{
|
|
ASSERT(noteNumber < 128);
|
|
ASSERT(noteNumber >= 0);
|
|
UNUSED(velocity);
|
|
const auto normalizedVelocity = normalizeVelocity(velocity);
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
resources.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity);
|
|
|
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
// 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);
|
|
const auto replacedVelocity = resources.midiState.getNoteVelocity(noteNumber);
|
|
|
|
for (auto& voice : voices)
|
|
voice->registerNoteOff(delay, noteNumber, replacedVelocity);
|
|
|
|
noteOffDispatch(delay, noteNumber, replacedVelocity);
|
|
}
|
|
|
|
void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
|
|
{
|
|
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
|
for (auto& region : noteActivationLists[noteNumber]) {
|
|
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
|
auto voice = findFreeVoice();
|
|
if (voice == nullptr)
|
|
continue;
|
|
|
|
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept
|
|
{
|
|
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
|
for (auto& region : noteActivationLists[noteNumber]) {
|
|
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
|
unsigned activeNotesInGroup { 0 };
|
|
unsigned activeNotes { 0 };
|
|
Voice* selfMaskCandidate { nullptr };
|
|
|
|
for (auto& voice : voices) {
|
|
const auto voiceRegion = voice->getRegion();
|
|
if (voiceRegion == nullptr)
|
|
continue;
|
|
|
|
if (voiceRegion->group == region->group)
|
|
activeNotesInGroup += 1;
|
|
|
|
if (region->notePolyphony) {
|
|
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
|
activeNotes += 1;
|
|
switch (region->selfMask) {
|
|
case SfzSelfMask::mask:
|
|
if (voice->getTriggerValue() < velocity) {
|
|
if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue())
|
|
selfMaskCandidate = voice.get();
|
|
}
|
|
break;
|
|
case SfzSelfMask::dontMask:
|
|
if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition())
|
|
selfMaskCandidate = voice.get();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (voice->checkOffGroup(delay, region->group))
|
|
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue());
|
|
}
|
|
|
|
if (activeNotesInGroup >= groupMaxPolyphony[region->group])
|
|
continue;
|
|
|
|
if (region->notePolyphony && activeNotes >= *region->notePolyphony) {
|
|
if (selfMaskCandidate != nullptr)
|
|
selfMaskCandidate->release(delay);
|
|
else // We're the lowest velocity guy here
|
|
continue;
|
|
}
|
|
|
|
auto voice = findFreeVoice();
|
|
if (voice == nullptr)
|
|
continue;
|
|
|
|
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept
|
|
{
|
|
const auto normalizedCC = normalizeCC(ccValue);
|
|
hdcc(delay, ccNumber, normalizedCC);
|
|
}
|
|
|
|
void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
|
|
{
|
|
ASSERT(ccNumber < config::numCCs);
|
|
ASSERT(ccNumber >= 0);
|
|
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
resources.midiState.ccEvent(delay, ccNumber, normValue);
|
|
|
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
if (ccNumber == config::resetCC) {
|
|
resetAllControllers(delay);
|
|
return;
|
|
}
|
|
|
|
for (auto& voice : voices)
|
|
voice->registerCC(delay, ccNumber, normValue);
|
|
|
|
for (auto& region : ccActivationLists[ccNumber]) {
|
|
if (region->registerCC(ccNumber, normValue)) {
|
|
auto voice = findFreeVoice();
|
|
if (voice == nullptr)
|
|
continue;
|
|
|
|
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::pitchWheel(int delay, int pitch) noexcept
|
|
{
|
|
ASSERT(pitch <= 8192);
|
|
ASSERT(pitch >= -8192);
|
|
const auto normalizedPitch = normalizeBend(float(pitch));
|
|
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
resources.midiState.pitchBendEvent(delay, normalizedPitch);
|
|
|
|
for (auto& region : regions) {
|
|
region->registerPitchWheel(normalizedPitch);
|
|
}
|
|
|
|
for (auto& voice : voices) {
|
|
voice->registerPitchWheel(delay, normalizedPitch);
|
|
}
|
|
}
|
|
void sfz::Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept
|
|
{
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
}
|
|
void sfz::Synth::tempo(int /* delay */, float /* secondsPerQuarter */) noexcept
|
|
{
|
|
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
|
}
|
|
|
|
int sfz::Synth::getNumRegions() const noexcept
|
|
{
|
|
return static_cast<int>(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 static_cast<int>(resources.curves.getNumCurves());
|
|
}
|
|
|
|
std::string sfz::Synth::exportMidnam(absl::string_view model) const
|
|
{
|
|
pugi::xml_document doc;
|
|
absl::string_view manufacturer = config::midnamManufacturer;
|
|
|
|
if (model.empty())
|
|
model = config::midnamModel;
|
|
|
|
doc.append_child(pugi::node_doctype).set_value("MIDINameDocument PUBLIC"
|
|
" \"-//MIDI Manufacturers Association//DTD MIDINameDocument 1.0//EN\""
|
|
" \"http://www.midi.org/dtds/MIDINameDocument10.dtd\"");
|
|
|
|
pugi::xml_node root = doc.append_child("MIDINameDocument");
|
|
|
|
root.append_child(pugi::node_comment)
|
|
.set_value("Generated by Sfizz for the current instrument");
|
|
|
|
root.append_child("Author");
|
|
|
|
pugi::xml_node device = root.append_child("MasterDeviceNames");
|
|
device.append_child("Manufacturer")
|
|
.append_child(pugi::node_pcdata)
|
|
.set_value(std::string(manufacturer).c_str());
|
|
device.append_child("Model")
|
|
.append_child(pugi::node_pcdata)
|
|
.set_value(std::string(model).c_str());
|
|
|
|
{
|
|
pugi::xml_node devmode = device.append_child("CustomDeviceMode");
|
|
devmode.append_attribute("Name").set_value("Default");
|
|
|
|
pugi::xml_node nsas = devmode.append_child("ChannelNameSetAssignments");
|
|
for (unsigned c = 0; c < 16; ++c) {
|
|
pugi::xml_node nsa = nsas.append_child("ChannelNameSetAssign");
|
|
nsa.append_attribute("Channel").set_value(std::to_string(c + 1).c_str());
|
|
nsa.append_attribute("NameSet").set_value("Play");
|
|
}
|
|
}
|
|
|
|
{
|
|
pugi::xml_node chns = device.append_child("ChannelNameSet");
|
|
chns.append_attribute("Name").set_value("Play");
|
|
|
|
pugi::xml_node acs = chns.append_child("AvailableForChannels");
|
|
for (unsigned c = 0; c < 16; ++c) {
|
|
pugi::xml_node ac = acs.append_child("AvailableChannel");
|
|
ac.append_attribute("Channel").set_value(std::to_string(c + 1).c_str());
|
|
ac.append_attribute("Available").set_value("true");
|
|
}
|
|
|
|
chns.append_child("UsesControlNameList")
|
|
.append_attribute("Name")
|
|
.set_value("Controls");
|
|
chns.append_child("UsesNoteNameList")
|
|
.append_attribute("Name")
|
|
.set_value("Notes");
|
|
}
|
|
|
|
{
|
|
pugi::xml_node cns = device.append_child("ControlNameList");
|
|
cns.append_attribute("Name").set_value("Controls");
|
|
for (const auto& pair : ccLabels) {
|
|
pugi::xml_node cn = cns.append_child("Control");
|
|
cn.append_attribute("Type").set_value("7bit");
|
|
cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str());
|
|
cn.append_attribute("Name").set_value(pair.second.c_str());
|
|
}
|
|
}
|
|
|
|
{
|
|
pugi::xml_node nnl = device.append_child("NoteNameList");
|
|
nnl.append_attribute("Name").set_value("Notes");
|
|
for (const auto& pair : keyswitchLabels) {
|
|
pugi::xml_node nn = nnl.append_child("Note");
|
|
nn.append_attribute("Number").set_value(std::to_string(pair.first).c_str());
|
|
nn.append_attribute("Name").set_value(pair.second.c_str());
|
|
}
|
|
for (const auto& pair : keyLabels) {
|
|
pugi::xml_node nn = nnl.append_child("Note");
|
|
nn.append_attribute("Number").set_value(std::to_string(pair.first).c_str());
|
|
nn.append_attribute("Name").set_value(pair.second.c_str());
|
|
}
|
|
}
|
|
|
|
///
|
|
struct string_writer : pugi::xml_writer {
|
|
std::string result;
|
|
|
|
string_writer()
|
|
{
|
|
result.reserve(8192);
|
|
}
|
|
|
|
void write(const void* data, size_t size) override
|
|
{
|
|
result.append(static_cast<const char*>(data), size);
|
|
}
|
|
};
|
|
|
|
///
|
|
string_writer writer;
|
|
doc.save(writer);
|
|
return std::move(writer.result);
|
|
}
|
|
|
|
const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept
|
|
{
|
|
return (size_t)idx < regions.size() ? regions[idx].get() : nullptr;
|
|
}
|
|
|
|
const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept
|
|
{
|
|
return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr;
|
|
}
|
|
|
|
const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept
|
|
{
|
|
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
|
|
}
|
|
|
|
const std::vector<std::string>& sfz::Synth::getUnknownOpcodes() const noexcept
|
|
{
|
|
return unknownOpcodes;
|
|
}
|
|
size_t sfz::Synth::getNumPreloadedSamples() const noexcept
|
|
{
|
|
return resources.filePool.getNumPreloadedSamples();
|
|
}
|
|
|
|
float sfz::Synth::getVolume() const noexcept
|
|
{
|
|
return volume;
|
|
}
|
|
void sfz::Synth::setVolume(float volume) noexcept
|
|
{
|
|
this->volume = Default::volumeRange.clamp(volume);
|
|
}
|
|
|
|
int sfz::Synth::getNumVoices() const noexcept
|
|
{
|
|
return numVoices;
|
|
}
|
|
|
|
void sfz::Synth::setNumVoices(int numVoices) noexcept
|
|
{
|
|
ASSERT(numVoices > 0);
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
// fast path
|
|
if (numVoices == this->numVoices)
|
|
return;
|
|
|
|
resetVoices(numVoices);
|
|
}
|
|
|
|
void sfz::Synth::resetVoices(int numVoices)
|
|
{
|
|
voices.clear();
|
|
for (int i = 0; i < numVoices; ++i)
|
|
voices.push_back(absl::make_unique<Voice>(resources));
|
|
|
|
voiceViewArray.clear();
|
|
for (auto& voice : voices) {
|
|
voice->setSampleRate(this->sampleRate);
|
|
voice->setSamplesPerBlock(this->samplesPerBlock);
|
|
voiceViewArray.push_back(voice.get());
|
|
}
|
|
|
|
this->numVoices = numVoices;
|
|
}
|
|
|
|
void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
// fast path
|
|
if (factor == oversamplingFactor)
|
|
return;
|
|
|
|
for (auto& voice : voices)
|
|
voice->reset();
|
|
|
|
resources.filePool.emptyFileLoadingQueues();
|
|
resources.filePool.setOversamplingFactor(factor);
|
|
oversamplingFactor = factor;
|
|
}
|
|
|
|
sfz::Oversampling sfz::Synth::getOversamplingFactor() const noexcept
|
|
{
|
|
return oversamplingFactor;
|
|
}
|
|
|
|
void sfz::Synth::setPreloadSize(uint32_t preloadSize) noexcept
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
// fast path
|
|
if (preloadSize == resources.filePool.getPreloadSize())
|
|
return;
|
|
|
|
resources.filePool.setPreloadSize(preloadSize);
|
|
}
|
|
|
|
uint32_t sfz::Synth::getPreloadSize() const noexcept
|
|
{
|
|
return resources.filePool.getPreloadSize();
|
|
}
|
|
|
|
void sfz::Synth::enableFreeWheeling() noexcept
|
|
{
|
|
if (!freeWheeling) {
|
|
freeWheeling = true;
|
|
DBG("Enabling freewheeling");
|
|
}
|
|
}
|
|
void sfz::Synth::disableFreeWheeling() noexcept
|
|
{
|
|
if (freeWheeling) {
|
|
freeWheeling = false;
|
|
DBG("Disabling freewheeling");
|
|
}
|
|
}
|
|
|
|
void sfz::Synth::resetAllControllers(int delay) noexcept
|
|
{
|
|
resources.midiState.resetAllControllers(delay);
|
|
|
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
|
if (!lock.owns_lock())
|
|
return;
|
|
|
|
for (auto& voice : voices) {
|
|
voice->registerPitchWheel(delay, 0);
|
|
for (int cc = 0; cc < config::numCCs; ++cc)
|
|
voice->registerCC(delay, cc, 0.0f);
|
|
}
|
|
|
|
for (auto& region : regions) {
|
|
for (int cc = 0; cc < config::numCCs; ++cc)
|
|
region->registerCC(cc, 0.0f);
|
|
}
|
|
}
|
|
|
|
fs::file_time_type sfz::Synth::checkModificationTime()
|
|
{
|
|
auto returnedTime = modificationTime;
|
|
for (const auto& file : parser.getIncludedFiles()) {
|
|
std::error_code ec;
|
|
const auto fileTime = fs::last_write_time(file, ec);
|
|
if (!ec && returnedTime < fileTime)
|
|
returnedTime = fileTime;
|
|
}
|
|
return returnedTime;
|
|
}
|
|
|
|
bool sfz::Synth::shouldReloadFile()
|
|
{
|
|
return (checkModificationTime() > modificationTime);
|
|
}
|
|
|
|
void sfz::Synth::enableLogging(absl::string_view prefix) noexcept
|
|
{
|
|
resources.logger.enableLogging(prefix);
|
|
}
|
|
|
|
void sfz::Synth::setLoggingPrefix(absl::string_view prefix) noexcept
|
|
{
|
|
resources.logger.setPrefix(prefix);
|
|
}
|
|
|
|
void sfz::Synth::disableLogging() noexcept
|
|
{
|
|
resources.logger.disableLogging();
|
|
}
|
|
|
|
void sfz::Synth::allSoundOff() noexcept
|
|
{
|
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
|
|
|
for (auto& voice : voices)
|
|
voice->reset();
|
|
for (auto& effectBus : effectBuses)
|
|
effectBus->clear();
|
|
}
|
|
|
|
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
|
{
|
|
while (groupMaxPolyphony.size() <= groupIdx)
|
|
groupMaxPolyphony.push_back(config::maxVoices);
|
|
|
|
groupMaxPolyphony[groupIdx] = polyphony;
|
|
}
|