Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSC in Mixxx (20241001) #13714

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

Eve00000
Copy link
Contributor

@Eve00000 Eve00000 commented Oct 1, 2024

Now with rebase from 2.6
OSC working in Mixxx (also Epic, my first real PR )

Preferences: Enabled / Receivers / Ports
Sending Track Info / Title / Duration ...

Receiving in thread and executing all CO's + Stems + FX + .... (everything)
CMakeLists.txt is also adapted.
Bi-directional  // sync knob / timers elapsed & remain
Switch decks 1-3 & 2-4

Q1: now 5 clients can be configured in settings, should I make it 10?

Open for feedback
Test with MixxxInTouchOSCv43.tosc.txt -> rename: drop the .txt

MixxxInTouchOSCv43.tosc.txt

OSC in Mixxx
Why?
There are different user cases where OSC-functionality in Mixxx is an added value:

  • If the user's hardware controller doesn't have enough knobs / encoders / potmeters to control all control objects of Mixxx, fi Stem controls on a tablet
  • If the user want to have a remote controller
  • If the user's hardware controller doesn't haven sceens that can give information or needed functionality a tablet can be added to display info of a deck or for all decks
  • If a user wants to receive information from Mixxx in a light controller
  • If a user want"s to extract information from Mixxx to display on a screen for the audience or on a website
  • IF a user want a combination of previous listed items
    The example enclosed has the functionality of a combination of an extra controller, an info screen and a remote controller.

Environment
Different to hardware controllers that are in general connected to the Mixxx-machine through USB (Midi / HID) OSC works over the network (LAN). Networkconnections can be wired or wireless. Keep in mind that the user needs to provide a LAN connection between the Mixxx-machine and the OSC-Receivers. There are many reasons why a DJ doesn't want to connect it's Mixxx-machine to a public network. Preferable the DJ sets up a private LAN: a hardware or software wireless accesspoint, to connect tablets or smartphones, a small switch to connect machines with a LAN-connector or in case only one other machine needs to be connected to Mixxx a crossed-cable can be used. (all tested).

Mixxx Ecosystem
The integration of OSC in Mixxx needed to respect Mixxx's Ecosystem, OSC had to work by analogy to the hardware controllers Midi/Hid integration. Some remarks:

  • mapping a basic OSC controller must be easy accessible for everyone, advanced scripting can require some basic programming skills.
  • a control object can be controlled by a 'simple' mapping like it can be mapped as 'normal' in the XML for a Midi controller
  • a control object can be controlled through a script like control objects can be mapped as 'script' and through a JS script in the midi.js file for a Midi controller.
  • a logic and simple translation is needed between the Mixxx control object names and the OSC controller
  • to write script functions the OSC controller must be able to 'pull' information from Mixxx when needed like 'getValue / getParameter in Midi-scripting.
  • for particular functions in the OSC-controller the information needs to be accurate, so a solution similar to engine.makeConnection needed to be found.
  • some controllers can only be configured as 'receivers' (fi when they don't have scripting possibilities), Mixxx needs to be able to 'push' information to update these receivers.
  • the information must be accurate on all connected OSC-receivers, even if only a part of the information is used.

Remark: as TouchOSC is seen as the 'standard' OSC-editor, all following OSC functions, scripts ... are created and tested in TouchOSC.

The OSC - object.
OSC is a protocol to send/receive small commands and informationspackages over a network.
That package is constructed of severl parts:

  • The address and the port of the machine to which the package is addressed
  • a path
  • a value and it's type

The address and port of the machine to which the package is addressed is configured in the connections.
The path starts always with a slash, followed with a 1st address-part, a slash, a 2nd adress-part .... a slash, a x-th address-part, a slash and the name of the object.
(in Mixxx we don't use all the address-parts, just a slash and the name of the object: /name)
A value can be a String, Int, Boolean or Float.
(in Mixxx we'll use only the float)

There a different types of OSC-Controls
General:
A control has a type, shape, value, orientation, color... and can contain a script form.
A control can send information when touched/pressed/released or with a script.
with Momentary (on the moment of the action) if 'press' and 'release' are selected touching the control will send information and releasing the button will send information as well.
With 'Toggle Press' you only send information on the moment that you press the button (with Toggle Realease on the moment that you release the button)
A control has also a value, this can be a text (in case of a label) or a key. The key can be x or touch.
Touch is a Boolean: you touch the control or you don't. fi in case you touch the button you want to change the color of the button.
The value of x can be defined in the scale of the OSC-message or can be given in a script.

Main Types used to control Mixxx:

  • a Label: can be the descriving text overlay on a button or a 'standalone field' used to give some information, or even an 'receiving control' (to receive text =string information)

  • a Button: serves to send an instruction (information) when touched or to activate a script.
    *when the information is send can be defined: Momentary (on the moment of the action): if 'press' and 'release' are selected touching the button will send information and releasing the button will send information as well.
    If you're defining a 'play-button' you don't want this because in the same action (press-release) you start (send 1) and pause (send 0) the track.
    With 'Toggle Press' you only send information on the moment that you press the button (with Toggle Realease on the moment that you release the button)

  • a Radial: can be used to control an encoder fi high / mid / low eq, gain. The information send is the value on the scale where the control is last touched (can be through a script), a Radial has a minimum and maximum value.

  • a Fader: can be used to control a fader (rate / volume / crossfader), The information send is the value on the scale where the control is last touched (can be through a script), a Fader has a minimum and maximum value.

  • an Encoder: can be used to control a jogwheel, to browse, in the library.. The information send is calulated relative to the previous position of the control.

A control has different properties:
name, tag, size, color, shape.
These properties can be changed through scripting.
Fi change the color of a playbutton when it's pressed to start the plaing.

function update()
  if key == 'x' and self.values.x == 0 then
    self.color = Color.fromHexString("0000FFAA")
  elseif key == 'x' and self.values.x == 1 then
    self.color = Color.fromHexString("0000FFFF")
  end
end

Fi the play-button '(Channel2)@play' can be changed to '(Channel4)@play' when a 'deckswitchbutton' to switch to deck 4 is activated. When pressing the 'deckswitchbutton' again to return to deck 2 the name of the control can be changed back to
'(Channel2)@play'.

function onValueChanged(key)
  if key == "x" and self.values.x == 1 then
    self.parent.children['(Channel2)@play'].properties.name = '(Channel4)@play'
  end
  if key == "x" and self.values.x == 0 then
    self.parent.children['(Channel4)@play'].properties.name = '(Channel2)@play'
  end
end

Remark:
In the duration that the button is renamed to '(Channel4)@play' the controller can't receive messages for '(Channel2)@play',
if the deck 2 player is started meanwhile the OSC controller does not know. You'lll need to cover this issue with scripting after changing the deck fi;

if self.values.x == 0 then
  sendOSC('/GetP#(Channel2)@play', 1)
end
if  self.values.x == 1 then
  sendOSC('/GetP#(Channel4)@play', 1)
end

A OSC-Controller can be devided in parts to create more space or to devide controls in groups.
This can be done with a 'pager'. A pager contains different 'pages'.
Properties of controls on another 'page' can be called using the 'parent' and 'children' instructions.
fi: if you want to use the value of a 'Shift-button' that is located on another page, you can start from the 'root' of the document:

if root.children.Stems.children.Controller.children['Shift'].values.x == 0
if root.children['Stems'].children['Controller'].children['Shift'].values.x == 0

Remark: both lines are valid and call the value of the same 'Shift-Button'
To change the name of a control that is located in a 'sub-pager' on the same main 'page':

self.parent.children['Controller_Ch2'].children.P5.children['(EqualizerRack1_(Channel2)_Effect1)@button_parameter2'].properties.name = '(EqualizerRack1_(Channel4)_Effect1)@button_parameter2

Remark: you can see both addressing methods can be used.

Controls on different pages can have the same name (OSC uses an internal ID system to identify the controls),
so you can have the same control in different forms or on diferent 'pages'.
If the 'naming' and 'path' rules are respected all controls will receive the same information.
fi: If you have a 1st 'page' called 'Stems' on which you created controls for stems in [Channel1] (Mixxx CO group is between [ ] ):

name: (Channel1Stem1)@mute
path: /name

and on a 2nd 'page' you created a virtual controller on which you want to control the stems of [Channel1] as well, you just create a new control with the same name and path.
if you send a scripted instruction like

sendOSC('/GetP#(Channel1Stem1)@mute', 1)

both controls will be receive the information and will be updated.

OSC integration in Mixxx

  • in Mixxx Preferences:
    In OSC the terms OSC Server and Client are used, the machine sending information is the server, the machine receiving information is the client. In a system where both machines send and receive information I prefer to talk about Receivers (the OSC Controllers and the Mixxx-Machine)
  • OSC enabled (or not)
  • OSC Port-In: the UDP port on which Mixxx receives OSC packages, this port should be configured as Port-Out on the receiver.
  • OSC Port-Out: the UDP port on which Mixxx sends OSC packages, at the moment 1 standard port for all receivers. This port should be configured as Port-In on the receiver.
  • OSC Receivers, at the moment there's the possibility to configure 5 clients (maybe this should be 10) of which the IP-Address needs to be entered. The IP-address needs to be reachable from the Mixxx-Machine. Each Receiver can be set Active, which means that Mixxx will send a copy of the OSC packages to this address.
  • If 5 Receivers are set Active, Mixxx will send all outgoing information to these 5 machines.
  • A time value for a periodic 'syns push' can be entered in milliseconds.This value tells Mixxx the interval between 'sync push'-triggers.
  • In general:
  • OSC uses the '[' and ']' (square brackets) internally, so all square brackets are replaced with normal brackets '(' and ')'
  • In OSC all control objects are written with an @-symbol between the group and the key in the name, fi (Channel1)@play, (Master)@crossfader, (QuickEffectRack1_(Channel1Stem2))@super1
  • All OSC-addresses in the OSC-messaging system on the controller are rooted in the path, this means they are written as
    '/name' (so NOT as '/page/subpage/whatever/name'
  • Attention for capital letters!
  • At the moment OSC doe not standard support Double values (as all control object values are in Mixxx), so all values are sent and received as Float

Simple mapping of Control Object in TouchOSC:

  • create an object (button), name it as the CO fi (Channel1)@play
  • set the button properties, fi Toggle Press, Check Press, Check Receive
  • add the OSC Message in messaging, change the path so it's only '/name', click on the x, set the value on float, and the scale to 0 - 1
  • press the 'play' function in the OSC editor so the contoller gets executed
  • try your button, an OSC message is sent when you press play with value 1, when you release the button nothiong is send.
    when you press again an OSC message is sent with the value 0.

OSC functions on Mixxx Machine

  • As written earlier the Mixxx-Machine will send all OSC packages to all active Receivers.
  • Some basic information information is send 'on value change' in Mixxx, some information is sent as a new 'pseudo control object'
  • Trackinfo : (Group)@Trackartist, (Group)@TrackTitle (should be preferably received in a 'label'-object; will be expanded, unfortunately OSC does not support transferring images at the moment)
  • (Group)@track_loaded, (Group)@duration, (Group)@play, (Group)@eject

Scripting

  • An object does not have to be configured to send a standard OSC message, OSC messages can be send with a script.
  • In TouchOSC scripts can be written in LUA (very simple)
  • TouchOSC does not have the possibility to define global variables, local variable in functions can be defined but these can not be addressed in other functions. If you want to interchange information between functions / objects you need to store the values in (hidden) objects. It's recommended to create (define) all Control Objects so all information can be received, even if you don't need it at the moment.
  • Mixxx can be (periodically) triggered to send information with a '/GetP#' or '/GetV#' prefix before the control object's path. (a future change in these is possible), 'GetV#' requests Mixxx to send the value of the CO (Control Object) and 'GetP#' requests Mixxx to send the parameterized value of the CO (Control Object).
    Used as: 'sendOSC('/GetP#(Master)@crossfader', 1)', 'sendOSC('/GetV#(EqualizerRack1_(Channel1)Effect1)@Parameter3', 1)' ...
    When Mixxx receives these requests, Mixxx will send the values of these CO's instantly to all Active Receivers. These values will be received in the configured objects fi '(Master)@crossfader' and '(EqualizerRack1
    (Channel1)_Effect1)@Parameter3' if these are configured, else they'll be ignored.
  • There are some standard functions you can use to write scripts
  • update(), onValueChanged(key), onReceiveOSC, onReceiveNotify()...
    fi function to select the behaviour of key depending on the chosen deck to control (value of Ch2_Deckswitch- and the value of the Shift button.
function onValueChanged(key)
  if key == "x" and self.values.x == 1 then
    if self.parent.children['Ch2_Deckswitch'].values.x == 0 then
      if self.parent.children['Shift'].values.x == 0 then
        sendOSC('/(Channel2)@hotcue_1_activate', 1)    
      end 
      if self.parent.children['Shift'].values.x == 1 then
        sendOSC('/(Channel2)@hotcue_1_clear', 1)    
      end 
    end
    if self.parent.children['Ch2_Deckswitch'].values.x == 1 then
      if self.parent.children['Shift'].values.x == 0 then
        sendOSC('/(Channel4)@hotcue_1_activate', 1)    
      end 
      if self.parent.children['Shift'].values.x == 1 then
        sendOSC('/(Channel4)@hotcue_1_clear', 1)    
      end 
    end
  end    
end
  • Periodically (see earlier) Mixxx wil send a 'sync push'-trigger. This is a message to the object name '(Osc)@oscsync'.
    You can define an object with this name or a global 'onReceiveOSC'function for the correspondinf path ' if path == '/Osc@OscSync' then' ... in which you can list all objects for which you want to request an update:
    fi:
function onReceiveOSC(message, connections)
  local path = message[1]
  local arguments = message[2]
  if path == '/Osc@OscSync' then
    sendOSC('/GetP#(Master)@balance', 1)
    sendOSC('/GetP#(Master)@volume', 1)

Remark: in TouchOSC you can also create a (global) periodical function, fi to update a control like playposition. Herefor you need to call the 'time'-element of the device:

local time = getTime()
local miilisecs = getMillis()

In the next example we swant to receive the actual playpostion to calculate the elapsed and remain time of the track in Deck 1. If the Deck is not playing the interval between the updates can be longer than when the track is playing.
In the same example you can see the conversion of a string to a number 'tonumber())', the conversion of a number to a string 'tostring()', and the concatenate function ( .. ) as well as marking text as comment '--'
This script can be stored in a (Channel1)@playposition ' label coontrol.

local delay = 500
-- 500 = 1/2 second, 1000 = 1 second
local last = 0
local duration
local position 
local elapsed
local elapsedmin
local elapsedsec
local remain
local remainmin
local remainsec

function update()
  if self.parent.children['Ch1_Deckswitch'].values.x == 0 then
    if self.parent.children['(Channel1)@track_loaded'].values.text == '1' then
      local now = getMillis()
      if self.parent.children['(Channel1)@play'].values.x == 1 then
        if(now - last > delay) then
          last = now
          sendOSC('/GetP#(Channel1)@playposition', 1)
        end
      else
        if(now - last > delay * 5) then
          last = now
          --sendOSC('/GetP#(Channel1)@track_loaded', 1)
          sendOSC('/GetP#(Channel1)@playposition', 1)
        end 
      end
      self.parent.children['Ch1_position'].values.x = tonumber(self.values.text)
      duration = tonumber(self.parent.children['(Channel1)@duration'].values.text)
      position = tonumber(self.values.text)
  
      elapsed = duration * position
      elapsedmin = math.floor(elapsed / 60)
      elapsedsec = math.floor(elapsed - (elapsedmin * 60))
    
      remain = duration * (1 - position)
      remainmin = math.floor(remain / 60)
      remainsec = math.floor(remain - (remainmin * 60))
    else
      elapsedmin = 0
      elapsedsec = 0
      remainmin = 0
      remainsec = 0
    end

    if elapsedsec < 10 then
      self.parent.children['Ch1_Elapsed'].values.text = tostring(elapsedmin) .. ':0' .. tostring(elapsedsec)
    else 
      self.parent.children['Ch1_Elapsed'].values.text = tostring(elapsedmin) .. ':' .. tostring(elapsedsec)
    end
    if remainsec < 10 then
      self.parent.children['Ch1_Remain'].values.text = tostring(remainmin) .. ':0' .. tostring(remainsec)
    else 
      self.parent.children['Ch1_Remain'].values.text = tostring(remainmin) .. ':' .. tostring(remainsec)
    end   
  end 
end

Example: combination of print (to view received values in script window combined with a loo:

function onReceiveOSC(message, connections)
  local path = message[1]
  local arguments = message[2]
  for i=1,#arguments do
    print('\t path        =', path)
    print('\t argument    =', arguments[i .tag, arguments[i].value)
  end
end

Remark: in Mixxx we only send one argument in each OSC message (package), in other words: we send a new package for each value. So the example above will have only 1 argument in Mixxx - OSC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants