Files
topos/src/IO/MidiConnection.ts
2023-08-16 13:59:16 +02:00

217 lines
7.4 KiB
TypeScript

export class MidiConnection{
/**
* Wrapper class for Web MIDI API. Provides methods for sending MIDI messages.
*
*
* @param midiAccess - Web MIDI API access object
* @param midiOutputs - Array of MIDI output objects
* @param currentOutputIndex - Index of the currently selected MIDI output
* @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs.
*/
private midiAccess: MIDIAccess | null = null;
public midiOutputs: MIDIOutput[] = [];
private currentOutputIndex: number = 0;
private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId }
constructor() {
this.initializeMidiAccess();
}
private async initializeMidiAccess(): Promise<void> {
/**
* Initializes Web MIDI API access and populates the list of MIDI outputs.
*
* @returns Promise
*/
try {
this.midiAccess = await navigator.requestMIDIAccess();
this.midiOutputs = Array.from(this.midiAccess.outputs.values());
if (this.midiOutputs.length === 0) {
console.warn('No MIDI outputs available.');
}
} catch (error) {
console.error('Failed to initialize MIDI:', error);
}
}
public getCurrentMidiPort(): string | null {
/**
* Returns the name of the currently selected MIDI output.
*
* @returns Name of the currently selected MIDI output or null if no MIDI output is selected or available.
*/
if (this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) {
return this.midiOutputs[this.currentOutputIndex].name;
} else {
console.error('No MIDI output selected or available.');
return null;
}
}
public sendMidiClock(): void {
/**
* Sends a single MIDI clock message to the currently selected MIDI output.
*/
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xF8]); // Send a single MIDI clock message
} else {
console.error('MIDI output not available.');
}
}
public switchMidiOutput(outputName: string): boolean {
/**
* Switches the currently selected MIDI output.
*
* @param outputName Name of the MIDI output to switch to
* @returns True if the MIDI output was found and switched to, false otherwise
*/
const index = this.midiOutputs.findIndex((output) => output.name === outputName);
if (index !== -1) {
this.currentOutputIndex = index;
return true;
} else {
console.error(`MIDI output "${outputName}" not found.`);
return false;
}
}
public listMidiOutputs(): void {
/**
* Lists all available MIDI outputs to the console.
*/
console.log('Available MIDI Outputs:');
this.midiOutputs.forEach((output, index) => {
console.log(`${index + 1}. ${output.name}`);
});
}
public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number): void {
/**
* Sending a MIDI Note on/off message with the same note number and channel. Automatically manages
* the note off message after the specified duration.
*
* @param noteNumber MIDI note number (0-127)
* @param channel MIDI channel (0-15)
* @param velocity MIDI velocity (0-127)
* @param duration Duration in milliseconds
*
*/
const output = this.midiOutputs[this.currentOutputIndex];
noteNumber = Math.min(Math.max(noteNumber, 0), 127);
if (output) {
const noteOnMessage = [0x90 + channel, noteNumber, velocity];
const noteOffMessage = [0x80 + channel, noteNumber, 0];
// Send Note On
output.send(noteOnMessage);
// Schedule Note Off
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000);
this.scheduledNotes[noteNumber] = timeoutId;
} else {
console.error('MIDI output not available.');
}
}
public sendSysExMessage(message: number[]): void {
/**
* Sends a SysEx message to the currently selected MIDI output.
*
* @param message Array of SysEx message bytes
*
* @example
* // Send a SysEx message to set the pitch bend range to 12 semitones
* sendSysExMessage([0xF0, 0x43, 0x10, 0x4C, 0x08, 0x00, 0x01, 0x00, 0x02, 0xF7]);
*/
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send(message);
} else {
console.error('MIDI output not available.');
}
}
public sendPitchBend(value: number, channel: number): void {
/**
* Sends a MIDI Pitch Bend message to the currently selected MIDI output.
*
* @param value MIDI pitch bend value (0-16383)
* @param channel MIDI channel (0-15)
*
*/
if (value < 0 || value > 16383) {
console.error('Invalid pitch bend value. Value must be in the range 0-16383.');
}
if (channel < 0 || channel > 15) {
console.error('Invalid MIDI channel. Channel must be in the range 0-15.');
}
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
const lsb = value & 0x7F;
const msb = (value >> 7) & 0x7F;
output.send([0xE0 | channel, lsb, msb]);
} else {
console.error('MIDI output not available.');
}
}
public sendProgramChange(programNumber: number, channel: number): void {
/**
* Sends a MIDI Program Change message to the currently selected MIDI output.
*
* @param programNumber MIDI program number (0-127)
* @param channel MIDI channel (0-15)
*
* @example
* // Send a Program Change message to select program 1 on channel 1
* sendProgramChange(0, 0);
*/
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xC0 + channel, programNumber]); // Program Change
} else {
console.error('MIDI output not available.');
}
}
public sendMidiControlChange(controlNumber: number, value: number, channel: number): void {
/**
* Sends a MIDI Control Change message to the currently selected MIDI output.
*
* @param controlNumber MIDI control number (0-127)
* @param value MIDI control value (0-127)
* @param channel MIDI channel (0-15)
*/
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xB0 + channel, controlNumber, value]); // Control Change
} else {
console.error('MIDI output not available.');
}
}
public panic(): void {
/**
* Sends a Note Off message for all scheduled notes.
*/
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
for (const noteNumber in this.scheduledNotes) {
const timeoutId = this.scheduledNotes[noteNumber];
clearTimeout(timeoutId);
output.send([0x80, parseInt(noteNumber), 0]); // Note Off
}
this.scheduledNotes = {};
} else {
console.error('MIDI output not available.');
}
}
}