midiman
Version:
Manager for synthesizer sounds that are transmitted via MIDI sysex
659 lines (600 loc) • 17.5 kB
JavaScript
;
/**
* Class KorgPatch
* manages patches on an access virus
* A single patch consists of 2 pages __A and __B
* A multi patch consists of one buffer __C.
*/
// Must correspond to Korg Triton.html, TODO: transmit them here
const SingleBanks = "ABCDE HIJKLMN"; // This tells us, how to address the bank via midi
const MultiBanks = "ABCDEHIJKLMN";
var NetSingleBanks = SingleBanks.replace(/ /g, ''); // This tells us, how the banks are stored: unused banks are not stored
var NetMultiBanks = MultiBanks.replace(/ /g, '');
// For tracing
var Combi = require('./sysex');
var Sysex = Combi.kg;
const _NOPP = 0x6e;
const _DLC = 0x23;
const _PPDR = 0x1C;
const _CPDR = 0x1D;
const _CPPD = 0x40;
const _CCPD = 0x49;
const _PPD = 0x4c;
const _CPD = 0x4d;
const LengthOfProg = 540;
const LengthOfCombi = 448;
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
function hexblock(val) {
let res = val.toString(16);
if (res.length < 2) res = ' 0x0' + res;
else res = ' 0x' + res;
return res;
}
function _getMidiLength(iLng) {
var ml = Math.trunc(iLng/7)*8;
if (iLng%7) ml += iLng%7 +1;
return ml;
}
function _convertMidi2Int(midi) {
let intern = [];
for (let i=0; i< midi.length; i+=8) {
let carry = midi[i];
let scan = 1;
for (let j=1; j<8; j++) {
if (i+j>=midi.length) break;
if (scan&carry)
intern.push(midi[i+j] | 0x80);
else
intern.push(midi[i+j]);
scan = scan<<1;
}
}
return intern;
}
function _convertInt2Midi(intern) {
let midi = [];
for (let i=0; i<intern.length; i+= 7) {
let carry = 0;
let set = 1;
for (let j=0; j<7; j++) {
if (i+j >= intern.length) break;
if (intern[i+j] & 0x80) carry |= set;
set = set << 1;
}
midi.push(carry);
for (let j=0; j<7; j++) {
if (i+j >= intern.length) break;
midi.push(intern[i+j] & 0x7f);
}
}
return midi;
}
function _handleResponse(Ret, mIn) {
return new Promise((resolve,reject) => {
Ret.then((sx) => {
if (sx.command == _NOPP) {
let nds = new Sysex();
let nRet = nds.listen(mIn);
resolve(_handleResponse(nRet, mIn));
} else {
resolve(sx);
}
}).catch((e) =>{
console.log('exception occured in _handleResponse: ' + e);
reject(e);
});
});
}
/**
* class Mode
* handles the operating mode of the synth
*/
class Mode {
constructor() {
}
static #mode = undefined;
static #MR = 0x12;
static #MD = 0x42;
static #MC = 0x4e;
static get(mIn, mOut, mChan) {
if (Mode.#mode) return Promise.resolve(Mode.#mode);
return new Promise((resolve, reject) => {
var mr = new Sysex(mChan, Mode.#MR);
var ds = new Sysex();
var Ret = ds.listen(mIn);
mr.send(mOut);
Ret.then((sx) => {
if (sx.command == Mode.#MD) {
Mode.#mode = sx.raw[0];
resolve(Mode.#mode);
} else {
reject(`Received ${sx.command.toString(16)} istead of ${Mode.#MD.toString(16)}`);
}
})
.catch ((e) => {
reject(e);
});
});
}
static set(mIn, mOut, mChan, new_mode) {
return new Promise((resolve, reject) => {
let mc = new Sysex(mChan, Mode.#MC);
let ds = new Sysex();
let Ret = ds.listen(mIn);
mc.append([new_mode]);
mc.send(mOut);
Ret.then((sx) => {
//console.log("0x" + sx.command.toString(16));
setTimeout(() => {
Mode.#mode = new_mode;
resolve("Ok");
}, 1000);
});
});
}
}
class KorgPatch {
constructor() {
this._complete = false;
}
get complete() {
return this._complete;
}
get patchname() {
var res = "";
if (this.__sd == undefined) {
res = "<undefined>";
} else {
this.__sd.slice(0,16).forEach((val) => {
res += String.fromCharCode(val);
});
if (!this._complete) res += " <incomplete>";
}
return res;
}
/**
* bankLetter2Index
* Converts the bank letter of a bank of type btp into the internal address,
* i.e the index into the "patches" array
*/
static bankLetter2Index(btp, letter) {
if (btp == 'S')
return NetSingleBanks.indexOf(letter);
else
return NetMultiBanks.indexOf(letter) + NetSingleBanks.length;
}
static getDeviceMode(mIn, mOut, mChan) {
return Mode.get(mIn, mOut, mChan);
}
static _sendProgChange(mOut, mChan, to) {
let bn = to.charCodeAt(1) - "A".charCodeAt(0);
if (bn > 4) bn += 1;
let s1 = 0xb0 + mChan;
let s2 = 0xc0 + mChan;
//console.log(/*s1,0,0,*/hexblock(s1),"0x20",hexblock(bn),hexblock(s2),hexblock(Number(to.substr(2))));
mOut/*.send(s1, [0,0])*/.send(s1,[0x20,bn]).send(s2, [Number(to.substr(2))]);
}
/**
* changeProg
* triggers a program change to the desired program /combi number.
* This may send a mode change first.
*/
static changeProg(mIn, mOut, mChan, to) {
return new Promise((resolve,reject) => {
//console.log(`Change to ${to}`);
let combi = to[0]=='M'?0:2;
Mode.get(mIn, mOut, mChan).then((dm) => {
//console.log(`dm = ${dm}`);
if (combi != dm) {
Mode.set(mIn, mOut, mChan, combi).then(() => {
this._sendProgChange(mOut, mChan, to);
resolve("Ok");
});
} else {
this._sendProgChange(mOut, mChan, to);
resolve("Ok");
}
}).catch(e => {
reject(e);
});
});
}
/**
* readFromSynth
* reads the current edit patch into this object
*/
readFromSynth(mIn, mOut, mChan) {
//Sysex.debug = true;
return new Promise((resolve,reject) => {
var rd = new Sysex(mChan, this._CurParDumpRequest);
var ds = new Sysex();
var Ret = ds.listen(mIn);
rd.append([0]);
rd.send(mOut);
_handleResponse(Ret, mIn).then((sx) => {
if (sx.raw.shift()) console.log("Got MOSS patch instead of PCM patch");
this.__sd = _convertMidi2Int(sx.raw);
this._complete = true;
resolve("ok");
}).catch((e) =>{
console.log('exception occured in read from synth: ' + e);
reject(e);
});
});
}
/**
* writeToSynth
* writes this patch into the current edit patch on the synth
*/
writeToSynth(mIn, mOut, mChan) {
//Sysex.debug = true;
if (!this._complete) {
return Promise.reject("no patch");
}
return new Promise ((resolve, reject) => {
try {
var dump = new Sysex(mChan, this._CurParDump);
var ds = new Sysex;
var Ret = ds.listen(mIn);
dump.raw = [0];
this.sanitize();
dump.append(_convertInt2Midi(this.__sd));
dump.send(mOut);
Ret.then((sx) => {
if (sx.command == _DLC) {
resolve("ok");
} else {
reject(`Could not write data, received 0x${sx.command.toString(16)}`);
}
});
} catch(e) {
console.log('exception occured in write to synth: ' + e);
return Promise.reject(e);
}
});
}
writeToBlob() {
var dump = new Sysex(0, this._CurParDump); // the channel is just a placeholder
dump.raw = [0];
dump.append(_convertInt2Midi(this.__sd));
let dat = dump.asBlob();
return Buffer.from(Uint8Array.from(dat));
}
/**
* readFromBlob
* Reads a program or combi patch from a file that contains only one patch
* and returns it as a specific patch object (single or multi)
*/
static readFromBlob(ext, fbuf) {
var patch;
if (ext == ".syx") {
if (fbuf[4] == _CPPD) { //F0, 42, 3g, 50 Excl Header, 4C Function
patch = new KorgSinglePatch();
} else if (fbuf[4] == _CCPD) {
patch = new KorgMultiPatch();
} else {
throw `Cmd 0x${fbuf[4].toString(16)} instead of 0x${_CPPD.toString(16)} or 0x${_CCPD.toString(16)}`;
}
patch.__sd = _convertMidi2Int(fbuf.slice(6, -1));
} else if (ext == ".bin") {
if (fbuf.length == LengthOfProg) {
patch = new KorgSinglePatch();
} else if (fbuf.length == LengthOfCombi) {
patch = new KorgMultiPatch();
} else {
throw `Illegal length of binary file: ${fbuf.length}`;
}
patch.__sd = fbuf;
} else {
throw `wrong extension ${ext}`;
}
patch._complete = true;
return patch;
}
/**
*readMemoryBankFromSynth
* Reads one bank. Returns a thenable with the 'type' letter
* and the array of patches (either Single or Multi patches).
*/
readMemoryBankFromSynth(mIn, mOut, mChan, postdat) {
return new Promise((resolve,reject) => {
var Result = new Array(128);
let brq;
let fillfunc;
let ds = new Sysex();
let Ret = ds.listen(mIn);
brq = new Sysex(mChan, this._ParDumpRequest);
brq.append([0x10 | postdat.Bank, 0, 0]);
brq.send(mOut);
_handleResponse(Ret, mIn).then((sx) => {
if (sx.command == 0x24) reject(`Data load error ${sx.raw[0]}`);
if (sx.raw[0] & 1) reject(`MOSS board not supported: ${sx.raw[0]}`);
let IData = _convertMidi2Int(sx.raw.slice(3));
this.fillFromSysex(Result, IData);
setTimeout(() => {
resolve({pat:Result, type:postdat.type});
}, 1000);
}).catch ((e) =>{
console.log('exception occured in readMemoryBankFromSynth: ' + e);
reject(e);
});
});
}
/**
* writeMemoryToSynth
* writes the current bank in Mem to the Synth.
*/
writeMemoryBankToSynth(Mem, mIn, mOut, mChan, postdat) {
return new Promise((resolve, reject) => {
let dump = this.buildSysex(Mem, postdat.bnk[1], mChan);
let ds = new Sysex;
let Ret = ds.listen(mIn);
dump.send(mOut);
Ret.then((sx) => {
if (sx.command == _DLC)
resolve("ok");
else
reject(`Could not write data, received 0x${sx.command.toString(16)}`);
});
});
}
static readMemoryFromBlob(fbuf) {
var Result = [];
var som;
var btp;
var ml;
if (fbuf[0] != 0xf0) throw "Not a sysex file";
som = 0;
while (som < fbuf.length) {
let bank = [];
let patch;
let iData;
if (fbuf[som+4] == _PPD) { //F0, 42, 3g, 50 Excl Header, 4C Function
btp = 'S';
ml = _getMidiLength(LengthOfProg*128);
} else if (fbuf[som+4] == _CPD) {
btp = 'M';
ml = _getMidiLength(LengthOfCombi*128);
} else {
throw `Cmd 0x${fbuf[som+4].toString(16)} instead of 0x${_PPD.toString(16)} or 0x${_CPD.toString(16)}`;
}
som += 8; // TODO: some more checking
iData = _convertMidi2Int(fbuf.slice(som, som + ml));
for (let i=0; i<128; i++) {
if (btp == 'S')
patch = new KorgSinglePatch();
else
patch = new KorgMultiPatch();
patch.__sd = iData.slice(i*patch.dlength, (i+1)*patch.dlength);
patch._complete = true;
bank.push(patch);
}
Result.push({pat:bank, type:btp});
som += ml+1;
if (som >= fbuf.length) break; //preemptive termination is ok
if (fbuf[som] != 0xf0)
throw "Syntax error in sysex file";
}
return Result;
}
/**
* writeMemoryToBlob
* constructs a.asBlob from all the banks in datarr converted into sysexes
* implementation:
* iterate over all the banks in datarr
* make one sysex and concat it to the blob
*/
static writeMemoryToBlob(datarr) {
var dat = [];
var i = 0;
var BankTypeObject;
datarr.forEach((bank) => {
if (bank.type == 'S') {
BankTypeObject = new KorgSinglePatch();
} else {
BankTypeObject = new KorgMultiPatch();
}
let BankDump = BankTypeObject.buildSysex(datarr, BankTypeObject.index2BankLetter(i), 0);
dat = dat.concat(BankDump.asBlob());
i++;
});
return Buffer.from(Uint8Array.from(dat));
}
/**
* buildSysex
* will build a Sysex from a whole bank.
* Same code will work for both descendants.
*/
buildSysex(Mem, Bank, mChan) {
var bank = this.bankLetter2Ext(Bank);
var dump = new Sysex(mChan, this._ParDump);
dump.raw = [2, 0x10|bank, 0];
var idata = [];
Mem[this.bankLetter2Index(Bank)].pat.forEach((pt)=> {
idata = idata.concat(pt.__sd);
});
dump.append(_convertInt2Midi(idata));
return dump;
}
/**
* test
* checks, if a round trip thru the synth changes the clipboard patch
*/
test(mIn, mOut, mChan, postdat) {
//Sysex.trace = true;
console.log(`Testing ${this.patchname}`);
return new Promise((resolve,reject) => {
let save = this.__sd;
this.writeToSynth(mIn, mOut, mChan)
.then(() => {
return this.readFromSynth(mIn, mOut, mChan);
})
.then(() => {
resolve(this.diffTo(save));
})
.catch((e) => {
reject(e);
});
});
}
// performs the actual comparison
_diffTo(pat, minor) {
let rep;
let minor_fail = false;
if (!pat) return {changed:2, text:"Comparison data undefined"};
if (pat.length != this.dlength) console.log(`Size mismatch: ${pat.length} instead of ${this.dlength}`);
let size = pat.length < this.dlength ? pat.length : this.dlength;
for (let ind=0; ind < size; ind ++) {
if (this.__sd[ind] != pat[ind]) {
console.log(`${ind}: 0x${pat[ind].toString(16)} -> 0x${this.__sd[ind].toString(16)}`);
if (minor && minor.includes(ind)) {minor_fail = true; continue;}
if (!rep) rep = [ind, pat[ind], this.__sd[ind]];
}
}
if (rep) return {changed:2, text: `Byte ${rep[0]} changed from 0x${rep[1].toString(16)} to 0x${rep[2].toString(16)}`};
if (minor_fail) return {changed:1};
return {changed:0};
}
}
class KorgSinglePatch extends KorgPatch {
constructor () {
super();
this.dlength = LengthOfProg;
this._CurParDumpRequest = 0x10;
this._CurParDump = 0x40;
this._ParDumpRequest = _PPDR;
this._ParDump = _PPD;
}
fillFromSysex(Result, idata) {
for (let i=0; i <128; i++) {
Result[i] = new KorgSinglePatch();
Result[i].__sd = idata.slice(i*this.dlength,(i+1)*this.dlength);
if (Array.isArray(Result[i].__sd)) Result[i]._complete = true;
else Result[i].__sd = Uint8Array.from("Transmission Error");
}
}
/**
* bankLetter2Ext
* Converts the bank letter of a single bank into the external address,
* i.e the bank value in the midi telegram
*/
bankLetter2Ext(letter) {
return SingleBanks.indexOf(letter);
}
/**
* bankLetter2Index
* Converts the bank letter of a bank of type btp into the internal address,
* i.e the index into the "patches" array
*/
bankLetter2Index(letter) {
return NetSingleBanks.indexOf(letter);
}
index2BankLetter(i) {
return NetSingleBanks[i];
}
fillFromBlob(fbuf, som) {
this.__sd = fbuf.slice(som, som+this.dlength);
this._complete = true;
}
/**
* sanitize
* fixes illegal value combinations in the patch. The synthesizer seems to fix them "on the fly" while loading them,
* but when the patch is sent via sysex, it breaks the engine :-(
*/
sanitize() {
if (!this.__sd) return; // nothing to fix
/*
let vp = this.__sd.slice(34,36).concat(this.__sd.slice(58,60), this.__sd.slice(82,84), this.__sd.slice(106,108), this.__sd.slice(130,132));
console.log('Valve effect parameters: ' + vp.map(hexblock));
console.log('Master effect parameters: ' + this.__sd.slice(136,154).map(hexblock));
*/
// change the outputs of all insert effects from 3/4 to L/R, because the valve
// will be forced to L/R
for (let ieff=16; ieff <= 112; ieff += 24) {
if ((this.__sd[ieff+17] & 0x80) == 0) {
if (this.__sd[ieff+21] = 6)
this.__sd[ieff+21] = 0;
}
}
}
// filter out the known changes from the valve effect
// and the MSBit from ST BPM Long Dly
diffTo(pat) {
const valve = [34,35,49,58,59,82,83,106,107,130,131];
let res = this._diffTo(pat, valve);
switch (res.changed) {
case 0:
return "All bytes correctly compared equal";
case 1:
return "Patch compared equivalent";
case 2:
return res.text;
}
}
isA() {return "KorgProgramPatch";}
}
class KorgMultiPatch extends KorgPatch {
constructor () {
super();
this.dlength = LengthOfCombi;
this._CurParDumpRequest = 0x19;
this._CurParDump = 0x49;
this._ParDumpRequest = _CPDR;
this._ParDump = _CPD;
}
fillFromSysex(Result, idata) {
for (let i=0; i <128; i++) {
Result[i] = new KorgMultiPatch();
Result[i].__sd = idata.slice(i*this.dlength,(i+1)*this.dlength);
if (Array.isArray(Result[i].__sd)) Result[i]._complete = true;
else Result[i].__sd = Uint8Array.from("Transmission Error");
}
}
/**
* bankLetter2Ext
* Converts the bank letter of of a multi bank into the externel address,
* i.e the bank value in the midi telegram
*/
bankLetter2Ext(letter) {
return MultiBanks.indexOf(letter);
}
/**
* bankLetter2Index
* Converts the bank letter of a mult bank into the internal address,
* i.e the index into the "patches" array
*/
bankLetter2Index(letter) {
return NetMultiBanks.indexOf(letter) + NetSingleBanks.length;
}
index2BankLetter(i) {
return NetMultiBanks[i-NetSingleBanks.length];
}
fillFromBlob(fbuf, som) {
this.__sd = fbuf.slice(som, som+this.dlength);
this._complete = true;
}
sanitize() {
// nothing to fix (so far)
}
diffTo(pat) {
let res = this._diffTo(pat);
switch (res.changed) {
case 0:
return "All bytes correctly compared equal";
case 1:
return "Patch compared equivalent";
case 2:
return changed.text;
}
}
isA() {return "KorgCombiPatch";}
}
exports.base = KorgPatch;
exports.single = KorgSinglePatch;
exports.multi = KorgMultiPatch;