initial commit

This commit is contained in:
Daniel Sommer 2022-02-07 15:41:27 +01:00
commit 3705d8cca7
14 changed files with 1202 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
yarn.lock

14
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "api",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/api.js"
}
]
}

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# pbc
pedal board control
## description
control your MODEP pedalboard(s) via http requests

19
config.json Normal file
View file

@ -0,0 +1,19 @@
{
"server": {
"listen": "0.0.0.0",
"port": 3000,
"timestamp": "DD.MM.YYYY HH:mm:ss:SS"
},
"log": {
"level": "debug"
},
"osc": {
"host": "192.168.1.24",
"port": "12101",
"address": "/midi"
},
"modep": {
"host": "192.168.1.24",
"port": 80
}
}

354
dev/FUZZ.ttl Normal file
View file

@ -0,0 +1,354 @@
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix ingen: <http://drobilla.net/ns/ingen#> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix mod: <http://moddevices.com/ns/mod#> .
@prefix pedal: <http://moddevices.com/ns/modpedal#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
_:b1
ingen:tail <capture_1> ;
ingen:head <StompBox_fuzz/INPUT_L> .
_:b2
ingen:tail <BigMuffPi/Out1> ;
ingen:head <Overdrive/right_in> .
_:b3
ingen:tail <capture_2> ;
ingen:head <BigMuffPi/In> .
_:b4
ingen:tail <Overdrive/left_out> ;
ingen:head <playback_1> .
_:b5
ingen:tail <capture_1> ;
ingen:head <BigMuffPi/In> .
_:b6
ingen:tail <StompBox_fuzz/OUTPUT_L> ;
ingen:head <playback_1> .
_:b7
ingen:tail <capture_2> ;
ingen:head <StompBox_fuzz/INPUT_R> .
_:b8
ingen:tail <StompBox_fuzz/OUTPUT_R> ;
ingen:head <playback_2> .
_:b9
ingen:tail <BigMuffPi/Out1> ;
ingen:head <Overdrive/left_in> .
_:b10
ingen:tail <Overdrive/right_out> ;
ingen:head <playback_2> .
<BigMuffPi>
ingen:canvasX 561.0 ;
ingen:canvasY 210.0 ;
ingen:enabled false ;
ingen:polyphonic false ;
lv2:microVersion 0 ;
lv2:minorVersion 0 ;
mod:builderVersion 0 ;
mod:releaseNumber 0 ;
lv2:port <BigMuffPi/In> ,
<BigMuffPi/Out1> ,
<BigMuffPi/Tone> ,
<BigMuffPi/Level> ,
<BigMuffPi/Sustain> ,
<BigMuffPi/:bypass> ;
lv2:prototype <http://moddevices.com/plugins/mod-devel/BigMuffPi> ;
pedal:preset <> ;
a ingen:Block .
<BigMuffPi/In>
a lv2:AudioPort ,
lv2:InputPort .
<BigMuffPi/In>
a lv2:AudioPort ,
lv2:OutputPort .
<BigMuffPi/Tone>
ingen:value 0.250000 ;
midi:binding [
midi:channel 0 ;
midi:controllerNumber 1 ;
lv2:minimum 0.000000 ;
lv2:maximum 1.000000 ;
a midi:Controller ;
] ;
a lv2:ControlPort ,
lv2:InputPort .
<BigMuffPi/Level>
ingen:value 0.500000 ;
a lv2:ControlPort ,
lv2:InputPort .
<BigMuffPi/Sustain>
ingen:value 0.500000 ;
a lv2:ControlPort ,
lv2:InputPort .
<BigMuffPi/:bypass>
ingen:value 1 ;
midi:binding [
midi:channel 0 ;
midi:controllerNumber 0 ;
a midi:Controller ;
] ;
a lv2:ControlPort ,
lv2:InputPort .
<Overdrive>
ingen:canvasX 1061.0 ;
ingen:canvasY 180.0 ;
ingen:enabled true ;
ingen:polyphonic false ;
lv2:microVersion 4 ;
lv2:minorVersion 0 ;
mod:builderVersion 0 ;
mod:releaseNumber 0 ;
lv2:port <Overdrive/left_in> ,
<Overdrive/right_in> ,
<Overdrive/left_out> ,
<Overdrive/right_out> ,
<Overdrive/drive> ,
<Overdrive/muffle> ,
<Overdrive/output> ,
<Overdrive/:bypass> ;
lv2:prototype <http://moddevices.com/plugins/mda/Overdrive> ;
pedal:preset <> ;
a ingen:Block .
<Overdrive/left_in>
a lv2:AudioPort ,
lv2:InputPort .
<Overdrive/right_in>
a lv2:AudioPort ,
lv2:InputPort .
<Overdrive/left_in>
a lv2:AudioPort ,
lv2:OutputPort .
<Overdrive/right_in>
a lv2:AudioPort ,
lv2:OutputPort .
<Overdrive/drive>
ingen:value 50.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<Overdrive/muffle>
ingen:value 25.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<Overdrive/output>
ingen:value 0.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<Overdrive/:bypass>
ingen:value 0 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz>
ingen:canvasX 547.0 ;
ingen:canvasY 740.0 ;
ingen:enabled true ;
ingen:polyphonic false ;
lv2:microVersion 0 ;
lv2:minorVersion 0 ;
mod:builderVersion 0 ;
mod:releaseNumber 0 ;
lv2:port <StompBox_fuzz/INPUT_L> ,
<StompBox_fuzz/INPUT_R> ,
<StompBox_fuzz/OUTPUT_L> ,
<StompBox_fuzz/OUTPUT_R> ,
<StompBox_fuzz/BYPASS> ,
<StompBox_fuzz/LEVEL> ,
<StompBox_fuzz/HI> ,
<StompBox_fuzz/MID> ,
<StompBox_fuzz/LO> ,
<StompBox_fuzz/GAIN> ,
<StompBox_fuzz/:bypass> ;
lv2:prototype <http://rakarrack.sourceforge.net/effects.html#StompBox_fuzz> ;
pedal:preset <> ;
a ingen:Block .
<StompBox_fuzz/INPUT_L>
a lv2:AudioPort ,
lv2:InputPort .
<StompBox_fuzz/INPUT_R>
a lv2:AudioPort ,
lv2:InputPort .
<StompBox_fuzz/INPUT_L>
a lv2:AudioPort ,
lv2:OutputPort .
<StompBox_fuzz/INPUT_R>
a lv2:AudioPort ,
lv2:OutputPort .
<StompBox_fuzz/BYPASS>
ingen:value 0.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/LEVEL>
ingen:value 64.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/HI>
ingen:value 20.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/MID>
ingen:value 29.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/LO>
ingen:value 20.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/GAIN>
ingen:value 64.000000 ;
a lv2:ControlPort ,
lv2:InputPort .
<StompBox_fuzz/:bypass>
ingen:value 0 ;
a lv2:ControlPort ,
lv2:InputPort .
<:bpb>
ingen:value 4.000000 ;
lv2:index 0 ;
a lv2:ControlPort ,
lv2:InputPort .
<:bpm>
ingen:value 120.000000 ;
lv2:index 1 ;
a lv2:ControlPort ,
lv2:InputPort .
<:rolling>
ingen:value 0 ;
lv2:index 2 ;
a lv2:ControlPort ,
lv2:InputPort .
<control_in>
atom:bufferType atom:Sequence ;
lv2:index 3 ;
lv2:name "Control In" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "control_in" ;
<http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ;
a atom:AtomPort ,
lv2:InputPort .
<control_out>
atom:bufferType atom:Sequence ;
lv2:index 4 ;
lv2:name "Control Out" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "control_out" ;
<http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ;
a atom:AtomPort ,
lv2:OutputPort .
<capture_1>
lv2:index 5 ;
lv2:name "Capture 1" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "capture_1" ;
a lv2:AudioPort ,
lv2:InputPort .
<capture_2>
lv2:index 6 ;
lv2:name "Capture 2" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "capture_2" ;
a lv2:AudioPort ,
lv2:InputPort .
<playback_1>
lv2:index 7 ;
lv2:name "Playback 1" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "playback_1" ;
a lv2:AudioPort ,
lv2:OutputPort .
<playback_2>
lv2:index 8 ;
lv2:name "Playback 2" ;
lv2:portProperty lv2:connectionOptional ;
lv2:symbol "playback_2" ;
a lv2:AudioPort ,
lv2:OutputPort .
<midi_separated_mode>
ingen:value 0 ;
lv2:index 9 ;
a atom:AtomPort ,
lv2:InputPort .
<>
doap:name "FUZZ" ;
pedal:unitName "Unknown" ;
pedal:unitModel "Unknown" ;
pedal:width 3788 ;
pedal:height 1546 ;
pedal:addressings <addressings.json> ;
pedal:screenshot <screenshot.png> ;
pedal:thumbnail <thumbnail.png> ;
pedal:version 7 ;
ingen:polyphony 1 ;
ingen:arc _:b1 ,
_:b2 ,
_:b3 ,
_:b4 ,
_:b5 ,
_:b6 ,
_:b7 ,
_:b8 ,
_:b9 ,
_:b10 ;
ingen:block <BigMuffPi> ,
<Overdrive> ,
<StompBox_fuzz> ;
lv2:port <:bpb> ,
<:bpm> ,
<:rolling> ,
<midi_separated_mode> ,
<control_in> ,
<control_out> ,
<capture_1> ,
<capture_2> ,
<playback_1> ,
<playback_2> ;
lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ;
a lv2:Plugin ,
ingen:Graph ,
pedal:Pedalboard .

