The Manual
The Manual
The Manual
_ _
/\ | | | |
/ \ | |_ __ | |__ __ _
/ /\ \ | | '_ \| '_ \ / _` |
/ ____ \| | |_) | | | | (_| |
/_/____\_\_| .__/|_| |_|\__,_|
| ____| | |
| |__ ___ |_|_
| __/ _ \| '__|
| | | (_) | |
|_|__\___/|_| _ _
| __ \ (_) | |
| |__) |_____ ___ _____ _| |
| _ // _ \ \ / / |/ _ \ \ /\ / / |
| | \ \ __/\ V /| | __/\ V V /|_|
|_| \_\___| \_/ |_|\___| \_/\_/ (_)
This manual is now ready for review - please read the For Reviewers section.
PDF Version
As well as reading the manual online you can download a PDF for printing
Introduction
Installing Supercollider
First beep in SuperCollider
First synth in Sonic Pi
Setting up Sonic Pi from source
Deep dive
Investigating Sonic Pi and error messages
Loggin, loggin, loggin
How synthdefs are loaded
How load_synthdefs works
How Sonic Pi plays synths
What's next
For Reviewers
Does it work? So if you breeze through and don’t have any problems TELL ME [email protected]
Where do you get stuck? If you get stuck raise an issue of GitHub
What did you get past successfully but you didn’t understand? And why? raise an issue of GitHub
If you are struggling with the whole GitHub/issues/doing stuff bit then you can still just email me
[email protected]
For Reviewers
There are some aspects that I still don’t know but would like to, so I can finish this damn manual off and get on with
writing my own synths:
Some parameters can be changed with control and some can’t. What are the mechanics of that? Is it
something Sonic Pi does or SuperCollider?
How can I get debugging information out of SuperCollider to investigate what is happening at runtime - I can’t
seem to work it out
How can I get a trace of a dump of OSC commands between the Sonic Pi server and the running SuperCollider
instance? Setting use_osc_logging to true only gives me error messages.
Can I pass arrays to my new synth, or is it only values? And is that a limitation of the Sonic Pi implementation
or OSC or what?
How does BPM management work - I pass in beat-length values for attack , sustain , decay , release but
they magically change in reality with changes of BPM - I presume that Sonic Pi manages some global BPM
value for SuperCollider?
_ _ _ _
| | | | | | | |
| |__| | __ _| |_ __ | |
| __ |/ _` | | '_ \| |
| | | | (_| | | |_) |_|
|_| |_|\__,_|_| .__/(_)
| |
|_|
Also I am not sure if my fourth synthesiser is working correctly but I am struggling to debug it.
The heavy lifting of making actual noises in Sonic Pi is provided by SuperCollider which is a full-blown sonic engine
for making and manipulating audio signals. SuperCollider is far from user-friendly requiring a high degree of both
programming skill and a profound understanding of synthesiser design.
Sonic Pi has a great range of synthesisers but there is always room for more. This manual will make it easier for a
small number of synthesiser designers and developers to make a much large range of synthesisers and effects
available to the very large community of normal users of Sonic Pi.
The goal of this project is to enable a small community of sound sculptors to build new synthesisers for the large
community of live coders to incorporate by demystifying the messy business of making one coding paradigm
available in a completely foreign one.
It is a step-by-step manual explaining in detail the intricacies of the relationship between the Sonic Pi front end and
the SuperCollider back end.
As we go through the book it will become clear that SuperCollider is used for all things that make sounds in Sonic Pi,
synths, samples and effects (FXs). This book only covers synths.
SuperCollider offers functionality that allows you build and play synthesisers.
This manual focusses on building them only: for people to play them using Sonic Pi.
SuperCollider is a major programming environment in its own right and to learn how to get the most out of it you
will need to study it.
This chapter shows you how to make a beep in SuperCollider and then how to make that same beep from within
Sonic Pi.
This chapter looks at the existing synths and the two different ways they are implemented - the old way (Overtone)
and the new approved way (SuperCollider).
Installing SuperCollider
Download SuperCollider for your platform and install it.
The left hand panel untitled is the coding panel, the top right has the SuperCollider documentation browser
and the bottom right is the logs panel.
Reference
There is a guide to using the code panel (also known as the interpreter).
This tutorial is to enable you to write your own synths and for you, or other people, to use them in Sonic Pi.
Obviously if you can write Super Collider code you could just program Super Collider without Sonic Pi. And this
manual will help you learn some, but not all, of how to do just that.
Like Sonic Pi this manual has an overriding design principle - to get from nothing to make a noise as quickly as
possible.
use_synth :beep
play 69
To start with you need to tell the SuperCollider App to start the server engine.
This can be done with the menu item: Server -> Boot Server
You then need to check the log window to see if there are error messages. Sometimes you will need to edit audio
settings on your machine to get SuperCollider to work.
{SinOsc.ar(440, 0, 0.2)}.play;
Copy the snippet into the coding window on SuperCollider. Place the cursor in the code line and press [SHIFT]
[ENTER] and you will hear an A4 note (the Stuttgart pitch for standard tuning).
To stop it you press [COMMAND][.] (the command key and the full stop at the same time.
One of the key differences between Super Collider and Sonic Pi is that Sonic Pi is sound-based (play this sound for
this time period) and Super Collider is state-based - get this synthesiser into this state. By default sounds in Super
Collider are of indefinite duration.
Lets break down that beep:
Its a function:
{ ... }
{ ...}.play;
{SinOsc.ar(440, 0, 0.2)}.play;
ar is a method called on that object with the parameters 440 , 0 and 0.2 .
If we look up SinOsc in the documentation browser we will see that the object exposes two functions with the
same signature:
In this instance the mul is effectively the volume, make it higher and the note will sound louder, lower it will be
quieter.
Lets look again at the interface and the two methods ar and kr . The ar / kr pairing appears throughout
SuperCollider so its important to understand what they mean.
For the moment we will stick to ar . Later on when we start building a proper synthesiser for Sonic Pi we will look
again at the meaning of these.
{SinOsc.ar(440, 0, 0.2)}.play;
If we run this definition in Super Collider we can get a synth that we can play in Sonic Pi. Notice that it is writing the
synthdef to a default location in my home directory (on a Mac) where Sonic Pi will find it - you will need to put your
home directory in whatever operating system you use into that path.
To use it in Sonic Pi we need to do two things. Firstly we need to enable it in the GUI:
Nota Bene/Pay Attention the GUI enables both external synths and external FXs. This manual only covers synths but
in theory you could write your own effects too.
use_synth(:myfirstsynth)
play 69
Play about with this. There are a couple of things to notice - the sound will always be coming from one side of the
speakers. That’s a bit odd. And also as you change the note up and down it makes no difference. It just plays A4 that
fades out in 1 second.
We will fix both of these things later on. But first lets breakdown the synth definition:
/*
Define the synth - give it a name "myfirstsynth"
let it take one argument: `out_bus`
*/
// define 2 variables
var note, envelope;
/*
Create a sound envelope that goes from 0.1 to 0 in 1 second
and when it has done that trigger an action
that destroys the running instance of
the synthesiser and frees all memory
*/
envelope = Line.kr(0.1, 0.0, 1.0, doneAction: 2);
// define the note we are going to play A4 at 440Hz and set the volume to be the envelope
note = SinOsc.ar(440, 0, envelope);
We have had to do a bit more work to get it to play nice with Sonic Pi. It has a few problems:
But it’s not all bad - the line that determines the length of the note also calls a self-destruct function that cleans up
and frees resources - without it your computer would gradually fill up with unused instances of synthesisers
consuming both memory and CPU and eventually would just crash.
In Chapter 3 we will gradually build up this synthesiser until it is a clone of the sine synthesiser that is built into
Sonic Pi.
_slide
_slide_shape
_slide_curve
But before we can build a proper synth we need to understand a lot of stuff:
The Sonic Pi github has a load of READMEs covering installing on different platforms.
There is a problem tho. When we install Sonic Pi we pull down a SuperCollider server to run but we don’t bring the
GUI components.
On the Mac OS X - the installed Sonic Pi instance has a pre-built SuperCollider server bundled with it in an
executable package. We can just install SuperCollider the app with a GUI alongside it.
With Linux and Raspberry Pi we install the SuperCollider server (but not GUI or CLI) as a dependency alongside our
compiled SonicPi.
To muck about with the synths we need to install the other components.
for other distros you will need to find the appropriate incantation.
scide
The synths
These are all the synths in Sonic Pi:
Beep Clojure
Blade Clojure
Bnoise Clojure
Chipbass Clojure
Chiplead Clojure
Chipnoise Clojure
Cnoise Clojure
Dpulse Clojure
Dsaw Clojure
Dtri Clojure
Fm Clojure
Gnoise Clojure
Growl Clojure
Hollow Clojure
Hoover Clojure
Kalimba SuperCollider
Kalimba SuperCollider
Synth name Where defined
Mod Beep Clojure
Mod Fm Clojure
Noise Clojure
Organ
SuperCollider
Tonewheel
Piano SuperCollider
Pluck Clojure
Pnoise Clojure
Prophet Clojure
Pulse Clojure
Rodeo SuperCollider
Saw Clojure
Sine Clojure
Sound In Special
Square Clojure
Subpulse Clojure
Supersaw Clojure
Tb303 Clojure
Tri Clojure
Zawa Clojure
There is a directory with all the Clojure synths synths defined in it, and another directory with all the SuperCollider
synths.
The synths marked special are the ones that use the soundcard as a synthesiser and won’t be discussed here.
When you look at the sources you will find lots of other stuff that doesn’t appear as a synth in Sonic Pi. This is
because all the effects are also created in SuperCollider. The sample handling code also uses it.
Later on in this chapter we will investigate how these synths are defined and invoked.
Remember that FX and sample handling and anything that also affects sound is handled in SuperCollider under the
covers and the source code that you find in the Clojure synths directory will include code to do that too.
The reason for this is that it is easier to incorporate Clojure source code into a multi-server compile and tooling
chain - but harder to do the same with SuperCollider source which is designed to be saved and compiled inside the
SuperCollider built in IDE.
Sonic PI V5.0.0 Tech Preview 2 has some newer, more sophisticated synths written directly in SuperCollider.
To see how Sonic PI V5.0.0 Tech Preview 2 handles the built in synths written in overtone let’s look at the file
basic.clj
If we scroll down to the bottom we can see the synths being compiled:
(comment
(core/save-synthdef sonic-pi-beep)
(core/save-synthdef sonic-pi-saw)
(core/save-synthdef sonic-pi-tri)
(core/save-synthdef sonic-pi-pulse)
(core/save-synthdef sonic-pi-subpulse)
(core/save-synthdef sonic-pi-square)
(core/save-synthdef sonic-pi-dsaw)
...
If we scroll back up to the top we can find the definition of the beep synth in Clojure:
(without-namespace-in-synthdef
(defsynth sonic-pi-beep [note 52
note_slide 0
note_slide_shape 1
note_slide_curve 0
amp 1
amp_slide 0
amp_slide_shape 1
amp_slide_curve 0
pan 0
pan_slide 0
pan_slide_shape 1
pan_slide_curve 0
attack 0
decay 0
sustain 0
release 1
attack_level 1
decay_level -1
sustain_level 1
env_curve 1
out_bus 0]
(let [decay_level (select:kr (= -1 decay_level) [decay_level sustain_level])
note (varlag note note_slide note_slide_curve note_slide_shape)
amp (varlag amp amp_slide amp_slide_curve amp_slide_shape)
amp-fudge 1
pan (varlag pan pan_slide pan_slide_curve pan_slide_shape)
freq (midicps note)
snd (sin-osc freq)
env (env-gen:kr (core/shaped-adsr attack decay sustain release attack_level decay_level
(out out_bus (pan2 (* amp-fudge env snd) pan amp))))
defsynth sonic-pi-beep
SynthDef("myfirstsynth"
The next bit is all the arguments the synth takes (with their defaults).
If we go through this code we can see a lot of things that are clearly UGens and methods:
We can also see that when a uGen offers a .kr and .ar option the clojure default is .ar and we have to specify
.kr explicitly. This makes sense as we use .ar for sound signals (that we care a lot about) and .kr for control
signals that we are a bit meh about.
This synth uses the uGen SinOsc to generate its output - just like the first synth in Chapter 1. This is the synth we
will be recreating in Chapter 3.
If we look at our old first synth definition we can see some of these elements and how we have to compose them:
// define 2 variables
var note, envelope;
/*
Create a sound envelope that goes from 0.1 to 0 in 1 second
and when it has done that trigger an action
that destroys the running instance of
the synthesiser and frees all memory
*/
envelope = Line.kr(0.1, 0.0, 1.0, doneAction: 2);
// define the note we are going to play A4 at 440Hz and set the volume to be the envelope
note = SinOsc.ar(440, 0, envelope);
So using the Overtone library we can transcribe a SuperCollider definition of a synthesizer into a Lisp format and
then use a compiler against that to emit the appropriate compiled SuperCollider bytecode for Sonic Pi to use.
When we develop our own version of beep we will reverse engineer this Overtone description into SuperCollider
code.
This manual will not spend a lot of time looking at these - but as we build out our version of beep directly in
SuperCollider it will end up looking a lot like any of these ones.
Remember that all sound manipulation in Sonic Pi is done in SuperCollider so don’t be surprised when you find
things in this directory that aren’t synthesisers but FX etc (for instance autotuner ).
_ _ _
| | | | | |
| |__| | ___ _ _| |
| __ |/ _ \ | | | |
| | | | __/ |_| |_|
|_| |_|\___|\__, (_)
__/ |
|___/
Don’t feel that you MUST read this chapter, you can skip it if you want.
If you want to add your own well-behaved user-defined synth in Sonic Pi and compile it in so it works as native you
can just skip this chapter.
But if you want your synth to be a bit funky, to not work quite like the built-in ones then you will need to take a
swatch at the source code to figure out why it works as it does and how your synth may or may not work with all the
features of Sonic Pi.
This section doesn’t cover all the pathways by which your synth will be called, nor is it exhaustive in the one, main
pathway it explores - its job is to cut a track through the jungle - it’s up to you to make the journey
Small apology
It’s been 25 years since last I wrote Ruby in anger, I can sort-of read it still, but not write it, socaveat lector/reader
beware .
Let’s just muck about with this programme, first with a built-in Sonic Pi synth (by default beep ) and then with our
own one.
Built-in synths
play 60
The integer note 60 has become a float 60.0 but we can use floats in our play command quite happily too:
play 60.0
gives:
We haven’t named the note argument but we can, and we get the same result:
play note: 60
play :c4
So the notes parameter has magically been transformed into note with a list.
play notes: 60
The sound has changed - the note played is different. What’s that about?
So whereas when we passed a list tagged notes Sonic Pi recognised it as a list and said, “ooh, notes is just the
plural of note, lets move this value to the note slot”.
Sonic Pi just saying “I don’t know what these things are but they are not numbers so into the bin with them”.
But a chord isn’t a list of notes, its a data structure called a ring:
print(chord(:e3, :minor))
gives:
What we type into Sonic Pi is not what is being played - the term of art for this is munging - the inputs are being
munged into something else.
giving:
gives:
It seems to be kept:
Just for badness, trust me it will make sense later, lets go again with a non-option but make it out_bus :
What happens if we try and leave out the note, is there a default value here? The result from passing in a single
value in notes would tend to suggest there is:
as we suspected:
But look what happens when we try and pass a duff value to a known parameter:
play :c4, pan: [3, 4, 5], gordon: 99
And the same if we try and bust the bounds. The pan parameter places the sound on the left/right pan with -1.0
being hard left and 1.0 being hard right:
throws an error:
And if we try and use play without a number, symbol or list we also get a crash:
play :juicyfruit
resulting in:
Custom synths
If we switch to our myfirstsynth we get a slightly different story:
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
play 60
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
pan is sanity checked to be between -1.0 and 1.0 when we use a built-it synth, that doesn’t happen here.
When we read the code for our synthesizer we realise this is a bit odd. Our function only takes one argument out
and yet we are calling it with 3, none of which is out .
So whatever parameters we send are all being chucked away - apart from out . What happens if we actually pass in
an out parameter:
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
It plays.
The fact that it plays is a clue - because on reading the SuperCollider code it shouldn’t. We use it as the first
parameter in the uGen Out . It determines the output channel. The way Sonic Pi is wired up the channel 0 makes
our computer play noise, any other channel is not connected to something to turn signal into sound - so passing in a
different value of out_bus should cause silence. The fact that it doesn’t shows that Sonic Pi is overwritting it.
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
You can’t play chords either. But a note-free incantation still works:
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfirstsynth)
play pan: 44
Giving:
If you are using a built-in synth, Sonic Pi checks your parameters systematically - but passes on additional
parameters unchecked - this makes Sonic Pi work seamlessly, if you switch a built-in synth with additional
parameters out for a simpler one the extended values are silently dropped.
By contrast with a synth that Sonic Pi doesn’t recognise - it just sends all the parameters unchanged to the synth.
In the next section we will look at ways to find out what is happening in the code, and in the one after that we will
peek inside Sonic Pi to figure out what’s really going on.
The various markdown files at the root level in the source code have instructions on how to build for many
platforms.
For this exploration we will be looking at the ruby part of Sonic Pi.
Luckily once Sonic Pi is built this is very straightforward. ruby is an interpreted and not a compiled language and by
simply editing ruby source code and stopping and restarting Sonic Pi we can see the changes.
Once we have compiled a built Sonic Pi we can start and run it by invoking the binary sonic-pi which is created in
the directory app/build/gui/qt .
False friends
The Sonic Pi language has a couple of false friend functions - things that look like they will be helpful in this context,
but they mostly aren’t.
They are the commands use_debug and with_debug in the language reference. They only affect logging of synth
triggers to the front end.
use_synth :bass_foundation
play 60
we see the following log message in the log window of the GUI:
=> Starting run 6
If we now add the use_debug command the log message goes away:
use_debug false
use_synth :bass_foundation
play 60
This is just a convenience function for front end users and not a proper debugging tool.
gordon@raspberrypi:~/.sonic-pi/log $ ls
daemon.log gui.log jackd.log spider.log tau.log
debug.log history scsynth.log tau_boot.log
You can get a lot more info if you go into theutil module and set the debug mode to true tho:
def debug_mode
false
end
BEWARE: there is more than one module called util.lib - you want the one in /app/server/ruby/lib/sonicpi/
load_synthdefs "/home/gordon/.synthdefs"
use_synth :myfirstsynth
play 60
If we grep the string Loaded synthdefs we can find the origin - in the module sound.rb:
def load_synthdefs(path=Paths.synthdef_path)
raise "load_synthdefs argument must be a valid path to a synth design. Got an empty string." if
path = File.expand_path(path)
raise "No directory or file exists called #{path.inspect}" unless File.exist? path
if File.file?(path)
load_synthdef(path)
else
@mod_sound_studio.load_synthdefs(path)
sep = " - "
synthdefs = Dir.glob(path + "/*.scsyndef").join("#{sep}\n")
__info "Loaded synthdefs in path: #{path}
#{sep}#{synthdefs}"
end
end
doc name: :load_synthdefs,
introduced: Version.new(2,0,0),
summary: "Load external synthdefs",
doc: "Load all pre-compiled synth designs in the specified directory. This is useful if y
...
The function __info that is being called to write the msg to the front end is found in the module runtime.rb:
So now when we stop and start Sonic Pi and run the same code we see that every msg we get in the front end, we
get a banjo before it.
=> banjo
=> banjo
So by simply adding lines to our ruby that calls this __info function we can see what’s going on when we do stuff.
Well it turns out that Sonic Pi has that sorted too. You can write a message to a buffer and when the boot is
completed the buffer is dumped into the log window. Let’s see that in action in the studio module which handles
the boot process and the creation of the GUI.
If I invoke the function message with a string, as I do here, it will appear in the log screen on boot.
def init_scsynth
message "bingo bongo dandy dongo"
@server = Server.new(@scsynth_port, @msg_queue, @state, @register_cue_event_lambda, @current_spider_time
message "Initialised SuperCollider Audio Server #{@server.version}"
end
https://patreon.com/samaaron
use_osc_logging true
It will dump error messages into the Sonic Pi log scsynth.log in ~/.sonic_pi/logs`
*** ERROR: SynthDef sonic-pi-mysecondsynth not found
FAILURE IN SERVER /s_new SynthDef not found
FAILURE IN SERVER /n_set Node 10 not found
FAILURE IN SERVER /n_set Node 10 not found
FAILURE IN SERVER /n_set Node 10 not found
FAILURE IN SERVER /n_set Node 10 not found
We can now sprinkle the code with log calls and try and figure out how the server works.
Using Logger is pretty straightforward, you need to load the library into the module, create a new logger with a fully
qualified filename to log to and write a log statement:
require 'logger'
...
logger = Logger.new("/home/gordon/Dev/tmp/sonic_pi.log")
logger.debug("normalising synth args")
I am telling Logger to use a file in my home directory, you need to get it write it to wherever suits you. The file must
already exist and the path must be fully qualified so no ../.. or ~ s.
Using a combination of the logging techniques in the previous section we can soon find out roughly how it works.
def __load_buffer(id)
id = id.to_s
raise "Aborting load: file name is blank" if id.empty?
path = File.expand_path("#{Paths.project_path}/#{id}.spi")
s = "# Welcome to Sonic Pi\n\n"
if File.exist? path
s = IO.read(path)
end
__replace_buffer(id, s)
end
That function __load_buffer is called in one place and one place only, by the spider server.
We know that the spider-server is special because it sits in app/bin and not app/server/ruby/lib/sonicpi like
the rest of the ruby code.
Spider has its own log in ~/.sonic-pi/logs and if we look at them we can figure out what’s going on:
It co-ordinates the dance with the Tau server that handles timing and events and the SuperCollider server which
actually makes the sounds.
Somewhere in here it starts up the sound module that actually starts the studio .
If we examine the initialize method of the class Studio and match what happens we can see how the synth
definitions are loaded:
def initialize(ports, msg_queue, state, register_cue_event_lambda, current_spider_time_lambda)
@state = state
@scsynth_port = ports[:scsynth_port]
@scsynth_send_port = ports[:scsynth_send_port]
@msg_queue = msg_queue
@error_occured_mutex = Mutex.new
@error_occurred_since_last_check = false
@sample_sem = Mutex.new
@reboot_mutex = Mutex.new
@rebooting = false
@cent_tuning = 0
@sample_format = "int16"
@paused = false
@register_cue_event_lambda = register_cue_event_lambda
@current_spider_time_lambda = current_spider_time_lambda
@global_timewarp = 0
init_scsynth
reset_server
init_studio
end
If we trace down the last three function invocations and match their log messages to those in the spider.log we
can see this function setting everything up for the user - after the SuperCollider and Tau engines have both started.
The last function to run on creating a new Studio is init_studio and it loads the synthdefs:
def init_studio
@server.load_synthdefs(Paths.synthdef_path)
@amp = [0.0, 1.0]
@server.add_event_handler("/sonic-pi/amp", "/sonic-pi/amp") do |payload|
@amp = [payload[2], payload[3]]
end
Actually it tells the server to load them: which it does by sending an OSC message to SuperCollider:
def load_synthdefs(path)
info "Loading synthdefs from path: #{path}" if @debug_mode
with_done_sync [@osc_path_d_loaddir] do
osc @osc_path_d_loaddir, path.to_s
end
end
The format of the message is an instruction to load code and a filepath - so at this stage Sonic Pi doesn’t know
anything more about the built in synthesisers other than their location etc/synthdefs/compiled .
use_synth :beep
play chord(:E3, :minor) , pan: -0.3, george: 44
The function use_synth sets the name of the synth into a shared memory area where it can be used later.
Then the function play is called - it does some preparatory work on setting up the call to the synthesisers - in
particular taking an unnamed first value n passed in that isn’t a hash of any sort and tagging it as {note: n} .
It then calls the function synth. If the option to use external synths isn’t checked and the synth isn’t a built-in one it
will crash out with an error here. If no synthesiser is specified in this call (which in our example there won’t be) then
the synth name is taken from the thread-shared storage that use_synth popped it into.
synth does a call to Synths::SynthInfo.get_info(sn_sym) to pick up the information about the synth - this will be
used later on.
This is the critical part for the difference between handling built-in synths and user-defined ones . If the call to
get_info returns nil then Sonic Pi knows that its not a built-in synth and will simply not try and use the validation
that comes with built-in synths.
In Sonic PI V5.0.0 Tech Preview 2 code for built in synths is extended over a base class called BaseInfo in the file
synthinfo.rb .
The class has a whole range of functions which must be overwritten in implementing a new synth. Some refer to the
lifetime of the synth like initialize , on_start and on_finish , some are invoked at runtime like munge_opts and
some relate to how the synth presents to Sonic Pi like arg_doc and introduced .
The functions in synthinfo.rb and its role in defining the behaviour of Sonic Pi will be covered extensively in
Chapter 4 - the world of built-in synths.
synth takes the arguments passed in and call the external utility function resolve_synth_opts_hash_or_array which
does the first munge - it looks at the data structure that is passed in and checks it is an object that it can use, or it
needs to be sanitised elsewhere. If this function is called with an SPVector it is sent off to
merge_synth_arg_maps_array to fix up.
Next synth checks if the note is a rest note - and if it does it returns nothing.
Now we start getting to where built-in and user-defined synths are treated differently.
synth checks if the synth info is nil - if it isn’t it then knows that this is a built-in synth and is well behaved.
There are a number of global settings that can be applied to code blocks to change the notes being played:
use_cent_tuning / with_cent_tuning
use_octave / with_octave
use_transpose / with_transpose
If the synth is well behaved these global settings will be applied in normalise_transpose_and_tune_note_from_args.
Earlier we looked at error messages and saw that you could play chords with built-in synths but not with user-
defined ones.
Playing chords requires a transform which is only done for built-in functions.
A call to a built-in synth may (if it is a chord) be passed onto the function trigger_chord but if it is a user-defined one
it will always be passed to trigger_inst.
Nota Bene/Take Note: the function trigger_chord DOESN’T call the synth in SuperCollider and pass it a chord - it
asks SuperCollider to play each note separately.
It does some housekeeping - including calling normalise_and_resolve_synth_args - to make sure that SuperCollider
behaves well - the synth that plays each note is grouped, the volume of each note is normalised - the volume of
each note is divided by the number of the notes so that the chord as a whole sounds as loud as the specified
volume.
Let’s look at how Sonic Pi handles synth arguments in some more details. Here is the function:
This block handles the use of synth defaults and we can see if it we run code like this in Sonic Pi:
play 50
In this case the synth defaults are stashed and retrieved by the call to get the
:sonic_pi_mod_sound_synth_defaults setting t_l_args to (map amp: 0.5, pan: -1) .
The function normalise_args! later on turns options like bpm_scale which take true or false as options into
numerical arguments - so 1.0 and 0.0 .
when we log the transform we see that a sustain has been added by the function calculate_sustain! :
synth :beep, {note: 44.0, pan: -0.3, george: 44, sustain: 0.3}
at the end of normalise_and_resolve_synth_args all the user supplied arguments have been tidied up and made
coherent.
If we try the same thing with our custom synth we see that these transforms have also been made:
use_synth :myfirstsynth
play note: 44, duration: 0.3, pan: -0.3, george: 44
is transformed to:
synth :myfirstsynth, {note: 44.0, pan: -0.3, george: 44, sustain: 0.3}
This function actually makes the sound happen - but before it does that it does validation of the arguments in
validate_if_necessary!
This call to validate_if_necessary! is the end of our deep dive. This function takes the current synth object from
all the way back up in the call to play and asks it to validate itself.
If the synth is built-in, it calls its validator function and borks if the parameters are invalid. If the synth is user-
defined there is no validator and the parameters are sent across to SuperCollider as-is.
What you need and don’t need to know to write your own synth
You don’t need to know anything of this chapter to write your own well-behaved synthesiser - to write a badly-
behaved one, this spelunk should get you started.
Philosophy
One of the working philosophies of Sonic Pi is that the tech shouldn’t get in the way of experimentation.
All the built-in synths share common parameters - some have additional parameters. The idea is that if you have
some running code (or are live coding) and you swap out one synth for another then bad things SHOULDN’T happen
- it should behave much as you expected and not in a surprising way.
BaseInfo
All synthesiser objects inherit from the base class BaseInfo mostly by the chain of indirection: SonicPiSynth <
SynthInfo < BaseInfo .
But looking at all the synthesiser classes we see a simple pattern - synths come in families and often descend from
a common base class.
Note that sometimes synth names are just aliases for each other sine / beep and mod_sine / mod_beep . This
aliasing happens in the global variable @@synth_info.
To be recognised as a synth you need to be added to the global variable @@synth_infos in synthinfo.rb .
We can tell what functions are designed to be implemented in the sub-classes by looking for base class members
that will blow up if they are not. Examining the code we see:
def doc
"Please write documentation!"
end
def arg_defaults
raise "please implement arg_defaults for #{self.class}"
end
def name
raise "please implement name for synth info: #{self.class}"
end
def category
raise "please implement category for synth info: #{self.class}"
end
def prefix
""
end
def synth_name
raise "Please implement synth_name for #{self.class}"
end
def introduced
raise "please implement introduced version for synth info: #{self.class}"
end
def trigger_with_logical_clock?
raise "please implement trigger_with_logical_clock? for synth info: #{self.class}"
end
There is an extra function specific_arg_info which isn’t in this list and by default which returns empty hashes.
function doc
This function is called when the displaying the GUI. If we go to the doc function for the beeb synth and edit it to
add the word yowza:
def doc
"A simple pure sine wave, yowza. The sine wave is the simplest, purest sound there is and is the fund
end
This function is called during the compile process (not at run time) and is used to generate the entry about the
synth in the GUI.
function arg_defaults
{
:note => 52,
:note_slide => 0,
:note_slide_shape => 1,
:note_slide_curve => 0,
:amp => 1,
:amp_slide => 0,
:amp_slide_shape => 1,
:amp_slide_curve => 0,
:pan => 0,
:pan_slide => 0,
:pan_slide_shape => 1,
:pan_slide_curve => 0,
:attack => 0,
:decay => 0,
:sustain => 0,
:release => 1,
:attack_level => 1,
:decay_level => :sustain_level,
:sustain_level => 1,
:env_curve => 2
}
end
It is simply the list of all the arguments and their default values - note how the values are chained - the default value
of :decay_level is defined as :sustain_level . (The chaining is only 1 level deep - you can chain a variable to the
value of another one, but that one needs an actual value.)
Oftentimes this function is shared between multiple synths by use of an intermediate class. See later on where the
following functions inherit their arguments from the Noise synth:
BNoise
ChipNoise
CNoise
GNoise
PNoise
If you are writing a family of synths you should consider this strategy.
The values here show one side of the story - but the function default_arg_info contains another:
def default_arg_info
{
:note =>
{
:doc => "Note to play. Either a MIDI number or a symbol representing a note. For example: `30`, `
:validations => [v_positive(:note)],
:modulatable => true
},
:note_slide =>
{
:doc => "Amount of time (in beats) for the note to change. A long slide value means that the note
:validations => [v_positive(:note_slide)],
:modulatable => true,
:bpm_scale => true
},
...
This function contains a big set of standard, well-named common parameters. Different synthesisers support
different subsets (and different families like the detuned ones or the mod ones or the pulse ones) support similar
sets of parameters.
note
note_slide
note_slide_shape
note_slide_curve
amp
amp_slide
pan
pan_slide
attack
decay
sustain
release
attack_level
decay_level
sustain_level
env_curve
cuttoff
cuttoff_slide
detune
detune_slide
mod_phase
mod_phase_offset
mod_phase_slide
mod_range
mod_range_slide
res
res_slide
pulse_width
pulse_width_slide
mod_pulse_width
mod_pulse_width_slide
mod_wave
mod_invert_wave
Using these parameters (where appropriate) with these names and the default validations in them will determine if
your synthesiser feels like a well behaved Sonic Pi synthesiser.
To understand that better you will need to study the synthesiser definitions in Sonic Pi and figure out which synth
uses which parameter and then dig in and see how it is defined in the synthdefs.
Some, of course, will be in Overtone and you will have to reverse engineer the underlying SuperCollider form.
function name
This is the name of the synth as it appears in the GUI - the name you use in code is defined in the function
synth_name .
Both of these are preset for you during the class inheritance chain: MySynth > SynthInfo > BaseInfo
def prefix
"sonic-pi-"
end
end
This code base is used to support all three - but the dip into SynthInfo makes a synth a synth and your class
invoked wherever synths are in play.
function synth_name
This is the name of the synth as used in Sonic Pi code - all lowercase and spaces replaced with _ s.
function trigger_with_logical_clock
This function is used by FXs and not synths - so don’t worry about it.
function specific_arg_info
The function specific_arg_info lets you do validation on arguments that you have added to your synth that aren’t
part of the set that was discussed in the section arg_defaults - they take the same format.
Bnoise Noise
Cnoise Noise
Dtri Dsaw
Gnoise Noise
alias for
Mod Beep Yes
ModSine
Mod Fm FM Yes
Pnoise Noise
Tri Pulse
The base class broadly defines a well-behaved Sonic Pi synth, particularly in the function default_arg_info which
defines a complete set of arguments most built-in synthesisers accept.
def introduced
Version.new(2,0,0)
end
def synth_name
"beep"
end
def doc
"A simple pure sine wave. The sine wave is the simplest, purest sound there is and is the fundamental bui
end
def arg_defaults
{
:note => 52,
:note_slide => 0,
:note_slide_shape => 1,
:note_slide_curve => 0,
:amp => 1,
:amp_slide => 0,
:amp_slide_shape => 1,
:amp_slide_curve => 0,
:pan => 0,
:pan_slide => 0,
:pan_slide_shape => 1,
:pan_slide_curve => 0,
:attack => 0,
:decay => 0,
:sustain => 0,
:release => 1,
:attack_level => 1,
:decay_level => :sustain_level,
:sustain_level => 1,
:env_curve => 2
}
end
end
To make our reimplementation a first-class Sonic Pi synth we will create a new Class for it, inheriting from
SonicPiSynth and implement the following functions:
name
arg_defaults
introduced
synth_name
doc
(without-namespace-in-synthdef
(defsynth sonic-pi-beep [note 52
note_slide 0
note_slide_shape 1
note_slide_curve 0
amp 1
amp_slide 0
amp_slide_shape 1
amp_slide_curve 0
pan 0
pan_slide 0
pan_slide_shape 1
pan_slide_curve 0
attack 0
decay 0
sustain 0
release 1
attack_level 1
decay_level -1
sustain_level 1
env_curve 1
out_bus 0]
(let [decay_level (select:kr (= -1 decay_level) [decay_level sustain_level])
note (varlag note note_slide note_slide_curve note_slide_shape)
amp (varlag amp amp_slide amp_slide_curve amp_slide_shape)
amp-fudge 1
pan (varlag pan pan_slide pan_slide_curve pan_slide_shape)
freq (midicps note)
snd (sin-osc freq)
env (env-gen:kr (core/shaped-adsr attack decay sustain release attack_level decay_level
(out out_bus (pan2 (* amp-fudge env snd) pan amp))))
So there are some things we can see that overlap with the Overtone description.
In both the source of sound is a Sine Oscillator the uGen SinOsc , and the sound is patched to the speakers using
the Out uGen.
Our synth uses a doneAction: 2 and Overtone has an :action: FREE to destroy the synth and free up its
resource.
Our synth has use the Line uGen whereas the Overtone one uses EnvGen - the fact that our Line uGen is bound
to a variable called envelope does give the game away a bit here - our synth plays a constant volume, but beep has
an envelope with attack , decay , sustain and release .
There’s some weird stuff tho. The Overtone definition has all the default arguments from the function
arg_defaults baked in too, along with an outbus set to 0 (which just means play the sound on the computer). But
a couple are different. In Overtone the env_curve default is 1 and the decay_level is -1 whereas in
arg_defaults both are set to 1 .
There are good reasons that default values are different in the Sonic Pi ruby and the SuperCollider code - read the
example synth code in Chapter5 carefully to understand this
Each of these synths will be implemented in both Sonic Pi and SuperCollider so they will have working error
checking and can play chords.
The synths in this manual are all written in literate SuperCollider - the code that generates the page you are reading
is runnable in SuperCollider.
The Ruby parts you need to copy into Sonic Pi are also written in literate Ruby - so runnable Ruby code you can copy
and paste.
{SinOsc.ar(440, 0, 0.2)}.play;
(f = {SinOsc.ar(440, 0, 0.2)};
f.play;)
Similarly when we define Synths using SynthDef we have saved them as a compiled thing with:
(SynthDef("somesynth",{
...
super collider code
...
}).writeDefFile("/Users/gordonguthrie/.synthdefs"))
We can swap out the writeDefFile method with the add method like so:
(SynthDef("somesynth",{
...
super collider code
...
}).add;)
a = Synth.new("somesynth");
(This is what happens when you use the play command in Sonic Pi.)
And we can control it. First invoke the synth with a long duration:
a.set("note", 75);
a.set("amp", 0.5);
Unsurprisingly set is what Sonic Pi uses under the covers to control SuperCollider synths.
f={SinOsc.ar(440, 0, 0.2)};
f.play;
The name we chose was f . Try and replace that with a better name like my_synth .
s=Server.local;
s.boot;
s.quit;
Compile errors
There are a number of common errors you might see.
If you see this, you are trying to do things in the SuperCollider REPL but haven’t started the server. You can use the
menu command Server -> Boot Server to do it.
Or conventionally you can set the variable s to the server and invoke methods on it:
=Server.local;
s.boot;
s.quit;
You didn’t edit the file name in the synth def and it can’t save the compiled output
}).writeDefFile("/Users/gordonguthrie/.synthdefs"))
uGens
The code that we write for SuperCollider seems familiar, it seems like normal computer code, but it’s not.
a=3;
We can ask what value does a have and the answer is three.
b={SinOsc(440 0, 0.2)};
What value does b have? Well it fluctuates between -1 and 1 440 times a second. b is not a variable like in
javascript or c which holds discrete values, it holds a signal.
Channels
Channels are just the software equivalent of the cables that you wire your stereo up with.
Maybe you have a fancy hifi setup on your flat screen at home with 5 speakers:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ │ │ │ │ │ │ │ │ │
│ Left Tweeter │ │ Left Woofer │ │ Sub Base │ │ Right Woofer │ │ Right Tweeter │
│ │ │ │ │ │ │ │ │ │
└────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘
You
If you want SuperCollider to use this you would need to have it generate 5 Channels of output:
╔════════════════╗
║ ║
║ Supercollider ║
║ ║
╚════════════════╝
│
│
│
│
│
│
┌───────────────────┬──────────────────┼───────────────────┬──────────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
Channel 2 Channel 0 Channel 4 Channel 1 Channel 3
│ │ │ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ │ │ │ │ │ │ │ │ │
│ Left Tweeter │ │ Left Woofer │ │ Sub Base │ │ Right Woofer │ │ Right Tweeter │
│ │ │ │ │ │ │ │ │ │
└────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘
You
Obviously SuperCollider doesn’t know your setup - you have to tell it, so it organises outputs using a simple
approach and lets you handle how you want to wire up its outputs to a physical system.
┌────────────┐
│ │
Input: 440 ────▶│ SinOsc │────▶ Output: sine wave at 440Hz on Channel 0
│ │
└────────────┘
So what happens when we call that from Sonic Pi? Sonic Pi assumes you are on a computer and you have 2
speakers only, a left and a right, either on your computer, on your desktop or in your headphones or earphones. So
when we play our simple synth in Sonic Pi, we are making a noise in a setup like this:
╔════════════════╗
║ ║
║ Sonic Pi ║
║ ║
╚════════════════╝
│
▼
╔════════════════╗
║ ║
║ Supercollider ║
║ ║
╚════════════════╝
│
│
│
┌─────────────────┴────────────────────┐
│ │
Channel 0 Channel 1
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ │ │ │
│ Left │ │ Right │
│ │ │ │
└────────────────┘ └────────────────┘
You
This is why our basic synth makes a sound in the left hand speaker only.
We have seen that the SinOsc UGen takes a frequency parameter and outputs on a single channel. Well if instead
of one value we accept an array of frequencies we will get an array of channels:
Because SuperCollider can’t know your setup it just numbers the channels from 0. Its up to you to organise sending
the signals to where they want to go.
So what happens if I have a 2 speaker setup, with Channel 0 on the left and Channel 1 on the right like in Sonic Pi?
What happens to the noise on Channels 2 and up? They plop out of the back of your computer and make a little pile
of discarded noise on the table.
Channel 4─ ─ ┘
Great, so now if we get our SinOsc to play a chord, we can mix all the beeps together and actually hear them all, as
a single thing. But it’s still in the left speaker only. Maybe we want it on the left, maybe on the right, maybe in the
middle. (Now this is not how a well behaved Sonic Pi synth plays a chord - it sends each note to a clone of the same
synth. In building our beep clone we won’t be using Mix but it make sense for you to know the context in which
Pan operates.)
╔════════════╗
║ ║ ┌──▶Channel 0
Channel 0─────▶║ Pan ║───┤
║ ║ └──▶Channel 1
╚════════════╝
Previously in the chapter on mixing and panning we saw that we can create many channels of sound - to get stereo
we create two - and these channels need to be connected to our physical speakers.
A bus is just a collection of channels - and by default the SuperCollider bus 0 is connected to the default output
channels of the computer (as defined in the settings). So either the computer built in speakers, or your headphone
or your bluetooth bar and boom box. Which is what you would expect - we want noises to be played.
The question then is: why do we pass in the bus no, why don’t we just hard code it? And the answer is FXs.
╔════════════════╗
║ ║
║ Sonic Pi ║
║ ║
╚════════════════╝
│
▼
╔════════════════╗
║ Supercollider ║
║ Synth ║
║ ║
╚════════════════╝
│
Bus 0
┌──────────┴──────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ │ │ │
│ Left Speaker │ │ Right Speaker │
│ │ │ │
└────────────────┘ └────────────────┘
Sonic Pi passes in an out_bus value of 0 and jobs a good ‘un.
When you wrap a synth (or sample) in FXs Sonic Pi needs to disconnect the synth from your speakers, patch it into
the FX or FXs and then patch it back - and busses are how it does it:
with_fx :reverb do
play 60, out_bus: 3
end
Sonic Pi creates a new Reverb FX with an input on bus 1 and out an output on bus 0 and then calls the synth
passing in a value of 1 as the out_bas parameter:
╔════════════════╗
║ ║
║ Sonic Pi ║
║ ║
╚════════════════╝
│
│
▼
╔════════════════╗
║ Supercollider ║
║ Synth ║
║ ║
╚════════════════╝
│
Bus 1
▼
╔════════════════╗
║ ║
║Supercollider FX║
║ ║
╚════════════════╝
│
Bus 0
┌──────────┴──────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ │ │ │
│ Left Speaker │ │ Right Speaker │
│ │ │ │
└────────────────┘ └────────────────┘
freq = midicps(note);
get a beep from the Sin Oscillator and then place it in the Pan and set the volume
play
Out.ar(out_bus, snd)
}).writeDefFile("/home/gordon/.synthdefs"))
def introduced
Version.new(5,0,0)
end
def synth_name
"mysecondsynth"
end
def doc
"my sine wave"
end
def arg_defaults
{
:note => 52,
:amp => 1,
:pan => 0,
:duration => 1, # WATCH OUT - this is not called `sustain`
}
end
end
# We also need to add this line to the `BaseInfo` class under the variable name `@@synth_infos`
We can tell the class has loaded correctly tho. The default validations are being applied as we can see if we invoke
the synth setting pan to an invalid value:
load_synthdefs "/home/gordon/.synthdefs"
use_synth(:mysecondsynth)
If we do a full recompile we can now see that the GUI has been updated with our new synth:
load_synthdefs "/home/gordon/.synthdefs"
use_synth(:mysecondsynth)
play 65
sleep 1
print(s)
sleep 1
sleep 1
sleep 1
We have to use load_synthdefs unless we actually save the synthdef to the correct boot directory - which is
etc/synthdefs/compiled
In the default parameter munging process the duration has been turned into a sustain - this is going to be a well-
behaved Sonic Pi synth.
BEWARE: The behaviour of the synthesiser will change between loading it as an external synth and compiling in
support for it in ruby.
(SynthDef('sonic-pi-mythirdsynth', {| out_bus = 0,
note = 52.0, note_slide = 0, note_slide_shape = 1, note_slide_curve = 0,
pan = 0, pan_slide = 0, pan_slide_shape = 1, pan_slide_curve = 0,
amp = 1, amp_slide = 0, amp_slide_shape = 1, amp_slide_curve = 0,
sustain = 1|
get a beep from the Sin Oscillator and then place it in the Pan and set the volume
play
Out.ar(out_bus, snd)
}).writeDefFile("/home/gordon/.synthdefs"))
def introduced
Version.new(5,0,0)
end
def synth_name
"mythirdsynth"
end
def doc
"my sine wave improved"
end
def arg_defaults
{
:note => 52,
:note_slide => 0,
:note_slide_shape => 1,
:note_slide_curve => 0,
:amp => 1,
:amp_slide => 0,
:amp_slide_shape => 1,
:amp_slide_curve => 0,
:pan => 0,
:pan_slide => 0,
:pan_slide_shape => 1,
:pan_slide_curve => 0,
:sustain => 1 # WATCH OUT! - see the 2nd synth for why this isn't `duration`
}
end
end
# We also need to add this line to the `BaseInfo` class under the variable name `@@synth_infos`
use_synth(:mythirdsynth)
use_osc_logging true
play 65
sleep 1
s = play 69, duration: 12, note_slide: 0.2, note_slide_curve: 0.4, amp_slide: 0.4, amp_slide_shape: 4, pan_sli
print(s)
sleep 1
sleep 1
sleep 1
sleep 1
If you get errors when you do this, go back and read the section on running Version 2 for tips.
Just like in version 2 we have to use load_synthdefs unless we actually save the synthdef to the correct boot
directory - which is etc/synthdefs/compiled
(SynthDef("sonic-pi-myfourthsynth", {| out_bus = 0,
note = 52.0, note_slide = 0, note_slide_shape = 1, note_slide_curve = 0,
amp = 1, amp_slide = 0, amp_slide_shape = 1, amp_slide_curve = 0,
pan = 0, pan_slide = 0, pan_slide_shape = 1, pan_slide_curve = 0,
attack = 0, decay = 0, sustain = 0, release = 1,
attack_level = 1, decay_level = -1, sustain_level = 1,
env_curve = 1 |
use the Select uGen and the invalid default value to make the decay_level be the same as the sustain_level if
no decay_level is set if the decay_level is less than zero (i.e., the default -1) - use the zeroth member of the
index, itself otherwise use the first member - the sustain_level
Create an envelope with the full attack, decay, sustain, release shape NOTE: there are 5 levels, but only 4 times and
curves - the levels are the start and end of each section the times and curves are the section itself - in this synth the
curve is the same for all elements
freq = midicps(slid_note);
get a beep from the Sin Oscillator and then place it in the Pan and set the volume
play
Out.ar(out_bus, snd)
}).writeDefFile("/home/gordon/.synthdefs"))
def introduced
Version.new(5,0,0)
end
def synth_name
"myfourthsynth"
end
def doc
"my sine wave improved and finished"
end
def arg_defaults
{
:note => 52,
:note_slide => 0,
:note_slide_shape => 1,
:note_slide_curve => 0,
:amp => 1,
:amp_slide => 0,
:amp_slide_shape => 1,
:amp_slide_curve => 0,
:pan => 0,
:pan_slide => 0,
:pan_slide_shape => 1,
:pan_slide_curve => 0,
:attack => 0,
:decay => 0,
:sustain => 0,
:release => 1,
:attack_level => 1,
:decay_level => :sustain_level,
:sustain_level => 1,
:env_curve => 2
}
end
end
# We also need to add this line to the `BaseInfo` class under the variable name `@@synth_infos`
load_synthdefs "/Users/gordonguthrie/.synthdefs"
use_synth(:myfourthsynth)
play 54, pan: 0.9, amp: 0.4, attack: 1, decay: 0.5, sustain: 3
sleep 0.5
sleep 0.5
If you get errors when you do this, go back and read the section on running Version 2 for tips.
Just like in version have to use load_synthdefs unless we actually save the synthdef to the correct boot directory -
which is etc/synthdefs/compiled
We take a set of audio signals in, we monkey with them based on parameters we pass in and we pass a set of
signals out. Usually the signals are audio signals, but they can be controls.
a=3;
b={SinOsc(440, 0.1, 1)};
Where a is a constant and b is a signal. Reading SuperCollider code it often seems like these are different things,
parameters and streams. In fact a is a constant signal, not a discrete constant. Anywhere you see numbers passed
in, as volume parameters, as panning parameters to place sounds in the stereo field, you can also pass in variable
streams.
Here we have started a synth and passed in a constant stream of 60 as the note value. Then we change the value of
the stream to 65, then 67 and then 72.
We know understand a bit better what that means. audio rate means the stream is fine grained and of high
enough quality that it can be used to generate sounds (think representing an analogue sound wave by a digital
signal that goes up and down in tiny steps - the .ar is 44,100 steps per second.). control rate is a lower quality,
less fine grained signal that is good enough for controls (think turning volumes up or down in big-ish steps - the
.kr is 1/64th of the .ar or 690 steps per second).
.ar signals are 64 times as expensive to calculate than .kr ones. You can use .ar for everything (including
controls) but best practice is not to.
With this understanding of .kr and .ar we see that there are two main uGen configurations - ones for
manipulating sounds:
│
│
.kr
(control rate)
│
▼
┌─────────────────────────────────┐
│ │
.ar │ │ .ar
─────(audio input)────▶│ uGen │────────(audio output)───────▶
│ │
│ │
└─────────────────────────────────┘
(Sometimes you might want to use an audio signal both in sound processing and in controlling another uGen so
don’t obsess about this.)
┌─────────────────────────────────┐
│ │
Signal 0 in───────┐ │ * is a uGen that multiplies │
├──────▶│ two signals together │──────────▶Signal 0 out
Signal 1 in───────┘ │ │
│ │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ │
Signal 0 in───────┐ │ + is a uGen that adds │
├──────▶│ two signals together │──────────▶Signal 0 out
Signal 1 in───────┘ │ │
│ │
└─────────────────────────────────┘
SuperCollider itself has comprehensive documentation which includes a big set of tutorials.
There are lots - of - different - open source - synths written for SuperCollider. These you will have to adapt to work
with Sonic Pi (if you want them to be well behaved).
We can add your synths to this manual too! (see the Next Steps).
Integration
At the moment the loading of a synth and the presentation of it as built in are in two separate places.
The load command doesn’t know or check if the synth description, parameters and validations are loaded. The
runtime looks and treats synths one way if they are built in and a different way if they are not recognised.
The strategic view is to merge these two things - to have a load process that loads both the synthesiser and the
synth description, parameters and validation.
The load process should enable user-defined synths to be well-behaved (i.e. behave like and are subject to the same
possible transforms as built-in ones) or badly-behaved.
well-behaved - have parameter sets aligned with the built in synths so that Sonic Pi is happy to do the standard
transpositions and pitch shifts
badly-behaved - have freaky parameters that might cause Sonic Pi to bork if the user mistypes or switches
synths either live coding or in mucking about, or with odd ways of specifying notes that should not/cannot be
subject to transpositions
experimental
This manual is compiled with the Literate Code Reader escript which means that valid SuperCollider code will
compile into a readable html page.
This means that simply by committing a working SuperCollider synth for Sonic Pi it will be added to the book.