Skip to content

Tutorials: Synthesizing a Song

GregJ edited this page Sep 21, 2011 · 2 revisions

In this tutorial, we’re going to synthesize a song that contains single notes and chords.

Tips

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.

Part 1 - Calculating Note Duration

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.

Part 2 - Fading in and out Notes

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);

Part 3 - Playing Chords

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);