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 { /** * 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.'); } } }