UNPKG

midiman

Version:

Manager for synthesizer sounds that are transmitted via MIDI sysex

599 lines (545 loc) 19.1 kB
"use strict" const defaultBorderstyle = "outset"; const highlightBorderstyle = "inset"; const ButtonLabels = SingleReadBanks + MultiReadBanks; const ButtonPrefix = "SF"; var NetSingleBanks = SingleReadBanks.replace(/ /g, ''); var NetMultiBanks = MultiReadBanks.replace(/ /g, ''); var LastProgramChangedTo; /** * class PatId * Generalized patch id. * Consists of: * *not* Source/Destination: S[ynth] of F[ile] , because this is determined by the containing class * *not* Bank Type: S[ingle] or M[ulti] , because this can be calculated from the bank number * *not* display page: 1 or 2 , because this can be calculated from pnum * Bank Number: 0..14(currently), depending on Synth architecture * Patch Number: 0..127, depending on Synth architecture * It takes account of 'holes' (space characters) in the ..ReadBanks specifiers, which * have no corresponding entry in the 'pat' array and no bank number * With pnum = 0 or 64 it will also be used as value for Navi._curpage */ class PatId { constructor(bank, pnum) { this._bank = bank; this._pnum = pnum; } get BankTypePrefix() { return this._bank < NetSingleBanks.length ? 'S' : 'M'; } get BankLetter() { if (this.BankTypePrefix == 'S') return NetSingleBanks[this._bank]; else return NetMultiBanks[this._bank-NetSingleBanks.length]; } get bank() {return this._bank;} get pnum() {return this._pnum;} get page() {return this._pnum < 64 ? 0 : 1;} /** * toString: a readable and parseable string representation */ toString() { var str = this.BankTypePrefix; str += this.BankLetter; str += this.pnum; return str; } /** * asButtonId * a readable id for a select button. * Needs to be told, where this instance is in. * Does not include pnum, but calculated page. */ asButtonId(synth_or_file) { var str = synth_or_file + this.BankTypePrefix;; str += this.BankLetter; str += this.pnum < 64 ? '1' : '2'; return str; } /** * convertDndToServer * converts the drop source/target id to a server string. * The server string takes the form [sf][SM][A-N]\d{1,3} with the number in the range 0-127. */ convertDndToServer(target_id) { let sv = target_id[0].toLowerCase(); sv += this.BankTypePrefix; sv += this.BankLetter; let num = Number(target_id.substr(2)); if (this.page) num += 60; // patchnumbers on page 2 start at 60! return sv + num; } } // don't let the 'this' confuse you here, it is resolved dynamically // https://www.quirksmode.org/js/this.html function dblclicker() {onDoubleClick(this.id);} /** * class Navi * organizes the navigation and drag'n'drop on the patch tables. * Provides ids for the patches on server side and on ui side. * Patches on the server are organized in 8 banks @ 128 patches. * Patches on the ui side are organized in 8 banks @ 2 pages @ 64 patches (Numbers apply to access virus). * Additionally, there are 2 slots for the synth patches and the file patches, so * there is one instance for the SynthPatches and one for the FilePatches. * One extra patch is for the clipboard. * A side consideration is that HTML element ids should be unique on a page and should not * be changed once they are assigned. * Banks of patches can be of several types (Single, Multi), which are denoted by 'type' letters (currently 'S' or 'M'). * The original sysex data in a bank are the "pat" properties of the bank. * To distinguish between "Single Patches" or "Programs" and "Multi Patches" or "Combinations" * the former are denoted by upper case letters and the latter by lower case letters. */ class Navi { constructor(pid, seltab) { this._pelem = document.getElementById(pid); this._seltab = seltab; this._patches = undefined; this._curpage = undefined; } get patches() {return this._patches;} set patches(pat) {this._patches = pat;} // The current page member is of class PatId get curpage() {return this._curpage;} set curpage(pg) {this._curpage = pg;} // push a patch name (instead of getting and setting push_pat(pat) {this._patches.push(pat);} // complete pnum with the info from curpage and set the text // of the corresponding entry in this._patches set_pat(pnum, text) { this._patches[this._curpage.bank].pat[pnum] = text; } /** * swap * exchanges the patches of the current page in SynthPatches and FilePatches. * The current page members are corrected "on the fly". * It can be invoked on any of the two instances with the same result. * Warning: invoking it a second time on the other instance reverses the effect of the first invocation! */ swap(rOther) { var tmp = this._patches; this._patches = rOther._patches; rOther._patches = tmp; if (this._curpage) document.getElementById(this._curpage.asButtonId(this._buttonPrefix())).style.borderStyle = defaultBorderstyle; if (rOther._curpage) document.getElementById(rOther._curpage.asButtonId(rOther._buttonPrefix())).style.borderStyle = defaultBorderstyle; tmp = this.curpage; this._curpage = rOther._curpage; rOther._curpage = tmp; } /** * prepareSwitchTable * Constructs the buttons for the switch tables including the unique ids. */ prepareSwitchTable(TabId, ButList) { var Tab = document.getElementById(TabId); Tab.textContent = ""; var CurrentRow; var Offset; if (TabId[1] == 'm') Offset = NetSingleBanks.length; else Offset = 0; for(let i=0; i< ButList.length; i++) { if (i%PagesPerRow == 0) { CurrentRow = Tab.insertRow(-1); } let td = CurrentRow.insertCell(-1); td.className = this._seltab[0] == 's' ? "slb" : "flb"; let btn = document.createElement("button"); let pid = new PatId(i+Offset,0); btn.innerText = ButList[i] + "1"; btn.onclick = this._makeDisplayNames(pid); btn.id = pid.asButtonId(this._buttonPrefix()); btn.className = 'lbb'; td.appendChild(btn); td = CurrentRow.insertCell(-1); td.className = this._seltab[0] == 's' ? "slb" : "flb"; btn = document.createElement("button"); pid = new PatId(i+Offset,64); btn.innerText = ButList[i] + "2"; btn.onclick = this._makeDisplayNames(pid); btn.id = pid.asButtonId(this._buttonPrefix()); btn.className = 'lbb'; td.appendChild(btn); } } /** * refreshDisplay * extracts bank and page from curpage and calls displayNames. */ refreshDisplay() { var patid; if (this._curpage == undefined) { patid = new PatId(0,0); // erase display } else { patid = this._curpage; } this.displayNames(patid); } /** * displayNames * displays the patchnames of the current page (identified by patid). * It also sets the drag'n'drop ids from the fields. */ displayNames(patid) { var arr = this._pelem.querySelectorAll(".pname"); var ind = this._pelem.querySelectorAll(".pnh"); var i = 0; var tabdat; this._highlightButton(patid); if (this._patches == undefined || this.patches[patid.bank] == undefined) { for (;i<arr.length;) { arr[i].removeEventListener('dblclick', dblclicker); arr[i++].innerText = ""; } return; } if (patid.page == 0) { for (let n=0; n<ind.length; n++) { ind[n].innerText = n.toString() + "_"; } } else { for (let n=0; n<ind.length; n++) { ind[n].innerText = (n+6).toString() + "_"; } } let btab = this._patches[patid.bank].pat; if (btab && patid.page == 0) { tabdat = btab.slice(0,70); } else if (btab) { tabdat = btab.slice(60); } if (tabdat) { tabdat.forEach((dt) => { arr[i].removeEventListener('dblclick', dblclicker); arr[i].innerText = dt; arr[i].setAttribute("draggable", true); // build the id for the fields, we also need the bank type prefix to remeber it on the clipboard arr[i].id = this._buttonPrefix() + patid.BankTypePrefix + i; arr[i].addEventListener('dblclick', dblclicker); arr[i++].setAttribute("ondragstart","dragStart(event)"); }); } for (;i<arr.length;) { arr[i].removeEventListener('dblclick', dblclicker); arr[i].innerText = ""; arr[i].setAttribute("draggable", false); arr[i++].removeAttribute("ondragstart"); } } /** * serverFromTarget * determines the patch id for the server from the curpage and the drop target (or drop source). * The current page member is an object of class PatId. * The drop targets/sources are "S" or "F" followed by the BankTypePrefix and the * relative patch number 0-69. (Take care of the "decimal view"!) * The server string takes the form [sf][SM][A-N]\d{1,3} with the number in the range 0-127. * A special case is the clipboard, which has only "c" as the server string. */ static serverFromTarget(tg) { var sv; var num; switch (tg[0]) { case "c": return "c"; case "S": return SynthPatches.curpage.convertDndToServer(tg); case "F": return FilePatches.curpage.convertDndToServer(tg); } } _buttonPrefix() { if (this._seltab == "slb") return ButtonPrefix[0]; else return ButtonPrefix[1]; } _highlightButton(patid) { if (this._curpage) { document.getElementById(this._curpage.asButtonId(this._buttonPrefix())).style.borderStyle = defaultBorderstyle; } this._curpage = patid; document.getElementById(this._curpage.asButtonId(this._buttonPrefix())).style.borderStyle = highlightBorderstyle; } _makeDisplayNames(patid) { var This = this; return function() { This.displayNames(patid); }; } } var SynthPatches; var FilePatches; var ClipboardType; function forcequit() { var xhttp = new XMLHttpRequest(); xhttp.open("GET", '/forcequit', true); xhttp.send(); //ignore the answer }; function getJsonData(url, func) { var jsonResponse, error, detail; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4) { if (this.status == 200) { try { jsonResponse = JSON.parse(this.responseText) func(jsonResponse); return; } catch(e) { error = "parse/function"; detail = e.name; } } else { error = "HTML"; detail = this.status; } alert("get failed: " + url + ", error: " + error + ", d:" + detail); } // ignore other ready states }; xhttp.open("GET", url, true); xhttp.send(); } function getJsonParam(url, param, func) { var jsonResponse, error, detail; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4) { if (this.status == 200) { try { jsonResponse = JSON.parse(this.responseText) func(jsonResponse); return; } catch(e) { error = "JSON parse error"; detail = e.name; alert("Did not receive json data from " + url + ", error: " + error + ", detail:" + detail); } } else { error = "HTML error"; detail = this.status; } } // ignore other ready states }; xhttp.open("POST", url, true); xhttp.setRequestHeader('Content-Type', 'text/json; charset=utf-8'); xhttp.send(param); } function quit() { window.open('/quit','_self'); } function selectInterface() { var Settings = {MidiIn:document.getElementById("MidiIn").value, MidiOut:document.getElementById("MidiOut").value, MidiChan:document.getElementById("MidiChan").value, Mdl:Model}; getJsonParam('/selIfc', JSON.stringify(Settings), (data) => { if (data.error) document.getElementById("Result").innerText = data.error; else document.getElementById("c").innerText = data.patch; }); } function readCurrentPatch() { let Settings = {Mdl: Model}; getJsonParam('/readPatch', JSON.stringify(Settings), (data) => { if (data.error) document.getElementById("Result").innerText = data.error; else document.getElementById("c").innerText = data.patch; }); } function writeCurrentPatch() { let Settings = {Mdl: Model}; getJsonParam('/writePatch', JSON.stringify(Settings), (data) => { if (data.error) document.getElementById("Result").innerText = data.error; else document.getElementById("c").innerText = data.patch; }); } /* * Note: It is useless to return a 'thenable' from the function for 'getJsonParam' * because it will not be used. * This schebang is working more by chance, but the mix of recursion and sequence is not easy. * TODO: Find something more straight forward. */ function readMemoryBank(i, typ, followup) { let Settings = {Mdl: Model, Bank: i, type: typ}; let ctrl; if (typ == 'S') { ctrl = SingleReadBanks; } else { ctrl = MultiReadBanks; } document.getElementById("Result").innerText = `Receiving synth memory ${ctrl[i]}`; try { getJsonParam('/readMemory', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; if (data.names) { SynthPatches.push_pat(data.names); } do { i++; } while (i < ctrl.length && ctrl[i] == ' '); if (i < ctrl.length) { readMemoryBank(i, typ, followup); } else if(followup) { followup(i); } }); } catch (e) { document.getElementById("Result").innerText = `Error reading memory banks: ${e}`; } } function readMemoryBanks() { SynthPatches.patches = []; var display = function(ign) {SynthPatches.displayNames(new PatId(0,0));}; var readMultis = function(i) {readMemoryBank(0, 'M', display);}; readMemoryBank(0, 'S', readMultis); } function writeMemory() { if (SynthPatches.curpage == undefined) { document.getElementById("Result").innerText = 'No page for upload'; return; } let Settings = {Mdl: Model, bnk:SynthPatches.curpage.BankTypePrefix+SynthPatches.curpage.BankLetter}; document.getElementById("Result").innerText = 'Writing current bank to synth'; getJsonParam('/writeMemory', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; }); } function readFile() { var fl = new FormData(document.getElementById("readForm")).get("fname"); var rd = new FileReader(); rd.onloadend = function() { let Settings = {Mdl: Model, Cont:rd.result}; getJsonParam( '/readFile', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; if (data.names) { FilePatches.patches = data.names; FilePatches.displayNames(new PatId(0,0)); } }); }; rd.readAsDataURL(fl); } function comparePatch() { let fl = new FormData(document.getElementById("compareForm")).get("cfname"); let rd = new FileReader(); let extension = fl.name.substr(fl.name.lastIndexOf('.')); rd.onloadend = function() { let Settings = {Mdl: Model, ext: extension, Cont:rd.result}; getJsonParam( '/comparePatch', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; }); }; rd.readAsDataURL(fl); } function test() { let Settings = {Mdl: Model}; document.getElementById("Result").innerText = ""; getJsonParam('/test', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; }); } function swap() { getJsonData('/swap?Mdl=' + Model, function() { SynthPatches.swap(FilePatches); SynthPatches.refreshDisplay(); FilePatches.refreshDisplay(); }); } function allowDrop(ev) { ev.preventDefault(); } function dragStart(ev) { ev.dataTransfer.setData("text", ev.target.innerText); ev.dataTransfer.setData("id", ev.target.id); } function drop(ev) { ev.preventDefault(); if (!ev.dataTransfer) { document.getElementById("Result").innerText = `drop with no source from ${ev}` ; return; } let src_id = ev.dataTransfer.getData("id"); let src_txt = ev.dataTransfer.getData("text"); let dest_id = ev.target.id; let dest_txt = ev.target.text; let Settings = { from: Navi.serverFromTarget(src_id), to: Navi.serverFromTarget(dest_id), Mdl: Model }; getJsonParam('/move', JSON.stringify(Settings), (answ) => { if (answ.ok) { document.getElementById(dest_id).innerText = answ.ok; if (dest_id[0] == 'S' ) SynthPatches.set_pat(Number(dest_id.substr(2)), src_txt); if (dest_id[0] == 'F' ) FilePatches.set_pat(Number(dest_id.substr(2)), src_txt); if (dest_id[0] == 'c' ) ClipboardType = dest_id[1]; } else { document.getElementById("Result").innerText = answ.error; } }); } function onDoubleClick(id) { document.getElementById("Result").innerText = ""; let Settings = {Mdl: Model, to:Navi.serverFromTarget(id)}; getJsonParam('/changeProg', JSON.stringify(Settings), (data) => { LastProgramChangedTo = Settings.to; document.getElementById("Result").innerText = data.result; }); } function compare() { document.getElementById("Result").innerText = ""; let Settings = {Mdl: Model, to:LastProgramChangedTo}; getJsonParam('/compare', JSON.stringify(Settings), (data) => { document.getElementById("Result").innerText = data.result; }); } function prepareSwitchTable() { SynthPatches.prepareSwitchTable("ssstab", NetSingleBanks); SynthPatches.prepareSwitchTable("smstab", NetMultiBanks); FilePatches.prepareSwitchTable("fsstab", NetSingleBanks); FilePatches.prepareSwitchTable("fmstab", NetMultiBanks); } function displayForm() { var Settings; SynthPatches = new Navi("stab","slb"); FilePatches = new Navi("ftab","flb"); var sel1 = document.getElementById("MidiOut"); getJsonData('/outputs?Mdl=' + Model, (answ) => { answ.list.forEach((nam) => { var opt = document.createElement("OPTION"); opt.text = nam; sel1.add(opt); }); Settings = answ.settings; if (Settings) sel1.value = Settings.MidiOut; else document.getElementById("Result").innerText = answ.error; }); var sel2 = document.getElementById("MidiIn"); getJsonData('/inputs?Mdl=' + Model, (answ) => { answ.list.forEach((nam) => { var opt = document.createElement("OPTION"); opt.text = nam; sel2.add(opt); }); if (Settings) { sel2.value = Settings.MidiIn; document.getElementById("MidiChan").value = Settings.MidiChan; document.title = Settings.name; } else { document.getElementById("Result").innerText = answ.error; } }); document.getElementById("readpatch").addEventListener('click',readCurrentPatch); document.getElementById("writepatch").addEventListener('click',writeCurrentPatch); document.getElementById("readMem").addEventListener('click',readMemoryBanks); document.getElementById("writeMem").addEventListener('click',writeMemory); document.getElementById("readFile").addEventListener('click',readFile); document.getElementById("swapbutton").addEventListener('click',swap); document.getElementById("test").addEventListener('click',test); } window.addEventListener("load", displayForm); window.addEventListener("load", prepareSwitchTable);