midiman
Version:
Manager for synthesizer sounds that are transmitted via MIDI sysex
461 lines (417 loc) • 14.9 kB
JavaScript
;
const finalhandler = require('finalhandler');
const http = require('http');
const { spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
global.navigator = require('web-midi-api');
if (!global.performance) global.performance = { now: require('performance-now') };
var WebMidi = new require('webmidi');
var Roland = require(__dirname + '/proc/roland.js');
var Access = require(__dirname + '/proc/access.js');
var Korg = require(__dirname + '/proc/korg.js');
const hostname = 'localhost';
//const hostname = '192.168.32.31';
const port = 10532;
const src_dir = 'file://' + __dirname + '/';
const Express = require('express');
var app = Express();
app.use(Express.json());
app.use(Express.urlencoded({extended:true}));
var server = http.createServer(function(req, res) {
app(req, res, finalhandler(req, res));
});
var AllSettings;
var Triggered = false;
var MySettings = [];
var theSynthesizers = [];
const SynthClasses = {"Roland":Roland, "Access":Access, "Korg": Korg};
var bServerMode = process.argv[process.argv.length-1] == "-S" || process.argv[process.argv.length-1] == "--server-only";
function readSettings(model) {
try {
var settings = MySettings.find((it) =>{return it.name == model;});
if (settings) return settings;
var fn = os.homedir() + path.sep + ".synths.json";
AllSettings = JSON.parse(fs.readFileSync(fn));
settings = AllSettings.find((it) =>{return it.name == model;});
if (settings) {
let mIn = WebMidi.getInputByName(settings.MidiIn);
let mOut = WebMidi.getOutputByName(settings.MidiOut);
let mChan = settings.MidiChan-1;
let that = new SynthClasses[settings.class](mIn, mOut, mChan);
let Instance = {name: settings.name, synth: that};
theSynthesizers.push(Instance);
MySettings.push(settings);
return settings;
}
else
return undefined;
} catch (e) {
console.log("Cannot read settings, " + e);
return undefined;
}
}
function getInstance(model) {
readSettings(model);
return theSynthesizers.find((it) => {return it.name == model;}).synth;
}
function handlePost(url, func) {
app.post(url, (req, res) => {
//console.log(`Received a post to ${req.path}`);
var querydata = "";
req.on('data', (data) => {
//console.log(`Received data for ${req.path}`);
querydata += data;
if (querydata.length > 1e8) {
querydata = "";
res.writeHead(413, {'Content-Type': 'text/plain'}).end();
req.connection.destroy();
}
}).on('end', () => {
try {
//console.log(`Received end for ${req.path}`);
func(JSON.parse(querydata), res);
} catch (e){
console.log(`error handling ${req.path}: ${e}`);
}
}).on('error', (err) => {
console.log(`Received error ${err} for ${req.path}`);
});
});
}
function deliver(path, res) {
let req = new URL(path);
if (req.pathname.endsWith("html")) {
res.setHeader('Content-Type', 'text/html');
} else if (req.pathname.endsWith("js")) {
res.setHeader('Content-Type', 'text/javascript');
}
fs.readFile(req, function(err, data) {
if (err) {
console.log(req.toString() + ' not accesible');
res.statusCode = 403;
res.statusMessage = 'Forbidden';
res.end('not accesible');
} else {
console.log('sending ' + path);
res.end(data);
}
});
}
// forcing quit when window unloads:
app.get('/forcequit', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
console.log('forcing quit');
WebMidi.removeListener();
WebMidi.disable();
server.close();
process.exit(0); // TODO: make sure everything is written yet
});
// TODO: quit also when closing the browser
app.get('/quit', function (req, res) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.write('<html><head><script>document.onload = function() {window.close();};</script></head>');
//res.write('<html><head><script>function max_zu() {window.close();};</script></head>');
res.end('<body>Bye!</body></html>');
if (!bServerMode) {
server.close();
WebMidi.disable();
console.log('server closed and WebMidi disabled');
process.exit(0);
}
});
app.get('/swap',function (req,res) {
getInstance(req.query.Mdl).swap();
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.end('"Ok"');
});
handlePost('/move',function (req,res) {
//console.log(`moving ${req.from} to ${req.to}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.end(JSON.stringify(getInstance(req.Mdl).move(req.from, req.to)));
});
handlePost('/readPatch', (postdat, res) => {
var Msg;
var PatchName;
try {
getInstance(postdat.Mdl).readCurrentPatch().then((patch) => {
let Msg = patch.patchname;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"patch":"' + Msg + '"}');
}).catch((value) =>{
let Msg = 'Could not read patch, reason: ' + value;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"error":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"error":"' + e + '"}');
}
});
handlePost('/writePatch', (postdat, res) => {
var Msg;
var PatchName;
try {
getInstance(postdat.Mdl).writeCurrentPatch().then((patch) => {
let Msg = patch.patchname;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"patch":"' + Msg + '"}');
}).catch((value) =>{
let Msg = 'Could not write patch, reason: ' + value;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"error":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"error":"' + e + '"}');
}
});
//TODO: adjust the logic
handlePost('/selIfc', (postdat, res) => {
let mIn = WebMidi.getInputByName(postdat.MidiIn);
let mOut = WebMidi.getOutputByName(postdat.MidiOut);
let mChan = postdat.MidiChan-1;
try {
Roland.getInstance(mIn, mOut, mChan);
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + e + '"}');
}
});
handlePost('/readMemory', (postdat, res) => {
try {
getInstance(postdat.Mdl).readMemoryFromSynth(postdat).then((answ) => {
var result = {result:"Successfully read memory", names:answ};
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not read memory, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + e + '"}');
}
});
handlePost('/writeMemory', (postdat, res) => {
try {
getInstance(postdat.Mdl).writeMemoryToSynth(postdat).then((arr) => {
var result = {result:"Memory successfully written"};
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not write memory, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + e + '"}');
}
});
handlePost('/readFile', (postdat, res) => {
try {
getInstance(postdat.Mdl).readMemoryFromDataURL(postdat.Cont).then((answ) => {
var result = {result:"Successfully read file", names:answ};
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not read file, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + e + '"}');
}
});
app.get('/writeFile.syx', (req, res) => {
getInstance(req.query.Mdl).writeMemoryToData().then((answ) => {
//var result = {result:"Successfully wrote file", names:answ};
res.setHeader('Content-Type', 'audio/x-midi');
res.setHeader("cache-control", "no-store");
res.end(answ);
}).catch((err) =>{
let Msg = 'Could not read file, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
});
app.get('/writePatch.syx', (req, res) => {
getInstance(req.query.Mdl).writePatchToData().then((answ) => {
res.setHeader('Content-Type', 'audio/x-midi');
res.setHeader("cache-control", "no-store");
res.end(answ);
}).catch((err) =>{
let Msg = 'Could not read file, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
});
handlePost('/test', (postdat, res) => {
try {
getInstance(postdat.Mdl).test(postdat).then((answ) => {
var result = {result:answ};
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not perform test, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
} catch(e) {
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + e + '"}');
}
});
handlePost('/comparePatch', (postdat, res) => {
getInstance(postdat.Mdl).comparePatchToFile(postdat).then((answ) => {
var result = {result:answ};
res.setHeader('Content-Type', 'text/plain');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not compare file, ' + err;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
});
handlePost('/compare', (postdat, res) => {
getInstance(postdat.Mdl).compare(postdat).then((answ) => {
var result = {result:answ};
res.setHeader('Content-Type', 'text/plaini');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not compare file, ' + err;
res.setHeader('Content-Type', 'text/plain');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
});
handlePost('/changeProg', (postdat, res) => {
getInstance(postdat.Mdl).changeProg(postdat).then((answ) => {
var result = {result:answ};
res.setHeader('Content-Type', 'text/plain');
res.setHeader("cache-control", "no-store");
res.end(JSON.stringify(result));
}).catch((err) =>{
let Msg = 'Could not changeProg, ' + err;
res.setHeader('Content-Type', 'text/plain');
res.setHeader("cache-control", "no-store");
res.end('{"result":"' + Msg + '"}');
});
});
/**
* SynthPage.html
* The request for this page will not only deliver the page for the Mdl type synth
* but also add the Synth to MySettings.
* The "Generic" Mdl wil never bne entered into synths.json, but is used to create a new entry.
*/
app.get('/SynthPage.html', (req, res) => {
var mod = req.query.Mdl;
var ret = readSettings(mod);
if (ret == undefined) {
deliver(src_dir + "/Synths/Generic.html", res);
} else {
deliver(src_dir + "/Synths/"+ ret.name + ".html", res);
}
});
app.get('/inputs', (req, res) =>{
var lst = [];
var answ;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
WebMidi.inputs.forEach((inp) => {
lst.push(inp.name);
});
var mod = req.query.Mdl;
var ret = readSettings(mod);
if (ret == undefined)
answ = {list: lst, error: `Cannot read .synths.json in ${os.homedir()}`};
else
answ = {list: lst, settings: ret};
res.end(JSON.stringify(answ));
});
app.get('/outputs', (req, res) =>{
var lst = [];
var answ;
res.setHeader('Content-Type', 'text/json; charset=utf-8');
WebMidi.outputs.forEach((ot) => {
lst.push(ot.name);
});
var mod = req.query.Mdl;
var ret = readSettings(mod);
if (ret == undefined)
answ = {list:lst, error: `Cannot read .synths.json in ${os.homedir()}`};
else
answ = {list: lst, settings: ret};
res.end(JSON.stringify(answ));
});
app.get ('/*', function(req,res) {
if (fs.existsSync(new URL(src_dir + req.url))) {
deliver(src_dir + req.url, res);
} else if (fs.existsSync(new URL(src_dir + "ui" + req.url))) {
deliver(src_dir + "ui" + req.url, res);
} else if (fs.existsSync(new URL(src_dir + "proc" + req.url))) {
deliver(src_dir + "process" + req.url, res);
} else {
console.log(req.url + ' not found');
res.statusCode = 404;
res.statusMessage = 'Not Found';
res.end();
}
});
WebMidi.enable(function(err) {
if (err) {
console.log(`webmidi enable failed with ${err}`);
server.close();
}else {
console.log("WebMidi enabled!");
}
}, true);
if (!bServerMode) {
if (os.type().includes("indows")) {
const browser = spawn('cmd.exe', ['"/c start /max http://' + hostname + ':' + port + '/index.html"']);
browser.on('exit', (code) => {
console.log(`browser child exited with code ${code}`);
});
} else {
const browser = spawn('xdg-open', ['http://' + hostname + ':' + port + '/index.html']);
browser.on('exit', (code) => {
console.log(`browser child exited with code ${code}`);
});
}
} else {
console.log("MidiManager started in server mode");
}
server.listen(port, hostname);
console.log(`serving at http://${hostname}:${port}/`);