KEXEL's picture
1.1
b0bfea8 verified
import { BasicMIDI } from "./basic_midi.js";
import { messageTypes, MIDIMessage } from "./midi_message.js";
import { IndexedByteArray } from "../utils/indexed_array.js";
import { SpessaSynthWarn } from "../utils/loggin.js";
/**
* A class that helps to build a MIDI file from scratch.
*/
export class MIDIBuilder extends BasicMIDI
{
/**
* @param name {string} The MIDI's name
* @param timeDivision {number} the file's time division
* @param initialTempo {number} the file's initial tempo
*/
constructor(name, timeDivision = 480, initialTempo = 120)
{
super();
this.timeDivision = timeDivision;
this.midiName = name;
this.encoder = new TextEncoder();
this.rawMidiName = this.encoder.encode(name);
// create the first track with the file name
this.addNewTrack(name);
this.addSetTempo(0, initialTempo);
}
/**
* Adds a new Set Tempo event
* @param ticks {number} the tick number of the event
* @param tempo {number} the tempo in beats per minute (BPM)
*/
addSetTempo(ticks, tempo)
{
const array = new IndexedByteArray(3);
tempo = 60000000 / tempo;
// Extract each byte in big-endian order
array[0] = (tempo >> 16) & 0xFF;
array[1] = (tempo >> 8) & 0xFF;
array[2] = tempo & 0xFF;
this.addEvent(ticks, 0, messageTypes.setTempo, array);
}
/**
* Adds a new MIDI track
* @param name {string} the new track's name
* @param port {number} the new track's port
*/
addNewTrack(name, port = 0)
{
this.tracksAmount++;
if (this.tracksAmount > 1)
{
this.format = 1;
}
this.tracks.push([]);
this.tracks[this.tracksAmount - 1].push(
new MIDIMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
);
this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
}
/**
* Adds a new MIDI Event
* @param ticks {number} the tick time of the event
* @param track {number} the track number to use
* @param event {number} the MIDI event number
* @param eventData {Uint8Array|Iterable<number>} the raw event data
*/
addEvent(ticks, track, event, eventData)
{
if (!this.tracks[track])
{
throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
}
if (event === messageTypes.endOfTrack)
{
SpessaSynthWarn(
"The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead.");
return;
}
// remove the end of track
this.tracks[track].pop();
this.tracks[track].push(new MIDIMessage(
ticks,
event,
new IndexedByteArray(eventData)
));
// add the end of track
this.tracks[track].push(new MIDIMessage(
ticks,
messageTypes.endOfTrack,
new IndexedByteArray(0)
));
}
/**
* Adds a new Note On event
* @param ticks {number} the tick time of the event
* @param track {number} the track number to use
* @param channel {number} the channel to use
* @param midiNote {number} the midi note of the keypress
* @param velocity {number} the velocity of the keypress
*/
addNoteOn(ticks, track, channel, midiNote, velocity)
{
channel %= 16;
midiNote %= 128;
velocity %= 128;
this.addEvent(
ticks,
track,
messageTypes.noteOn | channel,
[midiNote, velocity]
);
}
/**
* Adds a new Note Off event
* @param ticks {number} the tick time of the event
* @param track {number} the track number to use
* @param channel {number} the channel to use
* @param midiNote {number} the midi note of the key release
*/
addNoteOff(ticks, track, channel, midiNote)
{
channel %= 16;
midiNote %= 128;
this.addEvent(
ticks,
track,
messageTypes.noteOff | channel,
[midiNote, 64]
);
}
/**
* Adds a new Program Change event
* @param ticks {number} the tick time of the event
* @param track {number} the track number to use
* @param channel {number} the channel to use
* @param programNumber {number} the MIDI program to use
*/
addProgramChange(ticks, track, channel, programNumber)
{
channel %= 16;
programNumber %= 128;
this.addEvent(
ticks,
track,
messageTypes.programChange | channel,
[programNumber]
);
}
/**
* Adds a new Controller Change event
* @param ticks {number} the tick time of the event
* @param track {number} the track number to use
* @param channel {number} the channel to use
* @param controllerNumber {number} the MIDI CC to use
* @param controllerValue {number} the new CC value
*/
addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
{
channel %= 16;
controllerNumber %= 128;
controllerValue %= 128;
this.addEvent(
ticks,
track,
messageTypes.controllerChange | channel,
[controllerNumber, controllerValue]
);
}
/**
* Adds a new Pitch Wheel event
* @param ticks {number} the tick time of the event
* @param track {number} the track to use
* @param channel {number} the channel to use
* @param MSB {number} SECOND byte of the MIDI pitchWheel message
* @param LSB {number} FIRST byte of the MIDI pitchWheel message
*/
addPitchWheel(ticks, track, channel, MSB, LSB)
{
channel %= 16;
MSB %= 128;
LSB %= 128;
this.addEvent(
ticks,
track,
messageTypes.pitchBend | channel,
[LSB, MSB]
);
}
}