-
Notifications
You must be signed in to change notification settings - Fork 49
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
MIDI input/output disconnection - Ports should be persistent across connects/disconnects #79
Comments
Thanks for posting that, Chris!
Why? Wouldn't it be more convenient to mantain the event handler? The worst it can happen, when an interface disconnects, is not getting MIDI events anymore. A web application could, for example, listen for midi message on every input and be oblivious that some of them are actually disconnected or reconnected (obviously, if it wants to know, it could still listen to the disconnect / connect events). |
My idea has been that the instance would work like nothing happened (unless you choose to discard the whole thing) on disconnect -> reconnect. To me it seems that if two ports' that are of the same type and share the ID, they should behave exactly the same. I can't see that if they're from the same MIDIAccess they would even be a different instance. However, for isolation, it's probably better that different MIDIAccess instances don't share the same port instances, so that you can safely assume that if you get a port from a fresh MIDIAccess, no one has set any |
Actually, I don't know how devices map to MIDI ports. Let's say I plug in device1 and device2. device1 gets port 0 and device2 gets port 1. Then I disconnect device1 and plug in device3. Will device3 either take the first unallocated port (port 0) or will it take another port? or the behaviour is random/undefined? |
(Obviously I'm talking about USB MIDI interfaces / MIDI hubs) |
What do you mean by "gets port 0"? Gets the index of 0 in the array? The index is going away in the issue about getInputs(). |
"gets port 0" means "is at index 0 in the live input() list". What happens in the live input list if I disconnect a device and attach another device? Suppose that: var inputs = midiAccess.inputs(); Results in: inputs === [InputPort_forDevice1, InputPort_forDevice2] What will the same operation result if I disconnect Device1 and connect Device3? |
today, if you used inputs[0] in your example, it would begin by referring to device1, then it would refer to device2. This temporal funkiness is why we're moving to Maplikes - the index identifier will go away. |
So, if a device goes away and comes back, all the event handlers are gone, right? It seems like this is the most sane way to avoid sending things to the wrong place and is analogous to a file descriptor being closed. |
That was the rough proposal I was making above, yes. But it might make more sense to keep them alive, and restore them if the device comes back, I don't know. |
What does coremidi do? They seem to have the sanest OS-level API of anything. I know Alsa permanently disconnects when hardware goes away: you have to react to the disconnect/reconnect events yourself if you want to get messages again. |
Hmm. I'm not sure the file descriptor analogue works here as closing file descriptors is controlled by the program whereas disconnecting a device is nothing the program can affect. However, if the program doesn't listen to disconnect events, you might get messy situations where buffered MIDI messages all get fired simultaneously when the device is reconnected (unless we specify quite explicitly how to handle such situations). Then again in the same case, if it's an input device, it would start working seamlessly when the user reconnects which would be quite nice user experience. My personal preference would be to just resume if the device becomes reconnected, given that the device is essentially the same before and after reconnect. But in the end, either option doesn't make anything impossible or more possible. |
Core MIDI provides a permanent access. So once the application get the handle, even the device is disconnected, and connected again, the handle continuously works. Chrome also provides permanent MIDIPort object on OS X thanks to this Core MIDI behavior. My preference is also keeping the MIDIPort alive even after device disconnection, and the MIDIPort gets to work again once the device is reconnected. Now we have MIDIInputMap and MIDIOutputMap, and is going to have MIDIPortState. So Core MIDI like behavior looks more reasonable. |
Now Chromium implements MIDIConnectionEvent, and MIDIPort works as a permanent reference that can be reused to control the reconnected same device. |
I've opened issue #123 for a problem I see with this new behavior. We need to be clearer about underlying system resource use. Since I haven't seen it anywhere, let me propose a state machine using the currently defined MIDIPortStates (blank states mean no MIDIPort entry at all):
I do want to see MIDIPorts be able to disappear from the maps, in the two transitions I show above. Otherwise you'll get an ever-growing list of dead connections as you plug/unplug different hardware or start/stop other MIDI programs on the same machine. Moreover, it is a limitation of USB that 2 identical pieces of hardware are usually indistinguishable. I definitely imagine this scenario (even on CoreMIDI):
We have the caveat ("some systems may not support completely unique persistent identifiers") on the |
I think my only concern is if we need to represent "disconnected and closed" as a separate state - i.e. what will happen if this disconnected port becomes reconnected. (The first blank cell in your table.) The second blank cell should be "disconnected" (remember, you may already HAVE a MIDIPort you're holding on to. But yeah, they shouldn't show up in the list then.) On your identical-ports issue - actually, on Windows IIRC they will still show up in the same order and have the same HMIDI, based on the USB port they were plugged in to. (I kid you not.) |
Useful table! But as I interpreted the states, Now, arguably, a feature to auto-open a disconnected port that was open at time of disconnection can be useful (but maybe not generally expected?), but then I'd opt for a new state Regarding disconnected ports in the map of ports: if we have semantically unambiguous states, we can leave it up to the implementation if disconnected ports appear in the map or not. I do see value to keep disconnected ports there. The implementation can purge old dead ports whenever the map grows too much. |
@cwilso: Line 486 in 5713cf3
"connected" means "closed" in the current specification, unless I am misunderstanding the states. I wanted to avoid suggesting renaming them, but if they are confusing, we should consider it. Also, the Windows decision of binding USB devices to their ports is a different way to work around the optional/duplication of USB serial numbers, with different effects: My understanding is that if you switch the port of your MIDI device, you won't be able to find it again with the same handle on Windows. Probably the best way is to use port as a tie breaker. But you can still defeat the system by switching the 2 devices. It's the best of an unfortunate situation. @bome: As for dead ports, I feel that if we are asking clients to hold onto What we really may need is a |
On device swap issue, I agreed that it's better to have a caveat in the spec. But, as one DAW user, I do not expect that applications is capable to identify two same model music instruments correctly when we connect and disconnect them in such a misleading way. So, this is not a serious problem. For long term goal, it would be more important to find a way to give a name in a standardized way against each device on all operating systems. Once I create a song with online DAW running on Chrome/Windows that is connected with a motif rack, I hope I can open the song correctly with the same online DAW running on Chrome/Mac that is connected with the same or another motif rack. But for now, it will be difficult because manufacturer, name, and so on depends on operating systems. So, I may need reconfiguration on device assignment against the song project. |
Here is a new proposed state table. The side effects column has some questions in bold that we should resolve.
|
@toyoshim I like your idea of cross-device persistent identifiers. Can you open a new issue for this? It seems like a tricky problem. |
|
I put it here for easier editing: Feel free to fork or whatever (I am not good with gist) |
Let me try another table:
Does this capture the current specification? If so, then I think you are proposing this change:
I suggest this change:
My argument is that I do not like the nondeterminism introduced by the optional missing in the first row. I would rather make it always missing, to reduce the number of special cases that clients must understand. |
What does "state name: (missing)" mean? That "state" is removed? null? "missing"? |
I see two options - names chosen optimizing for developer use, feel free to bikeshed. Option 1:
Option 2:
I prefer option 1. I think developers are typically interested just in the device state (connected vs disconnected), since the open/closed connection status they are control of, for the most part. We should be explicit about whether we queue up send() data that is received during disconnected state. |
is it common to have boolean attributes in ECMAScript? Option 3: partial interface MIDIPort {
readonly attribute boolean connected;
readonly attribute boolean open;
}
if (port.connected)
console.log("there you are, MIDI port!")
if (port.open)
console.log("yes, indeed I have opened the port") For me, it's best to discard any data that is sent to a disconnected port (regardless of |
It's not, but mostly due to concern that the list of states might need to be expanded at some point. It's not proscribed. Since MIDI is 30 years old, I doubt the states are going to expand. So, option 3: booleans, as @bome suggests. I'm on the fence on 1 vs 3. |
My vote is on enums. If you ever end up needing to pass both states in a method, it's clearer to have 2 enums instead of 2 booleans.
|
I like option 1 and 3 rather than 2. Less typing is NOT my priority because usually developers spend most time on other things, designing, reading, testing, and debugging on development. IDE is also so smart today, and developers are great typist. So it does not make much difference. |
@agoode we won't ever have a "makeState", though - the connection (hardware) state is absolutely read-only and outside of software control, and the connection state already has open()/close(). That said, preference seems to be on the enum side - aka Option 1 - so unless someone speaks up in the next 24 hours or so, I'll write that option up in the spec. |
As a bonus point of separating states to MIDIPortDeviceState and MIDIPortConnectionState, open() and close() can be sync methods, right? Now that they do not depend on device connection state, it can be processed immediately without any IPC or I/O operations. |
There is still I/O work that happens with open() or close(). I think you are proposing that open() and close() return immediately, and the onstatechange handler reporting the result? open() can fail though, so promise does seem better. An example is for a device already opened by another program in exclusive mode. Both Windows and Linux can fail in this way. |
What @agoode said. Open() can still fail. Close() returning a promise probably isn't necessary, but probably good symmetry. |
open() should not depend on device status that happens completely in parallel. So, if the device is occupied by another application, we may want to show the device as disconnected or in another new MIDIPortConnectionState, e.g., unavailable. Otherwise, we lose major benefit of separating state to two states, that avoid many kinds of race conditions. |
In other words, we allow open state even on disconnected state. That means open() succeeds even in disconnected state. Otherwise, we could not be consistent on some race situations. E.g., device is disconnected on opening a device. |
Hmm, it's true that we need to explicitly enable open() on disconnected ports, and define precisely how/when it does/does not fail. However, we have a need to determine, in the explicit open() case at least, whether or not we ACTUALLY got access to the port. In a exclusive-access-to-ports system (Windows or Linux, e.g.), this is important. As it is right now, open() will fail if the port is unavailable - e.g. if the port is disconnected. Similarly, an implicit open() on an output port will fail ("If the port is "disconnected", throw an InvalidStateError exception."), and on an input port it will fail to open the port (since it just runs the "open" algorithm). I'm not convinced this is bad. I'm really only concerned that if I had an input port opened, and it gets disconnected then reconnected, it becomes reopened. |
Reopened this just until the discussion on open()ing a disconnected port resolves. |
@toyoshim I don't think "we allow open state even on disconnected state" MUST mean "open() succeeds even in disconnected state." I think we can avoid this as a race condition - it's deterministic. I think the only point here is that we MUST give some way of determining that you can ACTUALLY send/receive on a port; are you saying that is "open the port, then make sure that .state is 'connected'"? |
Since a device can be disconnected at any time, it is impossible to ensure that a port is ready to send a data in a certain situation. Also with the same reason, it's impossible to say open() will success only on connected state because MIDIPortDeviceState may be changed at any time. Even if the browser implementation succeeded to lock the device, on returning a result to the JavaScript side, the device may be disconnected. So only we can say is that the data will be sent if the device is connected for a certain period before and after calling send(). And incoming data will be received if the device is connected and port is in open state at that time. Generally speaking, reliable communication needs end-to-end handshake, and MIDI does not have such mechanisms. That was my first reason why I prefer separated two states. |
Although I agree that it's impossible to ensure a port is ready to send at all times, we need to capture in the API the state of "probably ready to send". The big problem here is on Windows/Linux, where a piece of software will definitively need to know if it was allowed access to the port (since port access is exclusive). I'm thinking that this EITHER means we ensure the port is always "closed" when "disconnected", even if it is automatically reopened when the port is reconnected (my personal preference), or we add another exposure state of "underlying system has given access". I think the changes that are necessary are: open() should only succeed if the port is allowed access by the underlying system (i.e. Windows gives us exclusive access); disconnection should move connection to "closed", but on device connection, if there is already a Port with this id and it has an event handler, it gets automatically re-opened if possible. (in other words, "open" means Windows has given access; a disconnected port would never be "open".) Thoughts? |
So, I'm ok to keep open() as an async method returning Promise. That can be interpreted as a process to get an access permission from the underlying system that is different concept from device state change. That idea will be easily extended for obtaining port-based sysex permission in the future. For exclusively retained case, I'm negative to add a new state. If we introduce a new state for the exclusive case, we may want to invoke a statechange handler when the device gets to be available. But I'm afraid that only we can implement it is to poll the retained device periodically. How about just rejecting the Promise for open() with a certain error? Probably InvalidAccessError? |
Sorry, I should have said there are three options:
Yes, the open() rejects with InvalidAccessError today if underlying access is not given. |
Thank you Chris. |
The advantages of 1) are that you can determine if a disconnected port will reopen when connected (state is open), and it gives you a way to stop that (call
Even though it's not particularly pretty, an additional state will solve all these problems:
|
Indeed, that's why I separated the states to begin with, and somehow failed to noticed I'd messed up that value - so thus I give you option 2a): open() is async, and rejects if the underlying system doesn't give you access. If an "open" device is disconnected, its connection state transitions to "pending"; when a Port with connection state "pending" is reconnected (.state=>"connected"), the open algorithm is run on it to attempt to reopen. If this connection fails (e.g. the Port is reserved by something else in the underlying system, and therefore unavailable), the connection state moves to "closed", else it transitions back to "open". This is done prior to the state change event for the device state change, so the state would reflect this reopening (or lack thereof). close() is allowed on ports that are "pending", and will quickly transition to "closed". open() is allowed on disconnected ports, but it will resolve quickly - but only transition the state to "pending", not to "open". (In effect, it is marking the port for auto-opening.) |
+1 for naming it "pending", +1 for adding |
@cwilso Do you mean MIDIPortConnectionState has three states, "open", "pending", and "closed", and MIDIPortConnectionState has two states, "connected", "disconnected"? |
@toyoshim yes, precisely. I'll write that up. |
I am a musician and developer working on multiple WebMIDI projects and greatly appreciate all those here and elsewhere have done to implement it. Should the last reference showing on this page talking about MIDIPortConnectionState refer to MIDIPortDeviceState as follows? Do you mean MIDIPortConnectionState has three states, "open", "pending", and "closed", and MIDIPortDeviceState has two states, "connected", "disconnected"? |
@PozEnergy Yes, that's correct. (That was what @toyoshim meant I'm sure, and what I put in the spec, but you're right, he wrote it incorrectly in the comment above.) |
Reported externally:
What is the exact workflow when a MIDI input disconnects? Suppose I'm listening on port 2, which is connected at start, setting onmidimessage appropriately.
Then port 2 disconnects (I receive a disconnect event on my ondisconnect handler) - What is the fate of my onmidimessage port 2 handler?
If input 2 re-connects, should I re-set the appropriate onmidimessage handler to listen for midi events on port 2, or, having set it before, the old handler (set before disconnection and reconnection) will still work?
I think that when a port disconnects, it should release the event handler and the object, and they should not become live again, even when reconnected. At the same time, the Maplike rearchitecture may change this, since that port would be the same as the previous port. Discuss.
The text was updated successfully, but these errors were encountered: