A MOD File Viewer in JavaScript

The JavaScript code below, when run, allows the user to specify a file in the .MOD music tracker format, parses it, and displays the contents as JSON (in a rather inconvenient way). To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

Note that this code doesn’t actually play the file as audio, it just displays it as text. It is intended only as a basis for further work.

ModFileReader.png




	
MOD File to Load:<br />
<br />
File Contents as JSON:<br />
<br />
	


// events

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	FileHelper.loadFileAsBytes(file, inputFile_Changed_Loaded);
}

function inputFile_Changed_Loaded(songFile, songAsBytes)
{
	var songAsModFile = ModFile.fromBytes(songAsBytes);
	var jsonTabSizeInSpaces = 4;
	var songAsJSON = JSON.stringify(songAsModFile, null, jsonTabSizeInSpaces);
	var textareaSongAsJSON = document.getElementById("textareaSongAsJSON");
	textareaSongAsJSON.value = songAsJSON;
}
	
// classes

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

	this.numberOfBytesTotal = this.bytes.length;
	this.byteIndexCurrent = 0;
}

{
	ByteStreamBigEndian.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent &lt; this.numberOfBytesTotal);
	}

	ByteStreamBigEndian.prototype.peekBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

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

		return returnValue;
	}

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

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

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteIndexCurrent];

		this.byteIndexCurrent++;

		return returnValue;
	}

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

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readShort = function()
	{
		var returnValue =
		(
			(this.readByte() &amp; 0xFF) &lt;&lt; 8
			| ((this.readByte() &amp; 0xFF) )
		);

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readString = function(numberOfBytesToRead)
	{
		var returnValue = &quot;&quot;;

		for (var b = 0; b &lt; numberOfBytesToRead; b++)
		{
			var charAsByte = this.readByte();
			returnValue += String.fromCharCode(charAsByte);
		}

		return returnValue;
	}

	ByteStreamBigEndian.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b &gt;&gt; 24 );
		this.bytes.push( (integerToWrite &amp; 0x00FF0000) &gt;&gt;&gt; 16 );
		this.bytes.push( (integerToWrite &amp; 0x0000FF00) &gt;&gt;&gt; 8 );
		this.bytes.push( (integerToWrite &amp; 0x000000FF) );

		this.byteIndexCurrent += 4;
	}

	ByteStreamBigEndian.prototype.writeShort = function(shortToWrite)
	{
		this.bytes.push( (shortToWrite &amp; 0xFF00) &gt;&gt;&gt; 8 );
		this.bytes.push( (shortToWrite &amp; 0x00FF) );

		this.byteIndexCurrent += 2;
	}

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

function FileHelper()
{
	// static class
}

{
	FileHelper.loadFileAsBytes = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent)
		{
			var fileContentsAsBinaryString = fileLoadedEvent.target.result;
			var fileContentsAsBytes = [];
			for (var i = 0; i &lt; fileContentsAsBinaryString.length; i++)
			{
				var byte = fileContentsAsBinaryString.charCodeAt(i);
				fileContentsAsBytes.push(byte);
			}
			callback(fileToLoad, fileContentsAsBytes);
		};
		fileReader.readAsBinaryString(fileToLoad);
	}

	FileHelper.saveBytesToFile = function(bytesToSave, filenameToSaveTo)
	{
		var numberOfBytes = bytesToSave.length;
		var bytesAsArrayBuffer = new ArrayBuffer(numberOfBytes);
		var bytesAsUIntArray = new Uint8Array(bytesAsArrayBuffer);
		for (var i = 0; i &lt; numberOfBytes; i++)
		{
			bytesAsUIntArray[i] = bytesToSave[i];
		}

		var bytesAsBlob = new Blob
		(
			[ bytesAsArrayBuffer ],
			{type:&quot;application/type&quot;}
		);

		var downloadLink = document.createElement(&quot;a&quot;);
		downloadLink.href = URL.createObjectURL(bytesAsBlob);
		downloadLink.download = filenameToSaveTo;
		downloadLink.click();
	}
}

