diff --git a/README.md b/README.md
index ed360253..0a5be8e5 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,8 @@ App icon by Valery Davletbaev, distributed under [CC-BY](https://creativecommons
Built-in [metronome sounds](https://stash.reaper.fm/40824/Metronomes.zip) are recorded by Ludwig Peter Müller, [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
+The built-in SoundFont player instument is based on [SFZero](https://github.com/stevefolta/SFZero), written by Steve Folta and [extended](https://github.com/cognitone/SFZeroMT) by Leo Olivers and Cognitone.
+
All documentation, translations and logotypes are distributed under [CC-BY](https://creativecommons.org/licenses/by/4.0/).
### Translation and proofreading credits
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/README.md b/Source/Core/Audio/BuiltIn/SoundFont/README.md
new file mode 100644
index 00000000..52044453
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/README.md
@@ -0,0 +1,10 @@
+The SoundFont synth implementation in this folder is largely based on SFZero by Steve Folta, extended by Leo Olivers and Cognitone, all distributed under MIT license:
+
+Original code copyright (C) 2012 Steve Folta
+https://github.com/stevefolta/SFZero
+
+Converted to Juce module (C) 2016 Leo Olivers
+https://github.com/altalogix/SFZero
+
+Extended for multi-timbral operation (C) 2017 Cognitone
+https://github.com/cognitone/SFZeroMT
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.cpp b/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.cpp
new file mode 100644
index 00000000..0ed07bec
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.cpp
@@ -0,0 +1,1156 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#include "Common.h"
+#include "SoundFont2Sound.h"
+#include "SoundFontSample.h"
+#include "SoundFontRegion.h"
+
+#include
+#include
+
+typedef char sf2fourcc[4];
+typedef unsigned char sf2byte;
+typedef unsigned long sf2dword;
+typedef unsigned short sf2word;
+typedef char sf2string[20];
+
+#define FourCCEquals(value1, value2) \
+ (value1[0] == value2[0] && \
+ value1[1] == value2[1] && \
+ value1[2] == value2[2] && \
+ value1[3] == value2[3])
+
+struct RIFFChunk final
+{
+ enum class Type
+ {
+ RIFF,
+ LIST,
+ Custom
+ };
+
+ bool isTypeOf(sf2fourcc chunkName) const
+ {
+ return this->id[0] == chunkName[0] &&
+ this->id[1] == chunkName[1] &&
+ this->id[2] == chunkName[2] &&
+ this->id[3] == chunkName[3];
+ };
+
+ sf2fourcc id;
+ sf2dword size;
+ Type type;
+ int64 start;
+
+ void readFrom(InputStream &file)
+ {
+ file.read(&this->id, sizeof(sf2fourcc));
+ this->size = static_cast(file.readInt());
+ this->start = file.getPosition();
+
+ if (FourCCEquals(id, "RIFF"))
+ {
+ this->type = Type::RIFF;
+ file.read(&this->id, sizeof(sf2fourcc));
+ this->start += sizeof(sf2fourcc);
+ this->size -= sizeof(sf2fourcc);
+ }
+ else if (FourCCEquals(id, "LIST"))
+ {
+ this->type = Type::LIST;
+ file.read(&this->id, sizeof(sf2fourcc));
+ this->start += sizeof(sf2fourcc);
+ this->size -= sizeof(sf2fourcc);
+ }
+ else
+ {
+ type = Type::Custom;
+ }
+ }
+
+ void seek(InputStream &file)
+ {
+ file.setPosition(this->start);
+ }
+
+ void seekAfter(InputStream &file)
+ {
+ int64 next = this->start + this->size;
+
+ if (next % 2 != 0)
+ {
+ next += 1;
+ }
+
+ file.setPosition(next);
+ }
+
+ int64 end()
+ {
+ return (this->start + this->size);
+ }
+
+ String readString(InputStream &file)
+ {
+ return file.readEntireStreamAsString();
+ }
+
+ JUCE_LEAK_DETECTOR(RIFFChunk)
+};
+
+namespace SF2
+{
+struct rangesType final
+{
+ sf2byte lo, hi;
+};
+
+union genAmountType
+{
+ rangesType range;
+ short shortAmount;
+ sf2word wordAmount;
+};
+
+struct iver final
+{
+ sf2word major;
+ sf2word minor;
+
+ void readFrom(InputStream &file);
+
+ JUCE_LEAK_DETECTOR(iver)
+};
+
+struct phdr final
+{
+ sf2string presetName;
+ sf2word preset;
+ sf2word bank;
+ sf2word presetBagNdx;
+ sf2dword library;
+ sf2dword genre;
+ sf2dword morphology;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 38;
+
+ JUCE_LEAK_DETECTOR(phdr)
+};
+
+struct pbag final
+{
+ sf2word genNdx;
+ sf2word modNdx;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 4;
+
+ JUCE_LEAK_DETECTOR(pbag)
+};
+
+struct pmod final
+{
+ sf2word modSrcOper;
+ sf2word modDestOper;
+ short modAmount;
+ sf2word modAmtSrcOper;
+ sf2word modTransOper;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 10;
+
+ JUCE_LEAK_DETECTOR(pmod)
+};
+
+struct pgen final
+{
+ sf2word genOper;
+ genAmountType genAmount;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 4;
+
+ JUCE_LEAK_DETECTOR(pgen)
+};
+
+struct inst final
+{
+ sf2string instName;
+ sf2word instBagNdx;
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 22;
+
+ JUCE_LEAK_DETECTOR(inst)
+};
+
+struct ibag final
+{
+ sf2word instGenNdx;
+ sf2word instModNdx;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 4;
+
+ JUCE_LEAK_DETECTOR(ibag)
+};
+
+struct imod final
+{
+ sf2word modSrcOper;
+ sf2word modDestOper;
+ short modAmount;
+ sf2word modAmtSrcOper;
+ sf2word modTransOper;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 10;
+
+ JUCE_LEAK_DETECTOR(imod)
+};
+
+struct igen final
+{
+ sf2word genOper;
+ genAmountType genAmount;
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 4;
+
+ JUCE_LEAK_DETECTOR(igen)
+};
+
+struct shdr final
+{
+ sf2string sampleName;
+ sf2dword start;
+ sf2dword end;
+ sf2dword startLoop;
+ sf2dword endLoop;
+ sf2dword sampleRate;
+ sf2byte originalPitch;
+ char pitchCorrection;
+ sf2word sampleLink;
+ sf2word sampleType;
+
+ void readFrom(InputStream &file);
+
+ static const int sizeInFile = 46;
+
+ JUCE_LEAK_DETECTOR(shdr)
+};
+
+struct Hydra final
+{
+ std::vector presetHeaderList;
+ std::vector pbagItems;
+ std::vector pmodItems;
+ std::vector pgenItems;
+ std::vector instItems;
+ std::vector ibagItems;
+ std::vector imodItems;
+ std::vector igenItems;
+ std::vector shdrItems;
+
+ template
+ void readChunkItems(const RIFFChunk &chunk,
+ std::vector &chunkItems,
+ InputStream &file)
+ {
+ int numItems = (int)chunk.size / T::sizeInFile;
+ for (int i = 0; i < numItems; ++i)
+ {
+ T t;
+ t.readFrom(file);
+ chunkItems.push_back(t);
+ }
+ }
+
+ void readFrom(InputStream &file, int64 pdtaChunkEnd);
+ bool isComplete();
+
+ JUCE_LEAK_DETECTOR(Hydra)
+};
+} // namespace SF2
+
+void SF2::iver::readFrom(InputStream &file)
+{
+ this->major = (sf2word)file.readShort();
+ this->minor = (sf2word)file.readShort();
+}
+
+void SF2::phdr::readFrom(InputStream &file)
+{
+ file.read(this->presetName, 20);
+ this->preset = (sf2word)file.readShort();
+ this->bank = (sf2word)file.readShort();
+ this->presetBagNdx = (sf2word)file.readShort();
+ this->library = (sf2dword)file.readInt();
+ this->genre = (sf2dword)file.readInt();
+ this->morphology = (sf2dword)file.readInt();
+}
+
+void SF2::pbag::readFrom(InputStream &file)
+{
+ this->genNdx = (sf2word)file.readShort();
+ this->modNdx = (sf2word)file.readShort();
+}
+
+void SF2::pmod::readFrom(InputStream &file)
+{
+ this->modSrcOper = (sf2word)file.readShort();
+ this->modDestOper = (sf2word)file.readShort();
+ this->modAmount = file.readShort();
+ this->modAmtSrcOper = (sf2word)file.readShort();
+ this->modTransOper = (sf2word)file.readShort();
+}
+
+void SF2::pgen::readFrom(InputStream &file)
+{
+ this->genOper = (sf2word)file.readShort();
+ this->genAmount.shortAmount = file.readShort();
+}
+
+void SF2::inst::readFrom(InputStream &file)
+{
+ file.read(this->instName, 20);
+ this->instBagNdx = (sf2word)file.readShort();
+}
+
+void SF2::ibag::readFrom(InputStream &file)
+{
+ this->instGenNdx = (sf2word)file.readShort();
+ this->instModNdx = (sf2word)file.readShort();
+}
+
+void SF2::imod::readFrom(InputStream &file)
+{
+ this->modSrcOper = (sf2word)file.readShort();
+ this->modDestOper = (sf2word)file.readShort();
+ this->modAmount = file.readShort();
+ this->modAmtSrcOper = (sf2word)file.readShort();
+ this->modTransOper = (sf2word)file.readShort();
+}
+
+void SF2::igen::readFrom(InputStream &file)
+{
+ this->genOper = (sf2word)file.readShort();
+ this->genAmount.shortAmount = file.readShort();
+}
+
+void SF2::shdr::readFrom(InputStream &file)
+{
+ file.read(this->sampleName, 20);
+ this->start = (sf2dword)file.readInt();
+ this->end = (sf2dword)file.readInt();
+ this->startLoop = (sf2dword)file.readInt();
+ this->endLoop = (sf2dword)file.readInt();
+ this->sampleRate = (sf2dword)file.readInt();
+ this->originalPitch = (sf2byte)file.readByte();
+ this->pitchCorrection = file.readByte();
+ this->sampleLink = (sf2word)file.readShort();
+ this->sampleType = (sf2word)file.readShort();
+}
+
+void SF2::Hydra::readFrom(InputStream &file, int64 pdtaChunkEnd)
+{
+ auto check = [](RIFFChunk &chunk, sf2fourcc chunkName)
+ {
+ sf2fourcc &chunkID = chunk.id;
+ return chunkID[0] == chunkName[0] &&
+ chunkID[1] == chunkName[1] &&
+ chunkID[2] == chunkName[2] &&
+ chunkID[3] == chunkName[3];
+ };
+
+ sf2fourcc phdrType = {'p', 'h', 'd', 'r'};
+ sf2fourcc pbagType = {'p', 'b', 'a', 'g'};
+ sf2fourcc pmodType = {'p', 'm', 'o', 'd'};
+ sf2fourcc pgenType = {'p', 'g', 'e', 'n'};
+ sf2fourcc instType = {'i', 'n', 's', 't'};
+ sf2fourcc ibagType = {'i', 'b', 'a', 'g'};
+ sf2fourcc imodType = {'i', 'm', 'o', 'd'};
+ sf2fourcc igenType = {'i', 'g', 'e', 'n'};
+ sf2fourcc shdrType = {'s', 'h', 'd', 'r'};
+
+ while (file.getPosition() < pdtaChunkEnd)
+ {
+ RIFFChunk chunk;
+ chunk.readFrom(file);
+
+ if (check(chunk, phdrType))
+ {
+ this->readChunkItems(chunk, this->presetHeaderList, file);
+ }
+ else if (check(chunk, pbagType))
+ {
+ this->readChunkItems(chunk, this->pbagItems, file);
+ }
+ else if (check(chunk, pmodType))
+ {
+ this->readChunkItems(chunk, this->pmodItems, file);
+ }
+ else if (check(chunk, pgenType))
+ {
+ this->readChunkItems(chunk, this->pgenItems, file);
+ }
+ else if (check(chunk, instType))
+ {
+ this->readChunkItems(chunk, this->instItems, file);
+ }
+ else if (check(chunk, ibagType))
+ {
+ this->readChunkItems(chunk, this->ibagItems, file);
+ }
+ else if (check(chunk, imodType))
+ {
+ this->readChunkItems(chunk, this->imodItems, file);
+ }
+ else if (check(chunk, igenType))
+ {
+ this->readChunkItems(chunk, this->igenItems, file);
+ }
+ else if (check(chunk, shdrType))
+ {
+ this->readChunkItems(chunk, this->shdrItems, file);
+ }
+
+ chunk.seekAfter(file);
+ }
+}
+
+bool SF2::Hydra::isComplete()
+{
+ return !this->presetHeaderList.empty() &&
+ !this->pbagItems.empty() &&
+ !this->pmodItems.empty() &&
+ !this->pgenItems.empty() &&
+ !this->instItems.empty() &&
+ !this->ibagItems.empty() &&
+ !this->imodItems.empty() &&
+ !this->igenItems.empty() &&
+ !this->shdrItems.empty();
+}
+
+//===----------------------------------------------------------------------===//
+// SoundFont2Generator
+//===----------------------------------------------------------------------===//
+
+struct SoundFont2Generator final
+{
+ enum Type
+ {
+ Word,
+ Short,
+ Range
+ };
+
+ const char *name;
+ Type type;
+
+ enum
+ {
+ startAddrsOffset,
+ endAddrsOffset,
+ startloopAddrsOffset,
+ endloopAddrsOffset,
+ startAddrsCoarseOffset,
+ modLfoToPitch,
+ vibLfoToPitch,
+ modEnvToPitch,
+ initialFilterFc,
+ initialFilterQ,
+ modLfoToFilterFc,
+ modEnvToFilterFc,
+ endAddrsCoarseOffset,
+ modLfoToVolume,
+ unused1,
+ chorusEffectsSend,
+ reverbEffectsSend,
+ pan,
+ unused2,
+ unused3,
+ unused4,
+ delayModLFO,
+ freqModLFO,
+ delayVibLFO,
+ freqVibLFO,
+ delayModEnv,
+ attackModEnv,
+ holdModEnv,
+ decayModEnv,
+ sustainModEnv,
+ releaseModEnv,
+ keynumToModEnvHold,
+ keynumToModEnvDecay,
+ delayVolEnv,
+ attackVolEnv,
+ holdVolEnv,
+ decayVolEnv,
+ sustainVolEnv,
+ releaseVolEnv,
+ keynumToVolEnvHold,
+ keynumToVolEnvDecay,
+ instrument,
+ reserved1,
+ keyRange,
+ velRange,
+ startloopAddrsCoarseOffset,
+ keynum,
+ velocity,
+ initialAttenuation,
+ reserved2,
+ endloopAddrsCoarseOffset,
+ coarseTune,
+ fineTune,
+ sampleID,
+ sampleModes,
+ reserved3,
+ scaleTuning,
+ exclusiveClass,
+ overridingRootKey,
+ unused5,
+ endOper
+ };
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFont2Generator)
+};
+
+static const SoundFont2Generator generators[] = {
+ { "startAddrsOffset", SoundFont2Generator::Short },
+ { "endAddrsOffset", SoundFont2Generator::Short },
+ { "startloopAddrsOffset", SoundFont2Generator::Short },
+ { "endloopAddrsOffset", SoundFont2Generator::Short },
+ { "startAddrsCoarseOffset", SoundFont2Generator::Short },
+ { "modLfoToPitch", SoundFont2Generator::Short },
+ { "vibLfoToPitch", SoundFont2Generator::Short },
+ { "modEnvToPitch", SoundFont2Generator::Short },
+ { "initialFilterFc", SoundFont2Generator::Short },
+ { "initialFilterQ", SoundFont2Generator::Short },
+ { "modLfoToFilterFc", SoundFont2Generator::Short },
+ { "modEnvToFilterFc", SoundFont2Generator::Short },
+ { "endAddrsCoarseOffset", SoundFont2Generator::Short },
+ { "modLfoToVolume", SoundFont2Generator::Short },
+ { "unused1", SoundFont2Generator::Short },
+ { "chorusEffectsSend", SoundFont2Generator::Short },
+ { "reverbEffectsSend", SoundFont2Generator::Short },
+ { "pan", SoundFont2Generator::Short },
+ { "unused2", SoundFont2Generator::Short },
+ { "unused3", SoundFont2Generator::Short },
+ { "unused4", SoundFont2Generator::Short },
+ { "delayModLFO", SoundFont2Generator::Short },
+ { "freqModLFO", SoundFont2Generator::Short },
+ { "delayVibLFO", SoundFont2Generator::Short },
+ { "freqVibLFO", SoundFont2Generator::Short },
+ { "delayModEnv", SoundFont2Generator::Short },
+ { "attackModEnv", SoundFont2Generator::Short },
+ { "holdModEnv", SoundFont2Generator::Short },
+ { "decayModEnv", SoundFont2Generator::Short },
+ { "sustainModEnv", SoundFont2Generator::Short },
+ { "releaseModEnv", SoundFont2Generator::Short },
+ { "keynumToModEnvHold", SoundFont2Generator::Short },
+ { "keynumToModEnvDecay", SoundFont2Generator::Short },
+ { "delayVolEnv", SoundFont2Generator::Short },
+ { "attackVolEnv", SoundFont2Generator::Short },
+ { "holdVolEnv", SoundFont2Generator::Short },
+ { "decayVolEnv", SoundFont2Generator::Short },
+ { "sustainVolEnv", SoundFont2Generator::Short },
+ { "releaseVolEnv", SoundFont2Generator::Short },
+ { "keynumToVolEnvHold", SoundFont2Generator::Short },
+ { "keynumToVolEnvDecay", SoundFont2Generator::Short },
+ { "instrument", SoundFont2Generator::Word },
+ { "reserved1", SoundFont2Generator::Short },
+ { "keyRange", SoundFont2Generator::Range },
+ { "velRange", SoundFont2Generator::Range },
+ { "startloopAddrsCoarseOffset", SoundFont2Generator::Short },
+ { "keynum", SoundFont2Generator::Short },
+ { "velocity", SoundFont2Generator::Short },
+ { "initialAttenuation", SoundFont2Generator::Short },
+ { "reserved2", SoundFont2Generator::Short },
+ { "endloopAddrsCoarseOffset", SoundFont2Generator::Short },
+ { "coarseTune", SoundFont2Generator::Short },
+ { "fineTune", SoundFont2Generator::Short },
+ { "sampleID", SoundFont2Generator::Word },
+ { "sampleModes", SoundFont2Generator::Word },
+ { "reserved3", SoundFont2Generator::Short },
+ { "scaleTuning", SoundFont2Generator::Short },
+ { "exclusiveClass", SoundFont2Generator::Short },
+ { "overridingRootKey", SoundFont2Generator::Short },
+ { "unused5", SoundFont2Generator::Short },
+ { "endOper", SoundFont2Generator::Short }
+};
+
+const SoundFont2Generator *GeneratorFor(int index)
+{
+ static const int numGenerators = sizeof(generators) / sizeof(generators[0]);
+
+ if (index >= numGenerators)
+ {
+ return nullptr;
+ }
+
+ return &generators[index];
+}
+
+//===----------------------------------------------------------------------===//
+// SoundFont2Reader
+//===----------------------------------------------------------------------===//
+
+class SoundFont2Reader final
+{
+public:
+
+ SoundFont2Reader(SoundFont2Sound &sound, const File &file);
+
+ void read();
+
+ SharedAudioSampleBuffer::Ptr readSamples();
+
+private:
+
+ SoundFont2Sound &sf2Sound;
+
+ UniquePointer fileInputStream;
+
+ void addGeneratorToRegion(sf2word genOper, SF2::genAmountType *amount, SoundFontRegion *region);
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFont2Reader)
+};
+
+SoundFont2Reader::SoundFont2Reader(SoundFont2Sound &soundIn, const File &fileIn) :
+ sf2Sound(soundIn),
+ fileInputStream(fileIn.createInputStream()) {}
+
+void SoundFont2Reader::read()
+{
+ if (this->fileInputStream == nullptr)
+ {
+ this->sf2Sound.addError("Couldn't open file.");
+ return;
+ }
+
+ this->fileInputStream->setPosition(0);
+ RIFFChunk riffChunk;
+ riffChunk.readFrom(*this->fileInputStream);
+
+ // Read the hydra.
+ SF2::Hydra hydra;
+ while (this->fileInputStream->getPosition() < riffChunk.end())
+ {
+ RIFFChunk chunk;
+ chunk.readFrom(*this->fileInputStream);
+ if (FourCCEquals(chunk.id, "pdta"))
+ {
+ hydra.readFrom(*this->fileInputStream, chunk.end());
+ break;
+ }
+ chunk.seekAfter(*this->fileInputStream);
+ }
+
+ if (!hydra.isComplete())
+ {
+ this->sf2Sound.addError("Invalid SF2 file (missing or incomplete hydra).");
+ return;
+ }
+
+ // Read each preset.
+ for (size_t whichPreset = 0; whichPreset < hydra.presetHeaderList.size() - 1; ++whichPreset)
+ {
+ SF2::phdr *presetHeader = &hydra.presetHeaderList[whichPreset];
+ auto preset = make(presetHeader->presetName, presetHeader->bank, presetHeader->preset);
+
+ // Zones.
+ //*** TODO: Handle global zone (modulators only).
+ int zoneEnd = hydra.presetHeaderList[whichPreset + 1].presetBagNdx;
+ for (int whichZone = presetHeader->presetBagNdx; whichZone < zoneEnd; ++whichZone)
+ {
+ SF2::pbag *presetZone = &hydra.pbagItems[whichZone];
+ SoundFontRegion presetRegion;
+ presetRegion.clearForRelativeSF2();
+
+ // Generators.
+ int genEnd = hydra.pbagItems[whichZone + 1].genNdx;
+ for (int whichGen = presetZone->genNdx; whichGen < genEnd; ++whichGen)
+ {
+ SF2::pgen *presetGenerator = &hydra.pgenItems[whichGen];
+
+ // Instrument.
+ if (presetGenerator->genOper == SoundFont2Generator::instrument)
+ {
+ sf2word whichInst = presetGenerator->genAmount.wordAmount;
+ if (whichInst < hydra.instItems.size())
+ {
+ SoundFontRegion instRegion;
+ instRegion.clearForSF2();
+ // Preset generators are supposed to be "relative" modifications of
+ // the instrument settings, but that makes no sense for ranges.
+ // For those, we'll have the instrument's generator take
+ // precedence, though that may not be correct.
+ instRegion.lokey = presetRegion.lokey;
+ instRegion.hikey = presetRegion.hikey;
+ instRegion.lovel = presetRegion.lovel;
+ instRegion.hivel = presetRegion.hivel;
+
+ SF2::inst *inst = &hydra.instItems[whichInst];
+ int firstZone = inst->instBagNdx;
+ int zoneEnd2 = inst[1].instBagNdx;
+ for (int whichZone2 = firstZone; whichZone2 < zoneEnd2; ++whichZone2)
+ {
+ SF2::ibag *ibag = &hydra.ibagItems[whichZone2];
+
+ // Generators.
+ SoundFontRegion zoneRegion = instRegion;
+ bool hadSampleID = false;
+ int genEnd2 = ibag[1].instGenNdx;
+ for (int whichGen2 = ibag->instGenNdx; whichGen2 < genEnd2; ++whichGen2)
+ {
+ SF2::igen *igen = &hydra.igenItems[whichGen2];
+ if (igen->genOper == SoundFont2Generator::sampleID)
+ {
+ int whichSample = igen->genAmount.wordAmount;
+ SF2::shdr *shdr = &hydra.shdrItems[whichSample];
+ zoneRegion.addForSF2(&presetRegion);
+ zoneRegion.sf2ToSFZ();
+ zoneRegion.offset += shdr->start;
+ zoneRegion.end += shdr->end;
+ zoneRegion.loopStart += shdr->startLoop;
+ zoneRegion.loopEnd += shdr->endLoop;
+ if (shdr->endLoop > 0)
+ {
+ zoneRegion.loopEnd -= 1;
+ }
+ if (zoneRegion.pitchKeyCenter == -1)
+ {
+ zoneRegion.pitchKeyCenter = shdr->originalPitch;
+ }
+ zoneRegion.tune += shdr->pitchCorrection;
+
+ // Pin initialAttenuation to max +6dB.
+ if (zoneRegion.volume > 6.0)
+ {
+ zoneRegion.volume = 6.0;
+ this->sf2Sound.addUnsupportedOpcode("extreme gain in initialAttenuation");
+ }
+
+ // SoundFontRegion *newRegion = new SoundFontRegion();
+ auto newRegion = make();
+ *newRegion = zoneRegion;
+ newRegion->sample = this->sf2Sound.getSampleFor(shdr->sampleRate).get();
+ preset->addRegion(move(newRegion));
+ hadSampleID = true;
+ }
+ else
+ {
+ this->addGeneratorToRegion(igen->genOper, &igen->genAmount, &zoneRegion);
+ }
+ }
+
+ // Handle instrument's global zone.
+ if ((whichZone2 == firstZone) && !hadSampleID)
+ {
+ instRegion = zoneRegion;
+ }
+
+ // Modulators.
+ int modEnd = ibag[1].instModNdx;
+ int whichMod = ibag->instModNdx;
+ if (whichMod < modEnd)
+ {
+ this->sf2Sound.addUnsupportedOpcode("any modulator");
+ }
+ }
+ }
+ else
+ {
+ this->sf2Sound.addError("Instrument out of range.");
+ }
+ }
+ // Other generators.
+ else
+ {
+ addGeneratorToRegion(presetGenerator->genOper, &presetGenerator->genAmount, &presetRegion);
+ }
+ }
+
+ // Modulators.
+ int modEnd = presetZone[1].modNdx;
+ int whichMod = presetZone->modNdx;
+ if (whichMod < modEnd)
+ {
+ this->sf2Sound.addUnsupportedOpcode("any modulator");
+ }
+ }
+
+ this->sf2Sound.addPreset(move(preset));
+ }
+}
+
+SharedAudioSampleBuffer::Ptr SoundFont2Reader::readSamples()
+{
+ static const int bufferSize = 32768;
+
+ if (this->fileInputStream == nullptr)
+ {
+ this->sf2Sound.addError("Couldn't open file.");
+ return nullptr;
+ }
+
+ // Find the "sdta" chunk.
+ this->fileInputStream->setPosition(0);
+ RIFFChunk riffChunk;
+ riffChunk.readFrom(*this->fileInputStream);
+ bool found = false;
+ RIFFChunk chunk;
+
+ while (this->fileInputStream->getPosition() < riffChunk.end())
+ {
+ chunk.readFrom(*this->fileInputStream);
+ if (FourCCEquals(chunk.id, "sdta"))
+ {
+ found = true;
+ break;
+ }
+ chunk.seekAfter(*this->fileInputStream);
+ }
+
+ int64 sdtaEnd = chunk.end();
+ found = false;
+ while (this->fileInputStream->getPosition() < sdtaEnd)
+ {
+ chunk.readFrom(*this->fileInputStream);
+ if (FourCCEquals(chunk.id, "smpl"))
+ {
+ found = true;
+ break;
+ }
+ chunk.seekAfter(*this->fileInputStream);
+ }
+
+ if (!found)
+ {
+ this->sf2Sound.addError("SF2 is missing its \"smpl\" chunk.");
+ return nullptr;
+ }
+
+ const int numSamples = (int)chunk.size / sizeof(short);
+ SharedAudioSampleBuffer::Ptr sampleBuffer(new SharedAudioSampleBuffer(1, numSamples));
+
+ // Read and convert.
+ HeapBlock buffer(bufferSize);
+ int samplesLeft = numSamples;
+ float *out = sampleBuffer->getWritePointer(0);
+ while (samplesLeft > 0)
+ {
+ // Read <= 32768 bytes at a time from the buffer
+ int samplesToRead = bufferSize;
+ if (samplesToRead > samplesLeft)
+ {
+ samplesToRead = samplesLeft;
+ }
+
+ this->fileInputStream->read(buffer.getData(), samplesToRead * sizeof(short));
+
+ // Convert from signed 16-bit to float.
+ int samplesToConvert = samplesToRead;
+ short *in = buffer.getData();
+ for (; samplesToConvert > 0; --samplesToConvert)
+ {
+ // If we ever need to compile for big-endian platforms, we'll need to
+ // byte-swap here.
+ *out++ = *in++ / 32767.0f;
+ }
+
+ samplesLeft -= samplesToRead;
+ }
+
+ return sampleBuffer;
+}
+
+void SoundFont2Reader::addGeneratorToRegion(sf2word genOper, SF2::genAmountType *amount, SoundFontRegion *region)
+{
+ switch (genOper)
+ {
+ case SoundFont2Generator::startAddrsOffset:
+ region->offset += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::endAddrsOffset:
+ region->end += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::startloopAddrsOffset:
+ region->loopStart += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::endloopAddrsOffset:
+ region->loopEnd += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::startAddrsCoarseOffset:
+ region->offset += amount->shortAmount * 32768;
+ break;
+
+ case SoundFont2Generator::endAddrsCoarseOffset:
+ region->end += amount->shortAmount * 32768;
+ break;
+
+ case SoundFont2Generator::pan:
+ region->pan = amount->shortAmount * (2.0f / 10.0f);
+ break;
+
+ case SoundFont2Generator::delayVolEnv:
+ region->ampeg.delay = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::attackVolEnv:
+ region->ampeg.attack = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::holdVolEnv:
+ region->ampeg.hold = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::decayVolEnv:
+ region->ampeg.decay = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::sustainVolEnv:
+ region->ampeg.sustain = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::releaseVolEnv:
+ region->ampeg.release = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::keyRange:
+ region->lokey = amount->range.lo;
+ region->hikey = amount->range.hi;
+ break;
+
+ case SoundFont2Generator::velRange:
+ region->lovel = amount->range.lo;
+ region->hivel = amount->range.hi;
+ break;
+
+ case SoundFont2Generator::startloopAddrsCoarseOffset:
+ region->loopStart += amount->shortAmount * 32768;
+ break;
+
+ case SoundFont2Generator::initialAttenuation:
+ // The spec says "initialAttenuation" is in centibels. But everyone
+ // seems to treat it as millibels.
+ region->volume += -amount->shortAmount / 100.0f;
+ break;
+
+ case SoundFont2Generator::endloopAddrsCoarseOffset:
+ region->loopEnd += amount->shortAmount * 32768;
+ break;
+
+ case SoundFont2Generator::coarseTune:
+ region->transpose += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::fineTune:
+ region->tune += amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::sampleModes: {
+ SoundFontRegion::LoopMode loopModes[] = {
+ SoundFontRegion::LoopMode::noLoop,
+ SoundFontRegion::LoopMode::loopContinuous,
+ SoundFontRegion::LoopMode::noLoop,
+ SoundFontRegion::LoopMode::loopSustain};
+
+ region->loopMode = loopModes[amount->wordAmount & 0x03];
+ }
+ break;
+
+ case SoundFont2Generator::scaleTuning:
+ region->pitchKeyTrack = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::exclusiveClass:
+ region->offBy = amount->wordAmount;
+ region->group = static_cast(region->offBy);
+ break;
+
+ case SoundFont2Generator::overridingRootKey:
+ region->pitchKeyCenter = amount->shortAmount;
+ break;
+
+ case SoundFont2Generator::endOper:
+ // Ignore.
+ break;
+
+ case SoundFont2Generator::modLfoToPitch:
+ case SoundFont2Generator::vibLfoToPitch:
+ case SoundFont2Generator::modEnvToPitch:
+ case SoundFont2Generator::initialFilterFc:
+ case SoundFont2Generator::initialFilterQ:
+ case SoundFont2Generator::modLfoToFilterFc:
+ case SoundFont2Generator::modEnvToFilterFc:
+ case SoundFont2Generator::modLfoToVolume:
+ case SoundFont2Generator::unused1:
+ case SoundFont2Generator::chorusEffectsSend:
+ case SoundFont2Generator::reverbEffectsSend:
+ case SoundFont2Generator::unused2:
+ case SoundFont2Generator::unused3:
+ case SoundFont2Generator::unused4:
+ case SoundFont2Generator::delayModLFO:
+ case SoundFont2Generator::freqModLFO:
+ case SoundFont2Generator::delayVibLFO:
+ case SoundFont2Generator::freqVibLFO:
+ case SoundFont2Generator::delayModEnv:
+ case SoundFont2Generator::attackModEnv:
+ case SoundFont2Generator::holdModEnv:
+ case SoundFont2Generator::decayModEnv:
+ case SoundFont2Generator::sustainModEnv:
+ case SoundFont2Generator::releaseModEnv:
+ case SoundFont2Generator::keynumToModEnvHold:
+ case SoundFont2Generator::keynumToModEnvDecay:
+ case SoundFont2Generator::keynumToVolEnvHold:
+ case SoundFont2Generator::keynumToVolEnvDecay:
+ case SoundFont2Generator::instrument:
+ // Only allowed in certain places, where we already special-case it.
+ case SoundFont2Generator::reserved1:
+ case SoundFont2Generator::keynum:
+ case SoundFont2Generator::velocity:
+ case SoundFont2Generator::reserved2:
+ case SoundFont2Generator::sampleID:
+ // Only allowed in certain places, where we already special-case it.
+ case SoundFont2Generator::reserved3:
+ case SoundFont2Generator::unused5: {
+ const SoundFont2Generator *generator = GeneratorFor(static_cast(genOper));
+ this->sf2Sound.addUnsupportedOpcode(generator->name);
+ }
+ break;
+ }
+}
+
+//===----------------------------------------------------------------------===//
+// SoundFont2Sound
+//===----------------------------------------------------------------------===//
+
+SoundFont2Sound::SoundFont2Sound(const File &file) : SoundFontSound(file) {}
+
+SoundFont2Sound::~SoundFont2Sound() = default;
+
+class PresetComparator final
+{
+public:
+
+ static int compareElements(const SoundFont2Sound::Preset *first, const SoundFont2Sound::Preset *second)
+ {
+ const auto bankDiff = first->bank - second->bank;
+ if (bankDiff != 0)
+ {
+ return bankDiff;
+ }
+
+ return first->preset - second->preset;
+ }
+};
+
+void SoundFont2Sound::loadRegions()
+{
+ SoundFont2Reader reader(*this, this->file);
+ reader.read();
+
+ PresetComparator comparator;
+ this->presets.sort(comparator);
+
+ this->setSelectedPreset(0);
+}
+
+void SoundFont2Sound::loadSamples(AudioFormatManager &formatManager)
+{
+ /*
+ each SoundFont2Sound is given a File as the source of the actual sample data when they're created
+ this reader adds any errors encountered while reading to the SoundFont2Sound object
+ */
+ SoundFont2Reader reader(*this, this->file);
+ const auto buffer = reader.readSamples();
+
+ if (buffer)
+ {
+ // All the SFZSamples will share the buffer.
+ for (auto &sample : this->samplesByRate)
+ {
+ sample.second->setBuffer(buffer);
+ }
+ }
+}
+
+void SoundFont2Sound::addPreset(UniquePointer &&preset)
+{
+ this->presets.add(preset.release());
+}
+
+int SoundFont2Sound::getNumPresets() const
+{
+ return this->presets.size();
+}
+
+String SoundFont2Sound::getPresetName(int whichSubsound) const
+{
+ Preset *preset = this->presets[whichSubsound];
+ String result;
+
+ if (preset->bank != 0)
+ {
+ result += preset->bank;
+ result += "/";
+ }
+ result += preset->preset;
+ result += ": ";
+ result += preset->name;
+ return result;
+}
+
+void SoundFont2Sound::setSelectedPreset(int whichPreset)
+{
+ this->selectedPreset = whichPreset;
+ this->regions.clear();
+ this->regions.addArray(this->presets[whichPreset]->regions);
+}
+
+int SoundFont2Sound::getSelectedPreset() const
+{
+ return this->selectedPreset;
+}
+
+WeakReference SoundFont2Sound::getSampleFor(double sampleRate)
+{
+ if (!this->samplesByRate.contains(int(sampleRate)))
+ {
+ this->samplesByRate[int(sampleRate)] = make(sampleRate);
+ }
+
+ return this->samplesByRate[int(sampleRate)].get();
+}
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.h b/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.h
new file mode 100644
index 00000000..0d9ea64a
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFont2Sound.h
@@ -0,0 +1,55 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#pragma once
+
+#include "SoundFontSound.h"
+
+class SoundFont2Sound final : public SoundFontSound
+{
+public:
+
+ explicit SoundFont2Sound(const File &file);
+ ~SoundFont2Sound() override;
+
+ void loadRegions() override;
+ void loadSamples(AudioFormatManager &formatManager) override;
+
+ int getNumPresets() const override;
+ String getPresetName(int whichPreset) const override;
+ void setSelectedPreset(int whichPreset) override;
+ int getSelectedPreset() const override;
+
+ WeakReference getSampleFor(double sampleRate);
+
+private:
+
+ friend class SoundFont2Reader;
+ void addPreset(UniquePointer &&preset);
+
+ OwnedArray presets;
+
+ FlatHashMap> samplesByRate;
+
+ int selectedPreset = 0;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFont2Sound)
+};
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontRegion.h b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontRegion.h
new file mode 100644
index 00000000..d860b1a5
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontRegion.h
@@ -0,0 +1,212 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#pragma once
+
+#include "SoundFontSample.h"
+
+// SoundFontRegion is designed to be able to be bitwise-copied.
+
+struct EGParameters final
+{
+ float delay = 0.f;
+ float start = 0.f;
+ float attack = 0.f;
+ float hold = 0.f;
+ float decay = 0.f;
+ float sustain = 0.f;
+ float release = 0.f;
+
+ void clear()
+ {
+ this->delay = this->start = this->attack = this->hold = this->decay = this->release = 0.0;
+ this->sustain = 100.0;
+ }
+
+ void clearMod()
+ {
+ // Clear for velocity or other modification.
+ this->delay = this->start = this->attack = this->hold = this->decay = this->sustain = this->release = 0.0;
+ }
+};
+
+struct SoundFontRegion final
+{
+ enum class Trigger
+ {
+ attack,
+ release,
+ first,
+ legato
+ };
+
+ enum class LoopMode
+ {
+ sampleLoop,
+ noLoop,
+ oneShot,
+ loopContinuous,
+ loopSustain
+ };
+
+ enum class OffMode
+ {
+ fast,
+ normal
+ };
+
+ SoundFontRegion()
+ {
+ this->clear();
+ }
+
+ void clear()
+ {
+ memset(this, 0, sizeof(*this));
+ this->hikey = 127;
+ this->hivel = 127;
+ this->pitchKeyCenter = 60; // C4
+ this->pitchKeyTrack = 100;
+ this->bendUp = 200;
+ this->bendDown = -200;
+ this->volume = this->pan = 0.0;
+ this->ampVelTrack = 100.0;
+ this->ampeg.clear();
+ this->ampegVelTrack.clearMod();
+ }
+
+ void clearForSF2()
+ {
+ this->clear();
+ this->pitchKeyCenter = -1;
+ this->loopMode = LoopMode::noLoop;
+
+ // SF2 defaults in timecents.
+ this->ampeg.delay = -12000.0;
+ this->ampeg.attack = -12000.0;
+ this->ampeg.hold = -12000.0;
+ this->ampeg.decay = -12000.0;
+ this->ampeg.sustain = 0.0;
+ this->ampeg.release = -12000.0;
+ }
+
+ void clearForRelativeSF2()
+ {
+ this->clear();
+ this->pitchKeyTrack = 0;
+ this->ampVelTrack = 0.0;
+ this->ampeg.sustain = 0.0;
+ }
+
+ void addForSF2(SoundFontRegion *other)
+ {
+ this->offset += other->offset;
+ this->end += other->end;
+ this->loopStart += other->loopStart;
+ this->loopEnd += other->loopEnd;
+ this->transpose += other->transpose;
+ this->tune += other->tune;
+ this->pitchKeyTrack += other->pitchKeyTrack;
+ this->volume += other->volume;
+ this->pan += other->pan;
+
+ this->ampeg.delay += other->ampeg.delay;
+ this->ampeg.attack += other->ampeg.attack;
+ this->ampeg.hold += other->ampeg.hold;
+ this->ampeg.decay += other->ampeg.decay;
+ this->ampeg.sustain += other->ampeg.sustain;
+ this->ampeg.release += other->ampeg.release;
+ }
+
+ void sf2ToSFZ()
+ {
+ const auto timeCents2Secs = [](int timeCents)
+ {
+ return static_cast(pow(2.0, timeCents / 1200.0));
+ };
+
+ // EG times need to be converted from timecents to seconds.
+ this->ampeg.delay = timeCents2Secs(int(this->ampeg.delay));
+ this->ampeg.attack = timeCents2Secs(int(this->ampeg.attack));
+ this->ampeg.hold = timeCents2Secs(int(this->ampeg.hold));
+ this->ampeg.decay = timeCents2Secs(int(this->ampeg.decay));
+ if (this->ampeg.sustain < 0.0f)
+ {
+ this->ampeg.sustain = 0.0f;
+ }
+ this->ampeg.sustain = 100.0f * Decibels::decibelsToGain(-this->ampeg.sustain / 10.0f);
+ this->ampeg.release = timeCents2Secs(int(this->ampeg.release));
+
+ // Pin very short EG segments. Timecents don't get to zero, and our EG is
+ // happier with zero values.
+ this->ampeg.delay = jmax(this->ampeg.delay, 0.01f);
+ this->ampeg.attack = jmax(this->ampeg.attack, 0.01f);
+ this->ampeg.hold = jmax(this->ampeg.hold, 0.01f);
+ this->ampeg.decay = jmax(this->ampeg.decay, 0.01f);
+ this->ampeg.release = jmax(this->ampeg.release, 0.01f);
+
+ // Pin values to their ranges.
+ this->pan = jlimit(-100.f, 100.f, this->pan);
+ }
+
+ String dump()
+ {
+ String info = String::formatted("%d - %d, vel %d - %d", lokey, hikey, lovel, hivel);
+ if (sample)
+ {
+ info << sample->getShortName();
+ }
+ info << "\n";
+ return info;
+ }
+
+ bool matches(int note, int velocity, Trigger trig) const
+ {
+ return (note >= this->lokey && note <= this->hikey && velocity >= this->lovel && velocity <= this->hivel &&
+ (trig == this->trigger || (this->trigger == Trigger::attack && (trig == Trigger::first || trig == Trigger::legato))));
+ }
+
+ WeakReference sample;
+
+ int lokey, hikey;
+ int lovel, hivel;
+ Trigger trigger;
+ int group;
+ int64 offBy;
+ OffMode offMode;
+
+ int64 offset;
+ int64 end;
+ bool negativeEnd;
+ LoopMode loopMode;
+ int64 loopStart, loopEnd;
+ int transpose;
+ int tune;
+ int pitchKeyCenter, pitchKeyTrack;
+ int bendUp, bendDown;
+
+ float volume, pan;
+ float ampVelTrack;
+
+ EGParameters ampeg, ampegVelTrack;
+
+ JUCE_LEAK_DETECTOR(SoundFontRegion)
+};
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSample.h b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSample.h
new file mode 100644
index 00000000..46a952af
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSample.h
@@ -0,0 +1,111 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#pragma once
+
+class SharedAudioSampleBuffer final : public ReferenceCountedObject, public AudioSampleBuffer
+{
+public:
+
+ using Ptr = ReferenceCountedObjectPtr;
+
+ explicit SharedAudioSampleBuffer(int numChannels, int numSamples) :
+ AudioSampleBuffer(numChannels, numSamples) {}
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SharedAudioSampleBuffer)
+};
+
+class SoundFontSample final
+{
+public:
+
+ explicit SoundFontSample(const File &fileIn) :
+ file(fileIn) {}
+
+ explicit SoundFontSample(double sampleRateIn) :
+ sampleRate(sampleRateIn) {}
+
+ File getFile() const noexcept { return this->file; }
+ String getShortName() const noexcept { return this->file.getFileName(); }
+
+ const AudioSampleBuffer *getBuffer() const noexcept { return this->buffer.get(); }
+ void setBuffer(SharedAudioSampleBuffer::Ptr newBuffer)
+ {
+ this->buffer = newBuffer;
+ if (this->buffer != nullptr)
+ {
+ this->sampleLength = this->buffer->getNumSamples();
+ }
+ else
+ {
+ this->sampleLength = 0;
+ }
+ }
+
+ double getSampleRate() const noexcept { return this->sampleRate; }
+ uint64 getSampleLength() const noexcept { return this->sampleLength; }
+ uint64 getLoopStart() const noexcept { return this->loopStart; }
+ uint64 getLoopEnd() const noexcept { return this->loopEnd; }
+
+ bool load(AudioFormatManager &formatManager)
+ {
+ UniquePointer reader(formatManager.createReaderFor(this->file));
+ if (reader == nullptr)
+ {
+ return false;
+ }
+
+ this->sampleRate = reader->sampleRate;
+ this->sampleLength = reader->lengthInSamples;
+
+ // Read some extra samples, which will be filled with zeros, so interpolation
+ // can be done without having to check for the edge all the time.
+ jassert(this->sampleLength < std::numeric_limits::max());
+
+ this->buffer = new SharedAudioSampleBuffer(reader->numChannels, static_cast(this->sampleLength + 4));
+ reader->read(this->buffer.get(), 0, static_cast(this->sampleLength + 4), 0, true, true);
+
+ const auto *metadata = &reader->metadataValues;
+ const int numLoops = metadata->getValue("NumSampleLoops", "0").getIntValue();
+ if (numLoops > 0)
+ {
+ this->loopStart = metadata->getValue("Loop0Start", "0").getLargeIntValue();
+ this->loopEnd = metadata->getValue("Loop0End", "0").getLargeIntValue();
+ }
+
+ return true;
+ }
+
+private:
+
+ File file;
+
+ // all samples share the single buffer:
+ SharedAudioSampleBuffer::Ptr buffer;
+
+ double sampleRate = 0.0;
+ uint64 sampleLength = 0;
+ uint64 loopStart = 0;
+ uint64 loopEnd = 0;
+
+ JUCE_DECLARE_WEAK_REFERENCEABLE(SoundFontSample)
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontSample)
+};
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.cpp b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.cpp
new file mode 100644
index 00000000..c204ced9
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.cpp
@@ -0,0 +1,821 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#include "Common.h"
+#include "SoundFontSound.h"
+#include "SoundFontRegion.h"
+#include "SoundFontSample.h"
+
+class SoundFontReader final
+{
+public:
+
+ explicit SoundFontReader(SoundFontSound *sound) : sound(sound) {}
+ ~SoundFontReader() = default;
+
+ void read(const File &file);
+ void read(const char *text, unsigned int length);
+
+private:
+
+ const char *handleLineEnd(const char *p);
+ const char *readPathInto(String *pathOut, const char *p, const char *end);
+ int parseKeyValue(const String &str);
+ void finishRegion(SoundFontRegion ®ionToCopyFrom);
+ void addError(const String &message);
+
+ static SoundFontRegion::Trigger parseTriggerValue(const String &str);
+ static SoundFontRegion::LoopMode parseLoopModeValue(const String &str);
+
+ SoundFontSound *sound = nullptr;
+ int line = 1;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontReader)
+};
+
+class StringSlice final
+{
+public:
+
+ StringSlice(const char *startIn, const char *endIn) : start(startIn), end(endIn) {}
+
+ unsigned int length() { return static_cast(this->end - this->start); }
+ bool operator==(const char *other) { return (strncmp(this->start, other, length()) == 0); }
+ bool operator!=(const char *other) { return (strncmp(this->start, other, length()) != 0); }
+ const char *getStart() const { return this->start; }
+ const char *getEnd() const { return this->end; }
+
+private:
+
+ const char *start = nullptr;
+ const char *end = nullptr;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StringSlice)
+};
+
+void SoundFontReader::read(const File &file)
+{
+ MemoryBlock contents;
+
+ if (!file.loadFileAsData(contents))
+ {
+ this->sound->addError("Couldn't read \"" + file.getFullPathName() + "\"");
+ return;
+ }
+
+ this->read(static_cast(contents.getData()), static_cast(contents.getSize()));
+}
+
+void SoundFontReader::read(const char *text, unsigned int length)
+{
+ const char *p = text;
+ const char *end = text + length;
+ char c = 0;
+
+ SoundFontRegion currentGroup;
+ SoundFontRegion currentRegion;
+ SoundFontRegion *buildingRegion = nullptr;
+ bool inControl = false;
+ String defaultPath;
+
+ while (p < end)
+ {
+ // We're at the start of a line; skip any whitespace.
+ while (p < end)
+ {
+ c = *p;
+ if ((c != ' ') && (c != '\t'))
+ {
+ break;
+ }
+ p += 1;
+ }
+ if (p >= end)
+ {
+ break;
+ }
+
+ // Check if it's a comment line.
+ if (c == '/')
+ {
+ // Skip to end of line.
+ while (p < end)
+ {
+ c = *++p;
+ if ((c == '\n') || (c == '\r'))
+ {
+ break;
+ }
+ }
+ p = this->handleLineEnd(p);
+ continue;
+ }
+
+ // Check if it's a blank line.
+ if ((c == '\r') || (c == '\n'))
+ {
+ p = this->handleLineEnd(p);
+ continue;
+ }
+
+ // Handle elements on the line.
+ while (p < end)
+ {
+ c = *p;
+
+ // Tag.
+ if (c == '<')
+ {
+ p += 1;
+ const char *tagStart = p;
+ while (p < end)
+ {
+ c = *p++;
+ if ((c == '\n') || (c == '\r'))
+ {
+ this->addError("Unterminated tag");
+ goto fatalError;
+ }
+ else if (c == '>')
+ {
+ break;
+ }
+ }
+ if (p >= end)
+ {
+ this->addError("Unterminated tag");
+ goto fatalError;
+ }
+ StringSlice tag(tagStart, p - 1);
+ if (tag == "region")
+ {
+ if (buildingRegion && (buildingRegion == ¤tRegion))
+ {
+ this->finishRegion(currentRegion);
+ }
+ currentRegion = currentGroup;
+ buildingRegion = ¤tRegion;
+ inControl = false;
+ }
+ else if (tag == "group")
+ {
+ if (buildingRegion && (buildingRegion == ¤tRegion))
+ {
+ this->finishRegion(currentRegion);
+ }
+ currentGroup.clear();
+ buildingRegion = ¤tGroup;
+ inControl = false;
+ }
+ else if (tag == "control")
+ {
+ if (buildingRegion && (buildingRegion == ¤tRegion))
+ {
+ this->finishRegion(currentRegion);
+ }
+ currentGroup.clear();
+ buildingRegion = nullptr;
+ inControl = true;
+ }
+ else
+ {
+ this->addError("Illegal tag");
+ }
+ }
+ // Comment.
+ else if (c == '/')
+ {
+ // Skip to end of line.
+ while (p < end)
+ {
+ c = *p;
+ if ((c == '\r') || (c == '\n'))
+ {
+ break;
+ }
+ p += 1;
+ }
+ }
+ // Parameter.
+ else
+ {
+ // Get the parameter name.
+ const char *parameterStart = p;
+ while (p < end)
+ {
+ c = *p++;
+ if ((c == '=') || (c == ' ') || (c == '\t') || (c == '\r') || (c == '\n'))
+ {
+ break;
+ }
+ }
+ if ((p >= end) || (c != '='))
+ {
+ this->addError("Malformed parameter");
+ goto nextElement;
+ }
+ StringSlice opcode(parameterStart, p - 1);
+ if (inControl)
+ {
+ if (opcode == "default_path")
+ {
+ p = this->readPathInto(&defaultPath, p, end);
+ }
+ else
+ {
+ const char *valueStart = p;
+ while (p < end)
+ {
+ c = *p;
+ if ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r'))
+ {
+ break;
+ }
+ p++;
+ }
+ String value(valueStart, p - valueStart);
+ String fauxOpcode = String(opcode.getStart(), opcode.length()) + " (in )";
+ this->sound->addUnsupportedOpcode(fauxOpcode);
+ }
+ }
+ else if (opcode == "sample")
+ {
+ String path;
+ p = this->readPathInto(&path, p, end);
+ if (!path.isEmpty())
+ {
+ if (buildingRegion)
+ {
+ buildingRegion->sample = this->sound->addSample(path, defaultPath);
+ }
+ else
+ {
+ this->addError("Adding sample outside a group or region");
+ }
+ }
+ else
+ {
+ this->addError("Empty sample path");
+ }
+ }
+ else
+ {
+ const char *valueStart = p;
+ while (p < end)
+ {
+ c = *p;
+ if ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r'))
+ {
+ break;
+ }
+ p++;
+ }
+ String value(valueStart, p - valueStart);
+ if (buildingRegion == nullptr)
+ {
+ this->addError("Setting a parameter outside a region or group");
+ }
+ else if (opcode == "lokey")
+ {
+ buildingRegion->lokey = this->parseKeyValue(value);
+ }
+ else if (opcode == "hikey")
+ {
+ buildingRegion->hikey = this->parseKeyValue(value);
+ }
+ else if (opcode == "key")
+ {
+ buildingRegion->hikey = buildingRegion->lokey = buildingRegion->pitchKeyCenter = parseKeyValue(value);
+ }
+ else if (opcode == "lovel")
+ {
+ buildingRegion->lovel = value.getIntValue();
+ }
+ else if (opcode == "hivel")
+ {
+ buildingRegion->hivel = value.getIntValue();
+ }
+ else if (opcode == "trigger")
+ {
+ buildingRegion->trigger = this->parseTriggerValue(value);
+ }
+ else if (opcode == "group")
+ {
+ buildingRegion->group = static_cast(value.getLargeIntValue());
+ }
+ else if (opcode == "off_by")
+ {
+ buildingRegion->offBy = value.getLargeIntValue();
+ }
+ else if (opcode == "offset")
+ {
+ buildingRegion->offset = value.getLargeIntValue();
+ }
+ else if (opcode == "end")
+ {
+ int64 end2 = value.getLargeIntValue();
+ if (end2 < 0)
+ {
+ buildingRegion->negativeEnd = true;
+ }
+ else
+ {
+ buildingRegion->end = end2;
+ }
+ }
+ else if (opcode == "loop_mode")
+ {
+ bool modeIsSupported = value == "no_loop" || value == "one_shot" || value == "loop_continuous";
+ if (modeIsSupported)
+ {
+ buildingRegion->loopMode = this->parseLoopModeValue(value);
+ }
+ else
+ {
+ const auto fauxOpcode = String(opcode.getStart(), opcode.length()) + "=" + value;
+ this->sound->addUnsupportedOpcode(fauxOpcode);
+ }
+ }
+ else if (opcode == "loop_start")
+ {
+ buildingRegion->loopStart = value.getLargeIntValue();
+ }
+ else if (opcode == "loop_end")
+ {
+ buildingRegion->loopEnd = value.getLargeIntValue();
+ }
+ else if (opcode == "transpose")
+ {
+ buildingRegion->transpose = value.getIntValue();
+ }
+ else if (opcode == "tune")
+ {
+ buildingRegion->tune = value.getIntValue();
+ }
+ else if (opcode == "pitch_keycenter")
+ {
+ buildingRegion->pitchKeyCenter = parseKeyValue(value);
+ }
+ else if (opcode == "pitch_keytrack")
+ {
+ buildingRegion->pitchKeyTrack = value.getIntValue();
+ }
+ else if (opcode == "bthis->endup")
+ {
+ buildingRegion->bendUp = value.getIntValue();
+ }
+ else if (opcode == "bthis->enddown")
+ {
+ buildingRegion->bendDown = value.getIntValue();
+ }
+ else if (opcode == "volume")
+ {
+ buildingRegion->volume = value.getFloatValue();
+ }
+ else if (opcode == "pan")
+ {
+ buildingRegion->pan = value.getFloatValue();
+ }
+ else if (opcode == "amp_veltrack")
+ {
+ buildingRegion->ampVelTrack = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_delay")
+ {
+ buildingRegion->ampeg.delay = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_start")
+ {
+ buildingRegion->ampeg.start = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_attack")
+ {
+ buildingRegion->ampeg.attack = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_hold")
+ {
+ buildingRegion->ampeg.hold = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_decay")
+ {
+ buildingRegion->ampeg.decay = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_sustain")
+ {
+ buildingRegion->ampeg.sustain = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_release")
+ {
+ buildingRegion->ampeg.release = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2delay")
+ {
+ buildingRegion->ampegVelTrack.delay = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2attack")
+ {
+ buildingRegion->ampegVelTrack.attack = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2hold")
+ {
+ buildingRegion->ampegVelTrack.hold = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2decay")
+ {
+ buildingRegion->ampegVelTrack.decay = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2sustain")
+ {
+ buildingRegion->ampegVelTrack.sustain = value.getFloatValue();
+ }
+ else if (opcode == "ampeg_vel2release")
+ {
+ buildingRegion->ampegVelTrack.release = value.getFloatValue();
+ }
+ else if (opcode == "default_path")
+ {
+ this->addError("\"default_path\" outside of tag");
+ }
+ else
+ {
+ this->sound->addUnsupportedOpcode(String(opcode.getStart(), opcode.length()));
+ }
+ }
+ }
+
+ // Skip to next element.
+ nextElement:
+ c = 0;
+ while (p < end)
+ {
+ c = *p;
+ if ((c != ' ') && (c != '\t'))
+ {
+ break;
+ }
+ p += 1;
+ }
+ if ((c == '\r') || (c == '\n'))
+ {
+ p = this->handleLineEnd(p);
+ break;
+ }
+ }
+ }
+
+fatalError:
+
+ if (buildingRegion && (buildingRegion == ¤tRegion))
+ {
+ this->finishRegion(currentRegion);
+ }
+}
+
+const char *SoundFontReader::handleLineEnd(const char *p)
+{
+ // Check for DOS-style line ending.
+ char lineEndChar = *p++;
+
+ if ((lineEndChar == '\r') && (*p == '\n'))
+ {
+ p += 1;
+ }
+
+ this->line += 1;
+ return p;
+}
+
+const char *SoundFontReader::readPathInto(String *pathOut, const char *pIn, const char *endIn)
+{
+ // Paths are kind of funny to parse because they can contain whitespace.
+ const char *p = pIn;
+ const char *end = endIn;
+ const char *pathStart = p;
+ const char *potentialEnd = nullptr;
+
+ while (p < end)
+ {
+ char c = *p;
+ if (c == ' ')
+ {
+ // Is this space part of the path? Or the start of the next opcode? We
+ // don't know yet.
+ potentialEnd = p;
+ p += 1;
+ // Skip any more spaces.
+ while (p < end && *p == ' ')
+ {
+ p += 1;
+ }
+ }
+ else if ((c == '\n') || (c == '\r') || (c == '\t'))
+ {
+ break;
+ }
+ else if (c == '=')
+ {
+ // We've been looking at an opcode; we need to rewind to
+ // potentialEnd.
+ p = potentialEnd;
+ break;
+ }
+ p += 1;
+ }
+ if (p > pathStart)
+ {
+ // Can't do this:
+ // String path(CharPointer_UTF8(pathStart), CharPointer_UTF8(p));
+ // It won't compile for some unfathomable reason.
+ CharPointer_UTF8 end2(p);
+ String path(CharPointer_UTF8(pathStart), end2);
+ *pathOut = path;
+ }
+ else
+ {
+ *pathOut = String();
+ }
+ return p;
+}
+
+int SoundFontReader::parseKeyValue(const String &str)
+{
+ auto chars = str.toRawUTF8();
+
+ char c = chars[0];
+
+ if ((c >= '0') && (c <= '9'))
+ {
+ return str.getIntValue();
+ }
+
+ int note = 0;
+ static const int notes[] = {
+ 12 + 0,
+ 12 + 2,
+ 3,
+ 5,
+ 7,
+ 8,
+ 10,
+ };
+
+ if ((c >= 'A') && (c <= 'G'))
+ {
+ note = notes[c - 'A'];
+ }
+ else if ((c >= 'a') && (c <= 'g'))
+ {
+ note = notes[c - 'a'];
+ }
+
+ int octaveStart = 1;
+
+ c = chars[1];
+ if ((c == 'b') || (c == '#'))
+ {
+ octaveStart += 1;
+ if (c == 'b')
+ {
+ note -= 1;
+ }
+ else
+ {
+ note += 1;
+ }
+ }
+
+ const int octave = str.substring(octaveStart).getIntValue();
+ // A3 == 57.
+ return octave * 12 + note + (57 - 4 * 12);
+}
+
+SoundFontRegion::Trigger SoundFontReader::parseTriggerValue(const String &str)
+{
+ if (str == "release")
+ {
+ return SoundFontRegion::Trigger::release;
+ }
+ if (str == "first")
+ {
+ return SoundFontRegion::Trigger::first;
+ }
+ if (str == "legato")
+ {
+ return SoundFontRegion::Trigger::legato;
+ }
+
+ return SoundFontRegion::Trigger::attack;
+}
+
+SoundFontRegion::LoopMode SoundFontReader::parseLoopModeValue(const String &str)
+{
+ if (str == "no_loop")
+ {
+ return SoundFontRegion::LoopMode::noLoop;
+ }
+ if (str == "one_shot")
+ {
+ return SoundFontRegion::LoopMode::oneShot;
+ }
+ if (str == "loop_continuous")
+ {
+ return SoundFontRegion::LoopMode::loopContinuous;
+ }
+ if (str == "loop_sustain")
+ {
+ return SoundFontRegion::LoopMode::loopSustain;
+ }
+
+ return SoundFontRegion::LoopMode::sampleLoop;
+}
+
+void SoundFontReader::finishRegion(SoundFontRegion ®ionToCopyFrom)
+{
+ auto newRegion = make();
+ *newRegion = regionToCopyFrom;
+ this->sound->addRegion(move(newRegion));
+}
+
+void SoundFontReader::addError(const String &message)
+{
+ const auto fullMessage = message + " (line " + String(this->line) + ").";
+ this->sound->addError(fullMessage);
+}
+
+//===----------------------------------------------------------------------===//
+// SoundFontSound
+//===----------------------------------------------------------------------===//
+
+SoundFontSound::SoundFontSound(const File &fileIn) : file(fileIn)
+{
+ this->preset = make();
+}
+
+SoundFontSound::~SoundFontSound() = default;
+
+bool SoundFontSound::appliesToNote(int /*midiNoteNumber*/)
+{
+ // Just say yes; we can't truly know unless we're told the velocity as well.
+ return true;
+}
+
+bool SoundFontSound::appliesToChannel(int /*midiChannel*/) { return true; }
+
+void SoundFontSound::addRegion(UniquePointer &®ion)
+{
+ this->regions.add(region.get());
+ this->preset->addRegion(move(region));
+}
+
+WeakReference SoundFontSound::addSample(String path, String defaultPath)
+{
+ path = path.replaceCharacter('\\', '/');
+ defaultPath = defaultPath.replaceCharacter('\\', '/');
+
+ File sampleFile;
+ if (defaultPath.isEmpty())
+ {
+ sampleFile = this->file.getSiblingFile(path);
+ }
+ else
+ {
+ const auto defaultDir = this->file.getSiblingFile(defaultPath);
+ sampleFile = defaultDir.getChildFile(path);
+ }
+
+ const auto samplePath = sampleFile.getFullPathName();
+ if (!this->samples.contains(samplePath))
+ {
+ this->samples[samplePath] = make(sampleFile);
+ }
+
+ return this->samples[samplePath].get();
+}
+
+void SoundFontSound::addError(const String &message) { this->errors.add(message); }
+
+void SoundFontSound::addUnsupportedOpcode(const String &opcode)
+{
+ if (!this->unsupportedOpcodes.contains(opcode))
+ {
+ this->unsupportedOpcodes[opcode] = opcode;
+ String warning = "unsupported opcode: ";
+ warning << opcode;
+ this->warnings.add(warning);
+ }
+}
+
+void SoundFontSound::loadRegions()
+{
+ SoundFontReader reader(this);
+ reader.read(this->file);
+}
+
+void SoundFontSound::loadSamples(AudioFormatManager &formatManager)
+{
+ for (auto &it : this->samples)
+ {
+ const bool ok = it.second->load(formatManager);
+ if (!ok)
+ {
+ this->addError("Couldn't load sample \"" + it.second->getShortName() + "\"");
+ }
+ }
+}
+
+SoundFontRegion *SoundFontSound::getRegionFor(int note, int velocity, SoundFontRegion::Trigger trigger) const
+{
+ for (auto *region : this->regions)
+ {
+ if (region->matches(note, velocity, trigger))
+ {
+ return region;
+ }
+ }
+
+ return nullptr;
+}
+
+int SoundFontSound::getNumRegions() const { return this->regions.size(); }
+
+SoundFontRegion *SoundFontSound::regionAt(int index) { return this->regions[index]; }
+
+int SoundFontSound::getNumPresets() const { return 1; }
+
+String SoundFontSound::getPresetName(int) const { return this->preset->name; }
+
+void SoundFontSound::setSelectedPreset(int) {}
+
+int SoundFontSound::getSelectedPreset() const { return 0; }
+
+String SoundFontSound::dump()
+{
+ String info;
+ auto &errors = this->getErrors();
+ if (errors.size() > 0)
+ {
+ info << errors.size() << " errors: \n";
+ info << errors.joinIntoString("\n");
+ info << "\n";
+ }
+ else
+ {
+ info << "no errors.\n\n";
+ }
+
+ auto &warnings = this->getWarnings();
+ if (warnings.size() > 0)
+ {
+ info << warnings.size() << " warnings: \n";
+ info << warnings.joinIntoString("\n");
+ }
+ else
+ {
+ info << "no warnings.\n";
+ }
+
+ if (this->regions.size() > 0)
+ {
+ info << this->regions.size() << " regions: \n";
+ for (int i = 0; i < this->regions.size(); ++i)
+ {
+ info << this->regions[i]->dump();
+ }
+ }
+ else
+ {
+ info << "no regions.\n";
+ }
+
+ if (this->samples.size() > 0)
+ {
+ info << this->samples.size() << " samples: \n";
+ for (const auto &it : this->samples)
+ {
+ info << it.first << "\n";
+ }
+ }
+ else
+ {
+ info << "no samples.\n";
+ }
+ return info;
+}
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.h b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.h
new file mode 100644
index 00000000..7b460b40
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSound.h
@@ -0,0 +1,102 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#pragma once
+
+#include "SoundFontRegion.h"
+
+class SoundFontSample;
+
+class SoundFontSound : public SynthesiserSound
+{
+public:
+
+ explicit SoundFontSound(const File &file);
+ ~SoundFontSound() override;
+
+ using Ptr = ReferenceCountedObjectPtr;
+
+ bool appliesToNote(int midiNoteNumber) override;
+ bool appliesToChannel(int midiChannel) override;
+
+ virtual void loadRegions();
+ virtual void loadSamples(AudioFormatManager &formatManager);
+
+ SoundFontRegion *getRegionFor(int note, int velocity,
+ SoundFontRegion::Trigger trigger = SoundFontRegion::Trigger::attack) const;
+
+ int getNumRegions() const;
+ SoundFontRegion *regionAt(int index);
+
+ const StringArray &getErrors() const { return this->errors; }
+ const StringArray &getWarnings() const { return this->warnings; }
+
+ virtual int getNumPresets() const;
+ virtual String getPresetName(int whichSubsound) const;
+ virtual void setSelectedPreset(int whichSubsound);
+ virtual int getSelectedPreset() const;
+
+ void addError(const String &message);
+ void addUnsupportedOpcode(const String &opcode);
+ String dump();
+
+ struct Preset final
+ {
+ const String name;
+ const int bank = 0;
+ const int preset = 0;
+
+ OwnedArray regions;
+
+ Preset() = default;
+ Preset(String nameIn, int bankIn, int presetIn) :
+ name(nameIn), bank(bankIn), preset(presetIn) {}
+
+ void addRegion(UniquePointer &®ion)
+ {
+ this->regions.add(move(region));
+ }
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Preset)
+ };
+
+protected:
+
+ File file;
+
+ Array regions;
+
+private:
+
+ friend class SoundFontReader;
+ void addRegion(UniquePointer &®ion);
+ WeakReference addSample(String path, String defaultPath = String());
+
+ UniquePointer preset; // a single virtual "preset" to own the regions
+
+ FlatHashMap> samples;
+
+ StringArray errors;
+ StringArray warnings;
+ FlatHashMap unsupportedOpcodes;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontSound)
+};
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.cpp b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.cpp
new file mode 100644
index 00000000..7b749aa9
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.cpp
@@ -0,0 +1,839 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#include "Common.h"
+#include "SoundFontSynth.h"
+#include "SoundFontSound.h"
+#include "SoundFontRegion.h"
+#include "SoundFontSample.h"
+#include "SerializationKeys.h"
+//#include
+
+class SoundFontEnvelope final
+{
+public:
+
+ SoundFontEnvelope() = default;
+
+ void setExponentialDecay(bool newExponentialDecay);
+ void startNote(const EGParameters *parameters,
+ float floatVelocity, double sampleRate,
+ const EGParameters *velMod = nullptr);
+
+ void nextSegment();
+ void noteOff();
+ void fastRelease();
+ bool isDone() const { return (this->segment == Segment::Done); }
+ bool isReleasing() const { return (this->segment == Segment::Release); }
+ int segmentIndex() const { return static_cast(this->segment); }
+ float getLevel() const { return this->level; }
+ void setLevel(float v) { this->level = v; }
+ float getSlope() const { return this->slope; }
+ void setSlope(float v) { this->slope = v; }
+ int getSamplesUntilNextSegment() const { return this->samplesUntilNextSegment; }
+ void setSamplesUntilNextSegment(int v) { this->samplesUntilNextSegment = v; }
+ bool getSegmentIsExponential() const { return this->segmentIsExponential; }
+ void setSegmentIsExponential(bool v) { this->segmentIsExponential = v; }
+
+private:
+
+ enum class Segment
+ {
+ Delay,
+ Attack,
+ Hold,
+ Decay,
+ Sustain,
+ Release,
+ Done
+ };
+
+ void startDelay();
+ void startAttack();
+ void startHold();
+ void startDecay();
+ void startSustain();
+ void startRelease();
+
+ Segment segment = Segment::Done;
+ EGParameters parameters;
+ double sampleRate = 0.0;
+ bool exponentialDecay = false;
+ float level = 0.f;
+ float slope = 0.f;
+ int samplesUntilNextSegment = 0;
+ bool segmentIsExponential = false;
+ static const float BottomLevel;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontEnvelope)
+};
+
+static const float fastReleaseTime = 0.01f;
+
+void SoundFontEnvelope::setExponentialDecay(bool newExponentialDecay)
+{
+ this->exponentialDecay = newExponentialDecay;
+}
+
+void SoundFontEnvelope::startNote(const EGParameters *newParameters, float floatVelocity, double newSampleRate,
+ const EGParameters *velMod)
+{
+ this->parameters = *newParameters;
+ if (velMod)
+ {
+ this->parameters.delay += floatVelocity * velMod->delay;
+ this->parameters.attack += floatVelocity * velMod->attack;
+ this->parameters.hold += floatVelocity * velMod->hold;
+ this->parameters.decay += floatVelocity * velMod->decay;
+ this->parameters.sustain += floatVelocity * velMod->sustain;
+ if (this->parameters.sustain < 0.0)
+ {
+ this->parameters.sustain = 0.0;
+ }
+ else if (this->parameters.sustain > 100.0)
+ {
+ this->parameters.sustain = 100.0;
+ }
+ this->parameters.release += floatVelocity * velMod->release;
+ }
+
+ this->sampleRate = newSampleRate;
+ this->startDelay();
+}
+
+void SoundFontEnvelope::nextSegment()
+{
+ switch (this->segment)
+ {
+ case Segment::Delay:
+ this->startAttack();
+ break;
+ case Segment::Attack:
+ this->startHold();
+ break;
+ case Segment::Hold:
+ this->startDecay();
+ break;
+ case Segment::Decay:
+ this->startSustain();
+ break;
+ case Segment::Sustain:
+ jassertfalse;
+ break;
+ case Segment::Release:
+ default:
+ this->segment = Segment::Done;
+ break;
+ }
+}
+
+void SoundFontEnvelope::noteOff()
+{
+ this->startRelease();
+}
+
+void SoundFontEnvelope::fastRelease()
+{
+ this->segment = Segment::Release;
+ this->samplesUntilNextSegment = int(fastReleaseTime * this->sampleRate);
+ this->slope = -this->level / this->samplesUntilNextSegment;
+ this->segmentIsExponential = false;
+}
+
+void SoundFontEnvelope::startDelay()
+{
+ if (this->parameters.delay <= 0)
+ {
+ this->startAttack();
+ }
+ else
+ {
+ this->segment = Segment::Delay;
+ this->level = 0.0;
+ this->slope = 0.0;
+ this->samplesUntilNextSegment = int(this->parameters.delay * this->sampleRate);
+ this->segmentIsExponential = false;
+ }
+}
+
+void SoundFontEnvelope::startAttack()
+{
+ if (this->parameters.attack <= 0)
+ {
+ this->startHold();
+ }
+ else
+ {
+ this->segment = Segment::Attack;
+ this->level = this->parameters.start / 100.0f;
+ this->samplesUntilNextSegment = static_cast(this->parameters.attack * this->sampleRate);
+ this->slope = 1.0f / this->samplesUntilNextSegment;
+ this->segmentIsExponential = false;
+ }
+}
+
+void SoundFontEnvelope::startHold()
+{
+ if (this->parameters.hold <= 0)
+ {
+ this->level = 1.0;
+ this->startDecay();
+ }
+ else
+ {
+ this->segment = Segment::Hold;
+ this->samplesUntilNextSegment = static_cast(this->parameters.hold * this->sampleRate);
+ this->level = 1.0;
+ this->slope = 0.0;
+ this->segmentIsExponential = false;
+ }
+}
+
+void SoundFontEnvelope::startDecay()
+{
+ if (this->parameters.decay <= 0)
+ {
+ this->startSustain();
+ }
+ else
+ {
+ this->segment = Segment::Decay;
+ this->samplesUntilNextSegment = static_cast(this->parameters.decay * this->sampleRate);
+ this->level = 1.0;
+ if (this->exponentialDecay)
+ {
+ // I don't truly understand this; just following what LinuxSampler does.
+ float mysterySlope = -9.226f / this->samplesUntilNextSegment;
+ this->slope = exp(mysterySlope);
+ this->segmentIsExponential = true;
+ if (this->parameters.sustain > 0.0)
+ {
+ // Again, this is following LinuxSampler's example, which is similar to
+ // SF2-style decay, where "decay" specifies the time it would take to
+ // get to zero, not to the sustain level. The SFZ spec is not that
+ // specific about what "decay" means, so perhaps it's really supposed
+ // to specify the time to reach the sustain level.
+ this->samplesUntilNextSegment = static_cast(log((this->parameters.sustain / 100.0) / this->level) / mysterySlope);
+ if (this->samplesUntilNextSegment <= 0)
+ {
+ this->startSustain();
+ }
+ }
+ }
+ else
+ {
+ this->slope = (this->parameters.sustain / 100.0f - 1.0f) / this->samplesUntilNextSegment;
+ this->segmentIsExponential = false;
+ }
+ }
+}
+
+void SoundFontEnvelope::startSustain()
+{
+ if (this->parameters.sustain <= 0)
+ {
+ this->startRelease();
+ }
+ else
+ {
+ this->segment = Segment::Sustain;
+ this->level = this->parameters.sustain / 100.0f;
+ this->slope = 0.0;
+ this->samplesUntilNextSegment = 0x7FFFFFFF;
+ this->segmentIsExponential = false;
+ }
+}
+
+void SoundFontEnvelope::startRelease()
+{
+ float release = this->parameters.release;
+ if (release <= 0)
+ {
+ // Enforce a short release, to prevent clicks.
+ release = fastReleaseTime;
+ }
+
+ this->segment = Segment::Release;
+ this->samplesUntilNextSegment = static_cast(release * this->sampleRate);
+ if (this->exponentialDecay)
+ {
+ // I don't truly understand this; just following what LinuxSampler does.
+ const float mysterySlope = -9.226f / this->samplesUntilNextSegment;
+ this->slope = exp(mysterySlope);
+ this->segmentIsExponential = true;
+ }
+ else
+ {
+ this->slope = -this->level / this->samplesUntilNextSegment;
+ this->segmentIsExponential = false;
+ }
+}
+
+const float SoundFontEnvelope::BottomLevel = 0.001f;
+
+//===----------------------------------------------------------------------===//
+// SoundFontVoice
+//===----------------------------------------------------------------------===//
+
+class SoundFontVoice final : public SynthesiserVoice
+{
+public:
+
+ SoundFontVoice()
+ {
+ this->envelope.setExponentialDecay(true);
+ }
+
+ bool canPlaySound(SynthesiserSound *sound) override;
+ void startNote(int midiNoteNumber, float velocity, SynthesiserSound *sound, int currentPitchWheelPosition) override;
+ void stopNote(float velocity, bool allowTailOff) override;
+ void stopNoteForGroup();
+ void stopNoteQuick();
+ void pitchWheelMoved(int newValue) override;
+ void controllerMoved(int controllerNumber, int newValue) override {}
+ void renderNextBlock(AudioSampleBuffer &outputBuffer, int startSample, int numSamples) override;
+ bool isPlayingNoteDown();
+ bool isPlayingOneShot();
+
+ int getGroup();
+ uint64 getOffBy();
+
+ // Set the region to be used by the next startNote().
+ void setRegion(SoundFontRegion *nextRegion);
+
+ String infoString();
+
+private:
+
+ SoundFontRegion *region = nullptr;
+ int trigger = 0;
+ int currentMidiNote = 0;
+ int currentPitchWheel = 0;
+ double pitchRatio = 0.0;
+ float noteGainLeft = 0.f;
+ float noteGainRight = 0.f;
+ double sourceSamplePosition = 0.0;
+ SoundFontEnvelope envelope;
+ int64 sampleEnd = 0;
+ int64 loopStart = 0;
+ int64 loopEnd = 0;
+
+ // Info only.
+ int numLoops = 0;
+ int currentVelocity = 0;
+
+ void calcPitchRatio();
+ void killNote();
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontVoice)
+};
+
+static const float globalGain = -1.0;
+
+bool SoundFontVoice::canPlaySound(SynthesiserSound *sound)
+{
+ return dynamic_cast(sound) != nullptr;
+}
+
+void SoundFontVoice::startNote(int midiNoteNumber, float floatVelocity, SynthesiserSound *soundIn,
+ int currentPitchWheelPosition)
+{
+ const auto *sound = dynamic_cast(soundIn);
+
+ if (sound == nullptr)
+ {
+ this->killNote();
+ return;
+ }
+
+ const int velocity = static_cast(floatVelocity * 127.0);
+ this->currentVelocity = velocity;
+ if (this->region == nullptr)
+ {
+ this->region = sound->getRegionFor(midiNoteNumber, velocity);
+ }
+
+ if ((this->region == nullptr) || (this->region->sample == nullptr) || (this->region->sample->getBuffer() == nullptr))
+ {
+ this->killNote();
+ return;
+ }
+
+ if (this->region->negativeEnd)
+ {
+ this->killNote();
+ return;
+ }
+
+ // Pitch.
+ this->currentMidiNote = midiNoteNumber;
+ this->currentPitchWheel = currentPitchWheelPosition;
+ this->calcPitchRatio();
+
+ // Gain.
+ double noteGainDB = globalGain + this->region->volume;
+ // Thanks to for explaining the
+ // velocity curve in a way that I could understand, although they mean
+ // "log10" when they say "log".
+ double velocityGainDB = -20.0 * log10((127.0 * 127.0) / (velocity * velocity));
+ velocityGainDB *= this->region->ampVelTrack / 100.0;
+ noteGainDB += velocityGainDB;
+ this->noteGainLeft = this->noteGainRight = static_cast(Decibels::decibelsToGain(noteGainDB));
+ // The SFZ spec is silent about the pan curve, but a 3dB pan law seems
+ // common. This sqrt() curve matches what Dimension LE does; Alchemy Free
+ // seems closer to sin(adjustedPan * pi/2).
+ const double adjustedPan = (this->region->pan + 100.0) / 200.0;
+ this->noteGainLeft *= static_cast(sqrt(1.0 - adjustedPan));
+ this->noteGainRight *= static_cast(sqrt(adjustedPan));
+ this->envelope.startNote(&this->region->ampeg, floatVelocity,
+ this->getSampleRate(), &this->region->ampegVelTrack);
+
+ // Offset/end.
+ this->sourceSamplePosition = static_cast(this->region->offset);
+ this->sampleEnd = this->region->sample->getSampleLength();
+ if ((this->region->end > 0) && (this->region->end < this->sampleEnd))
+ {
+ this->sampleEnd = this->region->end + 1;
+ }
+
+ // Loop.
+ this->loopStart = this->loopEnd = 0;
+ auto loopMode = this->region->loopMode;
+ if (loopMode == SoundFontRegion::LoopMode::sampleLoop)
+ {
+ if (this->region->sample->getLoopStart() < this->region->sample->getLoopEnd())
+ {
+ loopMode = SoundFontRegion::LoopMode::loopContinuous;
+ }
+ else
+ {
+ loopMode = SoundFontRegion::LoopMode::noLoop;
+ }
+ }
+ if ((loopMode != SoundFontRegion::LoopMode::noLoop) && (loopMode != SoundFontRegion::LoopMode::oneShot))
+ {
+ if (this->region->loopStart < this->region->loopEnd)
+ {
+ this->loopStart = this->region->loopStart;
+ this->loopEnd = this->region->loopEnd;
+ }
+ else
+ {
+ this->loopStart = this->region->sample->getLoopStart();
+ this->loopEnd = this->region->sample->getLoopEnd();
+ }
+ }
+
+ this->numLoops = 0;
+}
+
+void SoundFontVoice::stopNote(float /*velocity*/, bool allowTailOff)
+{
+ if (!allowTailOff || (this->region == nullptr))
+ {
+ this->killNote();
+ return;
+ }
+
+ if (this->region->loopMode != SoundFontRegion::LoopMode::oneShot)
+ {
+ this->envelope.noteOff();
+ }
+ if (this->region->loopMode == SoundFontRegion::LoopMode::loopSustain)
+ {
+ // Continue playing, but stop looping.
+ this->loopEnd = this->loopStart;
+ }
+}
+
+void SoundFontVoice::stopNoteForGroup()
+{
+ if (this->region->offMode == SoundFontRegion::OffMode::fast)
+ {
+ this->envelope.fastRelease();
+ }
+ else
+ {
+ this->envelope.noteOff();
+ }
+}
+
+void SoundFontVoice::stopNoteQuick()
+{
+ this->envelope.fastRelease();
+}
+
+void SoundFontVoice::pitchWheelMoved(int newValue)
+{
+ if (this->region == nullptr)
+ {
+ return;
+ }
+
+ this->currentPitchWheel = newValue;
+ this->calcPitchRatio();
+}
+
+void SoundFontVoice::renderNextBlock(AudioSampleBuffer &outputBuffer, int startSample, int numSamples)
+{
+ if (this->region == nullptr)
+ {
+ return;
+ }
+
+ const auto *buffer = this->region->sample->getBuffer();
+ if (buffer == nullptr)
+ {
+ jassertfalse;
+ return;
+ }
+
+ const float *inL = buffer->getReadPointer(0, 0);
+ const float *inR = buffer->getNumChannels() > 1 ? buffer->getReadPointer(1, 0) : nullptr;
+
+ float *outL = outputBuffer.getWritePointer(0, startSample);
+ float *outR = outputBuffer.getNumChannels() > 1 ? outputBuffer.getWritePointer(1, startSample) : nullptr;
+
+ const int bufferNumSamples = buffer->getNumSamples();
+
+ // Cache some values, to give them at least some chance of ending up in registers.
+ double sourceSamplePosition = this->sourceSamplePosition;
+ float ampegGain = this->envelope.getLevel();
+ float ampegSlope = this->envelope.getSlope();
+ int samplesUntilNextAmpSegment = this->envelope.getSamplesUntilNextSegment();
+ bool ampSegmentIsExponential = this->envelope.getSegmentIsExponential();
+
+ const float loopStart = float(this->loopStart);
+ const float loopEnd = float(this->loopEnd);
+ const float sampleEnd = float(this->sampleEnd);
+
+ while (--numSamples >= 0)
+ {
+ int pos = int(sourceSamplePosition);
+ jassert(pos >= 0 && pos < bufferNumSamples);
+ float alpha = float(sourceSamplePosition - pos);
+ float invAlpha = 1.0f - alpha;
+ int nextPos = pos + 1;
+ if ((loopStart < loopEnd) && (nextPos > loopEnd))
+ {
+ nextPos = int(loopStart);
+ }
+
+ // Simple linear interpolation with buffer overrun check
+ float nextL = nextPos < bufferNumSamples ? inL[nextPos] : inL[pos];
+ float nextR = inR ? (nextPos < bufferNumSamples ? inR[nextPos] : inR[pos]) : nextL;
+ float l = (inL[pos] * invAlpha + nextL * alpha);
+ float r = inR ? (inR[pos] * invAlpha + nextR * alpha) : l;
+
+ float gainLeft = this->noteGainLeft * ampegGain;
+ float gainRight = this->noteGainRight * ampegGain;
+ l *= gainLeft;
+ r *= gainRight;
+ // Shouldn't we dither here?
+
+ if (outR)
+ {
+ *outL++ += l;
+ *outR++ += r;
+ }
+ else
+ {
+ *outL++ += (l + r) * 0.5f;
+ }
+
+ // Next sample.
+ sourceSamplePosition += this->pitchRatio;
+ if ((loopStart < loopEnd) && (sourceSamplePosition > loopEnd))
+ {
+ sourceSamplePosition = loopStart;
+ this->numLoops += 1;
+ }
+
+ // Update EG.
+ if (ampSegmentIsExponential)
+ {
+ ampegGain *= ampegSlope;
+ }
+ else
+ {
+ ampegGain += ampegSlope;
+ }
+ if (--samplesUntilNextAmpSegment < 0)
+ {
+ this->envelope.setLevel(ampegGain);
+ this->envelope.nextSegment();
+ ampegGain = this->envelope.getLevel();
+ ampegSlope = this->envelope.getSlope();
+ samplesUntilNextAmpSegment = this->envelope.getSamplesUntilNextSegment();
+ ampSegmentIsExponential = this->envelope.getSegmentIsExponential();
+ }
+
+ if ((sourceSamplePosition >= sampleEnd) || this->envelope.isDone())
+ {
+ this->killNote();
+ break;
+ }
+ }
+
+ this->sourceSamplePosition = sourceSamplePosition;
+ this->envelope.setLevel(ampegGain);
+ this->envelope.setSamplesUntilNextSegment(samplesUntilNextAmpSegment);
+}
+
+bool SoundFontVoice::isPlayingNoteDown()
+{
+ return this->region && this->region->trigger != SoundFontRegion::Trigger::release;
+}
+
+bool SoundFontVoice::isPlayingOneShot()
+{
+ return this->region && this->region->loopMode == SoundFontRegion::LoopMode::oneShot;
+}
+
+int SoundFontVoice::getGroup()
+{
+ return this->region ? this->region->group : 0;
+}
+
+uint64 SoundFontVoice::getOffBy()
+{
+ return this->region ? this->region->offBy : 0;
+}
+
+void SoundFontVoice::setRegion(SoundFontRegion *nextRegion)
+{
+ this->region = nextRegion;
+}
+
+String SoundFontVoice::infoString()
+{
+ const char *egSegmentNames[] = {"delay", "attack", "hold", "decay", "sustain", "release", "done"};
+
+ const static int numEGSegments(sizeof(egSegmentNames) / sizeof(egSegmentNames[0]));
+
+ const char *egSegmentName = "-Invalid-";
+ int egSegmentIndex = this->envelope.segmentIndex();
+ if ((egSegmentIndex >= 0) && (egSegmentIndex < numEGSegments))
+ {
+ egSegmentName = egSegmentNames[egSegmentIndex];
+ }
+
+ String info;
+ info << "note: " << this->currentMidiNote << ", vel: " << this->currentVelocity
+ << ", pan: " << this->region->pan << ", eg: " << egSegmentName
+ << ", loops: " << this->numLoops;
+
+ return info;
+}
+
+void SoundFontVoice::calcPitchRatio()
+{
+ double note = this->currentMidiNote;
+
+ note += this->region->transpose;
+ note += this->region->tune / 100.0;
+
+ double adjustedPitch = this->region->pitchKeyCenter +
+ (note - this->region->pitchKeyCenter) * (this->region->pitchKeyTrack / 100.0);
+
+ if (this->currentPitchWheel != 8192)
+ {
+ double wheel = ((2.0 * this->currentPitchWheel / 16383.0) - 1.0);
+ if (wheel > 0)
+ {
+ adjustedPitch += wheel * this->region->bendUp / 100.0;
+ }
+ else
+ {
+ adjustedPitch += wheel * this->region->bendDown / -100.0;
+ }
+ }
+
+ // todo someday: support equal temperaments like the default synth does?
+ const auto fractionalMidiNoteInHz = [](double note, double freqOfA = 440.0) {
+ // Like MidiMessage::getMidiNoteInHertz(), but with a float note.
+ note -= 69;
+ // Now 0 = A
+ return freqOfA * pow(2.0, note / 12.0);
+ };
+
+ const double targetFreq = fractionalMidiNoteInHz(adjustedPitch);
+ const double naturalFreq = MidiMessage::getMidiNoteInHertz(this->region->pitchKeyCenter);
+ this->pitchRatio = (targetFreq * this->region->sample->getSampleRate()) / (naturalFreq * getSampleRate());
+}
+
+void SoundFontVoice::killNote()
+{
+ this->region = nullptr;
+ this->clearCurrentNote();
+}
+
+//===----------------------------------------------------------------------===//
+// SoundFontSynth
+//===----------------------------------------------------------------------===//
+
+void SoundFontSynth::noteOn(int midiChannel, int midiNoteNumber, float velocity)
+{
+ int i;
+
+ const ScopedLock locker(this->lock);
+
+ int midiVelocity = static_cast(velocity * 127);
+
+ // First, stop any currently-playing sounds in the group.
+ // Currently, this only pays attention to the first matching region.
+ int group = 0;
+ auto *sound = dynamic_cast(this->getSound(0).get());
+ if (sound != nullptr)
+ {
+ if (auto *region = sound->getRegionFor(midiNoteNumber, midiVelocity))
+ {
+ group = region->group;
+ }
+ }
+
+ if (group != 0)
+ {
+ for (auto *v : this->voices)
+ {
+ jassert(dynamic_cast(v));
+ auto *voice = static_cast(v);
+ if (voice->getOffBy() == group)
+ {
+ voice->stopNoteForGroup();
+ }
+ }
+ }
+
+ // Are any notes playing? (Needed for first/legato trigger handling.)
+ // Also stop any voices still playing this note.
+ bool anyNotesPlaying = false;
+ for (auto *v : this->voices)
+ {
+ jassert(dynamic_cast(v));
+ auto *voice = static_cast(v);
+
+ if (voice->isPlayingChannel(midiChannel) && voice->isPlayingNoteDown())
+ {
+ if (voice->getCurrentlyPlayingNote() == midiNoteNumber)
+ {
+ if (!voice->isPlayingOneShot())
+ {
+ voice->stopNoteQuick();
+ }
+ }
+ else
+ {
+ anyNotesPlaying = true;
+ }
+ }
+ }
+
+ // Play *all* matching regions.
+ if (sound != nullptr)
+ {
+ const auto trigger = anyNotesPlaying ? SoundFontRegion::Trigger::legato : SoundFontRegion::Trigger::first;
+ for (i = 0; i < sound->getNumRegions(); ++i)
+ {
+ auto *region = sound->regionAt(i);
+ if (region->matches(midiNoteNumber, midiVelocity, trigger))
+ {
+ if (auto *voice = dynamic_cast(this->findFreeVoice(sound,
+ midiNoteNumber, midiChannel, this->isNoteStealingEnabled())))
+ {
+ voice->setRegion(region);
+ this->startVoice(voice, sound, midiChannel, midiNoteNumber, velocity);
+ }
+ }
+ }
+ }
+
+ this->noteVelocities[midiNoteNumber] = midiVelocity;
+}
+
+void SoundFontSynth::noteOff(int midiChannel, int midiNoteNumber, float velocity, bool allowTailOff)
+{
+ const ScopedLock locker(this->lock);
+
+ Synthesiser::noteOff(midiChannel, midiNoteNumber, velocity, allowTailOff);
+
+ // Start release region.
+ if (auto *sound = dynamic_cast(this->getSound(0).get()))
+ {
+ if (auto *region = sound->getRegionFor(midiNoteNumber, this->noteVelocities[midiNoteNumber], SoundFontRegion::Trigger::release))
+ {
+ if (auto *voice = dynamic_cast(this->findFreeVoice(sound, midiNoteNumber, midiChannel, false)))
+ {
+ // Synthesiser is too locked-down (ivars are private rt protected), so
+ // we have to use a "setRegion()" mechanism.
+ voice->setRegion(region);
+ this->startVoice(voice, sound, midiChannel, midiNoteNumber, this->noteVelocities[midiNoteNumber] / 127.0f);
+ }
+ }
+ }
+}
+
+//===----------------------------------------------------------------------===//
+// Presets
+//===----------------------------------------------------------------------===//
+
+int SoundFontSynth::getNumPrograms() const
+{
+ if (auto *sound = dynamic_cast(this->getSound(0).get()))
+ {
+ return sound->getNumPresets();
+ }
+
+ return 0;
+}
+
+int SoundFontSynth::getCurrentProgram() const
+{
+ if (auto *sound = dynamic_cast(this->getSound(0).get()))
+ {
+ return sound->getSelectedPreset();
+ }
+
+ return 0;
+}
+
+void SoundFontSynth::setCurrentProgram(int index)
+{
+ if (auto *sound = dynamic_cast(this->getSound(0).get()))
+ {
+ return sound->setSelectedPreset(index < this->getNumPrograms() ? index : 0);
+ }
+}
+
+const String SoundFontSynth::getProgramName(int index) const
+{
+ if (auto *sound = dynamic_cast(this->getSound(0).get()))
+ {
+ return sound->getPresetName(index);
+ }
+
+ return {};
+}
+
+void SoundFontSynth::changeProgramName(int index, const String &newName)
+{
+ jassertfalse;
+}
diff --git a/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.h b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.h
new file mode 100644
index 00000000..9cd7ed7a
--- /dev/null
+++ b/Source/Core/Audio/BuiltIn/SoundFont/SoundFontSynth.h
@@ -0,0 +1,49 @@
+/*
+ This file is part of Helio Workstation.
+
+ Helio is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Helio is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Helio. If not, see .
+
+ This SoundFont implementation is based on SFZero,
+ written by Steve Folta and extended by Leo Olivers and Cognitone,
+ distributed under MIT license, see README.md for details.
+*/
+
+#pragma once
+
+class SoundFontSynth final : public Synthesiser
+{
+public:
+
+ SoundFontSynth() = default;
+
+ void noteOn(int midiChannel, int midiNoteNumber, float velocity) override;
+ void noteOff(int midiChannel, int midiNoteNumber, float velocity, bool allowTailOff) override;
+
+
+ //===------------------------------------------------------------------===//
+ // Presets
+ //===------------------------------------------------------------------===//
+
+ int getNumPrograms() const;
+ int getCurrentProgram() const;
+ void setCurrentProgram(int index);
+ const String getProgramName(int index) const;
+ void changeProgramName(int index, const String &newName);
+
+private:
+
+ int noteVelocities[128];
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SoundFontSynth)
+};