A Simple Music Synthesizer in JavaScript

The code included below implements a simple text-to-music synthesizer in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs Javascript. Or, for an online version, visit http://thiscouldbebetter.neocities.org/synthesizer.html.

Note that the code has only been tested in Google’s Chrome web browser, and that the “download” functionality in particular may not work in other browsers.

This code is adapted from a previous post, in which more-or-less the same functionality is implemented in Java.

UPDATE 2016/06/18 – I have updated this code, fixing a few things and cleaning things up a bit. I could probably also stand to update the model I’m using for WAV file samples, but I didn’t.



<html>
<body>

<!-- user interface -->

	<div>
		<label>Song Name:</label>
		<input id="inputSongName" value="DanceOfTheSugarPlumFairies"></input>
	</div>
	<div>
		<div><label>Song Content:</label></div>
		<textarea id="textareaSongAsStrings" cols="80" rows="8">
O:3;R.1;____________R.4;G.8;E.8;G.4;F#4;D#4;E.4;D.8;D.8;D.8
O:1;G.4;B.4;G.4;B.4;G.4;B.4;____G.4;B.4;G.4;B.4;G.4;____B.4
		</textarea>
	</div>
	</div>
		<button id="buttonPlaySong" onclick="buttonPlaySong_Clicked();">Play</button>
		<button id="buttonDownloadSong" onclick="buttonDownloadSong_Clicked();">Download</button>
	</div>


<script type="text/javascript">

// ui events

function buttonPlaySong_Clicked(event)
{
	var songAsBytes = getSongAsBytes();	

	var songAsStringBase64 = Base64Encoder.encodeBytes(songAsBytes);

	var songAsDataURI = "data:audio/wav;base64," + songAsStringBase64;

	var htmlElementSoundSource = document.createElement("source");
	htmlElementSoundSource.src = songAsDataURI;

	var htmlElementAudio = document.createElement("audio");
	htmlElementAudio.autoplay = true;

	htmlElementAudio.appendChild(htmlElementSoundSource);

	document.body.appendChild(htmlElementAudio);
}

function buttonDownloadSong_Clicked(event)
{
	var inputSongName = document.getElementById("inputSongName");
	var songName = inputSongName.value;

	var songAsBytes = getSongAsBytes();

	var songAsArrayBuffer = new ArrayBuffer(songAsBytes.length);
	var songAsUIntArray = new Uint8Array(songAsArrayBuffer);
	for (var i = 0; i < songAsBytes.length; i++) 
	{
		songAsUIntArray[i] = songAsBytes[i];
	}

	var songFileAsBlob = new Blob
	(
		[ songAsArrayBuffer ], 
		{type:"application/type"}
	);

	var downloadLink = document.createElement("a");
	downloadLink.href = window.URL.createObjectURL(songFileAsBlob);
	downloadLink.download = songName + ".wav";
	downloadLink.click();
}

function getSongAsBytes()
{
	var inputSongName = document.getElementById("inputSongName");
	var songName = inputSongName.value;

	var textareaSongAsStrings = document.getElementById("textareaSongAsStrings");
	var songAsString = textareaSongAsStrings.value;
	var songAsStrings = songAsString.split("\n");

	var songToSynthesize = new Music.Song
	(
		songName,
		[
			Music.Movement.parseFromStrings
			(
				songAsStrings
			)
		]
	);	

	var songAsWavFile = MusicToWavFileConverter.convertSongToWavFile
	(
		songToSynthesize, 
		SamplingInfo.buildDefault()
	);

	var songAsBytes = songAsWavFile.writeToBytes();

	return songAsBytes;
}

// classes

