Compact coding tools for SuperCollider, with Emacs Org-mode intergration
Canis plenus et libum totum
(Libum mandere et habere)
This is version 1.2alpha (retiny
).
- Download the library from http://github.com/iani/tiny-sc.
- Place the tiny-sc folder in the SuperCollider Extensions folder (found by executing this in SuperCollider):
Platform.userExtensionDir;
- Recompile the library (Command-Shift-L or menu Lang Recompile in the IDE).
Tiny-sc stores objects that create synths and tasks in the global Library under symbols, so that they can be recalled easily by their names, in order to play, start, stop, modify, and combine them. There are two main types of objects: A SynthPlayer
is an object that can execute a function or create a synth. A TaskPlayer
is an object that plays tasks. One can play a SynthPlayer on its own, or one can attach a SynthPlayer to a TaskPlayer in order to play a sequence of events.
SynthPlayer
and TaskPlayer
each have their own type of operator:
- +>
- Do something with a SynthPlayer
- *>
- Do something with a TaskPlayer
Basic example: Set the source of a SynthPlayer, and then play it:
{ SinOsc.arps(Rand(400, 1000)).perc } +> \test0; // set the source
\test0.play; // play the source
Tiny-sc is written so that one can achieve an audible change in the sound with a single statement, as far as possible. The example above can be written in one statement like this:
{ SinOsc.arps(Rand(400, 1000)).perc } ++> \test0; // ++> = set the source and play
One can combine a sound-source and a task expression in one statement like this:
{ SinOsc.arps(Rand(400, 1000)) * 0.3 } +> \test0 *> 0.1; // play a sequence of events
The above creates a SynthPlayer test0
playing the function as source, and schedules it to play events with a TaskPlayer named test0
. To stop the Task player, run:
\test0.stop;
There are two variants of the basic operators +>
and *>
:
- ++>
- Send a new function or SynthDef name to a SynthPlayer, and play it immediately. NOTE: This also disconnects the SynthPlayer from any task that it may be attached to.
- **>
- Modify a TaskPlayer, but do not start it.
For example:
Send a new SynthDef function to SynthPlayer test0
, and play it:
{ SinOsc.arp(Rand(1000, 2000)).perc } ++> \test0;
Send duration 2 to TaskPlayer test0
and start it:
2 *> \test0;
Send a func pattern as duration to TaskPlayer test0
, but do not start it. Change will take effect at the next event played by the already playing TaskPlayer:
Pfunc({ 0.01 rrand: 0.5 }) **> \test0;
Stop:
\test0.stop;
Tiny-sc uses class SynthPlayer
to define containers that can run synths. It also defines some shortcuts for commonly used UGen combinations and patterns and for control UGen arugments such as amp, pan and various envelopes. SynthPlayer instances are accessed by their name. Playing a SynthDef function or a SynthDef name will start a new Synth and fade out the previous one:
{ WhiteNoise.ar(0.1) } ++> \test; // ++> starts immediately
Play different synthdef, replace previous synth:
"default" ++> \test; // play with SynthDef "default"
Parameters of a SynthPlayer’s synth process can be set as numbers:
1000 +>.freq \test;
Or as patterns:
(50..40).midicps.pseq +>.freq \test;
Play again, using the next values from argument patterns:
\test.play;
Release with custom fade-out duration:
\test.release(3);
Overview of methods (messages) sent to symbols to play with SynthPlayer and TaskPlayer instances:
- To play SynthPlayers named by a symbol:
- send that symbol messages
play
to start,release
to stop. - To play tasks named by a symbol:
- send that symbol
start
to start playing,stop
to stop playing
In other words:
Examples:
"default" +> \c; // set source of SynthPlayer c to SynthDef "default", but do not play
\c.play; // play SynthPlayer c
Release (stop) SynthPlayer:
\c.release; // stop playing synthPlayer c
\c **> \c; // attach synthPlayer c to task c, without starting
\c.start; // start task c explicitly. Default event duration is 1 second
\c.stop; // stop task c
TaskPlayers stop when their duration pattern ends:
{ SinOsc.arp(\freq.kr(400)).perc } +> \c;
[60, 65, 67, 72].midicps.pseq +>.freq \c; // set an endless frequency patern to c
0.1.pn(16) *> \c; // Play the c synthPlayer with a task using a duration pattern of 16 events
A SynthPlayer can play with a duration pattern, which runs in a TaskPlayer.
It which may be a single number:
{Klank.arps(`[[40,120,200]*Rand(1,9),nil,(3..1)],PinkNoise.ar(0.1))}+>\test1*>1;
or a pattern:
\test1 *> [Pn(0.05, 50) ! 2, Pn(0.2, 10), Pn(2, 2)].flat.prand;
Set the fadeTime to create texture of cross-fading tones:
\test1.fadeTime = 1;
Stop the pattern that drives the SynthPlayer:
\test1.stop;
A SynthPlayer can be driven by a TaskPlayer to run patterns in a similar way as Pbind. The difference is that any key of a pattern as well as the duration pattern can be changed at any time. Furthermore, a SynthPlayer can switch TaskPlayers at any time, and can also be made to respond to a TaskFilter which only plays at beats that match a given condition. In this way, several SynthPlayer instances can be synchronized by following the same TaskPlayer or its attached filters.
Start the test2 SynthPlayer with a SynthDef function, and a pattern:
{ Blip.arps(\freq.kr(400), 3) } +> \test2 *> 0.1;
(60..90).midicps.prand +>.freq \test2
Add a second synthPlayer to the same pattern:
(Note: +>
waits to be triggered, and **>
does not restart the task)
{ SinOsc.arps(Rand(2000, 3000)).perc } +> \twock **> \test2;
Currently there is only one kind of filter, which is created by adding an adverb with its pattern to the *>
operator. The adverb may consist of characters x
, o
and _
. Their meanings are:
- x
- play new note for this beat.
- o
- stop note, remain silent.
- _
- hold previous note (do not release).
Important: The subfilter to a Task is registered under its own name, which is created automatically by prepending underscore =_= to the name of the parent task.
In the example below, the filter task of test2
has the name _test2
.
Example:
\test2 **>.x___xoxox_x_xoxxxooo \test2; // **> : Do not start parent task
Remove synthPlayer named test
from its task - but leave the other SynthPlayers still attached:
\test2.removeTask;
Stop the task, and all it’s dependent SynthPlayers:
\test2.stop;
Here is a more complicated example:
"default" +> \high; // set source of synthPlayer high
Pwhite(70, 90, inf).midicps +>.freq \high; // set frequency pattern for high
{ Blip.arps(Rand(50, 200), Rand(1, 10)).perc * 1.2 } +> \low; // set source of synthPlayer low
\high **> \low; // Make high follow task pattern of low
\low *> 0.1; // Set and start task pattern of low to play beats at 0.1 seconds
Let high
play a sub-pattern of the TaskPlayer low
.
\high **>.x___x_xoxoxxxooo \low; // creates filter \_low and attaches synthPlayer \high to it
Change the duration pattern of master Task low
:
\low **> Prand([Pn(0.08, 4), 0.3], inf);
Make both high
and low
play on the same filter of low
Task;
\high **>.xooox___xxox_xxxx \low; // create filter \_low and attaches synthPlayer \low to it
\low **> \_low; // also make synthPlayer low follow filter \_low
Change base beat pattern and reset low
to that root pattern:
\low **> 0.2;
Change base beat pattern again:
\low **> Prand([Pn(0.12, 4), 0.3, 0.6, Pn(0.06, 2)], inf);
Synch synthPlayer low
with synthPlayer high
again:
\low **> \_low; // also make synthPlayer low follow filter \_low
Stop the master pattern:
\low.stop;
\name *>.pattern \othername
Adds a pattern filter to othername
, or substitutees the new pattern to an existing pattern filter =othername=. In order to add a new pattern filter under an existing pattern filter, use the operator *>>
or **>>
.
Examples:
{ SinOsc.arps(2000).perc } +> \level1a *> 0.1;
{ SinOsc.arps(1800).perc } +> \level2a **>.xo \level1a;
{ SinOsc.arps(1500).perc } +> \level3a **>>.xo \_level1a;
Add level1a
as pattern filter of the master task leval1a
:
\level1a *>.xooxxoxxx \level1a;
\level1a.stop;
[1800, 2000, 2400].pseq +>.freq \level1b;
{ SinOsc.arps(\freq.kr(400)).sine } +> \level1b *> [0.1, 0.2].pseq;
{ SinOsc.arps(1500).sine } +> \level2b **>.xoo \level1b;
{ SinOsc.arps(1200).sine } +> \level3b **>>.xoo \_level1b;
{ SinOsc.arps(900).sine } +> \level4b **>>.xoo \__level1b;
{ SinOsc.arps(600).sine } +> \level5b **>>.xoo \___level1b;
As explained above, the task-filters are stored under names generated automatically by prepending _
to the name of the parent task.
\level1b.stop;
Note: MiniSteno is inspired by syntax of Steno by Julian Rohrhuber. (See https://github.com/telephon/Steno)
With MiniSteno one can specify the interconnections of several synthPlayer instances to create a tree consisting of parallel and serial branches. The tree is written as a string. The names of the synthPlayers are separated by “.”. Parentheses () indicate serial interconnections. Brackets [] indicate parallel intereconnections. Thus:"(source.effect)".miniSteno;
… creates a serial interconnection between source
and effect
. effect
reads its input from the output of source
.
"[c1.c2]".miniSteno;
… creates a group of parallel synthPlayers. c1
and c2
read from the same input and write to the same output bus.
Use the message addBranch
to add link configurations to a global tree for playing. The default branch of the tree is root
, and adding a branch to root
replaces the previous root, thus changing the global configuration. Thus, to install a MiniSteno tree as the root tree that is currently played, send it the message addBranch
.
Examples:
Add 2 synthPlayers a
and b
linked together in series:
"(a.b)".addBranch;
{ WhiteNoise.arp } ++> \a;
{ Ringz.arp(Inp.ar, LFNoise2.kr(1).range(30, 1000), 1) * 0.2 } ++> \b;
Remove the effect from the audible tree:
"a".addBranch
Bring back the effect and add a second effect to it, serially:
"(a.b.c)".addBranch;
{ Inp.ar.abs * Decay2.kr(Dust.kr(3), 0.1, 0.7) } ++> \c;
Linking effects in parallel:
First listen to a single effect in series
"(a.b)".addBranch;
{ Blip.arp(LFNoise2.kr(1).range(40, 400), 5) * 2 } ++> \a;
{ Ringz.ar(Inp.ar, LFNoise1.kr(25).range(30, 1000)) * 0.002 } ++> \b;
Add a second effect in series
"(a.b.c)".addBranch;
{ Inp.ar.abs * LFNoise0.kr(10) } ++> \c;
Now in parallel
"(a[b.c])".addBranch;
{ Inp.ar.abs.sqrt * LFPulse.kr(LFDNoise1.kr(0.3).range(0.6, 20), 0, 0.8, 0.03) } ++> \c;
Vary the effects:
{ Limiter.ar(Ringz.ar(Inp.ar, LFNoise1.kr(10).range(300, 4000)), 0.02) } ++> \b;
{ Inp.ar.sqrt.abs * SinOsc.ar(LFNoise0.kr(10).range(1000, 2000)) * 0.05 } ++> \c;
Add slow variations in amplitude to distinguish:
{ Limiter.ar(Ringz.ar(Inp.ar, LFNoise1.kr(10).range(300, 4000)), 0.02) * LFNoise1.kr(0.3) } ++> \b;
{ Inp.ar.sqrt.abs * SinOsc.ar(LFNoise0.kr(10).range(90, 200)) * 0.05 * LFNoise1.kr(0.3) } ++> \c;
Release with different durations:
\a.release(10);
\b.release(5);
\c.release(2);
Connect the reading SynthPlayer instance in multiple serial branches, and use “:” as separator between synthPlayer name and input name to indicate the name of the input.
Create source and effect synths, and listen to the 2 source synths separately:
{ SinOsc.arp (300 * LFPulse.kr(1).range (4, 5)) } ++> \sine;
{ SinOsc.arp (400) } ++> \pulse;
{ Inp.ar (\in1) * Inp.ar (\in2) * 5 } ++> \ringmod;
Link the output of the first source to in1
and the output of the second source to in2
:
"(sine.ringmod:in1)(pulse.ringmod:in2)".addBranch;
Just a demo with 8 SynthPlayer instances playing on 8 different TaskPlayers.
(
{ | n |
var name;
name = format("multi%", n).asSymbol;
n = n % 4;
{
Blip.arps(
1 + n / 2 * Line.kr(Rand(20, 80).midicps, Rand(20, 80).midicps, Rand(0.1, 1)),
Line.kr(Rand(5, 25), Rand(5, 25), 0.5)
).perform([\perc, \sine]@@n)
} +> name;
([0.25.pn(14), 5 ! 3, 1, 2, 0.1 ! 10, 3] / (0.5 + (n / 4))).flat.prand *> name;
} ! 8;
)
Use task filters to change density of the texture:
\multi0 **>.xoooo \multi0; // use **> to prevent re-starting the master Task
\multi1 **>.xoooo \multi1;
\multi2 **>.xoooo \multi2;
\multi3 **>.x \multi3;
\multi4 **>.xoooo \multi4;
\multi5 **>.xoooo \multi5;
\multi6 **>.xoooo \multi6;
\multi7 **>.xoooo \multi7;
Variation 1:
\multi0 **>.xoooo \multi0;
\multi1 **>.o \multi1;
\multi2 **>.o \multi2;
\multi3 **>.x \multi3;
\multi4 **>.o \multi4;
\multi5 **>.o \multi5;
\multi6 **>.o \multi6;
\multi7 **>.xo \multi7;
Variation 2:
\multi7 **> \multi7;
\multi6 **>.xo \multi7;
\multi5 **>>.xo \_multi7;
\multi4 **>>.xo \__multi7;
\multi3 **>>.xo \___multi7;
\multi2 **>>.xo \____multi7;
\multi1 **>>.xo \_____multi7;
\multi0 **>>.xo \______multi7;
Variation 3:
\multi7 *> 0.1;
Variation 4:
([0.25.pn(14), 5 ! 3, 1, 0.4 ! 10, 0.1 ! 20, 3] / 4).flat.prand *> \multi7;
{ | n | Pfunc({ 0.01 exprand: 0.35 }) +>.amp format("multi%", n).asSymbol } ! 8;
Variation 5:
([0.25.pn(14), 5 ! 3, 1, 0.4 ! 10, 0.1 ! 20, 3] / 4).flat.prand *> \multi7;
\multi7 **> \multi7;
\multi6 **>.xo \multi7;
\multi5 **>>.xo \_multi7;
\multi4 **>>.xo \__multi7;
([0.25.pn(14), 5 ! 3, 1, 0.4 ! 10, 0.1 ! 20, 3]).flat.prand *> \multi3;
\multi3 **>.xoo \multi3;
\multi2 **>>.xoo \_multi3;
\multi1 **>>.xoo \__multi3;
\multi0 **>>.xoo \___multi3;
To end the sound, stop all tasks:
TaskPlayer.stopAll;
Now gradually build some background texture and rhythms with the same synth sources:
0.01 *> \multi0;
0.1 *> \multi1;
\multi2 **>>.xo \multi1;
0.5 +>.amp \multi2;
\multi3 **>>.xo \_multi1;
Using the precise SynthDef loading method of SynthDefLoader
as of v1.2.2alpha, playing patterns with up to 500 events per second is safe, with light-weight SynthDefs, when there is only one task-thread playing, and dependent on general CPU capacity and other load on the machine. See: StressTests.scd.