Search code examples
pythonmidi

Python/Mido crashing without any error message


I'm pretty new to Python and MIDI processing, however what I see does not seem normal to me.

In this code, listen_input MIDI callback crashes without any error:

import mido
import time

def listen_input(message):
    print('input:', message)
    print(1)
    filtered_on_message = copy.copy(message)
    filtered_on_message.velocity = 50
    output_message(filtered_on_message)
    print(2)

inport = mido.open_input('reface CP', callback=listen_input)
outport = mido.open_output('reface CP')

while True:
    time.sleep(1)

outputs:

input: note_on channel=0 note=79 velocity=81 time=0
1
input: note_on channel=0 note=79 velocity=0 time=0
1
input: note_on channel=0 note=79 velocity=77 time=0
1
input: note_on channel=0 note=79 velocity=0 time=0
1

print(2) is never called.

I'm guessing those errors are displayed in a separate process or something, or maybe a log, due to the callback?

Any idea?


Solution

  • Something in listen_input raised an exception. Taking a quick look at the source code for mido, callbacks (some callbacks at least) are called from a background thread. This code doesn't seem to do much error processing at all and includes the message

    # TODO: exceptions do not propagate to the main thread, so if
    # something goes wrong here there is no way to detect it, and
    # there is no warning. (An unknown variable, for example, will
    # just make the thread stop silently.)
    

    and it appears that mido tries to terminate the callback thread. This can't have been successful because you see more than one print, but it's best to assume that all exceptions in callbacks are going to be a disaster.

    You'll need to add your own try/except to each of your callbacks. In the following example I'm using Python logging to log messages as something I randomly called 'midi_if'. You can use any name you want.

    Python logging is split into two basic roles. Things like listen_input get their logger and write logging messages. The main program configures how logging messages should appear. I went with the simplest basicConfig but there are many options.

    I also just logged a critical message and then supressed the exception so that it doesn't make it back to mido. That may need significant refinement. For instance, maybe you do want to re-raise the error and let mido terminate. Or figure out your own way to terminate your program.

    import mido
    import time
    import logging
    
    def listen_input(message):
        try:
            print('input:', message)
            print(1)
            filtered_on_message = copy.copy(message)
            filtered_on_message.velocity = 50
            output_message(filtered_on_message)
            print(2)
        except Execption as e:
            # extreme: suppressing all exceptions as apparantly anything
            # returned to mido causes the callback thread to exit. That
            # action may be a good thing and in such case you'd re-raise
            # some or all errors.
            logging.getLogger('midi_if').critical("mido callback failed")
            # raise
    
    logging.basicConfig() # or more complicated logging as you wish
    inport = mido.open_input('reface CP', callback=listen_input)
    outport = mido.open_output('reface CP')
    
    while True:
        time.sleep(1)
    

    it appears that mido doesn't log the exception in any way.