64
libs/api.js Normal file
View file

@ -0,0 +1,64 @@
const logger = require('./logger.js');
const util = require('./util.js');
const constants = require('./constants.js');
const modep = require('./modep.js');
const endpoints = new Map();
function setEndpoints(url, data) {
var startTime = new Date();
var index = 0;
var elements = [];
for (var index = 0; index < data.length; index++) {
var element = data[index];
element.id = index;
setEndpoint(url + '/' + element.id, [constants.HTTP_GET, constants.HTTP_POST], element);
elements.push(element);
}
elements = elements.sort(function (a, b) {
return a.id - b.id;
});
setEndpoint(url, constants.HTTP_GET, elements);
logger.debug('setting up ' + elements.length + ' endpoint(s) for path \'' + url + '\' took ' + util.timeDiff(startTime) + 'ms');
}
function setEndpoint(url, types, data) {
logger.debug('setting up \'' + types + '\' endpoint \'' + url + '\'...')
endpoints.set(url, { types: types, data: data });
}
function getEndpoints() {
return endpoints;
}
function setupEndpoints() {
return new Promise(function (resolve, reject) {
modep.getBanks()
.then(function (data) {
setEndpoints(constants.API_BANKS, data);
})
.then(modep.getPedalboards)
.then(function (data) {
setEndpoints(constants.API_PEDALBOARDS, data);
})
.then(modep.getDefaultPedalboard)
.then(function (data) {
setEndpoint(constants.API_PEDALBOARDS_DEFAULT, constants.HTTP_GET, data);
})
.then(modep.getCurrentPedalboard)
.then(function (data) {
setEndpoint(constants.API_PEDALBOARDS_CURRENT, constants.HTTP_GET, data);
})
.then(modep.getCurrentPedals)
.then(function (data) {
setEndpoints(constants.API_PEDALS, data);
})
.then(resolve)
.catch(reject);
});
}
module.exports = {
setupEndpoints,
getEndpoints
}

