jazz-soft / JZZ

MIDI library for Node.js and web-browsers
http://jazz-soft.net/doc/JZZ/
MIT License
528 stars 27 forks source link

How to temporarily stop midi input? #25

Closed Avijobo closed 4 years ago

Avijobo commented 4 years ago

Hi,

I am using jzz Web Midi API in node.js, however I can't find a way to stop midi input temporarily.

Closing the midi input ports does change the port.connection state to 'closed' but still keeps passing midi input messages to port.onmidimessage. Calling navigator.close() stops the entire midi engine, but after that I cannot get it to work again: a new navigator.requestMIDIAccess() fails from then on.

Any ideas?

jazz-soft commented 4 years ago

Looks like a bug... Thanks for letting me know! I'll take a look...

jazz-soft commented 4 years ago

That was a subtle bug to spot. Thank you for finding it! I have committed the fix, please get the latest release. Now you can call close() on the input port to pause the input and open() to resume it.

Avijobo commented 4 years ago

Thanks, however now I get this error on my linux node server where JZZ is running: W20200121-18:50:34.481(1)? (STDERR) TypeError: Cannot read property 'onstatechange' of undefined W20200121-18:50:34.482(1)? (STDERR) at _statechange (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2417:11) W20200121-18:50:34.482(1)? (STDERR) at _R._wm_watch (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2856:40) W20200121-18:50:34.482(1)? (STDERR) at _fireW (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:560:83) W20200121-18:50:34.483(1)? (STDERR) at _postRefresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:251:9) W20200121-18:50:34.483(1)? (STDERR) at Object._engine._refresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:754:7) W20200121-18:50:34.484(1)? (STDERR) at Timeout._onTimeout (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:859:64) W20200121-18:50:34.484(1)? (STDERR) at listOnTimeout (internal/timers.js:531:17) W20200121-18:50:34.484(1)? (STDERR) at processTimers (internal/timers.js:475:7)

jazz-soft commented 4 years ago

I've committed some fixes. Please copy the latest version to node_modules/jzz/javascript. If your issue is fixed, I'll release a new version. Otherwise, I'll need your help to reproduce the bug.

Avijobo commented 4 years ago

Hi, I really appreciate your efforts, thanks a lot, however unfortunately it did not fix it. I occasionally get a "MIDI CALL IS NOT AVAILABLE" error now instead. It turns out I get this error without your latest fix either.

The context in which I use JZZ is part of a bigger Meteor project so I cannot easily isolate it.

This is what I basically do: Incoming midi time code quarter frame messages are decoded, and as soon as a complete time code frame is received, the port is closed. It is then that I get above mentioned error, or a "MIDI CALL IS NOT AVAILABLE" error.

The goal is to reduce midi processing in node as much as possible when it is not needed, since I am only interested in occasionally monitoring incoming time code. So what I want to do is to turn off the midi port as soon as time code is received, an turn it on again a few seconds later. As a workaround, instead of calling port.close() I change the port.onmidimessage callback to a dummy 'empty' callback for some time. This works, however being able to close the port (and reopen later) would save substantial more processing I assume.

The code is running on a Linux ubuntu server, and the incoming midi is coming as ipMidi from the network via qmidinet.

jazz-soft commented 4 years ago

I don't think I have a "MIDI CALL IS NOT AVAILABLE" string anywhere in my code, but need to double check.

The most likely cause of the error is closing the port from within the onmidimessage handler. - I'll experiment with it tonite.

There are a couple solutions you can try:

Avijobo commented 4 years ago

<<I don't think I have a "MIDI CALL IS NOT AVAILABLE" string anywhere in my code, but need to double check.

You are right, that string does not come from your code, weird, could it come from the lower level midi port driver?

<<The most likely cause of the error is closing the port from within the onmidimessage handler. - I'll experiment with it tonite.

That was my first thought too, so I already tested this, with a setTimout() of 2 seconds, but it didn't make a difference.

<<use JZZ API directly instead of WebMIDI API - that will eliminate a complete JZZ-to-WebMIDI software layer with its resource consumption and possible bugs.

I'll probably have to do that then... thanks anyway for looking into it, if I find something new that could help fixing this, I'll definitely let you know.

Johan

jazz-soft commented 4 years ago

Does the program crash after printing "MIDI CALL IS NOT AVAILABLE" ?

Avijobo commented 4 years ago

Yes null

jazz-soft commented 4 years ago

Ok, I think I know where the error is... Will try to rebuild the jazz-midi module during the weekend.

jazz-soft commented 4 years ago

Please try v1.0.1. Hope it works... Happy Chinese New Year! :)

Avijobo commented 4 years ago

Happy Chinese New Year too!

