zynaddsubfx

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

commit a0228c1d51bec576f5f431797e92b93f68baf8c2
parent f3b20b975daca7c924d9951fb8b4f2676e72f1e2
Author: fundamental <mark.d.mccurry@gmail.com>
Date:   Sun,  1 Nov 2015 11:26:18 -0500

Midi-Learn: Update Rtosc Implementation

- Adds MIDI Learn Serialization
- Adds Multiple Concurrent Learning
- Adds Fine/Coarse Learning
- Adds A Bundle Of Undiscovered Bugs

Diffstat:
Msrc/Misc/Master.cpp | 69+++++++++++++++++++++++++++------------------------------------------
Msrc/Misc/Master.h | 2+-
Msrc/Misc/MiddleWare.cpp | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/Misc/XMLwrapper.cpp | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Misc/XMLwrapper.h | 27++++++++++++++++++++++++---
Msrc/Tests/MessageTest.h | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/UI/Connection.cpp | 2+-
Msrc/UI/MasterUI.fl | 26++++++++++++++++++++++++++
8 files changed, 320 insertions(+), 71 deletions(-)

diff --git a/src/Misc/Master.cpp b/src/Misc/Master.cpp @@ -166,6 +166,10 @@ static const Ports master_ports = { [](const char *m,RtData &d){ Master *M = (Master*)d.obj; M->noteOff(rtosc_argument(m,0).i,rtosc_argument(m,1).i);}}, + {"virtual_midi_cc:iii", rDoc("MIDI CC Event"), 0, + [](const char *m,RtData &d){ + Master *M = (Master*)d.obj; + M->setController(rtosc_argument(m,0).i,rtosc_argument(m,1).i,rtosc_argument(m,2).i);}}, {"setController:iii", rDoc("MIDI CC Event"), 0, [](const char *m,RtData &d){ @@ -189,19 +193,13 @@ static const Ports master_ports = { [](const char *,RtData &d) { Master *M = (Master*)d.obj; M->frozenState = false;}}, - {"register:iis", rDoc("MIDI Mapping Registration"), 0, - [](const char *m,RtData &d){ - Master *M = (Master*)d.obj; - M->midi.addElm(rtosc_argument(m,0).i, rtosc_argument(m,1).i,rtosc_argument(m,2).s);}}, - {"learn:s", rDoc("Begin Learning for specified address"), 0, - [](const char *m, RtData &d){ + {"midi-learn/", 0, &rtosc::MidiMapperRT::ports, + [](const char *msg, RtData &d) { Master *M = (Master*)d.obj; - printf("learning '%s'\n", rtosc_argument(m,0).s); - M->midi.learn(rtosc_argument(m,0).s);}}, - {"unlearn:s", rDoc("Remove Learning for specified address"), 0, - [](const char *m, RtData &d){ - Master *M = (Master*)d.obj; - M->midi.clear_entry(rtosc_argument(m,0).s);}}, + SNIP; + printf("residue message = <%s>\n", msg); + d.obj = &M->midi; + rtosc::MidiMapperRT::ports.dispatch(msg,d);}}, {"close-ui:", rDoc("Request to close any connection named \"GUI\""), 0, [](const char *, RtData &d) { d.reply("/close-ui", "");}}, @@ -236,12 +234,6 @@ static const Ports master_ports = { }; const Ports &Master::ports = master_ports; -#ifndef PLUGINVERSION -//XXX HACKS -Master *the_master; -rtosc::ThreadLink *the_bToU; -#endif - class DataObj:public rtosc::RtData { public: @@ -305,20 +297,22 @@ vuData::vuData(void) Master::Master(const SYNTH_T &synth_, Config* config) :HDDRecorder(synth_), ctl(synth_), microtonal(config->cfg.GzipCompression), bank(config), - midi(Master::ports), frozenState(false), pendingMemory(false), + frozenState(false), pendingMemory(false), synth(synth_), time(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);}; + memory = new AllocatorClass(); swaplr = 0; off = 0; smps = 0; bufl = new float[synth.buffersize]; bufr = new float[synth.buffersize]; -#ifndef PLUGINVERSION - the_master = this; -#endif last_xmz[0] = 0; fft = new FFTwrapper(synth.oscilsize); @@ -344,24 +338,6 @@ Master::Master(const SYNTH_T &synth_, Config* config) defaults(); -#ifndef PLUGINVERSION - midi.event_cb = [](const char *m) - { - char loc_buf[1024]; - DataObj d{loc_buf, 1024, the_master, the_bToU}; - memset(loc_buf, 0, sizeof(loc_buf)); - //printf("sending an event to the owner of '%s'\n", m); - Master::ports.dispatch(m, d, true); - }; -#else - midi.event_cb = [](const char *) {}; -#endif - - midi.error_cb = [](const char *a, const char *b) - { - fprintf(stderr, "MIDI- got an error '%s' -- '%s'\n",a,b); - }; - mastercb = 0; mastercb_ptr = 0; } @@ -372,9 +348,17 @@ void Master::applyOscEvent(const char *msg) DataObj d{loc_buf, 1024, this, bToU}; memset(loc_buf, 0, sizeof(loc_buf)); d.matches = 0; + + if(strcmp(msg, "/get-vu") && false) { + fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 5 + 30, 0 + 40); + fprintf(stdout, "backend[*]: '%s'<%s>\n", msg, + rtosc_argument_string(msg)); + fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 7 + 30, 0 + 40); + } + ports.dispatch(msg, d, true); if(d.matches == 0 || d.forwarded) - fprintf(stderr, "Unknown path '%s'\n", msg); + fprintf(stderr, "Unknown path '%s:%s'\n", msg, rtosc_argument_string(msg)); } void Master::defaults() @@ -460,7 +444,8 @@ void Master::setController(char chan, int type, int par) { if(frozenState) return; - midi.process(chan,type,par); + //TODO add chan back + midi.handleCC(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); diff --git a/src/Misc/Master.h b/src/Misc/Master.h @@ -166,7 +166,7 @@ class Master //Statistics on output levels vuData vu; - rtosc::MidiTable midi;//<1024,64> + rtosc::MidiMapperRT midi; bool frozenState;//read-only parameters for threadsafe actions Allocator *memory; diff --git a/src/Misc/MiddleWare.cpp b/src/Misc/MiddleWare.cpp @@ -41,9 +41,6 @@ #endif using std::string; -#ifndef PLUGINVERSION -extern rtosc::ThreadLink *the_bToU;//XXX -#endif /****************************************************************************** * LIBLO And Reflection Code * @@ -190,6 +187,55 @@ void preparePadSynth(string path, PADnoteParameters *p, rtosc::ThreadLink *uToB) } /****************************************************************************** + * 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"); +} + +/****************************************************************************** * Non-RealTime Object Store * * * * * @@ -356,6 +402,7 @@ namespace Nio }; } + /* Implementation */ class MiddleWareImpl : TmpFileMgr { @@ -587,8 +634,12 @@ public: std::atomic_int pending_load[NUM_MIDI_PARTS]; std::atomic_int actual_load[NUM_MIDI_PARTS]; + //Undo/Redo rtosc::UndoHistory undo; + //MIDI Learn + rtosc::MidiMappernRT midi_mapper; + //Link To the Realtime rtosc::ThreadLink *bToU; rtosc::ThreadLink *uToB; @@ -852,6 +903,20 @@ 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}, {"save_xmz:s", 0, 0, rBegin; const char *file = rtosc_argument(msg, 0).s; @@ -900,14 +965,24 @@ static rtosc::Ports middwareSnoopPorts = { printf("clear part port...\n"); impl.loadClearPart(extractInt(msg)); rEnd}, - {"/undo:", 0, 0, + {"undo:", 0, 0, rBegin; impl.undo.seekHistory(-1); rEnd}, - {"/redo:", 0, 0, + {"redo:", 0, 0, rBegin; impl.undo.seekHistory(+1); 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}, //drop this message into the abyss {"ui/title:", 0, 0, [](const char *msg, RtData &d) {}} }; @@ -947,6 +1022,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}, {"broadcast:", 0, 0, rBegin; impl.broadcast = true; rEnd}, {"foward:", 0, 0, rBegin; impl.forward = true; rEnd}, }; @@ -964,6 +1043,8 @@ MiddleWareImpl::MiddleWareImpl(MiddleWare *mw, SYNTH_T synth_, { bToU = new rtosc::ThreadLink(4096*2,1024); uToB = new rtosc::ThreadLink(4096*2,1024); + 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); @@ -990,9 +1071,6 @@ MiddleWareImpl::MiddleWareImpl(MiddleWare *mw, SYNTH_T synth_, idle = 0; idle_ptr = 0; -#ifndef PLUGINVERSION - the_bToU = bToU; -#endif master = new Master(synth, config); master->bToU = bToU; master->uToB = uToB; @@ -1106,8 +1184,8 @@ void MiddleWareImpl::broadcastToRemote(const char *rtmsg) void MiddleWareImpl::sendToRemote(const char *rtmsg, std::string dest) { - printf("sendToRemote(%s:%s,%s)\n", rtmsg, rtosc_argument_string(rtmsg), - dest.c_str()); + //printf("sendToRemote(%s:%s,%s)\n", rtmsg, rtosc_argument_string(rtmsg), + // dest.c_str()); if(dest == "GUI") { cb(ui, rtmsg); } else if(!dest.empty()) { @@ -1236,9 +1314,11 @@ void MiddleWareImpl::handleMsg(const char *msg) assert(strcmp(msg, "/sysefx0preset")); assert(strcmp(msg, "Psysefxvol0/part0")); - fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 6 + 30, 0 + 40); - fprintf(stdout, "middleware: '%s':%s\n", msg, rtosc_argument_string(msg)); - fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 7 + 30, 0 + 40); + if(strcmp("/get-vu", msg)) { + fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 6 + 30, 0 + 40); + fprintf(stdout, "middleware: '%s':%s\n", msg, rtosc_argument_string(msg)); + fprintf(stdout, "%c[%d;%d;%dm", 0x1B, 0, 7 + 30, 0 + 40); + } const char *last_path = rindex(msg, '/'); if(!last_path) { @@ -1252,7 +1332,9 @@ void MiddleWareImpl::handleMsg(const char *msg) //A message unmodified by snooping if(d.matches == 0 || d.forwarded) { - printf("Message Continuing on<%s:%s>...\n", msg, rtosc_argument_string(msg)); + if(strcmp("/get-vu", msg)) { + printf("Message Continuing on<%s:%s>...\n", msg, rtosc_argument_string(msg)); + } uToB->raw_write(msg); } else printf("Message Handled<%s:%s>...\n", msg, rtosc_argument_string(msg)); diff --git a/src/Misc/XMLwrapper.cpp b/src/Misc/XMLwrapper.cpp @@ -621,3 +621,55 @@ mxml_node_t *XMLwrapper::addparams(const char *name, unsigned int params, } return element; } + +XmlNode::XmlNode(std::string name_) + :name(name_) +{} + +std::string &XmlNode::operator[](std::string name) +{ + //fetch an existing one + for(auto &a:attrs) + if(a.name == name) + return a.value; + + //create a new one + attrs.push_back({name, ""}); + return attrs[attrs.size()-1].value; +} + +bool XmlNode::has(std::string name_) +{ + //fetch an existing one + for(auto &a:attrs) + if(a.name == name_) + return true; + return false; +} + +void XMLwrapper::add(const XmlNode &node_) +{ + mxml_node_t *element = mxmlNewElement(node, node_.name.c_str()); + for(auto attr:node_.attrs) + mxmlElementSetAttr(element, attr.name.c_str(), + attr.value.c_str()); +} + +std::vector<XmlNode> XMLwrapper::getBranch(void) const +{ + std::vector<XmlNode> res; + mxml_node_t *current = node->child; + while(current) { + if(current->type == MXML_ELEMENT) { + auto elm = current->value.element; + XmlNode n(elm.name); + for(int i=0; i<elm.num_attrs; ++i) { + auto &attr = elm.attrs[i]; + n[attr.name] = attr.value; + } + res.push_back(n); + } + current = mxmlWalkNext(current, node, MXML_NO_DESCEND); + } + return res; +} diff --git a/src/Misc/XMLwrapper.h b/src/Misc/XMLwrapper.h @@ -24,13 +24,30 @@ #include <mxml.h> #include <string> -#ifndef float -#define float float -#endif +#include <vector> #ifndef XML_WRAPPER_H #define XML_WRAPPER_H +class XmlAttr +{ + public: + std::string name; + std::string value; +}; + + +class XmlNode +{ + public: + XmlNode(std::string name_); + std::string name; + std::vector<XmlAttr> attrs; + + std::string &operator[](std::string name); + bool has(std::string); +}; + /**Mxml wrapper*/ class XMLwrapper { @@ -220,6 +237,10 @@ class XMLwrapper */ bool hasPadSynth() const; + void add(const XmlNode &node); + + std::vector<XmlNode> getBranch(void) const; + private: /** diff --git a/src/Tests/MessageTest.h b/src/Tests/MessageTest.h @@ -36,7 +36,7 @@ #include "../Misc/Util.h" #include "../globals.h" #include "../UI/NSM.H" -NSM_Client *nsm = 0; +class NSM_Client *nsm = 0; MiddleWare *middleware = 0; using namespace std; @@ -50,9 +50,10 @@ class MessageTest:public CxxTest::TestSuite public: Config config; void setUp() { - synth = new SYNTH_T; - mw = new MiddleWare(std::move(*synth), &config); - ms = mw->spawnMaster(); + synth = new SYNTH_T; + mw = new MiddleWare(std::move(*synth), &config); + ms = mw->spawnMaster(); + realtime = NULL; } void tearDown() { @@ -60,7 +61,8 @@ class MessageTest:public CxxTest::TestSuite delete synth; } - void testKitEnable(void) +#if 0 + void _testKitEnable(void) { const char *msg = NULL; mw->transmitMsg("/part0/kit0/Psubenabled", "T"); @@ -72,7 +74,7 @@ class MessageTest:public CxxTest::TestSuite TS_ASSERT_EQUALS(string("/part0/kit0/Psubenabled"), msg); } - void testBankCapture(void) + void _testBankCapture(void) { mw->transmitMsg("/bank/slots", ""); TS_ASSERT(!ms->uToB->hasNext()); @@ -82,7 +84,7 @@ class MessageTest:public CxxTest::TestSuite TS_ASSERT_EQUALS(string("/bank/fake"), msg); } - void testOscCopyPaste(void) + void _testOscCopyPaste(void) { //Enable pad synth mw->transmitMsg("/part0/kit0/Ppadenabled", "T"); @@ -119,11 +121,92 @@ class MessageTest:public CxxTest::TestSuite do_exit = 1; t.join(); TS_ASSERT_EQUALS(ms->part[0]->kit[0].padpars->oscilgen->Pbasefuncpar, 32); + } +#endif + + void start_realtime(void) + { + do_exit = false; + realtime = new std::thread([this](){ + int tries = 0; + while(tries < 10000) { + if(!ms->uToB->hasNext()) { + if(do_exit) + break; + + usleep(500); + continue; + } + const char *msg = ms->uToB->read(); + printf("RT: handling <%s>\n", msg); + ms->applyOscEvent(msg); + }}); + } + + void stop_realtime(void) + { + do_exit = true; + realtime->join(); + delete realtime; + realtime = NULL; + } + + void run_realtime(void) + { + start_realtime(); + stop_realtime(); + } + + void testMidiLearn(void) + { + mw->transmitMsg("/learn", "s", "/Pvolume"); + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 108); + TS_ASSERT_EQUALS(ms->Pvolume, 80); + + //Perform a learning operation + + run_realtime(); //1. runs learning and identifies a CC to bind + mw->tick(); //2. produces new binding table + run_realtime(); //3. applies new binding table + + + //Verify that the learning actually worked + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 13); + run_realtime(); + TS_ASSERT_EQUALS(ms->Pvolume, 13); + + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 2); + run_realtime(); + TS_ASSERT_EQUALS(ms->Pvolume, 2); + + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 0); + run_realtime(); + TS_ASSERT_EQUALS(ms->Pvolume, 0); + + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 127); + run_realtime(); + TS_ASSERT_EQUALS(ms->Pvolume, 127); + } + + void testMidiLearnSave(void) + { + mw->transmitMsg("/learn", "s", "/Pvolume"); + mw->transmitMsg("/virtual_midi_cc", "iii", 0, 23, 108); + + //Perform a learning operation + + run_realtime(); //1. runs learning and identifies a CC to bind + mw->tick(); //2. produces new binding table + run_realtime(); //3. applies new binding table + mw->transmitMsg("/save_xlz", "s", "test-midi-learn.xlz"); + mw->transmitMsg("/load_xlz", "s", "test-midi-learn.xlz"); } private: - SYNTH_T *synth; - MiddleWare *mw; - Master *ms; + SYNTH_T *synth; + MiddleWare *mw; + Master *ms; + std::thread *realtime; + bool do_exit; }; diff --git a/src/UI/Connection.cpp b/src/UI/Connection.cpp @@ -263,7 +263,7 @@ class UI_Interface:public Fl_Osc_Interface ////fprintf(stderr, "."); //fprintf(stderr, "write(%s:%s)\n", s.c_str(), args); //fprintf(stderr, "%c[%d;%d;%dm", 0x1B, 0, 7 + 30, 0 + 40); - impl->transmitMsg(s.c_str(), args, va); + impl->transmitMsg_va(s.c_str(), args, va); va_end(va); } diff --git a/src/UI/MasterUI.fl b/src/UI/MasterUI.fl @@ -305,6 +305,32 @@ class MasterUI {open xywh {15 15 100 20} divider } MenuItem {} { + label {&Load Midi Learn...} + callback {char *filename; +filename=fl_file_chooser("Open:","({*.xlz})",NULL,0); +if (filename==NULL) return; + +osc->write("/load_xlz", "s", filename);} + xywh {40 40 100 20} + } + MenuItem {} { + label {Save Midi Learn...} + callback {char *filename; +int result; +filename=fl_file_chooser("Save:","({*.xlz})",NULL,0); +if (filename==NULL) return; +filename=fl_filename_setext(filename,".xlz"); + +result=fileexists(filename); +if (result) { + result=0; + if (!fl_choice("The file exists. \\nOverwrite it?","No","Yes",NULL)) return; +}; + +osc->write("/save_xlz", "s", filename);} + xywh {30 30 100 20} divider + } + MenuItem {} { label {&Load Scale Settings...} callback {char *filename; filename=fl_file_chooser("Open:","({*.xsz})",NULL,0);