|  | @@ -0,0 +1,238 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | +class to parse the .mid file format
 | 
	
		
			
				|  |  | +(depends on stream.js)
 | 
	
		
			
				|  |  | +*/
 | 
	
		
			
				|  |  | +function MidiFile(data) {
 | 
	
		
			
				|  |  | +	function readChunk(stream) {
 | 
	
		
			
				|  |  | +		var id = stream.read(4);
 | 
	
		
			
				|  |  | +		var length = stream.readInt32();
 | 
	
		
			
				|  |  | +		return {
 | 
	
		
			
				|  |  | +			'id': id,
 | 
	
		
			
				|  |  | +			'length': length,
 | 
	
		
			
				|  |  | +			'data': stream.read(length)
 | 
	
		
			
				|  |  | +		};
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	var lastEventTypeByte;
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	function readEvent(stream) {
 | 
	
		
			
				|  |  | +		var event = {};
 | 
	
		
			
				|  |  | +		event.deltaTime = stream.readVarInt();
 | 
	
		
			
				|  |  | +		var eventTypeByte = stream.readInt8();
 | 
	
		
			
				|  |  | +		if ((eventTypeByte & 0xf0) == 0xf0) {
 | 
	
		
			
				|  |  | +			/* system / meta event */
 | 
	
		
			
				|  |  | +			if (eventTypeByte == 0xff) {
 | 
	
		
			
				|  |  | +				/* meta event */
 | 
	
		
			
				|  |  | +				event.type = 'meta';
 | 
	
		
			
				|  |  | +				var subtypeByte = stream.readInt8();
 | 
	
		
			
				|  |  | +				var length = stream.readVarInt();
 | 
	
		
			
				|  |  | +				switch(subtypeByte) {
 | 
	
		
			
				|  |  | +					case 0x00:
 | 
	
		
			
				|  |  | +						event.subtype = 'sequenceNumber';
 | 
	
		
			
				|  |  | +						if (length != 2) throw "Expected length for sequenceNumber event is 2, got " + length;
 | 
	
		
			
				|  |  | +						event.number = stream.readInt16();
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x01:
 | 
	
		
			
				|  |  | +						event.subtype = 'text';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x02:
 | 
	
		
			
				|  |  | +						event.subtype = 'copyrightNotice';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x03:
 | 
	
		
			
				|  |  | +						event.subtype = 'trackName';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x04:
 | 
	
		
			
				|  |  | +						event.subtype = 'instrumentName';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x05:
 | 
	
		
			
				|  |  | +						event.subtype = 'lyrics';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x06:
 | 
	
		
			
				|  |  | +						event.subtype = 'marker';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x07:
 | 
	
		
			
				|  |  | +						event.subtype = 'cuePoint';
 | 
	
		
			
				|  |  | +						event.text = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x20:
 | 
	
		
			
				|  |  | +						event.subtype = 'midiChannelPrefix';
 | 
	
		
			
				|  |  | +						if (length != 1) throw "Expected length for midiChannelPrefix event is 1, got " + length;
 | 
	
		
			
				|  |  | +						event.channel = stream.readInt8();
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x2f:
 | 
	
		
			
				|  |  | +						event.subtype = 'endOfTrack';
 | 
	
		
			
				|  |  | +						if (length != 0) throw "Expected length for endOfTrack event is 0, got " + length;
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x51:
 | 
	
		
			
				|  |  | +						event.subtype = 'setTempo';
 | 
	
		
			
				|  |  | +						if (length != 3) throw "Expected length for setTempo event is 3, got " + length;
 | 
	
		
			
				|  |  | +						event.microsecondsPerBeat = (
 | 
	
		
			
				|  |  | +							(stream.readInt8() << 16)
 | 
	
		
			
				|  |  | +							+ (stream.readInt8() << 8)
 | 
	
		
			
				|  |  | +							+ stream.readInt8()
 | 
	
		
			
				|  |  | +						)
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x54:
 | 
	
		
			
				|  |  | +						event.subtype = 'smpteOffset';
 | 
	
		
			
				|  |  | +						if (length != 5) throw "Expected length for smpteOffset event is 5, got " + length;
 | 
	
		
			
				|  |  | +						var hourByte = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.frameRate = {
 | 
	
		
			
				|  |  | +							0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30
 | 
	
		
			
				|  |  | +						}[hourByte & 0x60];
 | 
	
		
			
				|  |  | +						event.hour = hourByte & 0x1f;
 | 
	
		
			
				|  |  | +						event.min = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.sec = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.frame = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.subframe = stream.readInt8();
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x58:
 | 
	
		
			
				|  |  | +						event.subtype = 'timeSignature';
 | 
	
		
			
				|  |  | +						if (length != 4) throw "Expected length for timeSignature event is 4, got " + length;
 | 
	
		
			
				|  |  | +						event.numerator = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.denominator = Math.pow(2, stream.readInt8());
 | 
	
		
			
				|  |  | +						event.metronome = stream.readInt8();
 | 
	
		
			
				|  |  | +						event.thirtyseconds = stream.readInt8();
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x59:
 | 
	
		
			
				|  |  | +						event.subtype = 'keySignature';
 | 
	
		
			
				|  |  | +						if (length != 2) throw "Expected length for keySignature event is 2, got " + length;
 | 
	
		
			
				|  |  | +						event.key = stream.readInt8(true);
 | 
	
		
			
				|  |  | +						event.scale = stream.readInt8();
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					case 0x7f:
 | 
	
		
			
				|  |  | +						event.subtype = 'sequencerSpecific';
 | 
	
		
			
				|  |  | +						event.data = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +					default:
 | 
	
		
			
				|  |  | +						// console.log("Unrecognised meta event subtype: " + subtypeByte);
 | 
	
		
			
				|  |  | +						event.subtype = 'unknown'
 | 
	
		
			
				|  |  | +						event.data = stream.read(length);
 | 
	
		
			
				|  |  | +						return event;
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +				event.data = stream.read(length);
 | 
	
		
			
				|  |  | +				return event;
 | 
	
		
			
				|  |  | +			} else if (eventTypeByte == 0xf0) {
 | 
	
		
			
				|  |  | +				event.type = 'sysEx';
 | 
	
		
			
				|  |  | +				var length = stream.readVarInt();
 | 
	
		
			
				|  |  | +				event.data = stream.read(length);
 | 
	
		
			
				|  |  | +				return event;
 | 
	
		
			
				|  |  | +			} else if (eventTypeByte == 0xf7) {
 | 
	
		
			
				|  |  | +				event.type = 'dividedSysEx';
 | 
	
		
			
				|  |  | +				var length = stream.readVarInt();
 | 
	
		
			
				|  |  | +				event.data = stream.read(length);
 | 
	
		
			
				|  |  | +				return event;
 | 
	
		
			
				|  |  | +			} else {
 | 
	
		
			
				|  |  | +				throw "Unrecognised MIDI event type byte: " + eventTypeByte;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		} else {
 | 
	
		
			
				|  |  | +			/* channel event */
 | 
	
		
			
				|  |  | +			var param1;
 | 
	
		
			
				|  |  | +			if ((eventTypeByte & 0x80) == 0) {
 | 
	
		
			
				|  |  | +				/* running status - reuse lastEventTypeByte as the event type.
 | 
	
		
			
				|  |  | +					eventTypeByte is actually the first parameter
 | 
	
		
			
				|  |  | +				*/
 | 
	
		
			
				|  |  | +				param1 = eventTypeByte;
 | 
	
		
			
				|  |  | +				eventTypeByte = lastEventTypeByte;
 | 
	
		
			
				|  |  | +			} else {
 | 
	
		
			
				|  |  | +				param1 = stream.readInt8();
 | 
	
		
			
				|  |  | +				lastEventTypeByte = eventTypeByte;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			var eventType = eventTypeByte >> 4;
 | 
	
		
			
				|  |  | +			event.channel = eventTypeByte & 0x0f;
 | 
	
		
			
				|  |  | +			event.type = 'channel';
 | 
	
		
			
				|  |  | +			switch (eventType) {
 | 
	
		
			
				|  |  | +				case 0x08:
 | 
	
		
			
				|  |  | +					event.subtype = 'noteOff';
 | 
	
		
			
				|  |  | +					event.noteNumber = param1;
 | 
	
		
			
				|  |  | +					event.velocity = stream.readInt8();
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x09:
 | 
	
		
			
				|  |  | +					event.noteNumber = param1;
 | 
	
		
			
				|  |  | +					event.velocity = stream.readInt8();
 | 
	
		
			
				|  |  | +					if (event.velocity == 0) {
 | 
	
		
			
				|  |  | +						event.subtype = 'noteOff';
 | 
	
		
			
				|  |  | +					} else {
 | 
	
		
			
				|  |  | +						event.subtype = 'noteOn';
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x0a:
 | 
	
		
			
				|  |  | +					event.subtype = 'noteAftertouch';
 | 
	
		
			
				|  |  | +					event.noteNumber = param1;
 | 
	
		
			
				|  |  | +					event.amount = stream.readInt8();
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x0b:
 | 
	
		
			
				|  |  | +					event.subtype = 'controller';
 | 
	
		
			
				|  |  | +					event.controllerType = param1;
 | 
	
		
			
				|  |  | +					event.value = stream.readInt8();
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x0c:
 | 
	
		
			
				|  |  | +					event.subtype = 'programChange';
 | 
	
		
			
				|  |  | +					event.programNumber = param1;
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x0d:
 | 
	
		
			
				|  |  | +					event.subtype = 'channelAftertouch';
 | 
	
		
			
				|  |  | +					event.amount = param1;
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				case 0x0e:
 | 
	
		
			
				|  |  | +					event.subtype = 'pitchBend';
 | 
	
		
			
				|  |  | +					event.value = param1 + (stream.readInt8() << 7);
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +				default:
 | 
	
		
			
				|  |  | +					throw "Unrecognised MIDI event type: " + eventType
 | 
	
		
			
				|  |  | +					/* 
 | 
	
		
			
				|  |  | +					console.log("Unrecognised MIDI event type: " + eventType);
 | 
	
		
			
				|  |  | +					stream.readInt8();
 | 
	
		
			
				|  |  | +					event.subtype = 'unknown';
 | 
	
		
			
				|  |  | +					return event;
 | 
	
		
			
				|  |  | +					*/
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	stream = Stream(data);
 | 
	
		
			
				|  |  | +	var headerChunk = readChunk(stream);
 | 
	
		
			
				|  |  | +	if (headerChunk.id != 'MThd' || headerChunk.length != 6) {
 | 
	
		
			
				|  |  | +		throw "Bad .mid file - header not found";
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	var headerStream = Stream(headerChunk.data);
 | 
	
		
			
				|  |  | +	var formatType = headerStream.readInt16();
 | 
	
		
			
				|  |  | +	var trackCount = headerStream.readInt16();
 | 
	
		
			
				|  |  | +	var timeDivision = headerStream.readInt16();
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	if (timeDivision & 0x8000) {
 | 
	
		
			
				|  |  | +		throw "Expressing time division in SMTPE frames is not supported yet"
 | 
	
		
			
				|  |  | +	} else {
 | 
	
		
			
				|  |  | +		ticksPerBeat = timeDivision;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	var header = {
 | 
	
		
			
				|  |  | +		'formatType': formatType,
 | 
	
		
			
				|  |  | +		'trackCount': trackCount,
 | 
	
		
			
				|  |  | +		'ticksPerBeat': ticksPerBeat
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	var tracks = [];
 | 
	
		
			
				|  |  | +	for (var i = 0; i < header.trackCount; i++) {
 | 
	
		
			
				|  |  | +		tracks[i] = [];
 | 
	
		
			
				|  |  | +		var trackChunk = readChunk(stream);
 | 
	
		
			
				|  |  | +		if (trackChunk.id != 'MTrk') {
 | 
	
		
			
				|  |  | +			throw "Unexpected chunk - expected MTrk, got "+ trackChunk.id;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		var trackStream = Stream(trackChunk.data);
 | 
	
		
			
				|  |  | +		while (!trackStream.eof()) {
 | 
	
		
			
				|  |  | +			var event = readEvent(trackStream);
 | 
	
		
			
				|  |  | +			tracks[i].push(event);
 | 
	
		
			
				|  |  | +			//console.log(event);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	return {
 | 
	
		
			
				|  |  | +		'header': header,
 | 
	
		
			
				|  |  | +		'tracks': tracks
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 |