Programming for Interactive Digital Arts
CS 2, Fall 2023

Sound and Music I

Working with SoundFile

SoundFile and FreeSound

To play audio clips--whole songs, sound loops, or sound clips--we use the loadSound() function, which is part of the p5js sound library.

FreeSound is a great free Web resource to download sound files that you can use in your sketches. Create a free login to search and download sounds. You will want sounds to be in .mp3, .ogg, or .wav format (not .aiff or .aif). The following examples use the following three Roland TR808m drum machine sound samples from FreeSound:

  1. bass drum (bd.wav)
  2. snare drum (sn.wav)
  3. hi hat closed (hhc.wav)

First, we need to upload sounds to our current p5js sketch folder. To do this, expand the file browser pane on the left of the p5js editor, add a folder called "sounds", and upload the three sounds "bd.wav", "sn.wav", and "hhc.wav" into that folder, by selecting the folder's menu item "upload files...".

Once the files are uploaded to your sketch directory sounds folder, then try the following code to interact with the sounds. Clicking on the left of the canvas triggers the bd.wav sound, and the right side of the canvas triggers the sn.wav sound. This sketch contains the basic elements of an interactive drum machine that you can play.

let bd, sn; // global variables for sound files

function preload() { // load the sounds first
  bd = loadSound('sounds/bd.wav');
  sn = loadSound('sounds/sn.wav');
}

function setup() {
  createCanvas(400, 200);
  background(255,0,0)
  fill(0,0,255)
  rect(width/2,0,width/2,height)
  fill(255)
  textSize(24)
  text('BASS', width/6, height/2);
  text('SNARE', 2*width/3, height/2);
}


function mousePressed() {
  if(mouseX < width/2)
    bd.play();
  else
    sn.play();
}

Sequencing with a pulse counter

We can use the p5js draw() function to implement timing. Each frame represents a fixed amount of time, which is a pulse, and the frameRate is the tempo of that pulse in pulses (or beats) per second.
We can use the modulo operator and a pulse counter to play on every 4th pulse and every 4th pulse offset by 2 pulses to create a "back-beat" rhythm:

let bd, sn, pulse=0; // global variables for sound files and pulse counter

function preload() { // load the sounds first
  bd = loadSound('sounds/bd.wav');
  sn = loadSound('sounds/sn.wav');
D}

function setup() {
  createCanvas(400, 400);
  background(220);
  frameRate(4)
}


function draw(){
  if( pulse % 4 == 0 ) // use modulo operator % to test for n-th pulse
    bd.play()    
  if( (pulse-2) % 4 == 0) 
    sn.play()
  pulse++ // increment pulse counter
}

Now that we've got a basic pattern, we can use the same trick to add more detail. Let's double the pulse tempo, but keep the bass and snare the same speed by doubling the modulo comparison number, and adding the hihat at the pulse rate. For variety, we'll also add an extra bass kick on the 15th of every 16th pulses:

let bd, sn, hh, pulse; // global variables for sound files

function preload() { // load the sounds first
  bd = loadSound('sounds/bd.wav');
  sn = loadSound('sounds/sn.wav');
  hh = loadSound('sounds/hhc.wav');
  pulse = 0;
}

function setup() {
  createCanvas(400, 400);
  background(220);
  text('tap here to play', 10, 20);
  frameRate(8)
}


function draw(){
  if(pulse % 8 == 0) bd.play()    
  if(pulse % 8 == 4) sn.play()
  if(pulse % 16 == 15) bd.play()    
  hh.play()    
  pulse++
}

Sequencing with Arrays

The TR808 drum machine was a classic electronic instrument of the 1980s that had a profound impact on pop, hip-hop, and electronica. It featured an array of 16 buttons that indicated when a sound was "on" or "off" with respect to an underlying pulse.

If we want to specify arbitrary patterns for bd, sn, and hh in this way we can use lists of 1s and 0s. The Array datatype in javascript contains a list of comma-separated values enclosed between square brackets, e.g. let myArray = [1,2,4,5,6]. We access the values of the array with an index (integer) starting at 0 for the first value, e.g. myArray[0] returns the value 1 and myArray[2] returns the value 4. We are going to use binary arrays to indicate the presence of a sound for each pulse, 1 means play the sound on this pulse and 0 means do not play the sound.

let bd, sn, hh, pulse; // global variables for sound files

let bd_seq = [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,1,1,1,1]
let sn_seq = [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0]
let hh_seq = [0,1,1,1,1,1,1,1,0,1,1,1,1,0,1,1]

function preload() { // load the sounds first
  bd = loadSound('sounds/bd.wav');
  sn = loadSound('sounds/sn.wav');
  hh = loadSound('sounds/hhc.wav');
  pulse = 0;
}

