Skip to content

MIDI Class

spessasus edited this page Jul 19, 2024 · 34 revisions

MIDI Class

This is the module responsible for parsing MIDI files into objects readable by Sequencer class.

Tip

If you encounter any errors in this documentation, please open an issue!

Importing

import {MIDI} from "./spessasynth_lib/midi_parser/midi_loader.js";

Initialization

const parsedMIDI = new MIDI(arrayBuffer);
  • arrayBuffer - an arrayBuffer anstance of the midi file.

Properties

tracksAmount

The amount of tracks in the file.

console.log(`this file has ${parsedMIDI.tracksAmount}`);

timeDivision

The time division of the midi file. Used by the Sequencer class

console.log(`this sequence's time division is ${parsedMIDI.timeDivision}`);

midiName

The sequence's name. The first track's Track Name's event text.

console.log(`This sequence is named "${parsedMIDI.midiName}"`);

Tip

This property uses basic decoding. If the name is encoded in a different encoding, like shift_jis, it might be better to use rawMidiName.

rawMidiName

The sequence's name, as a raw Uint8Array. Useful for handling unusual encodings.

console.log(new TextDecoder("shift-jis").decode(parsedMIDI.rawMidiName)); // ダメなりんご!!

copyright

The decoded copyright and description of the file. Also includes the Sound Canvas display messages.

console.log(`Midi file description: ${parsedMIDI.copyright}`);

tempoChanges

Ordered from last to first, all the tempo changes in the file. Will always contain at least 1 tempo (the default 120BPM).

[
   {
       tempo: 140 /* tempo in BPM */,
       ticks: 5437 /* absolute amount of MIDI Ticks from the start */
   },

   /*...*/

   {
       tempo: 120,
       ticks: 0
   }
];

loop

The points of the loop detected in the MIDI file in ticks. If there's nothing detected, the loop will start from the first note on event and end will be the last note off. Current looping detection is: CC 2/4, 116/117 and "start", "loopStart" and "loopEnd" markers.

console.log(parsedMIDI.loop); // {start: 1294, end: 49573}

format

The MIDI file format. Usually 0 or 1, rarely 2.

console.log(parsedMIDI.format); // 1

firstNoteOn

The tick number of the first noteOn event in the sequence. Can be used to skip the initial silence.

console.log(parsedMIDI.firstNoteOn); // 1294

duration

The sequence's duration in seconds.

console.log(parsedMIDI.duration); // 125.64;

midiPorts

The detected midi ports for each track. Each port represents a batch of 16 channels.

console.log(parsedMIDI.midiPorts); // [0, 0, 0, 1, 1, 2, ...]

usedChannelsOnTrack

All the channels that each track refers to. An array of Sets.

console.log(parsedMIDI.usedChannelsOnTrack) // [ Set[0, 1, 2, 3, 4] ] - this sequence has 1 track which plays on channels 0, 1, 2, 3 and 4

lyrics

The detected lyrics, stored as binary text data, as MIDIs can use different encodings. Stored as an array of Uint8Arrays, each is a single lyrics event.

tracks

The actual MIDI sequence data. Described below.

How the file is stored

The file is stored as an array of tracks, accesible via parsedMIDI.tracks. Each track is an array of events. Each event is a MidiMessage class, which is defined as follows;

class MidiMessage
{
    constructor(ticks, byte, data) {
        this.ticks = ticks;
        this.messageStatusByte = byte;
        this.messageData = data;
    }
}
  • ticks - absolute amount of MIDI Ticks from the start of the track.
  • messageStatusByte - the status byte of the message as a number from 0 to 255. Learn more here and here.

Important

Note that for Meta Events, the status byte is the SECOND status byte, not the 0xFF!

  • messageData - a ShiftableByteArray(Pretty much exactly the same as Uint8Array) instance of the event's data.

Writing a .mid file

writeMIDIFile

Renders the sequence as a .mid file.

writeMIDIFile(midi);
  • midi - the MIDI instance to export.

The returned value is an Uint8Array - a binary representation of the .mid file.

modifyMIDI

Allows to easily modify the sequence's programs and controllers.

modifyMIDI(midi, desiredProgramChanges, desiredControllerChanges, desiredChannelsToClear, desiredChannelsToTranspose);
  • midi - the MIDI instance to modify.
  • desiredProgramChanges - an array of objects, defined as follows:
/**
 * @typedef desiredProgramChange {Object}
 * @property {number} channel - the channel to modify. Note that this allows going over 16 if the MIDI is a multi port file
 * @property {number} program - the MIDI program to use.
 * @property {number} bank - the bank to use.
 * @property {boolean} isDrum - if the channel is a drum channel. Will add GS Use Drums System exclusive and GS on if needed
 */
  • desiredControllerChanges - an array of objects, defined as follows:
/**
 * @typedef desiredControllerChange {Object}
 * @property {number} channel - same as above.
 * @property {number} controllerNumber - the MIDI CC number to use.
 * @property {number} controllerValue - the desired value of the controller.
 * 
 */
  • desiredChannelsToClear - an array of numbers, indicating the channel number to effectively mute.
  • desiredChannelsToTranspose - an array of objects, defined as follows:
/**
 * @typedef desiredTranspose {Object}
 * @property {number} channel - same as above.
 * @property {number} keyShift - the amount to shift the notes on this channel by. Can be negative, but must be integer.
 */

Warning

Clearing the channel removes the messages rather than setting volume to 0! This operation is irreversible if the original midi file is lost.

applySnapshot

Applies a SynthesizerSnapshot to the sequence in place. This means changing the programs and controllers if they are locked.

applySnapshotToMIDI(midi, snapshot);
  • midi - the MIDI instance to modify.
  • snapshot - the SynthesizerSnapshot to use.

For example if channel 1 has locked preset on Drawbar Organ, this will remove all program changes for channel 1 and add one at the start to change the program to Drawbar organ.