-
Notifications
You must be signed in to change notification settings - Fork 58
Tutorials: Synthesizing a Song
In this tutorial, we’re going to synthesize a song that contains single notes and chords.
You can use the JavaScript library music.js to calculate frequencies for notes. Using this library you can define a note by Latin name and octave and call the frequency method to return the note frequency.
Ex: Note.fromLatin('A4').frequency(); // returns 440
. For simplicity's sake, music.js is not used in this tutorial.
The code in this tutorial is based on David Govea's Duper Mario Synth demo. This awesome demo uses Audiolib.js to synthesize the Super Mario Brother's theme song and manipulate it using effects based on user interaction.
Let’s first define a basic song using JSON. Each note object in our song will have properties for frequency and duration. We will use an array to store all of our notes in sequence.
var simpleSong = [
{
freq: 440, // A4
dur: 1/4
},
{
freq: 329.63, // E4
dur: 1/4
},
{
freq: 440, // A4
dur: 1/4
},
{
freq: 440, // A4
dur: 1/4
}
];
Using an Oscillator instance we can generate a tone for each note. We will check to see if it’s time to load the next note on each sample. We can use the note duration, tempo (bpm), and sample rate to calculate note length in samples.
var theme = simpleSong,
noteCount = 0,
noteTotal = theme.length,
leadNoteLength = 0,
tempo = 120,
notesPerBeat = 4,
dev, sampleRate, lead;
function loadNote(){
var note = theme[noteCount];
// Reset oscillator
lead.frequency = 0;
lead.reset();
// Set oscillator frequency
lead.frequency = note.freq;
// Calculate note length in samples
leadNoteLength = Math.floor(note.dur * sampleRate * 60 * notesPerBeat / tempo);
noteCount += 1;
// Restart song when end is reached
if (noteCount >= theme.length) noteCount = 0;
};
function audioCallback(buffer, channelCount){
var l = buffer.length,
sample, note, n, current;
// loop through each sample in the buffer
for (current=0; current<l; current+= channelCount){
if (leadNoteLength == 0) loadNote();
sample = 0;
// Generate oscillator
lead.generate();
// Get oscillator mix and multiply by .5 to reduce amplitude
sample = lead.getMix()*0.5;
// Fill buffer for each channel
for (n=0; n<channelCount; n++){
buffer[current + n] = sample;
}
leadNoteLength -= 1;
}
};
window.addEventListener('load', function(){
// Create an instance of the AudioDevice class
dev = audioLib.AudioDevice(audioCallback /* callback for the buffer fills */, 2 /* channelCount */);
sampleRate = dev.sampleRate;
// Create an instance of the Oscillator class
lead = audioLib.Oscillator(sampleRate, 440);
}, true);
When you run the above code you will hear two problems. The first problem is we have three quarter notes played in a row at the same frequency and these three notes have blended together into one tone instead of being played as three separate tones. The second problem is the annoying crackling sound heard when the note changes from A4 to E4. This crackling sound is due to discontinuity in the sound wave caused by the note frequency change. We will solve both of these problems in Part 2 by fading in and out each note.
By fading in and out each note we can eliminate the discontinuity in our sound wave caused by frequency changes. To accomplish this we will define a fade point of 300 samples less than the note length. So for the first 300 samples of a note we will fade it in and for the last 300 samples of a note we will fade it out.
var theme = simpleSong,
noteCount = 0,
noteTotal = theme.length,
leadNoteLength = 0,
tempo = 120,
notesPerBeat = 4,
fade = 0,
fadePoint = 0,
dev, sampleRate, lead;
function loadNote(){
var note = theme[noteCount];
// Reset oscillator
lead.frequency = 0;
lead.reset();
// Set oscillator frequency
lead.frequency = note.freq;
// Calculate note length in samples
leadNoteLength = Math.floor(note.dur * sampleRate * 60 * notesPerBeat / tempo);
// reset fade
fade = 0;
// define fade point
fadePoint = leadNoteLength - 300;
noteCount += 1;
// Restart song when end is reached
if (noteCount >= theme.length) noteCount = 0;
};
function audioCallback(buffer, channelCount){
var l = buffer.length,
sample, note, n, current;
// loop through each sample in the buffer
for (current=0; current<l; current+= channelCount){
if (leadNoteLength == 0) loadNote();
// fade in
if (leadNoteLength > fadePoint){
fade = 1 - (leadNoteLength-fadePoint)/300;
// fade out
} else if (leadNoteLength<300){
fade = leadNoteLength/300;
} else {
fade = 1;
}
sample = 0;
// Generate oscillator
lead.generate();
// Get oscillator mix and multiply by .5 to reduce amplitude
sample = lead.getMix()*0.5*fade;
// Fill buffer for each channel
for (n=0; n<channelCount; n++){
buffer[current + n] = sample;
}
leadNoteLength -= 1;
}
};
window.addEventListener('load', function(){
// Create an instance of the AudioDevice class
dev = audioLib.AudioDevice(audioCallback /* callback for the buffer fills */, 2 /* channelCount */);
sampleRate = dev.sampleRate;
// Create an instance of the Oscillator class
lead = audioLib.Oscillator(sampleRate, 440);
}, true);
To play chords we can update our song's JSON format to use an array to store multiple frequencies for each note object.
var simpleSong = [
{
freq: [329.63, 440, 554.37], // E4, A4, C#5
dur: 1/4
},
{
freq: [329.63], // E4
dur: 1/4
},
{
freq: [329.63, 440, 554.37], // E4, A4, C#5
dur: 1/4
},
{
freq: [440], // A4
dur: 1/4
}
];
Then we can iterate through our frequency array for each note when calculating samples. We can use the compressor effect to prevent clipping from occurring with our chords.
var theme = simpleSong,
noteCount = 0,
noteTotal = theme.length,
leadNoteLength = 0,
leadCount = 0,
tempo = 120,
notesPerBeat = 4,
fade = 0,
fadePoint = 0,
dev, sampleRate, lead, comp;
function loadNote(){
var note = theme[noteCount],
l = note.freq.length,
i;
// Reset oscillator
for (i=0; i<leads.length; i++){
leads[i].frequency = 0;
leads[i].reset();
}
// Set oscillator frequencies
for (i=0; i < l; i++){
leads[i].frequency = note.freq[i];
}
leadCount = l;
// Calculate note length in samples
leadNoteLength = Math.floor(note.dur * sampleRate * 60 * notesPerBeat / tempo);
// reset fade
fade = 0;
// define fade point
fadePoint = leadNoteLength - 300;
noteCount += 1;
// Restart song when end is reached
if (noteCount >= theme.length) noteCount = 0;
};
function audioCallback(buffer, channelCount){
var l = buffer.length,
l2 = leads.length,
sample, note, i, n, current;
// loop through each sample in the buffer
for (current=0; current<l; current+= channelCount){
if (leadNoteLength == 0) loadNote();
// fade in
if (leadNoteLength > fadePoint){
fade = 1 - (leadNoteLength-fadePoint)/300;
// fade out
} else if (leadNoteLength<300){
fade = leadNoteLength/300;
} else {
fade = 1;
}
sample = 0;
for (i=0; i<leadCount; i++){
// Generate oscillator
leads[i].generate();
// Get oscillator mix and multiply by .5 to reduce amplitude
sample += leads[i].getMix()*0.5*fade;
}
// Fill buffer for each channel
for (n=0; n<channelCount; n++){
buffer[current + n] = comp.pushSample(sample);
}
leadNoteLength -= 1;
}
};
window.addEventListener('load', function(){
// Create an instance of the AudioDevice class
dev = audioLib.AudioDevice(audioCallback /* callback for the buffer fills */, 2 /* channelCount */);
sampleRate = dev.sampleRate;
// Create an array of Oscillator instances
leads = [
audioLib.Oscillator(sampleRate, 220),
audioLib.Oscillator(sampleRate, 440),
audioLib.Oscillator(sampleRate, 0)
];
// Compressor effect to prevent clipping w/ chords
comp = audioLib.Compressor(sampleRate, 3, 0.5);
}, true);