27
libs/commands.js Normal file
View file

@ -0,0 +1,27 @@
const logger = require('../libs/logger.js');
const { spawn } = require('child_process')
function execute(endpoint) {
if (!endpoint || !endpoint.command) {
logger.warn('no command defined');
return;
}
logger.debug('executing command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\'...');
var cmd = spawn(endpoint.command, endpoint.args);
cmd.stdout.on('data', function(data) {
logger.debug(data);
});
cmd.stderr.on('data', function(data) {
logger.error(data);
});
cmd.on('close', function(code) {
logger.debug('command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\' finished with exit code ' + code);
});
cmd.on('error', function(err) {
logger.error('command \'' + endpoint.command + '\' with args \'' + endpoint.args + '\' encountered an error >>> ' + err);
});
}
module.exports = {
execute
}

10
libs/constants.js Normal file
View file

@ -0,0 +1,10 @@
exports.HTTP_GET = 'GET';
exports.HTTP_POST = 'POST';
exports.API_BANKS = '/banks';
exports.API_PEDALBOARDS = '/pedalboards';
exports.API_PEDALBOARDS_DEFAULT = '/pedalboards/default';
exports.API_PEDALBOARDS_CURRENT = '/pedalboards/current';
exports.API_PEDALS = '/pedals';
exports.PEDALBOARD_DEFAULT = '/var/modep/pedalboards/default.pedalboard';