function Base64Encoder()
{
	// do nothing
}
{
	// static methods

	Base64Encoder.encodeBytes = function(bytesToEncode)
	{
		// Encode each three sets of eight bits (octets, or bytes)
		// as four sets of six bits (sextets, or Base 64 digits)

		var returnString = "";

		var bytesPerSet = 3;
		var base64DigitsPerSet = 4;

		var base64DigitsAsString = 
			"ABCDEFGHIJKLMNOPQRSTUVWXYZ" 
			+ "abcdefghijklmnopqrstuvwxyz"
			+ "0123456789"
			+ "+/";

		var numberOfBytesToEncode = bytesToEncode.length;
		var numberOfFullSets = Math.floor(numberOfBytesToEncode / bytesPerSet);
		var numberOfBytesInFullSets = numberOfFullSets * bytesPerSet;
		var numberOfBytesLeftAtEnd = numberOfBytesToEncode - numberOfBytesInFullSets;

		for (var s = 0; s < numberOfFullSets; s++)
		{
			var b = s * bytesPerSet;

			var valueToEncode = 
				(bytesToEncode[b] << 16)
				| (bytesToEncode[b + 1] << 8)
				| (bytesToEncode[b + 2]);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += base64DigitsAsString[((valueToEncode & 0x00003F))];
		}	

		var b = numberOfFullSets * bytesPerSet;

		if (numberOfBytesLeftAtEnd == 1)
		{
			var valueToEncode = (bytesToEncode[b] << 16);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += "==";
		}		
		else if (numberOfBytesLeftAtEnd == 2)
		{
			var valueToEncode = 
				(bytesToEncode[b] << 16)
				| (bytesToEncode[b + 1] << 8);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += "=";
		}

		return returnString;
	}
}

function ByteStreamLittleEndian(bytes)
{
	this.bytes = bytes;  

	this.numberOfBytesTotal = this.bytes.length;
	this.byteIndexCurrent = 0;
}
{
	ByteStreamLittleEndian.prototype.peekBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.bytes[this.byteIndexCurrent + b];
		}

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.readByte = function()
	{
		var returnValue = this.bytes.charCodeAt(this.byteIndexCurrent);

		this.byteIndexCurrent++;

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.readInt = function()
	{
		var returnValue =
		(
			(this.readByte() & 0xFF)
			| ((this.readByte() & 0xFF) << 8 )
			| ((this.readByte() & 0xFF) << 16)
			| ((this.readByte() & 0xFF) << 24)
		);

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.readShort = function()
	{
		var returnValue =
		(
			(this.readByte() & 0xFF)
			| ((this.readByte() & 0xFF) << 8 )
		);

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b < bytesToWrite.length; b++)
		{
			this.bytes.push(bytesToWrite[b]);
		}

		this.byteIndexCurrent = this.bytes.length;
	}

	ByteStreamLittleEndian.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);

		this.byteIndexCurrent++;
	}

	ByteStreamLittleEndian.prototype.writeInt = function(integerToWrite)
	{
		this.bytes.push( (integerToWrite & 0x000000FF) );
		this.bytes.push( (integerToWrite & 0x0000FF00) >>> 8 );
		this.bytes.push( (integerToWrite & 0x00FF0000) >>> 16 );
		this.bytes.push( (integerToWrite & 0xFF000000) >>> 24 );

		this.byteIndexCurrent += 4;
	}

	ByteStreamLittleEndian.prototype.writeShort = function(shortToWrite)
	{
		this.bytes.push( (shortToWrite & 0x00FF) );
		this.bytes.push( (shortToWrite & 0xFF00) >>> 8 );

		this.byteIndexCurrent += 2;
	}

	ByteStreamLittleEndian.prototype.writeString = function(stringToWrite)
	{
		for (var i = 0; i < stringToWrite.length; i++)
		{
			this.writeByte(stringToWrite.charCodeAt(i));
		}
	}

}

