supercollider

featureCreep

software written for and in collaboration with violinist George Kentros. premiered at after work - this violin must die, fylkingen stockholm 30 mar 2019.

the program is written in supercollider with additional features like ipad control and video mixer made in maxmspjitter.

main interface

the main window with its eight tracks (configurable) looks like this...

here you can set an amplitude threshold for the microphone and arm tracks to automatically detect and capture sounds.

with the waveform selection (dark grey areas) you select what part of the captured sound you want your different players to use.

there are many players - all with different behaviours...
slow, medium, repetitive, scanner, sweep, nervous, fastRhythmic, slowRhythmic, limpRhythmic, fast, drill, jump, plain, half, third, quarter, pingpong, crawl, wave, waveHalf, pattern1, pattern2, pattern3, round, sync

player behaviours include in which direction and how fast to progress in the captured sound buffer, when to play for how long, with which envelope and how loud, how much transposition, play in which audio channel etc etc.
some of these players are using granular synthesis techniques while others are scratching and jumping around in the sound buffer in more or less unpredictable ways. hopefully the names listed here above will give a hint of what players do to the sound.

all players are pieces of code. for example this the code for the scanner player...

SynthDef(\scanner, {|buf, amp= 1, start= 0, end= 1|
        var dur= BufDur.kr(buf);
        var pha= LFNoise2.kr(In.kr(100)+0.1)+1/2;
        var pos= pha*(end-start)+start;
        var add= LFNoise2.kr(1);
        var fre= In.kr(101).linexp(0, 1, 1.1, 18)+add*SinOsc.kr(0.1).max(0.5);
        var snd= TGrains.ar(
                2,
                Impulse.ar(fre),
                buf,
                1+LFNoise2.kr(1, 0.05),
                pos*dur,
                0.75,
                LFNoise2.kr(0.06, 0.5),
                amp.lag(0.5)
        );
        SendReply.kr(Impulse.kr(\updateRate.kr+1), '/pos', pos);
        Out.ar(\syncBus.ir(40)+\index.ir, K2A.ar(pha));
        snd= CompanderD.ar(snd, \compressorThresh.kr, 0.5, 0.5, 0.005, 0.01, EnvGate());
        Out.ar(\out.kr, snd);
}).add;

and most players will look very similar to this. but there are also a few special ones like sync which just matches the playback position of the player in the track above.

in the main window one can also set track volume, bypass any global effects and read/write soundfiles.

the computer keyboard may be used for shortcuts quickly arming, playing and selecting what each track should play.

global control

there is also an additional control gui with global volume, effects, automation and record...

the three sliders delay, sustainer, distortion are just simple wet/dry control for global effects (that can be bypassed for each track using the bp button), while the other sliders freeze, fire, industry, chaos, air are more complex and control a bunch of effects and behaviours. for example freeze will take the sound from all players and smear it out by removing transients and pitch shifting copies of the sound up/down in octaves.

when the automation button is clicked, the program itself can start and stop tracks, change effects and vary a lot of other parameters in the code. the idea here is that the software should be able to run independently and generate interesting variations over long periods of time. also it should never go completely silent nor go wild and overload everything.

remote ipad interface

all the above can be viewed and controlled remotely from a tablet or a phone using a web browser. we programmed a look-alike gui patch with maxmsp and the mira externals. this patch talks to the supercollider interface over opensound control.

the interface here is even more ugly - specially the waveform display because the sound data had to be downsampled and kept at a low resolution to reduce wifi network traffic. also it was hard to fit everything on a single screen as all widgets had to be quite large and not too close to each other to be usable on an ipad.

our software also includes a video mixer for camera and movie playback (not shown here).

sc: fixed number of decimals

here's a quick function for displaying float numbers as strings in supercollider.

(
~fixDec= {|val, numDecimals= 2|  //float to string with fixed number of decimals
        var str= val.round(0.1**numDecimals).asString;
        var num= str.size-str.indexOf($.)-1;
        str.extend(str.size+numDecimals-num, $0);
};
)

