154 lines
8.4 KiB
Markdown
154 lines
8.4 KiB
Markdown
# Global view of the engine
|
|
|
|
The sfizz engine is basically a "Synth" object that takes an SFZ file in, receives MIDI-type events
|
|
and is able to render audio through successive calls to a callback function. This is in line with
|
|
the way most audio applications and plugins are working. A high-level overview is presented in the
|
|
following diagram.
|
|
|
|
```
|
|
C and C++ API entry point
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+--------v-------+
|
|
| |
|
|
+-------------------------- Synth -----------------------------+
|
|
| | | |
|
|
| +----------------+ |
|
|
| | |
|
|
+--------v------+ | +---------v--------+
|
|
| | +---------v---------+ | |
|
|
| Region list | | Common resources | | Voice pool |
|
|
| | | and state | | |
|
|
+---------------+ | ----------------- | +------------------+
|
|
Built from the SFZ file | File pool |
|
|
| Envelope pool | The voices are the polyphony
|
|
Each region is a semi-passive | LFO pool | of the synth. They are idle
|
|
description object that can | Buffer pool | and they get activated by the
|
|
decide whether it is "active" | Midi state | synth to play a region on a
|
|
or not depending on the chain | ... | specific event. They are then
|
|
of MIDI events it receives. +-------------------+ "linked" to the region while
|
|
Once activated, a voice is There are a number of common it is played, and reset to
|
|
chosen to play the region until resources that are needed for their idle state when they
|
|
it ends naturally or through all the regions and in parti- are done playing the region.
|
|
note-offs or off-groups. cular the voices. This includes
|
|
all the (preloaded) files for
|
|
the SFZ instrument, but will
|
|
include in the future the EG
|
|
and LFOs that are needed to
|
|
achieve compliance with the
|
|
SFZ v2 specification. This will
|
|
also include a temporary buffer
|
|
holder that voices may share.
|
|
A common resource of importance
|
|
is the MIDI state: note durations
|
|
are needed for some opcodes --
|
|
for example rt_decay -- and
|
|
triggering velocities too.
|
|
```
|
|
|
|
The Synth, Voices and Regions form the bulk of the code complexity. The rest of the engine is dedicated of
|
|
mostly helper classes to enable easy management of floating-point buffers in which the audio data is held,
|
|
signal processing and accelerated (SIMD) computations, and abstractions that are specific to the SFZ format
|
|
such as envelope generators, curves or LFOs.
|
|
|
|
# Parsing the SFZ files
|
|
|
|
The sfz file logic is pretty simple and well defined. The sfzformat.com website contains an extensive documentation
|
|
on it. At its core, an SFZ file describes a list of `region` objects on which a certain number of "opcodes" will
|
|
apply. Opcodes can determine the sample played, the event conditions that will trigger the sample such as the range
|
|
of notes, channels, velocities, the processing to apply on the sample while playing, and many more things. It is
|
|
also possible to describe a `group` of regions, as well as exclusive groups that will shut off other regions that may
|
|
already be playing. There are also `master` groups, and `global` opcodes and some other types.
|
|
|
|
All the opcodes are declared within a header, in a pseudo-xml markup language that looks like this
|
|
```sfz
|
|
<global> volume=6
|
|
<control> set_cc4=5
|
|
<region> key=36 sample=kick.wav
|
|
```
|
|
Here we have 3 headers (`global`, `control` and `region`) and each header holds some opcodes. All of these opcodes
|
|
have a value---for example the volume is equal to 6 in the `global` header. Some opcodes also have parameters.
|
|
The `control` header holds an opcode `set_cc` with the parameter `4` and value `5`. The parameter here is the CC to set,
|
|
and the value at which to set it is 5.
|
|
|
|
The parsing logic of sfizz is handled through a base class called Parser---a very original choice. This parser has
|
|
a virtual callback that gets called whenever a header description is "complete", along with a list of opcodes that
|
|
apply to the header. Subclassing the Parser then allows to build different SFZ handlers, from full-blown synths as
|
|
with sfizz to simpler things such as printers (see in particular https://github.com/sfztools/sfz-flat/). If we look
|
|
at the core of the latter example, it will look something like the following
|
|
|
|
```cpp
|
|
class PrintingParser: public sfz::Parser
|
|
{
|
|
protected:
|
|
void callback(absl::string_view header, const std::vector<sfz::Opcode>& members) final
|
|
{
|
|
switch (hash(header)) // The hash(...) function transforms strings to large integers
|
|
{
|
|
case hash("global"): // It is also compile-time defined, which allows to do switch-case
|
|
// statements on strings, something that is usually not possible
|
|
globalMembers = members; // We save the global headers since they apply to the next
|
|
// region (and groups and masters)
|
|
masterMembers.clear();
|
|
groupMembers.clear();
|
|
break;
|
|
case hash("master"):
|
|
masterMembers = members; // So on
|
|
groupMembers.clear();
|
|
break;
|
|
case hash("group"):
|
|
groupMembers = members; // .. and so forth
|
|
break;
|
|
case hash("region"):
|
|
std::cout << "<" << header << ">" << ' '; // Now we print the region along with all the opcodes
|
|
// we memorized from earlier headers.
|
|
printMembers(globalMembers);
|
|
printMembers(masterMembers);
|
|
printMembers(groupMembers);
|
|
printMembers(members);
|
|
std::cout << '\n';
|
|
break;
|
|
default:
|
|
std::cout << "<" << header << ">" << ' ';
|
|
printMembers(members);
|
|
std::cout << '\n';
|
|
break;
|
|
}
|
|
}
|
|
private:
|
|
std::vector<sfz::Opcode> globalMembers;
|
|
std::vector<sfz::Opcode> masterMembers;
|
|
std::vector<sfz::Opcode> groupMembers;
|
|
void printMembers(const std::vector<sfz::Opcode>& members)
|
|
{
|
|
for (auto& member: members)
|
|
{
|
|
std::cout << member.opcode;
|
|
if (member.parameter)
|
|
std::cout << +*member.parameter;
|
|
std::cout << "=" << member.value;
|
|
std::cout << ' ';
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
The main function is then quite straightforward and we call a function from the Parser class that loads a file
|
|
```cpp
|
|
PrintingParser parser;
|
|
parser.loadSfzFile("my_sfz_file.sfz");
|
|
```
|
|
If you circle back to the parser you will see that opcodes are stored in an `Opcode` class. This class does some parsing
|
|
itself and separates the opcode name itself, parameters if any, and the value. Opcodes are very cheap to copy and pass
|
|
around because they only refer to characters in the file that are stored inside the `Parser` class, so feel free to
|
|
create vectors of them and move them around.
|
|
|
|
Note that you may also derive the loadSfzFile method if you have any processing you need to do before the actual parsing happens.
|
|
|
|
# Building the region list in sfizz
|
|
|
|
The callback method from sfizz is actually quite similar to the one shown above, except that instead of printing the region
|
|
we actually fill a big structure from it.
|