Open sourcebox opened 3 years ago
What OS/platform are you using/targeting? Hot-plugging really depends on the platform and behaves differently (see also #35 and #78). Unfortunately I cannot give you any more advice, because I don't know the details of how this could work on the various platforms.
Either some cloning is required or the scanner object needs to be instanciated newly on each iteration. What is the preferred way here?
Cloning probably won't work, because the relevant objects are not Clone
. You are right that the scanner object would lose ownership when a connect actually happens, so you would need to recreate the MidiInput
/MidiOutput
objects in that case (is that what you meant with "iteration"?).
My app will be cross-platform on desktop, e.g. macOS/Windows/Linux. With "iteration" I mean each pass of the scan, currently done at an 1 second interval. On macOS, there strangely seems to be a limit how often MidiOutput::new()
/ MidiInput::new()
can be called. After 8 or 9 times of scanning, an error is returned. But I have to do some further investigation on this.
Ok, here is some demo code that loops about 6 times and then raises the error "MIDI support could not be initialized".
Platform is macOS 10.13
use midir::MidiOutput;
fn main() {
loop {
println!("Scanning MIDI ports");
let output = MidiOutput::new("midi scan output");
match output {
Ok(output) => {}
Err(error) => {
println!("{}", error)
}
}
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
It seems that MidiOutput::new() acquires some resource from the OS that is not released again. Do I miss something here?
Running the same code on Linux loops forever without any error.
It seems that
MidiOutput::new()
acquires some resource from the OS that is not released again. Do I miss something here?
That would be a bug in the backend or in the coremidi crate ... it would be interesting to know if it can be reproduced with coremidi
directly.
However, you don't need to call MidiOutput::new
in every single iteration. You can create an Option<MidiOutput>
and then only move your MidiOutput
out of it and create a new one when you're actually re-connecting.
However, you don't need to call
MidiOutput::new
in every single iteration. You can create anOption<MidiOutput>
and then only move yourMidiOutput
out of it and create a new one when you're actually re-connecting.
Yes, I also had this idea and implemented it that way today. Errors are gone now. Nevertheless it would be nice to have the issue fixed in the coremidi
crate. Especially for more complex use cases, when several ports have to be managed.
Nevertheless it would be nice to have the issue fixed in the coremidi crate. Especially for more complex use cases, when several ports have to be managed.
I agree. Unfortunately I don't own any macOS hardware, so I can't investigate this ...
I changed the demo code to use coremidi directly and filed an issue at its repository.
I've investigated the problem, and it seems that it is a limitation of CoreMIDI.
https://github.com/chris-zen/coremidi/issues/32#issuecomment-855202032
It seems that you already have a solution that re-uses the clients, but I think that if you need a robust solution, either midir implements some mechanism to scan ports, or you implement your own using the underlying libs (coremidi, ...).
Some interesting stuff I discovered:
When I replace Ok(output) => {}
with Ok(_) => {}
in the match statement of the demo code, the loop runs much more often before throwing an error. But at some point after several hundred iterations it fails again ;-(
Hi everyone.
I encountered this issue when I was fiddling with my pet project. I wanted to enable hot-plugging of midi devices, but after a couple of hours of poking around and trying to make it work it feels like it was me who was hot-plugged to present it lightly.
In the end I forced it to work.
Notice that I work mainly on MacOS, but its just a working environment, so many of intricacies of OS under-the-hood stuff is invisible to my eye.
That said, my main concern was in the fact that if midi device was unplugged when MidiInput::new()
was called first time, it will remain invisible for the entire lifetime of the program. That sole fact renders any realisation of hot-plugging obsolete.
So how does it work?
I wrote a little gist as a POC. Take a look at the main
function. For some reason initialisation of a new client with notifications enables every MidiInput
to receive actual information on midi ports.
My main hypothesis is follows: midly
uses coremidi::Client::new()
which does not support notifications. CoreMIDI
treats it like a one-shot client and loads data into it just one time on initialization. On the other side client with notifications receives updates with every notification and data it holds is actual. Maybe CoreMIDI
holds one client per process. And maybe when client with notifications is first created library elevates process's client rights and now all MidiInput
's will hold actual data.
Just a hypothesis though.
Anyway, it's working. I think it may be useful for every stranger who is looking for an advice on hot-plugging.
Thank you for sharing your investigations! As I said before, I currently don't own any macOS hardware, so it's hard for me to say anything about it. If there's someone who would be willing to help maintaining midir's macOS backend, please raise your hand ;-)
So according to your gist, it is required to (a) use Client::new_with_notifications
and then also have an event loop (CFRunLoopRunInMode
)? Is that something that most macOS applications have anyway? What would happen if midir itself does CFRunLoopRunInMode
and then an applications using midir would do the same? I suppose there can't be two event loops?
According to my experiments, if there are two event loops running on different threads, there are two possible errors that will occur when midi device is being plugged in: segfault and trace trap. Both are kinda bad and will immediately kill the process.
Event loop is typically used by Mac/iOS applications that use core foundation framework as far as I understand. Applications that do not rely on it won't have event loop running. Any Rust application will have to manually enable it as gist shows. Maybe it's wise to start this loop under the hood only if feature is enabled. Otherwise start default client.
Actually I stole event loop idea from an example of coremidi. It does contain couple of links with details on event loops. Maybe it's maintainer would have any clue how does it work.
Oh, actually I just discovered that if you use coremidi
0.4 you will receive a segfault on device plugin even if you have just one event loop. It seems that is was fixed in 0.5.
Two loops are running in different threads without an issue and hot-plugging still works fine.
Hi, I am planning to use this library in a new musical live performance application. Hotplugging is very important for live performance. A party should not be ruined because a USB cable falls out. :smile:
Hello, thanks for project, in my case hotplugging is very important too.
Hi I suspect I am running into a similar issue. I'm trying to write some code that will notify when a new device is plugged in or unplugged. On macos, scanning the midi ports by creating a new MidiInput
on every iteration fails after a few attempts. If you attempt to reuse the MidiInput
and call input.ports()
and port_name()
on every iteration, you don't get this error however it doesn't detect when devices are connected or disconnected. ~I noticed that coremidi was upgraded but I think there is still an issue. Has anyone else been able to implement this?~
[Edit] After more research I found this it seems like in order to be notified of changes to the ports, your application must start using something like this from core_foundation.
use core_foundation::runloop::CFRunLoop;
fn main() {
CFRunLoop::run_current();
}
In order for midir to support notifications from coremidi, would we not need to create the core midi client using new_with_notifications()
?
For reference here is the code that doesn't throw but that can't detect changes in available ports. I'm on macos 12.6.3 midir 0.9.1
use midir::MidiInput;
use std::{thread, time};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let midi_input = MidiInput::new("")?;
loop {
let ports = midi_input.ports();
println!("Devices:");
for (i, port) in ports.iter().enumerate() {
match midi_input.port_name(port) {
Ok(name) => {
println!("{}", name);
},
_ => {}
}
}
println!("----");
thread::sleep(time::Duration::from_millis(500));
}
Ok(())
}
I'm working on an editor for a hardware synthesizer connected via USB MIDI. Ideally, the device is connected all the time so the application finds it right from the start. In reality, a more mature approach is required, meaning that the synth can be connected or disconnected at any time the editor is running.
My question is here, what is the best way to achieve this? Of course, something has to run periodically in the background and scan the ports, since change notification is not supported yet. Should this be done in a separate thread or just called from the main thread at an interval?
As soon as the port becomes available, a connect must be performed. It seems, that the connect() method takes the ownership, so the scanner object loses it. Either some cloning is required or the scanner object needs to be instanciated newly on each iteration. What is the preferred way here?