spatialaudio / jackclient-python

🂻 JACK Audio Connection Kit (JACK) Client for Python :snake:
https://jackclient-python.readthedocs.io/
MIT License
131 stars 26 forks source link

midi_file_player.py + multiprocessing #99

Open Stemby opened 3 years ago

Stemby commented 3 years ago

Hi, I'm trying to modify this example in order to make it compatible with multiprocessing.

Mainly I had to rename some global variables:

#!/usr/bin/env python3

import sys
import threading

import jack
from mido import MidiFile
from multiprocessing import Process

midi_file = '../Musica/Mozart/k265/Zwölf-Variationen.midi'

try:
    mid = iter(MidiFile(midi_file))
except Exception as e:
    sys.exit(type(e).__name__ + ' while loading MIDI: ' + str(e))

client = jack.Client('Luces')
midi_port = client.midi_outports.register('output')
midi_event = threading.Event()
midi_msg = next(mid)
midi_fs = None  # sampling rate
midi_offset = 0

@client.set_process_callback
def process(frames):
    global midi_offset
    global midi_msg
    midi_port.clear_buffer()
    while True:
        if midi_offset >= frames:
            midi_offset -= frames
            return  # We'll take care of this in the next block ...
        # Note: This may raise an exception:
        midi_port.write_midi_event(midi_offset, midi_msg.bytes())
        try:
            midi_msg = next(mid)
        except StopIteration:
            midi_event.set()
            raise jack.CallbackExit
        midi_offset += round(midi_msg.time * midi_fs)

@client.set_samplerate_callback
def samplerate(samplerate):
    global midi_fs
    midi_fs = samplerate

@client.set_shutdown_callback
def shutdown(status, reason):
    print('JACK shutdown:', reason, status)
    midi_event.set()

def run_midi():
    with client:
        print('Playing', repr(midi_file), '... press Ctrl+C to stop')
        try:
            midi_event.wait()
        except KeyboardInterrupt:
            print('\nInterrupted by user')

