Skip to main content
Glama
midi.ts5.33 kB
import JZZ from "jzz"; import { Drums, Instruments } from "./instruments"; import { DrumName, InstrumentName, Score, Track } from "./midi.types"; const openMidi = async () => { return await JZZ(); }; const openMidiOut = async () => { const midi = await JZZ(); return midi.openMidiOut(); }; type MidiOut = Awaited<ReturnType<typeof openMidiOut>>; class Midi { private midi: Awaited<ReturnType<typeof openMidi>> | null = null; private port: MidiOut | null = null; private bpm: number = 120; // Default tempo async init() { this.midi = await JZZ(); } private setBpm(bpm: number) { this.bpm = bpm; } // Convert note duration to milliseconds based on BPM // duration: note value (1 = whole, 0.5 = half, 0.25 = quarter, etc.) private beatsToMs(beats: number): number { return (60000 / this.bpm) * beats; } // Convert string representation of note duration to beat value // Examples: "1/4" -> 1, "1/2" -> 2, "1" -> 4, "1/8" -> 0.5, "1/4." -> 1.5 private parseNoteDuration(noteString: string): number { // Handle common string representations const commonDurations: { [key: string]: number } = { "1": 4, "1/2": 2, "1/4": 1, "1/8": 0.5, "1/16": 0.25, "1/32": 0.125, "2": 8, "4": 16, // Dotted notes "1.": 6, "1/2.": 3, "1/4.": 1.5, "1/8.": 0.75, "1/16.": 0.375, "1/32.": 0.1875, }; // Check if it's a common duration first if (noteString in commonDurations) { return commonDurations[noteString]; } // Parse fraction format (e.g., "1/4", "3/8", "2/3", "1/4.") const fractionMatch = noteString.match( /^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)(\.?)$/ ); if (fractionMatch) { const numerator = parseFloat(fractionMatch[1]); const denominator = parseFloat(fractionMatch[2]); const isDotted = fractionMatch[3] === "."; if (denominator === 0) { throw new Error( `Invalid note duration: ${noteString} (division by zero)` ); } // For note durations, convert to beats: 1/4 = 1 beat, 1/2 = 2 beats, etc. // Formula: (4 * numerator) / denominator let beats = (4 * numerator) / denominator; // If dotted, add half the value (multiply by 1.5) if (isDotted) { beats *= 1.5; } return beats; } // Try to parse as a decimal number const decimalValue = parseFloat(noteString); if (!isNaN(decimalValue)) { return decimalValue; } throw new Error( `Invalid note duration format: ${noteString}. Expected formats: "1/4".` ); } private setInstrument(channel: number, instrumentName: InstrumentName) { if (!this.port) { throw new Error("MIDI port not initialized"); } const programNumber = Instruments[instrumentName]; this.port.program(channel, programNumber); } private async playNote( note: string[] | number[], duration: number, channel: number = 0, instrumentName?: InstrumentName ) { if (!this.port) { throw new Error("MIDI port not initialized"); } // Handle rest notes if (note.length === 0) { await this.port.wait(duration); return; } if (instrumentName) { this.setInstrument(channel, instrumentName); } await Promise.all( note.map((n) => { return this.port?.noteOn(channel, n, 127); }) ); await this.port.wait(duration); await Promise.all( note.map((n) => { return this.port?.noteOff(channel, n); }) ); } private async playTrack(track: Track) { let actualChannel: number | null; if (track.channel) { actualChannel = track.channel; } else if (track.instrumentName === "drums") { actualChannel = 9; } else { actualChannel = 0; } const actualInstrumentName: InstrumentName | undefined = track.instrumentName === "drums" ? undefined : track.instrumentName; for (const { note, drums, noteDuration } of track.notes) { let actualNote = track.instrumentName === "drums" ? (drums?.map((d) => Drums[d as DrumName]) as unknown as number[]) : (note as string[]); await this.playNote( actualNote, this.beatsToMs(this.parseNoteDuration(noteDuration)), actualChannel, actualInstrumentName ); } } private async openMidiOut(midiOutputName?: string) { if (!this.midi) { throw new Error("MIDI engine not initialized"); } if (!midiOutputName) { this.port = await this.midi.openMidiOut(); } else { const midiOutput = (await this.listOutputs()).find( (output: { name: string }) => output.name === midiOutputName ); if (midiOutput) { this.port = await this.midi.openMidiOut(midiOutput.name); } else { this.port = await this.midi.openMidiOut(); } } } public async listOutputs() { if (!this.midi) { throw new Error("MIDI engine not initialized"); } return this.midi.info().outputs; } public async playScore(score: Score) { await this.openMidiOut(score.midiOuputName); this.setBpm(score.bpm); return Promise.all(score.tracks.map((track) => this.playTrack(track))); } } export { Midi };

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mikeborozdin/vibe-composer-midi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server