//test examples
~fixDec.value(0.1, 3)
-> 0.100
~fixDec.value(0.12345, 3)
-> 0.123

//rounds internally.  compare:
~fixDec.value(0.191, 2)
-> 0.19
~fixDec.value(0.197, 2)
-> 0.20

//can deal with negative values
~fixDec.value(-2pi, 4)
-> -6.2832

//and integers
~fixDec.value(10000, 4)
-> 10000.0000

~fixDec.value(10000, 0)
-> 10000.

f0mid

wireless midi <-> osc bridge using an esp8266-01. this circuit is extremely cheap to build. schematics, arduino code and examples for supercollider below.

i'm using the great Arduino MIDI Library that allows for both sending and receiving a multitude of midi messages including sysex, system realtime and time code messages. my arduino code just converts all these to/from osc and send or broadcast them over wifi network.

note: sending midi over wifi udp is generally a bad idea. there will be delays, glitches and even lost messages (hanging notes). this is specially problematic for midi time code (sync) messages. that said, in many situations this is ok and in my tests with simple note on/off messages + bend and control, things seem to work just fine.

f0mid

the circuit takes in 5v and then the regulator steps this down to 3.3v. notice the huge 220uF capacitor that's needed to provide power for the esp8266 during its infamous current draw spikes.

f0mid_schematics

supercollider example code...

//http://fortyseveneffects.github.io/arduino_midi_library/a00041.html
//http://fortyseveneffects.github.io/arduino_midi_library/a00043.html

OSCFunc.trace(true, true);
OSCFunc.trace(false);

n= NetAddr("f0mid.local", 18120);  //ip of esp8266
n.sendMsg(\ip, 192, 168, 1, 99);  //receiver ip (laptop - by default this is x.x.x.255 (broadcast))
n.sendMsg(\port, 57120);  //set receiver port (by default this is 57120)

n.sendMsg(\thru, 0);  //off
n.sendMsg(\thru, 1);  //full (default)
n.sendMsg(\thru, 2);  //same channel
n.sendMsg(\thru, 3);  //different channel

n.sendMsg(\noteOn, 66, 127, 1);  //(note, velo, chan)
n.sendMsg(\noteOff, 66, 0, 1);  //(note, velo, chan)
n.sendMsg(\afterTouchPoly, 50, 60, 3);  //poly pressure (note, press, chan)
n.sendMsg(\controlChange, 1, 64, 3);  //(num, val, chan)
n.sendMsg(\programChange, 10, 4);  //(num, chan)  note the -1 offset
n.sendMsg(\afterTouchChannel, 40, 2);  //(press, chan)
n.sendMsg(\pitchBend, -8000, 1);  //(bend, chan)  -8192 - 8191
n.sendMsg(\sysEx, 240, 14, 5, 0, 5, 247);  //(sysex) - 240 a b c d e ... 247

//realtime
(
var clock= 0xf8;  //248
var start= 0xfa;  //250
var continue= 0xfb;  //251
var stop= 0xfc;  //252
Routine.run({
        n.sendMsg(\realTime, start);
        100.do{
                n.sendMsg(\realTime, clock);
                0.02.wait;
        };
        n.sendMsg(\realTime, stop);
        1.wait;
        n.sendMsg(\realTime, continue);
        100.do{
                n.sendMsg(\realTime, clock);
                0.02.wait;
        };
        n.sendMsg(\realTime, stop);
});
)
n.sendMsg(\realTime, 0xfe);  //active sensing
n.sendMsg(\realTime, 0xff);  //system reset

n.sendMsg(\songPosition, 100);
n.sendMsg(\songSelect, 3);
n.sendMsg(\tuneRequest);

n.sendMsg(\beginNrpn, 10, 3);  //(number, channel)
n.sendMsg(\nrpnDecrement, 40, 3);  //(amount, channel)
n.sendMsg(\nrpnIncrement, 30, 3);  //(amount, channel)
n.sendMsg(\endNrpn, 3);  //(channel)

n.sendMsg(\beginRpn, 10, 4);  //(number, channel)
n.sendMsg(\rpnDecrement, 40, 4);  //(amount, channel)
n.sendMsg(\rpnIncrement, 30, 4);  //(amount, channel)
n.sendMsg(\endRpn, 4);  //(channel)

//--simple midi synth example
(
s.latency= 0.02;
s.waitForBoot{
        var busBend= Bus.control(s);
        var busCF= Bus.control(s);
        var busRQ= Bus.control(s);
        var busVol= Bus.control(s);
        var busPan= Bus.control(s);
        busBend.value= 0;
        busCF.value= 1000;
        busRQ.value= 0.5;
        busVol.value= 0.5;
        busPan.value= 0;
        SynthDef(\note, {|freq= 400, amp= 0.5, gate= 1, busBend, busCF, busRQ, busVol, busPan|
                var env= EnvGen.ar(Env.adsr(0.01, 1, 0.85, 0.1), gate, amp, doneAction:2);
                var bend= In.kr(busBend).lag(0.01);
                var cf= In.kr(busCF).lag(0.01);
                var rq= In.kr(busRQ).lag(0.01);
                var vol= In.kr(busVol).lag(0.01);
                var pan= In.kr(busPan).lag(0.01);
                var src= BLowPass4.ar(VarSaw.ar((freq+bend).midicps), cf, rq);
                OffsetOut.ar(0, Pan2.ar(src*env, pan, vol));
        }).add;
        d= ();
        OSCdef(\f0mid, {|msg|
                switch(msg[1],
                        \activeSensing, {},
                        \noteOn, {
                                d.at(msg[2]).set(\gate, 0);
                                d.put(msg[2], Synth(\note, [
                                        \freq, msg[2],
                                        \amp, msg[3].lincurve(0, 127, 0, 0.75, 4),
                                        \busBend, busBend,
                                        \busCF, busCF,
                                        \busRQ, busRQ,
                                        \busVol, busVol,
                                        \busPan, busPan
                                ]));
                        },
                        \noteOff, {
                                d.at(msg[2]).set(\gate, 0);
                                d.put(msg[2], nil);
                        },
                        \pitchBend, {
                                busBend.value= msg[2]/8192;
                        },
                        \controlChange, {
                                switch(msg[2],
                                        1, {
                                                busCF.value= msg[3].linexp(0, 127, 400, 4000);
                                        },
                                        7, {
                                                busVol.value= msg[3].lincurve(0, 127, 0, 1, 0);
                                        },
                                        10, {
                                                busPan.value= msg[3].linlin(0, 127, -1, 1);
                                        },
                                        74, {
                                                busRQ.value= msg[3].linlin(0, 127, 2, 0.1);
                                        },
                                        {("todo control: "+msg).postln}
                                );
                        },
                        {("todo command: "+msg).postln}
                );
        }, \f0mid);
        CmdPeriod.doOnce({
                busBend.free;
                busCF.free;
                busRQ.free;
                busVol.free;
                busPan.free;
        });
};
)

//mtc - receive
(
var a= MIDISMPTEAssembler({|time, format, dropFrame, srcID|
        [time, format, dropFrame, srcID].postln;
        //time.postln;
});
OSCdef(\f0mid, {|msg, time, addr|
        var chan, valu;
        if(msg[1]==\mtcQF, {
                chan= msg[2].rightShift(4);  //nibble high
                valu= msg[2].bitAnd(15);  //nibble low
                if(chan==7, {
                        valu= switch(valu,
                                6, {valu= 96},  //30fps
                                4, {valu= 64},  //30fps drop
                                2, {valu= 32},  //25fps
                                0, {valu= 0}     //24fps
                        );
                });
                a.value(addr.addr.bitAnd(255), chan, valu);
        });
}, \f0mid);
)

