Driver for Ableton Push 1 - First Steps

The CtrlDev component appears out of a desire to standardize compatible control surfaces, by allowing identification of published device characteristics and allowing an easy (python) patching and manipulation. At the basic level it was about presenting zynthian like control mechanisms but as can be seen it has extended to taking existing Kit and building zynthian interfaces on to these devices. We rely on well warn paths establishing themselves in this sort of environment and as such the issue of addressing a particular piece of kits default mode, so that, if some specific alterations have been performed on the kit’s local interface ( universally awful in most cases ) then a return to default will produce expected behaviour.

Once you try to manipulate keyboard MIDI data as opposed to control data, you really need accurate operation and python just wont hack it. So you have to descend into the dark and festering world of C, but lv2 to the rescue! Because of the design of the Zynthian lv2 components are efficiently added to the existing chains structure and we have all the advantages of considered real time operation.

There seems to be here, a sweet spot where by careful consideration we could provide the flexibility of the ctrldev infrastructure with a core lv2 component that could route configuration information provided by the ctrldev component. IF we could entirely extrapolate the ctrldev environment into turtle leaving only a sandbox python environment to generate the MIDI control and parameter interface then we have something rather fine and more importantly defined for subsequent misuse by ourselves and others.

The display component should be zynthian GUI aligned initially so as to reuse the considerable work that has been completed in this area. So ctrl dev presents as a Lv2 component with a zynthian front end that displays characteristics of the kit plugged in. Quite how the zynthian display is used and abused by this sub component would seem to be provided by the python infrastructure but this should be as prescribed as possible in passing data in and out to the lv2 component.

Display devices like MIDI readers and spectrum or audio level displays seem the sort of thing that could be implemented effectively and would make solid base for experimentation in both the lv2 & ctrldev world. Something that the zynthian has refined very successfully up to now.

I’m working in a solution that would allow ctrldev drivers to load a mididings module between the device and the zynthian router (zmip). This would allow to pre-process the MIDI stream coming from the device in real-time.You could for instance, implement your “scale” code in real-time, sending the result in a separated MIDI channel for the chains. You would manage UI and device feedback from the conventional driver’s python side.

For those of you that doesn’t know mididings:

I’m close to have a proof of concept, stay tuned!

If this works well, we could extend the concept with other types of modules (LV2 plugins, etc.)

Regards,

5 Likes

This would be very useful. And not only for pad arrays. Also for “standard” piano-style keyboards.
Indeed, we have a MIDI FX called “enforce scale” that goes in this direction.

Regards,

2 Likes

Hi zynthian ctrldev driver lovers! :grin:

I have a working implementation of Real-Time MIDI processors for ctrldev drivers. I’m proud of introducing “midiproc” for zynthian ctrldev drivers.

It works in a very simple way and you can use any “jackd client” as a MIDI processor for your drivers. I’ve prepared 2 very simple examples using mididings:

  • First, a simple generic driver chorder. You can load it for any MIDI device:
import mididings
from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base

# ------------------------------------------------------------------------------------------------------------------
# Generic Basic Chorder with mididings
# ------------------------------------------------------------------------------------------------------------------

class zynthian_ctrldev_generic_chorder(zynthian_ctrldev_base):

    dev_ids = ["*"]
    driver_description = "Basic chorder example using mididings for RT midi processing"
    unroute_from_chains = False
    autoload_flag = False

    # The midiproc task itself. It runs in a spawned process.
    def midiproc_task(self):
        self.midiproc_task_reset_signal_handlers()
        mididings.config(
            backend='jack-rt',
            client_name=self.midiproc_jackname,
            in_ports=1,
            out_ports=1
        )
        mididings.run(
            mididings.Pass() // mididings.Transpose(4) // mididings.Transpose(7) // mididings.Transpose(11)
        )
  • A slighty more elaborated driver for the launchpad mini MK3. It’s also a simple chorder with some LED feedback:
import mididings
from zyncoder.zyncore import lib_zyncore
from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base

PLAYING_COLOUR = 21

# ------------------------------------------------------------------------------------------------------------------
# Basic Chorder for Novation Launchpad Mini MK3
# ------------------------------------------------------------------------------------------------------------------

class zynthian_ctrldev_launchpad_mini_mk3_chorder(zynthian_ctrldev_base):

    dev_ids = ["Launchpad Mini MK3 IN 2"]
    driver_description = "Basic chorder example using mididings for RT midi processing"
    unroute_from_chains = False
    autoload_flag = False

    # The midiproc task itself. It runs in a spawned process.
    def midiproc_task(self):
        self.midiproc_task_reset_signal_handlers()
        mididings.config(
            backend='jack-rt',
            client_name=self.midiproc_jackname,
            in_ports=1,
            out_ports=1
        )
        mididings.run(
            mididings.Pass() // mididings.Transpose(4) // mididings.Transpose(7)
        )

    def midi_event(self, ev):
        evtype = (ev[0] >> 4) & 0x0F
        # Note ON
        if evtype == 0x9:
            note = ev[1] & 0x7F
            vel = ev[2] & 0x7F
            if vel > 0:
                lib_zyncore.dev_send_note_on(self.idev_out, 0, note, PLAYING_COLOUR)
            else:
                lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0)
            return True
        # Note OFF
        elif evtype == 0x8:
            note = ev[1] & 0x7F
            lib_zyncore.dev_send_note_on(self.idev_out, 0, note, 0)
            return True

