Skip to content

Instantly share code, notes, and snippets.

@digitalsignalperson
Last active March 31, 2023 08:24
Show Gist options
  • Save digitalsignalperson/3703ea6d60f719d415399857d5527a9f to your computer and use it in GitHub Desktop.
Save digitalsignalperson/3703ea6d60f719d415399857d5527a9f to your computer and use it in GitHub Desktop.
Mixxx midi output mappings generator for touchosc-dj (WIP)

Summary

What this does: controlling volume for deck 1 or deck 2 from any interface (midi controllers, GUI, or TouchOSC) results in midi output sent back to TouchOSC to update their positions

Note: I did this without reading https://github.com/mixxxdj/mixxx/wiki/midi%20scripting and I think the midi scripting interface probably resolves the issues I had and simplifies the approach.

Work log

A google of mixxx osc turned my afternoon into this...

https://www.reddit.com/r/DJs/comments/rwq59j/djing_on_a_budget_homemade_touchosc_controller/ https://github.com/grufkork/touchosc-dj

This TouchOSC controller is awesome, but I also wondered if it could work in reverse: use controls in the Mixxx GUI or other midi controllers, and have the updated mixer state be reflected in TouchOSC. For example, use the touchscreen for super fast fader cutting in/out, but use a midi controller for tactile faders most of the time.

The TouchOSC config is already good for this, just need to create output mappings...

The "Output Mappings" section for Mixxx controllers has the following columns:

  • Channel
  • Opcode (e.g. CC)
  • Control (default 0x00)
  • On Value (default 0x00)
  • Off Value (default 0x00)
  • Action (e.g. "[Channel2],volume" = Deck 2 volume fader)
  • On Range Min (default 0x00)
  • On Range Max (default 0x00)

The mixxx documentation doens't exactly explain these columns, and it took a lot of trial and error before it clicked to me what they did.

My original thinking was it was going to be trivial to assign a fader to a CC (fader value = 0% = CC=0, fader value = 100% = CC=127), and that would be one line in the Output Mapping table. It's more complicated.

