spatialaudio / jackclient-python

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

MIDI message blast #39

Closed macdroid53 closed 7 years ago

macdroid53 commented 7 years ago

Hi, when I run the code below, it repeats the MIDI message forever. I can't seem to find the correct way to send a single midi message under my control. The first port connected below is just the GMIDImonitor with JACK support, the second is a midi port on a Echo AF12 connected to a mixer. Both show the message repeated until I hit the keyboard which satisfies the input() at the end of the code.:

#!/usr/bin/env python3
import jack
import struct

client = jack.Client("MyGreatClient")
inport = client.midi_inports.register("input")
outport = client.midi_outports.register("output")

portlist = client.get_ports(is_midi=True, is_physical=True)

for port in portlist:
    print('port: {} is output: {}'.format(port.name, port.is_output))

@client.set_process_callback
def macsend(frames):
    print('frames: {}'.format(frames))
    outport.clear_buffer()
    outport.write_midi_event(0, (0xB2, 0x01, 0x40))

client.activate()
client.connect(outport,"MIDI monitor:midi_in")
client.connect(outport, portlist[1].name)
print("#" * 80)
print("press Return to quit")
print("#" * 80)
input()
mgeier commented 7 years ago

it repeats the MIDI message forever

Yes, that's what you tell it to do.

Starting with client.activate(), the callback function macsend() is called once for each audio block, each time generating a MIDI event. BTW, you should call client.deactivate() in the end (or use a with statement) to properly stop the audio callback.

I can't seem to find the correct way to send a single midi message under my control.

When do you want to send that message?

If you describe what you actually want to do, I can probably give you some kind of a hint.

macdroid53 commented 7 years ago

Yes, I read that it will be called each process loop on the web site. So, I really wasn't surprised that this snippet is doing that. (I cobbled it from one of the examples, so it is lacking deactivate due to my lazy cobbling.) ;o

I read through the examples and they all use the callback to do the work. I'm missing the part that says "the work" is only one midi packet.

The simplest example of what I'm trying to do is: When the user clicks a button, my app must send, for example, a control change message to a specific midi channel. (So, for the event in the example, on my mixer, on midi channel 2 set the slider for channel 1, to -10db.)

MaurizioB commented 7 years ago

I usually have a process callback which uses a Queue. This is an example, taken from a custom program I use.

class JackThread(SomeThreadingClass):
    def __init__(self):
        [...]
        self.client.set_process_callback(self.process)
        self.client.activate()
        [...]

    def output_event(self, event):
        self.equeue.put(event)

    def process(self, frames):
        for port in self.client.midi_outports:
            port.clear_buffer()
        offset = 0
        while not self.equeue.empty():
            event = self.equeue.get()
            self.client.midi_outports[event[0]].write_midi_event(offset, event[1:])
            offset += 1

Then, from the main loop, I just call the output_event method with the midi event as argument; in your case it might be something like this.

    def button_clicked(self):
        self.jack_client.output_event(0, 0xB2, 0x01, 0x40)
macdroid53 commented 7 years ago

I haven't tried to run this code, but I do use queues so I get what you're doing.

What does the offset variable and subsequent increment of offset do? Is this actually indexing the jack buffer so the next set of midi bytes are added rather than overwritten when there is more than one event in the queue? (I just looked at the doc and it says the argument is: "Time (in samples) relative to the beginning of the current audio block." Your example increments by 1, so 1 sample per 3 midi bytes? Am I reading/groking that right? )

