Skip to the content.

Chapter 3 - Deep dive

How Sonic Pi plays synths

Let’s put some logging in and see what happens when we write a simple synth command:

use_synth :beep
play chord(:E3, :minor) , pan: -0.3, george: 44

use_synth function in sound.rb

The function use_synth sets the name of the synth into a shared memory area where it can be used later.

play function in sound.rb

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}.

synth function in sound.rb

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:

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.

trigger_chord function in sound.rb

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.

Chords as notes

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.

trigger_chord ends up calling trigger_synth.

trigger_inst function in sound.rb

trigger_inst does a little housekeeping - including calling normalise_and_resolve_synth_args and possibly tweaking the slide times in add_arg_slide_times - before moving on to calling trigger_synth.

normalise_and_resolve_synth_args function in sound.rb

Let’s look at how Sonic Pi handles synth arguments in some more details. Here is the function:

      def normalise_and_resolve_synth_args(args_h, info, combine_tls=false)
        purge_nil_vals!(args_h)
        defaults = info ? info.arg_defaults : {}
        if combine_tls
          t_l_args = __thread_locals.get(:sonic_pi_mod_sound_synth_defaults) || {}
          t_l_args.each do |k, v|
            args_h[k] = v unless args_h.has_key? k || v.nil?
          end
        end

This block handles the use of synth defaults and we can see if it we run code like this in Sonic Pi:

use_synth_defaults amp: 0.5, pan: -1

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.

If we pass in a duration by calling this code:

play note: 44, duration: 0.3, pan: -0.3, george: 44

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}

trigger_synth function in sound.rb

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.