Open fedekrum opened 7 months ago
Doing a bit of research, it seems like rtmidi doesn't provide disconnect/reconnect events so any implementation would need to utilise polling which could be achieved just as well in your code (ref https://github.com/micdah/RtMidi.Core/issues/18)
I am open to this library being extended with an implementation of that to avoid everyone having to reimplement it, but it needs someone to implement it
I just made a base class for that.
const MidiDevice = class {
deviceName = "";
input = new midi.Input();
output = new midi.Output();
controls = new Map();
ticks = new Map();
contexts = new Map();
tickContext = {};
deltaTime = 0;
#contextTemplate = null;
constructor(deviceName, ctx) {
this.deviceName = deviceName;
this.#contextTemplate = function(ctx) {
if (typeof ctx === "object") Object.assign(this, ctx);
};
this.#contextTemplate.prototype = {
self: this,
tick: this.tickContext,
};
if (typeof ctx === "object") Object.assign(this.#contextTemplate.prototype, ctx);
this.input.on("message", (deltaTime, message) => {
this.control(deltaTime, message);
});
}
#checkPorts() {
const { input, output } = this;
const inC = input.getPortCount();
const outC = output.getPortCount();
const inConnected = input.isPortOpen();
const outConnected = output.isPortOpen();
let inPort = -1;
for (let i = 0; i < inC; i++) {
if (input.getPortName(i) === this.deviceName) {
inPort = i;
break;
}
}
let outPort = -1;
for (let i = 0; i < outC; i++) {
if (output.getPortName(i) === this.deviceName) {
outPort = i;
break;
}
}
if (inPort < 0 && inConnected) {
console.log(`device ${this.deviceName} input port is open but not found. disconnecting`);
input.closePort();
this.tickContext.ev = "closeInput";
if (this.ticks.has("closeInput")) this.ticks.get("closeInput").call(this.tickContext);
delete this.tickContext.ev;
}
else if (inPort >= 0 && !inConnected) {
console.log(`device ${this.deviceName} input port is closed but found. connecting`);
input.openPort(inPort);
this.tickContext.ev = "openInput";
if (this.ticks.has("openInput")) this.ticks.get("openInput").call(this.tickContext);
delete this.tickContext.ev;
}
if (outPort < 0 && outConnected) {
console.log(`device ${this.deviceName} output port is open but not found. disconnecting`);
output.closePort();
this.tickContext.ev = "closeOutput";
if (this.ticks.has("closeOutput")) this.ticks.get("closeOutput").call(this.tickContext);
delete this.tickContext.ev;
}
else if (outPort >= 0 && !outConnected) {
console.log(`device ${this.deviceName} output port is closed but found. connecting`);
output.openPort(outPort);
this.tickContext.ev = "openOutput";
if (this.ticks.has("openOutput")) this.ticks.get("openOutput").call(this.tickContext);
delete this.tickContext.ev;
}
}
#debugVerbose = false;
#debugEnabled = false;
debug(verbose = false) {
const self = this;
this.#debugVerbose = verbose;
this.#debugEnabled = true;
this.addControl("debug", function ({status, control, value}) {
console.log(`debug: ${self.deviceName} ${status} ${control} ${value} ${JSON.stringify(this, null, 2)} ${JSON.stringify(this.tick, null, 2)}`);
});
}
#addThing(type, control, callback, ctx) {
this[type].set(control, callback);
if (!this.contexts.has(control)) this.contexts.set(control, new this.#contextTemplate(ctx));
else if (typeof ctx === "object") Object.assign(this.contexts.get(control), ctx);
}
addControl(...args) {
this.#addThing("controls", ...args);
return this;
}
addTick(...args) {
this.#addThing("ticks", ...args);
return this;
}
#removeThing(type, control) {
this[type].delete(control);
if (!this.ticks.has(control)) this.contexts.delete(control);
}
removeControl(...args) {
this.#removeThing("controls", ...args);
return this;
}
removeTick(...args) {
this.#removeThing("ticks", ...args);
return this;
}
tick() {
this.#checkPorts();
this.ticks.forEach((callback, key) => {
callback.call(this.contexts.get(key));
});
}
control(deltaTime, message) {
const [status, control, value] = message;
const hasControl = this.controls.has(control);
const hasDebug = this.controls.has("debug");
if (!hasControl && !hasDebug) return;
const identifier = hasControl ? control : "debug";
const context = this.contexts.get(identifier);
const callback = this.controls.get(identifier);
const now = new Date();
if (!("lastEvent" in context)) {
context.lastEvent = now;
}
context.deltaTime = (now - context.lastEvent) / 1000;
context.lastEvent = now;
context.status = status;
context.control = control;
context.value = value;
this.deltaTime = deltaTime;
if (this.#debugEnabled && (this.#debugVerbose || !hasControl)) {
this.controls.get("debug").call(context, {status, control, value});
}
if (hasControl) callback.call(context, {status, control, value});
}
};
it can then be used like this:
const trinket = new class extends MidiDevice {
constructor() {
super("CircuitPython Audio");
const self = this;
/*// increase voicemeeter volume on control 1
this.addControl(1, function ({value}) {
if (value === 0) return;
const gain = vm.api["Bus[0].Gain"] += 4;
console.log(`setting voicemeeter gain to ${gain.toFixed(3)} dB`);
});
// decrease voicemeeter volume on control 2
this.addControl(2, function ({value}) {
if (value === 0) return;
const gain = vm.api["Bus[0].Gain"] -= 4;
console.log(`setting voicemeeter gain to ${gain.toFixed(3)} dB`);
});*/
// mute mic on control 3
this.addTick("openOutput", function () {
if (!ctx.vmdirty && this.ev !== "openOutput") return;
const isMicMuted = vm.raw["Strip[0].Mute"];
self.output.sendMessage([176, 0b0110001, isMicMuted ? 1 : 0]);
self.output.sendMessage([176, 0b0110100, isMicMuted ? 0 : 1]);
const isMainMuted = vm.raw["Bus[0].Mute"];
self.output.sendMessage([176, 0b1000001, isMainMuted ? 1 : 0]);
self.output.sendMessage([176, 0b1000010, isMainMuted ? 0 : 1]);
const isHeadsetMuted = vm.raw["Bus[3].Mute"];
self.output.sendMessage([176, 0b0001001, isHeadsetMuted ? 1 : 0]);
self.output.sendMessage([176, 0b0001010, isHeadsetMuted ? 0 : 1]);
});
this.addControl(3, function ({value}) {
if (value === 0 && this.deltaTime < 0.15) return;
const isMuted = vm.raw["Strip[0].Mute"] ^= 1;
console.log(`setting voicemeeter mic mute to ${isMuted ? "muted" : "unmuted"}`);
self.output.sendMessage([176, 0b0110001, isMuted ? 1 : 0]);
self.output.sendMessage([176, 0b0110100, isMuted ? 0 : 1]);
});
}
}
i have made it quite usable tbh..
// gain levels
for (let [i, bus] of sequence(4, 4)) {
this.addControl(i, function ({value}) {
// ToDo: debounce
const gain = gains[value];
console.log(`setting bus ${bus} gain for ${self.busNames.get(bus)} to ${gain.toFixed(3)} dB`);
vm.raw[`Bus[${bus}].Gain`] = gain;
});
}
all you then need to do, is to call tick()
on it like this:
let tick = 0;
while(true) {
tick++;
try {
const vmdirty = ctx.vmdirty = vm.isParametersDirty();
if (vmdirty) vm.updateDeviceList();
nanoKONTROL2.tick();
trinket.tick();
pr0Board.tick();
} catch (e) {
console.log("such e", e);
}
await sleep(100);
}
I'm currently re-designing the voicemeeter-remote module.. I'm re-imagining how to interact with some libraries, and currently work on solving such ideas:
const isMuted = vm.raw["Strip[0].Mute"] ^= 1;
while in future, it might just be:
const isMuted = vm.Strip[0].Mute.$ ^= 1;
when I'm done, I'll likely refine my node-midi-wrapper and post it as frontend for this module with such functions. rt-midi itself sure lacks features like that. you can't even properly pin-point devices if they have the same name.
I've been looking into this too.
It seems that on Linux+ALSA one could launch aseqdump -p 0:1
to listen to the events on the ALSA "Announce" port.
I think it would be best if it was possible to subscribe to the Announce port via node-midi instead of launching a separate process and parsing its output stream.
Here's a simple example of an EventEmitter that emits ALSA client connect / exit events:
import { EventEmitter } from 'node:events';
import { spawn } from 'node:child_process';
export const alsaClientEvents = new EventEmitter;
const childProcess = spawn( 'aseqdump', [ '-p', '0:1' ]);
childProcess.stdout.on( 'data', data => {
const content = data.toString();
if( content.includes( 'Client exit' )){
alsaClientEvents.emit( 'exit', content );
} else if( content.includes( 'Client start' )){
alsaClientEvents.emit( 'start', content );
}
});
Hi, There is a post on the original node-midi repository. https://github.com/justinlatimer/node-midi/issues/226
Is there a way to do that with this version? Thanks