├── .gitignore ├── index.js ├── package.json ├── LICENSE.md ├── README.md ├── index.d.ts ├── lib ├── midi-writer.js └── midi-parser.js └── tests └── tsd.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.parseMidi = require('./lib/midi-parser') 2 | exports.writeMidi = require('./lib/midi-writer') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midi-file", 3 | "version": "1.2.4", 4 | "description": "Parse and write MIDI files", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "ts-node ./tests/tsd.ts" 9 | }, 10 | "author": "Carter Thaxton", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/carter-thaxton/midi-file.git" 15 | }, 16 | "keywords": [ 17 | "MIDI", 18 | "file", 19 | "parse", 20 | "write" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/carter-thaxton/midi-file/issues" 24 | }, 25 | "homepage": "https://github.com/carter-thaxton/midi-file#readme", 26 | "devDependencies": { 27 | "@types/node": "^16.11.10", 28 | "ts-node": "^10.4.0", 29 | "typescript": "^4.5.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | [The MIT License](http://opensource.org/licenses/MIT) 2 | 3 | Copyright © 2016 Carter Thaxton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # midi-file 2 | 3 | Install with `yarn add midi-file` or `npm install midi-file`. 4 | 5 | The parser is loosely based on [midi-file-parser](https://github.com/NHQ/midi-file-parser) and [jasmid](https://github.com/gasman/jasmid), but totally rewritten to use arrays instead of strings for portability. 6 | 7 | ## Typescript Usage 8 | 9 | ```Typescript 10 | import * as fs from 'fs'; 11 | import * as midiManager from 'midi-file'; 12 | 13 | // Read MIDI file into a buffer 14 | const input = fs.readFileSync('star_wars.mid'); 15 | 16 | // Convert buffer to midi object 17 | const parsed = midiManager.parseMidi(input); 18 | 19 | // Convert object to midi buffer 20 | const output = midiManager.writeMidi(parsed); 21 | 22 | // Write into file 23 | const outputBuffer = Buffer.from(output); 24 | fs.writeFileSync('copy_star_wars.mid', outputBuffer); 25 | ``` 26 | 27 | ## Raw Javascript Usage 28 | 29 | ```js 30 | var fs = require('fs') 31 | var parseMidi = require('midi-file').parseMidi 32 | var writeMidi = require('midi-file').writeMidi 33 | 34 | // Read MIDI file into a buffer 35 | var input = fs.readFileSync('star_wars.mid') 36 | 37 | // Parse it into an intermediate representation 38 | // This will take any array-like object. It just needs to support .length, .slice, and the [] indexed element getter. 39 | // Buffers do that, so do native JS arrays, typed arrays, etc. 40 | var parsed = parseMidi(input) 41 | 42 | // Turn the intermediate representation back into raw bytes 43 | var output = writeMidi(parsed) 44 | 45 | // Note that the output is simply an array of byte values. writeFileSync wants a buffer, so this will convert accordingly. 46 | // Using native Javascript arrays makes the code portable to the browser or non-node environments 47 | var outputBuffer = Buffer.from(output) 48 | 49 | // Write to a new MIDI file. it should match the original 50 | fs.writeFileSync('copy_star_wars.mid', outputBuffer) 51 | ``` 52 | 53 | The intermediate representation has a 'header' and 'tracks', and each track is an array of events. 54 | 55 | ## Explicit Formatting 56 | 57 | Options are provided to `writeMidi` to control various ambiguities in the MIDI file format. 58 | 59 | The following will use byte 0x09 for noteOff messages with velocity zero. (Typically such messages use 0x08). 60 | It will also use running status bytes to compress consecutive events when possible. 61 | 62 | ```js 63 | var output = writeMidi(parsed, { useByte9ForNoteOff: true, running: true }) 64 | ``` 65 | 66 | When parsing the file with `readMidi`, each compressed event using running status bytes will have a `running` flag set on it. 67 | Similarly, each `noteOff` event that was encoded using 0x09 will have a `byte9` property set on it. 68 | 69 | By default, `writeMidi` will defer to each event to indicate the behavior it should use for encoding such ambiguities, which will produce an exact copy of the original file read with `parseMidi`. However, these options to `writeMidi` allow the behavior to be overridden at the top-level, which may be relevant if you are generating the MIDI events, rather than just reading them from a file. 70 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface MidiHeader { 2 | format: 0 | 1 | 2; 3 | numTracks: number; 4 | timeDivision?: number; 5 | framesPerSecond?: number; 6 | ticksPerFrame?: number; 7 | ticksPerBeat?: number; 8 | } 9 | export interface MidiBaseEvent { 10 | deltaTime: number; 11 | type: T; 12 | } 13 | export interface MidiMetaEvent extends MidiBaseEvent { 14 | meta?: true; 15 | } 16 | export interface MidiSmpteOffsetMixins { 17 | frameRate: 24 | 25 | 26 | 29 | 30; 18 | hour: number; 19 | min: number; 20 | sec: number; 21 | frame: number; 22 | subFrame: number; 23 | } 24 | export interface MidiTimeSignatureMixins { 25 | numerator: number; 26 | denominator: number; 27 | metronome: number; 28 | thirtyseconds: number; 29 | } 30 | export interface MidiKeySignatureMixins { 31 | key: number; 32 | scale: number; 33 | } 34 | export interface MidiDataMixins { 35 | data: ArrayLike; 36 | } 37 | export interface MidiUnknownMixins { 38 | data: ArrayLike; 39 | metatypeByte: number; 40 | } 41 | export interface MidiNumberMixins { 42 | number: number; 43 | } 44 | export interface MidiTextMixins { 45 | text: string; 46 | } 47 | export type MidiSequenceNumberEvent = MidiMetaEvent<"sequenceNumber"> & 48 | MidiNumberMixins; 49 | export type MidiTextEvent = MidiMetaEvent<"text"> & MidiTextMixins; 50 | export type MidiCopyrightNoticeEvent = MidiMetaEvent<"copyrightNotice"> & 51 | MidiTextMixins; 52 | export type MidiTrackNameEvent = MidiMetaEvent<"trackName"> & MidiTextMixins; 53 | export type MidiInstrumentNameEvent = MidiMetaEvent<"instrumentName"> & 54 | MidiTextMixins; 55 | export type MidiLyricsEvent = MidiMetaEvent<"lyrics"> & MidiTextMixins; 56 | export type MidiMarkerEvent = MidiMetaEvent<"marker"> & MidiTextMixins; 57 | export type MidiCuePointEvent = MidiMetaEvent<"cuePoint"> & MidiTextMixins; 58 | export type MidiChannelPrefixEvent = MidiMetaEvent<"channelPrefix"> & { 59 | channel: number; 60 | }; 61 | export type MidiPortPrefixEvent = MidiMetaEvent<"portPrefix"> & { 62 | port: number; 63 | }; 64 | export type MidiEndOfTrackEvent = MidiMetaEvent<"endOfTrack">; 65 | export type MidiSetTempoEvent = MidiMetaEvent<"setTempo"> & { 66 | microsecondsPerBeat: number; 67 | }; 68 | export type MidiSmpteOffsetEvent = MidiMetaEvent<"smpteOffset"> & 69 | MidiSmpteOffsetMixins; 70 | export type MidiTimeSignatureEvent = MidiMetaEvent<"timeSignature"> & 71 | MidiTimeSignatureMixins; 72 | export type MidiKeySignatureEvent = MidiMetaEvent<"keySignature"> & 73 | MidiKeySignatureMixins; 74 | export type MidiSequencerSpecificEvent = MidiMetaEvent<"sequencerSpecific"> & 75 | MidiDataMixins; 76 | export type MidiUnknownEvent = MidiMetaEvent<"unknownMeta"> & MidiUnknownMixins; 77 | 78 | export type MidiSysExEvent = MidiBaseEvent<"sysEx"> & MidiDataMixins; 79 | export type MidiEndSysExEvent = MidiBaseEvent<"endSysEx"> & MidiDataMixins; 80 | 81 | export interface MidiNoteMixins { 82 | noteNumber: number; 83 | velocity: number; 84 | byte9?: true; 85 | } 86 | export interface MidiNoteAftertouchMixins { 87 | noteNumber: number; 88 | amount: number; 89 | } 90 | export interface MidiControllerMixins { 91 | controllerType: number; 92 | value: number; 93 | } 94 | export interface MidiChannelEvent extends MidiBaseEvent { 95 | running?: true; 96 | channel: number; 97 | } 98 | export type MidiNoteOnEvent = MidiChannelEvent<"noteOn"> & MidiNoteMixins; 99 | export type MidiNoteOffEvent = MidiChannelEvent<"noteOff"> & MidiNoteMixins; 100 | export type MidiNoteAftertouchEvent = MidiChannelEvent<"noteAftertouch"> & 101 | MidiNoteAftertouchMixins; 102 | export type MidiControllerEvent = MidiChannelEvent<"controller"> & 103 | MidiControllerMixins; 104 | export type MidiProgramChangeEvent = MidiChannelEvent<"programChange"> & { 105 | programNumber: number; 106 | }; 107 | export type MidiChannelAftertouchEvent = 108 | MidiChannelEvent<"channelAftertouch"> & { 109 | amount: number; 110 | }; 111 | export type MidiPitchBendEvent = MidiChannelEvent<"pitchBend"> & { 112 | value: number; 113 | }; 114 | 115 | export type MidiEvent = 116 | | MidiSequenceNumberEvent 117 | | MidiTextEvent 118 | | MidiCopyrightNoticeEvent 119 | | MidiTrackNameEvent 120 | | MidiInstrumentNameEvent 121 | | MidiLyricsEvent 122 | | MidiMarkerEvent 123 | | MidiCuePointEvent 124 | | MidiChannelPrefixEvent 125 | | MidiPortPrefixEvent 126 | | MidiEndOfTrackEvent 127 | | MidiSetTempoEvent 128 | | MidiSmpteOffsetEvent 129 | | MidiTimeSignatureEvent 130 | | MidiKeySignatureEvent 131 | | MidiSequencerSpecificEvent 132 | | MidiUnknownEvent 133 | | MidiSysExEvent 134 | | MidiEndSysExEvent 135 | | MidiControllerEvent 136 | | MidiProgramChangeEvent 137 | | MidiChannelAftertouchEvent 138 | | MidiPitchBendEvent 139 | | MidiNoteAftertouchEvent 140 | | MidiNoteOnEvent 141 | | MidiNoteOffEvent; 142 | 143 | export interface MidiData { 144 | header: MidiHeader; 145 | tracks: Array; 146 | } 147 | 148 | export function parseMidi(data: ArrayLike): MidiData; 149 | 150 | export interface MidiWriteOption { 151 | running?: boolean; //reuse previous eventTypeByte when possible, to compress file 152 | useByte9ForNoteOff?: boolean; //use 0x09 for noteOff when velocity is zero 153 | } 154 | export function writeMidi( 155 | data: MidiData, 156 | opts?: MidiWriteOption 157 | ): Array; 158 | -------------------------------------------------------------------------------- /lib/midi-writer.js: -------------------------------------------------------------------------------- 1 | // data should be the same type of format returned by parseMidi 2 | // for maximum compatibililty, returns an array of byte values, suitable for conversion to Buffer, Uint8Array, etc. 3 | 4 | // opts: 5 | // - running reuse previous eventTypeByte when possible, to compress file 6 | // - useByte9ForNoteOff use 0x09 for noteOff when velocity is zero 7 | 8 | function writeMidi(data, opts) { 9 | if (typeof data !== 'object') 10 | throw 'Invalid MIDI data' 11 | 12 | opts = opts || {} 13 | 14 | var header = data.header || {} 15 | var tracks = data.tracks || [] 16 | var i, len = tracks.length 17 | 18 | var w = new Writer() 19 | writeHeader(w, header, len) 20 | 21 | for (i=0; i < len; i++) { 22 | writeTrack(w, tracks[i], opts) 23 | } 24 | 25 | return w.buffer 26 | } 27 | 28 | function writeHeader(w, header, numTracks) { 29 | var format = header.format == null ? 1 : header.format 30 | 31 | var timeDivision = 128 32 | if (header.timeDivision) { 33 | timeDivision = header.timeDivision 34 | } else if (header.ticksPerFrame && header.framesPerSecond) { 35 | timeDivision = (-(header.framesPerSecond & 0xFF) << 8) | (header.ticksPerFrame & 0xFF) 36 | } else if (header.ticksPerBeat) { 37 | timeDivision = header.ticksPerBeat & 0x7FFF 38 | } 39 | 40 | var h = new Writer() 41 | h.writeUInt16(format) 42 | h.writeUInt16(numTracks) 43 | h.writeUInt16(timeDivision) 44 | 45 | w.writeChunk('MThd', h.buffer) 46 | } 47 | 48 | function writeTrack(w, track, opts) { 49 | var t = new Writer() 50 | var i, len = track.length 51 | var eventTypeByte = null 52 | for (i=0; i < len; i++) { 53 | // Reuse last eventTypeByte when opts.running is set, or event.running is explicitly set on it. 54 | // parseMidi will set event.running for each event, so that we can get an exact copy by default. 55 | // Explicitly set opts.running to false, to override event.running and never reuse last eventTypeByte. 56 | if (opts.running === false || !opts.running && !track[i].running) eventTypeByte = null 57 | 58 | eventTypeByte = writeEvent(t, track[i], eventTypeByte, opts.useByte9ForNoteOff) 59 | } 60 | w.writeChunk('MTrk', t.buffer) 61 | } 62 | 63 | function writeEvent(w, event, lastEventTypeByte, useByte9ForNoteOff) { 64 | var type = event.type 65 | var deltaTime = event.deltaTime 66 | var text = event.text || '' 67 | var data = event.data || [] 68 | var eventTypeByte = null 69 | w.writeVarInt(deltaTime) 70 | 71 | switch (type) { 72 | // meta events 73 | case 'sequenceNumber': 74 | w.writeUInt8(0xFF) 75 | w.writeUInt8(0x00) 76 | w.writeVarInt(2) 77 | w.writeUInt16(event.number) 78 | break; 79 | 80 | case 'text': 81 | w.writeUInt8(0xFF) 82 | w.writeUInt8(0x01) 83 | w.writeVarInt(text.length) 84 | w.writeString(text) 85 | break; 86 | 87 | case 'copyrightNotice': 88 | w.writeUInt8(0xFF) 89 | w.writeUInt8(0x02) 90 | w.writeVarInt(text.length) 91 | w.writeString(text) 92 | break; 93 | 94 | case 'trackName': 95 | w.writeUInt8(0xFF) 96 | w.writeUInt8(0x03) 97 | w.writeVarInt(text.length) 98 | w.writeString(text) 99 | break; 100 | 101 | case 'instrumentName': 102 | w.writeUInt8(0xFF) 103 | w.writeUInt8(0x04) 104 | w.writeVarInt(text.length) 105 | w.writeString(text) 106 | break; 107 | 108 | case 'lyrics': 109 | w.writeUInt8(0xFF) 110 | w.writeUInt8(0x05) 111 | w.writeVarInt(text.length) 112 | w.writeString(text) 113 | break; 114 | 115 | case 'marker': 116 | w.writeUInt8(0xFF) 117 | w.writeUInt8(0x06) 118 | w.writeVarInt(text.length) 119 | w.writeString(text) 120 | break; 121 | 122 | case 'cuePoint': 123 | w.writeUInt8(0xFF) 124 | w.writeUInt8(0x07) 125 | w.writeVarInt(text.length) 126 | w.writeString(text) 127 | break; 128 | 129 | case 'channelPrefix': 130 | w.writeUInt8(0xFF) 131 | w.writeUInt8(0x20) 132 | w.writeVarInt(1) 133 | w.writeUInt8(event.channel) 134 | break; 135 | 136 | case 'portPrefix': 137 | w.writeUInt8(0xFF) 138 | w.writeUInt8(0x21) 139 | w.writeVarInt(1) 140 | w.writeUInt8(event.port) 141 | break; 142 | 143 | case 'endOfTrack': 144 | w.writeUInt8(0xFF) 145 | w.writeUInt8(0x2F) 146 | w.writeVarInt(0) 147 | break; 148 | 149 | case 'setTempo': 150 | w.writeUInt8(0xFF) 151 | w.writeUInt8(0x51) 152 | w.writeVarInt(3) 153 | w.writeUInt24(event.microsecondsPerBeat) 154 | break; 155 | 156 | case 'smpteOffset': 157 | w.writeUInt8(0xFF) 158 | w.writeUInt8(0x54) 159 | w.writeVarInt(5) 160 | var FRAME_RATES = { 24: 0x00, 25: 0x20, 29: 0x40, 30: 0x60 } 161 | var hourByte = (event.hour & 0x1F) | FRAME_RATES[event.frameRate] 162 | w.writeUInt8(hourByte) 163 | w.writeUInt8(event.min) 164 | w.writeUInt8(event.sec) 165 | w.writeUInt8(event.frame) 166 | w.writeUInt8(event.subFrame) 167 | break; 168 | 169 | case 'timeSignature': 170 | w.writeUInt8(0xFF) 171 | w.writeUInt8(0x58) 172 | w.writeVarInt(4) 173 | w.writeUInt8(event.numerator) 174 | var denominator = Math.floor((Math.log(event.denominator) / Math.LN2)) & 0xFF 175 | w.writeUInt8(denominator) 176 | w.writeUInt8(event.metronome) 177 | w.writeUInt8(event.thirtyseconds || 8) 178 | break; 179 | 180 | case 'keySignature': 181 | w.writeUInt8(0xFF) 182 | w.writeUInt8(0x59) 183 | w.writeVarInt(2) 184 | w.writeInt8(event.key) 185 | w.writeUInt8(event.scale) 186 | break; 187 | 188 | case 'sequencerSpecific': 189 | w.writeUInt8(0xFF) 190 | w.writeUInt8(0x7F) 191 | w.writeVarInt(data.length) 192 | w.writeBytes(data) 193 | break; 194 | 195 | case 'unknownMeta': 196 | if (event.metatypeByte != null) { 197 | w.writeUInt8(0xFF) 198 | w.writeUInt8(event.metatypeByte) 199 | w.writeVarInt(data.length) 200 | w.writeBytes(data) 201 | } 202 | break; 203 | 204 | // system-exclusive 205 | case 'sysEx': 206 | w.writeUInt8(0xF0) 207 | w.writeVarInt(data.length) 208 | w.writeBytes(data) 209 | break; 210 | 211 | case 'endSysEx': 212 | w.writeUInt8(0xF7) 213 | w.writeVarInt(data.length) 214 | w.writeBytes(data) 215 | break; 216 | 217 | // channel events 218 | case 'noteOff': 219 | // Use 0x90 when opts.useByte9ForNoteOff is set and velocity is zero, or when event.byte9 is explicitly set on it. 220 | // parseMidi will set event.byte9 for each event, so that we can get an exact copy by default. 221 | // Explicitly set opts.useByte9ForNoteOff to false, to override event.byte9 and always use 0x80 for noteOff events. 222 | var noteByte = ((useByte9ForNoteOff !== false && event.byte9) || (useByte9ForNoteOff && event.velocity == 0)) ? 0x90 : 0x80 223 | 224 | eventTypeByte = noteByte | event.channel 225 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 226 | w.writeUInt8(event.noteNumber) 227 | w.writeUInt8(event.velocity) 228 | break; 229 | 230 | case 'noteOn': 231 | eventTypeByte = 0x90 | event.channel 232 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 233 | w.writeUInt8(event.noteNumber) 234 | w.writeUInt8(event.velocity) 235 | break; 236 | 237 | case 'noteAftertouch': 238 | eventTypeByte = 0xA0 | event.channel 239 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 240 | w.writeUInt8(event.noteNumber) 241 | w.writeUInt8(event.amount) 242 | break; 243 | 244 | case 'controller': 245 | eventTypeByte = 0xB0 | event.channel 246 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 247 | w.writeUInt8(event.controllerType) 248 | w.writeUInt8(event.value) 249 | break; 250 | 251 | case 'programChange': 252 | eventTypeByte = 0xC0 | event.channel 253 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 254 | w.writeUInt8(event.programNumber) 255 | break; 256 | 257 | case 'channelAftertouch': 258 | eventTypeByte = 0xD0 | event.channel 259 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 260 | w.writeUInt8(event.amount) 261 | break; 262 | 263 | case 'pitchBend': 264 | eventTypeByte = 0xE0 | event.channel 265 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 266 | var value14 = 0x2000 + event.value 267 | var lsb14 = (value14 & 0x7F) 268 | var msb14 = (value14 >> 7) & 0x7F 269 | w.writeUInt8(lsb14) 270 | w.writeUInt8(msb14) 271 | break; 272 | 273 | default: 274 | throw 'Unrecognized event type: ' + type 275 | } 276 | return eventTypeByte 277 | } 278 | 279 | 280 | function Writer() { 281 | this.buffer = [] 282 | } 283 | 284 | Writer.prototype.writeUInt8 = function(v) { 285 | this.buffer.push(v & 0xFF) 286 | } 287 | Writer.prototype.writeInt8 = Writer.prototype.writeUInt8 288 | 289 | Writer.prototype.writeUInt16 = function(v) { 290 | var b0 = (v >> 8) & 0xFF, 291 | b1 = v & 0xFF 292 | 293 | this.writeUInt8(b0) 294 | this.writeUInt8(b1) 295 | } 296 | Writer.prototype.writeInt16 = Writer.prototype.writeUInt16 297 | 298 | Writer.prototype.writeUInt24 = function(v) { 299 | var b0 = (v >> 16) & 0xFF, 300 | b1 = (v >> 8) & 0xFF, 301 | b2 = v & 0xFF 302 | 303 | this.writeUInt8(b0) 304 | this.writeUInt8(b1) 305 | this.writeUInt8(b2) 306 | } 307 | Writer.prototype.writeInt24 = Writer.prototype.writeUInt24 308 | 309 | Writer.prototype.writeUInt32 = function(v) { 310 | var b0 = (v >> 24) & 0xFF, 311 | b1 = (v >> 16) & 0xFF, 312 | b2 = (v >> 8) & 0xFF, 313 | b3 = v & 0xFF 314 | 315 | this.writeUInt8(b0) 316 | this.writeUInt8(b1) 317 | this.writeUInt8(b2) 318 | this.writeUInt8(b3) 319 | } 320 | Writer.prototype.writeInt32 = Writer.prototype.writeUInt32 321 | 322 | 323 | Writer.prototype.writeBytes = function(arr) { 324 | this.buffer = this.buffer.concat(Array.prototype.slice.call(arr, 0)) 325 | } 326 | 327 | Writer.prototype.writeString = function(str) { 328 | var i, len = str.length, arr = [] 329 | for (i=0; i < len; i++) { 330 | arr.push(str.codePointAt(i)) 331 | } 332 | this.writeBytes(arr) 333 | } 334 | 335 | Writer.prototype.writeVarInt = function(v) { 336 | if (v < 0) throw "Cannot write negative variable-length integer" 337 | 338 | if (v <= 0x7F) { 339 | this.writeUInt8(v) 340 | } else { 341 | var i = v 342 | var bytes = [] 343 | bytes.push(i & 0x7F) 344 | i >>= 7 345 | while (i) { 346 | var b = i & 0x7F | 0x80 347 | bytes.push(b) 348 | i >>= 7 349 | } 350 | this.writeBytes(bytes.reverse()) 351 | } 352 | } 353 | 354 | Writer.prototype.writeChunk = function(id, data) { 355 | this.writeString(id) 356 | this.writeUInt32(data.length) 357 | this.writeBytes(data) 358 | } 359 | 360 | module.exports = writeMidi 361 | -------------------------------------------------------------------------------- /lib/midi-parser.js: -------------------------------------------------------------------------------- 1 | // data can be any array-like object. It just needs to support .length, .slice, and an element getter [] 2 | 3 | function parseMidi(data) { 4 | var p = new Parser(data) 5 | 6 | var headerChunk = p.readChunk() 7 | if (headerChunk.id != 'MThd') 8 | throw "Bad MIDI file. Expected 'MHdr', got: '" + headerChunk.id + "'" 9 | var header = parseHeader(headerChunk.data) 10 | 11 | var tracks = [] 12 | for (var i=0; !p.eof() && i < header.numTracks; i++) { 13 | var trackChunk = p.readChunk() 14 | if (trackChunk.id != 'MTrk') 15 | throw "Bad MIDI file. Expected 'MTrk', got: '" + trackChunk.id + "'" 16 | var track = parseTrack(trackChunk.data) 17 | tracks.push(track) 18 | } 19 | 20 | return { 21 | header: header, 22 | tracks: tracks 23 | } 24 | } 25 | 26 | 27 | function parseHeader(data) { 28 | var p = new Parser(data) 29 | 30 | var format = p.readUInt16() 31 | var numTracks = p.readUInt16() 32 | 33 | var result = { 34 | format: format, 35 | numTracks: numTracks 36 | } 37 | 38 | var timeDivision = p.readUInt16() 39 | if (timeDivision & 0x8000) { 40 | result.framesPerSecond = 0x100 - (timeDivision >> 8) 41 | result.ticksPerFrame = timeDivision & 0xFF 42 | } else { 43 | result.ticksPerBeat = timeDivision 44 | } 45 | 46 | return result 47 | } 48 | 49 | function parseTrack(data) { 50 | var p = new Parser(data) 51 | 52 | var events = [] 53 | while (!p.eof()) { 54 | var event = readEvent() 55 | events.push(event) 56 | } 57 | 58 | return events 59 | 60 | var lastEventTypeByte = null 61 | 62 | function readEvent() { 63 | var event = {} 64 | event.deltaTime = p.readVarInt() 65 | 66 | var eventTypeByte = p.readUInt8() 67 | 68 | if ((eventTypeByte & 0xf0) === 0xf0) { 69 | // system / meta event 70 | if (eventTypeByte === 0xff) { 71 | // meta event 72 | event.meta = true 73 | var metatypeByte = p.readUInt8() 74 | var length = p.readVarInt() 75 | switch (metatypeByte) { 76 | case 0x00: 77 | event.type = 'sequenceNumber' 78 | if (length !== 2) throw "Expected length for sequenceNumber event is 2, got " + length 79 | event.number = p.readUInt16() 80 | return event 81 | case 0x01: 82 | event.type = 'text' 83 | event.text = p.readString(length) 84 | return event 85 | case 0x02: 86 | event.type = 'copyrightNotice' 87 | event.text = p.readString(length) 88 | return event 89 | case 0x03: 90 | event.type = 'trackName' 91 | event.text = p.readString(length) 92 | return event 93 | case 0x04: 94 | event.type = 'instrumentName' 95 | event.text = p.readString(length) 96 | return event 97 | case 0x05: 98 | event.type = 'lyrics' 99 | event.text = p.readString(length) 100 | return event 101 | case 0x06: 102 | event.type = 'marker' 103 | event.text = p.readString(length) 104 | return event 105 | case 0x07: 106 | event.type = 'cuePoint' 107 | event.text = p.readString(length) 108 | return event 109 | case 0x20: 110 | event.type = 'channelPrefix' 111 | if (length != 1) throw "Expected length for channelPrefix event is 1, got " + length 112 | event.channel = p.readUInt8() 113 | return event 114 | case 0x21: 115 | event.type = 'portPrefix' 116 | if (length != 1) throw "Expected length for portPrefix event is 1, got " + length 117 | event.port = p.readUInt8() 118 | return event 119 | case 0x2f: 120 | event.type = 'endOfTrack' 121 | if (length != 0) throw "Expected length for endOfTrack event is 0, got " + length 122 | return event 123 | case 0x51: 124 | event.type = 'setTempo'; 125 | if (length != 3) throw "Expected length for setTempo event is 3, got " + length 126 | event.microsecondsPerBeat = p.readUInt24() 127 | return event 128 | case 0x54: 129 | event.type = 'smpteOffset'; 130 | if (length != 5) throw "Expected length for smpteOffset event is 5, got " + length 131 | var hourByte = p.readUInt8() 132 | var FRAME_RATES = { 0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30 } 133 | event.frameRate = FRAME_RATES[hourByte & 0x60] 134 | event.hour = hourByte & 0x1f 135 | event.min = p.readUInt8() 136 | event.sec = p.readUInt8() 137 | event.frame = p.readUInt8() 138 | event.subFrame = p.readUInt8() 139 | return event 140 | case 0x58: 141 | event.type = 'timeSignature' 142 | if (length != 2 && length != 4) throw "Expected length for timeSignature event is 4 or 2, got " + length 143 | event.numerator = p.readUInt8() 144 | event.denominator = (1 << p.readUInt8()) 145 | if (length === 4) { 146 | event.metronome = p.readUInt8() 147 | event.thirtyseconds = p.readUInt8() 148 | } else { 149 | event.metronome = 0x24 150 | event.thirtyseconds = 0x08 151 | } 152 | return event 153 | case 0x59: 154 | event.type = 'keySignature' 155 | if (length != 2) throw "Expected length for keySignature event is 2, got " + length 156 | event.key = p.readInt8() 157 | event.scale = p.readUInt8() 158 | return event 159 | case 0x7f: 160 | event.type = 'sequencerSpecific' 161 | event.data = p.readBytes(length) 162 | return event 163 | default: 164 | event.type = 'unknownMeta' 165 | event.data = p.readBytes(length) 166 | event.metatypeByte = metatypeByte 167 | return event 168 | } 169 | } else if (eventTypeByte == 0xf0) { 170 | event.type = 'sysEx' 171 | var length = p.readVarInt() 172 | event.data = p.readBytes(length) 173 | return event 174 | } else if (eventTypeByte == 0xf7) { 175 | event.type = 'endSysEx' 176 | var length = p.readVarInt() 177 | event.data = p.readBytes(length) 178 | return event 179 | } else { 180 | throw "Unrecognised MIDI event type byte: " + eventTypeByte 181 | } 182 | } else { 183 | // channel event 184 | var param1 185 | if ((eventTypeByte & 0x80) === 0) { 186 | // running status - reuse lastEventTypeByte as the event type. 187 | // eventTypeByte is actually the first parameter 188 | if (lastEventTypeByte === null) 189 | throw "Running status byte encountered before status byte" 190 | param1 = eventTypeByte 191 | eventTypeByte = lastEventTypeByte 192 | event.running = true 193 | } else { 194 | param1 = p.readUInt8() 195 | lastEventTypeByte = eventTypeByte 196 | } 197 | var eventType = eventTypeByte >> 4 198 | event.channel = eventTypeByte & 0x0f 199 | switch (eventType) { 200 | case 0x08: 201 | event.type = 'noteOff' 202 | event.noteNumber = param1 203 | event.velocity = p.readUInt8() 204 | return event 205 | case 0x09: 206 | var velocity = p.readUInt8() 207 | event.type = velocity === 0 ? 'noteOff' : 'noteOn' 208 | event.noteNumber = param1 209 | event.velocity = velocity 210 | if (velocity === 0) event.byte9 = true 211 | return event 212 | case 0x0a: 213 | event.type = 'noteAftertouch' 214 | event.noteNumber = param1 215 | event.amount = p.readUInt8() 216 | return event 217 | case 0x0b: 218 | event.type = 'controller' 219 | event.controllerType = param1 220 | event.value = p.readUInt8() 221 | return event 222 | case 0x0c: 223 | event.type = 'programChange' 224 | event.programNumber = param1 225 | return event 226 | case 0x0d: 227 | event.type = 'channelAftertouch' 228 | event.amount = param1 229 | return event 230 | case 0x0e: 231 | event.type = 'pitchBend' 232 | event.value = (param1 + (p.readUInt8() << 7)) - 0x2000 233 | return event 234 | default: 235 | throw "Unrecognised MIDI event type: " + eventType 236 | } 237 | } 238 | } 239 | } 240 | 241 | function Parser(data) { 242 | this.buffer = data 243 | this.bufferLen = this.buffer.length 244 | this.pos = 0 245 | } 246 | 247 | Parser.prototype.eof = function() { 248 | return this.pos >= this.bufferLen 249 | } 250 | 251 | Parser.prototype.readUInt8 = function() { 252 | var result = this.buffer[this.pos] 253 | this.pos += 1 254 | return result 255 | } 256 | 257 | Parser.prototype.readInt8 = function() { 258 | var u = this.readUInt8() 259 | if (u & 0x80) 260 | return u - 0x100 261 | else 262 | return u 263 | } 264 | 265 | Parser.prototype.readUInt16 = function() { 266 | var b0 = this.readUInt8(), 267 | b1 = this.readUInt8() 268 | 269 | return (b0 << 8) + b1 270 | } 271 | 272 | Parser.prototype.readInt16 = function() { 273 | var u = this.readUInt16() 274 | if (u & 0x8000) 275 | return u - 0x10000 276 | else 277 | return u 278 | } 279 | 280 | Parser.prototype.readUInt24 = function() { 281 | var b0 = this.readUInt8(), 282 | b1 = this.readUInt8(), 283 | b2 = this.readUInt8() 284 | 285 | return (b0 << 16) + (b1 << 8) + b2 286 | } 287 | 288 | Parser.prototype.readInt24 = function() { 289 | var u = this.readUInt24() 290 | if (u & 0x800000) 291 | return u - 0x1000000 292 | else 293 | return u 294 | } 295 | 296 | Parser.prototype.readUInt32 = function() { 297 | var b0 = this.readUInt8(), 298 | b1 = this.readUInt8(), 299 | b2 = this.readUInt8(), 300 | b3 = this.readUInt8() 301 | 302 | return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3 303 | } 304 | 305 | Parser.prototype.readBytes = function(len) { 306 | var bytes = this.buffer.slice(this.pos, this.pos + len) 307 | this.pos += len 308 | return bytes 309 | } 310 | 311 | Parser.prototype.readString = function(len) { 312 | var bytes = this.readBytes(len) 313 | return String.fromCharCode.apply(null, bytes) 314 | } 315 | 316 | Parser.prototype.readVarInt = function() { 317 | var result = 0 318 | while (!this.eof()) { 319 | var b = this.readUInt8() 320 | if (b & 0x80) { 321 | result += (b & 0x7f) 322 | result <<= 7 323 | } else { 324 | // b is last byte 325 | return result + b 326 | } 327 | } 328 | // premature eof 329 | return result 330 | } 331 | 332 | Parser.prototype.readChunk = function() { 333 | var id = this.readString(4) 334 | var length = this.readUInt32() 335 | var data = this.readBytes(length) 336 | return { 337 | id: id, 338 | length: length, 339 | data: data 340 | } 341 | } 342 | 343 | module.exports = parseMidi 344 | -------------------------------------------------------------------------------- /tests/tsd.ts: -------------------------------------------------------------------------------- 1 | import { writeMidi, parseMidi, MidiData } from ".."; 2 | 3 | var errors = 0; 4 | const assert = (condition: boolean, msg?: string) => { 5 | if (!condition) { 6 | errors++; 7 | console.error(`Assertion failed: ${msg}`); 8 | } 9 | }; 10 | 11 | const compare = (x: any, y: any, id: string, deep?: number) => { 12 | deep = deep || 0; 13 | const _assert = (condition: boolean) => 14 | assert(condition, `${id} x=${x},y=${y}`); 15 | const current = errors; 16 | const next = deep + 1; 17 | if (x == null) { 18 | _assert(y === null); 19 | } else if (Array.isArray(x) || x instanceof Uint8Array) { 20 | _assert(Array.isArray(y) || y instanceof Uint8Array); 21 | _assert(x.length == y.length); 22 | for (let i = 0; i < x.length; ++i) { 23 | compare(x[i], y[i], `${id}[${i}]`, next); 24 | } 25 | } else if (typeof x == "object") { 26 | _assert(typeof y == "object"); 27 | const [xkeys, ykeys] = [Object.keys(x), Object.keys(y)]; 28 | xkeys.forEach((k) => compare(x[k], y[k], `${id}.${k}`, next)); 29 | ykeys.filter(x=>!xkeys.includes(x)).forEach((k) => compare(x[k], y[k], `${id}.${k}`, next)); 30 | } else { 31 | _assert(x === y); 32 | } 33 | if (!deep) { 34 | const pass = current == errors; 35 | console.log("[Test]", id, pass ? "PASS" : "FAIL"); 36 | } 37 | }; 38 | const testCompare1={ 39 | num:10, 40 | str:'string', 41 | obj:{ 42 | key:12, 43 | array:[1,2,3] 44 | }, 45 | arr:[1,2,3,{ 46 | key:'value' 47 | }], 48 | xKey:'value' 49 | }; 50 | const testCompare2={ 51 | num:12, 52 | str:'string1', 53 | obj:{ 54 | key:12, 55 | array:[3,2,1] 56 | }, 57 | arr:[3,2,1,{ 58 | key:'value1', 59 | key2:'value2' 60 | }], 61 | yKey:'value' 62 | }; 63 | compare([testCompare1],[testCompare1],"testCompare1"); 64 | compare([testCompare1],[testCompare2],"[Should FAIL] testCompare2"); 65 | compare(10,errors,"testCompare"); 66 | 67 | errors=0; 68 | const testDefault: MidiData = { 69 | header: { 70 | format: 0, 71 | numTracks: 1, 72 | ticksPerBeat: 480, 73 | }, 74 | tracks: [ 75 | [ 76 | { 77 | deltaTime: 0, 78 | meta: true, 79 | type: "setTempo", 80 | microsecondsPerBeat: 500000, 81 | }, 82 | { 83 | deltaTime: 0, 84 | meta: true, 85 | type: "sequenceNumber", 86 | number: 1, 87 | }, 88 | { 89 | deltaTime: 0, 90 | meta: true, 91 | type: "smpteOffset", 92 | frameRate: 24, 93 | hour: 1, 94 | min: 2, 95 | sec: 3, 96 | frame: 4, 97 | subFrame: 5, 98 | }, 99 | { 100 | deltaTime: 0, 101 | meta: true, 102 | type: "timeSignature", 103 | numerator: 1, 104 | denominator: 2, 105 | metronome: 3, 106 | thirtyseconds: 4, 107 | }, 108 | { 109 | deltaTime: 0, 110 | meta: true, 111 | type: "keySignature", 112 | key: 0, 113 | scale: 1, 114 | }, 115 | { 116 | deltaTime: 0, 117 | meta: true, 118 | type: "sequencerSpecific", 119 | data: [1, 2, 3, 4], 120 | }, 121 | { 122 | deltaTime: 0, 123 | meta: true, 124 | type: "instrumentName", 125 | text: "instrumentName", 126 | }, 127 | { 128 | deltaTime: 0, 129 | type: "sysEx", 130 | data: [1, 2, 3, 4], 131 | }, 132 | { 133 | deltaTime: 0, 134 | type: "endSysEx", 135 | data: [1, 2, 3, 4], 136 | }, 137 | { 138 | deltaTime: 1, 139 | meta: true, 140 | type: "text", 141 | text: "text", 142 | }, 143 | { 144 | deltaTime: 2, 145 | meta: true, 146 | type: "copyrightNotice", 147 | text: "copyrightNotice", 148 | }, 149 | { 150 | deltaTime: 3, 151 | meta: true, 152 | type: "trackName", 153 | text: "trackName", 154 | }, 155 | { 156 | deltaTime: 4, 157 | meta: true, 158 | type: "instrumentName", 159 | text: "instrumentName", 160 | }, 161 | { 162 | deltaTime: 5, 163 | meta: true, 164 | type: "lyrics", 165 | text: "lyrics", 166 | }, 167 | { 168 | deltaTime: 5, 169 | meta: true, 170 | type: "marker", 171 | text: "marker", 172 | }, 173 | { 174 | deltaTime: 5, 175 | meta: true, 176 | type: "cuePoint", 177 | text: "cuePoint", 178 | }, 179 | { 180 | deltaTime: 5, 181 | meta: true, 182 | type: "channelPrefix", 183 | channel: 0, 184 | }, 185 | { 186 | deltaTime: 6, 187 | meta: true, 188 | type: "portPrefix", 189 | port: 0, 190 | }, 191 | { 192 | deltaTime: 7, 193 | channel: 0, 194 | type: "noteOn", 195 | noteNumber: 60, 196 | velocity: 80, 197 | }, 198 | { 199 | deltaTime: 7, 200 | channel: 0, 201 | type: "noteOn", 202 | noteNumber: 60, 203 | velocity: 80, 204 | }, 205 | { 206 | deltaTime: 8, 207 | channel: 0, 208 | type: "noteOff", 209 | noteNumber: 60, 210 | velocity: 0, 211 | }, 212 | { 213 | deltaTime: 8, 214 | channel: 0, 215 | type: "controller", 216 | controllerType: 0, 217 | value: 0, 218 | }, 219 | { 220 | deltaTime: 8, 221 | channel: 0, 222 | type: "noteAftertouch", 223 | noteNumber: 60, 224 | amount: 0, 225 | }, 226 | { 227 | deltaTime: 8, 228 | channel: 0, 229 | type: "programChange", 230 | programNumber: 1, 231 | }, 232 | { 233 | deltaTime: 8, 234 | channel: 0, 235 | type: "channelAftertouch", 236 | amount: 1, 237 | }, 238 | { 239 | deltaTime: 8, 240 | channel: 0, 241 | type: "pitchBend", 242 | value: 10, 243 | }, 244 | { 245 | deltaTime: 8, 246 | meta: true, 247 | type: "endOfTrack", 248 | }, 249 | ], 250 | ], 251 | }; 252 | const testWithRunningOpt: MidiData = { 253 | header: { 254 | format: 0, 255 | numTracks: 1, 256 | ticksPerBeat: 480, 257 | }, 258 | tracks: [ 259 | [ 260 | { 261 | deltaTime: 0, 262 | meta: true, 263 | type: "setTempo", 264 | microsecondsPerBeat: 500000, 265 | }, 266 | { 267 | deltaTime: 0, 268 | meta: true, 269 | type: "sequenceNumber", 270 | number: 1, 271 | }, 272 | { 273 | deltaTime: 0, 274 | meta: true, 275 | type: "smpteOffset", 276 | frameRate: 24, 277 | hour: 1, 278 | min: 2, 279 | sec: 3, 280 | frame: 4, 281 | subFrame: 5, 282 | }, 283 | { 284 | deltaTime: 0, 285 | meta: true, 286 | type: "timeSignature", 287 | numerator: 1, 288 | denominator: 2, 289 | metronome: 3, 290 | thirtyseconds: 4, 291 | }, 292 | { 293 | deltaTime: 0, 294 | meta: true, 295 | type: "keySignature", 296 | key: 0, 297 | scale: 1, 298 | }, 299 | { 300 | deltaTime: 0, 301 | meta: true, 302 | type: "sequencerSpecific", 303 | data: [1, 2, 3, 4], 304 | }, 305 | { 306 | deltaTime: 0, 307 | meta: true, 308 | type: "instrumentName", 309 | text: "instrumentName", 310 | }, 311 | { 312 | deltaTime: 0, 313 | type: "sysEx", 314 | data: [1, 2, 3, 4], 315 | }, 316 | { 317 | deltaTime: 0, 318 | type: "endSysEx", 319 | data: [1, 2, 3, 4], 320 | }, 321 | { 322 | deltaTime: 1, 323 | meta: true, 324 | type: "text", 325 | text: "text", 326 | }, 327 | { 328 | deltaTime: 2, 329 | meta: true, 330 | type: "copyrightNotice", 331 | text: "copyrightNotice", 332 | }, 333 | { 334 | deltaTime: 3, 335 | meta: true, 336 | type: "trackName", 337 | text: "trackName", 338 | }, 339 | { 340 | deltaTime: 4, 341 | meta: true, 342 | type: "instrumentName", 343 | text: "instrumentName", 344 | }, 345 | { 346 | deltaTime: 5, 347 | meta: true, 348 | type: "lyrics", 349 | text: "lyrics", 350 | }, 351 | { 352 | deltaTime: 5, 353 | meta: true, 354 | type: "marker", 355 | text: "marker", 356 | }, 357 | { 358 | deltaTime: 5, 359 | meta: true, 360 | type: "cuePoint", 361 | text: "cuePoint", 362 | }, 363 | { 364 | deltaTime: 5, 365 | meta: true, 366 | type: "channelPrefix", 367 | channel: 0, 368 | }, 369 | { 370 | deltaTime: 6, 371 | meta: true, 372 | type: "portPrefix", 373 | port: 0, 374 | }, 375 | { 376 | deltaTime: 7, 377 | channel: 0, 378 | type: "noteOn", 379 | noteNumber: 60, 380 | velocity: 80, 381 | }, 382 | { 383 | deltaTime: 7, 384 | running: true, 385 | channel: 0, 386 | type: "noteOn", 387 | noteNumber: 60, 388 | velocity: 80, 389 | }, 390 | { 391 | deltaTime: 8, 392 | channel: 0, 393 | type: "noteOff", 394 | noteNumber: 60, 395 | velocity: 0, 396 | }, 397 | { 398 | deltaTime: 8, 399 | channel: 0, 400 | type: "controller", 401 | controllerType: 0, 402 | value: 0, 403 | }, 404 | { 405 | deltaTime: 8, 406 | channel: 0, 407 | type: "noteAftertouch", 408 | noteNumber: 60, 409 | amount: 0, 410 | }, 411 | { 412 | deltaTime: 8, 413 | channel: 0, 414 | type: "programChange", 415 | programNumber: 1, 416 | }, 417 | { 418 | deltaTime: 8, 419 | channel: 0, 420 | type: "channelAftertouch", 421 | amount: 1, 422 | }, 423 | { 424 | deltaTime: 8, 425 | channel: 0, 426 | type: "pitchBend", 427 | value: 10, 428 | }, 429 | { 430 | deltaTime: 8, 431 | meta: true, 432 | type: "endOfTrack", 433 | }, 434 | ], 435 | ], 436 | }; 437 | const testWithRunningAndUseByte9ForNoteOffOpt: MidiData = { 438 | header: { 439 | format: 0, 440 | numTracks: 1, 441 | ticksPerBeat: 480, 442 | }, 443 | tracks: [ 444 | [ 445 | { 446 | deltaTime: 0, 447 | meta: true, 448 | type: "setTempo", 449 | microsecondsPerBeat: 500000, 450 | }, 451 | { 452 | deltaTime: 0, 453 | meta: true, 454 | type: "sequenceNumber", 455 | number: 1, 456 | }, 457 | { 458 | deltaTime: 0, 459 | meta: true, 460 | type: "smpteOffset", 461 | frameRate: 24, 462 | hour: 1, 463 | min: 2, 464 | sec: 3, 465 | frame: 4, 466 | subFrame: 5, 467 | }, 468 | { 469 | deltaTime: 0, 470 | meta: true, 471 | type: "timeSignature", 472 | numerator: 1, 473 | denominator: 2, 474 | metronome: 3, 475 | thirtyseconds: 4, 476 | }, 477 | { 478 | deltaTime: 0, 479 | meta: true, 480 | type: "keySignature", 481 | key: 0, 482 | scale: 1, 483 | }, 484 | { 485 | deltaTime: 0, 486 | meta: true, 487 | type: "sequencerSpecific", 488 | data: [1, 2, 3, 4], 489 | }, 490 | { 491 | deltaTime: 0, 492 | meta: true, 493 | type: "instrumentName", 494 | text: "instrumentName", 495 | }, 496 | { 497 | deltaTime: 0, 498 | type: "sysEx", 499 | data: [1, 2, 3, 4], 500 | }, 501 | { 502 | deltaTime: 0, 503 | type: "endSysEx", 504 | data: [1, 2, 3, 4], 505 | }, 506 | { 507 | deltaTime: 1, 508 | meta: true, 509 | type: "text", 510 | text: "text", 511 | }, 512 | { 513 | deltaTime: 2, 514 | meta: true, 515 | type: "copyrightNotice", 516 | text: "copyrightNotice", 517 | }, 518 | { 519 | deltaTime: 3, 520 | meta: true, 521 | type: "trackName", 522 | text: "trackName", 523 | }, 524 | { 525 | deltaTime: 4, 526 | meta: true, 527 | type: "instrumentName", 528 | text: "instrumentName", 529 | }, 530 | { 531 | deltaTime: 5, 532 | meta: true, 533 | type: "lyrics", 534 | text: "lyrics", 535 | }, 536 | { 537 | deltaTime: 5, 538 | meta: true, 539 | type: "marker", 540 | text: "marker", 541 | }, 542 | { 543 | deltaTime: 5, 544 | meta: true, 545 | type: "cuePoint", 546 | text: "cuePoint", 547 | }, 548 | { 549 | deltaTime: 5, 550 | meta: true, 551 | type: "channelPrefix", 552 | channel: 0, 553 | }, 554 | { 555 | deltaTime: 6, 556 | meta: true, 557 | type: "portPrefix", 558 | port: 0, 559 | }, 560 | { 561 | deltaTime: 7, 562 | channel: 0, 563 | type: "noteOn", 564 | noteNumber: 60, 565 | velocity: 80, 566 | }, 567 | { 568 | deltaTime: 7, 569 | running: true, 570 | channel: 0, 571 | type: "noteOn", 572 | noteNumber: 60, 573 | velocity: 80, 574 | }, 575 | { 576 | deltaTime: 8, 577 | running: true, 578 | channel: 0, 579 | type: "noteOff", 580 | noteNumber: 60, 581 | velocity: 0, 582 | byte9: true, 583 | }, 584 | { 585 | deltaTime: 8, 586 | channel: 0, 587 | type: "controller", 588 | controllerType: 0, 589 | value: 0, 590 | }, 591 | { 592 | deltaTime: 8, 593 | channel: 0, 594 | type: "noteAftertouch", 595 | noteNumber: 60, 596 | amount: 0, 597 | }, 598 | { 599 | deltaTime: 8, 600 | channel: 0, 601 | type: "programChange", 602 | programNumber: 1, 603 | }, 604 | { 605 | deltaTime: 8, 606 | channel: 0, 607 | type: "channelAftertouch", 608 | amount: 1, 609 | }, 610 | { 611 | deltaTime: 8, 612 | channel: 0, 613 | type: "pitchBend", 614 | value: 10, 615 | }, 616 | { 617 | deltaTime: 8, 618 | meta: true, 619 | type: "endOfTrack", 620 | }, 621 | ], 622 | ], 623 | }; 624 | 625 | const testWithUseByte9ForNoteOffOpt: MidiData = { 626 | header: { 627 | format: 0, 628 | numTracks: 1, 629 | ticksPerBeat: 480, 630 | }, 631 | tracks: [ 632 | [ 633 | { 634 | deltaTime: 0, 635 | meta: true, 636 | type: "setTempo", 637 | microsecondsPerBeat: 500000, 638 | }, 639 | { 640 | deltaTime: 0, 641 | meta: true, 642 | type: "sequenceNumber", 643 | number: 1, 644 | }, 645 | { 646 | deltaTime: 0, 647 | meta: true, 648 | type: "smpteOffset", 649 | frameRate: 24, 650 | hour: 1, 651 | min: 2, 652 | sec: 3, 653 | frame: 4, 654 | subFrame: 5, 655 | }, 656 | { 657 | deltaTime: 0, 658 | meta: true, 659 | type: "timeSignature", 660 | numerator: 1, 661 | denominator: 2, 662 | metronome: 3, 663 | thirtyseconds: 4, 664 | }, 665 | { 666 | deltaTime: 0, 667 | meta: true, 668 | type: "keySignature", 669 | key: 0, 670 | scale: 1, 671 | }, 672 | { 673 | deltaTime: 0, 674 | meta: true, 675 | type: "sequencerSpecific", 676 | data: [1, 2, 3, 4], 677 | }, 678 | { 679 | deltaTime: 0, 680 | meta: true, 681 | type: "instrumentName", 682 | text: "instrumentName", 683 | }, 684 | { 685 | deltaTime: 0, 686 | type: "sysEx", 687 | data: [1, 2, 3, 4], 688 | }, 689 | { 690 | deltaTime: 0, 691 | type: "endSysEx", 692 | data: [1, 2, 3, 4], 693 | }, 694 | { 695 | deltaTime: 1, 696 | meta: true, 697 | type: "text", 698 | text: "text", 699 | }, 700 | { 701 | deltaTime: 2, 702 | meta: true, 703 | type: "copyrightNotice", 704 | text: "copyrightNotice", 705 | }, 706 | { 707 | deltaTime: 3, 708 | meta: true, 709 | type: "trackName", 710 | text: "trackName", 711 | }, 712 | { 713 | deltaTime: 4, 714 | meta: true, 715 | type: "instrumentName", 716 | text: "instrumentName", 717 | }, 718 | { 719 | deltaTime: 5, 720 | meta: true, 721 | type: "lyrics", 722 | text: "lyrics", 723 | }, 724 | { 725 | deltaTime: 5, 726 | meta: true, 727 | type: "marker", 728 | text: "marker", 729 | }, 730 | { 731 | deltaTime: 5, 732 | meta: true, 733 | type: "cuePoint", 734 | text: "cuePoint", 735 | }, 736 | { 737 | deltaTime: 5, 738 | meta: true, 739 | type: "channelPrefix", 740 | channel: 0, 741 | }, 742 | { 743 | deltaTime: 6, 744 | meta: true, 745 | type: "portPrefix", 746 | port: 0, 747 | }, 748 | { 749 | deltaTime: 7, 750 | channel: 0, 751 | type: "noteOn", 752 | noteNumber: 60, 753 | velocity: 80, 754 | }, 755 | { 756 | deltaTime: 7, 757 | running: true, 758 | channel: 0, 759 | type: "noteOn", 760 | noteNumber: 60, 761 | velocity: 80, 762 | }, 763 | { 764 | deltaTime: 8, 765 | running: true, 766 | channel: 0, 767 | type: "noteOff", 768 | noteNumber: 60, 769 | velocity: 0, 770 | byte9: true, 771 | }, 772 | { 773 | deltaTime: 8, 774 | channel: 0, 775 | type: "controller", 776 | controllerType: 0, 777 | value: 0, 778 | }, 779 | { 780 | deltaTime: 8, 781 | channel: 0, 782 | type: "noteAftertouch", 783 | noteNumber: 60, 784 | amount: 0, 785 | }, 786 | { 787 | deltaTime: 8, 788 | channel: 0, 789 | type: "programChange", 790 | programNumber: 1, 791 | }, 792 | { 793 | deltaTime: 8, 794 | channel: 0, 795 | type: "channelAftertouch", 796 | amount: 1, 797 | }, 798 | { 799 | deltaTime: 8, 800 | channel: 0, 801 | type: "pitchBend", 802 | value: 10, 803 | }, 804 | { 805 | deltaTime: 8, 806 | meta: true, 807 | type: "endOfTrack", 808 | }, 809 | ], 810 | ], 811 | }; 812 | 813 | compare(testDefault, parseMidi(writeMidi(testDefault)), "testDefault"); 814 | compare( 815 | testWithRunningOpt, 816 | parseMidi(writeMidi(testWithRunningOpt, { running: true })), 817 | "testWithRunningOpt" 818 | ); 819 | compare( 820 | testWithRunningAndUseByte9ForNoteOffOpt, 821 | parseMidi( 822 | writeMidi(testWithRunningAndUseByte9ForNoteOffOpt, { 823 | running: true, 824 | useByte9ForNoteOff: true, 825 | }) 826 | ), 827 | "testWithRunningAndUseByte9ForNoteOffOpt" 828 | ); 829 | compare( 830 | testWithUseByte9ForNoteOffOpt, 831 | parseMidi( 832 | writeMidi(testWithUseByte9ForNoteOffOpt, { useByte9ForNoteOff: true }) 833 | ), 834 | "testWithUseByte9ForNoteOffOpt" 835 | ); 836 | 837 | const testHeader1: MidiData = { 838 | header: { 839 | format: 0, 840 | numTracks: 0, 841 | timeDivision: 120, 842 | }, 843 | tracks: [], 844 | }; 845 | const testHeader2: MidiData = { 846 | header: { 847 | format: 0, 848 | numTracks: 0, 849 | ticksPerBeat: 480, 850 | }, 851 | tracks: [], 852 | }; 853 | const testHeader3: MidiData = { 854 | header: { 855 | format: 0, 856 | numTracks: 0, 857 | framesPerSecond: 60, 858 | ticksPerFrame: 80, 859 | }, 860 | tracks: [], 861 | }; 862 | 863 | compare( 864 | 120, 865 | parseMidi(writeMidi(testHeader1)).header.ticksPerBeat, 866 | "testHeader1" 867 | ); 868 | compare( 869 | 480, 870 | parseMidi(writeMidi(testHeader2)).header.ticksPerBeat, 871 | "testHeader2" 872 | ); 873 | compare(testHeader3, parseMidi(writeMidi(testHeader3)), "testHeader3"); 874 | const testNoMeta: MidiData = { 875 | header: { 876 | format: 0, 877 | numTracks: 1, 878 | ticksPerBeat: 480, 879 | }, 880 | tracks: [ 881 | [ 882 | { 883 | deltaTime: 0, 884 | type: "setTempo", 885 | microsecondsPerBeat: 500000, 886 | }, 887 | ], 888 | ], 889 | }; 890 | const testNoMetaResult: MidiData = { 891 | header: { 892 | format: 0, 893 | numTracks: 1, 894 | ticksPerBeat: 480, 895 | }, 896 | tracks: [ 897 | [ 898 | { 899 | deltaTime: 0, 900 | meta: true, 901 | type: "setTempo", 902 | microsecondsPerBeat: 500000, 903 | }, 904 | ], 905 | ], 906 | }; 907 | compare(testNoMetaResult, parseMidi(writeMidi(testNoMeta)), "testNoMeta"); 908 | 909 | const YourCase: MidiData = { 910 | header: { 911 | format: 1, 912 | numTracks: 2, 913 | ticksPerBeat: 960, 914 | }, 915 | tracks: [ 916 | [ 917 | { 918 | deltaTime: 0, 919 | meta: true, 920 | type: "trackName", 921 | text: "midi_export", 922 | }, 923 | { 924 | deltaTime: 0, 925 | meta: true, 926 | type: "timeSignature", 927 | numerator: 4, 928 | denominator: 4, 929 | metronome: 24, 930 | thirtyseconds: 8, 931 | }, 932 | { 933 | deltaTime: 0, 934 | meta: true, 935 | type: "setTempo", 936 | microsecondsPerBeat: 500000, 937 | }, 938 | { 939 | deltaTime: 0, 940 | meta: true, 941 | type: "endOfTrack", 942 | }, 943 | ], 944 | [ 945 | { 946 | deltaTime: 0, 947 | channel: 0, 948 | type: "noteOn", 949 | noteNumber: 60, 950 | velocity: 96, 951 | }, 952 | { 953 | deltaTime: 0, 954 | channel: 0, 955 | type: "noteOn", 956 | noteNumber: 57, 957 | velocity: 96, 958 | }, 959 | { 960 | deltaTime: 3840, 961 | channel: 0, 962 | type: "noteOff", 963 | noteNumber: 60, 964 | velocity: 0, 965 | }, 966 | { 967 | deltaTime: 0, 968 | channel: 0, 969 | type: "noteOff", 970 | noteNumber: 57, 971 | velocity: 0, 972 | }, 973 | { 974 | deltaTime: 0, 975 | meta: true, 976 | type: "endOfTrack", 977 | }, 978 | ], 979 | ], 980 | }; 981 | compare(YourCase, parseMidi(writeMidi(YourCase)), "YourCase"); 982 | 983 | compare( 984 | testDefault, 985 | parseMidi(new Uint8Array(writeMidi(testDefault))), 986 | "Uint8Array" 987 | ); 988 | 989 | console.log(`errors=${errors}`); 990 | 991 | declare var process: { exitCode?: number }; 992 | process.exitCode = errors ? -1 : 0; 993 | --------------------------------------------------------------------------------