Fourier Frequency Analysis and Synthesis of a WAV Sound File in JavaScript

Fourier Frequency Analysis is a method of approximating an arbitrarily complex function (say, an audio waveform) as a combination of simple sinusoidal waves by determining how strongly each frequency is present in the original function.

Basically, you start with a fundamental frequency (call it f), multiply each sample in the waveform by the sine and/or cosine of the frequency at that point, add those products all together, and then divide by the number of samples to find the average amplitude for that frequency component. Then you take the next component frequency in the sequence (f, 2f, 3f, 4f, and so on) and repeat the process to find the average amplitude for that frequency. Depending on how large or complex the original waveform is, and how precise you want your approximation to be, you may repeat that a few times or a few thousand times. When it’s over, you have a bunch of amplitudes and the frequencies they correspond to. Then you can add the sine waves of those frequencies and amplitudes back together to approximate the original waveform.

Easy, right?

The JavaScript code included below prompts the user to load a WAV sound file, on which it then performs Fourier frequency analysis. Having determined the relative amplitudes of the original sound’s various harmonic components, it then re-synthesizes an approximation of the sound by superimposing a set of pure sine waves having the calculated harmonic frequencies and amplitudes. Finally, it plays that sound.

It is recommended that the WAV file used be short. For example, the sample WAV that this program was tested on was less than half a second long, and it still took two or three seconds to analyze.

Note that the actual Fourier analysis itself is taking place in the FrequencyAnalyzer.loadFileAndAnalyze_LoadComplete() method, which is kind of crufty. Ideally, I’d like to abstract the analysis out to its own class and/or method, but I haven’t done it yet. Sorry about the mess.

UPDATE 2016/05/20 – I have cleaned up the architecture and user interface of this code somewhat. Now that I take a look at it, though, I’m not sure if it ever worked that well. I tested it by analyzing the simple sound “ding.wav” that comes with Microsoft Windows, and then re-synthesizing 0.5 seconds of that sound. It sort of sounded okay. But if I change the number of harmonics from 2048, the volume changes, and becomes imperceptibly quiet at lower numbers of harmonics. So I’m obviously doing something wrong, but I can’t figure out exactly what. The internet is just as unhelpful as I remember about this stuff.

I also worked on some code that allowed the user to set the maximum frequency through the interface, but this resulted in non-integral multiples of the fundamental frequency, so I have mostly disabled it.

FrequencyAnalysisAndResynthesis

<html>
<body>

	<!-- user interface -->
	<label>Lowest Frequency in Hertz:<label>
	<input id="inputFundamentalInHertz" type="number" value="20" />
	<br />

	<!--<label>Highest Frequency in Hertz:<label>
	<input id="inputHighestFrequencyInHertz" type="number" value="20000" />
	<br />-->

	<label>Number of Frequency Components:</label>
	<input id="inputNumberOfHarmonics" type="number" value="2048" />
	<br />

	<label>WAV File to Analyze:</label>
	<input id="inputFileChooser" type="file"></input>
	<button id="buttonOriginalPlay" onclick="buttonOriginalPlay_Clicked();">Play</button>
	<br />

	<button id="buttonAnalyze" onclick="buttonAnalyze_Clicked();">Analyze</button>
	<label>Analysis Status:</label>
	<input id="inputAnalysisStatus" readonly="readonly" value="" />
	<br />

	<label>Amplitudes for harmonic oscillators:</label>
	<br />
	<textarea id="textareaHarmonicAmplitudes" style="width:400px;height:200px;"></textarea>
	<br />

	<label>Duration to Synthesize in Seconds:</label>
	<input id="inputDurationInSeconds" type="number" value="0.5" />
	
	<button id="buttonSynthesizeAndPlay" onclick="buttonSynthesizeAndPlay_Clicked();">Synthesize and Play</button>
	<label>Synthesis Status:</label>
	<input id="inputSynthesisStatus" readonly="readonly" value="" />

<script type="text/javascript">

// ui events

function buttonAnalyze_Clicked()
{
	var inputFileChooser = document.getElementById("inputFileChooser");
	var fileSpecified = inputFileChooser.files[0];

	var inputAnalysisStatus = document.getElementById("inputAnalysisStatus");
	inputAnalysisStatus.value = "Analyzing...";

	FileHelper.readFileAsBinaryString
	(
		fileSpecified,
		null, // contextForCallback
		buttonAnalyze_Clicked_2
	);
}

