Topic: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

I had help from this forum in making this design, so I thought I would share it in case it is useful for other people.

From looking at https://www.forum.rme-audio.de/viewtopic.php?id=24437, I had built a small control using MIDI-OX to translate the commands from my launchkey into the correct midi parameters for TotalMixFX, and that worked well enough, but I wanted the better control available from either Mackie protocol or OSC.

I had tried the TotalMix app for ipads, but didn't really want to use a touchscreen when I had all these buttons and dials available. Then I saw that touchdesigner has a free non-commercial license, and has some strong MIDI + OSC nodes plus python programmability.

There's clearly more that can be done, but mainly what I wanted was fader control of the software-playback layer, plus the ability to mute channels and do speakerB/dim settings, so that's what I've built for myself so far.

The architecture of it is:
1. A midiin1 listening to the main port of the launchkey. In the "midiin1_callbacks", I have a python script that sends OSC messages to Totalmix.
2. An oscout1 node I use to send the messages to Totalmix.
3. An oscin node I use to receive messages from Totalmix. I have a callback python script here to send MIDI messages to the secondary Launchkey MIDI port, which is how it make colors for the buttons to let me know what mutes are active, or which section I'm controlling (in/playback/out).

I'm not sure if I can attach the touchdesigner file to this post, so I'll put the code in followup posts.

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

Here's the code I'm using for the midi callbacks. The basic problem to address is that speakerB and Dim are both toggles; you just send 1.0, and they switch their value. But the mutes are on/off, so you have to store the information about the current mute status in a variable somewhere. I keep it in the oscin node, because they could have been muted by the GUI or the hardware controls.

The lookup tables are to map from note numbers and controller numbers to the OSC messages I want them to send.

# me - this DAT
#
# dat - the DAT that received the event
# rowIndex - the row number that was added
# message - a readable description of the event
# channel - the numeric value of the event channel
# index - the numeric value of the event index
# value - the numeric value of the event value
# input - true when the event was received
# bytes - a byte array of the received event
#
# Example:
# message  channel index value     bytes
# Note On  1        63   127       90 2f 127

mutefetch = op('/project1/oscin2').fetch
sendchan = op('oscout1').sendOSC

def normalize(inv, max):
    return (inv / max)

# Control -> toggle mappings.   
cclut = {
    105: '/1/mainDim',
    106: '/1/mainSpeakerB',
}

def handleCC(index, value):
    v = [normalize(value, 127)]
    toggle = cclut.get(index, False)
    # Toggle buttons; send on push.
    if toggle and value == 127:
        sendchan(toggle, [1.0])
    # main fader
    if index == 8:
        sendchan('/1/mastervolume', v)
    # 8 pots
    if index >= 22 and index <= 29:
        addr = "/1/volume{}".format(index-21)
        sendchan(addr, v)
    return

# kOSCScaleOnOff; requires looking up the current state
# to make the button reverse it.
# For reads, we strip the leading /; for sends we keep it.
tlut = {
    41: '/1/mute/1/1',
    42: '/1/mute/1/2',
    43: '/1/mute/1/3',
    44: '/1/mute/1/4',
    49: '/1/mute/1/5',
    50: '/1/mute/1/6',
    51: '/1/mute/1/7',
    52: '/1/mute/1/8',
}

# kOSCScaleToggle; sending "1.0" causes it to happen.
plut = {
    46: '/1/busInput',
    47: '/1/busPlayback',
    48: '/1/busOutput',
}

def handleNote(index):
    # The toggles; send blind.
    if plut.get(index, False):
        sendchan(plut[index], [1.0])
    # The stateful ones; read, then send the opposite.
    if tlut.get(index, False):
        # Get the mute represented by this pad
        chan = tlut[index]
        # Grab the status of it from the OSC DAT
        # Specify a default, because if the channel is disabled or doesn't
        # exist, we don't want an error here.
        curr = float(mutefetch('m' + chan[-1], 0))
        # Got a 1? send 0. Got a 0? send 1.
        sendchan(chan, [1.0 - curr])


       
def onReceiveMIDI(dat, rowIndex, message, channel, index, value, input, bytes):
    if (message == "Control Change"):
        handleCC(index, value)
    if (message == "Note Off" and channel == 10):
        handleNote(index)
    return

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

And here's the OSC side of things. This receives OSC messages from TotalMixFX and both updates the mute statuses and sends control signals to the Launchkey to make the buttons light up colors to let me know the current status of which section is selected, if speakerB or Dim are enabled, and which channels are muted.

I have the option selected in the OSCIn to unbundle messages for me automatically, so while totalmix sends a big bundle when you change bus, for instance, this script only has to read them one at a time.

# me - this DAT
#
# dat - the DAT that received a message
# rowIndex - the row number the message was placed into
# message - an ascii representation of the data
#           Unprintable characters and unicode characters will
#           not be preserved. Use the 'bytes' parameter to get
#           the raw bytes that were sent.
# bytes - a byte array of the message.
# timeStamp - the arrival time component the OSC message
# address - the address component of the OSC message
# args - a list of values contained within the OSC message
# peer - a Peer object describing the originating message
#   peer.close()    #close the connection
#   peer.owner  #the operator to whom the peer belongs
#   peer.address    #network address associated with the peer
#   peer.port       #network port associated with the peer
#

# Mute lookup table
mlut = {
    '/1/mute/1/1': 40,
    '/1/mute/1/2': 41,
    '/1/mute/1/3': 42,
    '/1/mute/1/4': 43,
    '/1/mute/1/5': 48,
    '/1/mute/1/6': 49,
    '/1/mute/1/7': 50,
    '/1/mute/1/8': 51,
}
# Controller lookup
clut = {
    '/1/mainDim': 104,
    '/1/mainSpeakerB': 105,
}
# Bank lookup
blut = {
    '/1/busInput': 45,
    '/1/busPlayback': 46,
    '/1/busOutput': 47,
}

midiout = op('midiout2')
sendNoteOn = midiout.sendNoteOn
sendNoteOff = midiout.sendNoteOff
sendControl = midiout.sendControl

def onReceiveOSC(dat, rowIndex, message, bytes, timeStamp, address, args, peer):
    if message == '/ 0':
        return
    # like '/1/mute/1/1 1' or '/1/mainSpeakerB 0'
    l = message.split(' ')
    addr = l[0]
    status = l[1][0]
    mute = mlut.get(addr, False)
    if mute:
        if status == '1':
            sendNoteOn(3, mute, 120)
        else:
            sendNoteOff(3, mute)
        # Store mute status into m1-m8 for lookup from midi callback script
        dat.store('m' + l[0][-1],  status)
    cc = clut.get(addr, False)
    if cc:
        if status == '1':
            sendControl(16, cc, 120)
        else:
            sendControl(16, cc, 0)
    bank = blut.get(addr, False)
    if bank:
        if status == '1':
            sendNoteOn(16, bank, 120)
        else:
            sendNoteOff(16, bank)
    return

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

And that's basically it. I added an executeDAT to run some commands at startup (to select the playback bus) and exit (to turn all the lights off - the keyboard lights stay on when I turn my PC off, so it's annoying otherwise).

The other things to do if you're new to TD like I was:
1) You have to add the midi devices via the midi device mapper
2) You have to toggle whether it should be responsive while minimized; otherwise these controls all work while the window is up, but not when you minimize it.

I hope this is useful to someone else! My next steps are probably to add in some snapshot navigation via other buttons. The other nice thing about this setup is that you can map multiple OSC messages into a single button press for some automation of things.

My main sadness with this is that the keyboard doesn't have motorized faders, so when you start moving a dial the mix will instantly snap to where your dial is, instead of waiting for you to crossover the current setting. That can probably be fixed with some more variable storage in the OSC dat, but I'm not there yet. :-)

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

Oh, I forgot to mention the two other big resources for the project:
This has the overview of OSC commands:
https://docs.google.com/spreadsheets/d/ … d=71521545

However, the oscin viewer in touchdesigner is also great for seeing what totalmix sends when you make changes in the GUI.

Similarly, this programmer's overview for the Launchkey mkii was great:
https://d2xhy469pqj8rc.cloudfront.net/s … -guide.pdf

but the MIDI In viewer also lets you see what's going on when you hit buttons or turn dials. It was most useful for finding out how to set the lights on buttons as desired.

The last thing which might be confusing - for some reason, the MIDI In takes things in 1-based offsets, while the MIDI Out does it 0-based. That's why you might notice that the MIDI In script refers to key 46 as representing the input bus, while the OSC callback operates on key 45 when lighting up for the input bus. Probably there's a way to change that in TD, but it was easier for me to just work with it as it was.

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

