Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disconnect/Reconnect events? #12

Open
fedekrum opened this issue Jan 15, 2024 · 4 comments
Open

Disconnect/Reconnect events? #12

fedekrum opened this issue Jan 15, 2024 · 4 comments

Comments

@fedekrum
Copy link

Hi,
There is a post on the original node-midi repository.
justinlatimer#226

Is there a way to do that with this version?
Thanks

@Julusian
Copy link
Owner

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 micdah/RtMidi.Core#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

@GottZ
Copy link

GottZ commented May 3, 2024

Warning!

don't blindly use the following code, as it's not really production ready yet (it's a PoC) and needs cleanup:

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.

@emilis
Copy link

emilis commented Jul 27, 2024

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.

@emilis
Copy link

emilis commented Jul 28, 2024

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 );
    }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants