Mini OSC Cotroller is a gesture-based music controller built on a Raspberry Pi using an ADC chip (MCP3008), the RPi’s GPIO pins and a Python program.
The controller is built with a minimalistic approach. You can control only one parameter at a time using a distance sensor and your hand (resembling the logic behind Theremin). Which parameter is affected can be controlled by using a set of push buttons.
The controller communicates with SuperCollider on a remote machine using the OSC protocol. At this point the communication uses an Ethernet cable, but it is possible to implement using WiFi.
- | | README.org
- |D| python-files-RPi
- | | controller.py
- |D| sc-files
- | | 1.globals.scd
- | | 2.synthdefs.scd
- | | 3.guis.scd
- | | 4.osc_handling.scd
- A Raspberry Pi Model B+
- An ADC chip (MCP3008)
- An IR distance sensor (SHARP)
- Push buttons, jumper cables and a breadboard
- Raspberry Pi:
- Python
- Python libraries:
spidev
: SPI interfacepyOsc
: OSC interfaceRPi.GPIO
: Raspberry Pi GPIO interface
- Remote machine:
- SuperCollider
The controller consists of 5 push buttons connected to GPIO pins on the Raspberry Pi and an IR distance sensor connected to the ADC chip, which is connected to the Raspberry Pi through an SPI dedicated pin.
The Raspberry Pi is connected to a local network in which the remote machine is also connected.
A Python script runs on the Raspberry Pi, reading the input of the buttons and the sensor and sending an appropriate OSC message to the remote machine.
On SuperCollider, on the remote machine, some instances of OSCdef
catch the messages and perform one of the following actions:
- Switch a synth ON or OFF
- Change the output of a synth (to route through effects)
- Change a parameter of a synth
What the Python script does can be broke down into three parts:
- Read the sensor’s output
- Read the buttons’ output
- Communicate with the remote machine.
To read the input of the ADC chip, to which the sensor is connected, the spidev
library is used. An instance of SpiDev
is opened and a function is used to read the value of the sensor.
A button controls wether to read or not from the sensor. If yes, then the output is read as described below. If not, the program sleeps for a small amount of time (equal to the interval used to stabilize the sensor’s output) and then goes to loop. Ommiting this step caused serious stability problems.
The output of the sensor needs to be stabilized. This is achieved by reading the output of the sensor into an array for a given tiny interval of time. After trial and error, the interval is set at 0.075 seconds, during which time the sensor is read approximately 300 times. Then, using the Counter
class from the collections
library, we find the most common value in the array, with the most_common
method.
The output of this method is a 2D array, with each pair of values representing the dominant value and the number of times it occured. We only need the first pair (the most dominant value) and only the value itself.
There are 5 push buttons performing the following actions:
- Cycle through the available synths
- Cycle through the available parameters of each synth
- Switch the sensor ON or OFF
- Change the selected synth’s output
- Switch the selected synth ON or OFF
To read from the GPIO pins, the RPi.GPIO
library is used. The pins are set to input and the internal resistor is set to pull down, so that the ouput is 1 when the button is pressed and 0 when it is not pressed.
The buttons are read using interrupts to avoid checking for the state for each button during the program’s loop, which is both not elegant and resource-consuming. The logic behind interrupts is that you assign a function at a GPIO pin, which is run when an “edge” (a transition of states) is detected on that pin. The edge can be either “RISING” (from 0 to 1), “FALLING” (from 1 to 0) or “BOTH”. Here, since we want to perform an action when the button is pressed and no action when the button is released, we use the “RISING” option.
To cycle between synths and parameters, a dictionary is being used. The keys correspond to the names of the synths (prefixed with alphabet letters to help sorting) and the values of the keys are lists containing the parameters of each synth.
The interrupt functions change the indicies or the values of the lists by reading them as globals.
The function that change the current parameter read the values from the dictionary, according to which synth is being selected. The functions to change synths, turn synths on or off and change routes use a sorted array of the keys of the dictionary, so that the synths appear with the correct order (dictionaries are unsorted by default).
Upon running the program the user is prompted to set the IP of the remote machine and the port used for OSC.
The OSC messages are used for: a. changing synth attributes (parameters, turning on or off and output) and b. monitoring purposes (synth and parameter to be changed and whether the sensor sends data or not).
The OSC message concerning the sensor is sent from the main loop, after reading and stabilizing the sensor’s value. Then, it is sent with current synth and parameter names. This is an example of an OSC message, as read in sclang:
(/sensor, fm, carr, 300)
The OSC messages concerning switching synths on or off and changing synths’ output are sent from the respective callback functions when an interrupt occurs. The message about monitoring synths and parameters is sent either from the change_synth function or the change_param function and consists of the name of the synth and the parameter to be changed. The OSC message about monitoring the sensor’s state is sent from the sensor_switch function.
In SuperCollider the main task is to create sound. Some GUI windows are also created, containing the controls for each synth. They are used both for visualizing the changes made to the synths, and also controlling the synths, bypassing the controller.
The SuperCollider script is divided in 4 different files, so that it can be easily read and revised:
1.globals.scd
: Initialization code, global variables2.synthdefs.scd
: Synth definitions3.guis.scd
: GUI creation4.osc_handling.scd
: OSC functions definitions
The files are numbered because they must be evaluated in this particular order.
This file contains definitions and variables used thoughout the script: the number of synths, an empty array to contain synths, universal lag value, different groups for synths and effects (to ensure proper order of execution), buses for the effects and the proper ranges for the different parameters. These are defined using the ControlSpec
class.
This file contains the synth definitions. Each of the synths used implements, or implies, different synthesis techniques: FM synthesis, granular synthesis, additive synthesis, subtractive synthesis. Synths:
\fm
: Simple frequency modulation. An audio rate sine wave (modulator) modulates another sine wave’s frequency (carrier). The modulator’s frequency represents the rate at which the carrier’s frequency is being changed. Themul
value of the modulator represents the modulation depth, so, if it is set to 20, the carrier’s frequency (freq) will have a range of(freq - 20, freq + 20)
. Parameters:- carr: The carrier’s frequency.
- mod: The modulator’s frequency.
- dp: The modulation depth.
- pan: Position of the sound in the stereo field.
- amp: The sound’s amplitude.
- out: Where the sound outputs.
\grains
: Granular synthesis implementation using theTGrains
UGen. An impulse triggers a sample’s grain to be played. As an impulse I used an instance ofDust
, which provides random impulses of varying density. The sample used is a default sample of the SC’s sound library. An instance ofLFNoise0
provides some randomization of the grains position in the sample’s duration. Parameters:- buf: The buffer containing the sample.
- dens: The density of the random impulse.
- rate: The playback rate of the sample.
- pos: Position of the grain in the sample’s duration.
- dur: Duration of the grain.
- rfreq: Frequency by which the randomization of the grain’s position changes.
- rdp: Depth of the randomization.
- pan: Position of the sound in the stero field.
- amp: Amplitude of the sound.
- out: Where the sound outputs.
\comb
: Simple comb filter (echo). This can also be used as a form of additive synthesis, when the time between the sound repeats gets very small. Parameters:- in: Input bus.
- time: Interval between repeats.
- dec: Sound decay in seconds.
- amp: Amplitude of the repeats.
- out: Where the sound outputs.
\bpf
: Band Pass Filter implementation with a kind of FM. The BPF is used as an example of subtractive synthesis. Optionally, a sine wave is applied to the BPF’s frequency. Parameters:- in: Input bus.
- freq: BPF frequency.
- rq: The filter’s quality.
- mfreq: Frequency of the modulator wave.
- mdp: Depth of the modulation.
- out: Where the sound outputs.
Each synth is visualized with a respective GUI window. An extra GUI window is created for monitor purposes. The GUI windows use knobs and slidebars to change the different parameters, while mapping them to the appropriate range by using the ranges defined in 1.globals.scd
.
Each window has a button to turn the synth ON or OFF and a drop-down menu to change the output of the synth.
The GUI windows also act as an “intermediate” between the OSC messages and the synths: Instead of directly affecting the parameters, the OSC messages affect the GUI element. This is useful because a. it automates the command used to alter the parameters, as the output of the sliders and knobs is standard from 0 to 1, and b. by setting different values of the knobs and sliders, we can visualize and keep track of the changes made in each parameter.
This file contains the OSC functions’ definitions. The OSC functions listen to the data sent from python and use them to formulate commands in the form of strings (so that we can set variables’ names according to the received message), which are then evaluated as SC code using the interpret
command. Because the commands alter GUI elements, we must use the defer
command, which executes code using the AppClock instead of the SystemClock. There are 4 OSC functions:
\monitor
: This is used to pass monitor data to the relevant window. It receives the names of the synth and the parameter and uses them to format a string to assign to the string element of the StaticText inside the window. It also receives whether the sensor is on or off and changes the color and the label on the monitor window.\onoff
: This function receives the name of the synth and the state we want to assign to it (0 or 1).\route
: This function receives the name of the synth that we want to change its output. It then uses the previous output to assign the next one (using the value of the PopUpMenu).\change_params
: This function reads three values: the name of the synth, the name of the parameter and the value of the sensor’s output. It then maps the sensor’s value to a linear range of 0 to 1 (the knob’s and slider’s range). Then, it assigns the new value to the relevant GUI element.
TODO list:
- Automate the way synths are read, so that you can create synths in SC and then send an OSC message to python that generates the synth dictionary (tricky, because of the use of GUI and global variables)
- Change the way the synths are stored in SC. Instead of an array use a dictionary.
- Change how the synths’ output is being switched. The current implementation is sloppy and error-prone.
- Implement WiFi. Use previous work (SecretSchool etc)
- Write code to enable sending to multiple SC instances in different machines
- Update this ReadMe file with info about used libraries, classes, modules etc