nice work... i don't understand code too well but the functionality looks good.. do you know if Touch Designer would be able to connect Totalmix with Reaper via OSC ? i have been looking for a way to get Reapers faders to control Totalmix faders (and vice versa) and mutes also..  Totalmix OSC responds to 1/Volume1 and Reaper OSC responds to n/track/1/volume, so from what i can tell i just need some software that can patch those OSC patterns..

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

For sure. I really only started learning the software for this project, but I think what you would want is an OSC In chop (https://docs.derivative.ca/OSC_In_CHOP)  which would receive information from Reaper, going into a Rename chop (https://docs.derivative.ca/Rename_CHOP) to rename the reaper in messages to what you want Totalmix to get, the go into the OSC Out chop to send the messages to Totalmix. No python coding required for at that level of things; the CHOPs are designed to just connect to each other.

The one funny thing I found is that the rename chop accepts only two parameters, the "in" to rename, and the "out" after rename. If you want to rename multiple things, you just space-separate them. So:
in: a b c
out: q r s

would rename a->q, b->r, and c->s. That wasn't obvious to me at first, I thought renaming was going to take a ton of rename nodes, but not so.

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

awesome.. looks like cool software, i'm downloading it now.. hopefully I can work it out..

9 (edited by cabacon 2021-04-28 04:32:36)

Re: Building a MIDI->OSC bridge with Touchdesigner for my Launchkey mkii

I finally came back to this, and I figured out how I wanted to handle things. Here's how my RME outputs are setup:

busOutput:
AN 1/2: To my speakers
PH 3/4: To my heaphones; Linked to AN 1/2 so volume stays the same across the two.
ADAT 7/8: Loopback enabled; serves as the "microphone" for discord.

busPlayback:
AN 1/2: Windows system sounds
PH 3/4: no mappings
AS 1/2: Sound from games
ADAT 3/4: Sound from chrome
ADAT 5/6: Sound from discord
ADAT 7/8: Sound from Ableton Live (used as a soundboard for discord)

busInput:
AN1: My microphone

The mix for me includes sound from all the playback channels so I can hear everything, but nothing from the mic.
The mix for discord includes the mic (input: AN1), chrome (playback: ADAT 3/4) so I can play music, and ableton (playback: ADAT 7/8) so I can play sound clips.

What I've really been wanting is to:
a) Hit a button on the launchkey to say whether I'm trying to modify the playback volume for me (output: AN 1/2) or for discord (output: ADAT 7/8)
b) Then, the 8 faders should control the playback volumes for that output submix. (If I'm controlling the discord output, I should control the mic volume (input: AN1) instead of the windows volume (playback: AN 1/2). The buttons under the faders should control mutes.

What I had above, though, wasn't doing any submix selection. People seem to ask about that a bunch on here, so here's what I found. What I had been doing with my bank lookup table wasn't doing the submix selection I wanted. Instead, what I should have been doing was:

midi 47 -> I want to control the mix for my listening, so I should:
sendOSC('/1/busOutput', [1.0]) # select the output bus
sendOSC('/setSubmix', [0.0]) # select the main output submix (left is 0.0, right is 1.0, but it's stereo so either is fine)
sendOSC('/1/busPlayback', [1.0]) # now that the main output submix is selected, start controlling the playback volumes for that submix.

midi 46 -> I want to control the mix for discord, so:
sendOSC('/1/busOutput', [1.0]) # select the output bus
sendOSC('/setSubmix', [10.0]) # select ADAT 7/8 output submix (left is 10.0, right is 11.0, but it's stereo so either is fine)
sendOSC('/1/busPlayback', [1.0]) # now that discord output submix is selected, start controlling the playback volumes


And while midi 46 (use discord) is most recently selected, using the first fader (my mic volume) should:
v = get the volume
sendOSC('/1/busInput', [1.0]) # sneak up to the input bus
sendOSC('/1/volume1', [v]) # set the mic (AN1) volume to the desired level
sendOSC('/1/busPlayback', [1.0]) # go back to the playback bus so the other faders work as expected.

Also, while in discord mode the first mute button should do the same input/mute/playback dance so I'm muting things at the right location.

I'll get this coded up now that I've figured it out from the python console, then I'll try to find a better way to share this project than just pasting things into forum text boxes.