Library / SDK
grimmdude/MidiWriterJS avatar
grimmdude/MidiWriterJS

MidiWriterJS builds multi-track MIDI files from a handful of JavaScript calls

♬ A JavaScript library which provides an API for programmatically generating and creating expressive multi-track MIDI files and JSON.

608 stars66 forksJavaScriptMIT

At a glance

What is it?
MidiWriterJS is a TypeScript library that turns note names and event objects into multi-track MIDI files in the browser or in Node.js, with chords, arpeggios, grace notes, pitch bends, controller changes and a set of output helpers for bytes, base64, data URIs and stdout. Its data model is a list of event objects, and the pitch notation is the part that will surprise you first.
Who is it for?
Adopt MidiWriterJS if you need to generate MIDI programmatically in a JavaScript or TypeScript codebase, for example in a music education tool, a generative piece or a server-side export service, because the API is small enough to learn in an afternoon and the output is a standard MIDI file. Do not adopt it if you need to parse existing MIDI files, since the library is a writer, and do not expect a rendering or synthesis pipeline, because it produces bytes rather than sound.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 129 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library produces and what it deliberately leaves out

MidiWriterJS is described as a JavaScript library which provides an API for programmatically generating and creating expressive multi-track MIDI files and JSON. The feature list is short and the technical notes are specific.

Multi-track MIDI file generation is the first item. The library is written in TypeScript with full type definitions. It works in Node.js in both CommonJS and ESM form, and in the browser. Note names such as C#4 and Eb5 or raw MIDI numbers are both accepted. Chords, arpeggios, grace notes, pitch bends and more are supported. And there is an experimental VexFlow integration, which is a music notation renderer, so a score can be drawn alongside the MIDI data.

What is absent is as important as what is present. This is a writer, not a reader, and it is not a synthesiser or a renderer. It does not decode an existing MIDI file, it does not produce audio, and it does not draw notation except through the experimental VexFlow path. The VexFlow integration being labelled experimental in the README is a signal that the notation side is the least settled part of the library.

The primary language is JavaScript, the licence is MIT, and the npm package is midi-writer-js. The last push was on 2026-06-16, with the recent releases v3.2.1 and v3.2.0 both dated 2026-03-01 and v3.1.1 from 2023-09-28 before that, so the v3.2 line is a substantial rework of a library whose previous major version had been stable for years.

The data model: a Track is a list of event objects

The API is built on two objects, a Track and a Writer, and understanding the Track explains the whole design.

A Track is a list of events, and every event is an instance of a class such as NoteEvent, ProgramChangeEvent, ControllerChangeEvent or PitchBendEvent. The Track exposes methods to set tempo in beats per minute, set time signature, set key signature with values like 'C', 'Dm' or 'F#', set pitch bend, add a controller change, and add metadata events including the track name, a text event, a copyright notice, an instrument name, a marker, a cue point and a lyric. It can also merge another track's events in, remove all events of a given type by name, enable poly mode, and add events with method chaining.

The Writer then takes one track or an array of tracks and produces the file. Its output methods are the practical surface: buildFile() returns a Uint8Array, base64() returns a base64-encoded string, dataUri() returns a data URI string, which the README describes as useful for playback or download links, and stdout() writes the MIDI file to stdout for Node.js CLI usage. There is also setOption for per-instance options.

So the shape of a program is: build a track, add events to it, hand it to a writer, and take bytes or a URI from the writer. The metadata events are a nice touch that most quick examples skip, and they are what turn a sequence of notes into something a DAW will display as a named track with a key signature.

Pitch and duration notation, and the chord versus arpeggio switch

The NoteEvent options are where the learning curve is, and two of them need care.

Pitch can be a string or an array, and each pitch can be a string or a valid MIDI note code, with the string format being C#4. The README also points at tonal, the music theory library, saying you can use its output to build scales and chords, which is the intended way to go beyond a fixed pitch list.

Duration is a string or an array, and the default is '4'. If an array of durations is passed, the sum is used. Wait is a rest before the note and takes the same values as duration. The default wait is '0'.

The switch that matters is sequential. When pitch is an array, the notes play as a chord by default, and setting sequential to true makes them play one after another. The README shows the two cases side by side:

javascript
// Chord (notes play simultaneously)
track.addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'E4', 'G4'], duration: '1'}));

// Arpeggio (notes play sequentially)
track.addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'E4', 'G4'], duration: '8', sequential: true}));

The default being a chord is the detail most likely to cost you an hour. Write a melody with an array of pitches, forget the flag, and you get a block chord where you expected a run of notes, with the duration divided between them.

The remaining options are numeric and predictable: velocity defaults to 50 with values 1 to 100, repeat defaults to 1, channel defaults to 1 across the MIDI range 1 to 16, grace adds grace notes before the main note in the same format as pitch, and startTick with its alias tick sets an explicit tick position, in which case wait is ignored. That last pair is what the notes-by-start-tick example in the repository exists for, and it is the escape hatch when you need to place an event on an exact timeline position rather than relative to the previous one.

The quick start path, from install to a data URI

The install is a single npm command:

sh
npm install midi-writer-js

The README's recommended quick start is ESM, and the whole path is five steps:

javascript
import MidiWriter from 'midi-writer-js';

const track = new MidiWriter.Track();
track.addEvent(new MidiWriter.ProgramChangeEvent({instrument: 1}));
track.addEvent(new MidiWriter.NoteEvent({pitch: ['C4', 'D4', 'E4'], duration: '4', sequential: true}));

const writer = new MidiWriter.Writer(track);
console.log(writer.dataUri());

A ProgramChangeEvent comes first to select the instrument, then a NoteEvent adds the pitches, then the writer produces the file. The CommonJS variant swaps the import for a require, and the TypeScript variant adds one line that shows the typed output:

typescript
const writer = new MidiWriter.Writer(track);
const data: Uint8Array = writer.buildFile();

The dependency picture is small. The package has one runtime dependency, @tonaljs/midi, which supplies the note name to MIDI number mapping. The rest are development tools: Rollup and Babel for the build, TypeScript and typedoc for the types and documentation, Mocha with nyc for coverage, and eslint with the standard config. The build script is mkdir -p build && rollup -c, and the test script runs nyc with the text reporter over mocha with --no-config --no-package, and pretest runs the build first, so the tests always run against a fresh bundle.

The package publishes both entry styles through the exports field, mapping import and require to the same build/index.js, with types at build/types/main.d.ts, so a TypeScript consumer gets the declarations without a separate @types package. The browser build is produced by Rollup into a browser directory, and a postinstall script runs on install.

Multi-track, controller changes and the pitch bend range

Multi-track output is a matter of passing an array of tracks to the Writer, and each track carries its own name, instrument and events:

javascript
const melody = new MidiWriter.Track();
melody.addTrackName('Melody');
melody.addEvent(new MidiWriter.ProgramChangeEvent({instrument: 1}));
melody.addEvent(new MidiWriter.NoteEvent({pitch: ['E5', 'D5', 'C5'], duration: '4', sequential: true}));

const bass = new MidiWriter.Track();
bass.addTrackName('Bass');
bass.addEvent(new MidiWriter.ProgramChangeEvent({instrument: 33}));
bass.addEvent(new MidiWriter.NoteEvent({pitch: ['C2'], duration: '1'}));

const writer = new MidiWriter.Writer([melody, bass]);
console.log(writer.dataUri());

The addTrackName calls are what make the file readable in a DAW, and the ProgramChangeEvent instrument numbers are General MIDI program numbers, so instrument 1 and instrument 33 are how you choose a sound.

Controller changes and pitch bends are separate event classes, and the README shows both together with a note following them:

javascript
// Set volume via CC #7
track.addEvent(new MidiWriter.ControllerChangeEvent({controllerNumber: 7, controllerValue: 100}));

// Pitch bend ranging from -1.0 to 1.0 (0 = no bend)
track.addEvent(new MidiWriter.PitchBendEvent({bend: 0.5}));

The bend range of -1.0 to 1.0 with 0 as no bend is the standard MIDI pitch bend convention, and it is worth noting because it is the one place where the library takes a floating point value where the underlying format is a 14-bit integer.