Both examples are included in Vangelis branch.

I will try to add more complex examples, showing how to communicate between the UI python side and the RT process (midiproc).

Enjoy the coding!

10 Likes

Super. Thanks @jofemodo got it working.

I switched my branch to Vangelis. Updated.
There were missing the boost libraries needed for mididings. I had to install them by hand with:

apt install libboost-all-dev libboost-python-dev (I think it should be in the install / Compile script.)

afterwards “pip install mididings” on command line went through without problem. Compiling mididings took some minutes on my Pi4.
After that your new drivers arised in the midi-in Menu.

I am starting to experiment with it.

1 Like

As noticed by @brumby, mididings is not installed by the update:

ERROR:zynthian_ctrldev_manager.update_available_drivers: Can't load ctrldev driver module 'zynthian_ctrldev_generic_chorder' => No module named 'mididings'

@jofemodo should fix this soon (because he always provides 0% bugs… :wink:).

[EDIT] Or I will. Update should now work.

3 Likes

Thanks for fixing my perfect code, mate :nerd_face:

3 Likes

I came to a problem.
When changing chains in Mixer away from chain 1, the keyboard input is dead. Output from mididings seams not to switch to new chain.

Here a part of my code to test in a keyboard driver. It should work as is everywhere:

################ start of mididings ############################

# for changing events with mididings
import mididings
from functools import partial # needed for function params in mididings process

def midiproc_task(self):
    self.midiproc_task_reset_signal_handlers()
    
          
    MODES = _MODES
    # scale_targets = MODES["Minor"]
    # scale_targets = MODES["Hungarian Minor"]
    scale_targets = [0, 2, 3, 6, 7, 8, 11] # is Hungarian Minor
    
    mididings.config(
        backend='jack-rt',
        client_name=self.midiproc_jackname,
        in_ports=1,
        out_ports=1,
    )
    
    
    # get parameters
    def translate_scale(ev, distance = None):
        note = ev.note
        octave = note // 12
        chroma_note = note % 12
        # Mapping: get white keys, remove black keys from piano notes
        key_map = (0, None, 1, None, 2, 3, None, 4, None, 5, None, 6)

        if chroma_note < 0 or chroma_note >= len(key_map): # is map right initialized
            return None  # for shorter modes with less then 7 tones
        
        chroma_note_cleaned = key_map[chroma_note]
        if chroma_note_cleaned == None: # is black key.
            return None # discard event

        if not 0 <= chroma_note_cleaned < len(scale_targets): # wrong scale_map values
            return None
        
        note_new = scale_targets[chroma_note_cleaned] + (octave * 12)

        ev.note = note_new # Herueka, a new Mode note event
        return ev
    
    mididings.run(
        [
            # #mididings.Pass() // (mididings.Channel(2) >> (mididings.Pass() // mididings.Transpose(4) // mididings.Transpose(7)))
            # mididings.Pass() //  mididings.Transpose(4) //  mididings.Transpose(7)
        

            # with params
            ## mididings.Filter(mididings.PROGRAM) // # all but note events
            #mididings.Channel(5) // # all to channel 5 which will not be routed
            
            mididings.Filter(mididings.CTRL) >> mididings.Channel(5),  # jst CTRLS to keyboard driver   
            
            mididings.Filter(mididings.NOTEON | mididings.NOTEOFF ) >> 
                mididings.Process( partial( translate_scale, distance = None) ) ,
                
            ~mididings.Filter(mididings.NOTEON | mididings.NOTEOFF) >> mididings.Pass()
                
        ]                  
    )

################### END of mididings #####################

I set:
unroute_from_chains = 0b0000_0000_0001_0000
to do my Zynpot-handling in event-chain, that reacts on chanel 5

This part of code switches off the black keys and lets white keys sound in “Hungarian Minor”

Full sources: zynthian-ui/zyngine/ctrldev/zynthian_ctrldev_keystation_pro_88_mk1.py at vangelis-mywork-20250924 · JBrumby/zynthian-ui · GitHub

Did I something wrong, or is there a bug?

I just tested your code and it works for me, in any chain. No problems at all.
Have you checked your midi device in the chain’s MIDI input menu? :wink:
Chains only receive MIDI input from selected MIDI devices.

BTW, remember that when using the mididings.Process object, it could be convenient to use:

backend='jack'

instead of:

backend='jack-rt'