function MusicToWavFileConverter()
{
	// do nothing
}
{
	// methods

	MusicToWavFileConverter.convertSongToWavFile = function(songToConvert, samplingInfo)
	{
		var returnValue = new WavFile
		(
			songToConvert.name + ".wav",
			samplingInfo,
			null // samples
		);

		for (var m = 0; m < songToConvert.movements.length; m++)
		{
			var movement = songToConvert.movements[m];

			var secondsPerBeat = 1.0 / (movement.tempo.beatsPerMinute / Music.SecondsPerMinute); 

			for (var p = 0; p < movement.parts.length; p++)
			{
				var part = movement.parts[p];
				var voice = part.voice;

				var timeOffsetInSecondsCurrent = 0;

				var numberOfNotes = part.notes.length;

				for (var n = 0; n < numberOfNotes; n++)
				{
					var note = part.notes[n];
					var notePitch = note.pitches[0];

					if (notePitch.noteLetter.isControlCode == false)
					{
						var noteDurationInSeconds = 
							movement.timeSignature.durationForBeat
							/ note.duration				
							* secondsPerBeat;

						MusicToWavFileConverter.addVoiceToWavFileSamples
						(
							returnValue, 
							voice, 
							note.volume,
							notePitch.frequencyInCyclesPerSecond(),
							timeOffsetInSecondsCurrent,
							noteDurationInSeconds
						);

						timeOffsetInSecondsCurrent += noteDurationInSeconds;
					}
				}
			}
		}

		return returnValue;
	}

	MusicToWavFileConverter.addVoiceToWavFileSamples = function
	(
		wavFile, 
		voice, 
		volume,
		frequencyInCyclesPerSecond,
		timeOffsetInSecondsStart, 
		durationToFilterInSeconds
	)
		{
			var samplingInfo = wavFile.samplingInfo;
			var numberOfChannels = samplingInfo.numberOfChannels;
			var samplesPerSecond = samplingInfo.samplesPerSecond;

			var sampleIndexStart = Math.round(timeOffsetInSecondsStart * samplesPerSecond);
			var durationToFilterInSamples = Math.round(durationToFilterInSeconds * samplesPerSecond);
			var sampleIndexEnd = sampleIndexStart + durationToFilterInSamples;

			var durationOfWavFileInSamples = wavFile.durationInSamples();
			if (durationOfWavFileInSamples < sampleIndexEnd)
			{
				wavFile.extendOrTrimSamples(sampleIndexEnd);
			}

			var samplesForChannels = wavFile.samplesForChannels;

			var secondsPerSample = 1.0 / samplingInfo.samplesPerSecond;

			var samplePrototype = wavFile.samplingInfo.samplePrototype();

			for (var s = sampleIndexStart; s < sampleIndexEnd; s++)
			{
				var timeOffsetInSecondsCurrent = s * secondsPerSample;

				for (var c = 0; c < numberOfChannels; c++)
				{
					var sampleExisting = samplesForChannels[c][s];					

					var voiceSampleValue = voice.sample
					(
						frequencyInCyclesPerSecond, 
						timeOffsetInSecondsCurrent
					) * volume.relativeLoudness;

					var sampleValueNew = sampleExisting.convertToDouble() + voiceSampleValue;
					var sampleValueNewAbsolute = Math.abs(sampleValueNew);
					if (sampleValueNewAbsolute > 1)
					{
						sampleValueNew = sampleValueNew / sampleValueNewAbsolute;   
					}

					sampleExisting.setFromDouble(sampleValueNew);
				}
			}
		}
}