function buttonAnalyze_Clicked_2(fileSpecified, wavFileAsBinaryString)
{
	var wavFileToAnalyze = WavFile.fromBinaryString
	(
		fileSpecified.name,
		wavFileAsBinaryString
	);

	Session.Instance.wavFileToAnalyze = wavFileToAnalyze;

	var inputFundamentalInHertz = document.getElementById("inputFundamentalInHertz");
	var fundamentalInHertz = parseFloat(inputFundamentalInHertz.value);

	//var inputHighestFrequencyInHertz = document.getElementById("inputHighestFrequencyInHertz");
	//var highestFrequencyInHertz = parseFloat(inputHighestFrequencyInHertz.value);

	var inputNumberOfHarmonics = document.getElementById("inputNumberOfHarmonics");
	var numberOfHarmonics = parseInt(inputNumberOfHarmonics.value);

	highestFrequencyInHertz = fundamentalInHertz * numberOfHarmonics;

	var frequencyAnalysis = new FrequencyAnalysis
	(
		wavFileToAnalyze,
		fundamentalInHertz,
		highestFrequencyInHertz,
		numberOfHarmonics
	);

	frequencyAnalysis.calculateHarmonicAmplitudes();

	Session.Instance.frequencyAnalysis = frequencyAnalysis;

	var frequencyAnalysisAsString = frequencyAnalysis.toString();
	var textareaHarmonicAmplitudes = document.getElementById("textareaHarmonicAmplitudes");
	textareaHarmonicAmplitudes.innerHTML = frequencyAnalysisAsString;

	var inputAnalysisStatus = document.getElementById("inputAnalysisStatus");
	inputAnalysisStatus.value = "Complete.";	
}

function buttonOriginalPlay_Clicked()
{	
	var inputFileChooser = document.getElementById("inputFileChooser");
	var fileSpecified = inputFileChooser.files[0];

	FileHelper.readFileAsBinaryString
	(
		fileSpecified,
		null, // contextForCallback
		buttonOriginalPlay_Clicked_2
	);
}

function buttonOriginalPlay_Clicked_2(fileSpecified, wavFileAsBinaryString)
{
	var wavFileToPlay = WavFile.fromBinaryString
	(
		fileSpecified.name,		
		wavFileAsBinaryString
	);

	playWavFile(wavFileToPlay);
}

function buttonSynthesizeAndPlay_Clicked()
{
	var inputSynthesisStatus = document.getElementById("inputSynthesisStatus");
	inputSynthesisStatus.value = "Synthesizing...";

	setTimeout(buttonSynthesizeAndPlay_Clicked_1, 1);
}

function buttonSynthesizeAndPlay_Clicked_1()
{
	var inputDurationInSeconds = document.getElementById("inputDurationInSeconds");
	var durationInSecondsAsString = inputDurationInSeconds.value;
	var durationInSeconds = parseFloat(durationInSecondsAsString);

	var frequencyAnalysis = Session.Instance.frequencyAnalysis;
	
	var wavFileSynthesized = frequencyAnalysis.toWavFileOfDurationInSeconds
	(
		durationInSeconds
	);	

	var inputSynthesisStatus = document.getElementById("inputSynthesisStatus");
	inputSynthesisStatus.value = "Done.";

	playWavFile(wavFileSynthesized);
}

function playWavFile(wavFileToPlay)
{
	document.body.appendChild(wavFileToPlay.toDOMElement())	
}

// 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 Constants()
{}
{
	Constants.Tau = Math.PI * 2.0;
}

function FileHelper()
{
	// static class
}
{
	FileHelper.readFileAsBinaryString = function
	(
		fileToReadFrom, 
		contextForCallback, 
		callbackFunction
	)
	{		
		var fileReader = new FileReader();

		fileReader.onloadend = function(fileLoadedEvent)
		{
			var fileReader2 = fileLoadedEvent.target;
			if (fileReader2.readyState == FileReader.DONE)
			{
				var fileAsBinaryString = fileReader2.result;
				callbackFunction.call
				(
					contextForCallback, 
					fileToReadFrom, 
					fileAsBinaryString
				);
			}
		}

		fileReader.readAsBinaryString(fileToReadFrom);
	}
}