I hate to say this, but it still keeps crashing on Linux with JZZ v1.0.1. as soon as I call port.close():

W20200127-17:05:29.486(1)? (STDERR) TypeError: Cannot read property 'onstatechange' of undefined W20200127-17:05:29.487(1)? (STDERR) at _statechange (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2417:11) W20200127-17:05:29.488(1)? (STDERR) at _R._wm_watch (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2856:40) W20200127-17:05:29.488(1)? (STDERR) at _fireW (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:560:83) W20200127-17:05:29.488(1)? (STDERR) at _postRefresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:251:9) W20200127-17:05:29.488(1)? (STDERR) at Object._engine._refresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:754:7) W20200127-17:05:29.489(1)? (STDERR) at Timeout._onTimeout (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:859:64) W20200127-17:05:29.489(1)? (STDERR) at listOnTimeout (internal/timers.js:531:17) W20200127-17:05:29.489(1)? (STDERR) at processTimers (internal/timers.js:475:7)

I don't seem to get the 'MIDI CALL IS NOT AVAILABLE' anymore though.

The weird thing is that in Windows I don't have this error/crash. FYI both my Linux and Windows systems have node v12.14.0 installed, and receive the same ipMidi messages.

jazz-soft commented 4 years ago

This output looks weird... It says that a MIDI-Out port is disconnected. Do you have any ideas how that can happen?

Avijobo commented 4 years ago

No, I don't use midi-out ports. This is my code (Typescript):

`export class MidiHandler { static instances = 0;

private midiAccess: WebMidi.MIDIAccess;
private midiInputCallback: (midiMsgEvent: WebMidi.MIDIMessageEvent) => void;

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static create(navigator: Navigator, sysex: boolean): MidiHandler {
    if (MidiHandler.instances) return;  // Force singleton

    if (navigator.requestMIDIAccess) {  // MIdi access supported?
        MidiHandler.instances++;
        return new MidiHandler(navigator, sysex);
    }
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private constructor(navigator: Navigator, sysex: boolean) {
    navigator.requestMIDIAccess({ sysex: sysex }).then((midiAccess: WebMidi.MIDIAccess) => {
        this.midiAccess = midiAccess;

        this.midiAccess.inputs.forEach((port: WebMidi.MIDIInput, id: string) => {
            let log = Meteor.isServer ? console.log : screenLog;
            log("midiinput id = " + id + ", port name = " + port.name);
            if (this.midiInputCallback)
                port.onmidimessage = this.midiInputCallback; // Automatically opens port
        });
    });
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
setMidiInputCallback(callback: (midiMsgEvent: WebMidi.MIDIMessageEvent) => void) {
    this.midiInputCallback = callback && Meteor.bindEnvironment(callback);

    if (this.midiAccess)
        this.midiAccess.inputs.forEach((port: WebMidi.MIDIInput, id: string) => {
            if (this.midiInputCallback) {
                port.onmidimessage = this.midiInputCallback;    // Automatically opens port if not open yet
                console.log(port.name + " port.state = " + port.state + ", port.connection = " + port.connection);
            }
        });
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
closeMidiInput() {
    if (this.midiAccess)
        this.midiAccess.inputs.forEach((port: WebMidi.MIDIInput, id: string) => {
            port.onmidimessage = () => {}; // Dummy do-nothing callback;
            //////port.close();
        });
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
openMidiInput() {
    if (this.midiAccess)
        this.midiAccess.inputs.forEach((port: WebMidi.MIDIInput, id: string) => {
            port.open();
        });
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

}`

jazz-soft commented 4 years ago

Can you please add the following output before the line #2854 in JZZ.js: console.log('Disconnected MIDI-Out:', p.id); hope it will give us some ideas...

Avijobo commented 4 years ago

This is the result (I got 2 midi ports, which I both close. I tried earlier with 1 port, same result):

I20200130-09:11:25.520(1)? Disconnected MIDI_Out I20200130-09:11:25.520(1)? Disconnected MIDI_Out W20200130-09:11:25.523(1)? (STDERR) TypeError: Cannot read property 'onstatechange' of undefined W20200130-09:11:25.523(1)? (STDERR) at _statechange (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2417:11) W20200130-09:11:25.524(1)? (STDERR) at _R._wm_watch (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2857:40) W20200130-09:11:25.524(1)? (STDERR) at _fireW (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:560:83) W20200130-09:11:25.524(1)? (STDERR) at _postRefresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:251:9) W20200130-09:11:25.525(1)? (STDERR) at Object._engine._refresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:754:7) W20200130-09:11:25.525(1)? (STDERR) at Timeout._onTimeout (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:859:64) W20200130-09:11:25.525(1)? (STDERR) at listOnTimeout (internal/timers.js:531:17) W20200130-09:11:25.525(1)? (STDERR) at processTimers (internal/timers.js:475:7)

