gljubojevic / akai-mpk-mini-editor

Akai MPK Mini editor using Web MIDI api
64 stars 5 forks source link

MPK mini plus - trying to configure transport controls #3

Open IanG opened 5 months ago

IanG commented 5 months ago

Hey Goran,

did you try and do anything like this with the Akai MPK mini plus ?

I'm currently trying to figure out how I can change the MIDI CC messages its transport buttons fire so I can get them controlling the transport controls in Universal Audio's LUNA DAW.

If I use MorningStar Midi Monitor I was able to determine the CC commands fired are:

Transport Button,MPK Device Port,CC#,Value,HEX
<<, 1, 115, 127, B0 73 7F
>>, 1, 116, 127, B0 74 7F
STOP, 1, 117, 127, B0 75 7F
PLAY, 1, 118, 127 B0 76 7F
REC, 1, 119, 127, B0 77 7F

So I at least know they reside within the undefined range 102-119 in the MIDI spec. I cant locate any of those sequences of bytes in the .mpkminiplus file the editor creates 😩 They could be split - or it could be that they are hard-wired into the device firmware which would be a shame.

By comparison this is the size difference between the files the 2 keyboards respective editors create:

.mpkmini3 2,873 bytes
.mpkminiplus - 44,497 bytes

(The mini plus has a sequencer built in so a lot of the extra space is configuration for that)

My ultimate goal is to write a little utility to modify the .mpkminiplus file so you can specify what DAW you are using and it will update the byte sequences in the file appropriately. Will make it very easy for people to use the transport in LUNA in the light that Universal Audio haven't built configuration for their transport controls (yet) and the Akai editor doesn't expose any functionality to change what the transport controls do. I hope they are not hard-wired in the firmware.

gljubojevic commented 5 months ago

Hi,

I don't have Akai MPK Mini Plus so I haven't tried anything similar. Looking at the numbers I would say that "Plus" version has a lot more data most likely it is step sequencer data. Just to avoid confusion I assume that you are talking about: https://www.akaipro.com/mpk-mini-plus.html

Looking at the product page I can see that it supports DAW transport control mapping. However after downloading original editor I can see that it only supports switching transport controls "on" or "on/off" MPK mini Plus Editor v1.0.7 (Mac - 10.2 MB) MPK mini Plus Editor v1.0.6 (PC - 50.6 MB)

Screenshot 2024-06-01 at 07 15 34

When I save presets with transport "on" and another with "on/off" there is single byte change in preset.

Screenshot 2024-06-01 at 07 17 37

Original MPK Mini uses simple SysEx command to send preset and it saves preset as simple bytes that you can directly send using any midi program that supports sending SysEx commands. Plus version has different format and probably different method of sending presets to controller.

It looks to me that transport control buttons have fixed values that you detected.

However there are 2 control scripts for available download: MPK Mini Plus Ableton Remote Script (Beta - 2.54 kB) MPK mini Plus FL Studio Beta Script (142.80 kB)

Looking at Ableton script:

[TransportControls]
# The settings below allow you to control various
# transport-related features.

# StopButton stops playback.
StopButton: 117

# PlayButton starts playback.
PlayButton: 118

# RecButton toggles Arrangement Record.
RecButton: 119

# SessionRecButton toggles Session Record.
SessionRecButton: -1

# OverButton toggles Arrangement Overdub.
OverButton: -1

# MetroButton toggles the Metronome.
MetroButton: -1

# LoopButton toggles the Arrangement Loop.
LoopButton: -1

# RwdButton rewinds playback.
RwdButton: 115

# FwdButton fast-forwards playback.
FfwdButton: 116

# PunchInButton controls Punch-In.
PunchInButton: -1

# PunchOutButton controls Punch-Out.
PunchOutButton: -1

# NudgeUpButton triggers Phase Nudge Up.
NudgeUpButton: -1

# NudgeDownButton triggers Phase Nudge Down.
NudgeDownButton: -1

# TapTempoButton triggers Tap Tempo.
TapTempoButton: -1

It is similar for FL Studio (uses Python for scripting DAW)...

ID_PREV = 115
ID_NEXT = 116
ID_STOP = 117
ID_PLAY = 118
ID_REC = 119

