Been thinking about how to create a playback function for musical notes connected in my icm / pcomp project. looking at maybe using setTimeout & setInterval (and closure) to play through an array of notes. Some references I’ve been watching/reading:
Coding Train:
P5.js References:
Article: Audio Synthesis in javaScript
Creating Music
First of all, I am not a musician by any stretch of the imagination – that fact will become obvious all too soon – but if you’re going to make sound with code, you wind up either making sound effects or music. So let’s start with music. My goal here was to create a simple “tracker” application. You program in notes, it plays those notes back in progression to form a song – of sorts.
I’m going to keep the song very simple: “Mary Had a Little Lamb.” It’s a song that you can play every note with one beat, so we don’t have to mess around with different note lengths. Here’s a simple version of the song transcribed into notes and rests:
g f e f g g g - f f f - g b b - g f e f g g g g f f g f e – - -
A quick search on the Internet gives you the frequency values for the notes b, e, f and g, which are the only ones we’ll need for this song. Let’s code that into an object:
scale = {
g: 392,
f: 349.23,
e: 329.63,
b: 493.88
}
And then we can just code the song as a string.
song = "gfefgg-fff-gbb-gfefggggffgfe---";
Now we can create an AudioContext
and an oscillator and set an interval that runs at a certain speed. In the interval callback, we get the next note, find its frequency and set the oscillator’s frequency to that value. Like this:
window.onload = function() {
var audio = new window.webkitAudioContext(),
osc = audio.createOscillator(),
position = 0,
scale = {
g: 392,
f: 349.23,
e: 329.63,
b: 493.88
},
song = "gfefgg-fff-gbb-gfefggggffgfe---";
osc.connect(audio.destination);
osc.start(0);
setInterval(play, 1000 / 4);
function play() {
var note = song.charAt(position),
freq = scale[note];
position += 1;
if(position >= song.length) {
position = 0;
}
if(freq) {
osc.frequency.value = freq;
}
}
};
Now this actually works and you should be able to recognize the melody somewhat. But it leaves a lot to be desired. The biggest thing is that there is no separation between notes. You have a single oscillator running and you’re just changing its frequency. This ends up creating a sort of slide between notes rather than distinct notes. And when there’s a rest, well, there is no rest. It just keeps playing the last note.
Articulation Gaps and Rests
There are various ways to try to handle this. One would be to call stop()
on the oscillator, then change its frequency, then call start()
again. But, when you read the documentation, it turns out that these are one time operations on an oscillator. Once we call stop()
, it’s done. That particular oscillator cannot be restarted. So what to do?
The suggested answer is actually to create a new oscillator for each note. Initially, this sounds like a horrible idea. Create and destroy a new object for every single note in the song??? Well, it turns out that it’s not so bad. There are some frameworks that create a sort of object pool of notes in the background and reuse them. But the downside to that is that every note you create and start continues playing even if you can’t hear it. It’s your choice, and I suppose you could do all sorts of profiling to see which is more performant. But for “Mary Had a Little Lamb,” I think we’re safe to create a new oscillator each time.
To do this, make a new function called createOscillator
. This will create an oscillator, specify its frequency and start it. After a given time, it will stop and disconnect that oscillator. We can then get rid of the main osc
variable in the code and call the createOscillator
function when we want to play a note.
window.onload = function() {
var audio = new window.webkitAudioContext(),
position = 0,
scale = {
g: 392,
f: 349.23,
e: 329.63,
b: 493.88
},
song = "gfefgg-fff-gbb-gfefggggffgfe---";
setInterval(play, 1000 / 4);
function createOscillator(freq) {
var osc = audio.createOscillator();
osc.frequency.value = freq;
osc.type = "square";
osc.connect(audio.destination);
osc.start(0);
setTimeout(function() {
osc.stop(0);
osc.disconnect(audio.destination);
}, 1000 / 4)
}
function play() {
var note = song.charAt(position),
freq = scale[note];
position += 1;
if(position >= song.length) {
position = 0;
}
if(freq) {
createOscillator(freq);
}
}
};
This sounds better already. Each note is distinct, and when there is a rest, no note plays. But we can do even better.