function FrequencyAnalysis
(
	wavFileToAnalyze, 
	frequencyOfFundamentalInHertz, 
	frequencyHighestInHertz,
	numberOfHarmonicComponents
)
{
	this.wavFileToAnalyze = wavFileToAnalyze;
	this.frequencyOfFundamentalInHertz = frequencyOfFundamentalInHertz;
	this.frequencyHighestInHertz = frequencyHighestInHertz;
	this.numberOfHarmonicComponents = numberOfHarmonicComponents;
}
{
	// methods

	FrequencyAnalysis.prototype.calculateHarmonicAmplitudes = function()
	{
		var amplitudesOfHarmonicComponents = [];

		var samples = this.wavFileToAnalyze.samplesForChannels[0];
		var numberOfSamples = samples.length;
		var samplesPerSecond = this.wavFileToAnalyze.samplingInfo.samplesPerSecond;
		var durationInSeconds = this.wavFileToAnalyze.durationInSeconds();

		var frequencyStepInHertz = this.frequencyStepInHertz();

		for (var h = 0; h < this.numberOfHarmonicComponents; h++)
		{
			var frequencyOfHarmonicComponent = 
				this.frequencyOfFundamentalInHertz 
				+ h * frequencyStepInHertz;

			var sumOfSamplesTimesCosines = 0;
			var sumOfSamplesTimesSines = 0;

			for (var s = 0; s < numberOfSamples; s++)
			{
				var sample = samples[s];

				var sampleValue = sample.convertToDouble();
				var timeOffsetInSeconds = s / samplesPerSecond;

				var timeOffsetInCycles = 
					timeOffsetInSeconds 
					* frequencyOfHarmonicComponent;

				var timeOffsetInRadians = 
					timeOffsetInCycles * Constants.Tau;

				var cosine = Math.cos(timeOffsetInRadians); 
				var sine = Math.sin(timeOffsetInRadians);

				var sampleTimesCosine = sampleValue * cosine;
				var sampleTimesSine = sampleValue * sine;

				sumOfSamplesTimesCosines += sampleTimesCosine;
				sumOfSamplesTimesSines += sampleTimesSine;
			}

			amplitudesOfHarmonicComponents[h] = 
				(sumOfSamplesTimesCosines
				+ sumOfSamplesTimesSines)
				/ numberOfSamples;
		}

		this.amplitudesOfHarmonicComponents = amplitudesOfHarmonicComponents;
	}

	FrequencyAnalysis.prototype.frequencyRangeInHertz = function()
	{
		var returnValue = 
			this.frequencyHighestInHertz - this.frequencyOfFundamentalInHertz;

		return returnValue;
	}

	FrequencyAnalysis.prototype.frequencyStepInHertz = function()
	{
		var frequencyRangeInHertz = this.frequencyRangeInHertz();
		var returnValue = frequencyRangeInHertz / this.numberOfHarmonicComponents;

		return returnValue;
	}

	FrequencyAnalysis.prototype.toWavFileOfDurationInSeconds = function
	(
		durationInSeconds
	)
	{
		// hack - Assuming stereo channels.
		var wavFileSynthesized = new WavFile
		(
			"[resynthesized].wav", // filePath
			this.wavFileToAnalyze.samplingInfo,
			[[], []] // samples
		);		

		wavFileSynthesized.silenceSamples
		(
			0, // timeOffsetInSeconds,
			durationInSeconds
		);

		var voiceSine = Voice.Instances.Sine;
		var voiceCosine = Voice.Instances.Cosine;
		var volume = new Volume("[volume]", 0);

		var harmonicAmplitudes = this.amplitudesOfHarmonicComponents;

		var frequencyStepInHertz = this.frequencyStepInHertz();

		for (var h = 0; h < this.numberOfHarmonicComponents; h++)
		{
			var frequencyOfHarmonicComponent = 
				this.frequencyOfFundamentalInHertz 
				+ h * frequencyStepInHertz;

			var amplitudeOfHarmonicComponent = harmonicAmplitudes[h];

			volume.relativeLoudness = 
				amplitudeOfHarmonicComponent;

			wavFileSynthesized.addVoiceToSamples
			(
				voiceSine,
				volume,
				frequencyOfHarmonicComponent,
				0, // timeOffsetInSeconds
				durationInSeconds
			);

			wavFileSynthesized.addVoiceToSamples
			(
				voiceCosine,
				volume,
				frequencyOfHarmonicComponent,
				0, // timeOffsetInSeconds 
				durationInSeconds
			);
		}

		return wavFileSynthesized;
	}

	// string-related instance methods

	FrequencyAnalysis.prototype.toString = function()
	{
		return this.amplitudesOfHarmonicComponents.join("\n");
	}	
}

function Session()
{
	// do nothing	
}
{
	Session.Instance = new Session();
}