And Python code that handles transport ...

    def OnControlChange(self, event):
        # transport
        if event.data1 == ID_PREV:
            event.handled = True
            self.ToggleLED(ID_PREV, event.data2 > 0)
            if event.data2 > 0:
                # TODO: move Toggle Knob Mode to different control
                #self.CurrentKnobMode = max(0, self.CurrentKnobMode - 1)
                #self.UpdateKnobBank(self.CurrentKnobMode)
                print("Current knob mode: " + str(self.CurrentKnobMode))
                # RWD one bar
                curr = transport.getSongPos(midi.SONGLENGTH_ABSTICKS)
                transport.setSongPos(curr - self.ticksPerBar, midi.SONGLENGTH_ABSTICKS)

        elif event.data1 == ID_NEXT:
            event.handled = True
            self.ToggleLED(ID_NEXT, event.data2 > 0)
            if event.data2 > 0:
                # TODO: move Toggle Knob Mode to different control
                #self.CurrentKnobMode = min(9, self.CurrentKnobMode + 1)
                #self.UpdateKnobBank(self.CurrentKnobMode)
                print("Current knob mode: " + str(self.CurrentKnobMode))
                # FWD one bar
                curr = transport.getSongPos(midi.SONGLENGTH_ABSTICKS)
                transport.setSongPos(curr + self.ticksPerBar, midi.SONGLENGTH_ABSTICKS)

        elif event.data1 == ID_STOP and event.data2 == 127:
            event.handled = True
            transport.stop()
            ui.setHintMsg("Stop")

        elif event.data1 == ID_PLAY and event.data2 == 127:
            event.handled = True
            transport.start()
            ui.setHintMsg("Play/Pause")

        elif event.data1 == ID_REC and event.data2 == 127:
            event.handled = True
            transport.record()
            ui.setHintMsg("Record")

        # knobs 
        elif (event.data1 >= ID_KNOB_BASE) & (event.data1 <= ID_KNOB_BASE + KNOB_COUNT):
            self.OnEndlessKnob(event)

        # mod wheel
        elif event.data1 == ID_MOD:
            event.handled = True
            print("Mod Wheel: " + str(event.data2))

Looking further at FL Studio python scripts looks like it possible to modify preset and state of controller using SySex commands however I don't see that is possible to change transport controls assignment, I might be wrong on that assumption but I couldn't find anything to see that is possible.

class SysExHandler():
    def requestIntroduction(self):
        arr = [0] * 12
        arr[0] = SYSEX_START
        arr[1] = SYSEX_MANUFACTURER_ID
        arr[2] = SYSEX_DEVICE_ID
        arr[3] = SYSEX_MODEL_ID
        arr[4] = 0x60 # Message Type
        arr[5] = 0x00
        arr[6] = 0x04
        arr[7] = 0x00

        arr[8] = FL_STUDIO_VER_MAJOR
        arr[9] = FL_STUDIO_VER_MINOR
        arr[10] = FL_STUDIO_VER_BUGFIX
        arr[11] = SYSEX_END

        device.midiOutSysex(bytes(arr))
        print("Sending SysEx introduction...")

    def updateKnobValue(self):
        arr = [0] * 11

        arr[0] = SYSEX_START
        arr[1] = SYSEX_MANUFACTURER_ID
        arr[2] = SYSEX_DEVICE_ID
        arr[3] = SYSEX_MODEL_ID
        arr[4] = 0x12 # Message Type
        arr[5] = 0x04 # dataLength

        arr[6] = 0x01 # Knob ID 1-8
        arr[7] = 0x31 # Value? Or Name?
        arr[10] = SYSEX_END

        device.midiOutSysex(bytes(arr))
        print("Sending SysEx knob value update...")

    def updatePadColour(self):
        numberOfPads = 1
        byteLength = 8 + numberOfPads * 13
        arr = [0] * byteLength

        arr[0] = SYSEX_START # Sysex Start
        arr[1] = SYSEX_MANUFACTURER_ID # Manufacturer ID
        arr[2] = 0x7F # SysEx Device ID
        arr[3] = 0x54 # Product Model ID
        arr[4] = 0x70 # MessageType

        # Calculate the length of the remaining message in bytes
        # The sysex terminator is *not* included in this number
        dataLength = 13 * numberOfPads
        dlMSB = (dataLength &0xFF00) >> 8
        dlLSB = dataLength & 0x00FF

        arr[5] = dlMSB
        arr[6] = dlLSB

        for i in range(0, numberOfPads):
            base = 7 + i * 13

            arr[base] = 0x00 # padName

            arr[base + 1] = 0x00 # Red Off MSB
            arr[base + 2] = 0x00 # Red Off LSB

            arr[base + 3] = 0x00 # Green Off MSB
            arr[base + 4] = 0x00 # Green Off LSB

            arr[base + 5] = 0x00 # Blue Off MSB
            arr[base + 6] = 0x00 # Blue Off LSB

            arr[base + 7] = 0x00 # Red On MSB
            arr[base + 8] = 0x00 # Red On LSB

            arr[base + 9] = 0x00 # Green On MSB
            arr[base + 10] = 0x00 # Green On LSB

            arr[base + 11] = 0x01 # Blue On MSB
            arr[base + 12] = 0x7f # Blue On LSB

        arr[byteLength - 1] = 0xF7 # Sysex terminator

        print(bytes(arr))
        for x in arr:
            print(hex(x))

        device.midiOutSysex(bytes(arr))
        print("Sending SysEx colour change...")

    def requestProgramData(self, programId):
        '''Retrieve the data of a program/preset, numbered 1-8.'''
        arr = [0] * 9
        arr[0] = SYSEX_START
        arr[1] = SYSEX_MANUFACTURER_ID
        arr[2] = SYSEX_DEVICE_ID
        arr[3] = SYSEX_MODEL_ID
        arr[4] = 0x66
        arr[5] = 0x00
        arr[6] = 0x01
        arr[7] = programId
        arr[8] = SYSEX_END

        device.midiOutSysex(bytes(arr))
        print("Requested SysEx program data...")

    def updateProgramData(self):
        '''This updates the device's FL Studio program to use relative encodings for the endless knobs (K1-8).'''
        print("Updating program data...")
        device.midiOutSysex(bytes(program_FLStudio20Rel.programData))

    def loadProgram(self, number):
        print("Loading program " + str(number) + "...")
        arr = [0] * 10

        arr[0] = SYSEX_START
        arr[1] = SYSEX_MANUFACTURER_ID
        arr[2] = SYSEX_DEVICE_ID
        arr[3] = SYSEX_MODEL_ID
        arr[4] = 0x62 # message type
        arr[5] = 0x00
        arr[6] = 0x01
        arr[7] = number
        arr[8] = SYSEX_END

        device.midiOutSysex(bytes(arr))