The examples directory holds five runnable programs, and their names say what they exercise: chopin-prelude-e-minor, hot-cross-buns, mauro.giuliani-op.47-main-theme, notes-by-start-tick and zelda-main-theme. The melody example in the README, the Hot Cross Buns one, shows the pattern for a longer piece, where several NoteEvent objects are added in one addEvent call with a callback that returns sequential: true for each of them.

For a server-side use case the stdout() method is the one that matters, since it writes the file to standard output for piping into a command line tool.

Where MidiWriterJS is the wrong tool

Three limits, and the first is the one that catches people who arrive from a MIDI parsing library.

The first is that this only writes. The API summary covers Track, Writer and the event classes, and there is no read, no parse, no decode. If your requirement starts with an existing MIDI file, for instance to transcode it, to inspect its structure or to extract its tracks, this library cannot do it and you need a reader as well.

The second is that the output is a file, not a sound. There is no synthesiser here, and the VexFlow integration is what produces notation rather than audio. A user of this library still needs a player, a DAW or a soundfont to hear anything, so if the requirement is an audio preview in the browser you are assembling a small pipeline of a MIDI file plus a synthesiser library.

The third is the version history. The recent releases are v3.2.1 and v3.2.0 from the same day in March 2026, with v3.1.1 from September 2023 in between, which suggests a large rework rather than incremental maintenance. A library that has just changed its major version, with the VexFlow integration still labelled experimental, is one where you should pin the exact version and read the changelog rather than tracking latest.

The alternative approach, for comparison, is to build the MIDI bytes yourself from the specification, which is a real option for a small, fixed kind of output. The difference in approach is that MidiWriterJS gives you a typed, tested representation of the format's structure, including the metadata events and the timing arithmetic that is easy to get wrong, while a hand-rolled writer gives you total control over the bytes and none of that. For anything beyond a single channel of notes, the library is the better bet, and the alternative worth knowing about is generating music through an audio library instead, where the output is a sample rather than a symbolic score, which is a different problem with different trade-offs.

Editorial conclusion

Adopt MidiWriterJS if you need to generate MIDI programmatically in a JavaScript or TypeScript codebase, for example in a music education tool, a generative piece or a server-side export service, because the API is small enough to learn in an afternoon and the output is a standard MIDI file. Do not adopt it if you need to parse existing MIDI files, since the library is a writer, and do not expect a rendering or synthesis pipeline, because it produces bytes rather than sound. Verify first that the version you install is 3.2.1 or later, that the pitch and duration notation in the API summary matches what you assume, since durations are strings like '4' and '8' and sequential changes whether an array of pitches is a chord or an arpeggio, and that the VexFlow integration, which the README calls experimental, is stable enough for what you need.

Frequently asked questions

How do I install MidiWriterJS and generate a MIDI file?

Run npm install midi-writer-js, then create a MidiWriter.Track, add events to it, and pass the track to a MidiWriter.Writer. The writer can return the file as a Uint8Array with buildFile, as base64 with base64, as a data URI with dataUri, or write it to stdout with stdout.

What is the difference between a chord and an arpeggio in MidiWriterJS?

When pitch is an array the notes play as a chord by default, and setting sequential to true makes them play one after another instead. The README shows the two cases with the same pitches, one with duration '1' and one with duration '8' and sequential true.

Does MidiWriterJS support multi-track MIDI files?

Yes. You build separate MidiWriter.Track objects, name each with addTrackName, set their instruments with ProgramChangeEvent, and pass an array of tracks to the Writer, as the README's melody and bass example does.

Can MidiWriterJS read existing MIDI files?

No. The documented API covers generating files: a Track that holds events, a Writer that serialises one or more tracks, and event classes for notes, program changes, controller changes and pitch bends. Parsing or decoding existing MIDI files is not part of the library.

What does the VexFlow integration in MidiWriterJS do?

The README lists an experimental VexFlow integration among the features, which is a music notation renderer, so it is the route to drawing a score alongside the generated MIDI. The README labels it experimental, and the wiki is the fuller source of detail.

Official sources

  1. grimmdude/MidiWriterJS on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/grimmdude-midiwriterjs.svg)](https://hysenlabs.com/projects/grimmdude-midiwriterjs)

Was this useful?