function setup() {
  createCanvas(400, 400);
  background(220);
  text('tap here to play', 10, 20);
  frameRate(8)
}

function draw(){
  if(bd_seq[pulse % bd_seq.length]) bd.play()  
  if(sn_seq[pulse % sn_seq.length]) sn.play()  
  if(hh_seq[pulse % hh_seq.length]) hh.play()  
  pulse++
}

Simple Melodies

To play simple melodies we need a different strategy to play sounds that will allow us to alter their pitch (frequency). Although we can technically do this with SoundFile audio clips, it is easier to start with creating sounds from scratch, using algorithmic methods. p5js provies a synthesizer class called PolySynth for making pitched notes.

Here we use random selection from a list of note names ['F4','G4'] to play two different pitches with fixed duration within a playSynth() callback function added to the canvas' mousePressed listeners...

let synth;

function setup() {
  createCanvas(100, 100);
  background(220);
  textAlign(CENTER);
  text('tap to play', width/2, height/2);
  synth = new p5.PolySynth();
  userStartAudio();
}

function mousePressed() {
  let note = random(['F4', 'G4']);
  // note velocity (volume, from 0 to 1)
  let velocity = random();
  // time from now (in seconds)
  let time = 0;
  // note duration (in seconds)
  let dur = 1/6;

  synth.play(note, velocity, time, dur);
}
// Simple Melody
// CS2 - Creative Code
// Prof. Michael Casey

let synth;
let seq = ["G4", "G4", "C4","D4","E4","F4","G4","G4","C4","C4","C4","C4",
           "A5","A5","F4","G4","A5","B5","C5","C5","C4","C4","C4","C4"] // A
let beat = 0

function setup() {
  frameRate(4); // Set the tempo to 4 beats per second
  synth = new p5.PolySynth(); // create the synthesizer
}

function draw() {
  let note = seq[ beat ];
  synth.play(note, 90, 0.1, 0.2);
  beat = beat + 1;
  if(beat==seq.length)
    beat = 0;
}

Finally, we can make polyphonic music (more than one part at a time) by combining two synths and having multiple note lists:

// Simple Melody:  Minuet in G, J.S. Bach
// CS2 - Creative Code
// Prof. Michael Casey

let polySynth;
let referencePitch = 60 // Middle C
let majorScale = [0, 2, 4, 5, 7, 9, 11] // Major Scale (half steps)

// RIGHT HAND (MELODY)
let a = [4,4,0,1,2,3,4,4,0,0,0,0,5,5,3,4,5,6,7,7,0,0,0,0] // phrase a (scale degrees)
let b = [3,3,4,3,2,1,2,2,3,2,1,0,-1,-1,0,1,2,0,1,1,-3,-3,-3,-3] // phrase b
let c = [4,4,0,1,2,3,4,4,0,0,0,0,5,5,3,4,5,6,7,7,0,0,0,0] // phrase c
let d = [3,3,4,3,2,1,2,2,3,2,1,0,1,1,2,1,0,-1,0,0,0,0,0,0] // phrase d
let melSeq = a.concat(b,c,d) // join phrases to make structure: a b c d (phrases)

// LEFT HAND (BASS)
let e = [0,0,0,0,1,1,2,2,2,2,2,2,3,3,3,3,3,3,2,2,1,1,0,0] // phrase e (scale degrees)
let f = [1,1,1,1,1,1,0,0,0,0,0,0,4,4,2,2,0,0,4,4,-3,3,2,1] // phrase f
let g = [2,2,2,2,1,1,0,0,2,2,0,0,3,3,3,3,3,3,2,2,3,2,1,0] // phrase g
let h = [1,1,1,1,-1,-1,0,0,0,0,2,2,3,3,4,4,-3,-3,0,0,0,0,-7,-7] // phrase h
let bassSeq = e.concat(f,g,h) // join phrases to make structure: e f g h (phrases)

function setup() {
  frameRate(4); // Set the tempo to 4 beats per second
  synth1 = new p5.PolySynth(); // create the synthesizer
  synth2 = new p5.PolySynth(); // create the synthesizer
  synth1.setADSR(0.05,0.01,0.75,0.1)
  synth2.setADSR(0.1,0.05,0.75,0.1)
  userStartAudio();
}

function getFreq(seq, scl){
    let degree = seq[ (frameCount-1) % seq.length ];
    let pitch = 12*floor(degree/7) + scl[(7 + degree) % scl.length];
    return midiToFreq(referencePitch + pitch);  
}

function draw() {
  if(frameCount\< melSeq.length){  
    let vel=0.25, lat=0.1, dur=0.2
    synth1.play(getFreq(melSeq, majorScale), vel, lat, dur);
    synth2.play(getFreq(bassSeq, majorScale)/2, vel, lat, dur);
  }
}