This would add some latency, but you will be sure of not generating XRUNs. Anyway, i tested your code with hack-rt and it worked like a charm. Simply be warned, if you get XRUNs, change backed to “jack”.

Also, remember that mididings is just an option. The midiproc stuff can be used with any jack client, including a python jack client. For heavier and very complex processing, you could consider a C or C++ jack-client, or a LV2 plugin that you could launch with jalv. You have a lot of options now :wink:

Regards,

2 Likes

Thanks, for testing and your advices.

Till now I didn’ find a way to debug the mididings process.

Hi @brumby !

I just added a new example that you could find very interesting. It’s a chorder too, but it uses a python jack client instead of mididings and implements a very simple IPC to allow changing the chord type using the pads..

import logging
import multiprocessing as mp

from zyngine.ctrldev.zynthian_ctrldev_base import zynthian_ctrldev_base

# ------------------------------------------------------------------------------------------------------------------
# Basic Chorder with mididings for the Akai MPK mini MK3
# ------------------------------------------------------------------------------------------------------------------

class zynthian_ctrldev_akai_mpk_chorder(zynthian_ctrldev_base):

    dev_ids = ["MPK mini 3 IN 1"]
    driver_description = "Basic chorder example using mididings for RT midi processing. Use pads to change chord type."
    unroute_from_chains = 0b0000001000000000  # Unroute channel 10 (akai MPK mini's pads)
    autoload_flag = False

    # List of chords
    chords = [
        (4, 7),
        (3, 7),
        (4, 7, 11),
        (3, 7, 11),
        (3, 7, 10),
        (3, 7, 9),
        (5, 9),
        (2, 7)
    ]
    # IPC => multiprocessing.Value() object to share an integer variable (chord index) across processes
    chord = mp.Value('i', 0)

    def midiproc_task(self):
        self.midiproc_task_reset_signal_handlers()

        import jack
        import struct
        from threading import Event

        NOTEON = 0x9
        NOTEOFF = 0x8

        client = jack.Client(self.midiproc_jackname)
        inport = client.midi_inports.register('in_1')
        outport = client.midi_outports.register('out_1')
        event = Event()

        @client.set_process_callback
        def process(frames):
            chord = self.chords[self.chord.value]
            outport.clear_buffer()
            for offset, indata in inport.incoming_midi_events():
                outport.write_midi_event(offset, indata)  # pass through
                if len(indata) == 3:
                    status, pitch, vel = struct.unpack('3B', indata)
                    if status >> 4 in (NOTEON, NOTEOFF) and (status & 0xF) != 9:
                        for i in chord:
                            try:
                                outport.write_midi_event(offset, (status, pitch + i, vel))
                            except:
                                pass

        @client.set_shutdown_callback
        def shutdown(status, reason):
            logging.debug('JACK-CLIENT shutdown:', reason, status)
            event.set()

        with client:
            event.wait()

    def midi_event(self, ev):
        evtype = (ev[0] >> 4) & 0x0F
        evchan = ev[0] & 0x0F
        # Use the Akai MPK mini's pads (channel 10) for selecting the chord =>
        if evchan == 9 and evtype == 0x9:
            note = ev[1] & 0x7F
            if 36 <= note <= 43:
                self.chord.value = note - 36
                logging.debug(f"CHORD => {self.chords[note - 36]}")
                return True

It’s included in Vangelis so you can test with it easily.

Regards,

2 Likes

Remeber that Python is not recommended for realtime or low latency MIDI processing (as previously discussed here). Heed this warning from the jackclient-python documentation:

Warning

Most Python interpreters use a global interpreter lock (GIL), which violates the above real-time requirement. Furthermore, Python’s garbage collector might become active at an inconvenient time and block the process callback for some time.

Because of this, Python is not really suitable for real-time processing. If you want to implement a reliable real-time audio/MIDI application, you should use a different programming language, such as C or C++.

If you can live with some random audio drop-outs now and then, feel free to continue using Python!

3 Likes

Thank you @jofemodo @riban

I have to polish up my skills to interface jack and C. I’ll be back again in few days.

2 Likes

Hi @brumby!

Perhaps you find inspiring the new “mode enforcer” driver i’ve developed for the “Akai MPK mini MK3”. It allows to change across modes using the pads. Take a look:

Regards,

2 Likes

Hi @jofemodo

That looks pretty good. Thanks. When I am back from my trip, I will adapt it to my driver. I don’t have the push and the Zynthian here on my trip. :wink:

Regards

1 Like

In the code the ‘all chains’ were still assumed to be encoded by 0xF, both as a test if not boolean and to set the channels when True.

And I think I found the cause of the totally unrouted drivers: when adding new chains they were unrouted entirely for ctrldev devices that use the bitwise mask.

I created a PR with commits for both issues:

1 Like

Hi @niels!

I solved both issues. Thanks for the PR. I took your solution for 1500, but found a better way for 1499. Check the github comments.

Please, update and test.

Regards,

2 Likes

Thanks, and I do love code getting better by removing it :).