jazz-soft commented 4 years ago

Does that means the MIDI-Out port name is empty?

Avijobo commented 4 years ago

Oops... here's the correct log:

I20200130-18:11:01.426(1)? Disconnected MIDI-Out: my_port I20200130-18:11:01.427(1)? Disconnected MIDI-Out: my_port W20200130-18:11:01.429(1)? (STDERR) TypeError: Cannot read property 'onstatechange' of undefined W20200130-18:11:01.430(1)? (STDERR) at _statechange (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2417:11) W20200130-18:11:01.430(1)? (STDERR) at _R._wm_watch (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:2857:40) W20200130-18:11:01.431(1)? (STDERR) at _fireW (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:560:83) W20200130-18:11:01.431(1)? (STDERR) at _postRefresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:251:9) W20200130-18:11:01.431(1)? (STDERR) at Object._engine._refresh (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:754:7) W20200130-18:11:01.432(1)? (STDERR) at Timeout._onTimeout (/home/johan/Meteor/TestProject/node_modules/jzz/javascript/JZZ.js:859:64) W20200130-18:11:01.432(1)? (STDERR) at listOnTimeout (internal/timers.js:531:17) W20200130-18:11:01.432(1)? (STDERR) at processTimers (internal/timers.js:475:7)

jazz-soft commented 4 years ago

What kind of device is that? What are the names of other input/output devices?

Avijobo commented 4 years ago

It is a virtual midi driver for linux: qmidinet, which uses the 'ipMidi' protocol, allowing midi connections between computers via ethernet on a LAN. These are all ports on the system:

I20200130-20:45:40.363(1)? midi input id = Midi Through Port-0, port name = Midi Through Port-0 I20200130-20:45:40.384(1)? midi input id = port 0, port name = port 0 I20200130-20:45:40.519(1)? midi output id = Midi Through Port-0, port name = Midi Through Port-0 I20200130-20:45:40.520(1)? midi output id = port 0, port name = port 0 I20200130-20:45:40.521(1)? midi output id = my_port, port name = my_port

The last midi output port 'my_port' is indeed a weird one, which I haven't defined. I'll see if I can update qmidinet to its latest version, will be something for tomorrow...

jazz-soft commented 4 years ago

Do you disconnect that MIDI port before the crash occurs? Or, how does it get disconnected?

Avijobo commented 4 years ago

I only close the midi input ports, then the crash happens. I am not using or closing the output ports.  null

jazz-soft commented 4 years ago

You just close the ports, you don't disconnect the ports form your system, correct?

Avijobo commented 4 years ago

Correct! null

jazz-soft commented 4 years ago

There is one more hack you can try - enumerate all MIDI outputs: midiAccess.outputs.forEach(call_some_dummy_function); and let me know if it helps...

Avijobo commented 4 years ago

That didn't help either.

I did found something though: When I launch qmidinet to create e.g. 2 virtual ports, it seems that an additional (temporary?) 3rd midi out port called "my_port" is being created. If I then open these 2 midi in ports, and then later close both of them, then this additional out port is being closed too, and this causes the program to crash.

The only way to avoid a crash is to open (and close) only 1 of the 2 input ports at a time. I tried this earlier too, before your last fix, but then I got a "MIDI CALL NOT AVAILABLE" error in that case. This is not occurring anymore now, thanks!

However when I close this 1 input port I now still occasionally get a console "Disconnected MIDI-Out: my_port" log coming from the console.log() line that you asked me to add at line #2854 in JZZ.js. Apart from this log, everything seems to work fine then.

So I would think the whole problem is related to a bug in qmidinet that generates a half-baked "my_port" that appears to be a midi out port, but seems to interfere with midi in ports as well? Anyway, I can live with using only 1 port now... I am just not feeling very comfortable with this occasional "Disconnected MIDI-Out: my_port" log that show up, so I am still hesitating to use my original workaround with a dummy callback instead of closing the midi in port.

Anyway, let me thank you once more for the time to look into this! Unfortunately (or fortunately?) the problem comes from elsewhere I think.

jazz-soft commented 4 years ago

Thanks! There is a lot of valuable information in your latest comment! Apparently, JZZ crashes when it gets the same "disconnected port" event twice. I'll see what I can do about it... Stay tuned :)

jazz-soft commented 4 years ago

Please try v1.0.2

Avijobo commented 4 years ago

That works!!! Can't get it to crash anymore now, even opening/closing all ports including "my_port" doesn't do any harm anymore.... works very nice now, thanks a million! I really appreciate the effort you did here!

jazz-soft commented 4 years ago

Happy to hear that! :)