Skip to content

The Engine

Global view

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 https://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:

<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:

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

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.