function ModFile(title, instruments, sequenceIndicesToPlay, sequenceDefns)
{
	this.title = title;
	this.instruments = instruments;
	this.sequenceIndicesToPlay = sequenceIndicesToPlay;
	this.sequenceDefns = sequenceDefns;
}
{
	ModFile.fromBytes = function(bytes)
	{
		// Based on a description of the MOD file format
		// found at the URL &quot;https://www.aes.id.au/modformat.html&quot;.
		
		var reader = new ByteStreamBigEndian(bytes);
		
		var titlePadded = reader.readString(20);
		var title = titlePadded.substr(0, titlePadded.indexOf(&quot;\0&quot;));
		
		var instruments = [];
		var numberOfInstruments = 31; // Or maybe 15?
		var bytesPerWord = 2;
		 
		for (var i = 0; i = 8)
			{
				// todo - Two's complement.
			}
		
			var volumeCode = reader.readByte(); // 0 to 64.
			
			var repeatOffsetInWords = reader.readShort();
			var repeatLengthInWords = reader.readShort();
			
			var instrument = new ModFile_Instrument
			(
				instrumentName, 
				pitchShiftInSixteenthTones, 
				volumeCode, 
				repeatOffsetInWords, 
				repeatLengthInWords, 
				lengthOfSamplesInBytes,
				[] // samples
			);
			
			instruments.push(instrument);
		}
		
		var numberOfSequencesToPlay = reader.readByte(); // 1 to 128.
		var reserved = reader.readByte(); // Should be 127.
		var sequenceIndicesToPlay = reader.readBytes(128); // Each 0 - 63.
		
		var signatureOrSequenceDefnsStart = reader.readString(4);
		var signatureFor32InstrumentMode = "M.K.";
		
		if (signatureOrSequenceDefnsStart == signatureFor32InstrumentMode)
		{
			// Someone hacked the format to support 31 instruments, not 15.
		}
		else
		{
			// It wasn't a signature, it was the start of sequence definitions.
			// Back up 4 bytes.
			reader.byteIndexCurrent -= 4;
		}
		
		var divisionsPerSequence = 64; // Or "divisions".
		var numberOfChannels = 4; // 1 and 4 on left, 2 and 3 on right.
		var bytesPerDivisionPerChannel = 4;
		
		var sequenceDefns = [];
		
		for (var s = 0; s &lt; numberOfSequencesToPlay; s++)
		{
			var divisionsForSequenceDefn = [];
			
			for (var d = 0; d &lt; divisionsPerSequence; d++)
			{
				var channelsForDivision = [];
				
				for (var c = 0; c &gt; 4) &amp; 0xF)
						);
						
					var pitchCodeOrEffectParameter = 
						(
							( (bytesForDivisionAndChannel[0] &amp; 0xF) &lt;&lt; 8)
							| bytes[1]
						);
					
					// Effects
					// 0 - Arpeggio
					// 1 - Slide Up
					// 2 - Slide Down
					// 3 - Slide to Note
					// 4 - Vibrato
					// 5 - Continue Slide to Note with Volume Slide
					// 6 - Continue Vibrato with Volume Slide
					// 7 - Tremolo
					// 8 - Set Panning Position
					// 9 - Set Sample Offset
					// 10 - Volume Slide
					// 11 - Position Jump
					// 12 - Set Volume
					// 13 - Pattern Break
					// 14 - Extended
						// 1 - Fineslide Up
						// 2 - Fineslide Down
						// 3 - Toggle Glissando
						// 4 - Set Vibrato Waveform
						// 5 - Set Finetune Value
						// 6 - Loop Pattern
						// 7 - Set Tremolo Waveform
						// 8 - Reserved
						// 9 - Retrigger Sample
						// 10 - Fine Volume Slide Up
						// 11 - Fine Volume Slide Down
						// 12 - Cut Sample
						// 13 - Delay Sample
						// 14 - Delay Pattern
						// 15 - Invert Loop
					// 15 - Set Speed
						
					var effectCode = 
						(
							( (bytesForDivisionAndChannel[2] &amp; 0xF) &lt;&lt; 8)
							| bytes[3]
						);
					
					var channel = new ModFile_SequenceDefn_Division_Channel
					(
						instrumentIndex,
						pitchCodeOrEffectParameter,
						effectCode
					);
					
					channelsForDivision.push(channel);
										
				} // end for each channel
				
				var division = new ModFile_SequenceDefn_Division(channelsForDivision);
				divisionsForSequenceDefn.push(division);

			} // end for each division
			
			var sequenceDefn = new ModFile_SequenceDefn
			(
				divisionsForSequenceDefn
			);
			
		} // end for each sequence defn
		
		for (var i = 0; i &lt; instruments.length; i++)
		{
			var instrument = instruments[i];
			var numberOfBytes = instrument.lengthOfSamplesInBytes;
			var samplesForInstrumentAsBytes = reader.readBytes(numberOfBytes);
			instrument.samplesAsBytes = samplesForInstrumentAsBytes;
		}
		
		var returnValue = new ModFile
		(
			title, 
			instruments,
			sequenceIndicesToPlay,
			sequenceDefns
		);
		
		return returnValue;

	} // end function fromBytes()
	
} // end class

function ModFile_Instrument
(
	name, 
	pitchShiftInSixteenthTones, 
	volumeCode, 
	repeatOffsetInWords, 
	repeatLengthInWords, 
	lengthOfSamplesInBytes,
	samplesAsBytes
)
{
	this.name = name;
	this.pitchShiftInSixteenthTones = pitchShiftInSixteenthTones;
	this.volumeCode = volumeCode;
	this.repeatOffsetInWords = repeatOffsetInWords;
	this.repeatLengthInWords = repeatLengthInWords;	
	this.lengthOfSamplesInBytes = lengthOfSamplesInBytes;
	this.samplesAsBytes = samplesAsBytes;
}

function ModFile_SequenceDefn(divisions)
{
	this.divisions = divisions;
}

function ModFile_SequenceDefn_Division(channels)
{
	this.channels = channels;
}

function ModFile_SequenceDefn_Division_Channel
(
	instrumentIndex, pitchCodeOrEffectParameter, effectCode
)
{
	this.instrumentIndex = instrumentIndex;
	this.pitchCodeOrEffectParameter = pitchCodeOrEffectParameter;
	this.effectCode = effectCode;
}






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

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s