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

Best practice to allow hot-plugging? #86

Open
sourcebox opened this issue May 31, 2021 · 17 comments
Open

Best practice to allow hot-plugging? #86

sourcebox opened this issue May 31, 2021 · 17 comments

Comments

@sourcebox
Copy link

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?

@Boddlnagg
Copy link
Owner

Boddlnagg commented May 31, 2021

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"?).

@sourcebox
Copy link
Author

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.

@sourcebox
Copy link
Author

sourcebox commented May 31, 2021

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.

@Boddlnagg
Copy link
Owner

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.

@sourcebox
Copy link
Author

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.

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.

@Boddlnagg
Copy link
Owner

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 ...

@sourcebox
Copy link
Author

I changed the demo code to use coremidi directly and filed an issue at its repository.

@chris-zen
Copy link
Contributor

chris-zen commented Jun 5, 2021

I've investigated the problem, and it seems that it is a limitation of CoreMIDI.

chris-zen/coremidi#32 (comment)

@chris-zen
Copy link
Contributor

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, ...).

@sourcebox
Copy link
Author

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 ;-(

@ssankko
Copy link

ssankko commented Dec 12, 2021

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.

@Boddlnagg
Copy link
Owner

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?

@ssankko
Copy link

ssankko commented Dec 13, 2021

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.

@ssankko
Copy link

ssankko commented Dec 13, 2021

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.

@Be-ing
Copy link
Contributor

Be-ing commented Dec 21, 2021

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. 😄

@iamguid
Copy link

iamguid commented Jan 10, 2022

Hello, thanks for project, in my case hotplugging is very important too.

@willm
Copy link

willm commented Jul 30, 2023

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

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

7 participants