if __name__ == "__main__":
    processes = [Process(target=run_midi)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

Now I get this error at the end of the execution:

Playing '../Musica/Mozart/k265/Zwölf-Variationen.midi' ... press Ctrl+C to stop
Cannot read socket fd = 5 err = Success
CheckRes error
JackSocketClientChannel read fail
JACK shutdown: JACK server has been closed <jack.Status 0x21: failure, server_error>

How can I fix it?

Thank you!

Carlo

HaHeho commented 3 years ago

What OS are you on? (there is some specifics of spawning child processes with multiprocessing that do not work on Windows quite the way they work on Linux or macOS)

Also, I believe you want to use multiprocessing.Event instead of threading.Event in order to be consistent. This is to begin with and most probably not going to solve your problem (yet).

Stemby commented 3 years ago

Hi @HaHeho, thank you for your reply.

What OS are you on?

Debian GNU/Linux (testing) with kernel realtime:

$ uname -r
5.9.0-1-rt-amd64

I believe you want to use multiprocessing.Event instead of threading.Event in order to be consistent.

Yes, this makes sense.

Here is my current code:

#!/usr/bin/env python3

import sys
import jack
from mido import MidiFile
from multiprocessing import Process, Event

midi_file = '../Musica/Mozart/k265/Zwölf-Variationen.midi'

try:
    mid = iter(MidiFile(midi_file))
except Exception as e:
    sys.exit(type(e).__name__ + ' while loading MIDI: ' + str(e))

client = jack.Client('Luces')
midi_port = client.midi_outports.register('output')
midi_event = Event()
midi_msg = next(mid)
midi_fs = None  # sampling rate
midi_offset = 0

@client.set_process_callback
def process(frames):
    global midi_offset
    global midi_msg
    midi_port.clear_buffer()
    while True:
        if midi_offset >= frames:
            midi_offset -= frames
            return  # We'll take care of this in the next block ...
        # Note: This may raise an exception:
        midi_port.write_midi_event(midi_offset, midi_msg.bytes())
        try:
            midi_msg = next(mid)
        except StopIteration:
            midi_event.set()
            raise jack.CallbackExit
        midi_offset += round(midi_msg.time * midi_fs)

@client.set_samplerate_callback
def samplerate(samplerate):
    global midi_fs
    midi_fs = samplerate

@client.set_shutdown_callback
def shutdown(status, reason):
    print('JACK shutdown:', reason, status)
    midi_event.set()

def run_midi():
    with client:
        print('Playing', repr(midi_file), '... press Ctrl+C to stop')
        try:
            midi_event.wait()
        except KeyboardInterrupt:
            print('\nInterrupted by user')

if __name__ == "__main__":
#    run_midi()
    processes = [Process(target=run_midi)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

When calling run_midi() (by commenting the multiprocessing part) now I get this output at the end of the execution:

Playing '../Musica/Mozart/k265/Zwölf-Variationen.midi' ... press Ctrl+C to stop
Cannot read socket fd = -1 err = Success
Could not read result type = 7

but after that the command line is available. So, something wrong happens with multiprocessing.Event() used as replacement of threading.Event().

When running the code I posted here (by commenting the run_midi() call) I get this output at the end of the execution:

Playing '../Musica/Mozart/k265/Zwölf-Variationen.midi' ... press Ctrl+C to stop
Cannot read socket fd = 5 err = Success
CheckRes error
JackSocketClientChannel read fail
JACK shutdown: JACK server has been closed <jack.Status 0x21: failure, server_error>

and the process doesn't stop: so no differences compared to my previous attempt with multiprocessing+threading.

Thanks!

mgeier commented 3 years ago

@Stemby Let's take a step back: do you really need multiprocessing?

This is really hard to get right and some libraries might not support it very well. I don't know if the jack module does or not.

If you don't really need multiprocessing, you should do yourself the favor and not use it.

If you are willing to load the whole audio file into memory (the MIDI file is already in memory), you might not even need threading!

If you really need multiprocessing, you should try to simplify your reproducible example:

Then we can start debugging ...

Stemby commented 3 years ago

Thanks @mgeier!

do you really need multiprocessing?

I'm not sure. I suppose it would be the must solid solution for my purpose (if it works properly). At the moment I'm testing a simpler way (threading) and postponing the migration to multiprocessing.

If you really need multiprocessing, you should try to simplify your reproducible example

OK, thank you!

HaHeho commented 3 years ago

do you really need multiprocessing?

Isn't that the only way to realize file playback (either audio or MIDI) as an independent component which can be e.g. invoked or remote controlled by other parts of an application?

If then multiprocessing or threading is used probably doesn't change the method or required effort of the implementation, or does it?

Since quite a while, I was thinking to create a simple reference example for (audio) file playback which utilizes multiprocessing (and/or maybe also one with threading). @mgeier do you think that would be useful, especially with regards of investigating the latter part of:

This is really hard to get right and some libraries might not support it very well. I don't know if the jack module does or not.

mgeier commented 3 years ago

do you really need multiprocessing?

Isn't that the only way to realize file playback (either audio or MIDI) as an independent component which can be e.g. invoked or remote controlled by other parts of an application?

It depends on what exactly you mean by "independent component", but I don't think so.

In simple cases, neither threading nor multiprocessing are needed, because the audio callback runs in a separate thread anyway, and whatever else has to be done, can be done in the main thread.

But if multiple things have to be done concurrently in the main thread, a solution has to be found. I guess the next best solution is to use threading, but probably async/await (with or without asyncio) could be used as well. multiprocessing should only be used if it's really needed.

If then multiprocessing or threading is used probably doesn't change the method or required effort of the implementation, or does it?

In my experience it very much does, because multiprocessing is harder to use than it might seem at first glance.

The resulting code is certainly less intuitive, because you have to think about which parts of the program state will be duplicated in multiple processes, which often isn't very obvious.

Since quite a while, I was thinking to create a simple reference example for (audio) file playback which utilizes multiprocessing (and/or maybe also one with threading). @mgeier do you think that would be useful, especially with regards of investigating the latter part of:

This is really hard to get right and some libraries might not support it very well. I don't know if the jack module does or not.

Yes, having an example to test whether the jack module works with multiprocessing would be great!

But it would be really good if the example would somehow represent a situation where multiprocessing matters.

Of course it should be as simple as possible, but it would be good for illustration if one of the processes would do some CPU-intensive work.

mgeier commented 3 years ago

@Stemby Any news on this?