// SPDX-License-Identifier: BSD-2-Clause

// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz

#include "SynthPrivate.h"
#include "FilePool.h"
#include "Curve.h"
#include "MidiState.h"
#include "SynthConfig.h"
#include "utility/StringViewHelpers.h"
#include <absl/strings/ascii.h>
#include <cstring>

// TODO: `ccModDepth` and `ccModParameters` are O(N), need better implementation

namespace sfz {
static constexpr unsigned maxIndices = 8;

static bool extractMessage(const char* pattern, const char* path, unsigned* indices);
static uint64_t hashMessagePath(const char* path, const char* sig);

void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, const char* sig, const sfizz_arg_t* args)
{
    UNUSED(args);
    Impl& impl = *impl_;
    unsigned indices[maxIndices];

    switch (hashMessagePath(path, sig)) {
        #define MATCH(p, s) case hash(p "," s): \
            if (extractMessage(p, path, indices) && !strcmp(sig, s))

        #define GET_REGION_OR_BREAK(idx)            \
            if (idx >= impl.layers_.size())         \
                break;                              \
            Layer& layer = *impl.layers_[idx];      \
            const Region& region = layer.getRegion();

        #define GET_FILTER_OR_BREAK(idx)                \
            if (idx >= region.filters.size())           \
                break;                                  \
            const auto& filter = region.filters[idx];

        #define GET_EQ_OR_BREAK(idx)                    \
            if (idx >= region.equalizers.size())        \
                break;                                  \
            const auto& eq = region.equalizers[idx];

        #define GET_LFO_OR_BREAK(idx)             \
            if (idx >= region.lfos.size())        \
                break;                            \
            const auto& lfo = region.lfos[idx];

        #define GET_EG_OR_BREAK(idx)              \
            if (idx >= region.flexEGs.size())     \
                break;                            \
            auto& eg = region.flexEGs[idx];

        #define GET_EG_POINT_OR_BREAK(idx)        \
            if (idx >= eg.points.size())          \
                break;                            \
            auto& point = eg.points[idx];

        MATCH("/hello", "") {
            client.receive(delay, "/hello", "", nullptr);
        } break;

        //----------------------------------------------------------------------

        MATCH("/num_regions", "") {
            client.receive<'i'>(delay, path, int(impl.layers_.size()));
        } break;

        MATCH("/num_groups", "") {
            client.receive<'i'>(delay, path, impl.numGroups_);
        } break;

        MATCH("/num_masters", "") {
            client.receive<'i'>(delay, path, impl.numMasters_);
        } break;

        MATCH("/num_curves", "") {
            client.receive<'i'>(delay, path, int(impl.resources_.getCurves().getNumCurves()));
        } break;

        MATCH("/num_samples", "") {
            client.receive<'i'>(delay, path, int(impl.resources_.getFilePool().getNumPreloadedSamples()));
        } break;

        MATCH("/octave_offset", "") {
            client.receive<'i'>(delay, path, impl.octaveOffset_);
        } break;

        MATCH("/note_offset", "") {
            client.receive<'i'>(delay, path, impl.noteOffset_);
        } break;

        MATCH("/num_outputs", "") {
            client.receive<'i'>(delay, path, impl.numOutputs_);
        } break;

        //----------------------------------------------------------------------

        MATCH("/key/slots", "") {
            const BitArray<128>& keys = impl.keySlots_;
            sfizz_blob_t blob { keys.data(), static_cast<uint32_t>(keys.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/key&/label", "") {
            if (indices[0] >= 128)
                break;
            const std::string* label = impl.getKeyLabel(indices[0]);
            client.receive<'s'>(delay, path, label ? label->c_str() : "");
        } break;

        //----------------------------------------------------------------------

        MATCH("/root_path", "") {
            client.receive<'s'>(delay, path, impl.rootPath_.c_str());
        } break;

        MATCH("/image", "") {
            client.receive<'s'>(delay, path, impl.image_.c_str());
        } break;

        MATCH("/image_controls", "") {
            client.receive<'s'>(delay, path, impl.image_controls_.c_str());
        } break;

        //----------------------------------------------------------------------

        MATCH("/sw/last/slots", "") {
            const BitArray<128>& switches = impl.swLastSlots_;
            sfizz_blob_t blob { switches.data(), static_cast<uint32_t>(switches.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/sw/last/current", "") {
            if (impl.currentSwitch_)
                client.receive<'i'>(delay, path, *impl.currentSwitch_);
            else
                client.receive<'N'>(delay, path, {});
        } break;

        MATCH("/sw/last/&/label", "") {
            if (indices[0] >= 128)
                break;
            const std::string* label = impl.getKeyswitchLabel(indices[0]);
            client.receive<'s'>(delay, path, label ? label->c_str() : "");
        } break;

        //----------------------------------------------------------------------

        MATCH("/cc/slots", "") {
            const BitArray<config::numCCs>& ccs = impl.currentUsedCCs_;
            sfizz_blob_t blob { ccs.data(), static_cast<uint32_t>(ccs.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/cc&/default", "") {
            if (indices[0] >= config::numCCs)
                break;
            client.receive<'f'>(delay, path, impl.defaultCCValues_[indices[0]]);
        } break;

        MATCH("/cc&/value", "") {
            if (indices[0] >= config::numCCs)
                break;
            // Note: result value is not frame-exact
            client.receive<'f'>(delay, path, impl.resources_.getMidiState().getCCValue(indices[0]));
        } break;

        MATCH("/cc&/value", "f") {
            if (indices[0] >= config::numCCs)
                break;
            impl.resources_.getMidiState().ccEvent(delay, indices[0], args[0].f);
        } break;

        MATCH("/cc&/label", "") {
            if (indices[0] >= config::numCCs)
                break;
            const std::string* label = impl.getCCLabel(indices[0]);
            client.receive<'s'>(delay, path, label ? label->c_str() : "");
        } break;

        MATCH("/cc/changed", "") {
            const BitArray<config::numCCs>& changedCCs = impl.changedCCsThisCycle_;
            sfizz_blob_t blob { changedCCs.data(), static_cast<uint32_t>(changedCCs.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/cc/changed~", "") {
            const BitArray<config::numCCs>& changedCCs = impl.changedCCsLastCycle_;
            sfizz_blob_t blob { changedCCs.data(), static_cast<uint32_t>(changedCCs.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/sustain_or_sostenuto/slots", "") {
            const BitArray<128>& sustainOrSostenuto = impl.sustainOrSostenuto_;
            sfizz_blob_t blob { sustainOrSostenuto.data(),
                static_cast<uint32_t>(sustainOrSostenuto.byte_size()) };
            client.receive<'b'>(delay, path, &blob);
        } break;

        MATCH("/aftertouch", "") {
            client.receive<'f'>(delay, path, impl.resources_.getMidiState().getChannelAftertouch());
        } break;

        MATCH("/poly_aftertouch/&", "") {
            if (indices[0] > 127)
                break;
            // Note: result value is not frame-exact
            client.receive<'f'>(delay, path, impl.resources_.getMidiState().getPolyAftertouch(indices[0]));
        } break;

        MATCH("/pitch_bend", "") {
            // Note: result value is not frame-exact
            client.receive<'f'>(delay, path, impl.resources_.getMidiState().getPitchBend());
        } break;

        //----------------------------------------------------------------------

        MATCH("/mem/buffers", "") {
            uint64_t total = BufferCounter::counter().getTotalBytes();
            client.receive<'h'>(delay, path, total);
        } break;

        //----------------------------------------------------------------------

        MATCH("/region&/delay", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.delay);
        } break;

        MATCH("/region&/sample", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'s'>(delay, path, region.sampleId->filename().c_str());
        } break;

        MATCH("/region&/direction", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.sampleId->isReverse())
                client.receive<'s'>(delay, path, "reverse");
            else
                client.receive<'s'>(delay, path, "forward");
        } break;

        MATCH("/region&/delay_random", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.delayRandom);
        } break;

        MATCH("/region&/delay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.delayCC.getWithDefault(indices[1]));
        } break;

        MATCH("/region&/offset", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.offset);
        } break;

        MATCH("/region&/offset_random", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.offsetRandom);
        } break;

        MATCH("/region&/offset_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.offsetCC.getWithDefault(indices[1]));
        } break;

        MATCH("/region&/end", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.sampleEnd);
        } break;

        MATCH("/region&/end_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.endCC.getWithDefault(indices[1]));
        } break;

        MATCH("/region&/enabled", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.disabled()) {
                client.receive<'F'>(delay, path, {});
            } else {
                client.receive<'T'>(delay, path, {});
            }
        } break;

        MATCH("/region&/trigger_on_note", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.triggerOnNote) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/trigger_on_cc", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.triggerOnCC) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/use_timer_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.useTimerRange) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/count", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.sampleCount)
                client.receive<'h'>(delay, path, *region.sampleCount);
            else
                client.receive<'N'>(delay, path, {});
        } break;

        MATCH("/region&/loop_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].h = region.loopRange.getStart();
            args[1].h = region.loopRange.getEnd();
            client.receive(delay, path, "hh", args);
        } break;

        MATCH("/region&/loop_start_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.loopStartCC.getWithDefault(indices[1]));
        } break;

        MATCH("/region&/loop_end_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.loopEndCC.getWithDefault(indices[1]));
        } break;

        MATCH("/region&/loop_mode", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.loopMode) {
                client.receive<'s'>(delay, path, "no_loop");
                break;
            }

            switch (*region.loopMode) {
            case LoopMode::no_loop:
                client.receive<'s'>(delay, path, "no_loop");
                break;
            case LoopMode::loop_continuous:
                client.receive<'s'>(delay, path, "loop_continuous");
                break;
            case LoopMode::loop_sustain:
                client.receive<'s'>(delay, path, "loop_sustain");
                break;
            case LoopMode::one_shot:
                client.receive<'s'>(delay, path, "one_shot");
                break;
            }
        } break;

        MATCH("/region&/loop_crossfade", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.loopCrossfade);
        } break;

        MATCH("/region&/loop_count", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.loopCount)
                client.receive<'h'>(delay, path, *region.loopCount);
            else
                client.receive<'N'>(delay, path, {});
        } break;

        MATCH("/region&/output", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.output);
        } break;

        MATCH("/region&/group", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.group);
        } break;

        MATCH("/region&/off_by", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.offBy) {
                client.receive<'N'>(delay, path, {});
            } else {
                client.receive<'h'>(delay, path, *region.offBy);
            }
        } break;

        MATCH("/region&/off_mode", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.offMode) {
            case OffMode::time:
                client.receive<'s'>(delay, path, "time");
                break;
            case OffMode::normal:
                client.receive<'s'>(delay, path, "normal");
                break;
            case OffMode::fast:
                client.receive<'s'>(delay, path, "fast");
                break;
            }
        } break;

        MATCH("/region&/key_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].i = region.keyRange.getStart();
            args[1].i = region.keyRange.getEnd();
            client.receive(delay, path, "ii", args);
        } break;

        MATCH("/region&/off_time", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.offTime);
        } break;

        MATCH("/region&/pitch_keycenter", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.pitchKeycenter);
        } break;

        MATCH("/region&/vel_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.velocityRange.getStart();
            args[1].f = region.velocityRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/bend_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.bendRange.getStart();
            args[1].f = region.bendRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/program_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].i = region.programRange.getStart();
            args[1].i = region.programRange.getEnd();
            client.receive(delay, path, "ii", args);
        } break;

        MATCH("/region&/cc_range&", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            const auto& conditions = region.ccConditions.getWithDefault(indices[1]);
            args[0].f = conditions.getStart();
            args[1].f = conditions.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/sw_last", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.lastKeyswitch) {
                client.receive<'i'>(delay, path, *region.lastKeyswitch);
            } else if (region.lastKeyswitchRange) {
                sfizz_arg_t args[2];
                args[0].i = region.lastKeyswitchRange->getStart();
                args[1].i = region.lastKeyswitchRange->getEnd();
                client.receive(delay, path, "ii", args);
            } else {
                client.receive<'N'>(delay, path, {});
            }

        } break;

        MATCH("/region&/sw_label", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.keyswitchLabel) {
                client.receive<'s'>(delay, path, region.keyswitchLabel->c_str());
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/sw_up", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.upKeyswitch) {
                client.receive<'i'>(delay, path, *region.upKeyswitch);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/sw_down", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.downKeyswitch) {
                client.receive<'i'>(delay, path, *region.downKeyswitch);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/sw_previous", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.previousKeyswitch) {
                client.receive<'i'>(delay, path, *region.previousKeyswitch);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/sw_vel", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.velocityOverride) {
            case VelocityOverride::current:
                client.receive<'s'>(delay, path, "current");
                break;
            case VelocityOverride::previous:
                client.receive<'s'>(delay, path, "previous");
                break;
            }
        } break;

        MATCH("/region&/chanaft_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.aftertouchRange.getStart();
            args[1].f = region.aftertouchRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/polyaft_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.polyAftertouchRange.getStart();
            args[1].f = region.polyAftertouchRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/bpm_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.bpmRange.getStart();
            args[1].f = region.bpmRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/rand_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.randRange.getStart();
            args[1].f = region.randRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/seq_length", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.sequenceLength);
        } break;

        MATCH("/region&/seq_position", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'h'>(delay, path, region.sequencePosition);
        } break;

        MATCH("/region&/trigger", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.trigger) {
            case Trigger::attack:
                client.receive<'s'>(delay, path, "attack");
                break;
            case Trigger::first:
                client.receive<'s'>(delay, path, "first");
                break;
            case Trigger::release:
                client.receive<'s'>(delay, path, "release");
                break;
            case Trigger::release_key:
                client.receive<'s'>(delay, path, "release_key");
                break;
            case Trigger::legato:
                client.receive<'s'>(delay, path, "legato");
                break;
            }
        } break;

        MATCH("/region&/start_cc_range&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto trigger = region.ccTriggers.get(indices[1]);
            if (trigger) {
                sfizz_arg_t args[2];
                args[0].f = trigger->getStart();
                args[1].f = trigger->getEnd();
                client.receive(delay, path, "ff", args);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/volume", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.volume);
        } break;

        MATCH("/region&/volume_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Volume);
            if (value) {
                client.receive<'f'>(delay, path, *value);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/volume_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Volume);
            if (params) {
                client.receive<'f'>(delay, path, params->step);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/volume_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Volume);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/volume_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Volume);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pan", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.pan * 100.0f);
        } break;

        MATCH("/region&/pan_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Pan);
            if (value) {
                client.receive<'f'>(delay, path, *value * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pan_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pan);
            if (params) {
                client.receive<'f'>(delay, path, params->step * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pan_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pan);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pan_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pan);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/width", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.width * 100.0f);
        } break;

        MATCH("/region&/width_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Width);
            if (value) {
                client.receive<'f'>(delay, path, *value * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/width_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Width);
            if (params) {
                client.receive<'f'>(delay, path, params->step * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/width_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Width);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/width_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Width);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/timer_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.timerRange.getStart();
            args[1].f = region.timerRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/position", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.position * 100.0f);
        } break;

        MATCH("/region&/position_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Position);
            if (value) {
                client.receive<'f'>(delay, path, *value * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/position_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Position);
            if (params) {
                client.receive<'f'>(delay, path, params->step * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/position_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Position);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/position_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Position);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amplitude", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitude * 100.0f);
        } break;

        MATCH("/region&/amplitude_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Amplitude);
            if (value) {
                client.receive<'f'>(delay, path, *value * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amplitude_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Amplitude);
            if (params) {
                client.receive<'f'>(delay, path, params->step * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amplitude_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Amplitude);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amplitude_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Amplitude);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amp_keycenter", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.ampKeycenter);
        } break;

        MATCH("/region&/amp_keytrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.ampKeytrack);
        } break;

        MATCH("/region&/amp_veltrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.ampVeltrack * 100.0f);
        } break;

        MATCH("/region&/amp_veltrack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.ampVeltrackCC.contains(indices[1])) {
                const auto& cc = region.ampVeltrackCC.getWithDefault(indices[1]);
                client.receive<'f'>(delay, path, cc.modifier * 100.0f);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amp_veltrack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.ampVeltrackCC.contains(indices[1])) {
                const auto& cc = region.ampVeltrackCC.getWithDefault(indices[1]);
                client.receive<'i'>(delay, path, cc.curve );
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/amp_random", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.ampRandom);
        } break;

        MATCH("/region&/xfin_key_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].i = region.crossfadeKeyInRange.getStart();
            args[1].i = region.crossfadeKeyInRange.getEnd();
            client.receive(delay, path, "ii", args);
        } break;

        MATCH("/region&/xfout_key_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].i = region.crossfadeKeyOutRange.getStart();
            args[1].i = region.crossfadeKeyOutRange.getEnd();
            client.receive(delay, path, "ii", args);
        } break;

        MATCH("/region&/xfin_vel_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.crossfadeVelInRange.getStart();
            args[1].f = region.crossfadeVelInRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/xfout_vel_range", "") {
            GET_REGION_OR_BREAK(indices[0])
            sfizz_arg_t args[2];
            args[0].f = region.crossfadeVelOutRange.getStart();
            args[1].f = region.crossfadeVelOutRange.getEnd();
            client.receive(delay, path, "ff", args);
        } break;

        MATCH("/region&/xfin_cc_range&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto range = region.crossfadeCCInRange.get(indices[1]);
            if (range) {
                sfizz_arg_t args[2];
                args[0].f = range->getStart();
                args[1].f = range->getEnd();
                client.receive(delay, path, "ff", args);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/xfout_cc_range&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto range = region.crossfadeCCOutRange.get(indices[1]);
            if (range) {
                sfizz_arg_t args[2];
                args[0].f = range->getStart();
                args[1].f = range->getEnd();
                client.receive(delay, path, "ff", args);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/xf_keycurve", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.crossfadeKeyCurve) {
            case CrossfadeCurve::gain:
                client.receive<'s'>(delay, path, "gain");
                break;
            case CrossfadeCurve::power:
                client.receive<'s'>(delay, path, "power");
                break;
            }
        } break;

        MATCH("/region&/xf_velcurve", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.crossfadeVelCurve) {
            case CrossfadeCurve::gain:
                client.receive<'s'>(delay, path, "gain");
                break;
            case CrossfadeCurve::power:
                client.receive<'s'>(delay, path, "power");
                break;
            }
        } break;

        MATCH("/region&/xf_cccurve", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch (region.crossfadeCCCurve) {
            case CrossfadeCurve::gain:
                client.receive<'s'>(delay, path, "gain");
                break;
            case CrossfadeCurve::power:
                client.receive<'s'>(delay, path, "power");
                break;
            }
        } break;

        MATCH("/region&/global_volume", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.globalVolume);
        } break;

        MATCH("/region&/master_volume", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.masterVolume);
        } break;

        MATCH("/region&/group_volume", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.groupVolume);
        } break;

        MATCH("/region&/global_amplitude", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.globalAmplitude * 100.0f);
        } break;

        MATCH("/region&/master_amplitude", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.masterAmplitude * 100.0f);
        } break;

        MATCH("/region&/group_amplitude", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.groupAmplitude * 100.0f);
        } break;

        MATCH("/region&/pitch_keytrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.pitchKeytrack);
        } break;

        MATCH("/region&/pitch_veltrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.pitchVeltrack);
        } break;

        MATCH("/region&/pitch_veltrack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.pitchVeltrackCC.contains(indices[1])) {
                const auto& cc = region.pitchVeltrackCC.getWithDefault(indices[1]);
                client.receive<'f'>(delay, path, cc.modifier);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitch_veltrack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.pitchVeltrackCC.contains(indices[1])) {
                const auto& cc = region.pitchVeltrackCC.getWithDefault(indices[1]);
                client.receive<'i'>(delay, path, cc.curve );
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitch_random", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.pitchRandom);
        } break;

        MATCH("/region&/transpose", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.transpose);
        } break;

        MATCH("/region&/pitch", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.pitch);
        } break;

        MATCH("/region&/pitch_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto value = region.ccModDepth(indices[1], ModId::Pitch);
            if (value) {
                client.receive<'f'>(delay, path, *value);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitch_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pitch);
            if (params) {
                client.receive<'f'>(delay, path, params->step);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitch_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pitch);
            if (params) {
                client.receive<'i'>(delay, path, params->smooth);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitch_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto params = region.ccModParameters(indices[1], ModId::Pitch);
            if (params) {
                client.receive<'i'>(delay, path, params->curve);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/bend_up", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.bendUp);
        } break;

        MATCH("/region&/bend_down", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.bendDown);
        } break;

        MATCH("/region&/bend_step", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.bendStep);
        } break;

        MATCH("/region&/bend_smooth", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.bendSmooth);
        } break;

        MATCH("/region&/ampeg_attack", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.attack);
        } break;

        MATCH("/region&/ampeg_delay", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.delay);
        } break;

        MATCH("/region&/ampeg_decay", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.decay);
        } break;

        MATCH("/region&/ampeg_hold", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.hold);
        } break;

        MATCH("/region&/ampeg_release", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.release);
        } break;

        MATCH("/region&/ampeg_start", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.start * 100.0f);
        } break;

        MATCH("/region&/ampeg_sustain", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.sustain * 100.0f);
        } break;

        MATCH("/region&/ampeg_depth", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.amplitudeEG.depth);
        } break;

        MATCH("/region&/ampeg_vel&attack", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2attack);
        } break;

        MATCH("/region&/ampeg_vel&delay", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2delay);
        } break;

        MATCH("/region&/ampeg_vel&decay", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2decay);
        } break;

        MATCH("/region&/ampeg_vel&hold", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2hold);
        } break;

        MATCH("/region&/ampeg_vel&release", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2release);
        } break;

        MATCH("/region&/ampeg_vel&sustain", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2sustain * 100.0f);
        } break;

        MATCH("/region&/ampeg_vel&depth", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (indices[1] != 2)
                break;
            client.receive<'f'>(delay, path, region.amplitudeEG.vel2depth);
        } break;

        MATCH("/region&/ampeg_dynamic", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.amplitudeEG.dynamic) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/fileg_dynamic", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.filterEG && region.filterEG->dynamic) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitcheg_dynamic", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.pitchEG && region.pitchEG->dynamic) {
                client.receive<'T'>(delay, path, {});
            } else {
                client.receive<'F'>(delay, path, {});
            }
        } break;

        MATCH("/region&/pitcheg_attack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/pitcheg_decay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/pitcheg_delay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/pitcheg_hold_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/pitcheg_release_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/pitcheg_start_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/pitcheg_sustain_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/pitcheg_attack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_decay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_delay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_hold_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_release_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_start_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/pitcheg_sustain_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.pitchEG) break;
            const auto& cc = region.pitchEG->ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/note_polyphony", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.notePolyphony) {
                client.receive<'i'>(delay, path, *region.notePolyphony);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/note_selfmask", "") {
            GET_REGION_OR_BREAK(indices[0])
            switch(region.selfMask) {
            case SelfMask::mask:
                client.receive(delay, path, "T", nullptr);
                break;
            case SelfMask::dontMask:
                client.receive(delay, path, "F", nullptr);
                break;
            }
        } break;

        MATCH("/region&/rt_dead", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.rtDead) {
                client.receive(delay, path, "T", nullptr);
            } else {
                client.receive(delay, path, "F", nullptr);
            }
        } break;

        MATCH("/region&/sustain_sw", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.checkSustain) {
                client.receive(delay, path, "T", nullptr);
            } else {
                client.receive(delay, path, "F", nullptr);
            }
        } break;

        MATCH("/region&/sostenuto_sw", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.checkSostenuto) {
                client.receive(delay, path, "T", nullptr);
            } else {
                client.receive(delay, path, "F", nullptr);
            }
        } break;

        MATCH("/region&/sustain_cc", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.sustainCC);
        } break;

        MATCH("/region&/sostenuto_cc", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.sostenutoCC);
        } break;

        MATCH("/region&/sustain_lo", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.sustainThreshold);
        } break;

        MATCH("/region&/sostenuto_lo", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.sostenutoThreshold);
        } break;

        MATCH("/region&/oscillator_phase", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.oscillatorPhase);
        } break;

        MATCH("/region&/oscillator_quality", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (region.oscillatorQuality) {
                client.receive<'i'>(delay, path, *region.oscillatorQuality);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/oscillator_mode", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.oscillatorMode);
        } break;

        MATCH("/region&/oscillator_multi", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, region.oscillatorMulti);
        } break;

        MATCH("/region&/oscillator_detune", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.oscillatorDetune);
        } break;

        MATCH("/region&/oscillator_mod_depth", "") {
            GET_REGION_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, region.oscillatorModDepth * 100.0f);
        } break;

        // TODO: detune cc, mod depth cc

        MATCH("/region&/effect&", "") {
            GET_REGION_OR_BREAK(indices[0])
            auto effectIdx = indices[1];
            if (indices[1] == 0)
                break;

            if (effectIdx < region.gainToEffect.size())
                client.receive<'f'>(delay, path, region.gainToEffect[effectIdx] * 100.0f);
        } break;

        MATCH("/region&/ampeg_attack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/ampeg_decay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/ampeg_delay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/ampeg_hold_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/ampeg_release_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/ampeg_start_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/ampeg_sustain_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/ampeg_attack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_decay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_delay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_hold_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_release_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_start_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/ampeg_sustain_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto& cc = region.amplitudeEG.ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/filter&/cutoff", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, filter.cutoff);
        } break;

        MATCH("/region&/filter&/cutoff_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto depth = region.ccModDepth(indices[2], ModId::FilCutoff, indices[1]);
            if (depth)
                client.receive<'f'>(delay, path, *depth);
        } break;

        MATCH("/region&/filter&/cutoff_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto params = region.ccModParameters(indices[2], ModId::FilCutoff, indices[1]);
            if (params)
                client.receive<'i'>(delay, path, params->curve);
        } break;

        MATCH("/region&/filter&/cutoff_stepcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto params = region.ccModParameters(indices[2], ModId::FilCutoff, indices[1]);
            if (params)
                client.receive<'i'>(delay, path, params->step);
        } break;

        MATCH("/region&/filter&/cutoff_smoothcc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            const auto params = region.ccModParameters(indices[2], ModId::FilCutoff, indices[1]);
            if (params)
                client.receive<'i'>(delay, path, params->smooth);
        } break;

        MATCH("/region&/filter&/resonance", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, filter.resonance);
        } break;

        MATCH("/region&/filter&/gain", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, filter.gain);
        } break;

        MATCH("/region&/filter&/keycenter", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'i'>(delay, path, filter.keycenter);
        } break;

        MATCH("/region&/filter&/keytrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'i'>(delay, path, filter.keytrack);
        } break;

        MATCH("/region&/filter&/veltrack", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            client.receive<'i'>(delay, path, filter.veltrack);
        } break;

        MATCH("/region&/filter&/veltrack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            if (filter.veltrackCC.contains(indices[2])) {
                const auto& cc = filter.veltrackCC.getWithDefault(indices[2]);
                client.receive<'f'>(delay, path, cc.modifier);
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/filter&/veltrack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            if (filter.veltrackCC.contains(indices[2])) {
                const auto& cc = filter.veltrackCC.getWithDefault(indices[2]);
                client.receive<'i'>(delay, path, cc.curve );
            } else {
                client.receive<'N'>(delay, path, {});
            }
        } break;

        MATCH("/region&/filter&/type", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            switch (filter.type) {
            case FilterType::kFilterLpf1p: client.receive<'s'>(delay, path, "lpf_1p"); break;
            case FilterType::kFilterHpf1p: client.receive<'s'>(delay, path, "hpf_1p"); break;
            case FilterType::kFilterLpf2p: client.receive<'s'>(delay, path, "lpf_2p"); break;
            case FilterType::kFilterHpf2p: client.receive<'s'>(delay, path, "hpf_2p"); break;
            case FilterType::kFilterBpf2p: client.receive<'s'>(delay, path, "bpf_2p"); break;
            case FilterType::kFilterBrf2p: client.receive<'s'>(delay, path, "brf_2p"); break;
            case FilterType::kFilterBpf1p: client.receive<'s'>(delay, path, "bpf_1p"); break;
            case FilterType::kFilterBrf1p: client.receive<'s'>(delay, path, "brf_1p"); break;
            case FilterType::kFilterApf1p: client.receive<'s'>(delay, path, "apf_1p"); break;
            case FilterType::kFilterLpf2pSv: client.receive<'s'>(delay, path, "lpf_2p_sv"); break;
            case FilterType::kFilterHpf2pSv: client.receive<'s'>(delay, path, "hpf_2p_sv"); break;
            case FilterType::kFilterBpf2pSv: client.receive<'s'>(delay, path, "bpf_2p_sv"); break;
            case FilterType::kFilterBrf2pSv: client.receive<'s'>(delay, path, "brf_2p_sv"); break;
            case FilterType::kFilterLpf4p: client.receive<'s'>(delay, path, "lpf_4p"); break;
            case FilterType::kFilterHpf4p: client.receive<'s'>(delay, path, "hpf_4p"); break;
            case FilterType::kFilterLpf6p: client.receive<'s'>(delay, path, "lpf_6p"); break;
            case FilterType::kFilterHpf6p: client.receive<'s'>(delay, path, "hpf_6p"); break;
            case FilterType::kFilterPink: client.receive<'s'>(delay, path, "pink"); break;
            case FilterType::kFilterLsh: client.receive<'s'>(delay, path, "lsh"); break;
            case FilterType::kFilterHsh: client.receive<'s'>(delay, path, "hsh"); break;
            case FilterType::kFilterPeq: client.receive<'s'>(delay, path, "peq"); break;
            case FilterType::kFilterBpf4p: client.receive<'s'>(delay, path, "bpf_4p"); break;
            case FilterType::kFilterBpf6p: client.receive<'s'>(delay, path, "bpf_6p"); break;
            case FilterType::kFilterNone: client.receive<'s'>(delay, path, "none"); break;
            }
        } break;

        MATCH("/region&/fileg_attack_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/fileg_decay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/fileg_delay_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/fileg_hold_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/fileg_release_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier);
        } break;

        MATCH("/region&/fileg_start_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/fileg_sustain_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.modifier * 100.0f);
        } break;

        MATCH("/region&/fileg_attack_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccAttack.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_decay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccDecay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_delay_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccDelay.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_hold_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccHold.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_release_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccRelease.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_start_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccStart.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/fileg_sustain_curvecc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            if (!region.filterEG) break;
            const auto& cc = region.filterEG->ccSustain.getWithDefault(indices[1]);
            client.receive<'f'>(delay, path, cc.curve);
        } break;

        MATCH("/region&/eq&/gain", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, eq.gain);
        } break;

        MATCH("/region&/eq&/bandwidth", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, eq.bandwidth);
        } break;

        MATCH("/region&/eq&/frequency", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            client.receive<'f'>(delay, path, eq.frequency);
        } break;

        MATCH("/region&/eq&/vel&freq", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            if (indices[2] != 2)
                break;
            client.receive<'f'>(delay, path, eq.vel2frequency);
        } break;

        MATCH("/region&/eq&/vel&gain", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            if (indices[2] != 2)
                break;
            client.receive<'f'>(delay, path, eq.vel2gain);
        } break;

        MATCH("/region&/eq&/type", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EQ_OR_BREAK(indices[1])
            switch (eq.type) {
            case EqType::kEqNone: client.receive<'s'>(delay, path, "none"); break;
            case EqType::kEqPeak: client.receive<'s'>(delay, path, "peak"); break;
            case EqType::kEqLshelf: client.receive<'s'>(delay, path, "lshelf"); break;
            case EqType::kEqHshelf: client.receive<'s'>(delay, path, "hshelf"); break;
            }
        } break;

        MATCH("/region&/lfo&/wave", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_LFO_OR_BREAK(indices[1])
            if (lfo.sub.size() == 0)
                break;

            client.receive<'i'>(delay, path, static_cast<int32_t>(lfo.sub[0].wave));
        } break;

        MATCH("/region&/eg&/point&/time", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EG_OR_BREAK(indices[1])
            GET_EG_POINT_OR_BREAK(indices[2] + 1)

            client.receive<'f'>(delay, path, point.time);
        } break;

        MATCH("/region&/eg&/point&/time_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EG_OR_BREAK(indices[1])
            GET_EG_POINT_OR_BREAK(indices[2] + 1)

            client.receive<'f'>(delay, path, point.ccTime.getWithDefault(indices[3]));
        } break;

        MATCH("/region&/eg&/point&/level", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EG_OR_BREAK(indices[1])
            GET_EG_POINT_OR_BREAK(indices[2] + 1)

            client.receive<'f'>(delay, path, point.level);
        } break;

        MATCH("/region&/eg&/point&/level_cc&", "") {
            GET_REGION_OR_BREAK(indices[0])
            GET_EG_OR_BREAK(indices[1])
            GET_EG_POINT_OR_BREAK(indices[2] + 1)

            client.receive<'f'>(delay, path, point.ccLevel.getWithDefault(indices[3]));
        } break;

        #undef GET_REGION_OR_BREAK
        #undef GET_FILTER_OR_BREAK
        #undef GET_EQ_OR_BREAK
        #undef GET_LFO_OR_BREAK
        #undef GET_EG_OR_BREAK
        #undef GET_EG_POINT_OR_BREAK

        //----------------------------------------------------------------------
        // Setting values
        // Note: all these must be rt-safe within the parseOpcode method in region

        MATCH("/sample_quality", "i") {
            impl.resources_.getSynthConfig().liveSampleQuality =
                Opcode::transform(Default::sampleQuality, static_cast<int>(args[0].i));
        } break;

        MATCH("/oscillator_quality", "i") {
            impl.resources_.getSynthConfig().liveOscillatorQuality =
                Opcode::transform(Default::oscillatorQuality, static_cast<int>(args[0].i));
        } break;

        MATCH("/freewheeling_sample_quality", "i") {
            impl.resources_.getSynthConfig().freeWheelingSampleQuality =
                Opcode::transform(Default::freewheelingSampleQuality, static_cast<int>(args[0].i));
        } break;

        MATCH("/freewheeling_oscillator_quality", "i") {
            impl.resources_.getSynthConfig().freeWheelingOscillatorQuality =
                Opcode::transform(Default::freewheelingOscillatorQuality, static_cast<int>(args[0].i));
        } break;

        MATCH("/sustain_cancels_release", "T") {
            impl.resources_.getSynthConfig().sustainCancelsRelease = true;
        } break;

        MATCH("/sustain_cancels_release", "F") {
            impl.resources_.getSynthConfig().sustainCancelsRelease = false;
        } break;

        #define GET_REGION_OR_BREAK(idx)            \
            if (idx >= impl.layers_.size())         \
                break;                              \
            Layer& layer = *impl.layers_[idx];      \
            Region& region = layer.getRegion();

        #define GET_FILTER_OR_BREAK(idx)          \
            if (idx >= region.filters.size())     \
                break;                            \
            auto& filter = region.filters[idx];

        #define GET_LFO_OR_BREAK(idx)             \
            if (idx >= region.lfos.size())        \
                break;                            \
            auto& lfo = region.lfos[idx];

        #define GET_LFO_SUB_OR_BREAK(idx)         \
            if (idx >= lfo.sub.size())            \
                break;                            \
            auto& sub = lfo.sub[idx];

        MATCH("/region&/pitch_keycenter", "i") {
            GET_REGION_OR_BREAK(indices[0])
            region.pitchKeycenter = Opcode::transform(Default::key, args[0].i);
        } break;

        MATCH("/region&/loop_mode", "s") {
            GET_REGION_OR_BREAK(indices[0])
            region.loopMode = Opcode::readOptional(Default::loopMode, args[0].s);
        } break;

        MATCH("/region&/filter&/type", "s") {
            GET_REGION_OR_BREAK(indices[0])
            GET_FILTER_OR_BREAK(indices[1])
            filter.type = Opcode::read(Default::filter, args[0].s);
        } break;

        MATCH("/region&/lfo&/wave", "i") {
            indices[2] = 0;
            goto set_lfoN_wave;
        } break;

        MATCH("/region&/lfo&/wave&", "i") {
        set_lfoN_wave:
            GET_REGION_OR_BREAK(indices[0])
            GET_LFO_OR_BREAK(indices[1])
            GET_LFO_SUB_OR_BREAK(indices[2])
            sub.wave = Opcode::transform(Default::lfoWave, args[0].i);
        } break;

        #undef GET_REGION_OR_BREAK
        #undef GET_FILTER_OR_BREAK
        #undef GET_LFO_OR_BREAK
        #undef GET_LFO_SUB_OR_BREAK

        //----------------------------------------------------------------------
        // Voices

        MATCH("/num_active_voices", "") {
            client.receive<'i'>(delay, path, impl.voiceManager_.getNumActiveVoices());
        } break;

        #define GET_VOICE_OR_BREAK(idx)                     \
            if (static_cast<int>(idx) >= impl.numVoices_)   \
                break;                                      \
            const auto& voice = impl.voiceManager_[idx];    \
            if (voice.isFree())                             \
                break;

        MATCH("/voice&/trigger_value", "") {
            GET_VOICE_OR_BREAK(indices[0])
            client.receive<'f'>(delay, path, voice.getTriggerEvent().value);
        } break;

        MATCH("/voice&/trigger_number", "") {
            GET_VOICE_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, voice.getTriggerEvent().number);
        } break;

        MATCH("/voice&/trigger_type", "") {
            GET_VOICE_OR_BREAK(indices[0])
            const auto& event = voice.getTriggerEvent();
            switch (event.type) {
            case TriggerEventType::CC:
                client.receive<'s'>(delay, path, "cc");
                break;
            case TriggerEventType::NoteOn:
                client.receive<'s'>(delay, path, "note_on");
                break;
            case TriggerEventType::NoteOff:
                client.receive<'s'>(delay, path, "note_on");
                break;
            }

        } break;

        MATCH("/voice&/remaining_delay", "") {
            GET_VOICE_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, voice.getRemainingDelay());
        } break;

        MATCH("/voice&/source_position", "") {
            GET_VOICE_OR_BREAK(indices[0])
            client.receive<'i'>(delay, path, voice.getSourcePosition());
        } break;

        #undef MATCH
        // TODO...
    }
}

static bool extractMessage(const char* pattern, const char* path, unsigned* indices)
{
    unsigned nthIndex = 0;

    while (const char *endp = strchr(pattern, '&')) {
        if (nthIndex == maxIndices)
            return false;

        size_t length = endp - pattern;
        if (strncmp(pattern, path, length))
            return false;
        pattern += length;
        path += length;

        length = 0;
        while (absl::ascii_isdigit(path[length]))
            ++length;

        if (!absl::SimpleAtoi(absl::string_view(path, length), &indices[nthIndex++]))
            return false;

        pattern += 1;
        path += length;
    }

    return !strcmp(path, pattern);
}

static uint64_t hashMessagePath(const char* path, const char* sig)
{
    uint64_t h = Fnv1aBasis;
    while (unsigned char c = *path++) {
        if (!absl::ascii_isdigit(c))
            h = hashByte(c, h);
        else {
            h = hashByte('&', h);
            while (absl::ascii_isdigit(*path))
                ++path;
        }
    }
    h = hashByte(',', h);
    while (unsigned char c = *sig++)
        h = hashByte(c, h);
    return h;
}

} // namespace sfz
