zynaddsubfx

ZynAddSubFX open source synthesizer
Log | Files | Refs | Submodules | LICENSE

commit 53c686916a7686e3527f5c6f0b48a50080213635
parent 37b6486c4c066c475f3265d4facd09a80839555a
Author: fundamental <mark.d.mccurry@gmail.com>
Date:   Thu, 29 Jun 2017 13:09:02 -0400

Merge branch 'automations'

Replace MIDI Learn subsystem with
rewritten host automation + MIDI learn subsystem.

Diffstat:
Msrc/Misc/Master.cpp | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/Misc/Master.h | 4++--
Msrc/Misc/MiddleWare.cpp | 228++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/Plugin/ZynAddSubFX/DistrhoPluginInfo.h | 16++++++++++++++++
Msrc/Plugin/ZynAddSubFX/ZynAddSubFX.cpp | 20+++++++++++++++++---
5 files changed, 371 insertions(+), 130 deletions(-)

diff --git a/src/Misc/Master.cpp b/src/Misc/Master.cpp @@ -100,6 +100,201 @@ static const Ports sysefsendto = }} }; +#define rBegin [](const char *msg, RtData &d) { rtosc::AutomationMgr &a = *(AutomationMgr*)d.obj +#define rEnd } + +static int extract_num(const char *&msg) +{ + while(*msg && !isdigit(*msg)) msg++; + int num = atoi(msg); + while(isdigit(*msg)) msg++; + return num; +} + +static int get_next_int(const char *msg) +{ + return extract_num(msg); +} + +static const Ports mapping_ports = { + {"offset::f", rProp(parameter) rShort("off") rLinear(-50, 50) rMap(unit, percent), 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + if(!strcmp("f",rtosc_argument_string(msg))) { + a.setSlotSubOffset(slot, param, rtosc_argument(msg, 0).f); + a.updateMapping(slot, param); + d.broadcast(d.loc, "f", a.getSlotSubOffset(slot, param)); + } else + d.reply(d.loc, "f", a.getSlotSubOffset(slot, param)); + rEnd}, + {"gain::f", rProp(parameter) rShort("gain") rLinear(-200, 200) rMap(unit, percent), 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + if(!strcmp("f",rtosc_argument_string(msg))) { + a.setSlotSubGain(slot, param, rtosc_argument(msg, 0).f); + a.updateMapping(slot, param); + d.broadcast(d.loc, "f", a.getSlotSubGain(slot, param)); + } else + d.reply(d.loc, "f", a.getSlotSubGain(slot, param)); + rEnd}, +}; + +static const Ports auto_param_ports = { + {"used:", rProp(parameter) rProp(read-only) rDoc("If automation is assigned to anything"), 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + + d.reply(d.loc, a.slots[slot].automations[param].used ? "T" : "F"); + rEnd}, + {"active::T:F", rProp(parameter) rDoc("If automation is being actively used"), 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + if(rtosc_narguments(msg)) + a.slots[slot].automations[param].active = rtosc_argument(msg, 0).T; + else + d.reply(d.loc, a.slots[slot].automations[param].active ? "T" : "F"); + rEnd}, + {"path:", rProp(parameter) rProp(read-only) rDoc("Path of parameter"), 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + d.reply(d.loc, "s", a.slots[slot].automations[param].param_path); + rEnd}, + {"clear:", 0, 0, + rBegin; + int slot = d.idx[1]; + int param = d.idx[0]; + a.clearSlotSub(slot, param); + rEnd}, + {"mapping/", 0, &mapping_ports, + rBegin; + SNIP; + mapping_ports.dispatch(msg, d); + rEnd}, + + //{"mapping", rDoc("Parameter mapping control"), 0, + // rBegin; + // int slot = d.idx[1]; + // int param = d.idx[0]; + // if(!strcmp("b", rtosc_argument_string(msg))) { + // int len = rtosc_argument(msg, 0).b.len / sizeof(float); + // float *data = (float*)rtosc_argument(msg, 0).b.data; + // } else { + // d.reply(d.loc, "b", + // a.slots[slot].automations[param].map.npoints*sizeof(float), + // a.slots[slot].automations[param].map.control_points); + // } + // rEnd}, +}; + +static const Ports slot_ports = { + //{"learn-binding:s", rDoc("Create binding for automation path with midi-learn"), 0, + // rBegin; + // (void) m; + // //m->automate.createBinding(rtosc_argument(msg, 0).i, + // // rtosc_argument(msg, 1).s, + // // rtosc_argument(msg, 2).T); + // rEnd}, + //{"create-binding:s", rDoc("Create binding for automation path"), 0, + // rBegin; + // m->automate.createBinding(rtosc_argument(msg, 0).i, + // rtosc_argument(msg, 1).s, + // rtosc_argument(msg, 2).T); + // rEnd}, + {"value::f", rProp(parameter) rMap(default, 0.5) rLinear(0, 1) rDoc("Access current value in slot 'i' (0..1)"), 0, + rBegin; + int num = d.idx[0]; + if(!strcmp("f",rtosc_argument_string(msg))) { + a.setSlot(num, rtosc_argument(msg, 0).f); + d.broadcast(d.loc, "f", a.getSlot(num)); + } else + d.reply(d.loc, "f", a.getSlot(num)); + rEnd}, + + {"name::s", rProp(parameter) rDoc("Access name of automation slot"), 0, + rBegin; + int num = d.idx[0]; + if(!strcmp("s",rtosc_argument_string(msg))) { + a.setName(num, rtosc_argument(msg, 0).s); + d.broadcast(d.loc, "s", a.getName(num)); + } else + d.reply(d.loc, "s", a.getName(num)); + rEnd}, + {"midi-cc::i", rProp(parameter) rMap(default, -1) rDoc("Access assigned midi CC slot") , 0, + rBegin; + int slot = d.idx[0]; + if(rtosc_narguments(msg)) + a.slots[slot].midi_cc = rtosc_argument(msg, 0).i; + else + d.reply(d.loc, "i", a.slots[slot].midi_cc); + + rEnd}, + {"active::T:F", rProp(parameter) rMap(default, F) rDoc("If Slot is enabled"), 0, + rBegin; + int slot = d.idx[0]; + if(rtosc_narguments(msg)) + a.slots[slot].active = rtosc_argument(msg, 0).T; + else + d.reply(d.loc, a.slots[slot].active ? "T" : "F"); + rEnd}, + {"learning:", rProp(parameter) rMap(default, -1) rDoc("If slot is trying to find a midi learn binding"), 0, + rBegin; + int slot = d.idx[0]; + d.reply(d.loc, "i", a.slots[slot].learning); + rEnd}, + {"clear:", 0, 0, + rBegin; + int slot = d.idx[0]; + a.clearSlot(slot); + rEnd}, + {"param#4/", rDoc("Info on individual param mappings"), &auto_param_ports, + rBegin; + (void)a; + d.push_index(get_next_int(msg)); + SNIP; + auto_param_ports.dispatch(msg, d); + d.pop_index(); + rEnd}, +}; + +static const Ports automate_ports = { + {"active-slot::i", rProp(parameter) rMap(min, -1) rMap(max, 16) rDoc("Active Slot for macro learning"), 0, + rBegin; + if(!strcmp("i",rtosc_argument_string(msg))) { + a.active_slot = rtosc_argument(msg, 0).i; + d.broadcast(d.loc, "i", a.active_slot); + } else + d.reply(d.loc, "i", a.active_slot); + rEnd}, + {"learn-binding-new-slot:s", rDoc("Learn a parameter assigned to a new slot"), 0, + rBegin; + int free_slot = a.free_slot(); + if(free_slot >= 0) { + a.createBinding(free_slot, rtosc_argument(msg, 0).s, true); + a.active_slot = free_slot; + } + rEnd}, + {"learn-binding-same-slot:s", rDoc("Learn a parameter appending to the active-slot"), 0, + rBegin; + if(a.active_slot >= 0) + a.createBinding(a.active_slot, rtosc_argument(msg, 0).s, true); + rEnd}, + {"slot#16/", rDoc("Parameters of individual automation slots"), &slot_ports, + rBegin; + (void)a; + d.push_index(get_next_int(msg)); + SNIP; + slot_ports.dispatch(msg, d); + d.pop_index(); + rEnd}, +}; + +#undef rBegin +#undef rEnd #define rBegin [](const char *msg, RtData &d) { Master *m = (Master*)d.obj #define rEnd } @@ -110,6 +305,7 @@ static const Ports watchPorts = { rEnd}, }; + extern const Ports bankPorts; static const Ports master_ports = { rString(last_xmz, XMZ_PATH_MAX, "File name for last name loaded if any."), @@ -233,13 +429,12 @@ static const Ports master_ports = { [](const char *,RtData &d) { Master *M = (Master*)d.obj; M->frozenState = false;}}, - {"midi-learn/", 0, &rtosc::MidiMapperRT::ports, + {"automate/", rDoc("MIDI Learn/Plugin Automation support"), &automate_ports, [](const char *msg, RtData &d) { - Master *M = (Master*)d.obj; SNIP; - printf("residue message = <%s>\n", msg); - d.obj = &M->midi; - rtosc::MidiMapperRT::ports.dispatch(msg,d);}}, + d.obj = (void*)&((Master*)d.obj)->automate; + automate_ports.dispatch(msg, d); + }}, {"close-ui:", rDoc("Request to close any connection named \"GUI\""), 0, [](const char *, RtData &d) { d.reply("/close-ui", "");}}, @@ -290,6 +485,14 @@ static const Ports master_ports = { rBOIL_END}, {"bank/", rDoc("Controls for instrument banks"), &bankPorts, [](const char*,RtData&) {}}, + {"learn:s", rProp(depricated) rDoc("MIDI Learn"), 0, + rBegin; + int free_slot = m->automate.free_slot(); + if(free_slot >= 0) { + m->automate.createBinding(free_slot, rtosc_argument(msg, 0).s, true); + m->automate.active_slot = free_slot; + } + rEnd}, }; #undef rBegin @@ -366,15 +569,18 @@ vuData::vuData(void) Master::Master(const SYNTH_T &synth_, Config* config) :HDDRecorder(synth_), time(synth_), ctl(synth_, &time), microtonal(config->cfg.GzipCompression), bank(config), + automate(16,4,8), frozenState(false), pendingMemory(false), synth(synth_), gzip_compression(config->cfg.GzipCompression) { bToU = NULL; uToB = NULL; - //Setup MIDI - midi.frontend = [this](const char *msg) {bToU->raw_write(msg);}; - midi.backend = [this](const char *msg) {applyOscEvent(msg);}; + //Setup MIDI Learn + automate.set_ports(master_ports); + automate.set_instance(this); + //midi.frontend = [this](const char *msg) {bToU->raw_write(msg);}; + automate.backend = [this](const char *msg) {applyOscEvent(msg);}; memory = new AllocatorClass(); swaplr = 0; @@ -523,8 +729,7 @@ void Master::setController(char chan, int type, int par) { if(frozenState) return; - //TODO add chan back - midi.handleCC(type,par); + automate.handleMidi(chan, type, par); if((type == C_dataentryhi) || (type == C_dataentrylo) || (type == C_nrpnhi) || (type == C_nrpnlo)) { //Process RPN and NRPN by the Master (ignore the chan) ctl.setparameternumber(type, par); @@ -728,13 +933,19 @@ bool Master::runOSC(float *outl, float *outr, bool offline) } if(!d.matches) {// && !ports.apropos(msg)) { fprintf(stderr, "%c[%d;%d;%dm", 0x1B, 1, 7 + 30, 0 + 40); - fprintf(stderr, "Unknown address<BACKEND:%s> '%s:%s'\n", + fprintf(stderr, "Unknown address<BACKEND:%s> '%s:%s'\n", offline ? "offline" : "online", uToB->peak(), rtosc_argument_string(uToB->peak())); fprintf(stderr, "%c[%d;%d;%dm", 0x1B, 0, 7 + 30, 0 + 40); } } + + if(automate.damaged) { + d.broadcast("/damage", "s", "/automate/"); + automate.damaged = 0; + } + if(events>1 && false) fprintf(stderr, "backend: %d events per cycle\n",events); diff --git a/src/Misc/Master.h b/src/Misc/Master.h @@ -16,7 +16,7 @@ #define MASTER_H #include "../globals.h" #include "Microtonal.h" -#include <rtosc/miditable.h> +#include <rtosc/automations.h> #include <rtosc/ports.h> #include "Time.h" @@ -171,7 +171,7 @@ class Master WatchManager watcher; //Midi Learn - rtosc::MidiMapperRT midi; + rtosc::AutomationMgr automate; bool frozenState;//read-only parameters for threadsafe actions Allocator *memory; diff --git a/src/Misc/MiddleWare.cpp b/src/Misc/MiddleWare.cpp @@ -227,50 +227,50 @@ void preparePadSynth(string path, PADnoteParameters *p, rtosc::RtData &d) * MIDI Serialization * * * ******************************************************************************/ -void saveMidiLearn(XMLwrapper &xml, const rtosc::MidiMappernRT &midi) -{ - xml.beginbranch("midi-learn"); - for(auto value:midi.inv_map) { - XmlNode binding("midi-binding"); - auto biject = std::get<3>(value.second); - binding["osc-path"] = value.first; - binding["coarse-CC"] = to_s(std::get<1>(value.second)); - binding["fine-CC"] = to_s(std::get<2>(value.second)); - binding["type"] = "i"; - binding["minimum"] = to_s(biject.min); - binding["maximum"] = to_s(biject.max); - xml.add(binding); - } - xml.endbranch(); -} - -void loadMidiLearn(XMLwrapper &xml, rtosc::MidiMappernRT &midi) -{ - using rtosc::Port; - if(xml.enterbranch("midi-learn")) { - auto nodes = xml.getBranch(); - - //TODO clear mapper - - for(auto node:nodes) { - if(node.name != "midi-binding" || - !node.has("osc-path") || - !node.has("coarse-CC")) - continue; - const string path = node["osc-path"]; - const int CC = atoi(node["coarse-CC"].c_str()); - const Port *p = Master::ports.apropos(path.c_str()); - if(p) { - printf("loading midi port...\n"); - midi.addNewMapper(CC, *p, path); - } else { - printf("unknown midi bindable <%s>\n", path.c_str()); - } - } - xml.exitbranch(); - } else - printf("cannot find 'midi-learn' branch...\n"); -} +//void saveMidiLearn(XMLwrapper &xml, const rtosc::MidiMappernRT &midi) +//{ +// xml.beginbranch("midi-learn"); +// for(auto value:midi.inv_map) { +// XmlNode binding("midi-binding"); +// auto biject = std::get<3>(value.second); +// binding["osc-path"] = value.first; +// binding["coarse-CC"] = to_s(std::get<1>(value.second)); +// binding["fine-CC"] = to_s(std::get<2>(value.second)); +// binding["type"] = "i"; +// binding["minimum"] = to_s(biject.min); +// binding["maximum"] = to_s(biject.max); +// xml.add(binding); +// } +// xml.endbranch(); +//} +// +//void loadMidiLearn(XMLwrapper &xml, rtosc::MidiMappernRT &midi) +//{ +// using rtosc::Port; +// if(xml.enterbranch("midi-learn")) { +// auto nodes = xml.getBranch(); +// +// //TODO clear mapper +// +// for(auto node:nodes) { +// if(node.name != "midi-binding" || +// !node.has("osc-path") || +// !node.has("coarse-CC")) +// continue; +// const string path = node["osc-path"]; +// const int CC = atoi(node["coarse-CC"].c_str()); +// const Port *p = Master::ports.apropos(path.c_str()); +// if(p) { +// printf("loading midi port...\n"); +// midi.addNewMapper(CC, *p, path); +// } else { +// printf("unknown midi bindable <%s>\n", path.c_str()); +// } +// } +// xml.exitbranch(); +// } else +// printf("cannot find 'midi-learn' branch...\n"); +//} /****************************************************************************** * Non-RealTime Object Store * @@ -747,7 +747,7 @@ public: rtosc::UndoHistory undo; //MIDI Learn - rtosc::MidiMappernRT midi_mapper; + //rtosc::MidiMappernRT midi_mapper; //Link To the Realtime rtosc::ThreadLink *bToU; @@ -1202,24 +1202,24 @@ static rtosc::Ports middwareSnoopPorts = { impl.kitEnable(msg); d.forward(); rEnd}, - {"save_xlz:s", 0, 0, - rBegin; - const char *file = rtosc_argument(msg, 0).s; - XMLwrapper xml; - saveMidiLearn(xml, impl.midi_mapper); - xml.saveXMLfile(file, impl.master->gzip_compression); - rEnd}, - {"load_xlz:s", 0, 0, - rBegin; - const char *file = rtosc_argument(msg, 0).s; - XMLwrapper xml; - xml.loadXMLfile(file); - loadMidiLearn(xml, impl.midi_mapper); - rEnd}, - {"clear_xlz:", 0, 0, - rBegin; - impl.midi_mapper.clear(); - rEnd}, + //{"save_xlz:s", 0, 0, + // rBegin; + // const char *file = rtosc_argument(msg, 0).s; + // XMLwrapper xml; + // saveMidiLearn(xml, impl.midi_mapper); + // xml.saveXMLfile(file, impl.master->gzip_compression); + // rEnd}, + //{"load_xlz:s", 0, 0, + // rBegin; + // const char *file = rtosc_argument(msg, 0).s; + // XMLwrapper xml; + // xml.loadXMLfile(file); + // loadMidiLearn(xml, impl.midi_mapper); + // rEnd}, + //{"clear_xlz:", 0, 0, + // rBegin; + // impl.midi_mapper.clear(); + // rEnd}, //scale file stuff {"load_xsz:s", 0, 0, rBegin; @@ -1391,51 +1391,51 @@ static rtosc::Ports middwareSnoopPorts = { impl.undo.seekHistory(+1); rEnd}, //port to observe the midi mappings - {"midi-learn-values:", 0, 0, - rBegin; - auto &midi = impl.midi_mapper; - auto key = keys(midi.inv_map); - //cc-id, path, min, max -#define MAX_MIDI 32 - rtosc_arg_t args[MAX_MIDI*4]; - char argt[MAX_MIDI*4+1] = {0}; - int j=0; - for(unsigned i=0; i<key.size() && i<MAX_MIDI; ++i) { - auto val = midi.inv_map[key[i]]; - if(std::get<1>(val) == -1) - continue; - argt[4*j+0] = 'i'; - args[4*j+0].i = std::get<1>(val); - argt[4*j+1] = 's'; - args[4*j+1].s = key[i].c_str(); - argt[4*j+2] = 'i'; - args[4*j+2].i = 0; - argt[4*j+3] = 'i'; - args[4*j+3].i = 127; - j++; - - } - d.replyArray(d.loc, argt, args); -#undef MAX_MIDI - rEnd}, - {"learn:s", 0, 0, - rBegin; - string addr = rtosc_argument(msg, 0).s; - auto &midi = impl.midi_mapper; - auto map = midi.getMidiMappingStrings(); - if(map.find(addr) != map.end()) - midi.map(addr.c_str(), false); - else - midi.map(addr.c_str(), true); - rEnd}, - {"unlearn:s", 0, 0, - rBegin; - string addr = rtosc_argument(msg, 0).s; - auto &midi = impl.midi_mapper; - auto map = midi.getMidiMappingStrings(); - midi.unMap(addr.c_str(), false); - midi.unMap(addr.c_str(), true); - rEnd}, + //{"midi-learn-values:", 0, 0, + // rBegin; + // auto &midi = impl.midi_mapper; + // auto key = keys(midi.inv_map); + // //cc-id, path, min, max +//#define MAX_MIDI 32 + // rtosc_arg_t args[MAX_MIDI*4]; + // char argt[MAX_MIDI*4+1] = {0}; + // int j=0; + // for(unsigned i=0; i<key.size() && i<MAX_MIDI; ++i) { + // auto val = midi.inv_map[key[i]]; + // if(std::get<1>(val) == -1) + // continue; + // argt[4*j+0] = 'i'; + // args[4*j+0].i = std::get<1>(val); + // argt[4*j+1] = 's'; + // args[4*j+1].s = key[i].c_str(); + // argt[4*j+2] = 'i'; + // args[4*j+2].i = 0; + // argt[4*j+3] = 'i'; + // args[4*j+3].i = 127; + // j++; + + // } + // d.replyArray(d.loc, argt, args); +//#undef MAX_MIDI + // rEnd}, + //{"learn:s", 0, 0, + // rBegin; + // string addr = rtosc_argument(msg, 0).s; + // auto &midi = impl.midi_mapper; + // auto map = midi.getMidiMappingStrings(); + // if(map.find(addr) != map.end()) + // midi.map(addr.c_str(), false); + // else + // midi.map(addr.c_str(), true); + // rEnd}, + //{"unlearn:s", 0, 0, + // rBegin; + // string addr = rtosc_argument(msg, 0).s; + // auto &midi = impl.midi_mapper; + // auto map = midi.getMidiMappingStrings(); + // midi.unMap(addr.c_str(), false); + // midi.unMap(addr.c_str(), true); + // rEnd}, //drop this message into the abyss {"ui/title:", 0, 0, [](const char *msg, RtData &d) {}}, {"quit:", 0, 0, [](const char *, RtData&) {Pexitprogram = 1;}}, @@ -1482,10 +1482,10 @@ static rtosc::Ports middlewareReplyPorts = { if(impl.recording_undo) impl.undo.recordEvent(msg); rEnd}, - {"midi-use-CC:i", 0, 0, - rBegin; - impl.midi_mapper.useFreeID(rtosc_argument(msg, 0).i); - rEnd}, + //{"midi-use-CC:i", 0, 0, + // rBegin; + // impl.midi_mapper.useFreeID(rtosc_argument(msg, 0).i); + // rEnd}, {"broadcast:", 0, 0, rBegin; impl.broadcast = true; rEnd}, {"forward:", 0, 0, rBegin; impl.forward = true; rEnd}, }; @@ -1510,8 +1510,8 @@ MiddleWareImpl::MiddleWareImpl(MiddleWare *mw, SYNTH_T synth_, { bToU = new rtosc::ThreadLink(4096*2*16,1024/16); uToB = new rtosc::ThreadLink(4096*2*16,1024/16); - midi_mapper.base_ports = &Master::ports; - midi_mapper.rt_cb = [this](const char *msg){handleMsg(msg);}; + //midi_mapper.base_ports = &Master::ports; + //midi_mapper.rt_cb = [this](const char *msg){handleMsg(msg);}; if(preferrred_port != -1) server = lo_server_new_with_proto(to_s(preferrred_port).c_str(), LO_UDP, liblo_error_cb); diff --git a/src/Plugin/ZynAddSubFX/DistrhoPluginInfo.h b/src/Plugin/ZynAddSubFX/DistrhoPluginInfo.h @@ -42,6 +42,22 @@ enum Parameters { kParamOscPort, + kParamSlot1, + kParamSlot2, + kParamSlot3, + kParamSlot4, + kParamSlot5, + kParamSlot6, + kParamSlot7, + kParamSlot8, + kParamSlot9, + kParamSlot10, + kParamSlot11, + kParamSlot12, + kParamSlot13, + kParamSlot14, + kParamSlot15, + kParamSlot16, kParamCount }; diff --git a/src/Plugin/ZynAddSubFX/ZynAddSubFX.cpp b/src/Plugin/ZynAddSubFX/ZynAddSubFX.cpp @@ -226,6 +226,15 @@ protected: parameter.ranges.def = 0.0f; break; } + if(kParamSlot1 <= index && index <= kParamSlot16) { + parameter.hints = kParameterIsAutomable; + parameter.name = ("Slot " + zyn::to_s(index)).c_str(); + parameter.symbol = ("slot" + zyn::to_s(index)).c_str(); + parameter.unit = ""; + parameter.ranges.min = 0.0f; + parameter.ranges.max = 1.0f; + parameter.ranges.def = 0.5f; + } } /** @@ -238,9 +247,11 @@ protected: { case kParamOscPort: return oscPort; - default: - return 0.0f; } + if(kParamSlot1 <= index && index <= kParamSlot16) { + return master->automate.getSlot(index - 1); + } + return 0.0f; } /** @@ -249,9 +260,12 @@ protected: When a parameter is marked as automable, you must ensure no non-realtime operations are performed. @note This function will only be called for parameter inputs. */ - void setParameterValue(uint32_t /*index*/, float /*value*/) noexcept override + void setParameterValue(uint32_t index, float value) noexcept override { // only an output port for now + if(kParamSlot1 <= index && index <= kParamSlot16) { + master->automate.setSlot(index - 1, value); + } } /* --------------------------------------------------------------------------------------------------------