├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── lint.js.yml │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── browser └── midiwriter.js ├── examples ├── chopin-prelude-e-minor.js ├── hot-cross-buns.js ├── mauro.giuliani-op.47-main-theme.js ├── notes-by-start-tick.js └── zelda-main-theme.js ├── jsdoc.json ├── package-lock.json ├── package.json ├── postinstall.js ├── rollup.config.js ├── runkit.js ├── src ├── abstract-event.ts ├── chunks │ ├── chunk.ts │ ├── header.ts │ └── track.ts ├── constants.ts ├── main.ts ├── meta-events │ ├── copyright-event.ts │ ├── cue-point-event.ts │ ├── end-track-event.ts │ ├── instrument-name-event.ts │ ├── key-signature-event.ts │ ├── lyric-event.ts │ ├── marker-event.ts │ ├── meta-event.ts │ ├── tempo-event.ts │ ├── text-event.ts │ ├── time-signature-event.ts │ └── track-name-event.ts ├── midi-events │ ├── controller-change-event.ts │ ├── midi-event.ts │ ├── note-event.ts │ ├── note-off-event.ts │ ├── note-on-event.ts │ ├── pitch-bend-event.ts │ └── program-change-event.ts ├── utils.ts ├── vexflow.ts └── writer.ts ├── test ├── main.js ├── vexflow.js └── writer.js ├── tsconfig.json └── typedoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = tab 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.json] 13 | insert_final_newline = false 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.md] 18 | indent_size = 2 19 | indent_style = space 20 | trim_trailing_whitespace = false 21 | 22 | [*.yml] 23 | indent_size = 2 24 | indent_style = space 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/__tests__/** 2 | **/coverage/** 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 8 | "parser": '@typescript-eslint/parser', 9 | "plugins": ['@typescript-eslint'], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | } 18 | }; -------------------------------------------------------------------------------- /.github/workflows/lint.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Lint 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run lint:js 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | build 5 | browser/types 6 | index.html 7 | index.js 8 | yarn.lock 9 | 10 | # test artifacts 11 | test.mid.mid 12 | .nyc_output/ 13 | 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | src 3 | .nyc_output 4 | .github -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.14.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Garrett Grimm 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ♬ MidiWriterJS 2 | =============== 3 | [![npm version](https://img.shields.io/npm/v/midi-writer-js.svg)](https://www.npmjs.com/package/midi-writer-js) 4 | ![Tests](https://github.com/grimmdude/MidiWriterJS/actions/workflows/lint.js.yml/badge.svg) 5 | ![Lint](https://github.com/grimmdude/MidiWriterJS/actions/workflows/node.js.yml/badge.svg) 6 | [![Try midi-writer-js on RunKit](https://badge.runkitcdn.com/midi-writer-js.svg)](https://npm.runkit.com/midi-writer-js) 7 | 8 | MidiWriterJS is a JavaScript library providing an API for generating expressive multi-track MIDI files. 9 | 10 | Note that the `master` branch is in active development so if you're looking for a tried and true stable version please use the latest release. 11 | 12 | [Source Documentation](https://grimmdude.com/MidiWriterJS/docs/) 13 | 14 | Install 15 | ------------ 16 | ```sh 17 | npm install midi-writer-js 18 | ``` 19 | Getting Started 20 | ------------ 21 | 22 | ```javascript 23 | import MidiWriter from 'midi-writer-js'; 24 | 25 | // Start with a new track 26 | const track = new MidiWriter.Track(); 27 | 28 | // Define an instrument (optional): 29 | track.addEvent(new MidiWriter.ProgramChangeEvent({instrument: 1})); 30 | 31 | // Add some notes: 32 | const note = new MidiWriter.NoteEvent({pitch: ['C4', 'D4', 'E4'], duration: '4'}); 33 | track.addEvent(note); 34 | 35 | // Generate a data URI 36 | const write = new MidiWriter.Writer(track); 37 | console.log(write.dataUri()); 38 | ``` 39 | Documentation 40 | ------------ 41 | 42 | ### `MidiWriter.Track()` 43 | 44 | - `addEvent({event}, mapFunction)` 45 | - `setTempo(tempo)` 46 | - `addText(text)` 47 | - `addCopyright(text)` 48 | - `addTrackName(text)` 49 | - `addInstrumentName(text)` 50 | - `addMarker(text)` 51 | - `addCuePoint(text)` 52 | - `addLyric(text)` 53 | - `setTimeSignature(numerator, denominator)` 54 | 55 | ### `MidiWriter.NoteEvent({options})` 56 | 57 | The MIDI spec defines that each note must have a `NoteOnEvent` and `NoteOffEvent` (or `NoteOnEvent` with zero velocity) event, marking the beginning and end of the sounding note. While it's possible to manually add these events to a track with `Track.addEvent()`, the `NoteEvent` provides a more intuitive interface for doing this with a single, "pseudo" event. Under the hood, the `NoteEvent` event generates the relevant `NoteOnEvent` and `NoteOffEvent` events. 58 | 59 | Each MIDI event has a `delta` property, which is used to define the number of ticks to wait after the previous event. This can be challenging to calculate if you're not necessarily adding events in a serial fashion. Because of this, you can alternatively use the `tick` property to define the exact tick where the event should fall. 60 | 61 | The `NoteEvent` supports these options: 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |
NameTypeDefaultDescription
pitchstring or arrayEach pitch can be a string or valid MIDI note code. Format for string is C#4. Pro tip: You can use the output from tonal functions to build scales, chords, intervals, etc. in this parameter.
durationstring or array 84 | How long the note should sound. 85 |
    86 |
  • 1 : whole
  • 87 |
  • 2 : half
  • 88 |
  • d2 : dotted half
  • 89 |
  • dd2 : double dotted half
  • 90 |
  • 4 : quarter
  • 91 |
  • 4t : quarter triplet
  • 92 |
  • d4 : dotted quarter
  • 93 |
  • dd4 : double dotted quarter
  • 94 |
  • 8 : eighth
  • 95 |
  • 8t : eighth triplet
  • 96 |
  • d8 : dotted eighth
  • 97 |
  • dd8 : double dotted eighth
  • 98 |
  • 16 : sixteenth
  • 99 |
  • 16t : sixteenth triplet
  • 100 |
  • 32 : thirty-second
  • 101 |
  • 64 : sixty-fourth
  • 102 |
  • Tn : where n is an explicit number of ticks (T128 = 1 beat)
  • 103 |
104 | If an array of durations is passed then the sum of the durations will be used. 105 |
waitstring or array0How long to wait before sounding note (rest). Takes same values as duration.
sequentialbooleanfalseIf true then array of pitches will be played sequentially as opposed to simulatanously.
velocitynumber50How loud the note should sound, values 1-100.
repeatnumber1How many times this event should be repeated.
channelnumber1MIDI channel to use.
gracestring or arrayGrace note to be applied to note event. Takes same value format as pitch
ticknumberSpecific tick where this event should be played. If this parameter is supplied then wait is disregarded if also supplied.
151 | 152 | 153 | ### `MidiWriter.Writer(tracks)` 154 | The `Writer` class provides a few ways to output the file: 155 | - `buildFile()` *Uint8Array* 156 | - `base64()` *string* 157 | - `dataUri()` *string* 158 | - `stdout()` *file stream (cli)* 159 | 160 | ### Hot Cross Buns 161 | Here's an example of how everyone's favorite song "Hot Cross Buns" could be written. Note use of the mapping function passed as the second argument of `addEvent()`. This can be used to apply specific properties to all events. With some 162 | street smarts you could also use it for programmatic crescendos and other property 'animation'. 163 | ```javascript 164 | import MidiWriter from 'midi-writer-js'; 165 | 166 | const track = new MidiWriter.Track(); 167 | 168 | track.addEvent([ 169 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 170 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}), 171 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 172 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}), 173 | new MidiWriter.NoteEvent({pitch: ['C4', 'C4', 'C4', 'C4', 'D4', 'D4', 'D4', 'D4'], duration: '8'}), 174 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 175 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}) 176 | ], function(event, index) { 177 | return {sequential: true}; 178 | } 179 | ); 180 | 181 | const write = new MidiWriter.Writer(track); 182 | console.log(write.dataUri()); 183 | ``` 184 | 185 | ### VexFlow Integration 186 | MidiWriterJS can export MIDI from VexFlow voices, though this feature is still experimental. Current usage is to use `MidiWriter.VexFlow.trackFromVoice(voice)` to create a MidiWriterJS `Track` object: 187 | ```javascript 188 | 189 | // ...VexFlow code defining notes 190 | const voice = create_4_4_voice().addTickables(notes); 191 | 192 | const vexWriter = new MidiWriter.VexFlow(); 193 | const track = vexWriter.trackFromVoice(voice); 194 | const writer = new MidiWriter.Writer([track]); 195 | console.log(writer.dataUri()); 196 | ``` 197 | 198 | 199 | ## Demos 200 | * [Example with Magenta player](https://codepen.io/dirkk0/pen/rNZLXjZ) by Dirk Krause [@dirkk0](https://github.com/dirkk0) 201 | -------------------------------------------------------------------------------- /browser/midiwriter.js: -------------------------------------------------------------------------------- 1 | var MidiWriter = (function () { 2 | 'use strict'; 3 | 4 | /** 5 | * MIDI file format constants. 6 | * @return {Constants} 7 | */ 8 | var Constants = { 9 | VERSION: '3.1.1', 10 | HEADER_CHUNK_TYPE: [0x4d, 0x54, 0x68, 0x64], 11 | HEADER_CHUNK_LENGTH: [0x00, 0x00, 0x00, 0x06], 12 | HEADER_CHUNK_FORMAT0: [0x00, 0x00], 13 | HEADER_CHUNK_FORMAT1: [0x00, 0x01], 14 | HEADER_CHUNK_DIVISION: [0x00, 0x80], 15 | TRACK_CHUNK_TYPE: [0x4d, 0x54, 0x72, 0x6b], 16 | META_EVENT_ID: 0xFF, 17 | META_SMTPE_OFFSET: 0x54 18 | }; 19 | 20 | // src/utils.ts 21 | var fillStr = (s, n) => Array(Math.abs(n) + 1).join(s); 22 | 23 | // src/named.ts 24 | function isNamed(src) { 25 | return src !== null && typeof src === "object" && typeof src.name === "string" ? true : false; 26 | } 27 | 28 | // src/pitch.ts 29 | function isPitch(pitch) { 30 | return pitch !== null && typeof pitch === "object" && typeof pitch.step === "number" && typeof pitch.alt === "number" ? true : false; 31 | } 32 | var FIFTHS = [0, 2, 4, -1, 1, 3, 5]; 33 | var STEPS_TO_OCTS = FIFTHS.map( 34 | (fifths) => Math.floor(fifths * 7 / 12) 35 | ); 36 | function encode(pitch) { 37 | const { step, alt, oct, dir = 1 } = pitch; 38 | const f = FIFTHS[step] + 7 * alt; 39 | if (oct === void 0) { 40 | return [dir * f]; 41 | } 42 | const o = oct - STEPS_TO_OCTS[step] - 4 * alt; 43 | return [dir * f, dir * o]; 44 | } 45 | 46 | // src/note.ts 47 | var NoNote = { empty: true, name: "", pc: "", acc: "" }; 48 | var cache = /* @__PURE__ */ new Map(); 49 | var stepToLetter = (step) => "CDEFGAB".charAt(step); 50 | var altToAcc = (alt) => alt < 0 ? fillStr("b", -alt) : fillStr("#", alt); 51 | var accToAlt = (acc) => acc[0] === "b" ? -acc.length : acc.length; 52 | function note(src) { 53 | const stringSrc = JSON.stringify(src); 54 | const cached = cache.get(stringSrc); 55 | if (cached) { 56 | return cached; 57 | } 58 | const value = typeof src === "string" ? parse(src) : isPitch(src) ? note(pitchName(src)) : isNamed(src) ? note(src.name) : NoNote; 59 | cache.set(stringSrc, value); 60 | return value; 61 | } 62 | var REGEX = /^([a-gA-G]?)(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)$/; 63 | function tokenizeNote(str) { 64 | const m = REGEX.exec(str); 65 | return [m[1].toUpperCase(), m[2].replace(/x/g, "##"), m[3], m[4]]; 66 | } 67 | var mod = (n, m) => (n % m + m) % m; 68 | var SEMI = [0, 2, 4, 5, 7, 9, 11]; 69 | function parse(noteName) { 70 | const tokens = tokenizeNote(noteName); 71 | if (tokens[0] === "" || tokens[3] !== "") { 72 | return NoNote; 73 | } 74 | const letter = tokens[0]; 75 | const acc = tokens[1]; 76 | const octStr = tokens[2]; 77 | const step = (letter.charCodeAt(0) + 3) % 7; 78 | const alt = accToAlt(acc); 79 | const oct = octStr.length ? +octStr : void 0; 80 | const coord = encode({ step, alt, oct }); 81 | const name = letter + acc + octStr; 82 | const pc = letter + acc; 83 | const chroma = (SEMI[step] + alt + 120) % 12; 84 | const height = oct === void 0 ? mod(SEMI[step] + alt, 12) - 12 * 99 : SEMI[step] + alt + 12 * (oct + 1); 85 | const midi = height >= 0 && height <= 127 ? height : null; 86 | const freq = oct === void 0 ? null : Math.pow(2, (height - 69) / 12) * 440; 87 | return { 88 | empty: false, 89 | acc, 90 | alt, 91 | chroma, 92 | coord, 93 | freq, 94 | height, 95 | letter, 96 | midi, 97 | name, 98 | oct, 99 | pc, 100 | step 101 | }; 102 | } 103 | function pitchName(props) { 104 | const { step, alt, oct } = props; 105 | const letter = stepToLetter(step); 106 | if (!letter) { 107 | return ""; 108 | } 109 | const pc = letter + altToAcc(alt); 110 | return oct || oct === 0 ? pc + oct : pc; 111 | } 112 | 113 | // index.ts 114 | function isMidi(arg) { 115 | return +arg >= 0 && +arg <= 127; 116 | } 117 | function toMidi(note$1) { 118 | if (isMidi(note$1)) { 119 | return +note$1; 120 | } 121 | const n = note(note$1); 122 | return n.empty ? null : n.midi; 123 | } 124 | 125 | /** 126 | * Static utility functions used throughout the library. 127 | */ 128 | var Utils = /** @class */ (function () { 129 | function Utils() { 130 | } 131 | /** 132 | * Gets MidiWriterJS version number. 133 | * @return {string} 134 | */ 135 | Utils.version = function () { 136 | return Constants.VERSION; 137 | }; 138 | /** 139 | * Convert a string to an array of bytes 140 | * @param {string} string 141 | * @return {array} 142 | */ 143 | Utils.stringToBytes = function (string) { 144 | return string.split('').map(function (char) { return char.charCodeAt(0); }); 145 | }; 146 | /** 147 | * Checks if argument is a valid number. 148 | * @param {*} n - Value to check 149 | * @return {boolean} 150 | */ 151 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 152 | Utils.isNumeric = function (n) { 153 | return !isNaN(parseFloat(n)) && isFinite(n); 154 | }; 155 | /** 156 | * Returns the correct MIDI number for the specified pitch. 157 | * Uses Tonal Midi - https://github.com/danigb/tonal/tree/master/packages/midi 158 | * @param {(string|number)} pitch - 'C#4' or midi note code 159 | * @param {string} middleC 160 | * @return {number} 161 | */ 162 | Utils.getPitch = function (pitch, middleC) { 163 | if (middleC === void 0) { middleC = 'C4'; } 164 | return 60 - toMidi(middleC) + toMidi(pitch); 165 | }; 166 | /** 167 | * Translates number of ticks to MIDI timestamp format, returning an array of 168 | * hex strings with the time values. Midi has a very particular time to express time, 169 | * take a good look at the spec before ever touching this function. 170 | * Thanks to https://github.com/sergi/jsmidi 171 | * 172 | * @param {number} ticks - Number of ticks to be translated 173 | * @return {array} - Bytes that form the MIDI time value 174 | */ 175 | Utils.numberToVariableLength = function (ticks) { 176 | ticks = Math.round(ticks); 177 | var buffer = ticks & 0x7F; 178 | // eslint-disable-next-line no-cond-assign 179 | while (ticks = ticks >> 7) { 180 | buffer <<= 8; 181 | buffer |= ((ticks & 0x7F) | 0x80); 182 | } 183 | var bList = []; 184 | // eslint-disable-next-line no-constant-condition 185 | while (true) { 186 | bList.push(buffer & 0xff); 187 | if (buffer & 0x80) 188 | buffer >>= 8; 189 | else { 190 | break; 191 | } 192 | } 193 | return bList; 194 | }; 195 | /** 196 | * Counts number of bytes in string 197 | * @param {string} s 198 | * @return {number} 199 | */ 200 | Utils.stringByteCount = function (s) { 201 | return encodeURI(s).split(/%..|./).length - 1; 202 | }; 203 | /** 204 | * Get an int from an array of bytes. 205 | * @param {array} bytes 206 | * @return {number} 207 | */ 208 | Utils.numberFromBytes = function (bytes) { 209 | var hex = ''; 210 | var stringResult; 211 | bytes.forEach(function (byte) { 212 | stringResult = byte.toString(16); 213 | // ensure string is 2 chars 214 | if (stringResult.length == 1) 215 | stringResult = "0" + stringResult; 216 | hex += stringResult; 217 | }); 218 | return parseInt(hex, 16); 219 | }; 220 | /** 221 | * Takes a number and splits it up into an array of bytes. Can be padded by passing a number to bytesNeeded 222 | * @param {number} number 223 | * @param {number} bytesNeeded 224 | * @return {array} - Array of bytes 225 | */ 226 | Utils.numberToBytes = function (number, bytesNeeded) { 227 | bytesNeeded = bytesNeeded || 1; 228 | var hexString = number.toString(16); 229 | if (hexString.length & 1) { // Make sure hex string is even number of chars 230 | hexString = '0' + hexString; 231 | } 232 | // Split hex string into an array of two char elements 233 | var hexArray = hexString.match(/.{2}/g); 234 | // Now parse them out as integers 235 | var intArray = hexArray.map(function (item) { return parseInt(item, 16); }); 236 | // Prepend empty bytes if we don't have enough 237 | if (intArray.length < bytesNeeded) { 238 | while (bytesNeeded - intArray.length > 0) { 239 | intArray.unshift(0); 240 | } 241 | } 242 | return intArray; 243 | }; 244 | /** 245 | * Converts value to array if needed. 246 | * @param {any} value 247 | * @return {array} 248 | */ 249 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 250 | Utils.toArray = function (value) { 251 | if (Array.isArray(value)) 252 | return value; 253 | return [value]; 254 | }; 255 | /** 256 | * Converts velocity to value 0-127 257 | * @param {number} velocity - Velocity value 1-100 258 | * @return {number} 259 | */ 260 | Utils.convertVelocity = function (velocity) { 261 | // Max passed value limited to 100 262 | velocity = velocity > 100 ? 100 : velocity; 263 | return Math.round(velocity / 100 * 127); 264 | }; 265 | /** 266 | * Gets the total number of ticks of a specified duration. 267 | * Note: type=='note' defaults to quarter note, type==='rest' defaults to 0 268 | * @param {(string|array)} duration 269 | * @return {number} 270 | */ 271 | Utils.getTickDuration = function (duration) { 272 | if (Array.isArray(duration)) { 273 | // Recursively execute this method for each item in the array and return the sum of tick durations. 274 | return duration.map(function (value) { 275 | return Utils.getTickDuration(value); 276 | }).reduce(function (a, b) { 277 | return a + b; 278 | }, 0); 279 | } 280 | duration = duration.toString(); 281 | if (duration.toLowerCase().charAt(0) === 't') { 282 | // If duration starts with 't' then the number that follows is an explicit tick count 283 | var ticks = parseInt(duration.substring(1)); 284 | if (isNaN(ticks) || ticks < 0) { 285 | throw new Error(duration + ' is not a valid duration.'); 286 | } 287 | return ticks; 288 | } 289 | // Need to apply duration here. Quarter note == Constants.HEADER_CHUNK_DIVISION 290 | var quarterTicks = Utils.numberFromBytes(Constants.HEADER_CHUNK_DIVISION); 291 | var tickDuration = quarterTicks * Utils.getDurationMultiplier(duration); 292 | return Utils.getRoundedIfClose(tickDuration); 293 | }; 294 | /** 295 | * Due to rounding errors in JavaScript engines, 296 | * it's safe to round when we're very close to the actual tick number 297 | * 298 | * @static 299 | * @param {number} tick 300 | * @return {number} 301 | */ 302 | Utils.getRoundedIfClose = function (tick) { 303 | var roundedTick = Math.round(tick); 304 | return Math.abs(roundedTick - tick) < 0.000001 ? roundedTick : tick; 305 | }; 306 | /** 307 | * Due to low precision of MIDI, 308 | * we need to keep track of rounding errors in deltas. 309 | * This function will calculate the rounding error for a given duration. 310 | * 311 | * @static 312 | * @param {number} tick 313 | * @return {number} 314 | */ 315 | Utils.getPrecisionLoss = function (tick) { 316 | var roundedTick = Math.round(tick); 317 | return roundedTick - tick; 318 | }; 319 | /** 320 | * Gets what to multiple ticks/quarter note by to get the specified duration. 321 | * Note: type=='note' defaults to quarter note, type==='rest' defaults to 0 322 | * @param {string} duration 323 | * @return {number} 324 | */ 325 | Utils.getDurationMultiplier = function (duration) { 326 | // Need to apply duration here. 327 | // Quarter note == Constants.HEADER_CHUNK_DIVISION ticks. 328 | if (duration === '0') 329 | return 0; 330 | var match = duration.match(/^(?d+)?(?\d+)(?:t(?\d*))?/); 331 | if (match) { 332 | var base = Number(match.groups.base); 333 | // 1 or any power of two: 334 | var isValidBase = base === 1 || ((base & (base - 1)) === 0); 335 | if (isValidBase) { 336 | // how much faster or slower is this note compared to a quarter? 337 | var ratio = base / 4; 338 | var durationInQuarters = 1 / ratio; 339 | var _a = match.groups, dotted = _a.dotted, tuplet = _a.tuplet; 340 | if (dotted) { 341 | var thisManyDots = dotted.length; 342 | var divisor = Math.pow(2, thisManyDots); 343 | durationInQuarters = durationInQuarters + (durationInQuarters * ((divisor - 1) / divisor)); 344 | } 345 | if (typeof tuplet === 'string') { 346 | var fitInto = durationInQuarters * 2; 347 | // default to triplet: 348 | var thisManyNotes = Number(tuplet || '3'); 349 | durationInQuarters = fitInto / thisManyNotes; 350 | } 351 | return durationInQuarters; 352 | } 353 | } 354 | throw new Error(duration + ' is not a valid duration.'); 355 | }; 356 | return Utils; 357 | }()); 358 | 359 | /** 360 | * Holds all data for a "controller change" MIDI event 361 | * @param {object} fields {controllerNumber: integer, controllerValue: integer, delta: integer} 362 | * @return {ControllerChangeEvent} 363 | */ 364 | var ControllerChangeEvent = /** @class */ (function () { 365 | function ControllerChangeEvent(fields) { 366 | this.channel = fields.channel - 1 || 0; 367 | this.controllerValue = fields.controllerValue; 368 | this.controllerNumber = fields.controllerNumber; 369 | this.delta = fields.delta || 0x00; 370 | this.name = 'ControllerChangeEvent'; 371 | this.status = 0xB0; 372 | this.data = Utils.numberToVariableLength(fields.delta).concat(this.status | this.channel, this.controllerNumber, this.controllerValue); 373 | } 374 | return ControllerChangeEvent; 375 | }()); 376 | 377 | /** 378 | * Object representation of a tempo meta event. 379 | * @param {object} fields {text: string, delta: integer} 380 | * @return {CopyrightEvent} 381 | */ 382 | var CopyrightEvent = /** @class */ (function () { 383 | function CopyrightEvent(fields) { 384 | this.delta = fields.delta || 0x00; 385 | this.name = 'CopyrightEvent'; 386 | this.text = fields.text; 387 | this.type = 0x02; 388 | var textBytes = Utils.stringToBytes(this.text); 389 | // Start with zero time delta 390 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 391 | textBytes); 392 | } 393 | return CopyrightEvent; 394 | }()); 395 | 396 | /** 397 | * Object representation of a cue point meta event. 398 | * @param {object} fields {text: string, delta: integer} 399 | * @return {CuePointEvent} 400 | */ 401 | var CuePointEvent = /** @class */ (function () { 402 | function CuePointEvent(fields) { 403 | this.delta = fields.delta || 0x00; 404 | this.name = 'CuePointEvent'; 405 | this.text = fields.text; 406 | this.type = 0x07; 407 | var textBytes = Utils.stringToBytes(this.text); 408 | // Start with zero time delta 409 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 410 | textBytes); 411 | } 412 | return CuePointEvent; 413 | }()); 414 | 415 | /** 416 | * Object representation of a end track meta event. 417 | * @param {object} fields {delta: integer} 418 | * @return {EndTrackEvent} 419 | */ 420 | var EndTrackEvent = /** @class */ (function () { 421 | function EndTrackEvent(fields) { 422 | this.delta = (fields === null || fields === void 0 ? void 0 : fields.delta) || 0x00; 423 | this.name = 'EndTrackEvent'; 424 | this.type = [0x2F, 0x00]; 425 | // Start with zero time delta 426 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type); 427 | } 428 | return EndTrackEvent; 429 | }()); 430 | 431 | /** 432 | * Object representation of an instrument name meta event. 433 | * @param {object} fields {text: string, delta: integer} 434 | * @return {InstrumentNameEvent} 435 | */ 436 | var InstrumentNameEvent = /** @class */ (function () { 437 | function InstrumentNameEvent(fields) { 438 | this.delta = fields.delta || 0x00; 439 | this.name = 'InstrumentNameEvent'; 440 | this.text = fields.text; 441 | this.type = 0x04; 442 | var textBytes = Utils.stringToBytes(this.text); 443 | // Start with zero time delta 444 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 445 | textBytes); 446 | } 447 | return InstrumentNameEvent; 448 | }()); 449 | 450 | /** 451 | * Object representation of a key signature meta event. 452 | * @return {KeySignatureEvent} 453 | */ 454 | var KeySignatureEvent = /** @class */ (function () { 455 | function KeySignatureEvent(sf, mi) { 456 | this.name = 'KeySignatureEvent'; 457 | this.type = 0x59; 458 | var mode = mi || 0; 459 | sf = sf || 0; 460 | // Function called with string notation 461 | if (typeof mi === 'undefined') { 462 | var fifths = [ 463 | ['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#'], 464 | ['ab', 'eb', 'bb', 'f', 'c', 'g', 'd', 'a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#'] 465 | ]; 466 | var _sflen = sf.length; 467 | var note = sf || 'C'; 468 | if (sf[0] === sf[0].toLowerCase()) 469 | mode = 1; 470 | if (_sflen > 1) { 471 | switch (sf.charAt(_sflen - 1)) { 472 | case 'm': 473 | mode = 1; 474 | note = sf.charAt(0).toLowerCase(); 475 | note = note.concat(sf.substring(1, _sflen - 1)); 476 | break; 477 | case '-': 478 | mode = 1; 479 | note = sf.charAt(0).toLowerCase(); 480 | note = note.concat(sf.substring(1, _sflen - 1)); 481 | break; 482 | case 'M': 483 | mode = 0; 484 | note = sf.charAt(0).toUpperCase(); 485 | note = note.concat(sf.substring(1, _sflen - 1)); 486 | break; 487 | case '+': 488 | mode = 0; 489 | note = sf.charAt(0).toUpperCase(); 490 | note = note.concat(sf.substring(1, _sflen - 1)); 491 | break; 492 | } 493 | } 494 | var fifthindex = fifths[mode].indexOf(note); 495 | sf = fifthindex === -1 ? 0 : fifthindex - 7; 496 | } 497 | // Start with zero time delta 498 | this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x02], // Size 499 | Utils.numberToBytes(sf, 1), // Number of sharp or flats ( < 0 flat; > 0 sharp) 500 | Utils.numberToBytes(mode, 1)); 501 | } 502 | return KeySignatureEvent; 503 | }()); 504 | 505 | /** 506 | * Object representation of a lyric meta event. 507 | * @param {object} fields {text: string, delta: integer} 508 | * @return {LyricEvent} 509 | */ 510 | var LyricEvent = /** @class */ (function () { 511 | function LyricEvent(fields) { 512 | this.delta = fields.delta || 0x00; 513 | this.name = 'LyricEvent'; 514 | this.text = fields.text; 515 | this.type = 0x05; 516 | var textBytes = Utils.stringToBytes(this.text); 517 | // Start with zero time delta 518 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 519 | textBytes); 520 | } 521 | return LyricEvent; 522 | }()); 523 | 524 | /** 525 | * Object representation of a marker meta event. 526 | * @param {object} fields {text: string, delta: integer} 527 | * @return {MarkerEvent} 528 | */ 529 | var MarkerEvent = /** @class */ (function () { 530 | function MarkerEvent(fields) { 531 | this.delta = fields.delta || 0x00; 532 | this.name = 'MarkerEvent'; 533 | this.text = fields.text; 534 | this.type = 0x06; 535 | var textBytes = Utils.stringToBytes(this.text); 536 | // Start with zero time delta 537 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 538 | textBytes); 539 | } 540 | return MarkerEvent; 541 | }()); 542 | 543 | /** 544 | * Holds all data for a "note on" MIDI event 545 | * @param {object} fields {data: []} 546 | * @return {NoteOnEvent} 547 | */ 548 | var NoteOnEvent = /** @class */ (function () { 549 | function NoteOnEvent(fields) { 550 | this.name = 'NoteOnEvent'; 551 | this.channel = fields.channel || 1; 552 | this.pitch = fields.pitch; 553 | this.wait = fields.wait || 0; 554 | this.velocity = fields.velocity || 50; 555 | this.tick = fields.tick || null; 556 | this.delta = null; 557 | this.data = fields.data; 558 | this.status = 0x90; 559 | } 560 | /** 561 | * Builds int array for this event. 562 | * @param {Track} track - parent track 563 | * @return {NoteOnEvent} 564 | */ 565 | NoteOnEvent.prototype.buildData = function (track, precisionDelta, options) { 566 | if (options === void 0) { options = {}; } 567 | this.data = []; 568 | // Explicitly defined startTick event 569 | if (this.tick) { 570 | this.tick = Utils.getRoundedIfClose(this.tick); 571 | // If this is the first event in the track then use event's starting tick as delta. 572 | if (track.tickPointer == 0) { 573 | this.delta = this.tick; 574 | } 575 | } 576 | else { 577 | this.delta = Utils.getTickDuration(this.wait); 578 | this.tick = Utils.getRoundedIfClose(track.tickPointer + this.delta); 579 | } 580 | this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta); 581 | this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection) 582 | .concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity)); 583 | return this; 584 | }; 585 | return NoteOnEvent; 586 | }()); 587 | 588 | /** 589 | * Holds all data for a "note off" MIDI event 590 | * @param {object} fields {data: []} 591 | * @return {NoteOffEvent} 592 | */ 593 | var NoteOffEvent = /** @class */ (function () { 594 | function NoteOffEvent(fields) { 595 | this.name = 'NoteOffEvent'; 596 | this.channel = fields.channel || 1; 597 | this.pitch = fields.pitch; 598 | this.velocity = fields.velocity || 50; 599 | this.tick = fields.tick || null; 600 | this.data = fields.data; 601 | this.delta = fields.delta || Utils.getTickDuration(fields.duration); 602 | this.status = 0x80; 603 | } 604 | /** 605 | * Builds int array for this event. 606 | * @param {Track} track - parent track 607 | * @return {NoteOffEvent} 608 | */ 609 | NoteOffEvent.prototype.buildData = function (track, precisionDelta, options) { 610 | if (options === void 0) { options = {}; } 611 | if (this.tick === null) { 612 | this.tick = Utils.getRoundedIfClose(this.delta + track.tickPointer); 613 | } 614 | this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta); 615 | this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection) 616 | .concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity)); 617 | return this; 618 | }; 619 | return NoteOffEvent; 620 | }()); 621 | 622 | /** 623 | * Wrapper for noteOnEvent/noteOffEvent objects that builds both events. 624 | * @param {object} fields - {pitch: '[C4]', duration: '4', wait: '4', velocity: 1-100} 625 | * @return {NoteEvent} 626 | */ 627 | var NoteEvent = /** @class */ (function () { 628 | function NoteEvent(fields) { 629 | this.data = []; 630 | this.name = 'NoteEvent'; 631 | this.pitch = Utils.toArray(fields.pitch); 632 | this.channel = fields.channel || 1; 633 | this.duration = fields.duration || '4'; 634 | this.grace = fields.grace; 635 | this.repeat = fields.repeat || 1; 636 | this.sequential = fields.sequential || false; 637 | this.tick = fields.startTick || fields.tick || null; 638 | this.velocity = fields.velocity || 50; 639 | this.wait = fields.wait || 0; 640 | this.tickDuration = Utils.getTickDuration(this.duration); 641 | this.restDuration = Utils.getTickDuration(this.wait); 642 | this.events = []; // Hold actual NoteOn/NoteOff events 643 | } 644 | /** 645 | * Builds int array for this event. 646 | * @return {NoteEvent} 647 | */ 648 | NoteEvent.prototype.buildData = function () { 649 | var _this = this; 650 | // Reset data array 651 | this.data = []; 652 | // Apply grace note(s) and subtract ticks (currently 1 tick per grace note) from tickDuration so net value is the same 653 | if (this.grace) { 654 | var graceDuration_1 = 1; 655 | this.grace = Utils.toArray(this.grace); 656 | this.grace.forEach(function () { 657 | var noteEvent = new NoteEvent({ pitch: _this.grace, duration: 'T' + graceDuration_1 }); 658 | _this.data = _this.data.concat(noteEvent.data); 659 | }); 660 | } 661 | // fields.pitch could be an array of pitches. 662 | // If so create note events for each and apply the same duration. 663 | // By default this is a chord if it's an array of notes that requires one NoteOnEvent. 664 | // If this.sequential === true then it's a sequential string of notes that requires separate NoteOnEvents. 665 | if (!this.sequential) { 666 | // Handle repeat 667 | for (var j = 0; j < this.repeat; j++) { 668 | // Note on 669 | this.pitch.forEach(function (p, i) { 670 | var noteOnNew; 671 | if (i == 0) { 672 | noteOnNew = new NoteOnEvent({ 673 | channel: _this.channel, 674 | wait: _this.wait, 675 | delta: Utils.getTickDuration(_this.wait), 676 | velocity: _this.velocity, 677 | pitch: p, 678 | tick: _this.tick, 679 | }); 680 | } 681 | else { 682 | // Running status (can ommit the note on status) 683 | //noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]}); 684 | noteOnNew = new NoteOnEvent({ 685 | channel: _this.channel, 686 | wait: 0, 687 | delta: 0, 688 | velocity: _this.velocity, 689 | pitch: p, 690 | tick: _this.tick, 691 | }); 692 | } 693 | _this.events.push(noteOnNew); 694 | }); 695 | // Note off 696 | this.pitch.forEach(function (p, i) { 697 | var noteOffNew; 698 | if (i == 0) { 699 | //noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))}); 700 | noteOffNew = new NoteOffEvent({ 701 | channel: _this.channel, 702 | duration: _this.duration, 703 | velocity: _this.velocity, 704 | pitch: p, 705 | tick: _this.tick !== null ? Utils.getTickDuration(_this.duration) + _this.tick : null, 706 | }); 707 | } 708 | else { 709 | // Running status (can omit the note off status) 710 | //noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]}); 711 | noteOffNew = new NoteOffEvent({ 712 | channel: _this.channel, 713 | duration: 0, 714 | velocity: _this.velocity, 715 | pitch: p, 716 | tick: _this.tick !== null ? Utils.getTickDuration(_this.duration) + _this.tick : null, 717 | }); 718 | } 719 | _this.events.push(noteOffNew); 720 | }); 721 | } 722 | } 723 | else { 724 | // Handle repeat 725 | for (var j = 0; j < this.repeat; j++) { 726 | this.pitch.forEach(function (p, i) { 727 | var noteOnNew = new NoteOnEvent({ 728 | channel: _this.channel, 729 | wait: (i > 0 ? 0 : _this.wait), 730 | delta: (i > 0 ? 0 : Utils.getTickDuration(_this.wait)), 731 | velocity: _this.velocity, 732 | pitch: p, 733 | tick: _this.tick, 734 | }); 735 | var noteOffNew = new NoteOffEvent({ 736 | channel: _this.channel, 737 | duration: _this.duration, 738 | velocity: _this.velocity, 739 | pitch: p, 740 | }); 741 | _this.events.push(noteOnNew, noteOffNew); 742 | }); 743 | } 744 | } 745 | return this; 746 | }; 747 | return NoteEvent; 748 | }()); 749 | 750 | /** 751 | * Holds all data for a "Pitch Bend" MIDI event 752 | * [ -1.0, 0, 1.0 ] -> [ 0, 8192, 16383] 753 | * @param {object} fields { bend : float, channel : int, delta: int } 754 | * @return {PitchBendEvent} 755 | */ 756 | var PitchBendEvent = /** @class */ (function () { 757 | function PitchBendEvent(fields) { 758 | this.channel = fields.channel || 0; 759 | this.delta = fields.delta || 0x00; 760 | this.name = 'PitchBendEvent'; 761 | this.status = 0xE0; 762 | var bend14 = this.scale14bits(fields.bend); 763 | var lsbValue = bend14 & 0x7f; 764 | var msbValue = (bend14 >> 7) & 0x7f; 765 | this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, lsbValue, msbValue); 766 | } 767 | PitchBendEvent.prototype.scale14bits = function (zeroOne) { 768 | if (zeroOne <= 0) { 769 | return Math.floor(16384 * (zeroOne + 1) / 2); 770 | } 771 | return Math.floor(16383 * (zeroOne + 1) / 2); 772 | }; 773 | return PitchBendEvent; 774 | }()); 775 | 776 | /** 777 | * Holds all data for a "program change" MIDI event 778 | * @param {object} fields {instrument: integer, delta: integer} 779 | * @return {ProgramChangeEvent} 780 | */ 781 | var ProgramChangeEvent = /** @class */ (function () { 782 | function ProgramChangeEvent(fields) { 783 | this.channel = fields.channel || 0; 784 | this.delta = fields.delta || 0x00; 785 | this.instrument = fields.instrument; 786 | this.status = 0xC0; 787 | this.name = 'ProgramChangeEvent'; 788 | // delta time defaults to 0. 789 | this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, this.instrument); 790 | } 791 | return ProgramChangeEvent; 792 | }()); 793 | 794 | /** 795 | * Object representation of a tempo meta event. 796 | * @param {object} fields {bpm: integer, delta: integer} 797 | * @return {TempoEvent} 798 | */ 799 | var TempoEvent = /** @class */ (function () { 800 | function TempoEvent(fields) { 801 | this.bpm = fields.bpm; 802 | this.delta = fields.delta || 0x00; 803 | this.tick = fields.tick; 804 | this.name = 'TempoEvent'; 805 | this.type = 0x51; 806 | var tempo = Math.round(60000000 / this.bpm); 807 | // Start with zero time delta 808 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, [0x03], // Size 809 | Utils.numberToBytes(tempo, 3)); 810 | } 811 | return TempoEvent; 812 | }()); 813 | 814 | /** 815 | * Object representation of a tempo meta event. 816 | * @param {object} fields {text: string, delta: integer} 817 | * @return {TextEvent} 818 | */ 819 | var TextEvent = /** @class */ (function () { 820 | function TextEvent(fields) { 821 | this.delta = fields.delta || 0x00; 822 | this.text = fields.text; 823 | this.name = 'TextEvent'; 824 | this.type = 0x01; 825 | var textBytes = Utils.stringToBytes(this.text); 826 | // Start with zero time delta 827 | this.data = Utils.numberToVariableLength(fields.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 828 | textBytes); 829 | } 830 | return TextEvent; 831 | }()); 832 | 833 | /** 834 | * Object representation of a time signature meta event. 835 | * @return {TimeSignatureEvent} 836 | */ 837 | var TimeSignatureEvent = /** @class */ (function () { 838 | function TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock) { 839 | this.name = 'TimeSignatureEvent'; 840 | this.type = 0x58; 841 | // Start with zero time delta 842 | this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x04], // Size 843 | Utils.numberToBytes(numerator, 1), // Numerator, 1 bytes 844 | Utils.numberToBytes(Math.log2(denominator), 1), // Denominator is expressed as pow of 2, 1 bytes 845 | Utils.numberToBytes(midiclockspertick || 24, 1), // MIDI Clocks per tick, 1 bytes 846 | Utils.numberToBytes(notespermidiclock || 8, 1)); 847 | } 848 | return TimeSignatureEvent; 849 | }()); 850 | 851 | /** 852 | * Object representation of a tempo meta event. 853 | * @param {object} fields {text: string, delta: integer} 854 | * @return {TrackNameEvent} 855 | */ 856 | var TrackNameEvent = /** @class */ (function () { 857 | function TrackNameEvent(fields) { 858 | this.delta = fields.delta || 0x00; 859 | this.name = 'TrackNameEvent'; 860 | this.text = fields.text; 861 | this.type = 0x03; 862 | var textBytes = Utils.stringToBytes(this.text); 863 | // Start with zero time delta 864 | this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size 865 | textBytes); 866 | } 867 | return TrackNameEvent; 868 | }()); 869 | 870 | /** 871 | * Holds all data for a track. 872 | * @param {object} fields {type: number, data: array, size: array, events: array} 873 | * @return {Track} 874 | */ 875 | var Track = /** @class */ (function () { 876 | function Track() { 877 | this.type = Constants.TRACK_CHUNK_TYPE; 878 | this.data = []; 879 | this.size = []; 880 | this.events = []; 881 | this.explicitTickEvents = []; 882 | // If there are any events with an explicit tick defined then we will create a "sub" track for those 883 | // and merge them in and the end. 884 | this.tickPointer = 0; // Each time an event is added this will increase 885 | } 886 | /** 887 | * Adds any event type to the track. 888 | * Events without a specific startTick property are assumed to be added in order of how they should output. 889 | * Events with a specific startTick property are set aside for now will be merged in during build process. 890 | * 891 | * TODO: Don't put startTick events in their own array. Just lump everything together and sort it out during buildData(); 892 | * @param {(NoteEvent|ProgramChangeEvent)} events - Event object or array of Event objects. 893 | * @param {Function} mapFunction - Callback which can be used to apply specific properties to all events. 894 | * @return {Track} 895 | */ 896 | Track.prototype.addEvent = function (events, mapFunction) { 897 | var _this = this; 898 | Utils.toArray(events).forEach(function (event, i) { 899 | if (event instanceof NoteEvent) { 900 | // Handle map function if provided 901 | if (typeof mapFunction === 'function') { 902 | var properties = mapFunction(i, event); 903 | if (typeof properties === 'object') { 904 | Object.assign(event, properties); 905 | } 906 | } 907 | // If this note event has an explicit startTick then we need to set aside for now 908 | if (event.tick !== null) { 909 | _this.explicitTickEvents.push(event); 910 | } 911 | else { 912 | // Push each on/off event to track's event stack 913 | event.buildData().events.forEach(function (e) { return _this.events.push(e); }); 914 | } 915 | } 916 | else { 917 | _this.events.push(event); 918 | } 919 | }); 920 | return this; 921 | }; 922 | /** 923 | * Builds int array of all events. 924 | * @param {object} options 925 | * @return {Track} 926 | */ 927 | Track.prototype.buildData = function (options) { 928 | var _this = this; 929 | if (options === void 0) { options = {}; } 930 | // Reset 931 | this.data = []; 932 | this.size = []; 933 | this.tickPointer = 0; 934 | var precisionLoss = 0; 935 | this.events.forEach(function (event) { 936 | // Build event & add to total tick duration 937 | if (event instanceof NoteOnEvent || event instanceof NoteOffEvent) { 938 | var built = event.buildData(_this, precisionLoss, options); 939 | precisionLoss = Utils.getPrecisionLoss(event.deltaWithPrecisionCorrection || 0); 940 | _this.data = _this.data.concat(built.data); 941 | _this.tickPointer = Utils.getRoundedIfClose(event.tick); 942 | } 943 | else if (event instanceof TempoEvent) { 944 | _this.tickPointer = Utils.getRoundedIfClose(event.tick); 945 | _this.data = _this.data.concat(event.data); 946 | } 947 | else { 948 | _this.data = _this.data.concat(event.data); 949 | } 950 | }); 951 | this.mergeExplicitTickEvents(); 952 | // If the last event isn't EndTrackEvent, then tack it onto the data. 953 | if (!this.events.length || !(this.events[this.events.length - 1] instanceof EndTrackEvent)) { 954 | this.data = this.data.concat((new EndTrackEvent).data); 955 | } 956 | this.size = Utils.numberToBytes(this.data.length, 4); // 4 bytes long 957 | return this; 958 | }; 959 | Track.prototype.mergeExplicitTickEvents = function () { 960 | var _this = this; 961 | if (!this.explicitTickEvents.length) 962 | return; 963 | // First sort asc list of events by startTick 964 | this.explicitTickEvents.sort(function (a, b) { return a.tick - b.tick; }); 965 | // Now this.explicitTickEvents is in correct order, and so is this.events naturally. 966 | // For each explicit tick event, splice it into the main list of events and 967 | // adjust the delta on the following events so they still play normally. 968 | this.explicitTickEvents.forEach(function (noteEvent) { 969 | // Convert NoteEvent to it's respective NoteOn/NoteOff events 970 | // Note that as we splice in events the delta for the NoteOff ones will 971 | // Need to change based on what comes before them after the splice. 972 | noteEvent.buildData().events.forEach(function (e) { return e.buildData(_this); }); 973 | // Merge each event individually into this track's event list. 974 | noteEvent.events.forEach(function (event) { return _this.mergeSingleEvent(event); }); 975 | }); 976 | // Hacky way to rebuild track with newly spliced events. Need better solution. 977 | this.explicitTickEvents = []; 978 | this.buildData(); 979 | }; 980 | /** 981 | * Merges another track's events with this track. 982 | * @param {Track} track 983 | * @return {Track} 984 | */ 985 | Track.prototype.mergeTrack = function (track) { 986 | var _this = this; 987 | // First build this track to populate each event's tick property 988 | this.buildData(); 989 | // Then build track to be merged so that tick property is populated on all events & merge each event. 990 | track.buildData().events.forEach(function (event) { return _this.mergeSingleEvent(event); }); 991 | return this; 992 | }; 993 | /** 994 | * Merges a single event into this track's list of events based on event.tick property. 995 | * @param {AbstractEvent} - event 996 | * @return {Track} 997 | */ 998 | Track.prototype.mergeSingleEvent = function (event) { 999 | // There are no events yet, so just add it in. 1000 | if (!this.events.length) { 1001 | this.addEvent(event); 1002 | return; 1003 | } 1004 | // Find index of existing event we need to follow with 1005 | var lastEventIndex; 1006 | for (var i = 0; i < this.events.length; i++) { 1007 | if (this.events[i].tick > event.tick) 1008 | break; 1009 | lastEventIndex = i; 1010 | } 1011 | var splicedEventIndex = lastEventIndex + 1; 1012 | // Need to adjust the delta of this event to ensure it falls on the correct tick. 1013 | event.delta = event.tick - this.events[lastEventIndex].tick; 1014 | // Splice this event at lastEventIndex + 1 1015 | this.events.splice(splicedEventIndex, 0, event); 1016 | // Now adjust delta of all following events 1017 | for (var i = splicedEventIndex + 1; i < this.events.length; i++) { 1018 | // Since each existing event should have a tick value at this point we just need to 1019 | // adjust delta to that the event still falls on the correct tick. 1020 | this.events[i].delta = this.events[i].tick - this.events[i - 1].tick; 1021 | } 1022 | }; 1023 | /** 1024 | * Removes all events matching specified type. 1025 | * @param {string} eventName - Event type 1026 | * @return {Track} 1027 | */ 1028 | Track.prototype.removeEventsByName = function (eventName) { 1029 | var _this = this; 1030 | this.events.forEach(function (event, index) { 1031 | if (event.name === eventName) { 1032 | _this.events.splice(index, 1); 1033 | } 1034 | }); 1035 | return this; 1036 | }; 1037 | /** 1038 | * Sets tempo of the MIDI file. 1039 | * @param {number} bpm - Tempo in beats per minute. 1040 | * @param {number} tick - Start tick. 1041 | * @return {Track} 1042 | */ 1043 | Track.prototype.setTempo = function (bpm, tick) { 1044 | if (tick === void 0) { tick = 0; } 1045 | return this.addEvent(new TempoEvent({ bpm: bpm, tick: tick })); 1046 | }; 1047 | /** 1048 | * Sets time signature. 1049 | * @param {number} numerator - Top number of the time signature. 1050 | * @param {number} denominator - Bottom number of the time signature. 1051 | * @param {number} midiclockspertick - Defaults to 24. 1052 | * @param {number} notespermidiclock - Defaults to 8. 1053 | * @return {Track} 1054 | */ 1055 | Track.prototype.setTimeSignature = function (numerator, denominator, midiclockspertick, notespermidiclock) { 1056 | return this.addEvent(new TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock)); 1057 | }; 1058 | /** 1059 | * Sets key signature. 1060 | * @param {*} sf - 1061 | * @param {*} mi - 1062 | * @return {Track} 1063 | */ 1064 | Track.prototype.setKeySignature = function (sf, mi) { 1065 | return this.addEvent(new KeySignatureEvent(sf, mi)); 1066 | }; 1067 | /** 1068 | * Adds text to MIDI file. 1069 | * @param {string} text - Text to add. 1070 | * @return {Track} 1071 | */ 1072 | Track.prototype.addText = function (text) { 1073 | return this.addEvent(new TextEvent({ text: text })); 1074 | }; 1075 | /** 1076 | * Adds copyright to MIDI file. 1077 | * @param {string} text - Text of copyright line. 1078 | * @return {Track} 1079 | */ 1080 | Track.prototype.addCopyright = function (text) { 1081 | return this.addEvent(new CopyrightEvent({ text: text })); 1082 | }; 1083 | /** 1084 | * Adds Sequence/Track Name. 1085 | * @param {string} text - Text of track name. 1086 | * @return {Track} 1087 | */ 1088 | Track.prototype.addTrackName = function (text) { 1089 | return this.addEvent(new TrackNameEvent({ text: text })); 1090 | }; 1091 | /** 1092 | * Sets instrument name of track. 1093 | * @param {string} text - Name of instrument. 1094 | * @return {Track} 1095 | */ 1096 | Track.prototype.addInstrumentName = function (text) { 1097 | return this.addEvent(new InstrumentNameEvent({ text: text })); 1098 | }; 1099 | /** 1100 | * Adds marker to MIDI file. 1101 | * @param {string} text - Marker text. 1102 | * @return {Track} 1103 | */ 1104 | Track.prototype.addMarker = function (text) { 1105 | return this.addEvent(new MarkerEvent({ text: text })); 1106 | }; 1107 | /** 1108 | * Adds cue point to MIDI file. 1109 | * @param {string} text - Text of cue point. 1110 | * @return {Track} 1111 | */ 1112 | Track.prototype.addCuePoint = function (text) { 1113 | return this.addEvent(new CuePointEvent({ text: text })); 1114 | }; 1115 | /** 1116 | * Adds lyric to MIDI file. 1117 | * @param {string} text - Lyric text to add. 1118 | * @return {Track} 1119 | */ 1120 | Track.prototype.addLyric = function (text) { 1121 | return this.addEvent(new LyricEvent({ text: text })); 1122 | }; 1123 | /** 1124 | * Channel mode messages 1125 | * @return {Track} 1126 | */ 1127 | Track.prototype.polyModeOn = function () { 1128 | var event = new NoteOnEvent({ data: [0x00, 0xB0, 0x7E, 0x00] }); 1129 | return this.addEvent(event); 1130 | }; 1131 | /** 1132 | * Sets a pitch bend. 1133 | * @param {float} bend - Bend value ranging [-1,1], zero meaning no bend. 1134 | * @return {Track} 1135 | */ 1136 | Track.prototype.setPitchBend = function (bend) { 1137 | return this.addEvent(new PitchBendEvent({ bend: bend })); 1138 | }; 1139 | /** 1140 | * Adds a controller change event 1141 | * @param {number} number - Control number. 1142 | * @param {number} value - Control value. 1143 | * @param {number} channel - Channel to send controller change event on (1-based). 1144 | * @param {number} delta - Track tick offset for cc event. 1145 | * @return {Track} 1146 | */ 1147 | Track.prototype.controllerChange = function (number, value, channel, delta) { 1148 | return this.addEvent(new ControllerChangeEvent({ controllerNumber: number, controllerValue: value, channel: channel, delta: delta })); 1149 | }; 1150 | return Track; 1151 | }()); 1152 | 1153 | var VexFlow = /** @class */ (function () { 1154 | function VexFlow() { 1155 | } 1156 | /** 1157 | * Support for converting VexFlow voice into MidiWriterJS track 1158 | * @return MidiWriter.Track object 1159 | */ 1160 | VexFlow.prototype.trackFromVoice = function (voice, options) { 1161 | var _this = this; 1162 | if (options === void 0) { options = { addRenderedAccidentals: false }; } 1163 | var track = new Track; 1164 | var wait = []; 1165 | voice.tickables.forEach(function (tickable) { 1166 | if (tickable.noteType === 'n') { 1167 | track.addEvent(new NoteEvent({ 1168 | pitch: tickable.keys.map(function (pitch, index) { return _this.convertPitch(pitch, index, tickable, options.addRenderedAccidentals); }), 1169 | duration: _this.convertDuration(tickable), 1170 | wait: wait 1171 | })); 1172 | // reset wait 1173 | wait = []; 1174 | } 1175 | else if (tickable.noteType === 'r') { 1176 | // move on to the next tickable and add this to the stack 1177 | // of the `wait` property for the next note event 1178 | wait.push(_this.convertDuration(tickable)); 1179 | } 1180 | }); 1181 | // There may be outstanding rests at the end of the track, 1182 | // pad with a ghost note (zero duration and velocity), just to capture the wait. 1183 | if (wait.length > 0) { 1184 | track.addEvent(new NoteEvent({ pitch: '[c4]', duration: '0', wait: wait, velocity: '0' })); 1185 | } 1186 | return track; 1187 | }; 1188 | /** 1189 | * Converts VexFlow pitch syntax to MidiWriterJS syntax 1190 | * @param pitch string 1191 | * @param index pitch index 1192 | * @param note struct from Vexflow 1193 | * @param addRenderedAccidentals adds Vexflow rendered accidentals 1194 | */ 1195 | VexFlow.prototype.convertPitch = function (pitch, index, note, addRenderedAccidentals) { 1196 | var _a; 1197 | if (addRenderedAccidentals === void 0) { addRenderedAccidentals = false; } 1198 | // Splits note name from octave 1199 | var pitchParts = pitch.split('/'); 1200 | // Retrieves accidentals from pitch 1201 | // Removes natural accidentals since they are not accepted in Tonal Midi 1202 | var accidentals = pitchParts[0].substring(1).replace('n', ''); 1203 | if (addRenderedAccidentals) { 1204 | (_a = note.getAccidentals()) === null || _a === void 0 ? void 0 : _a.forEach(function (accidental) { 1205 | if (accidental.index === index) { 1206 | if (accidental.type === 'n') { 1207 | accidentals = ''; 1208 | } 1209 | else { 1210 | accidentals += accidental.type; 1211 | } 1212 | } 1213 | }); 1214 | } 1215 | return pitchParts[0][0] + accidentals + pitchParts[1]; 1216 | }; 1217 | /** 1218 | * Converts VexFlow duration syntax to MidiWriterJS syntax 1219 | * @param note struct from VexFlow 1220 | */ 1221 | VexFlow.prototype.convertDuration = function (note) { 1222 | return 'd'.repeat(note.dots) + this.convertBaseDuration(note.duration) + (note.tuplet ? 't' + note.tuplet.num_notes : ''); 1223 | }; 1224 | /** 1225 | * Converts VexFlow base duration syntax to MidiWriterJS syntax 1226 | * @param duration Vexflow duration 1227 | * @returns MidiWriterJS duration 1228 | */ 1229 | VexFlow.prototype.convertBaseDuration = function (duration) { 1230 | switch (duration) { 1231 | case 'w': 1232 | return '1'; 1233 | case 'h': 1234 | return '2'; 1235 | case 'q': 1236 | return '4'; 1237 | default: 1238 | return duration; 1239 | } 1240 | }; 1241 | return VexFlow; 1242 | }()); 1243 | 1244 | /** 1245 | * Object representation of a header chunk section of a MIDI file. 1246 | * @param {number} numberOfTracks - Number of tracks 1247 | * @return {Header} 1248 | */ 1249 | var Header = /** @class */ (function () { 1250 | function Header(numberOfTracks) { 1251 | this.type = Constants.HEADER_CHUNK_TYPE; 1252 | var trackType = numberOfTracks > 1 ? Constants.HEADER_CHUNK_FORMAT1 : Constants.HEADER_CHUNK_FORMAT0; 1253 | this.data = trackType.concat(Utils.numberToBytes(numberOfTracks, 2), // two bytes long, 1254 | Constants.HEADER_CHUNK_DIVISION); 1255 | this.size = [0, 0, 0, this.data.length]; 1256 | } 1257 | return Header; 1258 | }()); 1259 | 1260 | /** 1261 | * Object that puts together tracks and provides methods for file output. 1262 | * @param {array|Track} tracks - A single {Track} object or an array of {Track} objects. 1263 | * @param {object} options - {middleC: 'C4'} 1264 | * @return {Writer} 1265 | */ 1266 | var Writer = /** @class */ (function () { 1267 | function Writer(tracks, options) { 1268 | if (options === void 0) { options = {}; } 1269 | // Ensure tracks is an array 1270 | this.tracks = Utils.toArray(tracks); 1271 | this.options = options; 1272 | } 1273 | /** 1274 | * Builds array of data from chunkschunks. 1275 | * @return {array} 1276 | */ 1277 | Writer.prototype.buildData = function () { 1278 | var _this = this; 1279 | var data = []; 1280 | data.push(new Header(this.tracks.length)); 1281 | // For each track add final end of track event and build data 1282 | this.tracks.forEach(function (track) { 1283 | data.push(track.buildData(_this.options)); 1284 | }); 1285 | return data; 1286 | }; 1287 | /** 1288 | * Builds the file into a Uint8Array 1289 | * @return {Uint8Array} 1290 | */ 1291 | Writer.prototype.buildFile = function () { 1292 | var build = []; 1293 | // Data consists of chunks which consists of data 1294 | this.buildData().forEach(function (d) { return build = build.concat(d.type, d.size, d.data); }); 1295 | return new Uint8Array(build); 1296 | }; 1297 | /** 1298 | * Convert file buffer to a base64 string. Different methods depending on if browser or node. 1299 | * @return {string} 1300 | */ 1301 | Writer.prototype.base64 = function () { 1302 | if (typeof btoa === 'function') { 1303 | var binary = ''; 1304 | var bytes = this.buildFile(); 1305 | var len = bytes.byteLength; 1306 | for (var i = 0; i < len; i++) { 1307 | binary += String.fromCharCode(bytes[i]); 1308 | } 1309 | return btoa(binary); 1310 | } 1311 | return Buffer.from(this.buildFile()).toString('base64'); 1312 | }; 1313 | /** 1314 | * Get the data URI. 1315 | * @return {string} 1316 | */ 1317 | Writer.prototype.dataUri = function () { 1318 | return 'data:audio/midi;base64,' + this.base64(); 1319 | }; 1320 | /** 1321 | * Set option on instantiated Writer. 1322 | * @param {string} key 1323 | * @param {any} value 1324 | * @return {Writer} 1325 | */ 1326 | Writer.prototype.setOption = function (key, value) { 1327 | this.options[key] = value; 1328 | return this; 1329 | }; 1330 | /** 1331 | * Output to stdout 1332 | * @return {string} 1333 | */ 1334 | Writer.prototype.stdout = function () { 1335 | return process.stdout.write(Buffer.from(this.buildFile())); 1336 | }; 1337 | return Writer; 1338 | }()); 1339 | 1340 | var main = { 1341 | Constants: Constants, 1342 | ControllerChangeEvent: ControllerChangeEvent, 1343 | CopyrightEvent: CopyrightEvent, 1344 | CuePointEvent: CuePointEvent, 1345 | EndTrackEvent: EndTrackEvent, 1346 | InstrumentNameEvent: InstrumentNameEvent, 1347 | KeySignatureEvent: KeySignatureEvent, 1348 | LyricEvent: LyricEvent, 1349 | MarkerEvent: MarkerEvent, 1350 | NoteOnEvent: NoteOnEvent, 1351 | NoteOffEvent: NoteOffEvent, 1352 | NoteEvent: NoteEvent, 1353 | PitchBendEvent: PitchBendEvent, 1354 | ProgramChangeEvent: ProgramChangeEvent, 1355 | TempoEvent: TempoEvent, 1356 | TextEvent: TextEvent, 1357 | TimeSignatureEvent: TimeSignatureEvent, 1358 | Track: Track, 1359 | TrackNameEvent: TrackNameEvent, 1360 | Utils: Utils, 1361 | VexFlow: VexFlow, 1362 | Writer: Writer 1363 | }; 1364 | 1365 | return main; 1366 | 1367 | })(); 1368 | -------------------------------------------------------------------------------- /examples/chopin-prelude-e-minor.js: -------------------------------------------------------------------------------- 1 | var MidiWriter = require('..'); 2 | 3 | var tracks = []; 4 | tracks[0] = new MidiWriter.Track(); 5 | 6 | // You can chain track methods. 7 | tracks[0] 8 | .setTempo(60) 9 | .addEvent([ 10 | // addEvent() accepts an array of event objects like this... 11 | new MidiWriter.ProgramChangeEvent({instrument : 1}), 12 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 13 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 14 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 15 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 16 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 17 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 18 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 19 | new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4'}), 20 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 21 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: '4'}), 22 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 23 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: '4'}), 24 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 25 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd8'}), 26 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '16'}), 27 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 28 | new MidiWriter.NoteEvent({pitch: ['Ab4'], duration: '2'}), 29 | new MidiWriter.NoteEvent({pitch: ['A4', 'B4', 'D5', 'C5', 'E4', 'A4'], duration: '8', sequential:true}), 30 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 31 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '4'}), 32 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 33 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '4'}), 34 | new MidiWriter.NoteEvent({pitch: ['G4', 'Gb4', 'C4', 'B3', 'Eb4', 'Gb4'], duration: '8', sequential:true}), 35 | new MidiWriter.NoteEvent({pitch: ['D5', 'C5', 'B4'], duration: '8t', sequential:true}), 36 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 37 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 38 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 39 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 40 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 41 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 42 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd8'}), 43 | new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '16'}), 44 | new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4'}), 45 | new MidiWriter.NoteEvent({pitch: ['G5'], duration: '4'}), 46 | new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: 'd8'}), 47 | new MidiWriter.NoteEvent({pitch: ['E5'], duration: '16'}), 48 | new MidiWriter.NoteEvent({pitch: ['E5', 'D#5', 'C6', 'D#5', 'D#5', 'E5', 'G5', 'B4'], duration: '8', sequential:true}), 49 | new MidiWriter.NoteEvent({pitch: ['D5', 'C5'], duration: '8', sequential:true}), 50 | new MidiWriter.NoteEvent({pitch: ['E5', 'E4', 'A4'], duration: '8t', sequential:true}), 51 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd4'}), 52 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '8'}), 53 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 54 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '4'}), 55 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 56 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd8'}), 57 | new MidiWriter.NoteEvent({pitch: ['E4'], duration: '16'}), 58 | new MidiWriter.NoteEvent({pitch: ['E4'], duration: 'd2'}), 59 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: '4'}), 60 | new MidiWriter.NoteEvent({pitch: ['E4'], duration: 'd2'}), 61 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: '4'}), 62 | new MidiWriter.NoteEvent({pitch: ['E4'], duration: '2'}), 63 | new MidiWriter.NoteEvent({pitch: ['C6', 'G5', 'D5', 'C5'], duration: '2', wait: '2'}), 64 | new MidiWriter.NoteEvent({pitch: ['C6', 'G5', 'D5', 'C5'], duration: '2'}), 65 | new MidiWriter.NoteEvent({pitch: ['C6', 'G5', 'E5', 'C5'], duration: '4'}) 66 | ], function(index, event) { 67 | return {velocity: 100}; 68 | } 69 | ); 70 | 71 | // You can optionally pass a single event object to addEvent() if you wish 72 | tracks[1] = new MidiWriter.Track(); 73 | tracks[1].addEvent(new MidiWriter.ProgramChangeEvent({instrument : 1})); 74 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8', repeat: 8})); 75 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'A3', 'F#3'], duration: '8', repeat: 4})); 76 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D#4', 'A3', 'F#3'], duration: '8', repeat: 4})); 77 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D#4', 'A3', 'F3'], duration: '8', repeat: 4})); 78 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'A3', 'F3'], duration: '8', repeat: 2})); 79 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G#3', 'F3'], duration: '8', repeat: 2})); 80 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G#3', 'E3'], duration: '8', repeat: 4})); 81 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G3', 'E3'], duration: '8', repeat: 2})); 82 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C#4', 'G3', 'E3'], duration: '8', repeat: 2})); 83 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'G3', 'E3'], duration: '8', repeat: 4})); 84 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'F#3', 'E3'], duration: '8', repeat: 8})); 85 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'F#3', 'D#3'], duration: '8', repeat: 4})); 86 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'F#3', 'D3'], duration: '8', repeat: 8})); 87 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'F3', 'D3'], duration: '8', repeat: 4})); 88 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B4', 'F3', 'D3'], duration: '8', repeat: 4})); 89 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B4', 'E3', 'C3'], duration: '8', repeat: 2})); 90 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'C3'], duration: '8', repeat: 6})); 91 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'B3'], duration: '8', repeat: 2})); 92 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'D#3', 'B3'], duration: '8', repeat: 2})); 93 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'C3'], duration: '8', repeat: 4})); 94 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'D#3', 'B3'], duration: '8', repeat: 4})); 95 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'C3'], duration: '8', repeat: 4})); 96 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'D#3', 'B3'], duration: '4'})); 97 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8', wait: 'd2'})); 98 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 99 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 100 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 101 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 102 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 103 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 104 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'B3', 'G3'], duration: '8'})); 105 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E4', 'A3', 'F#3'], duration: '8', repeat: 4})); 106 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D#4', 'A3', 'F3'], duration: '8', repeat: 4})); 107 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D#4', 'G#3', 'F3'], duration: '8', repeat: 2})); 108 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G#3', 'F3'], duration: '8', repeat: 2})); 109 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G3', 'E3'], duration: '8', repeat: 4})); 110 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['D4', 'G3', 'E3'], duration: '8', repeat: 2})); 111 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C#4', 'G3', 'E3'], duration: '8', repeat: 2})); 112 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A#4', 'E3', 'C#3'], duration: '8', repeat: 2})); 113 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'C3'], duration: '8', repeat: 2})); 114 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'B2'], duration: '8'})); 115 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A5', 'F#5', 'C4', 'A4'], duration: '8', repeat: 3})); 116 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F#5', 'D#5', 'B4', 'G4'], duration: '8'})); 117 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E5', 'B4', 'G4'], duration: '8', repeat: 3})); 118 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E5', 'C4', 'A4'], duration: '8', repeat: 2})); 119 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3'], duration: '8'})); 120 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'F#3', 'E3'], duration: '8'})); 121 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B4', 'E3', 'B3'], duration: '8', repeat: 2})); 122 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A4', 'E3', 'C3'], duration: '8', repeat: 2})); 123 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'E3', 'B2'], duration: '8', repeat: 4})); 124 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3', 'E3', 'C3'], duration: '8', repeat: 4})); 125 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'E3', 'B2'], duration: '8', repeat: 4})); 126 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'D#3', 'B3'], duration: '8', repeat: 2})); 127 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3', 'D#3', 'B3'], duration: '8', repeat: 2})); 128 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3', 'E3', 'C3'], duration: '8', repeat: 4})); 129 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb3', 'E3', 'C3'], duration: '8', repeat: 2})); 130 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3', 'E3', 'C3'], duration: '8', repeat: 2})); 131 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3', 'E3', 'B3'], duration: '8', repeat: 2})); 132 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G#3', 'E3', 'B3'], duration: '8', repeat: 2})); 133 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3', 'E3', 'B3'], duration: '8', repeat: 4})); 134 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3', 'C3', 'A#3'], duration: '2'})); 135 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'B2'], duration: '2', wait: '2'})); 136 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B3', 'F#3', 'B2'], duration: '2'})); 137 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2', 'E1'], duration: '1'})); 138 | 139 | var write = new MidiWriter.Writer(tracks); 140 | console.log(write.dataUri()); 141 | //write.stdout(); 142 | 143 | module.exports = write; -------------------------------------------------------------------------------- /examples/hot-cross-buns.js: -------------------------------------------------------------------------------- 1 | var MidiWriter = require('..'); 2 | 3 | var track = new MidiWriter.Track(); 4 | 5 | track.addEvent([ 6 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 7 | new MidiWriter.NoteEvent({pitch: 'C4', duration: '2'}), 8 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 9 | new MidiWriter.NoteEvent({pitch: 'C4', duration: '2'}), 10 | new MidiWriter.NoteEvent({pitch: ['C4', 'C4', 'C4', 'C4', 'D4', 'D4', 'D4', 'D4'], duration: '8'}), 11 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 12 | new MidiWriter.NoteEvent({pitch: 'C4', duration: '2'}) 13 | ], function(event, index) { 14 | return {sequential:true}; 15 | } 16 | ); 17 | 18 | 19 | var write = new MidiWriter.Writer(track); 20 | //console.log(track); 21 | //console.log(write.base64()) 22 | console.log(write.dataUri()); 23 | 24 | module.exports = write; -------------------------------------------------------------------------------- /examples/mauro.giuliani-op.47-main-theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | VARIATIONS 3 | Faciles 4 | Pour la Guitare 5 | Sur un air National Autrichien 6 | Composées 7 | PAR 8 | MAURO GIULIANI 9 | Oev: 47 Prix 3f 10 | à Paris 11 | chez RICHAULT... 12 | Boulevard Poissonièr No 16, au 1er 13 | [N.] 1303. R. 14 | 15 | This is the main theme, the pubblication comes along with 12 16 | variations, it´s a pretty good start for classical guitarists. 17 | **/ 18 | var MidiWriter = require('..'); 19 | var tracks = []; 20 | 21 | tracks[0] = new MidiWriter.Track(); 22 | tracks[0].setTimeSignature(3, 4); 23 | tracks[0].setTempo(100); 24 | 25 | var notes; 26 | 27 | // melody 28 | tracks[1] = new MidiWriter.Track(); 29 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '2'}); 30 | tracks[1].addEvent(notes); 31 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '4'}); 32 | tracks[1].addEvent(notes); 33 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '2'}); 34 | tracks[1].addEvent(notes); 35 | notes = new MidiWriter.NoteEvent({pitch:['A4', 'C#5'], duration: '4'}); 36 | tracks[1].addEvent(notes); 37 | notes = new MidiWriter.NoteEvent({pitch:['B4', 'D5'], duration: '2'}); 38 | tracks[1].addEvent(notes); 39 | notes = new MidiWriter.NoteEvent({pitch:['G#4', 'E5'], duration: '4'}); 40 | tracks[1].addEvent(notes); 41 | notes = new MidiWriter.NoteEvent({pitch:['A4', 'C#5'], duration: '2'}); 42 | tracks[1].addEvent(notes); 43 | notes = new MidiWriter.NoteEvent({pitch:['A4'], duration: '4'}); 44 | tracks[1].addEvent(notes); 45 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '2'}); 46 | tracks[1].addEvent(notes); 47 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '4'}); 48 | tracks[1].addEvent(notes); 49 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '2'}); 50 | tracks[1].addEvent(notes); 51 | notes = new MidiWriter.NoteEvent({pitch:['A4', 'C#5'], duration: '4'}); 52 | tracks[1].addEvent(notes); 53 | notes = new MidiWriter.NoteEvent({pitch:['B4', 'D5'], duration: '2'}); 54 | tracks[1].addEvent(notes); 55 | notes = new MidiWriter.NoteEvent({pitch:['G#4', 'E5'], duration: '4'}); 56 | tracks[1].addEvent(notes); 57 | notes = new MidiWriter.NoteEvent({pitch:['A4', 'C#5'], duration: '2'}); 58 | tracks[1].addEvent(notes); 59 | // note how the previous rest is handled: it became the wait 60 | notes = new MidiWriter.NoteEvent({wait: '4', pitch:['E5', 'E5'], duration: '4'}); 61 | tracks[1].addEvent(notes); 62 | notes = new MidiWriter.NoteEvent({pitch:['D#5', 'F#5'], duration: '4'}); 63 | tracks[1].addEvent(notes); 64 | notes = new MidiWriter.NoteEvent({pitch:['D5', 'G#5'], duration: '4'}); 65 | tracks[1].addEvent(notes); 66 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'A5'], duration: '2'}); 67 | tracks[1].addEvent(notes); 68 | notes = new MidiWriter.NoteEvent({pitch:['E5'], duration: '4'}); 69 | tracks[1].addEvent(notes); 70 | notes = new MidiWriter.NoteEvent({pitch:['E5', 'E5'], duration: '4'}); 71 | tracks[1].addEvent(notes); 72 | notes = new MidiWriter.NoteEvent({pitch:['D#5', 'F#5'], duration: '4'}); 73 | tracks[1].addEvent(notes); 74 | notes = new MidiWriter.NoteEvent({pitch:['D5', 'G#5'], duration: '4'}); 75 | tracks[1].addEvent(notes); 76 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'A5'], duration: '2'}); 77 | tracks[1].addEvent(notes); 78 | notes = new MidiWriter.NoteEvent({wait: '4', pitch:['C#5', 'E5'], duration: '2'}); 79 | tracks[1].addEvent(notes); 80 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '4'}); 81 | tracks[1].addEvent(notes); 82 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '4'}); 83 | tracks[1].addEvent(notes); 84 | notes = new MidiWriter.NoteEvent({pitch:['A5'], duration: '4'}); 85 | tracks[1].addEvent(notes); 86 | notes = new MidiWriter.NoteEvent({pitch:['A4', 'C#5'], duration: '4'}); 87 | tracks[1].addEvent(notes); 88 | notes = new MidiWriter.NoteEvent({pitch:['C#5', 'E5'], duration: '4'}); 89 | tracks[1].addEvent(notes); 90 | notes = new MidiWriter.NoteEvent({pitch:['B4', 'D5'], duration: '4'}); 91 | tracks[1].addEvent(notes); 92 | notes = new MidiWriter.NoteEvent({pitch:['G#4', 'B4'], duration: '4'}); 93 | tracks[1].addEvent(notes); 94 | notes = new MidiWriter.NoteEvent({pitch:['A4'], duration: '2'}); 95 | tracks[1].addEvent(notes); 96 | 97 | // bass 98 | tracks[2] = new MidiWriter.Track(); 99 | notes = new MidiWriter.NoteEvent({pitch:['A3'], duration: '2'}); 100 | tracks[2].addEvent(notes); 101 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 102 | tracks[2].addEvent(notes); 103 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['E3'], duration: '2'}); 104 | tracks[2].addEvent(notes); 105 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 106 | tracks[2].addEvent(notes); 107 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 108 | tracks[2].addEvent(notes); 109 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 110 | tracks[2].addEvent(notes); 111 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['E3'], duration: '2'}); 112 | tracks[2].addEvent(notes); 113 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 114 | tracks[2].addEvent(notes); 115 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['E3'], duration: '2'}); 116 | tracks[2].addEvent(notes); 117 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 118 | tracks[2].addEvent(notes); 119 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['E3'], duration: '2'}); 120 | tracks[2].addEvent(notes); 121 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 122 | tracks[2].addEvent(notes); 123 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 124 | tracks[2].addEvent(notes); 125 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 126 | tracks[2].addEvent(notes); 127 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['E3'], duration: '2'}); 128 | tracks[2].addEvent(notes); 129 | notes = new MidiWriter.NoteEvent({wait: '4', pitch: ['A3'], duration: '2'}); 130 | tracks[2].addEvent(notes); 131 | 132 | 133 | var write = new MidiWriter.Writer(tracks); 134 | 135 | console.log(write.dataUri()); 136 | module.exports = write; -------------------------------------------------------------------------------- /examples/notes-by-start-tick.js: -------------------------------------------------------------------------------- 1 | var MidiWriter = require('..'); 2 | 3 | var track = new MidiWriter.Track(); 4 | 5 | track.addEvent([ 6 | new MidiWriter.NoteEvent({ 7 | pitch: 'C4', 8 | duration: 'T50', 9 | tick: 0 10 | }), 11 | new MidiWriter.NoteEvent({ 12 | pitch: 'E4', 13 | duration: 'T50', 14 | tick: 50 15 | }), 16 | new MidiWriter.NoteEvent({ 17 | pitch: ['G4', 'B4'], 18 | duration: 'T50', 19 | tick: 100 20 | }), 21 | new MidiWriter.NoteEvent({ 22 | pitch: 'C5', 23 | duration: 'T50', 24 | tick: 150 25 | }), 26 | new MidiWriter.NoteEvent({ 27 | pitch: 'D5', 28 | duration: 'T50', 29 | tick: 200 30 | }), 31 | new MidiWriter.NoteEvent({ 32 | pitch: 'F5', 33 | duration: 'T50', 34 | tick: 250 35 | }), 36 | new MidiWriter.NoteEvent({ 37 | pitch: 'A5', 38 | duration: 'T50', 39 | tick: 300 40 | }), 41 | ]); 42 | 43 | var write = new MidiWriter.Writer(track); 44 | console.log(write.dataUri()); 45 | module.exports = write; 46 | -------------------------------------------------------------------------------- /examples/zelda-main-theme.js: -------------------------------------------------------------------------------- 1 | var MidiWriter = require('..'); 2 | var tracks = []; 3 | 4 | // Lead Instrument 5 | tracks[0] = new MidiWriter.Track(); 6 | tracks[0].addEvent(new MidiWriter.ProgramChangeEvent({instrument : 1})); 7 | 8 | // Bass 9 | tracks[1] = new MidiWriter.Track(); 10 | tracks[1].addEvent(new MidiWriter.ProgramChangeEvent({instrument : 1})); 11 | 12 | // Intro 13 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '2', velocity:100})); 14 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 15 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 16 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 17 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 18 | 19 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab4'], duration: '16', velocity:100})); 20 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: '16', velocity:100})); 21 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab4'], duration: 'd2', velocity:100})); 22 | 23 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '2', wait:'8', velocity:100})); 24 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 25 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 26 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 27 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 28 | 29 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A4'], duration: '16', velocity:100})); 30 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['G4'], duration: '16', velocity:100})); 31 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd4', velocity:100})); 32 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A4'], duration: '1', wait:'2', velocity:1})); 33 | 34 | 35 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '4', velocity:50})); 36 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F3'], duration: '4', velocity:50})); 37 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb3'], duration: '2', velocity:50})); 38 | 39 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '4', velocity:50})); 40 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb3'], duration: '4', velocity:50})); 41 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '2', velocity:50})); 42 | 43 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '4', velocity:50})); 44 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '4', velocity:50})); 45 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb3'], duration: '2', velocity:50})); 46 | 47 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '4', velocity:50})); 48 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F3'], duration: '4', velocity:50})); 49 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb3'], duration: '2', velocity:50})); 50 | 51 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 52 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 53 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 54 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 55 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 56 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 57 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 58 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 59 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 60 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 61 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 62 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 63 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 64 | 65 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 66 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 67 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 68 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 69 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 70 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 71 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 72 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 73 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 74 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 75 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 76 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 77 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 78 | 79 | // Main theme part 1 80 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4', wait:'1', velocity:100})); 81 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: 'd4', velocity:100})); 82 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 83 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4', 'C5', 'D5', 'Eb5'], duration: '16', sequential: 'true', velocity:100})); 84 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 85 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5', 'F5'], duration: '8', sequential: 'true', wait:'8', velocity:100})); 86 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5', 'Ab5'], duration: '16', sequential: 'true', velocity:100})); 87 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb5'], duration: '2', velocity:100})); 88 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb5', 'Bb5'], duration: '8', sequential: 'true', wait:'8', velocity:100})); 89 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab5', 'Gb5'], duration: '16', sequential: 'true', velocity:100})); 90 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab5'], duration: 'd8', sequential: 'true', velocity:100})); 91 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: '16', sequential: 'true', velocity:100})); 92 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 93 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '4', velocity:100})); 94 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb5'], duration: 'd8', velocity:100})); 95 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '16', velocity:100})); 96 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: '2', velocity:100})); 97 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5', 'Eb5'], duration: '8', sequential: 'true', velocity:100})); 98 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Db5'], duration: 'd8', velocity:100})); 99 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb5'], duration: '16', velocity:100})); 100 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 101 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb5', 'Db5'], duration: '8', sequential: 'true', velocity:100})); 102 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['C5'], duration: 'd8', velocity:100})); 103 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['D5'], duration: '16', velocity:100})); 104 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['E5'], duration: '2', velocity:100})); 105 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['G5'], duration: '4', velocity:100})); 106 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '8', velocity:100})); 107 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 108 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 109 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 110 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 111 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 112 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 113 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 114 | 115 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 116 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 117 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 118 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 119 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 120 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 121 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 122 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 123 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 124 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 125 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 126 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 127 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 128 | 129 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 130 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 131 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 132 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 133 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 134 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 135 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 136 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 137 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 138 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 139 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 140 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 141 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 142 | 143 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 144 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 145 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db2'], duration: '16', velocity:50})); 146 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 147 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 148 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db2'], duration: '16', velocity:50})); 149 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 150 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 151 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db2'], duration: '16', velocity:50})); 152 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 153 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db2'], duration: '16', velocity:50})); 154 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 155 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db2'], duration: '16', velocity:50})); 156 | 157 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '8', velocity:50})); 158 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '16', velocity:50})); 159 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 160 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '8', velocity:50})); 161 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '16', velocity:50})); 162 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 163 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '8', velocity:50})); 164 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '16', velocity:50})); 165 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 166 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '16', velocity:50})); 167 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 168 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Db3'], duration: '16', velocity:50})); 169 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 170 | 171 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 172 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 173 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 174 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 175 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 176 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 177 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 178 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 179 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 180 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 181 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 182 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 183 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 184 | 185 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 186 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 187 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 188 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 189 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 190 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 191 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 192 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 193 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 194 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 195 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 196 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 197 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 198 | 199 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 200 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 201 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G2'], duration: '16', velocity:50})); 202 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 203 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 204 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G2'], duration: '16', velocity:50})); 205 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 206 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 207 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G2'], duration: '16', velocity:50})); 208 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 209 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G2'], duration: '16', velocity:50})); 210 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 211 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G2'], duration: '16', velocity:50})); 212 | 213 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 214 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3'], duration: '16', velocity:50})); 215 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3'], duration: '16', velocity:50})); 216 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '8', velocity:50})); 217 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '16', velocity:50})); 218 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '16', velocity:50})); 219 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '8', velocity:50})); 220 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '16', velocity:50})); 221 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '16', velocity:50})); 222 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb3'], duration: '8', velocity:50})); 223 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 224 | 225 | // Main theme part 2 226 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4', velocity:100})); 227 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: 'd4', velocity:100})); 228 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '8', velocity:100})); 229 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4', 'C5', 'D5', 'Eb5'], duration: '16', sequential: 'true', velocity:100})); 230 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 231 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5', 'F5'], duration: '8', sequential: 'true', wait:'8', velocity:100})); 232 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5', 'Ab5'], duration: '16', sequential: 'true', velocity:100})); 233 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb5'], duration: 'd2', velocity:100})); 234 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Db6'], duration: '4', velocity:100})); 235 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['C6'], duration: '4', velocity:100})); 236 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A5'], duration: '2', velocity:100})); 237 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '4', velocity:100})); 238 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: 'd2', velocity:100})); 239 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb5'], duration: '4', velocity:100})); 240 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A5'], duration: '4', velocity:100})); 241 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 242 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '4', velocity:100})); 243 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: 'd2', velocity:100})); 244 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb5'], duration: '4', velocity:100})); 245 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['A5'], duration: '4', velocity:100})); 246 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '2', velocity:100})); 247 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['D5'], duration: '4', velocity:100})); 248 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb5'], duration: 'd2', velocity:100})); 249 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb5'], duration: '4', velocity:100})); 250 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '4', velocity:100})); 251 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Db5'], duration: '2', velocity:100})); 252 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4', velocity:100})); 253 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['C5'], duration: 'd8', velocity:100})); 254 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['D5'], duration: '16', velocity:100})); 255 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['E5'], duration: '2', velocity:100})); 256 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['G5'], duration: '4', velocity:100})); 257 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F5'], duration: '8', velocity:100})); 258 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 259 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 260 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 261 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 262 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4', 'F4'], duration: '16', sequential: 'true', velocity:100})); 263 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 264 | tracks[0].addEvent(new MidiWriter.NoteEvent({pitch: ['F4'], duration: '8', velocity:100})); 265 | 266 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 267 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 268 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 269 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 270 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 271 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 272 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 273 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 274 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 275 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 276 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 277 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 278 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 279 | 280 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 281 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 282 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 283 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 284 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 285 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 286 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '8', velocity:50})); 287 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 288 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 289 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 290 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 291 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab2'], duration: '16', velocity:50})); 292 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Eb2'], duration: '16', velocity:50})); 293 | 294 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 295 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 296 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 297 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 298 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 299 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 300 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '8', velocity:50})); 301 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 302 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 303 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 304 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 305 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 306 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb2'], duration: '16', velocity:50})); 307 | 308 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 309 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 310 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 311 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 312 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 313 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 314 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 315 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 316 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 317 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 318 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 319 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 320 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 321 | 322 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 323 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 324 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 325 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 326 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 327 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 328 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 329 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 330 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 331 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 332 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 333 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 334 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 335 | 336 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 337 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 338 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 339 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 340 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 341 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 342 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 343 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 344 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 345 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 346 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 347 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 348 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 349 | 350 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 351 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 352 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 353 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 354 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 355 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 356 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '8', velocity:50})); 357 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 358 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 359 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 360 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 361 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 362 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['E2'], duration: '16', velocity:50})); 363 | 364 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 365 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 366 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 367 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 368 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 369 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 370 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 371 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 372 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 373 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 374 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 375 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 376 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '16', velocity:50})); 377 | 378 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 379 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 380 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 381 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 382 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 383 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 384 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '8', velocity:50})); 385 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 386 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 387 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 388 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 389 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 390 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['B2'], duration: '16', velocity:50})); 391 | 392 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 393 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 394 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 395 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 396 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 397 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 398 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '8', velocity:50})); 399 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 400 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 401 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 402 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 403 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 404 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Bb2'], duration: '16', velocity:50})); 405 | 406 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 407 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 408 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 409 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 410 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 411 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 412 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '8', velocity:50})); 413 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 414 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 415 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 416 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 417 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 418 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['C3'], duration: '16', velocity:50})); 419 | 420 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 421 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3'], duration: '16', velocity:50})); 422 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['A3'], duration: '16', velocity:50})); 423 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '8', velocity:50})); 424 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '16', velocity:50})); 425 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Ab3'], duration: '16', velocity:50})); 426 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '8', velocity:50})); 427 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '16', velocity:50})); 428 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['G3'], duration: '16', velocity:50})); 429 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['Gb3'], duration: '8', velocity:50})); 430 | tracks[1].addEvent(new MidiWriter.NoteEvent({pitch: ['F2'], duration: '8', velocity:50})); 431 | 432 | var write = new MidiWriter.Writer(tracks); 433 | 434 | console.log(write.dataUri()); 435 | module.exports = write; 436 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "template": "node_modules/minami" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midi-writer-js", 3 | "version": "3.1.1", 4 | "description": "A library providing an API for generating MIDI files.", 5 | "main": "build/index.js", 6 | "types": "build/types/main.d.ts", 7 | "dependencies": { 8 | "@tonaljs/midi": "^4.9.0" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.9.6", 12 | "@babel/plugin-transform-destructuring": "^7.9.5", 13 | "@babel/preset-env": "^7.9.6", 14 | "@rollup/plugin-babel": "^5.0.0", 15 | "@rollup/plugin-node-resolve": "^15.0.1", 16 | "@rollup/plugin-replace": "^2.4.2", 17 | "@rollup/plugin-typescript": "^11.0.0", 18 | "@types/node": "^18.15.3", 19 | "@typescript-eslint/eslint-plugin": "^5.55.0", 20 | "@typescript-eslint/parser": "^5.55.0", 21 | "eslint": "^7.32.0", 22 | "eslint-config-standard": "^12.0.0", 23 | "eslint-plugin-import": "^2.20.2", 24 | "eslint-plugin-node": "^9.2.0", 25 | "eslint-plugin-promise": "^4.1.1", 26 | "eslint-plugin-standard": "^4.0.1", 27 | "jsdoc": "^3.6.4", 28 | "minami": "^1.2.3", 29 | "mocha": "^9.0.1", 30 | "nyc": "^15.0.1", 31 | "rollup": "^2.9.0", 32 | "tslib": "^2.5.0", 33 | "typedoc": "^0.25.1", 34 | "typescript": "^5.0.2", 35 | "watch": "^1.0.2" 36 | }, 37 | "directories": { 38 | "lib": "src", 39 | "example": "examples", 40 | "test": "test" 41 | }, 42 | "scripts": { 43 | "build": "mkdir -p build && rollup -c", 44 | "docs": "npx typedoc --options typedoc.json", 45 | "lint:js": "eslint 'src/**/**.ts'", 46 | "prepublishOnly": "npm test", 47 | "pretest": "npm run build", 48 | "test": "nyc --reporter=text mocha --no-config --no-package", 49 | "watch": "watch 'npm run build' src", 50 | "postinstall": "node postinstall.js" 51 | }, 52 | "runkitExampleFilename": "runkit.js", 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/grimmdude/MidiWriterJS.git" 56 | }, 57 | "keywords": [ 58 | "midi", 59 | "generator", 60 | "music" 61 | ], 62 | "author": "Garrett Grimm", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/grimmdude/MidiWriterJS/issues" 66 | }, 67 | "homepage": "https://github.com/grimmdude/MidiWriterJS#readme", 68 | "publishConfig": { 69 | "@grimmdude:registry": "https://npm.pkg.github.com" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | console.log('\x1b[36m%s\x1b[0m', "Please give MidiWriterJS a ⭐ on GitHub if you found useful, thx!"); -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import replace from "@rollup/plugin-replace"; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | 5 | export default [ 6 | { 7 | input: "src/main.ts", 8 | output: [ 9 | { 10 | file: "browser/midiwriter.js", 11 | format: "iife", 12 | name: "MidiWriter", 13 | }, 14 | { 15 | file: "build/index.browser.js", 16 | format: "es", 17 | name: "MidiWriter", 18 | }, 19 | ], 20 | plugins: [ 21 | typescript(), 22 | replace({ 23 | "process.browser": true, 24 | "preventAssignment": true 25 | }), 26 | nodeResolve(), 27 | ], 28 | }, 29 | { 30 | input: 'src/main.ts', 31 | output: { 32 | file: 'build/index.js', 33 | format: 'cjs', 34 | exports: 'default', 35 | }, 36 | external: ['tonal-midi', 'fs'], 37 | plugins: [ 38 | typescript(), 39 | nodeResolve() 40 | ] 41 | } 42 | ]; -------------------------------------------------------------------------------- /runkit.js: -------------------------------------------------------------------------------- 1 | const rkmidi = require('runkit-midi'); 2 | const MidiWriter = require('midi-writer-js'); 3 | const track = new MidiWriter.Track(); 4 | track.addEvent([ 5 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 6 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}), 7 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 8 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}), 9 | new MidiWriter.NoteEvent({pitch: ['C4', 'C4', 'C4', 'C4', 'D4', 'D4', 'D4', 'D4'], duration: '8'}), 10 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4'}), 11 | new MidiWriter.NoteEvent({pitch: ['C4'], duration: '2'}) 12 | ], function(event, index) { 13 | return {sequential: true}; 14 | } 15 | ); 16 | 17 | const write = new MidiWriter.Writer(track); 18 | rkmidi(write.buildFile()); -------------------------------------------------------------------------------- /src/abstract-event.ts: -------------------------------------------------------------------------------- 1 | interface AbstractEvent { 2 | data: number[]; 3 | delta: number; 4 | name: string; 5 | tick?: number; 6 | } 7 | 8 | export {AbstractEvent}; -------------------------------------------------------------------------------- /src/chunks/chunk.ts: -------------------------------------------------------------------------------- 1 | interface Chunk { 2 | type: number[]; 3 | size: number[]; 4 | } 5 | 6 | export {Chunk}; 7 | -------------------------------------------------------------------------------- /src/chunks/header.ts: -------------------------------------------------------------------------------- 1 | import {Chunk} from './chunk'; 2 | import {Constants} from '../constants'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a header chunk section of a MIDI file. 7 | * @param {number} numberOfTracks - Number of tracks 8 | * @return {Header} 9 | */ 10 | class Header implements Chunk { 11 | data: number[]; 12 | type: number[]; 13 | size: number[]; 14 | 15 | constructor(numberOfTracks) { 16 | this.type = Constants.HEADER_CHUNK_TYPE; 17 | 18 | const trackType = numberOfTracks > 1? Constants.HEADER_CHUNK_FORMAT1 : Constants.HEADER_CHUNK_FORMAT0; 19 | 20 | this.data = trackType.concat( 21 | Utils.numberToBytes(numberOfTracks, 2), // two bytes long, 22 | Constants.HEADER_CHUNK_DIVISION 23 | ); 24 | 25 | this.size = [0, 0, 0, this.data.length]; 26 | } 27 | } 28 | 29 | export {Header}; 30 | -------------------------------------------------------------------------------- /src/chunks/track.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEvent } from '../abstract-event'; 2 | import {Chunk} from './chunk'; 3 | import {Constants} from '../constants'; 4 | import {ControllerChangeEvent} from '../midi-events/controller-change-event'; 5 | import {CopyrightEvent} from '../meta-events/copyright-event'; 6 | import {CuePointEvent} from '../meta-events/cue-point-event'; 7 | import {EndTrackEvent} from '../meta-events/end-track-event'; 8 | import {InstrumentNameEvent} from '../meta-events/instrument-name-event'; 9 | import {KeySignatureEvent} from '../meta-events/key-signature-event'; 10 | import {LyricEvent} from '../meta-events/lyric-event'; 11 | import {MarkerEvent} from '../meta-events/marker-event'; 12 | import {NoteEvent} from '../midi-events/note-event'; 13 | import {NoteOnEvent} from '../midi-events/note-on-event'; 14 | import {NoteOffEvent} from '../midi-events/note-off-event'; 15 | import {PitchBendEvent} from '../midi-events/pitch-bend-event'; 16 | import {TempoEvent} from '../meta-events/tempo-event'; 17 | import {TextEvent} from '../meta-events/text-event'; 18 | import {TimeSignatureEvent} from '../meta-events/time-signature-event'; 19 | import {TrackNameEvent} from '../meta-events/track-name-event'; 20 | import {Utils} from '../utils'; 21 | 22 | /** 23 | * Holds all data for a track. 24 | * @param {object} fields {type: number, data: array, size: array, events: array} 25 | * @return {Track} 26 | */ 27 | class Track implements Chunk { 28 | data: number[]; 29 | events: AbstractEvent[]; 30 | explicitTickEvents: NoteEvent[]; 31 | size: number[]; 32 | type: number[]; 33 | tickPointer: number; 34 | 35 | constructor() { 36 | this.type = Constants.TRACK_CHUNK_TYPE; 37 | this.data = []; 38 | this.size = []; 39 | this.events = []; 40 | this.explicitTickEvents = []; 41 | 42 | // If there are any events with an explicit tick defined then we will create a "sub" track for those 43 | // and merge them in and the end. 44 | this.tickPointer = 0; // Each time an event is added this will increase 45 | } 46 | 47 | /** 48 | * Adds any event type to the track. 49 | * Events without a specific startTick property are assumed to be added in order of how they should output. 50 | * Events with a specific startTick property are set aside for now will be merged in during build process. 51 | * 52 | * TODO: Don't put startTick events in their own array. Just lump everything together and sort it out during buildData(); 53 | * @param {(NoteEvent|ProgramChangeEvent)} events - Event object or array of Event objects. 54 | * @param {Function} mapFunction - Callback which can be used to apply specific properties to all events. 55 | * @return {Track} 56 | */ 57 | addEvent(events: (AbstractEvent|AbstractEvent[]), mapFunction?: (i: number, event: AbstractEvent) => object): Track { 58 | Utils.toArray(events).forEach((event, i) => { 59 | if (event instanceof NoteEvent) { 60 | // Handle map function if provided 61 | if (typeof mapFunction === 'function') { 62 | const properties = mapFunction(i, event); 63 | 64 | if (typeof properties === 'object') { 65 | Object.assign(event, properties); 66 | } 67 | } 68 | 69 | // If this note event has an explicit startTick then we need to set aside for now 70 | if (event.tick !== null) { 71 | this.explicitTickEvents.push(event); 72 | 73 | } else { 74 | // Push each on/off event to track's event stack 75 | event.buildData().events.forEach((e) => this.events.push(e)); 76 | } 77 | 78 | } else { 79 | this.events.push(event); 80 | } 81 | }); 82 | 83 | return this; 84 | } 85 | 86 | /** 87 | * Builds int array of all events. 88 | * @param {object} options 89 | * @return {Track} 90 | */ 91 | buildData(options = {}) { 92 | // Reset 93 | this.data = []; 94 | this.size = []; 95 | this.tickPointer = 0; 96 | 97 | let precisionLoss = 0; 98 | 99 | this.events.forEach((event) => { 100 | // Build event & add to total tick duration 101 | if (event instanceof NoteOnEvent || event instanceof NoteOffEvent) { 102 | const built = event.buildData(this, precisionLoss, options); 103 | precisionLoss = Utils.getPrecisionLoss(event.deltaWithPrecisionCorrection || 0); 104 | this.data = this.data.concat(built.data); 105 | this.tickPointer = Utils.getRoundedIfClose(event.tick); 106 | 107 | } else if (event instanceof TempoEvent) { 108 | this.tickPointer = Utils.getRoundedIfClose(event.tick); 109 | this.data = this.data.concat(event.data); 110 | 111 | } else { 112 | this.data = this.data.concat(event.data); 113 | } 114 | }); 115 | 116 | this.mergeExplicitTickEvents(); 117 | 118 | // If the last event isn't EndTrackEvent, then tack it onto the data. 119 | if (!this.events.length || !(this.events[this.events.length - 1] instanceof EndTrackEvent)) { 120 | this.data = this.data.concat((new EndTrackEvent).data); 121 | } 122 | 123 | this.size = Utils.numberToBytes(this.data.length, 4); // 4 bytes long 124 | return this; 125 | } 126 | 127 | mergeExplicitTickEvents() { 128 | if (!this.explicitTickEvents.length) return; 129 | 130 | // First sort asc list of events by startTick 131 | this.explicitTickEvents.sort((a, b) => a.tick - b.tick); 132 | 133 | // Now this.explicitTickEvents is in correct order, and so is this.events naturally. 134 | // For each explicit tick event, splice it into the main list of events and 135 | // adjust the delta on the following events so they still play normally. 136 | this.explicitTickEvents.forEach((noteEvent) => { 137 | // Convert NoteEvent to it's respective NoteOn/NoteOff events 138 | // Note that as we splice in events the delta for the NoteOff ones will 139 | // Need to change based on what comes before them after the splice. 140 | noteEvent.buildData().events.forEach((e) => e.buildData(this)); 141 | 142 | // Merge each event individually into this track's event list. 143 | noteEvent.events.forEach((event) => this.mergeSingleEvent(event)); 144 | }); 145 | 146 | // Hacky way to rebuild track with newly spliced events. Need better solution. 147 | this.explicitTickEvents = []; 148 | this.buildData(); 149 | } 150 | 151 | /** 152 | * Merges another track's events with this track. 153 | * @param {Track} track 154 | * @return {Track} 155 | */ 156 | mergeTrack(track: Track): Track { 157 | // First build this track to populate each event's tick property 158 | this.buildData(); 159 | 160 | // Then build track to be merged so that tick property is populated on all events & merge each event. 161 | track.buildData().events.forEach((event) => this.mergeSingleEvent(event)); 162 | return this; 163 | } 164 | 165 | /** 166 | * Merges a single event into this track's list of events based on event.tick property. 167 | * @param {AbstractEvent} - event 168 | * @return {Track} 169 | */ 170 | mergeSingleEvent(event: AbstractEvent): Track { 171 | // There are no events yet, so just add it in. 172 | if (!this.events.length) { 173 | this.addEvent(event); 174 | return; 175 | } 176 | 177 | // Find index of existing event we need to follow with 178 | let lastEventIndex; 179 | 180 | for (let i = 0; i < this.events.length; i++) { 181 | if (this.events[i].tick > event.tick) break; 182 | lastEventIndex = i; 183 | } 184 | 185 | const splicedEventIndex = lastEventIndex + 1; 186 | 187 | // Need to adjust the delta of this event to ensure it falls on the correct tick. 188 | event.delta = event.tick - this.events[lastEventIndex].tick; 189 | 190 | // Splice this event at lastEventIndex + 1 191 | this.events.splice(splicedEventIndex, 0, event); 192 | 193 | // Now adjust delta of all following events 194 | for (let i = splicedEventIndex + 1; i < this.events.length; i++) { 195 | // Since each existing event should have a tick value at this point we just need to 196 | // adjust delta to that the event still falls on the correct tick. 197 | this.events[i].delta = this.events[i].tick - this.events[i - 1].tick; 198 | } 199 | } 200 | 201 | /** 202 | * Removes all events matching specified type. 203 | * @param {string} eventName - Event type 204 | * @return {Track} 205 | */ 206 | removeEventsByName(eventName: string): Track { 207 | this.events.forEach((event, index) => { 208 | if (event.name === eventName) { 209 | this.events.splice(index, 1); 210 | } 211 | }); 212 | 213 | return this; 214 | } 215 | 216 | /** 217 | * Sets tempo of the MIDI file. 218 | * @param {number} bpm - Tempo in beats per minute. 219 | * @param {number} tick - Start tick. 220 | * @return {Track} 221 | */ 222 | setTempo(bpm: number, tick = 0): Track { 223 | return this.addEvent(new TempoEvent({bpm, tick})); 224 | } 225 | 226 | /** 227 | * Sets time signature. 228 | * @param {number} numerator - Top number of the time signature. 229 | * @param {number} denominator - Bottom number of the time signature. 230 | * @param {number} midiclockspertick - Defaults to 24. 231 | * @param {number} notespermidiclock - Defaults to 8. 232 | * @return {Track} 233 | */ 234 | setTimeSignature(numerator: number, denominator: number, midiclockspertick: number, notespermidiclock: number): Track { 235 | return this.addEvent(new TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock)); 236 | } 237 | 238 | /** 239 | * Sets key signature. 240 | * @param {*} sf - 241 | * @param {*} mi - 242 | * @return {Track} 243 | */ 244 | setKeySignature(sf, mi) { 245 | return this.addEvent(new KeySignatureEvent(sf, mi)); 246 | } 247 | 248 | /** 249 | * Adds text to MIDI file. 250 | * @param {string} text - Text to add. 251 | * @return {Track} 252 | */ 253 | addText(text: string): Track { 254 | return this.addEvent(new TextEvent({text})); 255 | } 256 | 257 | /** 258 | * Adds copyright to MIDI file. 259 | * @param {string} text - Text of copyright line. 260 | * @return {Track} 261 | */ 262 | addCopyright(text: string): Track { 263 | return this.addEvent(new CopyrightEvent({text})); 264 | } 265 | 266 | /** 267 | * Adds Sequence/Track Name. 268 | * @param {string} text - Text of track name. 269 | * @return {Track} 270 | */ 271 | addTrackName(text: string): Track { 272 | return this.addEvent(new TrackNameEvent({text})); 273 | } 274 | 275 | /** 276 | * Sets instrument name of track. 277 | * @param {string} text - Name of instrument. 278 | * @return {Track} 279 | */ 280 | addInstrumentName(text: string): Track { 281 | return this.addEvent(new InstrumentNameEvent({text})); 282 | } 283 | 284 | /** 285 | * Adds marker to MIDI file. 286 | * @param {string} text - Marker text. 287 | * @return {Track} 288 | */ 289 | addMarker(text: string): Track { 290 | return this.addEvent(new MarkerEvent({text})); 291 | } 292 | 293 | /** 294 | * Adds cue point to MIDI file. 295 | * @param {string} text - Text of cue point. 296 | * @return {Track} 297 | */ 298 | addCuePoint(text: string): Track { 299 | return this.addEvent(new CuePointEvent({text})); 300 | } 301 | 302 | /** 303 | * Adds lyric to MIDI file. 304 | * @param {string} text - Lyric text to add. 305 | * @return {Track} 306 | */ 307 | addLyric(text: string): Track { 308 | return this.addEvent(new LyricEvent({text})); 309 | } 310 | 311 | /** 312 | * Channel mode messages 313 | * @return {Track} 314 | */ 315 | polyModeOn(): Track { 316 | const event = new NoteOnEvent({data: [0x00, 0xB0, 0x7E, 0x00]}); 317 | return this.addEvent(event); 318 | } 319 | 320 | 321 | /** 322 | * Sets a pitch bend. 323 | * @param {float} bend - Bend value ranging [-1,1], zero meaning no bend. 324 | * @return {Track} 325 | */ 326 | setPitchBend(bend: number): Track { 327 | return this.addEvent(new PitchBendEvent({bend})); 328 | } 329 | 330 | 331 | /** 332 | * Adds a controller change event 333 | * @param {number} number - Control number. 334 | * @param {number} value - Control value. 335 | * @param {number} channel - Channel to send controller change event on (1-based). 336 | * @param {number} delta - Track tick offset for cc event. 337 | * @return {Track} 338 | */ 339 | controllerChange(number: number, value: number, channel?: number, delta?: number): Track { 340 | return this.addEvent(new ControllerChangeEvent({controllerNumber: number, controllerValue: value, channel: channel, delta: delta})); 341 | } 342 | 343 | } 344 | 345 | export {Track}; 346 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MIDI file format constants. 3 | * @return {Constants} 4 | */ 5 | 6 | const Constants = { 7 | VERSION : '3.1.1', 8 | HEADER_CHUNK_TYPE : [0x4d, 0x54, 0x68, 0x64], // Mthd 9 | HEADER_CHUNK_LENGTH : [0x00, 0x00, 0x00, 0x06], // Header size for SMF 10 | HEADER_CHUNK_FORMAT0 : [0x00, 0x00], // Midi Type 0 id 11 | HEADER_CHUNK_FORMAT1 : [0x00, 0x01], // Midi Type 1 id 12 | HEADER_CHUNK_DIVISION : [0x00, 0x80], // Defaults to 128 ticks per beat 13 | TRACK_CHUNK_TYPE : [0x4d, 0x54, 0x72, 0x6b], // MTrk, 14 | META_EVENT_ID : 0xFF, 15 | META_SMTPE_OFFSET : 0x54 16 | }; 17 | 18 | export {Constants}; 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Constants} from './constants'; 3 | import {ControllerChangeEvent} from './midi-events/controller-change-event'; 4 | import {CopyrightEvent} from './meta-events/copyright-event'; 5 | import {CuePointEvent} from './meta-events/cue-point-event'; 6 | import {EndTrackEvent} from './meta-events/end-track-event'; 7 | import {InstrumentNameEvent} from './meta-events/instrument-name-event'; 8 | import {KeySignatureEvent} from './meta-events/key-signature-event'; 9 | import {LyricEvent} from './meta-events/lyric-event'; 10 | import {MarkerEvent} from './meta-events/marker-event'; 11 | import {NoteOnEvent} from './midi-events/note-on-event'; 12 | import {NoteOffEvent} from './midi-events/note-off-event'; 13 | import {NoteEvent} from './midi-events/note-event'; 14 | import {PitchBendEvent} from './midi-events/pitch-bend-event'; 15 | import {ProgramChangeEvent} from './midi-events/program-change-event'; 16 | import {TempoEvent} from './meta-events/tempo-event'; 17 | import {TextEvent} from './meta-events/text-event'; 18 | import {TimeSignatureEvent} from './meta-events/time-signature-event'; 19 | import {Track} from './chunks/track'; 20 | import {TrackNameEvent} from './meta-events/track-name-event'; 21 | import {Utils} from './utils'; 22 | import {VexFlow} from './vexflow'; 23 | import {Writer} from './writer'; 24 | 25 | export default { 26 | Constants, 27 | ControllerChangeEvent, 28 | CopyrightEvent, 29 | CuePointEvent, 30 | EndTrackEvent, 31 | InstrumentNameEvent, 32 | KeySignatureEvent, 33 | LyricEvent, 34 | MarkerEvent, 35 | NoteOnEvent, 36 | NoteOffEvent, 37 | NoteEvent, 38 | PitchBendEvent, 39 | ProgramChangeEvent, 40 | TempoEvent, 41 | TextEvent, 42 | TimeSignatureEvent, 43 | Track, 44 | TrackNameEvent, 45 | Utils, 46 | VexFlow, 47 | Writer 48 | }; 49 | -------------------------------------------------------------------------------- /src/meta-events/copyright-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a tempo meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {CopyrightEvent} 9 | */ 10 | class CopyrightEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x02; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'CopyrightEvent'; 20 | this.text = fields.text; 21 | this.type = 0x02; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {CopyrightEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/cue-point-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a cue point meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {CuePointEvent} 9 | */ 10 | class CuePointEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x07; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'CuePointEvent'; 20 | this.text = fields.text; 21 | this.type = 0x07; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {CuePointEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/end-track-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a end track meta event. 7 | * @param {object} fields {delta: integer} 8 | * @return {EndTrackEvent} 9 | */ 10 | class EndTrackEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | type: [0x2F, 0x00]; 15 | 16 | constructor(fields?: { delta: number; }) { 17 | this.delta = fields?.delta || 0x00; 18 | this.name = 'EndTrackEvent'; 19 | this.type = [0x2F, 0x00]; 20 | 21 | // Start with zero time delta 22 | this.data = Utils.numberToVariableLength(this.delta).concat( 23 | Constants.META_EVENT_ID, 24 | this.type 25 | ); 26 | } 27 | } 28 | 29 | export {EndTrackEvent}; 30 | -------------------------------------------------------------------------------- /src/meta-events/instrument-name-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of an instrument name meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {InstrumentNameEvent} 9 | */ 10 | class InstrumentNameEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x04; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'InstrumentNameEvent'; 20 | this.text = fields.text; 21 | this.type = 0x04; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Instrument name 31 | ); 32 | } 33 | } 34 | 35 | export {InstrumentNameEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/key-signature-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a key signature meta event. 7 | * @return {KeySignatureEvent} 8 | */ 9 | class KeySignatureEvent implements MetaEvent { 10 | data: number[]; 11 | delta: number; 12 | name: string; 13 | type: 0x59; 14 | 15 | constructor(sf, mi) { 16 | this.name = 'KeySignatureEvent'; 17 | this.type = 0x59; 18 | 19 | let mode = mi || 0; 20 | sf = sf || 0; 21 | 22 | // Function called with string notation 23 | if (typeof mi === 'undefined') { 24 | const fifths = [ 25 | ['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#'], 26 | ['ab', 'eb', 'bb', 'f', 'c', 'g', 'd', 'a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#'] 27 | ]; 28 | const _sflen = sf.length; 29 | let note = sf || 'C'; 30 | 31 | if (sf[0] === sf[0].toLowerCase()) mode = 1 32 | 33 | if (_sflen > 1) { 34 | switch (sf.charAt(_sflen - 1)) { 35 | case 'm': 36 | mode = 1; 37 | note = sf.charAt(0).toLowerCase(); 38 | note = note.concat(sf.substring(1, _sflen - 1)); 39 | break; 40 | case '-': 41 | mode = 1; 42 | note = sf.charAt(0).toLowerCase(); 43 | note = note.concat(sf.substring(1, _sflen - 1)); 44 | break; 45 | case 'M': 46 | mode = 0; 47 | note = sf.charAt(0).toUpperCase(); 48 | note = note.concat(sf.substring(1, _sflen - 1)); 49 | break; 50 | case '+': 51 | mode = 0; 52 | note = sf.charAt(0).toUpperCase(); 53 | note = note.concat(sf.substring(1, _sflen - 1)); 54 | break; 55 | } 56 | } 57 | 58 | const fifthindex = fifths[mode].indexOf(note); 59 | sf = fifthindex === -1 ? 0 : fifthindex - 7; 60 | } 61 | 62 | // Start with zero time delta 63 | this.data = Utils.numberToVariableLength(0x00).concat( 64 | Constants.META_EVENT_ID, 65 | this.type, 66 | [0x02], // Size 67 | Utils.numberToBytes(sf, 1), // Number of sharp or flats ( < 0 flat; > 0 sharp) 68 | Utils.numberToBytes(mode, 1), // Mode: 0 major, 1 minor 69 | ); 70 | } 71 | } 72 | 73 | export {KeySignatureEvent}; 74 | -------------------------------------------------------------------------------- /src/meta-events/lyric-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a lyric meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {LyricEvent} 9 | */ 10 | class LyricEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x05; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'LyricEvent'; 20 | this.text = fields.text; 21 | this.type = 0x05; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {LyricEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/marker-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a marker meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {MarkerEvent} 9 | */ 10 | class MarkerEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x06; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'MarkerEvent'; 20 | this.text = fields.text; 21 | this.type = 0x06; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {MarkerEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/meta-event.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEvent } from "../abstract-event"; 2 | 3 | interface MetaEvent extends AbstractEvent { 4 | type: number|number[]; 5 | } 6 | 7 | export {MetaEvent}; 8 | -------------------------------------------------------------------------------- /src/meta-events/tempo-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a tempo meta event. 7 | * @param {object} fields {bpm: integer, delta: integer} 8 | * @return {TempoEvent} 9 | */ 10 | class TempoEvent implements MetaEvent { 11 | bpm: number; 12 | data: number[]; 13 | delta: number; 14 | name: string; 15 | tick: number; 16 | type: 0x51; 17 | 18 | constructor(fields: { bpm: number; tick?: number; delta?: number; }) { 19 | this.bpm = fields.bpm; 20 | this.delta = fields.delta || 0x00; 21 | this.tick = fields.tick; 22 | this.name = 'TempoEvent'; 23 | this.type = 0x51; 24 | 25 | const tempo = Math.round(60000000 / this.bpm); 26 | 27 | // Start with zero time delta 28 | this.data = Utils.numberToVariableLength(this.delta).concat( 29 | Constants.META_EVENT_ID, 30 | this.type, 31 | [0x03], // Size 32 | Utils.numberToBytes(tempo, 3), // Tempo, 3 bytes 33 | ); 34 | } 35 | } 36 | 37 | export {TempoEvent}; 38 | -------------------------------------------------------------------------------- /src/meta-events/text-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a tempo meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {TextEvent} 9 | */ 10 | class TextEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x01; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.text = fields.text; 20 | this.name = 'TextEvent'; 21 | this.type = 0x01; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(fields.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {TextEvent}; 36 | -------------------------------------------------------------------------------- /src/meta-events/time-signature-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a time signature meta event. 7 | * @return {TimeSignatureEvent} 8 | */ 9 | class TimeSignatureEvent implements MetaEvent { 10 | data: number[]; 11 | delta: number; 12 | name: string; 13 | type: 0x58; 14 | 15 | constructor(numerator, denominator, midiclockspertick, notespermidiclock) { 16 | this.name = 'TimeSignatureEvent'; 17 | this.type = 0x58; 18 | 19 | // Start with zero time delta 20 | this.data = Utils.numberToVariableLength(0x00).concat( 21 | Constants.META_EVENT_ID, 22 | this.type, 23 | [0x04], // Size 24 | Utils.numberToBytes(numerator, 1), // Numerator, 1 bytes 25 | Utils.numberToBytes(Math.log2(denominator), 1), // Denominator is expressed as pow of 2, 1 bytes 26 | Utils.numberToBytes(midiclockspertick || 24, 1), // MIDI Clocks per tick, 1 bytes 27 | Utils.numberToBytes(notespermidiclock || 8, 1), // Number of 1/32 notes per MIDI clocks, 1 bytes 28 | ); 29 | } 30 | } 31 | 32 | export {TimeSignatureEvent}; 33 | -------------------------------------------------------------------------------- /src/meta-events/track-name-event.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from '../constants'; 2 | import {MetaEvent} from './meta-event'; 3 | import {Utils} from '../utils'; 4 | 5 | /** 6 | * Object representation of a tempo meta event. 7 | * @param {object} fields {text: string, delta: integer} 8 | * @return {TrackNameEvent} 9 | */ 10 | class TrackNameEvent implements MetaEvent { 11 | data: number[]; 12 | delta: number; 13 | name: string; 14 | text: string; 15 | type: 0x03; 16 | 17 | constructor(fields: { text: string; delta?: number; }) { 18 | this.delta = fields.delta || 0x00; 19 | this.name = 'TrackNameEvent'; 20 | this.text = fields.text; 21 | this.type = 0x03; 22 | 23 | const textBytes = Utils.stringToBytes(this.text); 24 | 25 | // Start with zero time delta 26 | this.data = Utils.numberToVariableLength(this.delta).concat( 27 | Constants.META_EVENT_ID, 28 | this.type, 29 | Utils.numberToVariableLength(textBytes.length), // Size 30 | textBytes, // Text 31 | ); 32 | } 33 | } 34 | 35 | export {TrackNameEvent}; 36 | -------------------------------------------------------------------------------- /src/midi-events/controller-change-event.ts: -------------------------------------------------------------------------------- 1 | import {MidiEvent} from './midi-event'; 2 | import {Utils} from '../utils'; 3 | 4 | /** 5 | * Holds all data for a "controller change" MIDI event 6 | * @param {object} fields {controllerNumber: integer, controllerValue: integer, delta: integer} 7 | * @return {ControllerChangeEvent} 8 | */ 9 | class ControllerChangeEvent implements MidiEvent { 10 | channel: number; 11 | controllerNumber: number; 12 | controllerValue: number; 13 | data: number[]; 14 | delta: number; 15 | name: string; 16 | status: 0xB0; 17 | 18 | constructor(fields: { controllerNumber: number; controllerValue: number; channel?: number; delta?: number; }) { 19 | this.channel = fields.channel - 1 || 0; 20 | this.controllerValue = fields.controllerValue; 21 | this.controllerNumber = fields.controllerNumber; 22 | this.delta = fields.delta || 0x00; 23 | this.name = 'ControllerChangeEvent'; 24 | this.status = 0xB0; 25 | 26 | this.data = Utils.numberToVariableLength(fields.delta).concat(this.status | this.channel, this.controllerNumber, this.controllerValue); 27 | } 28 | } 29 | 30 | export {ControllerChangeEvent}; 31 | -------------------------------------------------------------------------------- /src/midi-events/midi-event.ts: -------------------------------------------------------------------------------- 1 | import {AbstractEvent} from '../abstract-event'; 2 | 3 | 4 | interface MidiEvent extends AbstractEvent { 5 | channel: number; 6 | status: number; 7 | // eslint-disable-next-line @typescript-eslint/ban-types 8 | buildData?: Function; 9 | } 10 | 11 | export {MidiEvent}; 12 | -------------------------------------------------------------------------------- /src/midi-events/note-event.ts: -------------------------------------------------------------------------------- 1 | import {AbstractEvent} from '../abstract-event'; 2 | import {MidiEvent} from './midi-event'; 3 | import {NoteOnEvent} from './note-on-event'; 4 | import {NoteOffEvent} from './note-off-event'; 5 | import {Utils} from '../utils'; 6 | 7 | /** 8 | * Wrapper for noteOnEvent/noteOffEvent objects that builds both events. 9 | * @param {object} fields - {pitch: '[C4]', duration: '4', wait: '4', velocity: 1-100} 10 | * @return {NoteEvent} 11 | */ 12 | class NoteEvent implements AbstractEvent { 13 | data: number[]; 14 | delta: number; 15 | events: MidiEvent[]; 16 | name: string; 17 | pitch: string[]; 18 | grace: string|string[]; 19 | channel: number; 20 | repeat: number; 21 | tick: number; 22 | duration: string; 23 | sequential: boolean; 24 | wait: string; 25 | velocity: number; 26 | tickDuration: number; 27 | restDuration: number; 28 | 29 | constructor(fields) { 30 | this.data = []; 31 | this.name = 'NoteEvent'; 32 | this.pitch = Utils.toArray(fields.pitch); 33 | 34 | this.channel = fields.channel || 1; 35 | this.duration = fields.duration || '4'; 36 | this.grace = fields.grace; 37 | this.repeat = fields.repeat || 1; 38 | this.sequential = fields.sequential || false; 39 | this.tick = fields.startTick || fields.tick || null; 40 | this.velocity = fields.velocity || 50; 41 | this.wait = fields.wait || 0; 42 | 43 | this.tickDuration = Utils.getTickDuration(this.duration); 44 | this.restDuration = Utils.getTickDuration(this.wait); 45 | 46 | this.events = []; // Hold actual NoteOn/NoteOff events 47 | } 48 | 49 | /** 50 | * Builds int array for this event. 51 | * @return {NoteEvent} 52 | */ 53 | buildData(): NoteEvent { 54 | // Reset data array 55 | this.data = []; 56 | 57 | // Apply grace note(s) and subtract ticks (currently 1 tick per grace note) from tickDuration so net value is the same 58 | if (this.grace) { 59 | const graceDuration = 1; 60 | this.grace = Utils.toArray(this.grace); 61 | this.grace.forEach(() => { 62 | const noteEvent = new NoteEvent({pitch: this.grace, duration:'T' + graceDuration}); 63 | this.data = this.data.concat(noteEvent.data); 64 | }); 65 | } 66 | 67 | // fields.pitch could be an array of pitches. 68 | // If so create note events for each and apply the same duration. 69 | 70 | // By default this is a chord if it's an array of notes that requires one NoteOnEvent. 71 | // If this.sequential === true then it's a sequential string of notes that requires separate NoteOnEvents. 72 | if ( ! this.sequential) { 73 | // Handle repeat 74 | for (let j = 0; j < this.repeat; j++) { 75 | // Note on 76 | this.pitch.forEach((p, i) => { 77 | let noteOnNew; 78 | 79 | if (i == 0) { 80 | noteOnNew = new NoteOnEvent({ 81 | channel: this.channel, 82 | wait: this.wait, 83 | delta: Utils.getTickDuration(this.wait), 84 | velocity: this.velocity, 85 | pitch: p, 86 | tick: this.tick, 87 | }); 88 | 89 | } else { 90 | // Running status (can ommit the note on status) 91 | //noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]}); 92 | noteOnNew = new NoteOnEvent({ 93 | channel: this.channel, 94 | wait: 0, 95 | delta: 0, 96 | velocity: this.velocity, 97 | pitch: p, 98 | tick: this.tick, 99 | }); 100 | } 101 | 102 | this.events.push(noteOnNew); 103 | }); 104 | 105 | // Note off 106 | this.pitch.forEach((p, i) => { 107 | let noteOffNew; 108 | 109 | if (i == 0) { 110 | //noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))}); 111 | noteOffNew = new NoteOffEvent({ 112 | channel: this.channel, 113 | duration: this.duration, 114 | velocity: this.velocity, 115 | pitch: p, 116 | tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null, 117 | }); 118 | 119 | } else { 120 | // Running status (can omit the note off status) 121 | //noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]}); 122 | noteOffNew = new NoteOffEvent({ 123 | channel: this.channel, 124 | duration: 0, 125 | velocity: this.velocity, 126 | pitch: p, 127 | tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null, 128 | }); 129 | } 130 | 131 | this.events.push(noteOffNew); 132 | }); 133 | } 134 | 135 | } else { 136 | // Handle repeat 137 | for (let j = 0; j < this.repeat; j++) { 138 | this.pitch.forEach((p, i) => { 139 | const noteOnNew = new NoteOnEvent({ 140 | channel: this.channel, 141 | wait: (i > 0 ? 0 : this.wait), // wait only applies to first note in repetition 142 | delta: (i > 0 ? 0 : Utils.getTickDuration(this.wait)), 143 | velocity: this.velocity, 144 | pitch: p, 145 | tick: this.tick, 146 | }); 147 | 148 | const noteOffNew = new NoteOffEvent({ 149 | channel: this.channel, 150 | duration: this.duration, 151 | velocity: this.velocity, 152 | pitch: p, 153 | }); 154 | 155 | this.events.push(noteOnNew, noteOffNew); 156 | }); 157 | } 158 | } 159 | 160 | return this; 161 | } 162 | } 163 | 164 | export {NoteEvent}; 165 | -------------------------------------------------------------------------------- /src/midi-events/note-off-event.ts: -------------------------------------------------------------------------------- 1 | import {MidiEvent} from './midi-event'; 2 | import {Utils} from '../utils'; 3 | 4 | /** 5 | * Holds all data for a "note off" MIDI event 6 | * @param {object} fields {data: []} 7 | * @return {NoteOffEvent} 8 | */ 9 | class NoteOffEvent implements MidiEvent { 10 | channel: number; 11 | data: number[]; 12 | delta: number; 13 | deltaWithPrecisionCorrection: number; 14 | status: 0x80; 15 | name: string; 16 | velocity: number; 17 | pitch: string|number; 18 | duration: string|number; 19 | tick: number; 20 | 21 | constructor(fields: { channel: number; duration: string|number; velocity: number; pitch: string|number; tick?: number; data?: number[]; delta?: number }) { 22 | this.name = 'NoteOffEvent'; 23 | this.channel = fields.channel || 1; 24 | this.pitch = fields.pitch; 25 | this.velocity = fields.velocity || 50; 26 | this.tick = fields.tick || null; 27 | this.data = fields.data; 28 | this.delta = fields.delta || Utils.getTickDuration(fields.duration); 29 | this.status = 0x80; 30 | } 31 | 32 | /** 33 | * Builds int array for this event. 34 | * @param {Track} track - parent track 35 | * @return {NoteOffEvent} 36 | */ 37 | buildData(track, precisionDelta: number, options: {middleC?: string} = {}) { 38 | if (this.tick === null) { 39 | this.tick = Utils.getRoundedIfClose(this.delta + track.tickPointer); 40 | } 41 | 42 | this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta); 43 | 44 | this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection) 45 | .concat( 46 | this.status | this.channel - 1, 47 | Utils.getPitch(this.pitch, options.middleC), 48 | Utils.convertVelocity(this.velocity) 49 | ); 50 | 51 | return this; 52 | } 53 | } 54 | 55 | export {NoteOffEvent}; 56 | -------------------------------------------------------------------------------- /src/midi-events/note-on-event.ts: -------------------------------------------------------------------------------- 1 | import {MidiEvent} from './midi-event'; 2 | import {Utils} from '../utils'; 3 | 4 | /** 5 | * Holds all data for a "note on" MIDI event 6 | * @param {object} fields {data: []} 7 | * @return {NoteOnEvent} 8 | */ 9 | class NoteOnEvent implements MidiEvent { 10 | channel: number; 11 | data: number[]; 12 | delta: number; 13 | status: 0x90; 14 | name: string; 15 | pitch: string|string[]|number|number[]; 16 | velocity: number; 17 | wait: string|number; 18 | tick: number; 19 | deltaWithPrecisionCorrection: number; 20 | 21 | constructor(fields: { channel?: number; wait?: string|number; velocity?: number; pitch?: string|string[]|number|number[]; tick?: number; data?: number[]; delta?: number }) { 22 | this.name = 'NoteOnEvent'; 23 | this.channel = fields.channel || 1; 24 | this.pitch = fields.pitch; 25 | this.wait = fields.wait || 0; 26 | this.velocity = fields.velocity || 50; 27 | 28 | this.tick = fields.tick || null; 29 | this.delta = null; 30 | this.data = fields.data; 31 | this.status = 0x90; 32 | } 33 | 34 | /** 35 | * Builds int array for this event. 36 | * @param {Track} track - parent track 37 | * @return {NoteOnEvent} 38 | */ 39 | buildData(track, precisionDelta, options: {middleC?: string} = {}) { 40 | this.data = []; 41 | 42 | // Explicitly defined startTick event 43 | if (this.tick) { 44 | this.tick = Utils.getRoundedIfClose(this.tick); 45 | 46 | // If this is the first event in the track then use event's starting tick as delta. 47 | if (track.tickPointer == 0) { 48 | this.delta = this.tick; 49 | } 50 | 51 | } else { 52 | this.delta = Utils.getTickDuration(this.wait); 53 | this.tick = Utils.getRoundedIfClose(track.tickPointer + this.delta); 54 | } 55 | 56 | this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta); 57 | 58 | this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection) 59 | .concat( 60 | this.status | this.channel - 1, 61 | Utils.getPitch(this.pitch, options.middleC), 62 | Utils.convertVelocity(this.velocity) 63 | ); 64 | 65 | return this; 66 | } 67 | } 68 | 69 | export {NoteOnEvent}; 70 | -------------------------------------------------------------------------------- /src/midi-events/pitch-bend-event.ts: -------------------------------------------------------------------------------- 1 | import {MidiEvent} from './midi-event'; 2 | import {Utils} from '../utils'; 3 | 4 | 5 | /** 6 | * Holds all data for a "Pitch Bend" MIDI event 7 | * [ -1.0, 0, 1.0 ] -> [ 0, 8192, 16383] 8 | * @param {object} fields { bend : float, channel : int, delta: int } 9 | * @return {PitchBendEvent} 10 | */ 11 | class PitchBendEvent implements MidiEvent { 12 | channel: number; 13 | data: number[]; 14 | delta: number; 15 | name: string; 16 | status: 0xE0; 17 | 18 | constructor(fields) { 19 | this.channel = fields.channel || 0; 20 | this.delta = fields.delta || 0x00; 21 | this.name = 'PitchBendEvent'; 22 | this.status = 0xE0; 23 | 24 | const bend14 = this.scale14bits(fields.bend); 25 | 26 | const lsbValue = bend14 & 0x7f; 27 | const msbValue = ( bend14 >> 7 ) & 0x7f; 28 | this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, lsbValue, msbValue); 29 | } 30 | 31 | scale14bits(zeroOne) { 32 | if ( zeroOne <= 0 ) { 33 | return Math.floor( 16384 * ( zeroOne + 1 ) / 2 ); 34 | } 35 | 36 | return Math.floor( 16383 * ( zeroOne + 1 ) / 2 ); 37 | } 38 | } 39 | 40 | export {PitchBendEvent}; 41 | -------------------------------------------------------------------------------- /src/midi-events/program-change-event.ts: -------------------------------------------------------------------------------- 1 | import {MidiEvent} from './midi-event'; 2 | import {Utils} from '../utils'; 3 | 4 | /** 5 | * Holds all data for a "program change" MIDI event 6 | * @param {object} fields {instrument: integer, delta: integer} 7 | * @return {ProgramChangeEvent} 8 | */ 9 | class ProgramChangeEvent implements MidiEvent { 10 | channel: number; 11 | data: number[]; 12 | delta: number; 13 | status: 0xC0; 14 | name: string; 15 | instrument: number; 16 | 17 | constructor(fields: { channel?: number; delta?: number; instrument: number; }) { 18 | this.channel = fields.channel || 0; 19 | this.delta = fields.delta || 0x00; 20 | this.instrument = fields.instrument; 21 | this.status = 0xC0; 22 | this.name = 'ProgramChangeEvent'; 23 | // delta time defaults to 0. 24 | this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, this.instrument); 25 | } 26 | } 27 | 28 | export {ProgramChangeEvent}; 29 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {Constants} from './constants'; 2 | import {toMidi} from '@tonaljs/midi'; 3 | 4 | /** 5 | * Static utility functions used throughout the library. 6 | */ 7 | class Utils { 8 | 9 | /** 10 | * Gets MidiWriterJS version number. 11 | * @return {string} 12 | */ 13 | static version(): string { 14 | return Constants.VERSION; 15 | } 16 | 17 | /** 18 | * Convert a string to an array of bytes 19 | * @param {string} string 20 | * @return {array} 21 | */ 22 | static stringToBytes(string: string): number[] { 23 | return string.split('').map(char => char.charCodeAt(0)) 24 | } 25 | 26 | /** 27 | * Checks if argument is a valid number. 28 | * @param {*} n - Value to check 29 | * @return {boolean} 30 | */ 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | static isNumeric(n: any): boolean { 33 | return !isNaN(parseFloat(n)) && isFinite(n) 34 | } 35 | 36 | /** 37 | * Returns the correct MIDI number for the specified pitch. 38 | * Uses Tonal Midi - https://github.com/danigb/tonal/tree/master/packages/midi 39 | * @param {(string|number)} pitch - 'C#4' or midi note code 40 | * @param {string} middleC 41 | * @return {number} 42 | */ 43 | static getPitch(pitch: any, middleC = 'C4'): number { 44 | return 60 - toMidi(middleC) + toMidi(pitch); 45 | } 46 | 47 | /** 48 | * Translates number of ticks to MIDI timestamp format, returning an array of 49 | * hex strings with the time values. Midi has a very particular time to express time, 50 | * take a good look at the spec before ever touching this function. 51 | * Thanks to https://github.com/sergi/jsmidi 52 | * 53 | * @param {number} ticks - Number of ticks to be translated 54 | * @return {array} - Bytes that form the MIDI time value 55 | */ 56 | static numberToVariableLength(ticks: number): number[] { 57 | ticks = Math.round(ticks); 58 | let buffer = ticks & 0x7F; 59 | 60 | // eslint-disable-next-line no-cond-assign 61 | while (ticks = ticks >> 7) { 62 | buffer <<= 8; 63 | buffer |= ((ticks & 0x7F) | 0x80); 64 | } 65 | 66 | const bList = []; 67 | // eslint-disable-next-line no-constant-condition 68 | while (true) { 69 | bList.push(buffer & 0xff); 70 | 71 | if (buffer & 0x80) buffer >>= 8 72 | else { break; } 73 | } 74 | 75 | return bList; 76 | } 77 | 78 | /** 79 | * Counts number of bytes in string 80 | * @param {string} s 81 | * @return {number} 82 | */ 83 | static stringByteCount(s: string): number { 84 | return encodeURI(s).split(/%..|./).length - 1 85 | } 86 | 87 | /** 88 | * Get an int from an array of bytes. 89 | * @param {array} bytes 90 | * @return {number} 91 | */ 92 | static numberFromBytes(bytes: number[]): number { 93 | let hex = ''; 94 | let stringResult; 95 | 96 | bytes.forEach((byte) => { 97 | stringResult = byte.toString(16); 98 | 99 | // ensure string is 2 chars 100 | if (stringResult.length == 1) stringResult = "0" + stringResult 101 | 102 | hex += stringResult; 103 | }); 104 | 105 | return parseInt(hex, 16); 106 | } 107 | 108 | /** 109 | * Takes a number and splits it up into an array of bytes. Can be padded by passing a number to bytesNeeded 110 | * @param {number} number 111 | * @param {number} bytesNeeded 112 | * @return {array} - Array of bytes 113 | */ 114 | static numberToBytes(number: number, bytesNeeded: number): number[] { 115 | bytesNeeded = bytesNeeded || 1; 116 | 117 | let hexString = number.toString(16); 118 | 119 | if (hexString.length & 1) { // Make sure hex string is even number of chars 120 | hexString = '0' + hexString; 121 | } 122 | 123 | // Split hex string into an array of two char elements 124 | const hexArray = hexString.match(/.{2}/g); 125 | 126 | // Now parse them out as integers 127 | const intArray = hexArray.map(item => parseInt(item, 16)) 128 | 129 | // Prepend empty bytes if we don't have enough 130 | if (intArray.length < bytesNeeded) { 131 | while (bytesNeeded - intArray.length > 0) { 132 | intArray.unshift(0); 133 | } 134 | } 135 | 136 | return intArray; 137 | } 138 | 139 | /** 140 | * Converts value to array if needed. 141 | * @param {any} value 142 | * @return {array} 143 | */ 144 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 145 | static toArray(value: any): any[] { 146 | if (Array.isArray(value)) return value; 147 | return [value]; 148 | } 149 | 150 | /** 151 | * Converts velocity to value 0-127 152 | * @param {number} velocity - Velocity value 1-100 153 | * @return {number} 154 | */ 155 | static convertVelocity(velocity: number): number { 156 | // Max passed value limited to 100 157 | velocity = velocity > 100 ? 100 : velocity; 158 | return Math.round(velocity / 100 * 127); 159 | } 160 | 161 | /** 162 | * Gets the total number of ticks of a specified duration. 163 | * Note: type=='note' defaults to quarter note, type==='rest' defaults to 0 164 | * @param {(string|array)} duration 165 | * @return {number} 166 | */ 167 | static getTickDuration(duration: (string | string[] | number)): number { 168 | if (Array.isArray(duration)) { 169 | // Recursively execute this method for each item in the array and return the sum of tick durations. 170 | return duration.map((value) => { 171 | return Utils.getTickDuration(value); 172 | }).reduce((a, b) => { 173 | return a + b; 174 | }, 0); 175 | } 176 | 177 | duration = duration.toString(); 178 | 179 | if (duration.toLowerCase().charAt(0) === 't') { 180 | // If duration starts with 't' then the number that follows is an explicit tick count 181 | const ticks = parseInt(duration.substring(1)); 182 | 183 | if (isNaN(ticks) || ticks < 0) { 184 | throw new Error(duration + ' is not a valid duration.'); 185 | } 186 | 187 | return ticks; 188 | } 189 | 190 | // Need to apply duration here. Quarter note == Constants.HEADER_CHUNK_DIVISION 191 | const quarterTicks = Utils.numberFromBytes(Constants.HEADER_CHUNK_DIVISION); 192 | const tickDuration = quarterTicks * Utils.getDurationMultiplier(duration); 193 | return Utils.getRoundedIfClose(tickDuration) 194 | } 195 | 196 | /** 197 | * Due to rounding errors in JavaScript engines, 198 | * it's safe to round when we're very close to the actual tick number 199 | * 200 | * @static 201 | * @param {number} tick 202 | * @return {number} 203 | */ 204 | static getRoundedIfClose(tick: number): number { 205 | const roundedTick = Math.round(tick); 206 | return Math.abs(roundedTick - tick) < 0.000001 ? roundedTick : tick; 207 | } 208 | 209 | /** 210 | * Due to low precision of MIDI, 211 | * we need to keep track of rounding errors in deltas. 212 | * This function will calculate the rounding error for a given duration. 213 | * 214 | * @static 215 | * @param {number} tick 216 | * @return {number} 217 | */ 218 | static getPrecisionLoss(tick: number): number { 219 | const roundedTick = Math.round(tick); 220 | return roundedTick - tick; 221 | } 222 | 223 | /** 224 | * Gets what to multiple ticks/quarter note by to get the specified duration. 225 | * Note: type=='note' defaults to quarter note, type==='rest' defaults to 0 226 | * @param {string} duration 227 | * @return {number} 228 | */ 229 | static getDurationMultiplier(duration: string): number { 230 | // Need to apply duration here. 231 | // Quarter note == Constants.HEADER_CHUNK_DIVISION ticks. 232 | 233 | if (duration === '0') return 0; 234 | 235 | const match = duration.match(/^(?d+)?(?\d+)(?:t(?\d*))?/); 236 | if (match) { 237 | const base = Number(match.groups.base); 238 | // 1 or any power of two: 239 | const isValidBase = base === 1 || ((base & (base - 1)) === 0); 240 | if (isValidBase) { 241 | // how much faster or slower is this note compared to a quarter? 242 | const ratio = base / 4; 243 | let durationInQuarters = 1 / ratio; 244 | const {dotted, tuplet} = match.groups; 245 | if (dotted) { 246 | const thisManyDots = dotted.length; 247 | const divisor = Math.pow(2, thisManyDots); 248 | durationInQuarters = durationInQuarters + (durationInQuarters * ((divisor - 1) / divisor)); 249 | } 250 | if (typeof tuplet === 'string') { 251 | const fitInto = durationInQuarters * 2; 252 | // default to triplet: 253 | const thisManyNotes = Number(tuplet || '3'); 254 | durationInQuarters = fitInto / thisManyNotes; 255 | } 256 | return durationInQuarters 257 | } 258 | } 259 | throw new Error(duration + ' is not a valid duration.'); 260 | } 261 | } 262 | 263 | export {Utils}; 264 | -------------------------------------------------------------------------------- /src/vexflow.ts: -------------------------------------------------------------------------------- 1 | import {NoteEvent} from './midi-events/note-event'; 2 | import {Track} from './chunks/track'; 3 | 4 | class VexFlow { 5 | 6 | /** 7 | * Support for converting VexFlow voice into MidiWriterJS track 8 | * @return MidiWriter.Track object 9 | */ 10 | trackFromVoice(voice, options = {addRenderedAccidentals: false}): Track { 11 | const track = new Track; 12 | let wait = []; 13 | 14 | voice.tickables.forEach(tickable => { 15 | if (tickable.noteType === 'n') { 16 | track.addEvent(new NoteEvent({ 17 | pitch: tickable.keys.map((pitch, index) => this.convertPitch(pitch, index, tickable, options.addRenderedAccidentals)), 18 | duration: this.convertDuration(tickable), 19 | wait 20 | })); 21 | // reset wait 22 | wait = []; 23 | } else if (tickable.noteType === 'r') { 24 | // move on to the next tickable and add this to the stack 25 | // of the `wait` property for the next note event 26 | wait.push(this.convertDuration(tickable)); 27 | } 28 | }); 29 | 30 | // There may be outstanding rests at the end of the track, 31 | // pad with a ghost note (zero duration and velocity), just to capture the wait. 32 | if(wait.length > 0) { 33 | track.addEvent(new NoteEvent({pitch: '[c4]', duration: '0', wait, velocity: '0'})); 34 | } 35 | 36 | return track; 37 | } 38 | 39 | /** 40 | * Converts VexFlow pitch syntax to MidiWriterJS syntax 41 | * @param pitch string 42 | * @param index pitch index 43 | * @param note struct from Vexflow 44 | * @param addRenderedAccidentals adds Vexflow rendered accidentals 45 | */ 46 | convertPitch(pitch, index, note, addRenderedAccidentals = false) { 47 | // Splits note name from octave 48 | const pitchParts = pitch.split('/'); 49 | 50 | // Retrieves accidentals from pitch 51 | // Removes natural accidentals since they are not accepted in Tonal Midi 52 | let accidentals = pitchParts[0].substring(1).replace('n', ''); 53 | 54 | if (addRenderedAccidentals) { 55 | note.getAccidentals()?.forEach(accidental => { 56 | if (accidental.index === index) { 57 | if (accidental.type === 'n') { 58 | accidentals = ''; 59 | } else { 60 | accidentals += accidental.type; 61 | } 62 | } 63 | }); 64 | } 65 | 66 | return pitchParts[0][0] + accidentals + pitchParts[1]; 67 | } 68 | 69 | /** 70 | * Converts VexFlow duration syntax to MidiWriterJS syntax 71 | * @param note struct from VexFlow 72 | */ 73 | convertDuration(note) { 74 | return 'd'.repeat(note.dots) + this.convertBaseDuration(note.duration) + (note.tuplet ? 't' + note.tuplet.num_notes : ''); 75 | } 76 | 77 | /** 78 | * Converts VexFlow base duration syntax to MidiWriterJS syntax 79 | * @param duration Vexflow duration 80 | * @returns MidiWriterJS duration 81 | */ 82 | convertBaseDuration(duration: string): string { 83 | switch (duration) { 84 | case 'w': 85 | return '1'; 86 | case 'h': 87 | return '2'; 88 | case 'q': 89 | return '4'; 90 | default: 91 | return duration; 92 | } 93 | } 94 | } 95 | 96 | export {VexFlow}; 97 | -------------------------------------------------------------------------------- /src/writer.ts: -------------------------------------------------------------------------------- 1 | import {Header} from './chunks/header'; 2 | import {Track} from './chunks/track'; 3 | import {Utils} from './utils'; 4 | 5 | /** 6 | * Object that puts together tracks and provides methods for file output. 7 | * @param {array|Track} tracks - A single {Track} object or an array of {Track} objects. 8 | * @param {object} options - {middleC: 'C4'} 9 | * @return {Writer} 10 | */ 11 | class Writer { 12 | tracks: Track[]; 13 | options: object; 14 | 15 | constructor(tracks, options = {}) { 16 | // Ensure tracks is an array 17 | this.tracks = Utils.toArray(tracks); 18 | this.options = options; 19 | } 20 | 21 | /** 22 | * Builds array of data from chunkschunks. 23 | * @return {array} 24 | */ 25 | buildData() { 26 | const data = []; 27 | data.push(new Header(this.tracks.length)) 28 | 29 | // For each track add final end of track event and build data 30 | this.tracks.forEach((track) => { 31 | data.push(track.buildData(this.options)); 32 | }); 33 | 34 | return data; 35 | } 36 | 37 | /** 38 | * Builds the file into a Uint8Array 39 | * @return {Uint8Array} 40 | */ 41 | buildFile(): Uint8Array { 42 | let build = []; 43 | 44 | // Data consists of chunks which consists of data 45 | this.buildData().forEach((d) => build = build.concat(d.type, d.size, d.data)); 46 | 47 | return new Uint8Array(build); 48 | } 49 | 50 | /** 51 | * Convert file buffer to a base64 string. Different methods depending on if browser or node. 52 | * @return {string} 53 | */ 54 | base64(): string { 55 | if (typeof btoa === 'function') { 56 | let binary = ''; 57 | const bytes = this.buildFile(); 58 | const len = bytes.byteLength; 59 | 60 | for (let i = 0; i < len; i++) { 61 | binary += String.fromCharCode(bytes[i]); 62 | } 63 | 64 | return btoa(binary); 65 | } 66 | 67 | return Buffer.from(this.buildFile()).toString('base64'); 68 | } 69 | 70 | /** 71 | * Get the data URI. 72 | * @return {string} 73 | */ 74 | dataUri(): string { 75 | return 'data:audio/midi;base64,' + this.base64(); 76 | } 77 | 78 | 79 | /** 80 | * Set option on instantiated Writer. 81 | * @param {string} key 82 | * @param {any} value 83 | * @return {Writer} 84 | */ 85 | setOption(key: string, value: number|string): Writer { 86 | this.options[key] = value; 87 | return this; 88 | } 89 | 90 | /** 91 | * Output to stdout 92 | * @return {string} 93 | */ 94 | stdout() { 95 | return process.stdout.write(Buffer.from(this.buildFile())); 96 | } 97 | } 98 | 99 | export {Writer}; 100 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const MidiWriter = require('..'); 3 | 4 | describe('MidiWriterJS', function() { 5 | describe('#NoteEvent()', function () { 6 | describe('#getTickDuration()', function () { 7 | it('should create a dotted half note if passed three quarter notes.', function () { 8 | const track = new MidiWriter.Track(); // Need to instantiate to build note object 9 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: ['4', '4', '4']}); 10 | track.addEvent(note); 11 | const write = new MidiWriter.Writer(track); 12 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADQCQPECDAIA8QAD/LwA=', write.base64()); 13 | }); 14 | 15 | it('should create a note with duration of 50 ticks if passed T50', function () { 16 | const track = new MidiWriter.Track(); // Need to instantiate to build note object 17 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: 'T50'}); 18 | track.addEvent(note); 19 | const write = new MidiWriter.Writer(track); 20 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADACQPEAygDxAAP8vAA==', write.base64()); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('#Track()', function() { 26 | describe('#Time Signature', function() { 27 | it('should return specific base64 string when time signature is 4/4', function() { 28 | const track = new MidiWriter.Track(); 29 | track.setTimeSignature(4, 4); 30 | const write = new MidiWriter.Writer(track); 31 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADAD/WAQEAhgIAP8vAA==', write.base64()); 32 | }); 33 | 34 | it('should return specific base64 string when time signature is 2/2', function() { 35 | const track = new MidiWriter.Track(); 36 | track.setTimeSignature(2, 2); 37 | const write = new MidiWriter.Writer(track); 38 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADAD/WAQCARgIAP8vAA==', write.base64()); 39 | }); 40 | 41 | it('should return specific base64 string when time signature is 2/8', function() { 42 | const track = new MidiWriter.Track(); 43 | track.setTimeSignature(2, 8); 44 | const write = new MidiWriter.Writer(track); 45 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADAD/WAQCAxgIAP8vAA==', write.base64()); 46 | }); 47 | }); 48 | 49 | it('should return specific base64 string when setting C major key signature', function() { 50 | const track = new MidiWriter.Track(); 51 | track.setKeySignature('C'); 52 | const write = new MidiWriter.Writer(track); 53 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAACgD/WQIAAAD/LwA=', write.base64()); 54 | }); 55 | 56 | it('should return specific base64 string when adding copyright', function() { 57 | const track = new MidiWriter.Track(); 58 | track.addCopyright('2018 Garrett Grimm'); 59 | const write = new MidiWriter.Writer(track); 60 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAGgD/AhIyMDE4IEdhcnJldHQgR3JpbW0A/y8A', write.base64()); 61 | }); 62 | 63 | it('should return specific base64 string when adding text', function() { 64 | const track = new MidiWriter.Track(); 65 | track.addText('MidiWriterJS is the bomb!'); 66 | const write = new MidiWriter.Writer(track); 67 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAIQD/ARlNaWRpV3JpdGVySlMgaXMgdGhlIGJvbWIhAP8vAA==', write.base64()); 68 | }); 69 | 70 | it('should return specific base64 string when adding a track name', function() { 71 | const track = new MidiWriter.Track(); 72 | track.addTrackName('Name of a cool track'); 73 | const write = new MidiWriter.Writer(track); 74 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAHAD/AxROYW1lIG9mIGEgY29vbCB0cmFjawD/LwA=', write.base64()); 75 | }); 76 | 77 | it('should return specific base64 string when adding an instrument name', function() { 78 | const track = new MidiWriter.Track(); 79 | track.addInstrumentName('Alto Saxophone'); 80 | const write = new MidiWriter.Writer(track); 81 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAFgD/BA5BbHRvIFNheG9waG9uZQD/LwA=', write.base64()); 82 | }); 83 | 84 | it('should return specific base64 string when adding a marker', function() { 85 | const track = new MidiWriter.Track(); 86 | track.addMarker('This is my favorite part of the song.'); 87 | const write = new MidiWriter.Writer(track); 88 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAALQD/BiVUaGlzIGlzIG15IGZhdm9yaXRlIHBhcnQgb2YgdGhlIHNvbmcuAP8vAA==', write.base64()); 89 | }); 90 | 91 | it('should return specific base64 string when adding a cue point', function() { 92 | const track = new MidiWriter.Track(); 93 | track.addCuePoint('Here is a cue point.'); 94 | const write = new MidiWriter.Writer(track); 95 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAHAD/BxRIZXJlIGlzIGEgY3VlIHBvaW50LgD/LwA=', write.base64()); 96 | }); 97 | 98 | it('should return specific base64 string when adding a lyric', function() { 99 | const track = new MidiWriter.Track(); 100 | track.addLyric('Oh say can you see.'); 101 | const write = new MidiWriter.Writer(track); 102 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAGwD/BRNPaCBzYXkgY2FuIHlvdSBzZWUuAP8vAA==', write.base64()); 103 | }); 104 | 105 | it('should return specific base64 string when adding a controller change event', function() { 106 | const track = new MidiWriter.Track(); 107 | track.controllerChange(1, 127); 108 | const write = new MidiWriter.Writer(track); 109 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAACACwAX8A/y8A', write.base64()); 110 | }); 111 | 112 | it('should return specific base64 string when adding a controller change event with channel and delta specified', function() { 113 | const track = new MidiWriter.Track(); 114 | track.controllerChange(1, 127, 16, 256); 115 | const write = new MidiWriter.Writer(track); 116 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAACYIAvwF/AP8vAA==', write.base64()); 117 | }); 118 | 119 | it('should create 3 triplet eights followed by a quarter - on correct ticks', function() { 120 | const track = new MidiWriter.Track(); 121 | track.addEvent([ 122 | new MidiWriter.NoteEvent({pitch: ['D5', 'C5', 'B4'], duration: '8t', sequential: true}), 123 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 124 | ]) 125 | const builtTrack = track.buildData(); 126 | assert.equal(builtTrack.events[6].tick, 128) 127 | assert.equal(builtTrack.events[7].tick, 512) 128 | }) 129 | 130 | /* 131 | it('should create 3 triplet eights followed by a whole, after normal 6 eights - on correct ticks', function() { 132 | const track = new MidiWriter.Track(); 133 | track.addEvent([ 134 | new MidiWriter.NoteEvent({pitch: ['G4', 'Gb4', 'C4', 'B3', 'Eb4', 'Gb4'], duration: '8', sequential:true}), 135 | new MidiWriter.NoteEvent({pitch: ['D5', 'C5', 'B4'], duration: '8t', sequential: true}), 136 | new MidiWriter.NoteEvent({pitch: ['B#4'], duration: '1'}), 137 | ]) 138 | const builtTrack = track.buildData(); 139 | 140 | // Reversing these events breaks this test since EndOfTrackEvent gets misplaced. 141 | const lastEvents = [...builtTrack.events].reverse(); 142 | // 2nd bar: 143 | assert.equal(lastEvents[2].tick, 128 * 4); 144 | // 3rd bar: 145 | assert.equal(lastEvents[1].tick, 128 * 8); 146 | }) 147 | */ 148 | 149 | it('should write 14 bars where certain notes should start exactly at the beginning of each bar', function() { 150 | const track = new MidiWriter.Track(); 151 | track 152 | .setTempo(60) 153 | .addEvent([ 154 | new MidiWriter.ProgramChangeEvent({instrument : 1}), 155 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 156 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 157 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 158 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 159 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 160 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 161 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 162 | new MidiWriter.NoteEvent({pitch: ['Bb4'], duration: '4'}), 163 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 164 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: '4'}), 165 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 166 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: '4'}), 167 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 168 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd8'}), 169 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '16'}), 170 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: 'd2'}), 171 | new MidiWriter.NoteEvent({pitch: ['Ab4'], duration: '2'}), 172 | new MidiWriter.NoteEvent({pitch: ['A4', 'B4', 'D5', 'C5', 'E4', 'A4'], duration: '8', sequential:true}), 173 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 174 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '4'}), 175 | new MidiWriter.NoteEvent({pitch: ['Gb4'], duration: 'd2'}), 176 | new MidiWriter.NoteEvent({pitch: ['A4'], duration: '4'}), 177 | new MidiWriter.NoteEvent({pitch: ['G1', 'Gb4', 'C4', 'B3', 'Eb4', 'Gb4'], duration: '8', sequential: true}), 178 | new MidiWriter.NoteEvent({pitch: ['D5', 'C5', 'B5'], duration: '8t', sequential: true}), 179 | // 4x quintuplets 180 | new MidiWriter.NoteEvent({pitch: ['B1', 'A4', 'A4', 'A4', 'A4'], duration: '8t5', sequential: true}), 181 | new MidiWriter.NoteEvent({pitch: ['A4', 'A4', 'A4', 'A4', 'A4'], duration: '8t5', sequential: true}), 182 | new MidiWriter.NoteEvent({pitch: ['A4', 'A4', 'A4', 'A4', 'A4'], duration: '8t5', sequential: true}), 183 | new MidiWriter.NoteEvent({pitch: ['A4', 'A4', 'A4', 'A4', 'A4'], duration: '8t5', sequential: true}), 184 | new MidiWriter.NoteEvent({pitch: ['F1'], duration: '1'}), 185 | ]); 186 | 187 | const builtTrack = track.buildData(); 188 | const twelvethBarEvent = builtTrack.events.find((event) => event.pitch === 'G1') 189 | const thirteenthBarEvent = builtTrack.events.find((event) => event.pitch === 'B1'); 190 | const fourteenthBarEvent = builtTrack.events.find((event) => event.pitch === 'F1'); 191 | // 12th bar: 192 | assert.equal(twelvethBarEvent.tick, 512 * 11); 193 | // 13th bar: 194 | assert.equal(thirteenthBarEvent.tick, 512 * 12); 195 | // 14th bar: 196 | assert.equal(fourteenthBarEvent.tick, 512 * 13); 197 | // console.log(builtTrack.events) 198 | }) 199 | 200 | it('3 triplets and 1 dotted is the duration as one whole - next bar starts in the correct place', function() { 201 | const track = new MidiWriter.Track(); 202 | 203 | track 204 | .addEvent([ 205 | new MidiWriter.ProgramChangeEvent({instrument : 1}), 206 | new MidiWriter.NoteEvent({pitch: ['D5', 'G5', 'B4'], duration: '8t', sequential: true}), 207 | new MidiWriter.NoteEvent({pitch: ['B4'], duration: 'd2'}), 208 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 209 | ]); 210 | 211 | const builtTrack = track.buildData(); 212 | const bar2note = builtTrack.events.find((event) => event.pitch === 'C5'); 213 | 214 | const track2 = new MidiWriter.Track(); 215 | 216 | track2 217 | .addEvent([ 218 | new MidiWriter.ProgramChangeEvent({instrument : 1}), 219 | new MidiWriter.NoteEvent({pitch: ['D5'], duration: '1'}), 220 | new MidiWriter.NoteEvent({pitch: ['C5'], duration: '4'}), 221 | ]); 222 | 223 | const builtTrack2 = track2.buildData(); 224 | const bar2note2 = builtTrack2.events.find((event) => event.pitch === 'C5'); 225 | 226 | assert.equal(JSON.stringify(bar2note, null, 2), JSON.stringify(bar2note2, null, 2)); 227 | }) 228 | 229 | it('should return specific base64 string when manually adding EndTrackEvent', function() { 230 | const track = new MidiWriter.Track(); 231 | 232 | track.addEvent([ 233 | new MidiWriter.NoteEvent({pitch: ['C5', 'E5', 'G4'], duration: '4'}), 234 | new MidiWriter.EndTrackEvent({delta: 5000}), 235 | ]); 236 | 237 | const write = new MidiWriter.Writer(track); 238 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAHgCQSEAAkExAAJBDQIEAgEhAAIBMQACAQ0CnCP8vAA==', write.base64()); 239 | }) 240 | 241 | it('should return specific base64 string when removing events by name', function() { 242 | const track = new MidiWriter.Track(); 243 | 244 | track.addEvent(new MidiWriter.CopyrightEvent({text: "Garrett Grimm"})); 245 | track.removeEventsByName('CopyrightEvent'); 246 | track.addEvent(new MidiWriter.NoteEvent({pitch: ["C4", "D4", "E4"], sequential: true})); 247 | 248 | const write = new MidiWriter.Writer([track]); 249 | 250 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAAHwCQPECBAIA8QACQPkCBAIA+QACQQECBAIBAQAD/LwA=', write.base64()); 251 | }); 252 | 253 | it('should return specific base64 string when merging two tracks', function() { 254 | const track = new MidiWriter.Track(); 255 | 256 | track.addEvent([ 257 | new MidiWriter.NoteEvent({pitch: ['E4','D4'], duration: '4', sequential: true}), 258 | new MidiWriter.NoteEvent({pitch: 'C4', duration: '2'}) 259 | ]); 260 | 261 | const track2 = new MidiWriter.Track(); 262 | 263 | track2.addEvent([ 264 | new MidiWriter.NoteEvent({pitch: ['G4','F4'], duration: '4', sequential: true}), 265 | new MidiWriter.NoteEvent({pitch: 'E4', duration: '2'}) 266 | ]); 267 | 268 | track.mergeTrack(track2); 269 | const write = new MidiWriter.Writer([track]); 270 | 271 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAANwCQQEAAkENAgQCAQEAAkD5AAIBDQACQQUCBAIA+QACQPEAAgEFAAJBAQIIAgDxAAIBAQAD/LwA=', write.base64()); 272 | }); 273 | }); 274 | 275 | describe('#Utils()', function() { 276 | describe('#stringToBytes()', function () { 277 | it('should return [116, 101, 115, 116] when "test" is passed.', function () { 278 | assert.equal([116, 101, 115, 116].toString(), MidiWriter.Utils.stringToBytes('test').toString()); 279 | }); 280 | }); 281 | 282 | describe('#isNumeric()', function () { 283 | it('should return false when "t" is passed.', function () { 284 | assert.equal(false, MidiWriter.Utils.isNumeric('t')); 285 | }); 286 | }); 287 | 288 | describe('#getPitch()', function () { 289 | it('should return 101 when "F7" is passed.', function () { 290 | assert.equal(101, MidiWriter.Utils.getPitch('F7')); 291 | }); 292 | 293 | it('should return 98 when "D6" is passed and middle C is set to "C3".', function () { 294 | assert.equal(98, MidiWriter.Utils.getPitch('D6', 'C3')); 295 | }); 296 | 297 | it('should return 72 (C5) when "B#4" is passed.', function () { 298 | assert.equal(72, MidiWriter.Utils.getPitch('B#4')); 299 | }); 300 | }); 301 | 302 | describe('#stringByteCount()', function () { 303 | it('should return 7 when "Garrett" is passed.', function () { 304 | assert.equal(7, MidiWriter.Utils.stringByteCount('Garrett')); 305 | }); 306 | }); 307 | 308 | describe('#numberFromBytes()', function () { 309 | it('should return 8463 when [0x21, 0x0f] is passed.', function () { 310 | assert.equal(8463, MidiWriter.Utils.numberFromBytes([0x21, 0x0f])); 311 | }); 312 | }); 313 | 314 | describe('#numberToBytes()', function () { 315 | it('should return [0, 5] when converting the number 5 into two bytes.', function() { 316 | assert.equal([0, 5].toString(), MidiWriter.Utils.numberToBytes(5, 2)); 317 | }); 318 | }); 319 | 320 | describe('#getDurationMultiplier()', function () { 321 | it('should return 1 for a quarter note.', function () { 322 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: '4'}); 323 | assert.equal(MidiWriter.Utils.getDurationMultiplier(note.duration), 1); 324 | }); 325 | }); 326 | 327 | describe('#getTickDuration()', function () { 328 | it('should return 128 for a quarter note.', function () { 329 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: '4'}); 330 | assert.equal(MidiWriter.Utils.getTickDuration(note.duration), 128); 331 | }); 332 | it('should return 128 for 3 triplet eights.', function () { 333 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: ['8t', '8t', '8t']}); 334 | assert.equal(MidiWriter.Utils.getTickDuration(note.duration), 128); 335 | }); 336 | }); 337 | 338 | }); 339 | }); 340 | -------------------------------------------------------------------------------- /test/vexflow.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const MidiWriter = require('..'); 3 | 4 | /** 5 | * Mock a VexFlow StaveNote 6 | * @param {String} noteType 7 | * @param {String} duration 8 | * @param {[String]} keys 9 | * @param {boolean} isDotted 10 | */ 11 | function mockNote(noteType='n', duration='8', keys=['c/4'], dots=0, tuplet=null, accidentals=null) { 12 | const result = { 13 | noteType, 14 | duration, 15 | keys, 16 | dots, 17 | tuplet, 18 | getAccidentals() { 19 | return accidentals; 20 | } 21 | }; 22 | return result; 23 | } 24 | 25 | describe('MidiWriterJS', function() { 26 | describe('#VexFlow()', function() { 27 | it('instantiates', function() { 28 | const v = new MidiWriter.VexFlow(); 29 | assert.notStrictEqual(typeof v, 'undefined'); 30 | assert.strictEqual(v instanceof MidiWriter.VexFlow, true); 31 | }); 32 | describe('#trackFromVoice', function() { 33 | it('converts a VexFlow voice into a track', function() { 34 | const mockVoice = { 35 | tickables: [ 36 | mockNote('n', '8', ['g#/3']), 37 | mockNote('n', '8', ['b/3']), 38 | mockNote('n', '8', ['c#/3']), 39 | mockNote('r'), 40 | mockNote('n', '8', ['b/3']), 41 | mockNote('r'), 42 | mockNote('n', '8', ['c#/3']), 43 | mockNote('n', '8', ['b/3']), 44 | mockNote('n', '8', ['a#/3']), 45 | mockNote('r'), 46 | mockNote('n', '8', ['b/3']), 47 | mockNote('r') 48 | ] 49 | }; 50 | const vexFlow = new MidiWriter.VexFlow(); 51 | const track = vexFlow.trackFromVoice(mockVoice); 52 | const write = new MidiWriter.Writer(track); 53 | assert.strictEqual(write.base64(), 'TVRoZAAAAAYAAAABAIBNVHJrAAAATACQOEBAgDhAAJA7QECAO0AAkDFAQIAxQECQO0BAgDtAQJAxQECAMUAAkDtAQIA7QACQOkBAgDpAQJA7QECAO0BAkAAAAIAAAAD/LwA='); 54 | }); 55 | 56 | it('preserves multiple rests', function() { 57 | const mockVoice = { 58 | tickables: [ 59 | mockNote(), 60 | mockNote('r'), 61 | mockNote('r'), 62 | mockNote('') 63 | ] 64 | }; 65 | const vexFlow = new MidiWriter.VexFlow(); 66 | const track = vexFlow.trackFromVoice(mockVoice); 67 | const write = new MidiWriter.Writer(track); 68 | assert.strictEqual(write.base64(), 'TVRoZAAAAAYAAAABAIBNVHJrAAAAFQCQPEBAgDxAgQCQAAAAgAAAAP8vAA=='); 69 | }); 70 | 71 | it('appends trailing rests with a silent note', function() { 72 | const mockVoice = { 73 | tickables: [ 74 | mockNote(), 75 | mockNote(), 76 | mockNote('r'), 77 | mockNote('r') 78 | ] 79 | }; 80 | const vexFlow = new MidiWriter.VexFlow(); 81 | const track = vexFlow.trackFromVoice(mockVoice); 82 | const write = new MidiWriter.Writer(track); 83 | assert.strictEqual(write.base64(), 'TVRoZAAAAAYAAAABAIBNVHJrAAAAHQCQPEBAgDxAAJA8QECAPECBAJAAAACAAAAA/y8A'); 84 | }); 85 | }); 86 | 87 | describe('#convertPitch()', function() { 88 | it('converts pitch', function () { 89 | const vexFlow = new MidiWriter.VexFlow(); 90 | const tickable = mockNote('n', 'h', ['c/4']); 91 | assert.deepStrictEqual(tickable.keys.map((pitch, index) => vexFlow.convertPitch(pitch, index, tickable)), ['c4']); 92 | }); 93 | it('converts multiple pitch', function() { 94 | const vexFlow = new MidiWriter.VexFlow(); 95 | const tickable = mockNote('n', 'h', ['b/4', 'c/4']); 96 | assert.deepStrictEqual(tickable.keys.map((pitch, index) => vexFlow.convertPitch(pitch, index, tickable)), ['b4', 'c4']); 97 | }); 98 | it('converts accidentals pitch', function() { 99 | const vexFlow = new MidiWriter.VexFlow(); 100 | const tickable = mockNote('n', 'h', ['b#/4', 'cb/4', 'dn/4']); 101 | assert.deepStrictEqual(tickable.keys.map((pitch, index) => vexFlow.convertPitch(pitch, index, tickable)), ['b#4', 'cb4', 'd4']); 102 | }); 103 | it('converts rendered accidentals pitch', function() { 104 | const vexFlow = new MidiWriter.VexFlow(); 105 | const tickable = mockNote('n', 'h', ['b/4', 'c/4'], 0, null, [{index: 0, type: '#'}, {index: 1, type: 'b'}]); 106 | assert.deepStrictEqual(tickable.keys.map((pitch, index) => vexFlow.convertPitch(pitch, index, tickable, true)), ['b#4', 'cb4']); 107 | }); 108 | }); 109 | 110 | describe('#convertDuration()', function() { 111 | it('converts whole, half, quarter and eighth durations', function () { 112 | const vexFlow = new MidiWriter.VexFlow(); 113 | const tickable = mockNote('n', 'w'); 114 | assert.strictEqual(vexFlow.convertDuration(tickable), '1'); 115 | tickable.duration = 'h' 116 | assert.strictEqual(vexFlow.convertDuration(tickable), '2'); 117 | tickable.duration = 'q' 118 | assert.strictEqual(vexFlow.convertDuration(tickable), '4'); 119 | tickable.duration = '8' 120 | assert.strictEqual(vexFlow.convertDuration(tickable), '8'); 121 | }); 122 | it('converts dotted half, quarter and eighth durations', function () { 123 | const vexFlow = new MidiWriter.VexFlow(); 124 | const tickable = mockNote('n', 'h', ['c/4'], 1); 125 | assert.strictEqual(vexFlow.convertDuration(tickable), 'd2'); 126 | tickable.duration = 'q' 127 | assert.strictEqual(vexFlow.convertDuration(tickable), 'd4'); 128 | tickable.duration = '8' 129 | assert.strictEqual(vexFlow.convertDuration(tickable), 'd8'); 130 | }); 131 | it('converts multiple dotted durations', function() { 132 | const vexFlow = new MidiWriter.VexFlow(); 133 | const tickable = mockNote('n', 'h', ['c/4'], 2); 134 | assert.strictEqual(vexFlow.convertDuration(tickable), 'dd2'); 135 | tickable.dots = 3 136 | assert.strictEqual(vexFlow.convertDuration(tickable), 'ddd2'); 137 | tickable.dots = 4 138 | assert.strictEqual(vexFlow.convertDuration(tickable), 'dddd2'); 139 | }); 140 | it('converts tuplet durations', function() { 141 | const vexFlow = new MidiWriter.VexFlow(); 142 | const tickable = mockNote('n', 'h', ['c/4'], 0, {num_notes: 3}); 143 | assert.strictEqual(vexFlow.convertDuration(tickable), '2t3'); 144 | tickable.tuplet.num_notes = 5 145 | assert.strictEqual(vexFlow.convertDuration(tickable), '2t5'); 146 | tickable.tuplet.num_notes = 7 147 | assert.strictEqual(vexFlow.convertDuration(tickable), '2t7'); 148 | }); 149 | it('converts dotted tuplet durations', function() { 150 | const vexFlow = new MidiWriter.VexFlow(); 151 | const tickable = mockNote('n', 'h', ['c/4'], 1, {num_notes: 3}); 152 | assert.strictEqual(vexFlow.convertDuration(tickable), 'd2t3'); 153 | tickable.tuplet.num_notes = 5 154 | assert.strictEqual(vexFlow.convertDuration(tickable), 'd2t5'); 155 | tickable.dots = 2 156 | assert.strictEqual(vexFlow.convertDuration(tickable), 'dd2t5'); 157 | }); 158 | it('returns other durations', function () { 159 | const vexFlow = new MidiWriter.VexFlow(); 160 | const tickable = mockNote('n', '64', ['c/4']); 161 | assert.strictEqual(vexFlow.convertDuration(tickable), '64'); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/writer.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const MidiWriter = require('..'); 3 | 4 | describe('MidiWriterJS', function() { 5 | describe('#Writer()', function() { 6 | describe('#base64()', function() { 7 | it('should return specific base64 string when a single C4 quarter note is created.', function () { 8 | const track = new MidiWriter.Track(); 9 | const note = new MidiWriter.NoteEvent({pitch: 'C4', duration: '4'}); 10 | track.addEvent(note); 11 | const write = new MidiWriter.Writer(track); 12 | assert.equal('TVRoZAAAAAYAAAABAIBNVHJrAAAADQCQPECBAIA8QAD/LwA=', write.base64()); 13 | }); 14 | }); 15 | 16 | describe('#Hot Cross Buns', function() { 17 | it('should return specific base64 string when hot cross buns is created.', function () { 18 | const midi = require('../examples/hot-cross-buns.js'); 19 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAAABAIBNVHJrAAAAlQCQQECBAIBAQACQPkCBAIA+QACQPECCAIA8QACQQECBAIBAQACQPkCBAIA+QACQPECCAIA8QACQPEBAgDxAAJA8QECAPEAAkDxAQIA8QACQPEBAgDxAAJA+QECAPkAAkD5AQIA+QACQPkBAgD5AAJA+QECAPkAAkEBAgQCAQEAAkD5AgQCAPkAAkDxAggCAPEAA/y8A', midi.dataUri()); 20 | }); 21 | }); 22 | 23 | describe('#setMiddleC()', function() { 24 | it('should return specific base64 when setting middle C to C3 in Hot Cross Buns example.', function() { 25 | const midi = require('../examples/hot-cross-buns.js'); 26 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAAABAIBNVHJrAAAAlQCQTECBAIBMQACQSkCBAIBKQACQSECCAIBIQACQTECBAIBMQACQSkCBAIBKQACQSECCAIBIQACQSEBAgEhAAJBIQECASEAAkEhAQIBIQACQSEBAgEhAAJBKQECASkAAkEpAQIBKQACQSkBAgEpAAJBKQECASkAAkExAgQCATEAAkEpAgQCASkAAkEhAggCASEAA/y8A', midi.setOption('middleC', 'C3').dataUri()); 27 | }); 28 | }); 29 | 30 | describe('#Maruo Giuliani', function() { 31 | it('should return specific base64 string when Maruo Giuliani example is created.', function () { 32 | const midi = require('../examples/mauro.giuliani-op.47-main-theme.js'); 33 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAQADAIBNVHJrAAAAEwD/WAQDAhgIAP9RAwknwAD/LwBNVHJrAAACFwCQSUAAkExAggCASUAAgExAAJBJQACQTECBAIBJQACATEAAkElAAJBMQIIAgElAAIBMQACQRUAAkElAgQCARUAAgElAAJBHQACQSkCCAIBHQACASkAAkERAAJBMQIEAgERAAIBMQACQRUAAkElAggCARUAAgElAAJBFQIEAgEVAAJBJQACQTECCAIBJQACATEAAkElAAJBMQIEAgElAAIBMQACQSUAAkExAggCASUAAgExAAJBFQACQSUCBAIBFQACASUAAkEdAAJBKQIIAgEdAAIBKQACQREAAkExAgQCAREAAgExAAJBFQACQSUCCAIBFQACASUCBAJBMQACQTECBAIBMQACATEAAkEtAAJBOQIEAgEtAAIBOQACQSkAAkFBAgQCASkAAgFBAAJBJQACQUUCCAIBJQACAUUAAkExAgQCATEAAkExAAJBMQIEAgExAAIBMQACQS0AAkE5AgQCAS0AAgE5AAJBKQACQUECBAIBKQACAUEAAkElAAJBRQIIAgElAAIBRQIEAkElAAJBMQIIAgElAAIBMQACQSUAAkExAgQCASUAAgExAAJBJQACQTECBAIBJQACATEAAkFFAgQCAUUAAkEVAAJBJQIEAgEVAAIBJQACQSUAAkExAgQCASUAAgExAAJBHQACQSkCBAIBHQACASkAAkERAAJBHQIEAgERAAIBHQACQRUCCAIBFQAD/LwBNVHJrAAAAowCQOUCCAIA5QIEAkDlAggCAOUCBAJA0QIIAgDRAgQCQOUCCAIA5QIEAkDlAggCAOUCBAJA5QIIAgDlAgQCQNECCAIA0QIEAkDlAggCAOUCBAJA0QIIAgDRAgQCQOUCCAIA5QIEAkDRAggCANECBAJA5QIIAgDlAgQCQOUCCAIA5QIEAkDlAggCAOUCBAJA0QIIAgDRAgQCQOUCCAIA5QAD/LwA=', midi.dataUri()); 34 | }); 35 | }); 36 | 37 | describe('#Chopin Prelude', function() { 38 | it('should return specific base64 string when chopin prelude is created.', function () { 39 | const midi = require('../examples/chopin-prelude-e-minor.js'); 40 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAQACAIBNVHJrAAAC3gD/UQMPQkAAwAEAkEd/gwCAR38AkEh/gQCASH8AkEd/gwCAR38AkEh/gQCASH8AkEd/gwCAR38AkEh/gQCASH8AkEd/gwCAR38AkEZ/gQCARn8AkEV/gwCARX8AkEd/gQCAR38AkEV/gwCARX8AkEd/gQCAR38AkEV/gwCARX8AkEd/YIBHfwCQRX8ggEV/AJBFf4MAgEV/AJBEf4IAgER/AJBFf0CARX8AkEd/QIBHfwCQSn9AgEp/AJBIf0CASH8AkEB/QIBAfwCQRX9AgEV/AJBCf4MAgEJ/AJBFf4EAgEV/AJBCf4MAgEJ/AJBFf4EAgEV/AJBDf0CAQ38AkEJ/QIBCfwCQPH9AgDx/AJA7f0CAO38AkD9/QIA/fwCQQn9AgEJ/AJBKfyuASn8AkEh/KoBIfwCQR38rgEd/AJBHf4MAgEd/AJBIf4EAgEh/AJBHf4MAgEd/AJBIf4EAgEh/AJBHf4MAgEd/AJBIf4EAgEh/AJBHf2CAR38AkEZ/IIBGfwCQRn+BAIBGfwCQT3+BAIBPfwCQTn9ggE5/AJBMfyCATH8AkEx/QIBMfwCQS39AgEt/AJBUf0CAVH8AkEt/QIBLfwCQS39AgEt/AJBMf0CATH8AkE9/QIBPfwCQR39AgEd/AJBKf0CASn8AkEh/QIBIfwCQTH8rgEx/AJBAfyqAQH8AkEV/K4BFfwCQQn+BQIBCfwCQRX9AgEV/AJBCf4MAgEJ/AJBFf4EAgEV/AJBCf4MAgEJ/AJBCf2CAQn8AkEB/IIBAfwCQQH+DAIBAfwCQQn+BAIBCfwCQQH+DAIBAfwCQQn+BAIBCfwCQQH+CAIBAf4IAkFR/AJBPfwCQSn8AkEh/ggCAVH8AgE9/AIBKfwCASH8AkFR/AJBPfwCQSn8AkEh/ggCAVH8AgE9/AIBKfwCASH8AkFR/AJBPfwCQTH8AkEh/gQCAVH8AgE9/AIBMfwCASH8A/y8ATVRyawAAED4AwAEAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA7QACQN0BAgEBAAIA7QACAN0AAkEBAAJA5QACQNkBAgEBAAIA5QACANkAAkEBAAJA5QACQNkBAgEBAAIA5QACANkAAkEBAAJA5QACQNkBAgEBAAIA5QACANkAAkEBAAJA5QACQNkBAgEBAAIA5QACANkAAkD9AAJA5QACQNkBAgD9AAIA5QACANkAAkD9AAJA5QACQNkBAgD9AAIA5QACANkAAkD9AAJA5QACQNkBAgD9AAIA5QACANkAAkD9AAJA5QACQNkBAgD9AAIA5QACANkAAkD9AAJA5QACQNUBAgD9AAIA5QACANUAAkD9AAJA5QACQNUBAgD9AAIA5QACANUAAkD9AAJA5QACQNUBAgD9AAIA5QACANUAAkD9AAJA5QACQNUBAgD9AAIA5QACANUAAkD5AAJA5QACQNUBAgD5AAIA5QACANUAAkD5AAJA5QACQNUBAgD5AAIA5QACANUAAkD5AAJA4QACQNUBAgD5AAIA4QACANUAAkD5AAJA4QACQNUBAgD5AAIA4QACANUAAkD5AAJA4QACQNEBAgD5AAIA4QACANEAAkD5AAJA4QACQNEBAgD5AAIA4QACANEAAkD5AAJA4QACQNEBAgD5AAIA4QACANEAAkD5AAJA4QACQNEBAgD5AAIA4QACANEAAkD5AAJA3QACQNEBAgD5AAIA3QACANEAAkD5AAJA3QACQNEBAgD5AAIA3QACANEAAkD1AAJA3QACQNEBAgD1AAIA3QACANEAAkD1AAJA3QACQNEBAgD1AAIA3QACANEAAkDxAAJA3QACQNEBAgDxAAIA3QACANEAAkDxAAJA3QACQNEBAgDxAAIA3QACANEAAkDxAAJA3QACQNEBAgDxAAIA3QACANEAAkDxAAJA3QACQNEBAgDxAAIA3QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQNEBAgDxAAIA2QACANEAAkDxAAJA2QACQM0BAgDxAAIA2QACAM0AAkDxAAJA2QACQM0BAgDxAAIA2QACAM0AAkDxAAJA2QACQM0BAgDxAAIA2QACAM0AAkDxAAJA2QACQM0BAgDxAAIA2QACAM0AAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA2QACQMkBAgDxAAIA2QACAMkAAkDxAAJA1QACQMkBAgDxAAIA1QACAMkAAkDxAAJA1QACQMkBAgDxAAIA1QACAMkAAkDxAAJA1QACQMkBAgDxAAIA1QACAMkAAkDxAAJA1QACQMkBAgDxAAIA1QACAMkAAkEdAAJA1QACQMkBAgEdAAIA1QACAMkAAkEdAAJA1QACQMkBAgEdAAIA1QACAMkAAkEdAAJA1QACQMkBAgEdAAIA1QACAMkAAkEdAAJA1QACQMkBAgEdAAIA1QACAMkAAkEdAAJA0QACQMEBAgEdAAIA0QACAMEAAkEdAAJA0QACQMEBAgEdAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQO0BAgEVAAIA0QACAO0AAkEVAAJA0QACQO0BAgEVAAIA0QACAO0AAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJAzQACQO0BAgEVAAIAzQACAO0AAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJA0QACQMEBAgEVAAIA0QACAMEAAkEVAAJAzQACQO0CBAIBFQACAM0AAgDtAgwCQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDtAAJA3QECAQEAAgDtAAIA3QACQQEAAkDlAAJA2QECAQEAAgDlAAIA2QACQQEAAkDlAAJA2QECAQEAAgDlAAIA2QACQQEAAkDlAAJA2QECAQEAAgDlAAIA2QACQQEAAkDlAAJA2QECAQEAAgDlAAIA2QACQP0AAkDlAAJA1QECAP0AAgDlAAIA1QACQP0AAkDlAAJA1QECAP0AAgDlAAIA1QACQP0AAkDlAAJA1QECAP0AAgDlAAIA1QACQP0AAkDlAAJA1QECAP0AAgDlAAIA1QACQP0AAkDhAAJA1QECAP0AAgDhAAIA1QACQP0AAkDhAAJA1QECAP0AAgDhAAIA1QACQPkAAkDhAAJA1QECAPkAAgDhAAIA1QACQPkAAkDhAAJA1QECAPkAAgDhAAIA1QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPkAAkDdAAJA0QECAPkAAgDdAAIA0QACQPUAAkDdAAJA0QECAPUAAgDdAAIA0QACQPUAAkDdAAJA0QECAPUAAgDdAAIA0QACQRkAAkDRAAJAxQECARkAAgDRAAIAxQACQRkAAkDRAAJAxQECARkAAgDRAAIAxQACQRUAAkDRAAJAwQECARUAAgDRAAIAwQACQRUAAkDRAAJAwQECARUAAgDRAAIAwQACQO0AAkC9AQIA7QACAL0AAkFFAAJBOQACQPEAAkEVAQIBRQACATkAAgDxAAIBFQACQUUAAkE5AAJA8QACQRUBAgFFAAIBOQACAPEAAgEVAAJBRQACQTkAAkDxAAJBFQECAUUAAgE5AAIA8QACARUAAkE5AAJBLQACQR0AAkENAQIBOQACAS0AAgEdAAIBDQACQTEAAkEdAAJBDQECATEAAgEdAAIBDQACQTEAAkEdAAJBDQECATEAAgEdAAIBDQACQTEAAkEdAAJBDQECATEAAgEdAAIBDQACQTEAAkDxAAJBFQECATEAAgDxAAIBFQACQTEAAkDxAAJBFQECATEAAgDxAAIBFQACQOUBAgDlAAJA8QACQNkAAkDRAQIA8QACANkAAgDRAAJBHQACQNEAAkDtAQIBHQACANEAAgDtAAJBHQACQNEAAkDtAQIBHQACANEAAgDtAAJBFQACQNEAAkDBAQIBFQACANEAAgDBAAJBFQACQNEAAkDBAQIBFQACANEAAgDBAAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQNEAAkC9AQIA7QACANEAAgC9AAJA7QACQM0AAkDtAQIA7QACAM0AAgDtAAJA7QACQM0AAkDtAQIA7QACAM0AAgDtAAJA5QACQM0AAkDtAQIA5QACAM0AAgDtAAJA5QACQM0AAkDtAQIA5QACAM0AAgDtAAJA3QACQNEAAkDBAQIA3QACANEAAgDBAAJA3QACQNEAAkDBAQIA3QACANEAAgDBAAJA3QACQNEAAkDBAQIA3QACANEAAgDBAAJA3QACQNEAAkDBAQIA3QACANEAAgDBAAJA6QACQNEAAkDBAQIA6QACANEAAgDBAAJA6QACQNEAAkDBAQIA6QACANEAAgDBAAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA5QACQNEAAkDBAQIA5QACANEAAgDBAAJA5QACQNEAAkDtAQIA5QACANEAAgDtAAJA5QACQNEAAkDtAQIA5QACANEAAgDtAAJA4QACQNEAAkDtAQIA4QACANEAAgDtAAJA4QACQNEAAkDtAQIA4QACANEAAgDtAAJA3QACQNEAAkDtAQIA3QACANEAAgDtAAJA3QACQNEAAkDtAQIA3QACANEAAgDtAAJA3QACQNEAAkDtAQIA3QACANEAAgDtAAJA3QACQNEAAkDtAQIA3QACANEAAgDtAAJA3QACQMEAAkDpAggCAN0AAgDBAAIA6QIIAkDtAAJAvQIIAgDtAAIAvQACQO0AAkDZAAJAvQIIAgDtAAIA2QACAL0AAkChAAJAcQIQAgChAAIAcQAD/LwA=', midi.dataUri()); 41 | }); 42 | }); 43 | 44 | describe('#Zelda Theme', function() { 45 | it('should return specific base64 string when zelda theme is created.', function () { 46 | const midi = require('../examples/zelda-main-theme.js'); 47 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAQACAIBNVHJrAAADoQDAAQCQRn+CAIBGfwCQRn9AgEZ/AJBBf0CAQX8AkEF/QIBBfwCQRn9AgEZ/AJBEfyCARH8AkEJ/IIBCfwCQRH+DAIBEf0CQRn+CAIBGfwCQRn9AgEZ/AJBBf0CAQX8AkEF/QIBBfwCQRn9AgEZ/AJBFfyCARX8AkEN/IIBDfwCQRX+BQIBFf4IAkEUBhACARQGEAJBGf4EAgEZ/AJBBf4FAgEF/AJBGf0CARn8AkEZ/IIBGfwCQSH8ggEh/AJBKfyCASn8AkEt/IIBLfwCQTX+CAIBNf0CQTX9AgE1/AJBNf0CATX8AkE5/IIBOfwCQUH8ggFB/AJBSf4IAgFJ/QJBSf0CAUn8AkFJ/QIBSfwCQUH8ggFB/AJBOfyCATn8AkFB/YIBQfwCQTn8ggE5/AJBNf4IAgE1/AJBNf4EAgE1/AJBLf2CAS38AkE1/IIBNfwCQTn+CAIBOfwCQTX9AgE1/AJBLf0CAS38AkEl/YIBJfwCQS38ggEt/AJBNf4IAgE1/AJBLf0CAS38AkEl/QIBJfwCQSH9ggEh/AJBKfyCASn8AkEx/ggCATH8AkE9/gQCAT38AkE1/QIBNfwCQQX8ggEF/AJBBfyCAQX8AkEF/QIBBfwCQQX8ggEF/AJBBfyCAQX8AkEF/QIBBfwCQQX8ggEF/AJBBfyCAQX8AkEF/QIBBfwCQQX9AgEF/AJBGf4EAgEZ/AJBBf4FAgEF/AJBGf0CARn8AkEZ/IIBGfwCQSH8ggEh/AJBKfyCASn8AkEt/IIBLfwCQTX+CAIBNf0CQTX9AgE1/AJBNf0CATX8AkE5/IIBOfwCQUH8ggFB/AJBSf4MAgFJ/AJBVf4EAgFV/AJBUf4EAgFR/AJBRf4IAgFF/AJBNf4EAgE1/AJBOf4MAgE5/AJBSf4EAgFJ/AJBRf4EAgFF/AJBNf4IAgE1/AJBNf4EAgE1/AJBOf4MAgE5/AJBSf4EAgFJ/AJBRf4EAgFF/AJBNf4IAgE1/AJBKf4EAgEp/AJBLf4MAgEt/AJBOf4EAgE5/AJBNf4EAgE1/AJBJf4IAgEl/AJBGf4EAgEZ/AJBIf2CASH8AkEp/IIBKfwCQTH+CAIBMfwCQT3+BAIBPfwCQTX9AgE1/AJBBfyCAQX8AkEF/IIBBfwCQQX9AgEF/AJBBfyCAQX8AkEF/IIBBfwCQQX9AgEF/AJBBfyCAQX8AkEF/IIBBfwCQQX9AgEF/AJBBf0CAQX8A/y8ATVRyawAACUMAwAEAkCpAgQCAKkAAkDVAgQCANUAAkDpAggCAOkAAkClAgQCAKUAAkDNAgQCAM0AAkDhAggCAOEAAkCdAgQCAJ0AAkDFAgQCAMUAAkDpAggCAOkAAkCpAgQCAKkAAkDVAgQCANUAAkDpAggCAOkAAkC5AQIAuQACQLkAggC5AAJApQCCAKUAAkC5AQIAuQACQLkAggC5AAJApQCCAKUAAkC5AQIAuQACQLkAggC5AAJApQCCAKUAAkC5AIIAuQACQKUAggClAAJAuQCCALkAAkClAIIApQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkAggC5AAJApQCCAKUAAkC5AIIAuQACQKUAggClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQCCALkAAkClAIIApQACQLkAggC5AAJApQCCAKUAAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAIIAsQACQJ0AggCdAAJAsQCCALEAAkCdAIIAnQACQKkBAgCpAAJAqQCCAKkAAkCVAIIAlQACQKkBAgCpAAJAqQCCAKkAAkCVAIIAlQACQKkBAgCpAAJAqQCCAKkAAkCVAIIAlQACQKkAggCpAAJAlQCCAJUAAkCpAIIAqQACQJUAggCVAAJAxQECAMUAAkDFAIIAxQACQLEAggCxAAJAxQECAMUAAkDFAIIAxQACQLEAggCxAAJAxQECAMUAAkDFAIIAxQACQLEAggCxAAJAxQCCAMUAAkCxAIIAsQACQMUAggDFAAJAsQCCALEAAkC9AQIAvQACQL0AggC9AAJAqQCCAKkAAkC9AQIAvQACQL0AggC9AAJAqQCCAKkAAkC9AQIAvQACQL0AggC9AAJAqQCCAKkAAkC9AIIAvQACQKkAggCpAAJAvQCCAL0AAkCpAIIAqQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkBAgC5AAJAuQCCALkAAkClAIIApQACQLkAggC5AAJApQCCAKUAAkC5AIIAuQACQKUAggClAAJAwQECAMEAAkDBAIIAwQACQK0AggCtAAJAwQECAMEAAkDBAIIAwQACQK0AggCtAAJAwQECAMEAAkDBAIIAwQACQK0AggCtAAJAwQCCAMEAAkCtAIIArQACQMEAggDBAAJArQCCAK0AAkClAQIApQACQOUAggDlAAJA5QCCAOUAAkDhAQIA4QACQOEAggDhAAJA4QCCAOEAAkDdAQIA3QACQN0AggDdAAJA3QCCAN0AAkDZAQIA2QACQKUBAgClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQECALkAAkC5AIIAuQACQKUAggClAAJAuQCCALkAAkClAIIApQACQLkAggC5AAJApQCCAKUAAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAQIAsQACQLEAggCxAAJAnQCCAJ0AAkCxAIIAsQACQJ0AggCdAAJAsQCCALEAAkCdAIIAnQACQKkBAgCpAAJAqQCCAKkAAkCpAIIAqQACQKkBAgCpAAJAqQCCAKkAAkCpAIIAqQACQKkBAgCpAAJAqQCCAKkAAkCpAIIAqQACQKkAggCpAAJAqQCCAKkAAkCpAIIAqQACQKkAggCpAAJApQECAKUAAkClAIIApQACQKUAggClAAJApQECAKUAAkClAIIApQACQKUAggClAAJApQECAKUAAkClAIIApQACQKUAggClAAJApQCCAKUAAkClAIIApQACQKUAggClAAJApQCCAKUAAkChAQIAoQACQKEAggChAAJAoQCCAKEAAkChAQIAoQACQKEAggChAAJAoQCCAKEAAkChAQIAoQACQKEAggChAAJAoQCCAKEAAkChAIIAoQACQKEAggChAAJAoQCCAKEAAkChAIIAoQACQKUBAgClAAJApQCCAKUAAkClAIIApQACQKUBAgClAAJApQCCAKUAAkClAIIApQACQKUBAgClAAJApQCCAKUAAkClAIIApQACQKUAggClAAJApQCCAKUAAkClAIIApQACQKUAggClAAJAoQECAKEAAkChAIIAoQACQKEAggChAAJAoQECAKEAAkChAIIAoQACQKEAggChAAJAoQECAKEAAkChAIIAoQACQKEAggChAAJAoQCCAKEAAkChAIIAoQACQKEAggChAAJAoQCCAKEAAkClAQIApQACQKUAggClAAJApQCCAKUAAkClAQIApQACQKUAggClAAJApQCCAKUAAkClAQIApQACQKUAggClAAJApQCCAKUAAkClAIIApQACQKUAggClAAJApQCCAKUAAkClAIIApQACQL0BAgC9AAJAvQCCAL0AAkC9AIIAvQACQL0BAgC9AAJAvQCCAL0AAkC9AIIAvQACQL0BAgC9AAJAvQCCAL0AAkC9AIIAvQACQL0AggC9AAJAvQCCAL0AAkC9AIIAvQACQL0AggC9AAJAuQECALkAAkC5AIIAuQACQLkAggC5AAJAuQECALkAAkC5AIIAuQACQLkAggC5AAJAuQECALkAAkC5AIIAuQACQLkAggC5AAJAuQCCALkAAkC5AIIAuQACQLkAggC5AAJAuQCCALkAAkDBAQIAwQACQMEAggDBAAJAwQCCAMEAAkDBAQIAwQACQMEAggDBAAJAwQCCAMEAAkDBAQIAwQACQMEAggDBAAJAwQCCAMEAAkDBAIIAwQACQMEAggDBAAJAwQCCAMEAAkDBAIIAwQACQKUBAgClAAJA5QCCAOUAAkDlAIIA5QACQOEBAgDhAAJA4QCCAOEAAkDhAIIA4QACQN0BAgDdAAJA3QCCAN0AAkDdAIIA3QACQNkBAgDZAAJApQECAKUAA/y8A', midi.dataUri()); 48 | }); 49 | }); 50 | 51 | describe('#Notes by Start Tick', function() { 52 | it('should return specific base64 string when notes by start tick example is created.', function () { 53 | const midi = require('../examples/notes-by-start-tick.js'); 54 | assert.equal('data:audio/midi;base64,TVRoZAAAAAYAAAABAIBNVHJrAAAARACQPEAygDxAAJBAQDKAQEAAkENAAJBHQDKAQ0AAgEdAAJBIQDKASEAAkEpAMoBKQACQTUAygE1AAJBRQDKAUUAA/y8A', midi.dataUri()); 55 | }); 56 | }); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext", "dom"], 4 | "declaration": true, 5 | "declarationDir": "./types" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./src/main.ts"], 4 | "out": "docs" 5 | } 6 | --------------------------------------------------------------------------------