function Music()
{
	// do nothing
}
{
	// constants

	Music.SecondsPerMinute = 60;

	// inner classes 

	Music.Movement = function(name, timeSignature, tempo, parts)
	{
		this.name = name;
		this.timeSignature = timeSignature;
		this.tempo = tempo;
		this.parts = parts;
	}
	{
		Music.Movement.parseFromStrings = function(partsAsStrings)
		{
			var numberOfParts = partsAsStrings.length;
			var parts = [];
			for (var p = 0; p < numberOfParts; p++)
			{
				var partAsString = partsAsStrings[p].trim();
				if (partAsString.length > 0)
				{
					var part = Music.Part.parseFromString(partAsString);
					parts.push(part);
				}
			}

			var returnMovement = new Music.Movement
			(
				"[movement from string]",
				Music.TimeSignature.Instances.FourFour,
				Music.Tempo.Instances.Default,
				parts
			);

			return returnMovement;
		}
	}

	Music.Note = function(pitches, volume, duration)
	{
		this.pitches = pitches;
		this.volume = volume;
		this.duration = duration;
	}
	{
		Music.Note.parseFromString = function(volume, octave, noteAsString)
		{
			var noteLetterAsString = noteAsString.substring(0, 2);
			var durationAsString = noteAsString.substring(2);

			var returnValue = new Music.Note
			(
				// pitches
				[
					new Music.Pitch
					(
						octave,
						Music.NoteLetter.parseFromString(noteLetterAsString)
					),
				],
				volume,
				parseFloat(durationAsString)
			);

			return returnValue;
		}
	}

	Music.Octave = function(octaveIndex, frequencyOfNoteLetterC)
	{
		this.octaveIndex = octaveIndex;
		this.frequencyOfNoteLetterC = frequencyOfNoteLetterC;
	}
	{
		Music.Octave_Instances = function()
		{
			this.Octave0 = new Music.Octave(0, 55);
			this.Octave1 = new Music.Octave(1, 110);
			this.Octave2 = new Music.Octave(2, 220);
			this.Octave3 = new Music.Octave(3, 440);
			this.Octave4 = new Music.Octave(4, 880);

			this._All = 
			[
				this.Octave0,
				this.Octave1,
				this.Octave2,
				this.Octave3,
				this.Octave4,
			];
		}

		Music.Octave.Instances = new Music.Octave_Instances();	
	}

	Music.NoteLetter = function(symbol, frequencyMultiplier, isControlCode)
	{
		this.symbol = symbol;
		this.frequencyMultiplier = frequencyMultiplier;
		this.isControlCode = isControlCode;
	}
	{
		// constants

		Music.NoteLetter.TonesPerOctave = 12;
		Music.NoteLetter.OctavesPerTone = 1 / Music.NoteLetter.TonesPerOctave;

		Music.NoteLetter_Instances = function()
		{
			var octavesPerTone = Music.NoteLetter.OctavesPerTone;

			// todo
			this.Octave = new Music.NoteLetter("O:", 0, true);
			this.Volume = new Music.NoteLetter("V:", 0, true);

			this.Rest = new Music.NoteLetter("R.", 0, false);

			this.C_ = new Music.NoteLetter("C.", Math.pow(2, 0 * octavesPerTone), false);
			this.Cs = new Music.NoteLetter("C#", Math.pow(2, 1 * octavesPerTone), false);
			this.D_ = new Music.NoteLetter("D.", Math.pow(2, 2 * octavesPerTone), false);
			this.Ds = new Music.NoteLetter("D#", Math.pow(2, 3 * octavesPerTone), false);
			this.E_ = new Music.NoteLetter("E.", Math.pow(2, 4 * octavesPerTone), false);
			this.F_ = new Music.NoteLetter("F.", Math.pow(2, 5 * octavesPerTone), false);
			this.Fs = new Music.NoteLetter("F#", Math.pow(2, 6 * octavesPerTone), false);
			this.G_ = new Music.NoteLetter("G.", Math.pow(2, 7 * octavesPerTone), false);
			this.Gs = new Music.NoteLetter("G#", Math.pow(2, 8 * octavesPerTone), false);
			this.A_ = new Music.NoteLetter("A.", Math.pow(2, 9 * octavesPerTone), false);
			this.As = new Music.NoteLetter("A#", Math.pow(2, 10 * octavesPerTone), false);
			this.B_ = new Music.NoteLetter("B.", Math.pow(2, 11 * octavesPerTone), false);

			this._All = 
			[
				this.Octave,
				this.Volume,

				this.Rest,

				this.A_, 
				this.As, 
				this.B_, 
				this.C_, 
				this.Cs, 
				this.D_, 
				this.Ds, 
				this.E_, 
				this.F_, 
				this.Fs, 
				this.G_, 
				this.Gs,
			];
		}

		Music.NoteLetter.Instances = new Music.NoteLetter_Instances();

		Music.NoteLetter.parseFromString = function(noteLetterAsString)
		{
			var returnValue = null;

			var noteLettersAll = Music.NoteLetter.Instances._All;
			var numberOfNoteLetters = noteLettersAll.length;

			for (var n = 0; n < numberOfNoteLetters; n++)
			{
				var noteLetter = noteLettersAll[n];

				if (noteLetterAsString == noteLetter.symbol)
				{
					returnValue = noteLetter;
					break;
				}
			}

			return returnValue;
		}
	}

	Music.Part = function(name, voice, volumeInitial, notes)
	{
		this.name = name;
		this.voice = voice;
		this.volumeInitial = volumeInitial;
		this.notes = notes;
	}
	{
		Music.Part.parseFromString = function(partAsString)
		{
			var volumeCurrent = Music.Volume.Instances.Medium;
			var octaveCurrent = Music.Octave.Instances.Octave3;

			partAsString = partAsString.split(" ").join("");
			partAsString = partAsString.split("-").join("");
			partAsString = partAsString.split("_").join("");
			partAsString = partAsString.split("\t").join("");
			partAsString = partAsString.split("|").join("");

			var notesAsStrings = partAsString.split(";");
			var numberOfNotes = notesAsStrings.length;
			var notes = [];
			for (var n = 0; n < numberOfNotes; n++)
			{
				var noteAsString = notesAsStrings[n];
				var note = Music.Note.parseFromString(volumeCurrent, octaveCurrent, noteAsString);
				var noteLetter = note.pitches[0].noteLetter;
				if (noteLetter.isControlCode == true)
				{
					var controlCodeArgument = note.duration;

					if (noteLetter == Music.NoteLetter.Instances.Octave)
					{
						octaveCurrent = Music.Octave.Instances._All[controlCodeArgument];
					}
					else if (noteLetter == NoteLetter.Instances.Volume)
					{
						volumeCurrent = Music.Volume.Instances._All[controlCodeArgument];
					}		
				}

				notes[n] = note;
			}

			var returnValue = new Music.Part
			(
				"[part from string]",
				Music.Voice.Instances.Sine,
				Music.Volume.Instances.Medium,
				notes
			);

			return returnValue;
		}
	}

	Music.Pitch = function(octave, noteLetter)
	{
		this.octave = octave;
		this.noteLetter = noteLetter;
	}
	{
		Music.Pitch.prototype.frequencyInCyclesPerSecond = function()
		{
			var returnValue = 
				this.octave.frequencyOfNoteLetterC 
				* this.noteLetter.frequencyMultiplier;

			return returnValue;
		}
	}

	Music.Song = function(name, movements)
	{
		this.name = name;
		this.movements = movements;
	}

	Music.Tempo = function(name, beatsPerMinute)
	{
		this.name = name;
		this.beatsPerMinute = beatsPerMinute;
	}
	{	
		Music.Tempo_Instances = function()
		{
			this.Default = new Music.Tempo("Default", 80);

			this.Andante = new Music.Tempo("Andante", 60);
		}

		Music.Tempo.Instances = new Music.Tempo_Instances();
	}

	Music.TimeSignature = function(beatsPerMeasure, durationForBeat)
	{
		this.beatsPerMeasure = beatsPerMeasure;
		this.durationForBeat = durationForBeat;
	}
	{
		Music.TimeSignature_Instances = function()
		{
			this.FourFour = new Music.TimeSignature(4, 4);
		}

		Music.TimeSignature.Instances = new Music.TimeSignature_Instances();
	}

	Music.Voice = function(name, sample)
	{
		this.name = name;
		this.sample = sample;
	}
	{
		Music.Voice_Instances = function()
		{
			this.Sine = new Music.Voice
			(
				"Sine",
				// sample
				function(timeOffsetInSeconds, frequencyInCyclesPerSecond)
				{
					var timeOffsetInCycles = timeOffsetInSeconds * frequencyInCyclesPerSecond;
					var fractionOfCycleCurrent = timeOffsetInCycles - Math.floor(timeOffsetInCycles);
					returnValue = Math.sin(fractionOfCycleCurrent * Math.PI * 2.0); 

					return returnValue;
				}		
			);
		}

		Music.Voice.Instances = new Music.Voice_Instances();
	}

	Music.Volume = function(name, relativeLoudness)
	{
		this.name = name;
		this.relativeLoudness = relativeLoudness;
	}
	{
		Music.Volume_Instances = function()
		{
			this.Quiet = new Music.Volume("Quiet", .001);
			this.Medium = new Music.Volume("Medium", .01);
			this.Loud = new Music.Volume("Loud", .1);

			this._All =
			[
				this.Quiet,
				this.Medium,
				this.Loud,
			];
		}

		Music.Volume.Instances = new Music.Volume_Instances();
	}
}