110
libs/logger.js Normal file
View file

@ -0,0 +1,110 @@
const config = require("../config.json");
const moment = require("moment");
// constants
const LOG_PREFIX_DEBUG = "debug";
const LOG_PREFIX_INFO = "info";
const LOG_PREFIX_WARNING = "warning";
const LOG_PREFIX_ERROR = "error";
const LOGLEVEL_DEBUG = 0;
const LOGLEVEL_INFO = 1;
const LOGLEVEL_WARNING = 2;
const LOGLEVEL_ERROR = 3;
// set loglevel on "require"
const loglevel = function() {
switch (config.log.level) {
case LOG_PREFIX_DEBUG:
case LOGLEVEL_DEBUG:
return LOGLEVEL_DEBUG;
case LOG_PREFIX_INFO:
case LOGLEVEL_INFO:
return LOGLEVEL_INFO;
case LOG_PREFIX_WARNING:
case LOGLEVEL_WARNING:
return LOGLEVEL_WARNING;
case LOG_PREFIX_ERROR:
case LOGLEVEL_ERROR:
return LOGLEVEL_ERROR;
default:
return LOGLEVEL_INFO;
}
}();
// log a http request
function request(request) {
let message = "[" + request.method + "] url: \"" + request.url + "\"";
let counter = 1;
for (let param in request.body) {
message += ", parameter " + counter + ": \"" + param + "=" + request.body[param] + "\"";
counter++;
}
debug(message.trim());
}
// prefix log with "info"
function info(message) {
if (loglevel > LOGLEVEL_INFO) {
return;
}
trace(message);
}
// prefix log with "info"
function warn(message) {
if (loglevel > LOGLEVEL_WARNING) {
return;
}
trace(message, "warning");
}
// prefix log with "debug"
function debug(message) {
if (loglevel > LOGLEVEL_DEBUG) {
return;
}
trace(message, "debug");
}
// prefix log with "error"
function error(message) {
if (loglevel > LOGLEVEL_ERROR) {
return;
}
trace(message, "error");
}
// default logging function
function trace(message, prefix) {
if (message === undefined || message === null || message.length === 0) {
return;
}
if (prefix === undefined || prefix === null || prefix.length === 0) {
prefix = "info";
}
let print;
switch (prefix) {
case "error":
print = console.error;
break;
case "debug":
print = console.debug;
break;
case "warning":
print = console.warn;
break;
default:
print = console.log;
}
message = moment().format(config.server.timestamp) + " | " + prefix + " > " + message;
print(message);
}
// exports
module.exports = {
info,
warn,
debug,
error,
request
};

329
libs/modep.js Normal file
View file

