import type { Channel, MidiMessage } from "./midi-message.types.js"; const ASSERT_CHANNEL = 0xf; const STATUS_HIGH_NIBBLE = 0xf0; const STATUS_LOW_NIBBLE = 0x0f; const SYSEX_START = 0xf0; const SYSEX_END = 0xf7; function asChannel(n: number): Channel { return (n & ASSERT_CHANNEL) as Channel; } /** * Decode a single MIDI 1.0 message from a raw `Uint8Array`. Returns * the parsed `MidiMessage` plus the byte offset immediately after * the consumed message. SysEx without a terminating `0xF7` returns * `null` (caller treats as malformed). Status byte must be in the * high half of the byte (`>= 0x80`). */ export function decodeMessage( bytes: Uint8Array, offset = 0 ): { readonly msg: MidiMessage; readonly next: number; } | null { if (offset >= bytes.length) { return null; } const status = bytes[offset]; if (status === undefined || (status & 0x80) === 0) { return null; } const high = status & STATUS_HIGH_NIBBLE; const ch = asChannel(status & STATUS_LOW_NIBBLE); if (high === 0x80) { const d1 = bytes[offset + 1]; const d2 = bytes[offset + 2]; if (d1 === undefined || d2 === undefined) return null; return { msg: { type: "noteOff", channel: ch, note: d1, velocity: d2 }, next: offset + 3, }; } if (high === 0x90) { const d1 = bytes[offset + 1]; const d2 = bytes[offset + 2]; if (d1 === undefined || d2 === undefined) return null; const type = d2 === 0 ? "noteOff" : "noteOn"; return { msg: { type, channel: ch, note: d1, velocity: d2 } as MidiMessage, next: offset + 3, }; } if (high === 0xa0) { const d1 = bytes[offset + 1]; const d2 = bytes[offset + 2]; if (d1 === undefined || d2 === undefined) return null; return { msg: { type: "polyAftertouch", channel: ch, note: d1, pressure: d2, }, next: offset + 3, }; } if (high === 0xb0) { const d1 = bytes[offset + 1]; const d2 = bytes[offset + 2]; if (d1 === undefined || d2 === undefined) return null; return { msg: { type: "controlChange", channel: ch, controller: d1, value: d2, }, next: offset + 3, }; } if (high === 0xc0) { const d1 = bytes[offset + 1]; if (d1 === undefined) return null; return { msg: { type: "programChange", channel: ch, program: d1 }, next: offset + 2, }; } if (high === 0xd0) { const d1 = bytes[offset + 1]; if (d1 === undefined) return null; return { msg: { type: "channelPressure", channel: ch, pressure: d1 }, next: offset + 2, }; } if (high === 0xe0) { const d1 = bytes[offset + 1]; const d2 = bytes[offset + 2]; if (d1 === undefined || d2 === undefined) return null; return { msg: { type: "pitchBend", channel: ch, value: (d2 << 7) | d1 }, next: offset + 3, }; } // System common / real-time if (status === SYSEX_START) { const end = bytes.indexOf(SYSEX_END, offset + 1); if (end === -1) return null; return { msg: { type: "sysEx", channel: null, data: bytes.slice(offset + 1, end), }, next: end + 1, }; } if (status === 0xf8) { return { msg: { type: "clock", channel: null }, next: offset + 1 }; } if (status === 0xfa) { return { msg: { type: "start", channel: null }, next: offset + 1 }; } if (status === 0xfb) { return { msg: { type: "continue", channel: null }, next: offset + 1 }; } if (status === 0xfc) { return { msg: { type: "stop", channel: null }, next: offset + 1 }; } if (status === 0xfe) { return { msg: { type: "activeSensing", channel: null }, next: offset + 1, }; } if (status === 0xff) { return { msg: { type: "systemReset", channel: null }, next: offset + 1, }; } return null; } /** * Encode a single `MidiMessage` to its on-wire byte sequence. */ export function encodeMessage(msg: MidiMessage): Uint8Array { switch (msg.type) { case "noteOff": return new Uint8Array([0x80 | msg.channel, msg.note, msg.velocity]); case "noteOn": return new Uint8Array([0x90 | msg.channel, msg.note, msg.velocity]); case "polyAftertouch": return new Uint8Array([0xa0 | msg.channel, msg.note, msg.pressure]); case "controlChange": return new Uint8Array([ 0xb0 | msg.channel, msg.controller, msg.value, ]); case "programChange": return new Uint8Array([0xc0 | msg.channel, msg.program]); case "channelPressure": return new Uint8Array([0xd0 | msg.channel, msg.pressure]); case "pitchBend": return new Uint8Array([ 0xe0 | msg.channel, msg.value & 0x7f, (msg.value >> 7) & 0x7f, ]); case "sysEx": { const out = new Uint8Array(msg.data.length + 2); out[0] = SYSEX_START; out.set(msg.data, 1); out[out.length - 1] = SYSEX_END; return out; } case "clock": return new Uint8Array([0xf8]); case "start": return new Uint8Array([0xfa]); case "continue": return new Uint8Array([0xfb]); case "stop": return new Uint8Array([0xfc]); case "activeSensing": return new Uint8Array([0xfe]); case "systemReset": return new Uint8Array([0xff]); } }