/* 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");