(I have implemented based on MaurizioB's example, thanks!, that works just I was seeking. I would still like to understand the time/sample argument mentioned above though for future reference.)

MaurizioB commented 7 years ago

I'm glad you sorted that out, even if my example wasn't complete. About your question, I don't exactly know how it (jack midi) works, but as far as I understand, you can only have a certain number of events per audio block, and they have to be ordered, according to the timing in the current block. As explained in the help, indeed, unordered events are ignored, but I don't actually know what happens if you write two events with the same order, I haven't tested that yet.

mgeier commented 7 years ago

Multiple MIDI events can be generated for the same offset. See my example midi_chords.py, where three notes are generated at the same offset.

@macdroid53 A few caveats regarding @MaurizioB's code above:

See my example play_file.py for the (hopefully!) proper use of queue.Queue.

MaurizioB commented 7 years ago

Thank you @mgeier for those tips!

macdroid53 commented 7 years ago

I've looked at play_file.py extensively and I admit that I have not groked every detail. But, I've adjust my code somewhat based on what I think I understand.

At this point I have the following code in my process function and I have a xrun callback as well:

def process(self, frames):
    self.print_error( 'frames={0}'.format(frames))
    for port in self.client.midi_outports:
        port.clear_buffer()
    offset = 0
    try:
        midievent = self.MIDIsndrqueue.get_nowait()
        # while not self.MIDIsndrqueue.empty():
        #     event = self.MIDIsndrqueue.get()
        print('In midi process: {0} || {1}'.format(midievent[0], midievent[1:]))
        self.client.midi_outports[midievent[0]].write_midi_event(offset, midievent[1:])
        sleep(0.100)
        offset += 1
    except queue.Empty:
        self.print_error('JACK midi queue empty')

But, just siting there it gets xruns:

frames=1024
JACK midi queue empty
An xrun occured, increase JACK's period size?
An xrun occured, increase JACK's period size?
Jack: JackClient::ClientNotify ref = 11 name = jmidi_ShowMixer notify = 3
Jack: JackClient::kXRunCallback
frames=1024
JACK midi queue empty
frames=1024
JACK midi queue empty
An xrun occured, increase JACK's period size?
Jack: JackActivationCount::Signal value = 0 ref = 11
Jack: JackActivationCount::Signal value = 0 ref = 11
frames=1024
JACK midi queue empty
Jack: JackClient::ClientNotify ref = 11 name = jmidi_ShowMixer notify = 3
Jack: JackClient::kXRunCallback
frames=1024
An xrun occured, increase JACK's period size?
JACK midi queue empty
frames=1024
JACK midi queue empty
Jack: JackActivationCount::Signal value = 0 ref = 11
Jack: JackActivationCount::Signal value = 0 ref = 11
frames=1024

So, I don't get why it would xrun when nothing is being processed. And how does one increase JACK's period? Can that be done in code based on buffer size, etc.? Or do the settings have to be adjusted from a JACK controller like QJackctl?

It also gets an xrun every time I something is queued. I basically send 3 byte single midi commands, is the buffer in the client that small by default?

mgeier commented 7 years ago

I don't get why it would xrun when nothing is being processed.

Well that's not quite true, you are doing plenty in your process callback. Did you try it with an actually empty callback?

Strictly speaking, you shouldn't even print stuff in the process callback. But I guess the real problem is this:

sleep(0.100)

I don't know which exact function this is (it might be time.sleep() from the standard library?), but are you really sleeping 0.1 seconds here? Why are you doing that? You should never sleep for any amount of time in the process callback, except if you want to deliberately cause xruns! If it's really 0.1 seconds, you should think for a moment what that means: Assuming that you use a sampling rate of 48000 Hz and a block size of 1024 frames, playing back one frame takes 0.021333 seconds. Obviously, your process callback must not take longer than that. Now if you sleep 0.1 seconds, that's five times as long!

Apart from that, you still use offset += 1. If you don't really need this, please remove it.

And how does one increase JACK's period? Can that be done in code based on buffer size, etc.?

This might work by assigning a new value to self.client.blocksize, see http://jackclient-python.readthedocs.io/#jack.Client.blocksize. But it probably makes more sense to choose the block size when starting the JACK daemon.

Or do the settings have to be adjusted from a JACK controller like QJackctl?

Yes, that's typically where you would do that. Or via command line options if you are starting jackd manually.

I basically send 3 byte single midi commands, is the buffer in the client that small by default?

I don't think so. I guess the problem is somewhere else.

You should provide a minimal but runnable code example, then I can also try it and help find the problem.

macdroid53 commented 7 years ago

So, I removed the sleep (it was indeed a time.sleep() call). I had put it there attempting to debug what I thought was a timing issue with the moving faders on one of the mixers. The xruns have quieted down to occasionally. (I should have known better having written plenty of interrupt callbacks for micro-controllers.) My bad... :(

I guess I don't get what the offset is, but, I removed the offset += 1 and it seems to work. Does it offset into a ring buffer for large blocks of data when the buffer is not empty?

mgeier commented 7 years ago

Good. You can probably tweak some JACK settings to completely get rid of xruns.

offset is much simpler than you probably think. In the docs it's called time: http://jackclient-python.readthedocs.io/#jack.OwnMidiPort.write_midi_event. It's just the time when you want your MIDI event to be sent. And this time is given in samples relative to the beginning of the current audio block. If you want to send your event as soon as possible, just use offset = 0.

Note that write_midi_event() might raise a JackError, you should check for that, just to be sure. If you want to send multiple events at the same time anyway, it's probably easier to use reserve_midi_event(), which returns an empty buffer if anything goes wrong.

Unless you have more questions, can you please close this issue?

macdroid53 commented 7 years ago

Ok, I was definitely making offset out to be more complicated than it is. Thanks for the enlightenment!

And, thanks again for the help.