├── .gitignore ├── README.md ├── gulpfile.js ├── lib └── jsmidgen.js ├── package.json └── test ├── file-test.js ├── midi-util-test.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Dependency directory 20 | node_modules 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsmidgen, a pure-JavaScript MIDI file library 2 | 3 | ## Introduction 4 | 5 | jsmidgen (pronounced jay-smidgen or jer-smidgen) is a library that can be used 6 | to generate MIDI files in JavaScript. It currently provides output as a string, 7 | but there are plans to provide multiple output formats, including base64 and 8 | data URI. 9 | 10 | ## Example Usage 11 | 12 | The MIDI file structure is made up of one or more tracks, which contain one or 13 | more events. These events can be note on/off events, instrument changes, tempo 14 | changes, or more exotic things. A basic example is shown below: 15 | 16 | var fs = require('fs'); 17 | var Midi = require('jsmidgen'); 18 | 19 | var file = new Midi.File(); 20 | var track = new Midi.Track(); 21 | file.addTrack(track); 22 | 23 | track.addNote(0, 'c4', 64); 24 | track.addNote(0, 'd4', 64); 25 | track.addNote(0, 'e4', 64); 26 | track.addNote(0, 'f4', 64); 27 | track.addNote(0, 'g4', 64); 28 | track.addNote(0, 'a4', 64); 29 | track.addNote(0, 'b4', 64); 30 | track.addNote(0, 'c5', 64); 31 | 32 | fs.writeFileSync('test.mid', file.toBytes(), 'binary'); 33 | 34 | This example will create a MIDI file that will play an ascending C major scale, 35 | starting at middle C. 36 | 37 | ## Fluid API 38 | 39 | This library also has rudimentary support for a fluid (chained) style: 40 | 41 | file = new Midi.File(); 42 | file 43 | .addTrack() 44 | 45 | .note(0, 'c4', 32) 46 | .note(0, 'd4', 32) 47 | .note(0, 'e4', 32) 48 | .note(0, 'f4', 32) 49 | .note(0, 'g4', 32) 50 | .note(0, 'a4', 32) 51 | .note(0, 'b4', 32) 52 | .note(0, 'c5', 32) 53 | 54 | // church organ 55 | .instrument(0, 0x13) 56 | 57 | // by skipping the third arguments, we create a chord (C major) 58 | .noteOn(0, 'c4', 64) 59 | .noteOn(0, 'e4') 60 | .noteOn(0, 'g4') 61 | 62 | // by skipping the third arguments again, we stop all notes at once 63 | .noteOff(0, 'c4', 47) 64 | .noteOff(0, 'e4') 65 | .noteOff(0, 'g4') 66 | 67 | //alternatively, a chord may be created with the addChord function 68 | .addChord(0, ['c4', 'e4', 'g4'], 64) 69 | 70 | .noteOn(0, 'c4', 1) 71 | .noteOn(0, 'e4') 72 | .noteOn(0, 'g4') 73 | .noteOff(0, 'c4', 384) 74 | .noteOff(0, 'e4') 75 | .noteOff(0, 'g4') 76 | ; 77 | 78 | fs.writeFileSync('test2.mid', file.toBytes(), 'binary'); 79 | 80 | Note the use of `setInstrument()` to change to a church organ midway through, 81 | and the use of `addNoteOn()`/`addNoteOff()` to produce chords. 82 | 83 | ## Reference 84 | 85 | ### Midi.File 86 | 87 | - `addTrack()` - Add a new Track object to the file and return the new track 88 | - `addTrack(track)` - Add the given Track object to the file and return the file 89 | 90 | ### Midi.Track 91 | 92 | Time and duration are specified in "ticks", and there is a hardcoded 93 | value of 128 ticks per beat. This means that a quarter note has a duration of 128. 94 | 95 | Pitch can be specified by note name with octave (`a#4`) or by note number (`60`). 96 | Middle C is represented as `c4` or `60`. 97 | 98 | - `addNote(channel, pitch, duration[, time[, velocity]])` 99 | 100 | **Add a new note with the given channel, pitch, and duration** 101 | - If `time` is given, delay that many ticks before starting the note 102 | - If `velocity` is given, strike the note with that velocity 103 | - `addNoteOn(channel, pitch[, time[, velocity]])` 104 | 105 | **Start a new note with the given channel and pitch** 106 | - If `time` is given, delay that many ticks before starting the note 107 | - If `velocity` is given, strike the note with that velocity 108 | - `addNoteOff(channel, pitch[, time[, velocity]])` 109 | 110 | **End a note with the given channel and pitch** 111 | - If `time` is given, delay that many ticks before ending the note 112 | - If `velocity` is given, strike the note with that velocity 113 | - `addChord(channel, chord[, velocity])` 114 | 115 | **Add a chord with the given channel and pitches** 116 | - Accepts an array of pitches to play as a chord 117 | - If `velocity` is given, strike the chord with that velocity 118 | - `setInstrument(channel, instrument[, time])` 119 | 120 | **Change the given channel to the given instrument** 121 | - If `time` is given, delay that many ticks before making the change 122 | - `setTempo(bpm[, time])` 123 | 124 | **Set the tempo to `bpm` beats per minute** 125 | - If `time` is given, delay that many ticks before making the change 126 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var tape = require('gulp-tape'); 3 | 4 | gulp.task('test', function() { 5 | return gulp.src(['test/file-test.js', 'test/midi-util-test.js']) 6 | .pipe(tape()); 7 | }); 8 | 9 | gulp.task('default', ['test']); 10 | -------------------------------------------------------------------------------- /lib/jsmidgen.js: -------------------------------------------------------------------------------- 1 | var Midi = {}; 2 | 3 | (function(exported) { 4 | 5 | var DEFAULT_VOLUME = exported.DEFAULT_VOLUME = 90; 6 | var DEFAULT_DURATION = exported.DEFAULT_DURATION = 128; 7 | var DEFAULT_CHANNEL = exported.DEFAULT_CHANNEL = 0; 8 | 9 | /* ****************************************************************** 10 | * Utility functions 11 | ****************************************************************** */ 12 | 13 | var Util = { 14 | 15 | midi_letter_pitches: { a:21, b:23, c:12, d:14, e:16, f:17, g:19 }, 16 | 17 | /** 18 | * Convert a symbolic note name (e.g. "c4") to a numeric MIDI pitch (e.g. 19 | * 60, middle C). 20 | * 21 | * @param {string} n - The symbolic note name to parse. 22 | * @returns {number} The MIDI pitch that corresponds to the symbolic note 23 | * name. 24 | */ 25 | midiPitchFromNote: function(n) { 26 | var matches = /([a-g])(#+|b+)?([0-9]+)$/i.exec(n); 27 | var note = matches[1].toLowerCase(), accidental = matches[2] || '', octave = parseInt(matches[3], 10); 28 | return (12 * octave) + Util.midi_letter_pitches[note] + (accidental.substr(0,1)=='#'?1:-1) * accidental.length; 29 | }, 30 | 31 | /** 32 | * Ensure that the given argument is converted to a MIDI pitch. Note that 33 | * it may already be one (including a purely numeric string). 34 | * 35 | * @param {string|number} p - The pitch to convert. 36 | * @returns {number} The resulting numeric MIDI pitch. 37 | */ 38 | ensureMidiPitch: function(p) { 39 | if (typeof p == 'number' || !/[^0-9]/.test(p)) { 40 | // numeric pitch 41 | return parseInt(p, 10); 42 | } else { 43 | // assume it's a note name 44 | return Util.midiPitchFromNote(p); 45 | } 46 | }, 47 | 48 | midi_pitches_letter: { '12':'c', '13':'c#', '14':'d', '15':'d#', '16':'e', '17':'f', '18':'f#', '19':'g', '20':'g#', '21':'a', '22':'a#', '23':'b' }, 49 | midi_flattened_notes: { 'a#':'bb', 'c#':'db', 'd#':'eb', 'f#':'gb', 'g#':'ab' }, 50 | 51 | /** 52 | * Convert a numeric MIDI pitch value (e.g. 60) to a symbolic note name 53 | * (e.g. "c4"). 54 | * 55 | * @param {number} n - The numeric MIDI pitch value to convert. 56 | * @param {boolean} [returnFlattened=false] - Whether to prefer flattened 57 | * notes to sharpened ones. Optional, default false. 58 | * @returns {string} The resulting symbolic note name. 59 | */ 60 | noteFromMidiPitch: function(n, returnFlattened) { 61 | var octave = 0, noteNum = n, noteName, returnFlattened = returnFlattened || false; 62 | if (n > 23) { 63 | // noteNum is on octave 1 or more 64 | octave = Math.floor(n/12) - 1; 65 | // subtract number of octaves from noteNum 66 | noteNum = n - octave * 12; 67 | } 68 | 69 | // get note name (c#, d, f# etc) 70 | noteName = Util.midi_pitches_letter[noteNum]; 71 | // Use flattened notes if requested (e.g. f# should be output as gb) 72 | if (returnFlattened && noteName.indexOf('#') > 0) { 73 | noteName = Util.midi_flattened_notes[noteName]; 74 | } 75 | return noteName + octave; 76 | }, 77 | 78 | /** 79 | * Convert beats per minute (BPM) to microseconds per quarter note (MPQN). 80 | * 81 | * @param {number} bpm - A number in beats per minute. 82 | * @returns {number} The number of microseconds per quarter note. 83 | */ 84 | mpqnFromBpm: function(bpm) { 85 | var mpqn = Math.floor(60000000 / bpm); 86 | var ret=[]; 87 | do { 88 | ret.unshift(mpqn & 0xFF); 89 | mpqn >>= 8; 90 | } while (mpqn); 91 | while (ret.length < 3) { 92 | ret.push(0); 93 | } 94 | return ret; 95 | }, 96 | 97 | /** 98 | * Convert microseconds per quarter note (MPQN) to beats per minute (BPM). 99 | * 100 | * @param {number} mpqn - The number of microseconds per quarter note. 101 | * @returns {number} A number in beats per minute. 102 | */ 103 | bpmFromMpqn: function(mpqn) { 104 | var m = mpqn; 105 | if (typeof mpqn[0] != 'undefined') { 106 | m = 0; 107 | for (var i=0, l=mpqn.length-1; l >= 0; ++i, --l) { 108 | m |= mpqn[i] << l; 109 | } 110 | } 111 | return Math.floor(60000000 / mpqn); 112 | }, 113 | 114 | /** 115 | * Converts an array of bytes to a string of hexadecimal characters. Prepares 116 | * it to be converted into a base64 string. 117 | * 118 | * @param {Array} byteArray - Array of bytes to be converted. 119 | * @returns {string} Hexadecimal string, e.g. "097B8A". 120 | */ 121 | codes2Str: function(byteArray) { 122 | return String.fromCharCode.apply(null, byteArray); 123 | }, 124 | 125 | /** 126 | * Converts a string of hexadecimal values to an array of bytes. It can also 127 | * add remaining "0" nibbles in order to have enough bytes in the array as the 128 | * `finalBytes` parameter. 129 | * 130 | * @param {string} str - string of hexadecimal values e.g. "097B8A" 131 | * @param {number} [finalBytes] - Optional. The desired number of bytes 132 | * (not nibbles) that the returned array should contain. 133 | * @returns {Array} An array of nibbles. 134 | */ 135 | str2Bytes: function (str, finalBytes) { 136 | if (finalBytes) { 137 | while ((str.length / 2) < finalBytes) { str = "0" + str; } 138 | } 139 | 140 | var bytes = []; 141 | for (var i=str.length-1; i>=0; i = i-2) { 142 | var chars = i === 0 ? str[i] : str[i-1] + str[i]; 143 | bytes.unshift(parseInt(chars, 16)); 144 | } 145 | 146 | return bytes; 147 | }, 148 | 149 | /** 150 | * Translates number of ticks to MIDI timestamp format, returning an array 151 | * of bytes with the time values. MIDI has a very particular way to express 152 | * time; take a good look at the spec before ever touching this function. 153 | * 154 | * @param {number} ticks - Number of ticks to be translated. 155 | * @returns {number} Array of bytes that form the MIDI time value. 156 | */ 157 | translateTickTime: function(ticks) { 158 | var buffer = ticks & 0x7F; 159 | 160 | while (ticks = ticks >> 7) { 161 | buffer <<= 8; 162 | buffer |= ((ticks & 0x7F) | 0x80); 163 | } 164 | 165 | var bList = []; 166 | while (true) { 167 | bList.push(buffer & 0xff); 168 | 169 | if (buffer & 0x80) { buffer >>= 8; } 170 | else { break; } 171 | } 172 | return bList; 173 | }, 174 | 175 | }; 176 | 177 | /* ****************************************************************** 178 | * Event class 179 | ****************************************************************** */ 180 | 181 | /** 182 | * Construct a MIDI event. 183 | * 184 | * Parameters include: 185 | * - time [optional number] - Ticks since previous event. 186 | * - type [required number] - Type of event. 187 | * - channel [required number] - Channel for the event. 188 | * - param1 [required number] - First event parameter. 189 | * - param2 [optional number] - Second event parameter. 190 | */ 191 | var MidiEvent = function(params) { 192 | if (!this) return new MidiEvent(params); 193 | if (params && 194 | (params.type !== null || params.type !== undefined) && 195 | (params.channel !== null || params.channel !== undefined) && 196 | (params.param1 !== null || params.param1 !== undefined)) { 197 | this.setTime(params.time); 198 | this.setType(params.type); 199 | this.setChannel(params.channel); 200 | this.setParam1(params.param1); 201 | this.setParam2(params.param2); 202 | } 203 | }; 204 | 205 | // event codes 206 | MidiEvent.NOTE_OFF = 0x80; 207 | MidiEvent.NOTE_ON = 0x90; 208 | MidiEvent.AFTER_TOUCH = 0xA0; 209 | MidiEvent.CONTROLLER = 0xB0; 210 | MidiEvent.PROGRAM_CHANGE = 0xC0; 211 | MidiEvent.CHANNEL_AFTERTOUCH = 0xD0; 212 | MidiEvent.PITCH_BEND = 0xE0; 213 | 214 | 215 | /** 216 | * Set the time for the event in ticks since the previous event. 217 | * 218 | * @param {number} ticks - The number of ticks since the previous event. May 219 | * be zero. 220 | */ 221 | MidiEvent.prototype.setTime = function(ticks) { 222 | this.time = Util.translateTickTime(ticks || 0); 223 | }; 224 | 225 | /** 226 | * Set the type of the event. Must be one of the event codes on MidiEvent. 227 | * 228 | * @param {number} type - Event type. 229 | */ 230 | MidiEvent.prototype.setType = function(type) { 231 | if (type < MidiEvent.NOTE_OFF || type > MidiEvent.PITCH_BEND) { 232 | throw new Error("Trying to set an unknown event: " + type); 233 | } 234 | 235 | this.type = type; 236 | }; 237 | 238 | /** 239 | * Set the channel for the event. Must be between 0 and 15, inclusive. 240 | * 241 | * @param {number} channel - The event channel. 242 | */ 243 | MidiEvent.prototype.setChannel = function(channel) { 244 | if (channel < 0 || channel > 15) { 245 | throw new Error("Channel is out of bounds."); 246 | } 247 | 248 | this.channel = channel; 249 | }; 250 | 251 | /** 252 | * Set the first parameter for the event. Must be between 0 and 255, 253 | * inclusive. 254 | * 255 | * @param {number} p - The first event parameter value. 256 | */ 257 | MidiEvent.prototype.setParam1 = function(p) { 258 | this.param1 = p; 259 | }; 260 | 261 | /** 262 | * Set the second parameter for the event. Must be between 0 and 255, 263 | * inclusive. 264 | * 265 | * @param {number} p - The second event parameter value. 266 | */ 267 | MidiEvent.prototype.setParam2 = function(p) { 268 | this.param2 = p; 269 | }; 270 | 271 | /** 272 | * Serialize the event to an array of bytes. 273 | * 274 | * @returns {Array} The array of serialized bytes. 275 | */ 276 | MidiEvent.prototype.toBytes = function() { 277 | var byteArray = []; 278 | 279 | var typeChannelByte = this.type | (this.channel & 0xF); 280 | 281 | byteArray.push.apply(byteArray, this.time); 282 | byteArray.push(typeChannelByte); 283 | byteArray.push(this.param1); 284 | 285 | // Some events don't have a second parameter 286 | if (this.param2 !== undefined && this.param2 !== null) { 287 | byteArray.push(this.param2); 288 | } 289 | return byteArray; 290 | }; 291 | 292 | /* ****************************************************************** 293 | * MetaEvent class 294 | ****************************************************************** */ 295 | 296 | /** 297 | * Construct a meta event. 298 | * 299 | * Parameters include: 300 | * - time [optional number] - Ticks since previous event. 301 | * - type [required number] - Type of event. 302 | * - data [optional array|string] - Event data. 303 | */ 304 | var MetaEvent = function(params) { 305 | if (!this) return new MetaEvent(params); 306 | var p = params || {}; 307 | this.setTime(params.time); 308 | this.setType(params.type); 309 | this.setData(params.data); 310 | }; 311 | 312 | MetaEvent.SEQUENCE = 0x00; 313 | MetaEvent.TEXT = 0x01; 314 | MetaEvent.COPYRIGHT = 0x02; 315 | MetaEvent.TRACK_NAME = 0x03; 316 | MetaEvent.INSTRUMENT = 0x04; 317 | MetaEvent.LYRIC = 0x05; 318 | MetaEvent.MARKER = 0x06; 319 | MetaEvent.CUE_POINT = 0x07; 320 | MetaEvent.CHANNEL_PREFIX = 0x20; 321 | MetaEvent.END_OF_TRACK = 0x2f; 322 | MetaEvent.TEMPO = 0x51; 323 | MetaEvent.SMPTE = 0x54; 324 | MetaEvent.TIME_SIG = 0x58; 325 | MetaEvent.KEY_SIG = 0x59; 326 | MetaEvent.SEQ_EVENT = 0x7f; 327 | 328 | /** 329 | * Set the time for the event in ticks since the previous event. 330 | * 331 | * @param {number} ticks - The number of ticks since the previous event. May 332 | * be zero. 333 | */ 334 | MetaEvent.prototype.setTime = function(ticks) { 335 | this.time = Util.translateTickTime(ticks || 0); 336 | }; 337 | 338 | /** 339 | * Set the type of the event. Must be one of the event codes on MetaEvent. 340 | * 341 | * @param {number} t - Event type. 342 | */ 343 | MetaEvent.prototype.setType = function(t) { 344 | this.type = t; 345 | }; 346 | 347 | /** 348 | * Set the data associated with the event. May be a string or array of byte 349 | * values. 350 | * 351 | * @param {string|Array} d - Event data. 352 | */ 353 | MetaEvent.prototype.setData = function(d) { 354 | this.data = d; 355 | }; 356 | 357 | /** 358 | * Serialize the event to an array of bytes. 359 | * 360 | * @returns {Array} The array of serialized bytes. 361 | */ 362 | MetaEvent.prototype.toBytes = function() { 363 | if (!this.type) { 364 | throw new Error("Type for meta-event not specified."); 365 | } 366 | 367 | var byteArray = []; 368 | byteArray.push.apply(byteArray, this.time); 369 | byteArray.push(0xFF, this.type); 370 | 371 | // If data is an array, we assume that it contains several bytes. We 372 | // apend them to byteArray. 373 | if (Array.isArray(this.data)) { 374 | byteArray.push(this.data.length); 375 | byteArray.push.apply(byteArray, this.data); 376 | } else if (typeof this.data == 'number') { 377 | byteArray.push(1, this.data); 378 | } else if (this.data !== null && this.data !== undefined) { 379 | // assume string; may be a bad assumption 380 | byteArray.push(this.data.length); 381 | var dataBytes = this.data.split('').map(function(x){ return x.charCodeAt(0) }); 382 | byteArray.push.apply(byteArray, dataBytes); 383 | } else { 384 | byteArray.push(0); 385 | } 386 | 387 | return byteArray; 388 | }; 389 | 390 | /* ****************************************************************** 391 | * Track class 392 | ****************************************************************** */ 393 | 394 | /** 395 | * Construct a MIDI track. 396 | * 397 | * Parameters include: 398 | * - events [optional array] - Array of events for the track. 399 | */ 400 | var Track = function(config) { 401 | if (!this) return new Track(config); 402 | var c = config || {}; 403 | this.events = c.events || []; 404 | }; 405 | 406 | Track.START_BYTES = [0x4d, 0x54, 0x72, 0x6b]; 407 | Track.END_BYTES = [0x00, 0xFF, 0x2F, 0x00]; 408 | 409 | /** 410 | * Add an event to the track. 411 | * 412 | * @param {MidiEvent|MetaEvent} event - The event to add. 413 | * @returns {Track} The current track. 414 | */ 415 | Track.prototype.addEvent = function(event) { 416 | this.events.push(event); 417 | return this; 418 | }; 419 | 420 | /** 421 | * Add a note-on event to the track. 422 | * 423 | * @param {number} channel - The channel to add the event to. 424 | * @param {number|string} pitch - The pitch of the note, either numeric or 425 | * symbolic. 426 | * @param {number} [time=0] - The number of ticks since the previous event, 427 | * defaults to 0. 428 | * @param {number} [velocity=90] - The volume for the note, defaults to 429 | * DEFAULT_VOLUME. 430 | * @returns {Track} The current track. 431 | */ 432 | Track.prototype.addNoteOn = Track.prototype.noteOn = function(channel, pitch, time, velocity) { 433 | this.events.push(new MidiEvent({ 434 | type: MidiEvent.NOTE_ON, 435 | channel: channel, 436 | param1: Util.ensureMidiPitch(pitch), 437 | param2: velocity || DEFAULT_VOLUME, 438 | time: time || 0, 439 | })); 440 | return this; 441 | }; 442 | 443 | /** 444 | * Add a note-off event to the track. 445 | * 446 | * @param {number} channel - The channel to add the event to. 447 | * @param {number|string} pitch - The pitch of the note, either numeric or 448 | * symbolic. 449 | * @param {number} [time=0] - The number of ticks since the previous event, 450 | * defaults to 0. 451 | * @param {number} [velocity=90] - The velocity the note was released, 452 | * defaults to DEFAULT_VOLUME. 453 | * @returns {Track} The current track. 454 | */ 455 | Track.prototype.addNoteOff = Track.prototype.noteOff = function(channel, pitch, time, velocity) { 456 | this.events.push(new MidiEvent({ 457 | type: MidiEvent.NOTE_OFF, 458 | channel: channel, 459 | param1: Util.ensureMidiPitch(pitch), 460 | param2: velocity || DEFAULT_VOLUME, 461 | time: time || 0, 462 | })); 463 | return this; 464 | }; 465 | 466 | /** 467 | * Add a note-on and -off event to the track. 468 | * 469 | * @param {number} channel - The channel to add the event to. 470 | * @param {number|string} pitch - The pitch of the note, either numeric or 471 | * symbolic. 472 | * @param {number} dur - The duration of the note, in ticks. 473 | * @param {number} [time=0] - The number of ticks since the previous event, 474 | * defaults to 0. 475 | * @param {number} [velocity=90] - The velocity the note was released, 476 | * defaults to DEFAULT_VOLUME. 477 | * @returns {Track} The current track. 478 | */ 479 | Track.prototype.addNote = Track.prototype.note = function(channel, pitch, dur, time, velocity) { 480 | this.noteOn(channel, pitch, time, velocity); 481 | if (dur) { 482 | this.noteOff(channel, pitch, dur, velocity); 483 | } 484 | return this; 485 | }; 486 | 487 | /** 488 | * Add a note-on and -off event to the track for each pitch in an array of pitches. 489 | * 490 | * @param {number} channel - The channel to add the event to. 491 | * @param {array} chord - An array of pitches, either numeric or 492 | * symbolic. 493 | * @param {number} dur - The duration of the chord, in ticks. 494 | * @param {number} [velocity=90] - The velocity of the chord, 495 | * defaults to DEFAULT_VOLUME. 496 | * @returns {Track} The current track. 497 | */ 498 | Track.prototype.addChord = Track.prototype.chord = function(channel, chord, dur, velocity) { 499 | if (!Array.isArray(chord) && !chord.length) { 500 | throw new Error('Chord must be an array of pitches'); 501 | } 502 | chord.forEach(function(note) { 503 | this.noteOn(channel, note, 0, velocity); 504 | }, this); 505 | chord.forEach(function(note, index) { 506 | if (index === 0) { 507 | this.noteOff(channel, note, dur); 508 | } else { 509 | this.noteOff(channel, note); 510 | } 511 | }, this); 512 | return this; 513 | }; 514 | 515 | /** 516 | * Set instrument for the track. 517 | * 518 | * @param {number} channel - The channel to set the instrument on. 519 | * @param {number} instrument - The instrument to set it to. 520 | * @param {number} [time=0] - The number of ticks since the previous event, 521 | * defaults to 0. 522 | * @returns {Track} The current track. 523 | */ 524 | Track.prototype.setInstrument = Track.prototype.instrument = function(channel, instrument, time) { 525 | this.events.push(new MidiEvent({ 526 | type: MidiEvent.PROGRAM_CHANGE, 527 | channel: channel, 528 | param1: instrument, 529 | time: time || 0, 530 | })); 531 | return this; 532 | }; 533 | 534 | /** 535 | * Set the tempo for the track. 536 | * 537 | * @param {number} bpm - The new number of beats per minute. 538 | * @param {number} [time=0] - The number of ticks since the previous event, 539 | * defaults to 0. 540 | * @returns {Track} The current track. 541 | */ 542 | Track.prototype.setTempo = Track.prototype.tempo = function(bpm, time) { 543 | this.events.push(new MetaEvent({ 544 | type: MetaEvent.TEMPO, 545 | data: Util.mpqnFromBpm(bpm), 546 | time: time || 0, 547 | })); 548 | return this; 549 | }; 550 | 551 | /** 552 | * Serialize the track to an array of bytes. 553 | * 554 | * @returns {Array} The array of serialized bytes. 555 | */ 556 | Track.prototype.toBytes = function() { 557 | var trackLength = 0; 558 | var eventBytes = []; 559 | var startBytes = Track.START_BYTES; 560 | var endBytes = Track.END_BYTES; 561 | 562 | var addEventBytes = function(event) { 563 | var bytes = event.toBytes(); 564 | trackLength += bytes.length; 565 | eventBytes.push.apply(eventBytes, bytes); 566 | }; 567 | 568 | this.events.forEach(addEventBytes); 569 | 570 | // Add the end-of-track bytes to the sum of bytes for the track, since 571 | // they are counted (unlike the start-of-track ones). 572 | trackLength += endBytes.length; 573 | 574 | // Makes sure that track length will fill up 4 bytes with 0s in case 575 | // the length is less than that (the usual case). 576 | var lengthBytes = Util.str2Bytes(trackLength.toString(16), 4); 577 | 578 | return startBytes.concat(lengthBytes, eventBytes, endBytes); 579 | }; 580 | 581 | /* ****************************************************************** 582 | * File class 583 | ****************************************************************** */ 584 | 585 | /** 586 | * Construct a file object. 587 | * 588 | * Parameters include: 589 | * - ticks [optional number] - Number of ticks per beat, defaults to 128. 590 | * Must be 1-32767. 591 | * - tracks [optional array] - Track data. 592 | */ 593 | var File = function(config){ 594 | if (!this) return new File(config); 595 | 596 | var c = config || {}; 597 | if (c.ticks) { 598 | if (typeof c.ticks !== 'number') { 599 | throw new Error('Ticks per beat must be a number!'); 600 | return; 601 | } 602 | if (c.ticks <= 0 || c.ticks >= (1 << 15) || c.ticks % 1 !== 0) { 603 | throw new Error('Ticks per beat must be an integer between 1 and 32767!'); 604 | return; 605 | } 606 | } 607 | 608 | this.ticks = c.ticks || 128; 609 | this.tracks = c.tracks || []; 610 | }; 611 | 612 | File.HDR_CHUNKID = "MThd"; // File magic cookie 613 | File.HDR_CHUNK_SIZE = "\x00\x00\x00\x06"; // Header length for SMF 614 | File.HDR_TYPE0 = "\x00\x00"; // Midi Type 0 id 615 | File.HDR_TYPE1 = "\x00\x01"; // Midi Type 1 id 616 | 617 | /** 618 | * Add a track to the file. 619 | * 620 | * @param {Track} track - The track to add. 621 | */ 622 | File.prototype.addTrack = function(track) { 623 | if (track) { 624 | this.tracks.push(track); 625 | return this; 626 | } else { 627 | track = new Track(); 628 | this.tracks.push(track); 629 | return track; 630 | } 631 | }; 632 | 633 | /** 634 | * Serialize the MIDI file to an array of bytes. 635 | * 636 | * @returns {Array} The array of serialized bytes. 637 | */ 638 | File.prototype.toBytes = function() { 639 | var trackCount = this.tracks.length.toString(16); 640 | 641 | // prepare the file header 642 | var bytes = File.HDR_CHUNKID + File.HDR_CHUNK_SIZE; 643 | 644 | // set Midi type based on number of tracks 645 | if (parseInt(trackCount, 16) > 1) { 646 | bytes += File.HDR_TYPE1; 647 | } else { 648 | bytes += File.HDR_TYPE0; 649 | } 650 | 651 | // add the number of tracks (2 bytes) 652 | bytes += Util.codes2Str(Util.str2Bytes(trackCount, 2)); 653 | // add the number of ticks per beat (currently hardcoded) 654 | bytes += String.fromCharCode((this.ticks/256), this.ticks%256);; 655 | 656 | // iterate over the tracks, converting to bytes too 657 | this.tracks.forEach(function(track) { 658 | bytes += Util.codes2Str(track.toBytes()); 659 | }); 660 | 661 | return bytes; 662 | }; 663 | 664 | /* ****************************************************************** 665 | * Exports 666 | ****************************************************************** */ 667 | 668 | exported.Util = Util; 669 | exported.File = File; 670 | exported.Track = Track; 671 | exported.Event = MidiEvent; 672 | exported.MetaEvent = MetaEvent; 673 | 674 | })( Midi ); 675 | 676 | if (typeof module != 'undefined' && module !== null) { 677 | module.exports = Midi; 678 | } else if (typeof exports != 'undefined' && exports !== null) { 679 | exports = Midi; 680 | } else { 681 | this.Midi = Midi; 682 | } 683 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsmidgen", 3 | "version": "0.1.5", 4 | "author": "Dave Ingram ", 5 | "description": "a pure-JavaScript MIDI file generator", 6 | "main": "./lib/jsmidgen.js", 7 | "scripts": { 8 | "test": "gulp" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/dingram/jsmidgen.git" 13 | }, 14 | "keywords": [ 15 | "midi", 16 | "mid", 17 | "generate", 18 | "generator", 19 | "library" 20 | ], 21 | "license": "MIT", 22 | "engine": { 23 | "node": ">=0.6" 24 | }, 25 | "devDependencies": { 26 | "gulp": "^3.9.1", 27 | "gulp-tape": "0.0.7", 28 | "tape": "^4.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/file-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var jsmidgen = require('../lib/jsmidgen.js'); 3 | 4 | test('File -> setTicks should set the correct HDR_SPEED on valid input', function(t) { 5 | var file = new jsmidgen.File({ 6 | ticks: 1000 7 | }); 8 | t.equal(file.ticks, 1000, 'ticks should be set to 1000'); 9 | 10 | t.end(); 11 | }); 12 | 13 | test('File -> setTicks should throw error on invalid input ', function(t) { 14 | t.throws(function() { 15 | new jsmidgen.File({ 16 | ticks: 'not a number' 17 | }); 18 | }); 19 | t.throws(function() { 20 | new jsmidgen.File({ 21 | ticks: 85000 22 | }); 23 | }); 24 | 25 | t.throws(function() { 26 | new jsmidgen.File({ 27 | ticks: 133.7 28 | }); 29 | }); 30 | 31 | t.end(); 32 | }); -------------------------------------------------------------------------------- /test/midi-util-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var Util = require('../lib/jsmidgen.js').Util; 3 | 4 | test('Midi letter -> pitches', function(t) { 5 | var pitch = Util.midi_letter_pitches; 6 | 7 | t.equal(pitch.a, 21, 'Pitch for a should be 21'); 8 | t.equal(pitch.b, 23, 'Pitch for b should be 23'); 9 | t.equal(pitch.c, 12, 'Pitch for c should be 12'); 10 | t.equal(pitch.d, 14, 'Pitch for d should be 14'); 11 | t.equal(pitch.e, 16, 'Pitch for e should be 16'); 12 | t.equal(pitch.f, 17, 'Pitch for f should be 17'); 13 | t.equal(pitch.g, 19, 'Pitch for g should be 19'); 14 | 15 | t.end(); 16 | }); 17 | 18 | // Check pitchFromNote against a wide range to ensure no additional functionality breaks it 19 | test('Midi pitchFromNote', function(t) { 20 | var pitchFromNote = Util.midiPitchFromNote; 21 | 22 | t.equal(pitchFromNote('a1'), 33, 'Pitch of a1 is 33'); 23 | t.equal(pitchFromNote('b2'), 47, 'Pitch of b2 is 47'); 24 | t.equal(pitchFromNote('c3'), 48, 'Pitch of c3 is 48'); 25 | t.equal(pitchFromNote('c#3'), 49, 'Pitch of c#3 is 49'); 26 | t.equal(pitchFromNote('d4'), 62, 'Pitch of d4 is 62'); 27 | t.equal(pitchFromNote('e5'), 76, 'Pitch of e5 is 76'); 28 | t.equal(pitchFromNote('f6'), 89, 'Pitch of f6 is 89'); 29 | t.equal(pitchFromNote('f#6'), 90, 'Pitch of f#6 is 90'); 30 | t.equal(pitchFromNote('g7'), 103, 'Pitch of g7 is 103'); 31 | t.equal(pitchFromNote('g#7'), 104, 'Pitch of g#7 is 104'); 32 | 33 | // Check pitch with flattened notes 34 | t.equal(pitchFromNote('bb1'), 34, 'Pitch of a#1 is 34'); 35 | t.equal(pitchFromNote('eb4'), 63, 'Pitch of d#4 is 63'); 36 | 37 | // Check pitch with unconventional notes 38 | t.equal(pitchFromNote('fb4'), 64, 'Pitch for unconventional note fb4 is the same as e4'); 39 | t.equal(pitchFromNote('e#4'), 65, 'Pitch for unconventional note e#4 is the same as f4'); 40 | 41 | // Check pitch with cross octave numbers 42 | t.equal(pitchFromNote('b#2'), 48, 'Pitch for b#2 is the same as c3'); 43 | t.equal(pitchFromNote('cb3'), 47, 'Pitch for cb3 is the same as b2'); 44 | t.end(); 45 | }); 46 | 47 | test('Midi -> Util -> ensureMidiPitch', function(t) { 48 | var ensureMidiPitch = Util.ensureMidiPitch; 49 | 50 | t.equal(ensureMidiPitch(2), 2, 'Number input is accepted'); 51 | t.equal(ensureMidiPitch('c3'), 48, 'A string of note name and octave is accepted'); 52 | 53 | t.end(); 54 | }); 55 | 56 | test('Midi -> Util -> noteFromMidiPitch', function(t) { 57 | var noteFromMidiPitch = Util.noteFromMidiPitch; 58 | 59 | t.equal(noteFromMidiPitch(33), 'a1' , 'Note for Midi pitch 33 is a1') 60 | t.equal(noteFromMidiPitch(47), 'b2' , 'Note for Midi pitch 47 is b2') 61 | t.equal(noteFromMidiPitch(48), 'c3' , 'Note for Midi pitch 48 is c3') 62 | t.equal(noteFromMidiPitch(49), 'c#3' , 'Note for Midi pitch 49 is c#3') 63 | t.equal(noteFromMidiPitch(62), 'd4' , 'Note for Midi pitch 62 is d4') 64 | t.equal(noteFromMidiPitch(76), 'e5' , 'Note for Midi pitch 76 is e5') 65 | t.equal(noteFromMidiPitch(89), 'f6' , 'Note for Midi pitch 89 is f6') 66 | t.equal(noteFromMidiPitch(90), 'f#6' , 'Note for Midi pitch 90 is f#6') 67 | t.equal(noteFromMidiPitch(103), 'g7' , 'Note for Midi pitch 103 is g7') 68 | t.equal(noteFromMidiPitch(104), 'g#7' , 'Note for Midi pitch 104 is g#7') 69 | 70 | // Check with returnFlattened set to true 71 | t.equal(noteFromMidiPitch(34, true), 'bb1' , 'Note for Midi pitch 34 is bb1') 72 | t.equal(noteFromMidiPitch(63, true), 'eb4' , 'Note for Midi pitch 63 is eb4') 73 | t.end(); 74 | }); 75 | 76 | test('Midi -> Util -> mpqnFromBpm', function(t) { 77 | var mpqnFromBpm = Util.mpqnFromBpm; 78 | var mpqn = mpqnFromBpm(120); 79 | 80 | t.deepEquals(mpqn, [7, 161, 32], 'mpqnFromBpm returns expected array'); 81 | 82 | t.end(); 83 | }); 84 | 85 | test('Midi -> Util -> bpmFromMpqn', function(t) { 86 | var bpm = Util.bpmFromMpqn(500000); 87 | 88 | t.equal(bpm, 120, 'bpmFromMpqn returns expected BPM'); 89 | 90 | t.end(); 91 | }); 92 | 93 | test('Midi -> Util -> codes2Str', function(t) { 94 | t.equal(Util.codes2Str([65, 66, 67]), 'ABC', 'codes2Str returns expected output'); 95 | 96 | t.end(); 97 | }); 98 | 99 | test('Midi -> Util -> str2Bytes', function(t) { 100 | t.equal(Util.str2Bytes('c')[0], 12, 'str2Bytes returns expected output'); 101 | 102 | t.end(); 103 | }); 104 | 105 | test('Midi -> Util -> translateTickTime', function(t) { 106 | t.deepEquals(Util.translateTickTime(16), [16], 'translateTickTime translates tick to MIDI timestamp as expected'); 107 | t.deepEquals(Util.translateTickTime(32), [32], 'translateTickTime translates tick to MIDI timestamp as expected'); 108 | t.deepEquals(Util.translateTickTime(128), [129, 0], 'translateTickTime translates tick to MIDI timestamp as expected'); 109 | t.deepEquals(Util.translateTickTime(512), [132, 0], 'translateTickTime translates tick to MIDI timestamp as expected'); 110 | 111 | t.end(); 112 | }); 113 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var Midi = require('../lib/jsmidgen.js'); 2 | var fs = require('fs'); 3 | 4 | var file = new Midi.File(); 5 | var track = new Midi.Track(); 6 | file.addTrack(track); 7 | 8 | track.addNote(0, 'c4', 64); 9 | track.addNote(0, 'd4', 64); 10 | track.addNote(0, 'e4', 64); 11 | track.addNote(0, 'f4', 64); 12 | track.addNote(0, 'g4', 64); 13 | track.addNote(0, 'a4', 64); 14 | track.addNote(0, 'b4', 64); 15 | track.addNote(0, 'c5', 64); 16 | 17 | track.setInstrument(0, 0x13); 18 | 19 | track.addNoteOn(0, 'c4', 64); 20 | track.addNoteOn(0, 'e4'); 21 | track.addNoteOn(0, 'g4'); 22 | track.addNoteOff(0, 'c4', 47); 23 | track.addNoteOff(0, 'e4'); 24 | track.addNoteOff(0, 'g4'); 25 | 26 | track.addNoteOn(0, 'c4', 1); 27 | track.addNoteOn(0, 'e4'); 28 | track.addNoteOn(0, 'g4'); 29 | track.addNoteOff(0, 'c4', 384); 30 | track.addNoteOff(0, 'e4'); 31 | track.addNoteOff(0, 'g4'); 32 | 33 | fs.writeFileSync('test.mid', file.toBytes(), 'binary'); 34 | 35 | file = new Midi.File(); 36 | file 37 | .addTrack() 38 | 39 | .addNote(0, 'c4', 32) 40 | .addNote(0, 'd4', 32) 41 | .addNote(0, 'e4', 32) 42 | .addNote(0, 'f4', 32) 43 | .addNote(0, 'g4', 32) 44 | .addNote(0, 'a4', 32) 45 | .addNote(0, 'b4', 32) 46 | .addNote(0, 'c5', 32) 47 | 48 | .setInstrument(0, 0x13) 49 | 50 | .addNoteOn(0, 'c4', 64) 51 | .addNoteOn(0, 'e4') 52 | .addNoteOn(0, 'g4') 53 | .addNoteOff(0, 'c4', 47) 54 | .addNoteOff(0, 'e4') 55 | .addNoteOff(0, 'g4') 56 | 57 | .addNoteOn(0, 'c4', 1) 58 | .addNoteOn(0, 'e4') 59 | .addNoteOn(0, 'g4') 60 | .addNoteOff(0, 'c4', 384) 61 | .addNoteOff(0, 'e4') 62 | .addNoteOff(0, 'g4') 63 | ; 64 | 65 | fs.writeFileSync('test2.mid', file.toBytes(), 'binary'); 66 | --------------------------------------------------------------------------------