//mtc - send (kind of works - wslib quark required)
(
var startSec= 0;
var t= Main.elapsedTime-startSec;
var a= SMPTE(0, 30);
Routine.run({
        var chan= 0, valu= 0;
        inf.do{
                a.newSeconds(Main.elapsedTime-t);
                switch(chan,
                        0, {valu= a.frames.asInteger.bitAnd(15)},
                        1, {valu= a.frames.asInteger.rightShift(4)},
                        2, {valu= a.seconds.asInteger.bitAnd(15)},
                        3, {valu= a.seconds.asInteger.rightShift(4)},
                        4, {valu= a.minutes.asInteger.bitAnd(15)},
                        5, {valu= a.minutes.asInteger.rightShift(4)},
                        6, {valu= a.hours.asInteger.bitAnd(15)},
                        7, {
                                valu= a.hours.asInteger.bitAnd(1).rightShift(4);
                                switch(a.fps,
                                        30, {valu= valu.bitOr(6)},  //30fps
                                        //30fps drop not supported
                                        25, {valu= valu.bitOr(2)},  //25fps
                                        //24, {valu= valu.bitOr(0)}     //24fps
                                );
                        }
                );
                n.sendMsg(\mtcQF, chan.leftShift(4)+valu.bitAnd(15));
                chan= chan+1;
                if(chan==8, {
                        chan= 0;
                });
                (1/(a.fps*4)).wait;
        };
});
)

update 180421: added soft access-point option as well as osc commands for setting the receiver ip and port
update 181211: removed soft access-point and simplified wifi setup with the wifimanager library

AttachmentSize
Package icon f0mid_firmware.zip3.92 KB

rpi audio codec

here's how to set up the proto WM8731 based audio codec module from MikroElektronika and use it with supercollider on a raspberry pi 3.

power off the rpi and connect the proto board to the rpi with jump wires like this...

proto               raspberry
-----                -----
sck           ->    rpi 12
miso         ->    rpi 38
mosi         ->    rpi 40
adcl+dacl  ->    rpi 35  //both proto pins go to the same rpi pin
sda           ->    rpi 3
scl            ->    rpi 5
3.3v         ->    rpi 1
gnd          ->    rpi 6

see pinout diagram for help with the rpi gpio numbering.

power on the rpi and open a terminal and type...

sudo nano /boot/config.txt

find and uncomment the first line and add the second...

#dtparam=audio=on
dtoverlay=audioinjector-wm8731-audio

press ctrl+o to save and ctrl+x to exit.

sudo reboot

again open a terminal and type...

alsamixer

first press F5 to show all controls, then...
* enable item 'Mic' (space)
* set item 'Mic Boost' to 100 (up arrow key)
* enable item 'Playback Deemphasis' (m key)
* disable item 'ADC High Pass Filter' (m key)
* set item 'Input Mux' to Mic (arrow keys)
* enable item 'Output Mixer HiFi' (m key)

alsamixer-proto

now you should be able to start jackd with for example...

jackd -P75 -dalsa -dhw:0 -r48000 -p256 -n2

and get decent in/out sound at 5.3ms jack latency.

solar powered supercollider

here's how to run supercollider on power coming from the sun...

the main component is a raspberry pi zero with wifi that at startup creates a wireless access point, starts jackd+supercollider and launches a default sound patch.
to play around with the system and change the default sound log on to the access point with a laptop and start livecoding supercollider via the terminal or use the standard scide via vnc. one can for example also set up a couple of osc responders and let friends log on with their phones to control sounds.

front...
solarpoweredsupercollider02

back...
solarpoweredsupercollider01

the connections are pretty straightforward...

solarpanel -> dcdc converter -> battery -> rpi0 -> soundcard -> amplifier -> speaker(s)

the dcdc converter is taking the higher voltage coming out of the solar panel (~6v) and turns it into a stable 5v. this is then either charging the battery, or directly powering the raspberry pi. note that the amplifier also needs 5v and here i have taken that from pins 4 and 6 on the pi.

