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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>p5.js vers 0.5.12, Edit index.html to Change This Title</title> <script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/addons/p5.dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/addons/p5.sound.js"></script> <script src="sketch.js" type="text/javascript"></script> </head> <body> </body> </html> |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>p5.js vers 0.5.12, Edit index.html to Change This Title</title> <script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/addons/p5.dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.12/addons/p5.sound.js"></script> <script src="mybigproject.js" type="text/javascript"></script> <script src="drawingfunctions.js" type="text/javascript"></script> </head> <body> </body> </html> |
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()
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var osc; function setup() { createCanvas(400, 400); useSound(); } function soundSetup() { osc = new p5.Oscillator(); osc.setType('sine'); osc.freq(660); osc.amp(0.0); osc.start(); } function draw() { background(200); if (mouseIsPressed) { osc.amp(0.1); } else { osc.amp(0.0); } } |
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.
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var osc, env; function setup() { createCanvas(400, 400); useSound(); } function soundSetup() { osc = new p5.Oscillator(); osc.setType('sawtooth'); osc.freq(220); env = new p5.Env(); env.setADSR(0.01, 1.2, 0.3, 0.2); osc.amp(env); // envelope controls the oscillator amplitude (loudness) osc.start(); } function draw() { background(200); } function mousePressed() { env.triggerAttack(); // note on - play attack to sustain } function mouseReleased() { env.triggerRelease(); // play the release part - turn note off } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
var osc, env, vib; function setup() { createCanvas(400, 400); useSound(); } function soundSetup() { // 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 osc.start(); } function draw() { background(200); } function mousePressed() { env.triggerAttack(); // note on - play attack to sustain } function mouseReleased() { env.triggerRelease(); // play the release part - turn note off } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
var osc, env, vib, filt, fenv; function setup() { createCanvas(400, 400); useSound(); } function soundSetup() { // 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); } function mousePressed() { env.triggerAttack(); // note on - play attack to sustain fenv.triggerAttack(); } function mouseReleased() { env.triggerRelease(); // play the release part - turn note off fenv.triggerRelease(); } |
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);
useSound();
}
function soundSetup() {
// 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;
}