In conclusion looks like it is not possible to modify Akai MPK Mini Plus preset to send different byte sequence for transport control and you need to write specific script for each DAW you want to support since there is only FL Studio and Ableton supported by Akai.

In case of LUNA as you said:

Will make it very easy for people to use the transport in LUNA in the light that Universal Audio haven't built configuration for their transport controls (yet) and the Akai editor doesn't expose any functionality to change what the transport controls do.

Looks like you are out of luck to use preset modification for transport control in LUNA or to cover multiple DAWs with simple byte sequence change.

I'm not familiar with LUNA DAW but there might be some way to map controller and/or use "learn" option for transport controls and map them to control LUNA transport.

IanG commented 5 months ago

@gljubojevic thanks for you insight sir.

I had previously looked at the scripts for Ableton and FL Studio - yes they re-program the DAW to use the exact CC messages I caputred from the midi monitor that the transport controls emit. I know Logic has functionality to 'learn' transport controls so in those cases its software configuration changes to match the hardware and not the other way round.

LUNA doesn't currently have support for 'learning' commands from devices so I was just trying to see if I could side-step that problem from the hardware side. I did find an interesting file buried in the LUNA .app bundle. It looks like the file:

/Applications/LUNA.app/Contents/Resources/panel/luna_surface/hardware/luna_surface_items.json

Is how they control MIDI input/output commands both out of the DAW controls and into UI elements. As its .json its a little easier to follow whats happening. So you will see things like:

{
    "name": "FRwd Switch",
    "type": "switch_midi",
    "address": "/recv_cmd/144/91",
    "path": "/control/transport/frwd_switch"
},
{
    "name": "FFwd Switch",
    "type": "switch_midi",
    "address": "/recv_cmd/144/92",
    "path": "/control/transport/ffwd_switch"
},
{
    "name": "Stop Switch",
    "type": "switch_midi",
    "address": "/recv_cmd/144/93",
    "path": "/control/transport/stop_switch"
},
{
    "name": "Play Switch",
    "type": "switch_midi",
    "address": "/recv_cmd/144/94",
    "path": "/control/transport/play_switch"
},
{
    "name": "Record Switch",
    "type": "switch_midi",
    "address": "/recv_cmd/144/95",
    "path": "/control/transport/record_switch"
},
{
    "name": "FRwd Led",
    "type": "led_midi",
    "address": "/send_cmd/144/91",
    "path": "/control/transport/frwd_led"
},
{
    "name": "FFwd Led",
    "type": "led_midi",
    "address": "/send_cmd/144/92",
    "path": "/control/transport/ffwd_led"
},
{
    "name": "Stop Led",
    "type": "led_midi",
    "address": "/send_cmd/144/93",
    "path": "/control/transport/stop_led"
},
{
    "name": "Play Led",
    "type": "led_midi",
    "address": "/send_cmd/144/94",
    "path": "/control/transport/play_led"
},
{
    "name": "Record Led",
    "type": "led_midi",
    "address": "/send_cmd/144/95",
    "path": "/control/transport/record_led"
},

