
/*
	The OMINO MIDI FILE READER
	
	This ExtendScript library reads a Standard MIDI File (.mid) into memory, organizing the events
	in a friendly manner suitable for a variety of purposes.
	
	This version only captures note events. Controller info is all lost.
	
	To use it, you create it with a file path, and read info out of the various notes array. Here:
	
	var m = new MidiFile("/path/to/midifile.mid");
	
	var notes = m.notes; // array of NOTE events
	var note = notes[0]; // first note
	var pitch = note.pitch;
	var vel = note.vel; // if zero, it's the END of a note
	var dur = note.duration; // only for note-starts.
	var ch = note.channel; // 16 * trackIndex + midiChannel. note.channel % 16 for midiChannel.

also

	var tracks = m.tracks; // array with per-track goodies
	var channels = m.channesl; // array with per-channel goodies.
	
	Future enhancements:
	   • Write a MIDI file
	   • Properly track tempo changes
	   • Keep controller & pitch bend changes
	*/

/*
	This constructor takes a path to a MIDI file and, does a very light analysis of it.
	It does just enough to build up an array of note events, which includes the note on
	and note off as separate entries, in an array called notes.
	
	note.time is the time in seconds
	note.pitch
	note.vel is 0 for note off.
	*/
function MidiFile(filePath)
{
	addMidiFileMethods(this);

	this.microsecondsPerQuarterNote = 500000;

	this.filePath = filePath;
	this.file = readFile(filePath);
	this.fileLength = this.file.length;
	this.isMidi = isMidi(this.file);
	
	this.format = this.getShort(8);
	this.trackCount = this.getShort(10);
	this.timeDivision = this.getShort(12);
	if(this.timeDivision & 0x8000)
		this.framesPerSecond = this.timeDivision & 0x7fff;
	else
		this.ticksPerBeat = this.timeDivision; // more common.
	
	this.chunkCount = 0;
	this.noteOns = 0;
	this.noteOffs = 0;
	
	this.notes = new Array();
	this.channels = new Array();
	this.tracks = new Array();

	// read the rest of the chunks.
	var offset = 14;
	currentTrack = 0;
	while(offset < this.fileLength)
	{
		var chunkType = this.file.substring(offset,offset + 4);
		var chunkLength = this.getLong(offset + 4);
		this.chunkCount ++;
		
		if(chunkType == "MTrk")
		{
			var track = new Track();
			this.tracks.push(track);
			var previousStatus = 0;
			var chunkOffset = offset + 8;
			var chunkEnd = chunkOffset + chunkLength;
			var ticks = 0;
			var midiChannelPrefix = 0;
			while(chunkOffset < chunkEnd)
			{
				var partial = this.file.substring(chunkOffset,chunkEnd);
				var delta = this.getVarVal(chunkOffset);
				ticks += delta;

				var seconds = 0;
				var beats = 0;
				if(ticks && this.timeDivision && this.microsecondsPerQuarterNote)
				{
					seconds = ticks * this.microsecondsPerQuarterNote / this.timeDivision / 1000000;
					beats = ticks / this.timeDivision;
				}
				chunkOffset += this.getVarLen(chunkOffset);
				var status = this.file.charCodeAt(chunkOffset);
				if(status & 0x80)
					chunkOffset++;
				else
					status = previousStatus;
				
				var statusTop = (status & 0xf0) >> 4;
				var channel = (currentTrack) * 16 + (status & 0x0f);
				var b1 = this.file.charCodeAt(chunkOffset);
				var b2 = this.file.charCodeAt(chunkOffset + 1);
				
				if(status == 0xff)
					statusTop = status;
				switch(statusTop)
				{
					case 8: // note off
						this.addNote(seconds,beats,channel,b1,0);
						break;
					case 9: // note on (or note off if b2==0)
						this.addNote(seconds,beats,channel,b1,b2);
						break;
					case 0xff:
						{
							switch(b1)
							{
								case 0x03: // track name
									trackName = this.getVarString(chunkOffset + 1);
									track.name = trackName;
									break;
								case 0x04: // instrument name
									channel = (currentTrack) * 16 + midiChannelPrefix;
									var channelO = this.findChannel(channel);
									channelO.insrument = this.getVarString(chunkOffset + 1);
									break;
								case 0x20: // midi channel prefix
									midiChannelPrefix = this.getByte(chunkOffset + 2);
									break;
								case 0x51: // tempo
									this.microsecondsPerQuarterNote = this.getInt24(chunkOffset + 2);
									break;
								case 0x54: // sig
									var a = b2;
									break;
								case 0x58: // sig
									var a = b2;
									break;
							}
						}
						break;
					
				}
			
				var eventLength = this.getEventLength(status,chunkOffset);
				chunkOffset += eventLength;	
				previousStatus = status;
			}
			currentTrack++;
		}
		
		offset += 8 + chunkLength;
	}

	// sort the event-bucket
	this.notes.sort(function(a,b) { return a.time - b.time; });

}




function readFile(filePath)
{
	var f = new File(filePath);
	f.encoding = "BINARY";

	f.open ("r");
	var length = f.length;
	var result = f.read(length);
	f.close();
	return result;
}

function isMidi(s)
{
	var h = s.substring(0,4);
	var result = h == "MThd";
	return result;
}

// time in seconds
function Note(time,beats,channel,pitch,vel)
{
	this.time = time;
	this.beats = beats;
	this.channel = channel;
	this.pitch = pitch;
	this.vel = vel;
}

function Channel(index)
{
	this.index = index;
	this.trackIndex = Math.floor(index / 16);
	this.midiChannel = index % 16;
	this.notes = new Array();
}

