373 lines
No EOL
12 KiB
C++
373 lines
No EOL
12 KiB
C++
// 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 "Helpers.h"
|
|
#include "absl/algorithm/container.h"
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <utility>
|
|
|
|
sfz::Synth::Synth()
|
|
{
|
|
for (int i = 0; i < config::numVoices; ++i)
|
|
voices.push_back(std::make_unique<Voice>(ccState));
|
|
}
|
|
|
|
void sfz::Synth::callback(std::string_view header, const std::vector<Opcode>& members)
|
|
{
|
|
switch (hash(header)) {
|
|
case hash("global"):
|
|
// We shouldn't have multiple global headers in file
|
|
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<Opcode>& regionOpcodes)
|
|
{
|
|
auto lastRegion = std::make_unique<Region>();
|
|
|
|
auto parseOpcodes = [&](const auto& opcodes) {
|
|
for (auto& opcode : opcodes) {
|
|
const auto unknown = absl::c_find_if(unknownOpcodes, [&](std::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()
|
|
{
|
|
hasGlobal = false;
|
|
hasControl = false;
|
|
numGroups = 0;
|
|
numMasters = 0;
|
|
numCurves = 0;
|
|
defaultSwitch = std::nullopt;
|
|
for (auto& state : ccState)
|
|
state = 0;
|
|
ccNames.clear();
|
|
globalOpcodes.clear();
|
|
masterOpcodes.clear();
|
|
groupOpcodes.clear();
|
|
regions.clear();
|
|
}
|
|
|
|
void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& 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<Opcode>& members)
|
|
{
|
|
for (auto& member : members) {
|
|
switch (hash(member.opcode)) {
|
|
case hash("set_cc"):
|
|
if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter))
|
|
setValueFromOpcode(member, ccState[*member.parameter], Default::ccRange);
|
|
break;
|
|
case hash("label_cc"):
|
|
if (member.parameter && Default::ccRange.containsWithEnd(*member.parameter))
|
|
ccNames.emplace_back(*member.parameter, member.value);
|
|
break;
|
|
case hash("default_path"):
|
|
if (auto newPath = std::filesystem::path(member.value); std::filesystem::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<int, float>(127, 1.0f));
|
|
if (region.velocityPoints.front().first != sfz::Default::velocityRange.getStart())
|
|
region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair<int, float>(0, 0.0f));
|
|
}
|
|
else
|
|
{
|
|
if (region.velocityPoints.front().first != sfz::Default::velocityRange.getEnd())
|
|
region.velocityPoints.insert(region.velocityPoints.begin(), std::make_pair<int, float>(127, 0.0f));
|
|
if (region.velocityPoints.back().first != sfz::Default::velocityRange.getStart())
|
|
region.velocityPoints.push_back(std::make_pair<int, float>(0, 1.0f));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool sfz::Synth::loadSfzFile(const std::filesystem::path& filename)
|
|
{
|
|
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);
|
|
if (!fileInformation) {
|
|
DBG("Removing the region with sample " << region->sample);
|
|
std::iter_swap(currentRegion, lastRegion);
|
|
lastRegion--;
|
|
continue;
|
|
}
|
|
|
|
region->numChannels = fileInformation->numChannels;
|
|
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 = region->keyRange.getStart(); note <= region->keyRange.getEnd(); note++)
|
|
noteActivationLists[note].push_back(region);
|
|
|
|
for (auto cc = region->keyRange.getStart(); cc <= region->keyRange.getEnd(); cc++)
|
|
ccActivationLists[cc].push_back(region);
|
|
|
|
// Defaults
|
|
for (int ccIndex = 1; ccIndex < 128; ccIndex++)
|
|
region->registerCC(region->channelRange.getStart(), ccIndex, ccState[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()) {
|
|
DBG("Voices are overloaded, can't start a new note");
|
|
return {};
|
|
}
|
|
return freeVoice->get();
|
|
}
|
|
|
|
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
|
|
{
|
|
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) noexcept
|
|
{
|
|
DBG("[Synth] Sample rate set to " << sampleRate);
|
|
this->sampleRate = sampleRate;
|
|
for (auto& voice : voices)
|
|
voice->setSampleRate(sampleRate);
|
|
}
|
|
|
|
void sfz::Synth::renderBlock(StereoSpan<float> buffer) noexcept
|
|
{
|
|
ScopedFTZ ftz;
|
|
buffer.fill(0.0f);
|
|
StereoSpan<float> 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) noexcept
|
|
{
|
|
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) noexcept
|
|
{
|
|
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) noexcept
|
|
{
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
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 numCurves;
|
|
}
|
|
const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept
|
|
{
|
|
return (size_t)idx < regions.size() ? regions[idx].get() : nullptr;
|
|
}
|
|
std::set<std::string_view> sfz::Synth::getUnknownOpcodes() const noexcept
|
|
{
|
|
return unknownOpcodes;
|
|
}
|
|
size_t sfz::Synth::getNumPreloadedSamples() const noexcept
|
|
{
|
|
return filePool.getNumPreloadedSamples();
|
|
} |