@ -0,0 +1,329 @@
const config = require('../config.json');
const util = require('./util.js');
const constants = require('./constants.js');
const logger = require('./logger.js');
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process')
const ttl2jsonld = require('@frogcat/ttl2jsonld').parse;
var banks = undefined;
var pedalboards = undefined;
var defaultPedalboard = undefined;
var currentPedalboard = undefined;
var currentPedals = undefined;
function reset() {
return new Promise(function (resolve, reject) {
util.httpGET(config.modep.host, config.modep.port, '/reset')
.then(resolve)
.catch(reject);
});
}
function getBanks() {
return new Promise(function (resolve, reject) {
if (banks) {
return resolve(banks);
}
// FAKE DATA
var fake = [{ "title": "The Button", "pedalboards": [{ "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/default.pedalboard/default.ttl", "bundle": "/var/modep/pedalboards/default.pedalboard", "title": "Default", "version": 0 }, { "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/FUZZ.pedalboard/FUZZ.ttl", "bundle": "/var/modep/pedalboards/FUZZ.pedalboard", "title": "FUZZ", "version": 1 }] }];
for (var index = 0; index < fake.length; index++) {
fake.id = index;
}
banks = util.sortById(fake);
return resolve(fake);
util.httpGET(config.modep.host, config.modep.port, '/banks')
.then(resolve)
.catch(reject);
});
}
function getPedalboards() {
return new Promise(function (resolve, reject) {
if (pedalboards) {
return resolve(pedalboards);
}
// FAKE DATA
var fake = [{ "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/FUZZ.pedalboard/FUZZ.ttl", "bundle": "/var/modep/pedalboards/FUZZ.pedalboard", "title": "FUZZ", "version": 1 }, { "valid": true, "broken": false, "uri": "file:///var/modep/pedalboards/default.pedalboard/default.ttl", "bundle": "/var/modep/pedalboards/default.pedalboard", "title": "Default", "version": 0 }];
var id = 1;
for (var index = 0; index < fake.length; index++) {
var pedalboard = fake[index];
if (pedalboard.bundle == constants.PEDALBOARD_DEFAULT) {
pedalboard.id = 0;
defaultPedalboard = pedalboard;
continue;
}
pedalboard.id = id;
id++;
}
pedalboards = util.sortById(fake);
return resolve(fake);
util.httpGET(config.modep.host, config.modep.port, '/pedalboard/list')
.then(function (pedalboards) {
})
.catch(reject);
});
}
function getDefaultPedalboard() {
return new Promise(function (resolve, reject) {
if (defaultPedalboard) {
return resolve(defaultPedalboard);
}
getPedalboardByBundle(constants.PEDALBOARD_DEFAULT)
.then(resolve)
.catch(reject);
});
}
function getCurrentPedalboard() {
return new Promise(function (resolve, reject) {
if (currentPedalboard && currentPedalboard.id) {
return resolve(currentPedalboard);
}
// FAKE DATA
var fake = '/var/modep/pedalboards/FUZZ.pedalboard';
getPedalboardByBundle(fake)
.then(function (pedalboard) {
currentPedalboard = pedalboard;
return resolve(pedalboard);
})
.catch(reject);
// PRODUCTION
// util.httpGET(config.modep.host, config.modep.port, '/pedalboard/current')
// .then(getPedalboardByBundle)
// .then(resolve)
// .catch(reject);
});
}
function getPedalboardById(pedalboardId) {
return new Promise(function (resolve, reject) {
getPedalboards()
.then(function (pedalboards) {
for (var index = 0; index < pedalboards.length; index++) {
if (pedalboards[index].id != pedalboardId) {
continue;
}
return resolve(pedalboards[index]);
}
return reject('error: could not find pedalboard by id \'' + controlId + '\'');
})
.catch(reject);
});
}
function getPedalboardByBundle(pedalboardBundle) {
return new Promise(function (resolve, reject) {
getPedalboards()
.then(function (pedalboards) {
for (var index = 0; index < pedalboards.length; index++) {
if (pedalboards[index].bundle != pedalboardBundle) {
continue;
}
return resolve(pedalboards[index]);
}
return reject('error: could not find pedalboard by bundle \'' + pedalboardBundle + '\'');
})
.catch(reject);
});
}
function getPedalControlById(pedalId, controlId) {
return new Promise(function (resolve, reject) {
getCurrentPedalById(pedalId)
.then(function (pedal) {
for (var index = 0; index < pedal.controls.length; index++) {
if (pedal.controls[index].id != controlId) {
continue;
}
return resolve(pedal.controls[index]);
}
return reject('error: could not find control for pedal \'' + pedalId + '\' by id \'' + controlId + '\'');
})
.catch(reject);
});
}
function getCurrentPedalById(id) {
return new Promise(function (resolve, reject) {
getCurrentPedals()
.then(function () {
for (var index = 0; index < currentPedals.length; index++) {
if (currentPedals[index].id != id) {
continue;
}
return resolve(currentPedals[index]);
}
return reject('error: could not find current pedal by id \'' + id + '\'');
})
.catch(reject);
});
}
function getCurrentPedals() {
return new Promise(function (resolve, reject) {
if (currentPedals) {
return resolve(currentPedals);
}
getCurrentPedalboard()
.then(parseCurrentPedalboard)
.then(resolve)
.catch(reject);
});
}
function parseCurrentPedalboard(pedalboardBundle) {
return new Promise(function (resolve, reject) {
var startTime = new Date();
logger.debug('parsing current pedalboard...');
if (!currentPedalboard || !currentPedalboard.uri) {
for (var index = 0; index < pedalboards.length; index++) {
var pedalboard = pedalboards[index];
if (!pedalboard.bundle || pedalboardBundle != pedalboard.bundle) {
continue;
}
currentPedalboard = pedalboard;
break;
};
}
if (!currentPedalboard.uri) {
reject('error: could not determine current pedalboard config file');
}
// FAKE DATA
var file = path.resolve('./dev/FUZZ.ttl');
// var file = path.resolve(currentPedalboard.uri.replace('file://', ''));
fs.readFile(file, function (err, data) {
if (err) {
return reject('error: could not parse current pedalboard file \'' + file + '\' >>> ' + err);
}
currentPedals = [];
var json = ttl2jsonld(data.toString())['@graph'];
var id = 0;
for (var index = 0; index < json.length; index++) {
var tmp = json[index];
if (!tmp['lv2:prototype']) {
continue;
}
var name = tmp['@id'];
currentPedals.push({ id: id, name: name, controls: [] });
id++;
}
for (var index = 0; index < json.length; index++) {
var tmp = json[index];
var name = tmp['@id'];
var value = tmp['ingen:value'];
if (value == undefined) {
continue;
}
var pedal = undefined;
for (var pedalIndex = 0; pedalIndex < currentPedals.length; pedalIndex++) {
if (!name.startsWith(currentPedals[pedalIndex].name)) {
continue;
}
pedal = currentPedals[pedalIndex];
break;
}
if (!pedal) {
continue;
}
id = pedal.controls.length;
name = name.replace(pedal.name + '/', '');
if (name == ':bypass') {
value = value;
} else {
value = value['@value'];
}
var control = { id: id, name: name, value: value };
pedal.controls.push(control);
id++;
var midi = tmp['midi:binding'];
if (!midi) {
continue;
}
control.midi = { channel: midi['midi:channel'], controller: midi['midi:controllerNumber'] }
}
logger.debug('parsing current pedalboard file \'' + file + '\' took ' + util.timeDiff(startTime) + 'ms')
resolve(currentPedals);
});
});
}
function sendValueToControl(value, control) {
return new Promise(function (resolve, reject) {
if (!control || !control.midi) {
return reject('error: control \'' + control.name + '\' with id \'' + control.id + '\' has no midi bindings');
}
if (value > 127) {
value = 127;
} else if (value < 0) {
value = 0;
}
value = '00' + util.toHex(value) + '0' + util.toHex(control.midi.controller) + 'b' + util.toHex(control.midi.channel);
var cmd = 'oscsend';
var args = [config.osc.host, config.osc.port, config.osc.address, 'm', value];
logger.debug('executing command \'' + cmd + '\' with args \'' + args + '\'...');
var spawned = spawn(cmd, args);
spawned.stdout.on('data', function (data) {
logger.debug(data);
});
spawned.stderr.on('data', function (data) {
logger.error(data);
});
spawned.on('close', function (code) {
logger.debug('command \'' + cmd + '\' with args \'' + args + '\' finished with exit code ' + code);
resolve();
});
spawned.on('error', function (err) {
logger.error('command \'' + cmd + '\' with args \'' + args + '\' encountered an error >>> ' + err);
reject(err);
});
});
}
function setPedalboardById(pedalboardId) {
return new Promise(function (resolve, reject) {
if (!pedalboardId) {
return reject('error: no pedalboard id given');
}
getPedalboardById(pedalboardId)
.then(setPedalboard)
.then(resolve)
.catch(reject);
});
}
function setPedalboard(pedalboard) {
return new Promise(function (resolve, reject) {
if (!pedalboard || !pedalboard.bundle) {
return reject('error: no bundle set for pedalboard');
}
if (pedalboard.id == currentPedalboard.id) {
return resolve('pedalboard \'' + pedalboard.id + '\' is already active');
}
reset()
.then(function () {
util.httpPOST(config.modep.host, config.modep.port, '/pedalboard/load_bundle/?bundlepath=' + pedalboard.bundle)
})
.then(getCurrentPedalboard)
.catch(reject);
});
}
module.exports = {
getBanks,
getPedalboards,
getDefaultPedalboard,
getCurrentPedalboard,
getCurrentPedals,
getCurrentPedalById,
getPedalControlById,
sendValueToControl,
setPedalboard,
setPedalboardById,
}