function Track(index)
{
	this.index = index;
	this.channels = new Array();
}

function addMidiFileMethods(m)
{
	m.getShort = function(offset) {
		var result = this.file.charCodeAt(offset) * 256
				+ this.file.charCodeAt(offset + 1);
		return result;
	}

	m.getLong = function(offset) {
		var result = this.file.charCodeAt(offset) * (1<<24)
				+ this.file.charCodeAt(offset + 1) * (1<<16)
				+ this.file.charCodeAt(offset + 2) * (1<<8)
				+ this.file.charCodeAt(offset + 3);
		return result;
	}

	m.getByte = function(offset) {
		var result = this.file.charCodeAt(offset);
		return result;
	}

	m.getInt24 = function(offset) {
		var result = this.file.charCodeAt(offset) * (1<<16)
				+ this.file.charCodeAt(offset + 1) * (1<<8)
				+ this.file.charCodeAt(offset + 2) * (1<<0);
		return result;
	}

	/*
		Find a channel reference, or create it if needed, to assign.
		index is trackIndex * 16 + channel
		*/
	m.findChannel = function(index)
	{
		var channel = this.channels[index];
		if(!channel)
		{
			channel = new Channel(index);
			this.channels[index] = channel;
			var track = this.tracks[Math.floor(index / 16)]; // it MUST already be allocated.
			var midiChannel = index % 16;
			track.channels.push(channel);
		}
		return channel;
	}

	m.addNote = function(time,beats,channel,pitch,vel)
	{
		var note = new Note(time,beats,channel,pitch,vel);
		this.notes.push(note);
		
		var channelO = this.findChannel(channel);
		channelO.notes.push(note);

		if(vel)
		{
			this.noteOns++;
		}
		else
		{
			this.noteOffs++;
			// note-off? try to assign duration to a note-on
			for(var i = channelO.notes.length - 2; i >= 0; i--)
			{
				var note2 = channelO.notes[i];
				if(note2.vel && note2.pitch == pitch)
				{
					note2.durTime= time - note2.time;
					note2.durBeats = beats - note2.beats;
					i = 0;
//					break;
				}
			}
		}
		
		return note;
	}

	m.getEventLength = function(status,offset)
	{
		var statusTop = (status & 0xf0) >> 4;
		switch(statusTop)
		{
			case 0x8:
			case 0x9:
			case 0xa:
			case 0xb:
			case 0xe:
				return 2;
			case 0xc:
			case 0xd:
				return 1;
		}
		
		if(status == 0xff || status == 0xf0) // meta or sysex
		{
			var result = this.getVarVal(offset + 1);
			result += this.getVarLen(offset + 1);
			result += 1;
			return result;

// meta events
//~ 				case 0: // sequence number short
//~ 				case 1: //text event
//~ 				case 2: // copyright
//~ 				case 3: // seq/trk name
//~ 				case 4: // instrument name
//~ 				case 5: // lyrics
//~ 				case 6: // marker
//~ 				case 7: // cue point
//~ 				case 0x20: // midi channel for next instrument name
//~ 				case 0x2f: // end of track
//~ 				case 0x51: // tempo
//~ 				case 0x54: // smpte offset
//~ 				case 0x58: // time signature
//~ 				case 0x59: // key signature
//~ 				case 0x7f: // sequencer-specific
		}


	}

	m.getVarLen = function(offset) {
		var result = 1;
		while(1) {
			if(this.file.charCodeAt(offset) & 0x80)
			{
				result++;
				offset++;
			}
			else
				return result;
		}
	}

	m.getVarVal = function(offset) {
		var result = 0;
		while(1) {
			var b = this.file.charCodeAt(offset);
			result = result * 128 + (b & 0x7f);
			if(b & 0x80)
				offset++;
			else
				return result;
		}
	}

	m.getVarString = function(offset) {
		var result = "";
		var len = this.getVarVal(offset);
		var lenlen = this.getVarLen(offset);
		result = this.file.substring(offset + lenlen,offset + lenlen + len);
		return result;
	}

}

// Array Remove - By John Resig (MIT Licensed)
function remove(arr,from, to) {
  var rest = arr.slice((to || from) + 1 || arr.length);
  arr.length = from < 0 ? arr.length + from : from;
  return arr.push.apply(arr, rest);
};

function midiReaderTest1(mf)
{
	
	var d = "/Users/poly/Sites/src/adobe_scripts/in_progress/";
	var p = d + mf;
	m = new MidiFile(p);
	if(!m.isMidi)
	{
		alert(p + " is not midi!");
		return;
	}

	var alertString = 
			", chunkCount: " + m.chunkCount
			+ ", tracksFound: " + m.tracks.length
			+ ", noteOns: " + m.noteOns
			+ ", noteOffs: " + m.noteOffs
			;
	
	for(var i in m.tracks)
	{
		var track = m.tracks[i];
		alertString += "\n" + track.name + "[" + track.channels.length + "],";
	}

	var toShow = m.notes.length;
	if(toShow > 40)
		toShow = 40;
	for(var i = 0; i < toShow; i++)
	{
		var note = m.notes[i];
		alertString += "\n" + note.time + ": " + note.pitch + "/" + note.vel + " on " + note.channel + " beats:" + note.beats;
		if(note.durTime)
			alertString += "--- dur = " + note.durBeats + "---";
		}

	alert(alertString);
}

//midiReaderTest1("moreNotes.mid");
//midiReaderTest1("Gimme_Gimme_Gimme.mid");