Eventually figured out:

  • On Range (Min, Max) is the range of the "Value" where the CC will have "On Value"
  • The volume faders are non-linear, so while the "Parameter" goes from 0 to 1, the "Value" that is needed for midi mapping scales logarithimcally (this can be seen if running mixxx ---developer, opening Developer > Developer Tools menu, and finding Group [Channel 1], Item "volume", and observing the Value and Parameter columns.

For example

  • fader at 0%: Value = 0, Parameter = 0
  • fader at ~50%: Value = 0.231179, Parameter = 0.488636
  • fader at 100%: Value = 1, Parameter = 1

Found the math for the scaling in the source code here: src/engine/enginemaster.cpp pChannelInfo->m_pVolumeControl = new ControlAudioTaperPot(ConfigKey(group, "volume"), -20, 0, 1);

with some words in class ControlAudioTaperPot : public ControlPotmeter:

    // The AudioTaperPot has a log scale, starting at -Infinity
    // minDB is the Start value of the pure db scale it cranked to -Infinity by the linear part of the AudioTaperPot
    // maxDB is the Upper gain Value
    // neutralParameter is a knob position between 0 and 1 where the gain is 1 (0dB)

Testing

Doing a test with converting the C++ to python, comparing to obserbed params/values:

data = [(0, 0),
        (0.0764014, 0.22723),
        (0.222339, 0.477273),
        (0.744194, 0.88634),
        (1, 1)]

minDB = -20
maxDB = 0
neutralParameter = 1

for expected_value, parameter in data:
    value = parameter_to_value(parameter, minDB, maxDB, neutralParameter)
    print(f"Parameter: {parameter:.6f} --> Expected Value: {expected_value:.6f} / Computed Value: {value:.6f}")

# Parameter: 0.000000 --> Expected Value: 0.000000 / Computed Value: 0.000000
# Parameter: 0.227230 --> Expected Value: 0.076401 / Computed Value: 0.076383
# Parameter: 0.477273 --> Expected Value: 0.222339 / Computed Value: 0.222339
# Parameter: 0.886340 --> Expected Value: 0.744194 / Computed Value: 0.744148
# Parameter: 1.000000 --> Expected Value: 1.000000 / Computed Value: 1.000000

So that's perfect.

Centered ranges

Another thing is the "On Range Min" and "On Range Max" should be centered around the actual value, so in the script I did this with

range_min = (previous_value + center_value) * 0.5
range_max = (center_value + next_value) * 0.5

Other issues

Other issues I've encountered:

  • I'm using https://github.com/velolala/touchosc2midi to get the midi from my Microsoft Surface Pro 5 over to my linux box (notes on getting it working on Arch Linux)
  • related to previous point, I think Mixxx will not interpret output mappings under my "RtMidiIn Client:TouchOSC Bridge 129:0" Controller, since for some reason it doesn't output any midi to the related "TouchOSC Bridge" output... possibly because different name or it thinks they aren't the same device?
    • I tried messing with modprobe snd-virmidi for virtual midi devices, but those don't work as expected
    • I found out you can use "Midi Through" loopbacks in Mixxx only if you run mixxx --developer (issue: mixxxdj/mixxx#8356, PR: mixxxdj/mixxx#4148)
    • not sure if there is a solution in RtMidi and/or alsa/pipewire to avoid this
  • so I'm using a seperate "controller" for the outputs going to "Midi Through" and then routing that to TouchOSC Bridge, so have an independent mixxx xml file for this, but otherwise you could combine this xml with the touchosc-dj one

Next steps?

This solution only covers volume faders, and I think the Parameter vs Value problem exists for a lot of other controls we might want to map like this. E.g. tempo is complicated by rate ranges, cross-fader is non-linear, etc.

I have yet to look at the documentation for javascript controller stuff, so maybe this is already all solved there and this whole gist is obsolete. Ideally there would be an interface to directly map to "Parameter" and not have to work backwards from ParameterToValue functions

import math
def parameter_to_value(param, min_db=-20, max_db=0, neutral_param=1):
def db2ratio(db):
return math.pow(10, db / 20)
value = 1
if param <= 0:
value = 0
elif param < neutral_param:
if min_db != 0:
db = (param * min_db / (neutral_param * -1)) + min_db
offset = db2ratio(min_db)
value = (db2ratio(db) - offset) / (1 - offset)
else:
value = param / neutral_param
elif param == neutral_param:
value = 1.0
elif param < 1.0:
value = db2ratio((param - neutral_param) * max_db / (1 - neutral_param))
else:
value = db2ratio(max_db)
return value
name = 'TouchOSC_outputs'
xml = f"""<?xml version='1.0' encoding='utf-8'?>
<MixxxControllerPreset schemaVersion="1" mixxxVersion="">
<info>
<name>{name}</name>
<author>digitalsignalperson</author>
<description>Work in progress creating output mappings for https://github.com/grufkork/touchosc-dj</description>
</info>
<controller id="">
<scriptfiles>
</scriptfiles>
<controls/>
<outputs>
"""
# Do volume faders for deck 1 and 2
for channel, midino in [('1', '0x13'), ('2', '0x14')]:
for i in range(128):
if i == 0:
previous_value = 0
else:
previous_value = parameter_to_value((i - 1) / 127)
if i == 0:
center_value = 0
else:
center_value = parameter_to_value(i / 127)
if i == 127:
next_value = 1.0
else:
next_value = parameter_to_value((i + 1) / 127)
range_min = (previous_value + center_value) * 0.5
range_max = (center_value + next_value) * 0.5
on = str(hex(0 + i))
#off = str(hex(1 + i))
off = str(hex(128)) # invalid value, but we don't want it to set the "off" value when it goes out of the range
xml += f"""
<output>
<group>[Channel{channel}]</group>
<key>volume</key>
<status>0xB0</status>
<midino>{midino}</midino>
<on>{on}</on>
<off>{off}</off>
<maximum>{range_max}</maximum>
<minimum>{range_min}</minimum>
</output>\n"""[1:]
xml += """
</outputs>
</controller>
</MixxxControllerPreset>
"""[1:]
with open(f'{name}.xml', 'w') as f:
f.write(xml)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment