@lachenmayer/midi-messages
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

@lachenmayer/midi-messages

A MIDI message encoder/decoder written in TypeScript. Because all other ones I've found have weird APIs, weird dependencies, or were just generally weird. Which is weird, because MIDI is such a simple and mostly sensible format.

This module exists because node-midi and WebMIDI do not parse MIDI messages, and instead return cryptic arrays of numbers. That's not very useful, especially if you actually want to do something with the MIDI messages.

This module parses raw MIDI messages into JavaScript objects. These objects are designed to match the official MIDI spec as closely as possible, in naming and structure. See the type definitions for details on how the message objects are structured.

Non-scope / caveats

  • This module does not parse or play MIDI files. I wrote this to interface with MIDI controllers in real time, not to read MIDI files.

  • This module silently overflows. All MIDI data bytes are 7-bit values, ie. 0 to 127. You might expect this module to throw errors if values are out of range, but it doesn't. Values wrap around instead.

  • This module currently does not parse Sample Dump messages. While this would be a nice addition to this module, it is currently not implemented because I have no use for it.

  • This module currently does not parse the contents of timecode (MTC) messages. The contents of MTC quarter frame messages are currently only represented as an opaque number. MTC sysex messages are not parsed at all, and are just emitted as unparsed sysex messages. Partial support for this will probably be added at some point (probably only for MTC quarter frame messages).

Install

npm install @lachenmayer/midi-messages
(Why `@lachenmayer/midi-messages` and not just `midi-messages`?) While I believe that this module could be useful to a lot of people, I wrote it for my own purposes, and I'm not interested in maintaining it for others. If you find a bug, feel free to open an issue or pull request, but don't expect me to spend time fixing it unless it affects me.

I believe that most people should author packages under their own scope, (a) to avoid name-squatting and namespace pollution, and (b) so that it is clear who is maintaining the module.

Feel free to publish your own fork on NPM, but please publish it under your own scope.

Usage

Simple encoding

const { EncodeStream } = require('@lachenmayer/midi-messages')

const encode = new EncodeStream()

encode.on('data', buf => {
  console.log(buf)
})

encode.noteOn(1, 64, 100)
// ...equivalent to:
encode.write({ type: 'NoteOn', channel: 1, note: 64, velocity: 100 })

Output:

<Buffer 90 40 64>
<Buffer 90 40 64>

Simple decoding

const { DecodeStream } = require('@lachenmayer/midi-messages')

const decode = new DecodeStream()

decode.on('data', message => {
  console.log(message)
})

decode.write(Buffer.from('904064', 'hex'))
decode.write(Buffer.from('80407f', 'hex'))

Output:

{ type: 'NoteOn', channel: 1, note: 64, velocity: 100 }
{ type: 'NoteOff', channel: 1, note: 64, velocity: 127 }

Usage with node-midi

The following example creates a virtual MIDI output device and plays a random MIDI note every second.

const { EncodeStream } = require('@lachenmayer/midi-messages')
const midi = require('midi')

const output = new midi.output()
output.openVirtualPort('random note every second')

const encode = new EncodeStream()
encode.pipe(midi.createWriteStream(output))

setInterval(() => {
  const note = Math.floor(Math.random() * 128)
  const velocity = Math.floor(Math.random() * 128)
  console.log('Playing note:', note, 'velocity:', velocity)
  encode.noteOn(1, note, velocity)
  setTimeout(() => {
  console.log('Stopping note:', note)
    encode.noteOff(1, note, velocity)
  }, 200)
}, 1000)

API

This module exposes Node streams for encoding & decoding. If you are unfamiliar with how Node streams work, check out stream-handbook for a hands-on introduction.

MIDIMessage

A MIDIMessage object represents a single MIDI message. Every MIDIMessage object has a type field which corresponds to the message type (status byte) as defined in the MIDI specification. Most other messages contain data fields, eg. a lot of messages contain a channel field.

The MIDIMessage types are defined in src/types.ts, check this file for the exact definitions. An example definition looks like this:

type NoteOn = {
  type: 'NoteOn'
  channel: Channel
  note: U7
  velocity: U7
}

The Channel & U7 types are really just numbers. The type names are used as a documentation hint to remind you of the range of values that can be encoded in a message. The ranges are not enforced at runtime - you are responsible for checking that the values you write are within range, otherwise they will silently overflow. You should be aware of these types:

Type Range (inclusive) Comment
Channel 1-16 1-indexed, ie. the first channel is 1, not 0
U7 0-127 7-bit unsigned integer
U14 0-16383 14-bit unsigned integer

EncodeStream

A transform stream which turns MIDIMessage objects into buffers containing binary MIDI data. Use this if you want to generate new MIDI messages in your application and send them "down the wire", eg. to a MIDI device.

const encode = new EncodeStream(options?)

Options & default values:

  • useRunningStatus: true encode messages using running status, ie. omit the status byte when the previous message has the same status byte.

encode.write(message: MIDIMessage)

Use this to manually encode a MIDIMessage object. You can either call this directly, or use one of the convenience methods listed below to encode a message.

encode.pipe(destination: WritableStream)

Use this to automatically push data to a writable stream, for example a node-midi output stream, or a file stream.

encode.on('data', cb: (buf: Buffer) => any)

The data event is emitted (synchronously) with the message encoded in a Buffer every time a message has been written to the stream.

encode.on('error', cb: (err: Error) => any)

The error event is emitted when there is an error in your input.

The error can be one of the following:

  • err.name === 'TypeError': Thrown when the message you wrote using encode.write is not a valid MIDIMessage object.

Convenience methods

EncodeStream instances expose the following methods, which are very simple wrappers around encode.write.

encode.noteOff(channel: Channel, note: U7, velocity: U7)
encode.noteOn(channel: Channel, note: U7, velocity: U7)
encode.polyKeyPressure(channel: Channel, note: U7, pressure: U7)
encode.controlChange(channel: Channel, control: U7, value: U7 | U14)
encode.programChange(channel: Channel, number: U7)
encode.channelKeyPressure(channel: Channel, pressure: U7)
encode.pitchBendChange(channel: Channel, value: U14)
encode.rpnChange(channel: Channel, parameter: U14, value: U14)
encode.nrpnChange(channel: Channel, parameter: U14, value: U14)
encode.allSoundOff(channel: Channel)
encode.resetAllControllers(channel: Channel)
encode.localControl(channel: Channel, value: boolean)
encode.allNotesOff(channel: Channel)
encode.omniOff(channel: Channel)
encode.omniOn(channel: Channel)
encode.monoMode(channel: Channel)
encode.polyMode(channel: Channel)
encode.sysEx(deviceId: SysExDeviceID, data: U7[])
encode.mtcQuarterFrame(data: U7)
encode.songPositionPointer(position: U14)
encode.songSelect(number: U7)
encode.tuneRequest()
encode.timingClock()
encode.start()
encode.continue()
encode.stop()
encode.activeSensing()
encode.systemReset()

DecodeStream

A transform stream which parses binary MIDI data into MIDIMessage objects. Use this if you want to interpret MIDI data coming "from the wire", eg. from a MIDI device.

const decode = new DecodeStream()

This constructor has no options. Running status is handled automatically as needed.

decode.write(buf: Buffer)

Use this to manually decode a single Buffer containing binary MIDI data.

decode.pipe(destination: WritableStream)

Use this to automatically push data to a writable stream.

Note that the readable side of DecodeStream operates in object mode, so you will not be able to pipe this directly to a stream expecting binary data.

decode.on('data', cb: (message: MIDIMessage) => any)

The data event is emitted (synchronously) with a MIDIMessage object for every message found in the given buffer that was written to the stream.

decode.on('error', cb: (err: Error) => any)

The error event is emitted when there is an error in your input.

The error can be one of the following:

  • err.name === 'UnexpectedDataError': Thrown when the protocol expects a status byte (0x80-0xFF), but a data byte (0x00-0x7F) was found. Indicates that the contents of the buffer are not valid MIDI data (or that there is a bug in the decoding implementation!)
  • err.name === 'UnexpectedEOFError': Thrown when the protocol expects further data bytes, but end of the buffer has been reached. The input buffer must contain full MIDI messages.
  • err.name === 'InternalError': If you see one of these, please open an issue. Indicates an implementation error in the decoding logic.

Further reading

License

MIT © 2018 harry lachenmayer

Package Sidebar

Install

npm i @lachenmayer/midi-messages

Weekly Downloads

3

Version

1.0.1

License

MIT

Unpacked Size

110 kB

Total Files

30

Last publish

Collaborators

  • lachenmayer