the powerbank battery is optional and can be omitted but then the solar panel will have to stay in the sun at all times - else the system will turn off or reboot when the power from the panel drops. the battery acts like a reservoir for when clouds are passing by but not only that - it also lets the system be used for a couple of hours in the evening.

material/modules needed:

* rpi zero w
* 8gb micro sd card
* 5v usb powerbank (best if it can charge and output power at the same time)
* 6v 6watt solar panel ( www.adafruit.com/product/1525 )
* dc-dc converter ( www.olimex.com/Products/Power/DCDC6-16-TO5 )
* usb sound adapter
* pam8403 stereo amplifier module
* two full range speakers
* wooden board, double adhesive tape + various cables and screws

download raspbian jessie (here jessie desktop 2017-07-05-raspbian-jessie.zip) and burn it onto the sd card with etcher.

do the usual setup (change default password, activate ssh), optionally activate vnc and then install supercolliderStandaloneRPI1.

to set up a wifi access point do the following (basically the same as this)...

* sudo apt-get install dnsmasq hostapd
* sudo systemctl stop dnsmasq
* sudo systemctl stop hostapd
* sudo nano /etc/dhcpcd.conf  #and add...
        denyinterfaces wlan0
* sudo nano /etc/network/interfaces  #and make sure wlan0 looks like...
        allow-hotplug wlan0
        iface wlan0 inet static
            address 192.168.4.1
            netmask 255.255.255.0
            network 192.168.4.0
* sudo service dhcpcd restart
* sudo ifdown wlan0
* sudo ifup wlan0
* sudo nano /etc/dnsmasq.conf  #and add the following...
        interface=wlan0
        dhcp-range=192.168.4.2,192.168.4.20,255.255.255.0,24h
* sudo nano /etc/hostapd/hostapd.conf  #and add the following...
        interface=wlan0
        driver=nl80211
        ssid=solarsc
        hw_mode=g
        channel=7
        wmm_enabled=0
        macaddr_acl=0
        auth_algs=1
        ignore_broadcast_ssid=0
        wpa=2
        wpa_passphrase=mypass12345
        wpa_key_mgmt=WPA-PSK
        wpa_pairwise=TKIP
        rsn_pairwise=CCMP
* sudo nano /etc/default/hostapd  #and change to the following...
        DAEMON_CONF="/etc/hostapd/hostapd.conf"
* sudo service hostapd start
* sudo service dnsmasq start

last change the file mycode.scd and add this default sound (tweet0340)...

s.waitForBoot{
        play{a=SinOscFB;Mix(AllpassN ar:a.ar(midicps(Duty.ar(c=a.ar(1/[12,8])+3/24,0,Dseq([0,8,5,1,5,4,5]*round(c*18),inf))+60),c*2)/4)}// #SuperCollider
};

if it is distorting try lowering the volume in alsamixer.

udssrKontroll

after many years i finally got around to rebuild one of these boxes.

so this old soviet made device is now a wireless controller that send out osc. there are in total 34 buttons, 16 knobs and an additional rgb status led. it automatically connects via wifi to max or supercollider and run on 5v (usb powerbank).

kicad schematics, arduino firmware, supercollider classes and maxmsp abstractions attached below.

udssrKontroll01

the inside is quite a mess. i use an atmega168 together with six 4051 multiplexers to read all the inputs. the wifi module is an esp8266-01.

udssrKontroll02

update 190124: v1.1 fixed a small but breaking bug in the atmega168 code

AttachmentSize
Package icon udssrKontroll_1.1.zip116.32 KB

supercollider firmata 3

reading digital inputs from an arduino with the SCFirmata is a little bit more complicated than needed.

here an example that reads 6 analog and 6 digital at the same time.