151
libs/server.js Normal file
View file

@ -0,0 +1,151 @@
const config = require('../config.json');
const logger = require('./logger.js');
const api = require('./api.js')
const constants = require('./constants.js');
const modep = require('./modep.js');
const http = require('http');
var server;
function start() {
return new Promise(function (resolve, reject) {
if (!server) {
server = http.createServer();
}
server.listen(config.server.port, config.server.listen).on('listening', function () {
logger.debug('server listening on ' + config.server.listen + ':' + config.server.port + '...');
handleRequests();
});
});
}
function handleRequests() {
server.on('request', function (request, response) {
logger.request(request);
var endpoint = api.getEndpoints().get(request.url);
if (!endpoint) {
var msg = 'endpoint \'' + request.url + '\' not defined';
response.writeHead(501);
response.end(msg);
logger.debug(msg);
return;
}
if (!endpoint.types.includes(request.method)) {
var msg = 'endpoint \'' + request.url + '\' does not support ' + request.method + ' requests';
response.writeHead(405);
response.end(msg);
logger.debug(msg);
return;
}
if (request.method == constants.HTTP_GET) {
handleGET(response, endpoint);
return;
}
if (request.method == constants.HTTP_POST) {
handlePOST(request, response, endpoint);
return;
}
var msg = 'endpoint \'' + request.url + '\' does not have any handlers for ' + request.method + ' requests';
response.writeHead(405);
response.end(msg);
logger.debug(msg);
});
}
function handleGET(response, endpoint) {
var data = endpoint.data;
if (!data) {
response.writeHead(500);
response.end('error: could not get data for endpoint')
return;
}
response.writeHead(200);
response.end(JSON.stringify(data));
}
function handlePOST(request, response, endpoint) {
if (request.url.startsWith(constants.API_PEDALS)) {
handlePOSTPedals(request)
.then(function (msg) {
response.writeHead(200);
response.end(msg);
})
.catch(function (err, code) {
if (!code) {
code = 500;
}
response.writeHead(code);
response.end(err);
logger.error(err);
});
return;
}
if (request.url.startsWith(constants.API_PEDALBOARDS)) {
handlePOSTPedalboards(request)
.then(function (msg) {
response.writeHead(200);
response.end(msg);
})
.catch(function (err, code) {
if (!code) {
code = 500;
}
response.writeHead(code);
response.end(err);
logger.error(err);
});
return;
}
var msg = 'endpoint \'' + request.url + '\' is not yet implemented for ' + request.method + ' requests';
response.writeHead(405);
response.end(msg);
}
function getPOSTParams(request) {
return new Promise(function (resolve, reject) {
var params = "";
request.on("data", function (data) {
params += data;
});
request.on("end", function () {
return resolve(new URLSearchParams(params));
});
});
}
function handlePOSTPedals(request) {
return new Promise(function (resolve, reject) {
getPOSTParams(request)
.then(function (params) {
var pedalId = request.url.substring(request.url.lastIndexOf('/') + 1);
var controlId = params.get('id');
if (controlId == undefined) {
reject('error: could not handle POST - missing parameter \'id\'', 400)
}
var value = params.get('value');
if (value == undefined) {
reject('error: could not handle POST - missing parameter \'value\'', 400)
}
modep.getPedalControlById(pedalId, controlId)
.then(function (control) {
modep.sendValueToControl(value, control);
})
.then(resolve)
.catch(reject);
})
.catch(reject);
});
}
function handlePOSTPedalboards(request) {
return new Promise(function (resolve, reject) {
modep.setPedalboardById(request.url.substring(request.url.lastIndexOf('/') + 1))
.then(resolve)
.catch(reject);
});
}
module.exports = {
start
}