So recv_cmd and send_cmd maybe suggest respectivity when a CC command arrives, send a CC command when some internal event happens but its not obvious to me what the 2 int values that follow those mean. Possible type values are:

"type": "switch_midi",
"type": "led_midi",
"type": "fader_position_midi",
"type": "fader_setpoint_midi",
"type": "knob_position_midi",
"type": "jog_wheel_position_midi",
"type": "knob_halo_midi",
"type": "strip_lcd_midi",
"type": "timecode_display_midi",
"type": "strip_meter_midi",

Possible address values are always in the format <action>/<value>/<value> and actions can be:

recv_cmd
send_cmd
send_sysex

which maybe gives more indication this maybe controls keyboard/device interactions with their UI. Maybe if I can understand how the <value>/<value> bit maps onto the midi CC commands I can change their map to align it with the Akai device.

I will make a backup of that file and experiment with changing these values - could just break the software but you never know I guess.

So the trick to this is discovering how CC values emitted from the Akai map onto values in the map in LUNA

Akai CC LUNA
115 (RW) /recv_cmd/144/91
116 (FF) /recv_cmd/144/92
117 (STOP) /recv_cmd/144/93
118 (PLAY) /recv_cmd/144/94
119 (REC) /recv_cmd/144/95

Will let you know if I figure anything out and thanks again for your input 👍🏻

gljubojevic commented 5 months ago

Remember your first capture:

Transport Button,MPK Device Port,CC#,Value,HEX
<<, 1, 115, 127, B0 73 7F
>>, 1, 116, 127, B0 74 7F
STOP, 1, 117, 127, B0 75 7F
PLAY, 1, 118, 127 B0 76 7F
REC, 1, 119, 127, B0 77 7F

If I'm reading this correctly AKAI sent on channel 1

0xB0 - Chan 1 Control/Mode Change
0x73 - RW command
0x7F - Don't care value in ext command or end of command marker

See here: https://midi.org/expanded-midi-1-0-messages-list 0xB0 == 176 is designated as "Chan 1 Control/Mode Change" Looks like LUNA uses 0x90 == 144 is designated as "Chan 1 note on"

So I would try to map it to luna like this since 0xB0 == 176:

Akai CC LUNA Default LUNA AKAI Map
115 (RW) /recv_cmd/144/91 /recv_cmd/176/115
116 (FF) /recv_cmd/144/92 /recv_cmd/176/116
117 (STOP) /recv_cmd/144/93 /recv_cmd/176/117
118 (PLAY) /recv_cmd/144/94 /recv_cmd/176/118
119 (REC) /recv_cmd/144/95 /recv_cmd/176/119

Should be easy to try... you got nothing to lose.

gljubojevic commented 5 months ago

You might even consider that LUNA is capable receiving more than 2 bytes for transport so mapping would look like this:

Akai CC LUNA Default LUNA AKAI Map
115 (RW) /recv_cmd/144/91 /recv_cmd/176/115/127
116 (FF) /recv_cmd/144/92 /recv_cmd/176/116/127
117 (STOP) /recv_cmd/144/93 /recv_cmd/176/117/127
118 (PLAY) /recv_cmd/144/94 /recv_cmd/176/118/127
119 (REC) /recv_cmd/144/95 /recv_cmd/176/119/127
IanG commented 5 months ago

@gljubojevic - thanks again for taking the time to respond. I know this largely has nothing to do with your own project but its good to bounce ideas. I'll try and switch out some of those values and see what happens. Your second point is interesting to see if / is just a delimiter for each byte of the MIDI command.

I did a quick grep "\"address\": \"/recv_cmd" luna_surface_items.json | uniq just to see if there are ever more than 2 additional values supplied after a recv_cmd and there doesn't appear to be more than 2 int represented values. There are a few which only supply one value though.

Will let you know what I find and thanks again sir 👍🏻

IanG commented 5 months ago

Well - some good news...

I modified the line of the file from:

❯ diff luna_surface_items.json luna_surface_items.json.backup
450c450
<             "address": "/recv_cmd/176/118",
---
>             "address": "/recv_cmd/144/94",

And now if iI go into the Luna Controllers settings and tell it to use MPK mini Plus Port 1 the play button now controls the transport play function in LUNA 💪🏻. Its latching so a 2nd press stops the playback.

Only problem now is I have no more note control for midi note programming in instruments. I think I just need to configure either the Akai or Luna to listen to notes on its 2nd channel now.

Lets see.