NOTE: use resistors (10K) to pull up or pull down the digital inputs. (i couldn't figure out how to activate the built in pullups.)

SerialPort.devices;
d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

(
~analog= [0, 1, 2, 3, 4, 5];  //A0-A5
~digital= [2, 3, 4, 5, 6, 12];  //some digital input pins
s.latency= 0.05;
s.waitForBoot{
        var freqsArr= 0!~analog.size;
        var ampsArr= 0!~digital.size;
        Ndef(\snd3, {Splay.ar(SinOsc.ar(\freqs.kr(freqsArr, 0.05), 0, \amps.kr(ampsArr.lag(0.01))).tanh)}).play;
        ~analog.do{|x|
                f.reportAnalogPin(x, true);      //start reading analog pins
        };
        f.analogPinAction= {|num, val|
                //[num, val].postln;
                freqsArr.put(~analog.indexOf(num), val);
                Ndef(\snd3).setn(\freqs, freqsArr);
        };
        ~digital.do{|x|
                f.setPinMode(x, \INPUT);
        };
        f.reportDigitalPort(0, true);
        f.reportDigitalPort(1, true);
        f.digitalPortAction= {|port, mask|
                var dig;
                //[port, mask, mask.asBinaryString].postln;
                dig= ~digital.collect{|x| ((mask<<(port*8))&(1<<x)==(1<<x)).binaryValue};
                Ndef(\snd3).set(\amps, dig.postln);
        };
};
)

(
Ndef(\snd3).stop;
~analog.do{|i|
        f.reportAnalogPin(i, false);     //stop reading A0-Anum
};
f.reportDigitalPort(0, false);
f.reportDigitalPort(1, false);
f.end;
f.close;
)

previous articles...

www.fredrikolofsson.com/f0blog/node/647

www.fredrikolofsson.com/f0blog/node/629

supercollider firmata 2

+2 years ago i put up a simple example of how to use firmata with arduino and supercollider here. that code still work but it only show how to read a single analog input on the arduino.

here is how one can read both A0 and A1 and map those to synth parameters in supercollider...

//how to read pins A0 and A1 with SCFirmata...
//tested with Arduino1.8.0 and SC3.8.0
//first in Arduino IDE:
//  * select File / Examples / Firmata / StandardFirmata
//  * upload this example to an arduino
//then in SC install the SCFirmata classes
//  * download zip file https://github.com/blacksound/SCFirmata
//  * extract files and put them in your sc application support directory
//  * recompile sc

SerialPort.devices;
d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

s.boot

(
Ndef(\snd, {|freq1= 400, freq2= 500, amp= 0.5| SinOsc.ar([freq1, freq2].lag(0.08), 0, amp.lag(0.08)).tanh}).play;
f.reportAnalogPin(0, true);      //start reading A0
f.reportAnalogPin(1, true);      //start reading A1
f.analogPinAction= {|num, val|
        [num, val].postln;
        switch(num,
                0, {
                        Ndef(\snd).set(\freq1, val.linexp(0, 1023, 400, 800)); //A0 mapped to freq1
                },
                1, {
                        Ndef(\snd).set(\freq2, val.linexp(0, 1023, 400, 800)); //A1 mapped to freq2
                }
        );
};
)

(
Ndef(\snd).stop;
f.reportAnalogPin(0, false);     //stop reading A0
f.reportAnalogPin(1, false);     //stop reading A1
f.end;
f.close;
)

and to read all six analog inputs (A0-A5) one can do...

SerialPort.devices;
d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

s.boot
~numberOfAna= 6;  //number of analog inputs (here A0-A5)

(
var freqsArr= 0!~numberOfAna;
Ndef(\snd2, {|amp= 0.5| Splay.ar(SinOsc.ar(\freqs.kr(freqsArr, 0.05), 0, amp.lag(0.08)).tanh)}).play;
~numberOfAna.do{|i|
        f.reportAnalogPin(i, true);      //start reading A0-Anum
};
f.analogPinAction= {|num, val|
        [num, val].postln;
        freqsArr.put(num, val);
        Ndef(\snd2).setn(\freqs, freqsArr);
};
)

(
Ndef(\snd2).stop;
~numberOfAna.do{|i|
        f.reportAnalogPin(i, false);     //stop reading A0-Anum
};
f.end;
f.close;
)

Pages

Subscribe to RSS - supercollider