function Voice(name, sample)
{
	this.name = name;
	this.sample = sample;
}
{
	function Voice_Instances()
	{
		this.Cosine = new Voice
		(
			"Cosine",
			// sample
			function(timeOffsetInSeconds, frequencyInCyclesPerSecond)
			{
				var timeOffsetInCycles = timeOffsetInSeconds * frequencyInCyclesPerSecond;
				var fractionOfCycleCurrent = timeOffsetInCycles - Math.floor(timeOffsetInCycles);
				returnValue = Math.cos(fractionOfCycleCurrent * Constants.Tau); 

				return returnValue;
			}		
		);

		this.Sine = new Voice
		(
			"Sine",
			// sample
			function(timeOffsetInSeconds, frequencyInCyclesPerSecond)
			{
				var timeOffsetInCycles = timeOffsetInSeconds * frequencyInCyclesPerSecond;
				var fractionOfCycleCurrent = timeOffsetInCycles - Math.floor(timeOffsetInCycles);
				returnValue = Math.sin(fractionOfCycleCurrent * Constants.Tau); 

				return returnValue;
			}		
		);
	}

	Voice.Instances = new Voice_Instances();
}

function Volume(name, relativeLoudness)
{
	this.name = name;
	this.relativeLoudness = relativeLoudness;
}

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.fromBinaryString = function(name, fileContentsAsBinaryString)
	{		
		var returnValue = new WavFile(name, null, null);

		var reader = new ByteStreamLittleEndian(fileContentsAsBinaryString);

		returnValue.fromBinaryString_ReadChunks(reader);

		return returnValue;
	}

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

		var numberOfBytesInFile = reader.readInt();

		var waveStringAsBytes = reader.readBytes(4);

		this.fromBinaryString_ReadChunks_Format(reader);
		this.fromBinaryString_ReadChunks_Data(reader);
	}

	WavFile.prototype.fromBinaryString_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.fromBinaryString_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.addVoiceToSamples = function
	(
		voice, 
		volume,
		frequencyInCyclesPerSecond,
		timeOffsetInSecondsStart, 
		durationToFilterInSeconds
	)
	{
		var samplingInfo = this.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 = this.durationInSamples();
		if (durationOfWavFileInSamples < sampleIndexEnd)
		{
			this.extendOrTrimSamples(sampleIndexEnd);
		}

		var samplesForChannels = this.samplesForChannels;

		var secondsPerSample = 1.0 / samplingInfo.samplesPerSecond;

		var samplePrototype = this.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);
			}
		}
	}

	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.silenceSamples = function
	(
		timeOffsetInSecondsStart, 
		durationToFilterInSeconds
	)
	{
		var samplingInfo = this.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 = this.durationInSamples();
		if (durationOfWavFileInSamples < sampleIndexEnd)
		{
			this.extendOrTrimSamples(sampleIndexEnd);
		}

		var samplesForChannels = this.samplesForChannels;

		var secondsPerSample = 1.0 / samplingInfo.samplesPerSecond;

		var samplePrototype = this.samplingInfo.samplePrototype();

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

				sampleExisting.setFromDouble(0);
			}
		}
	}

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

	// dom methods

	WavFile.prototype.toDOMElement = function()
	{
		var soundAsBytes = this.writeToBytes();

		var soundAsStringBase64 = Base64Encoder.encodeBytes(soundAsBytes);

		var soundAsDataURI = 'data:audio/wav;base64,' + soundAsStringBase64;

		var domElementSoundSource = document.createElement("source");
		domElementSoundSource.src = soundAsDataURI;

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

		domElementAudio.appendChild(domElementSoundSource);

		return domElementAudio;
	}

	// inner classes

	function Sample()
	{
		// do nothing
	}
	{
		var prototype = Sample.prototype;

		prototype.build = function(){}
		prototype.setFromBytes = function(valueAsBytes){}
		prototype.setFromDouble = function(valueAsDouble){}
		prototype.convertToBytes = function(){}
		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);

		var prototype = Sample16.prototype;

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

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

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

			return this;
		}

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

			return this;
		}

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

			return returnValue;
		}		

		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

		var prototype = Sample24.prototype;

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

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

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

			return this;
		}

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

		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

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

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

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

			return this;
		}

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

		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;
	}
	{
		var prototype = SamplingInfo.prototype;

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

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

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

		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>

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

2 Responses to Fourier Frequency Analysis and Synthesis of a WAV Sound File in JavaScript

  1. carllos says:

    Hi,
    Great example. Just One question:
    If iI want to use it in separated frequencies, Should I change:
    sumOfSamplesTimesCosines += sampleTimesCosine;
    with
    freqArr[h][freq=sampleTimesCosine;

    ?

    • Great question. I’m sorry to report that I have no idea what a “separated frequency” even is, so I may not be the right person to ask. Better ask a physicist, or a mathematician. All I’m qualified to say is that you’ve got an unclosed bracket on the left side of your assignment statement, but I’m sure you would have figured that out almost immediately anyway. Sorry I can’t be more help.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s