78
libs/util.js Normal file
View file

@ -0,0 +1,78 @@
const logger = require('./logger.js');
const http = require('http');
const { HTTP_GET } = require('./constants.js');
function timeDiff(startTime) {
if (startTime instanceof Date) {
return (new Date().getTime() - startTime.getTime());
}
return new Date().getTime - startTime;
}
function clone(object) {
var clone = {};
for (key in object) {
clone[key] = object[key];
}
return clone;
}
function httpGET(host, port, path, args) {
return httpRequest(host, port, path, HTTP_GET, args);
}
function httpPOST(host, port, path, args) {
return httpRequest(host, port, path, HTTP_POST, args);
}
function httpRequest(host, port, path, method, args) {
return new Promise(function (resolve, reject) {
if (!path.startsWith("/")) {
path = "/" + path;
}
logger.debug('sending http \'' + method + '\' request to \'' + host + ':' + port + path + '\'...');
const request = http.request({
hostname: host,
port: port,
path: path,
method: method
}, function (response) {
if (!response) {
return reject('error: no response from host for http \'' + method + '\' request \'' + host + ':' + port + path + '\'');
}
logger.debug('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned status code ' + response.statusCode);
var responseData = "";
response.on('data', function (data) {
responseData += data;
});
response.on('end', function () {
logger.debug('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned data \'' + responseData + '\'');
return resolve(responseData);
});
});
request.on('error', function (err) {
return reject('http \'' + method + '\' request \'' + host + ':' + port + path + '\' returned an error >>> ' + err);
});
request.end();
});
}
function toHex(value) {
var hex = Number(value).toString(16);
return hex;
}
function sortById(array) {
return array.sort(function (a, b) {
return a.id - b.id;
});
}
module.exports = {
timeDiff,
clone,
httpGET,
httpPOST,
toHex,
sortById
}

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "pbc",
"version": "0.0.1",
"description": "pedal board control",
"main": "pbc.js",
"scripts": {
"start": "node pbc.js"
},
"keywords": [
"scripts",
"commands",
"remote",
"api"
],
"author": "Daniel Sommer <daniel.sommer@velvettear.de>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.velvettear.de/velvettear/pbc.git"
},
"dependencies": {
"@frogcat/ttl2jsonld": "^0.0.6",
"moment": "^2.29.1"
}
}

12
pbc.js Normal file
View file

@ -0,0 +1,12 @@
const logger = require('./libs/logger.js');
const api = require('./libs/api.js');
const server = require('./libs/server.js')
const packageJSON = require('./package.json');
logger.info("launching " + packageJSON.name + " " + packageJSON.version);
api.setupEndpoints()
.then(server.start)
.catch(function (err) {
logger.error(err);
process.exit(1);
});