function WavFile
(
	filePath,
	samplingInfo,
	samplesForChannels
)
{
	this.filePath = filePath;
	this.samplingInfo = samplingInfo;
	this.samplesForChannels = samplesForChannels;

	 // hack
	if (this.samplingInfo == null)
	{
		this.samplingInfo = SamplingInfo.buildDefault();
	}

	if (this.samplesForChannels == null)
	{
		var numberOfChannels = this.samplingInfo.numberOfChannels; 

		this.samplesForChannels = [];
		for (var c = 0; c < numberOfChannels; c++)
		{
			this.samplesForChannels[c] = [];
		}
	}
}
{
	// constants

	WavFile.BitsPerByte = 8;
	WavFile.NumberOfBytesInRiffWaveAndFormatChunks = 36;

	// static methods

	WavFile.readFromFile = function(fileToReadFrom)
	{		
		var returnValue = new WavFile(fileToReadFrom.name, null, null);

		var fileReader = new FileReader();
		fileReader.onloadend = function(fileLoadedEvent)
		{
			if (fileLoadedEvent.target.readyState == FileReader.DONE)
			{
				var bytesFromFile = fileLoadedEvent.target.result;
				var reader = new ByteStreamLittleEndian(bytesFromFile);

				returnValue.readFromFilePath_ReadChunks(reader);
			}

			Globals.Instance.visualizer.loadFileAndVisualize_LoadComplete(returnValue);
		}

		fileReader.readAsBinaryString(fileToReadFrom);
	}

	WavFile.prototype.readFromFilePath_ReadChunks = function(reader)
	{
		var riffStringAsBytes = reader.readBytes(4);		  

		var numberOfBytesInFile = reader.readInt();

		var waveStringAsBytes = reader.readBytes(4);

		this.readFromFile_ReadChunks_Format(reader);
		this.readFromFile_ReadChunks_Data(reader);
	}

	WavFile.prototype.readFromFile_ReadChunks_Format = function(reader)
	{
		var fmt_StringAsBytes = reader.readBytes(4);
		var chunkSizeInBytes = reader.readInt();
		var formatCode = reader.readShort();

		var numberOfChannels = reader.readShort();
		var samplesPerSecond = reader.readInt();

		var bytesPerSecond = reader.readInt();
		var bytesPerSampleMaybe = reader.readShort();
		var bitsPerSample = reader.readShort();

		var samplingInfo = new SamplingInfo
		(
			"[from file]",
			chunkSizeInBytes,
			formatCode,
			numberOfChannels,
			samplesPerSecond,
			bitsPerSample	
		);

		this.samplingInfo = samplingInfo;
	}

	WavFile.prototype.readFromFile_ReadChunks_Data = function(reader)
	{
		var dataStringAsBytes = reader.readBytes(4);
		var subchunk2SizeInBytes = reader.readInt();

		var samplesForChannelsMixedAsBytes = reader.readBytes(subchunk2SizeInBytes);

		var samplesForChannels = Sample.buildManyFromBytes
		(
			this.samplingInfo,
			samplesForChannelsMixedAsBytes
		);

		this.samplesForChannels = samplesForChannels;	
	}

	// instance methods

	WavFile.prototype.durationInSamples = function()
	{
		var returnValue = 0;
		if (this.samplesForChannels != null)
		{
			if (this.samplesForChannels.length > 0)
			{
				returnValue = this.samplesForChannels[0].length;
			}
		}

		return returnValue;		
	}

	WavFile.prototype.durationInSeconds = function()
	{
		return this.durationInSamples() * this.samplingInfo.samplesPerSecond;
	}

	WavFile.prototype.extendOrTrimSamples = function(numberOfSamplesToExtendOrTrimTo)
	{
		var numberOfChannels = this.samplingInfo.numberOfChannels;
		var samplesForChannelsNew = [];

		for (var c = 0; c < numberOfChannels; c++)
		{
			var samplesForChannelOld = this.samplesForChannels[c];
			var samplesForChannelNew = [];

			for (var s = 0; s < samplesForChannelOld.length && s < numberOfSamplesToExtendOrTrimTo; s++)
			{
				samplesForChannelNew[s] = samplesForChannelOld[s];				
			}

			var samplePrototype = this.samplingInfo.samplePrototype();

			for (var s = samplesForChannelOld.length; s < numberOfSamplesToExtendOrTrimTo; s++)
			{
				samplesForChannelNew[s] = samplePrototype.build();
			}

			samplesForChannelsNew[c] = samplesForChannelNew;
		}

		this.samplesForChannels = samplesForChannelsNew;
	}

	WavFile.prototype.writeToBytes = function()
	{
		var writer = new ByteStreamLittleEndian([]);

		this.writeToBytes_WriteChunks(writer);

		return writer.bytes;
	}

	WavFile.prototype.writeToBytes_WriteChunks = function(writer)
	{
		writer.writeString("RIFF");

		// hack
		var numberOfBytesOfOverhead = 
			"RIFF".length
			+ "WAVE".length
			+ "fmt ".length
			+ 20 // additional bytes In format header
			+ "data".length;

			//+ 4; // additional bytes in data header?

		var numberOfBytesInFile = 
			this.samplingInfo.numberOfChannels
			* this.samplesForChannels[0].length
			* this.samplingInfo.bitsPerSample
			/ WavFile.BitsPerByte
			+ numberOfBytesOfOverhead;

		writer.writeInt(numberOfBytesInFile);

		writer.writeString("WAVE");

		this.writeToBytes_WriteChunks_Format(writer);
		this.writeToBytes_WriteChunks_Data(writer);
	}

	WavFile.prototype.writeToBytes_WriteChunks_Format = function(writer)
	{
		writer.writeString("fmt ");

		writer.writeInt(this.samplingInfo.chunkSizeInBytes);
		writer.writeShort(this.samplingInfo.formatCode);

		writer.writeShort(this.samplingInfo.numberOfChannels);
		writer.writeInt(this.samplingInfo.samplesPerSecond);

		writer.writeInt(this.samplingInfo.bytesPerSecond);
		writer.writeShort(this.samplingInfo.bytesPerSampleMaybe);
		writer.writeShort(this.samplingInfo.bitsPerSample);
	}

	WavFile.prototype.writeToBytes_WriteChunks_Data = function(writer)
	{
		writer.writeString("data");

		var samplesForChannelsMixedAsBytes = Sample.convertManyToBytes
		(
			this.samplesForChannels,
			this.samplingInfo
		);

		writer.writeInt(samplesForChannelsMixedAsBytes.length);

		writer.writeBytes(samplesForChannelsMixedAsBytes);
	}

	// inner classes

	function Sample()
	{
		// do nothing
	}
	{
		Sample.prototype.build = function(){}
		Sample.prototype.setFromBytes = function(valueAsBytes){}
		Sample.prototype.setFromDouble = function(valueAsDouble){}
		Sample.prototype.convertToBytes = function(){}
		Sample.prototype.convertToDouble = function(){}

	   	Sample.buildManyFromBytes = function
		(
			samplingInfo,
			bytesToConvert
		)
		{
			var numberOfBytes = bytesToConvert.length;

			var numberOfChannels = samplingInfo.numberOfChannels;

			var returnSamples = [];

			var bytesPerSample = samplingInfo.bitsPerSample / WavFile.BitsPerByte;

			var samplesPerChannel =
				numberOfBytes
				/ bytesPerSample
				/ numberOfChannels;

			for (var c = 0; c < numberOfChannels; c++)
			{
				returnSamples[c] = [];
			}

			var b = 0;

			var halfMaxValueForEachSample = Math.pow
			(
				2, WavFile.BitsPerByte * bytesPerSample - 1
			);

			var samplePrototype = samplingInfo.samplePrototype();

			var sampleValueAsBytes = [];

			for (var s = 0; s < samplesPerChannel; s++)
			{				
				for (var c = 0; c < numberOfChannels; c++)
				{
					for (var i = 0; i < bytesPerSample; i++)
					{
						sampleValueAsBytes[i] = bytesToConvert[b];
						b++;
					}

					returnSamples[c][s] = samplePrototype.build().setFromBytes
					(
						sampleValueAsBytes
					);
				}
			}

			return returnSamples;
		}

		Sample.convertManyToBytes = function
		(
			samplesToConvert,
			samplingInfo
		)
		{
			var returnBytes = null;

			var numberOfChannels = samplingInfo.numberOfChannels;

			var samplesPerChannel = samplesToConvert[0].length;

			var bitsPerSample = samplingInfo.bitsPerSample;

			var bytesPerSample = bitsPerSample / WavFile.BitsPerByte;

			var numberOfBytes =
				numberOfChannels
				* samplesPerChannel
				* bytesPerSample;

			returnBytes = [];

			var halfMaxValueForEachSample = Math.pow
			(
				2, WavFile.BitsPerByte * bytesPerSample - 1
			);

			var b = 0;

			for (var s = 0; s < samplesPerChannel; s++)
			{
				for (var c = 0; c < numberOfChannels; c++)
				{
					var sample = samplesToConvert[c][s];	

					var sampleAsBytes = sample.convertToBytes();

					for (var i = 0; i < bytesPerSample; i++)
					{
						returnBytes[b] = sampleAsBytes[i];
						b++;
					}
				}						
			}

			return returnBytes;
		}	
	}

	function Sample16(value)
	{
		this.value = value;
	}
	{
		Sample16.MaxValue = Math.pow(2, 15) - 1;
		Sample16.DoubleMaxValue = Math.pow(2, 16);

		// Sample members
		Sample16.prototype.build = function()
		{
			return new Sample16(0);
		}

		Sample16.prototype.setFromBytes = function(valueAsBytes)
		{
			this.value =
			(
				(valueAsBytes[0] & 0xFF)
				| ((valueAsBytes[1] & 0xFF) << 8 )
			);

			if (this.value > Sample16.MaxValue) 
			{
				this.value -= Sample16.DoubleMaxValue;
			}

			return this;
		}

		Sample16.prototype.setFromDouble = function(valueAsDouble)
		{
			this.value =
			(
				valueAsDouble * Sample16.MaxValue
			);

			return this;
		}

		Sample16.prototype.convertToBytes = function()
		{
			var returnValue = 
			[
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF)
			];

			return returnValue;
		}		

		Sample16.prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample16.MaxValue;
		}
	}

	function Sample24(value)
	{
		this.value = value;
	}
	{
		Sample24.MaxValue = Math.pow(2, 23) - 1;
		Sample24.DoubleMaxValue = Math.pow(2, 24);

		// Sample members

		Sample24.prototype.build = function()
		{
			return new Sample24(0);
		}

		Sample24.prototype.setFromBytes = function(valueAsBytes)
		{
			this.value =
			(
				((valueAsBytes[0] & 0xFF))
				| ((valueAsBytes[1] & 0xFF) << 8 )
				| ((valueAsBytes[2] & 0xFF) << 16)
			);

			if (this.value > Sample24.MaxValue) 
			{
				this.value -= Sample24.DoubleMaxValue;
			}

			return this;
		}

		Sample24.prototype.setFromDouble = function(valueAsDouble)
		{
			this.value = 
			(
				valueAsDouble
				* Sample24.MaxValue
			);

			return this;
		}

		Sample24.prototype.convertToBytes = function()
		{
			return new Array()
			{
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF),
				((this.value >>> 16) & 0xFF)
			};
		}		

		Sample24.prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample24.MaxValue;
		}
	}

	function Sample32(value)
	{
		this.value = value;
	}
	{
		Sample32.MaxValue = Math.pow(2, 32);
		Sample32.MaxValueHalf = Math.pow(2, 31);

		// Sample members

		Sample32.prototype.build = function()
		{
			return new Sample32(0);
		}

		Sample32.prototype.setFromBytes = function(valueAsBytes)
		{
			this.value = 
			(
				((valueAsBytes[0] & 0xFF))
				| ((valueAsBytes[1] & 0xFF) << 8 )
				| ((valueAsBytes[2] & 0xFF) << 16)
				| ((valueAsBytes[3] & 0xFF) << 24)
			);

			if (this.value > Sample32.MaxValue) 
			{
				this.value -= Sample32.DoubleMaxValue;
			}

			return this;
		}

		Sample32.prototype.setFromDouble = function(valueAsDouble)
		{
			this.value = 
			(
				valueAsDouble
				* Sample32.MaxValue
			);

			return this;
		}

		Sample32.prototype.convertToBytes = function()
		{
			return new Array()
			{
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF),
				((this.value >>> 16) & 0xFF),
				((this.value >>> 24) & 0xFF)
			};
		}	

		Sample32.prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample32.MaxValue;
		}	
	}

	function SamplingInfo
	(
		 name,	   
		 chunkSizeInBytes,
		 formatCode,
		 numberOfChannels,		
		 samplesPerSecond,
		 bitsPerSample
	)
	{
		this.name = name;
		this.chunkSizeInBytes = chunkSizeInBytes;
		this.formatCode = formatCode;
		this.numberOfChannels = numberOfChannels;
		this.samplesPerSecond = samplesPerSecond;
		this.bitsPerSample = bitsPerSample;
	}
	{
		SamplingInfo.buildDefault = function()
		{
			return new SamplingInfo
			(
				"Default",
				16, // chunkSizeInBytes
				1, // formatCode
				1, // numberOfChannels
				44100,	 // samplesPerSecond
				16 // bitsPerSample
			);
		}

		SamplingInfo.prototype.bytesPerSecond = function()
		{	
			return this.samplesPerSecond
				* this.numberOfChannels
				* this.bitsPerSample / WavFile.BitsPerByte;
		}

		SamplingInfo.prototype.samplePrototype = function()
		{
			var returnValue = null;

			if (this.bitsPerSample == 16)
			{
				returnValue = new Sample16(0);
			}
			else if (this.bitsPerSample == 24)
			{
				returnValue = new Sample24(0);
			}
			else if (this.bitsPerSample == 32)
			{
				returnValue = new Sample32(0);
			}

			return returnValue;
		}

		SamplingInfo.prototype.toString = function()
		{
			var returnValue =
				"<SamplingInfo "
				+ "chunkSizeInBytes='" + this.chunkSizeInBytes + "' "
				+ "formatCode='" + this.formatCode + "' "
				+ "numberOfChannels='" + this.numberOfChannels + "' "
				+ "samplesPerSecond='" + this.samplesPerSecond + "' "
				+ "bitsPerSample='" + this.bitsPerSample + "' "
				+ "/>";

			return returnValue;
		}		
	}
}

</script>
</body>
</html>

This entry was posted in Uncategorized and tagged , , , , , . Bookmark the permalink.

Leave a comment