Week 12

Review

See code with object examples posted on Piazza.

Using Multiple Source Code Files

Larger programs are usually written using multiple files. Even p5.js complete libraries come in three files (which is why we have “min” and “all” templates).

To use multiple files, you need to modify index.html to include the additional files. Currently, your index.html looks like this:

To include multiple source files, you only need to insert additional script lines in your index.html. The following does not use sketch.js, but instead runs two files — mybigproject.js and drawingfunctions.js:

The resulting program will consist of all the top-level (global) function and variable definitions. The preload, setup, and draw functions will be called as usual, regardless of where they are defined.

Music Synthesis

Beginning in the early 20th C, “analog computers” were invented to perform simulation of physical systems. These “computers” often consisted of modules that could be interconnected to simulate different systems. Based on this approach, some early sound “synthesizers,” especially by Donald Buchla and Robert Moog in the early 1960’s, introduced a variety of modular sound generators and processors that could be interconnected to construct and shape sounds in a flexible way.

Even prior to this, in 1957, Max Mathews created software that also had modules to perform different functions. Nearly every synthesizer since that time either consists of multiple modules or is explained as if there are multiple modules even when flexible (re)configuration is not possible.

p5.js includes objects for sound processing that follows this tradition.

Basic Tone Generation with p5.Oscillator

The most basic sound generator is the oscillator, which generates a tone. You can control the pitch and amplitude of the tone, and also the “wave shape” which determines whether the tone sounds simple or bright and buzzy.

Like all the modules we’ll see, you use new to make a new instance of the module, then call methods to set properties such as freq(uency) (pitch) and amp(litude) (how loud, how strong).

Notice you must call the start() method to start the oscillator. It’s best to leave things running and turn the sound on and off by changing the amplitude (see this in draw()).

Envelopes

Musical tones do not typically start suddenly and end suddenly. The “shape” of the sound is called the “envelope”, which typically starts quickly (but not instantaneously) and tapers off at the end. Carefully controlled/designed shapes help make musical tones expressive. Here, we create an Envelope (Env) object and use it to control the amplitude of an oscillator.

The setADSR method lets you make a simple envelope with control over Attack time, Decay time, Sustain level, and Release time.

ADSR envelope diagram from Wikiaudio.org

Note that the triggerAttack method runs the envelope through to the sustain portion, and triggerRelease finishes the envelope with a release that brings the amplitude back to zero (silence).

Modular Synthesis and Vibrato

This next change illustrates the flexibility of sound synthesis systems based on modules. We want to add vibrato – a wavering frequency effect – to our tone. You might expect vibrato and other effects to be built-in or attributes, but instead, we create vibrato by using another oscillator to construct a low-frequency, sinusoidal “vibrato signal” and send that to the main tone oscillator (osc) where the vibrato signal modulates the frequency of the tone. You could make absurdly wide vibrato, change the vibrato signal from a smooth sinusoidal variation to an angular sawtooth variation, change the amount of vibrato with an envelope, or perform many other variations. Here’s some simple code implementing vibrato:

Filters

We are building a classic “subtractive synthesis” tone, where the basic tone is rich in harmonics, and where a filter can remove harmonics to vary and shape the quality of the sound. This example passes the oscillator output through a filter controlled by another envelope, fenv. This envelope controls the frequencies present in the sound. Higher values correspond to more and higher frequencies passing through the filter.

Sequencing

Playing one note is fun, but to play a melody or complex musical sequence, you need to turn notes on and off, change their frequencies (pitches), etc. p5.js is not particularly suited to this kind of scheduling because the model of p5.js is mainly one of “here’s the state NOW, draw the screen NOW.” You cannot say do this now and do that later, at least not without storing your plans as data and writing code to test the current time against the plan and peform actions at the right time. Even then, frame rates are not too predictable and while 60Hz is a reasonable frame rate for graphics, this makes for very coarse time steps as far as our ears are concerned.

Nevertheless, let’s make some note sequences. Here’s we’ll randomly choose to maybe play a note at every Nth frame.

Some Music Synthesis Silliness

var osc, env, vib, filt, fenv;
var DUR = 5; // how many frames per "beat"
var noteOn = false;

function setup() {
    createCanvas(400, 400);

    // osc is the main tone generator
    osc = new p5.Oscillator();
    osc.setType('sawtooth');
    osc.freq(220);

    // env controls the amplitude shape of osc
    env = new p5.Env();
    env.setADSR(0.01, 1.2, 0.3, 0.2);
    osc.amp(env); // envelope controls the oscillator amplitude (loudness)

    // low-frequency oscillator for vibrato
    vib = new p5.Oscillator();
    vib.setType('sine');
    vib.freq(6);
    vib.amp(5); // controls depth of vibrato
    vib.start();
    osc.freq(vib); // modulate the osc frequency with vibrato

    // a filter to change the timbre or spectral quality
    filt = new p5.LowPass(); // allows sound up to some frequency to pass through
    osc.disconnect(); // stop playing the oscillator directly
    osc.connect(filt); // pass the osc output through the filter

    // filter control envelope
    fenv = new p5.Env();
    fenv.setADSR(0.3, 1.2, 1000, 0.2);
    fenv.setRange(8000, 400);
    filt.freq(fenv);

    osc.start();
}


function draw() {
    background(200);
    text("HIGHER ^",  100, 70);
    text("FASTER ->", 120, 100);
    text("LOWER v",  100, 130);
    var density = constrain(mouseX, 0, width) / width;
    if (frameCount % DUR == 0 & random(0, 1) < density) {
        if (noteOn) {
            noteStop();
        } else {
            noteStart();
        }
    }   
}

function noteStart() {
    var y = constrain(mouseY, 0, height);
    osc.freq(55 * pow(2, int(random(0, 20) + (height - y) / 10) / 12));
    env.triggerAttack(); // note on - play attack to sustain
    fenv.triggerAttack();
    noteOn = true;
}

function noteStop() {
    env.triggerRelease(); // play the release part - turn note off
    fenv.triggerRelease();
    noteOn = false;
}