Skip to main content
Glama

MCP MIDI Bridge

by tezza1971
midi-manager.ts18.9 kB
import easymidi from 'easymidi'; import { NoteSequence, PlayMidiResult, PlaybackProgress, MidiInstrument, MidiDrum } from '../types'; /** * MIDI Manager for handling virtual MIDI devices and multi-channel support */ import { EventEmitter } from 'events'; export class MidiManager extends EventEmitter { private virtualOutput: easymidi.Output | null = null; private isPlaying: boolean = false; private activeChannels: Set<number> = new Set(); private currentPlaybackTimeout: NodeJS.Timeout | null = null; constructor() { super(); this.initialize(); } /** * Initialize the MIDI manager */ private initialize(): void { try { // List available MIDI outputs const outputs = easymidi.getOutputs(); console.log('Available MIDI outputs:', outputs); // Create a virtual MIDI output if needed if (!outputs.includes('MCP MIDI Bridge')) { this.virtualOutput = new easymidi.Output('MCP MIDI Bridge', true); console.log('Created virtual MIDI output: MCP MIDI Bridge'); } else { this.virtualOutput = new easymidi.Output('MCP MIDI Bridge'); console.log('Connected to existing virtual MIDI output: MCP MIDI Bridge'); } // Notify listeners about connection this.emit('midi-connection-status', { connected: true, deviceName: 'MCP MIDI Bridge' }); } catch (error) { console.error('Failed to initialize MIDI:', error); this.emit('midi-connection-status', { connected: false, error }); } } /** * Get available MIDI outputs * @returns {string[]} - Array of MIDI output names */ getOutputs(): string[] { return easymidi.getOutputs(); } /** * Send a program change to set the instrument for a channel * @param {number} channel - MIDI channel (0-15) * @param {number} program - Program number (0-127) */ setProgramForChannel(channel: number, program: number): void { if (!this.virtualOutput) return; this.virtualOutput.send('program', { channel: channel, number: program }); console.log(`Set channel ${channel} to program ${program}`); } /** * Stop current playback */ stopPlayback(): void { this.isPlaying = false; if (this.currentPlaybackTimeout) { clearTimeout(this.currentPlaybackTimeout); this.currentPlaybackTimeout = null; } // Send all notes off for all channels if (this.virtualOutput) { for (let channel = 0; channel < 16; channel++) { for (let note = 0; note < 128; note++) { this.virtualOutput.send('noteoff', { note: note, velocity: 0, channel: channel }); } } } } /** * Play a NoteSequence through the virtual MIDI output * @param {NoteSequence} noteSequence - The NoteSequence to play * @param {Function} progressCallback - Callback for playback progress updates * @returns {Promise<PlayMidiResult>} - Result of the playback */ async playNoteSequence( noteSequence: NoteSequence, progressCallback: ((progress: PlaybackProgress) => void) | null = null ): Promise<PlayMidiResult> { if (!this.virtualOutput) { return { success: false, error: 'MIDI output not available' }; } if (this.isPlaying) { return { success: false, error: 'Already playing' }; } if (!noteSequence || !noteSequence.notes || noteSequence.notes.length === 0) { return { success: false, error: 'Invalid or empty note sequence' }; } try { this.isPlaying = true; this.activeChannels.clear(); // Sort notes by start time const sortedNotes = [...noteSequence.notes].sort((a, b) => a.startTime - b.startTime); // Get the total duration const totalDuration = noteSequence.totalTime || Math.max(...sortedNotes.map(note => note.endTime)) || 0; // Track active notes to ensure we send noteoff events const activeNotes = new Map<number, Set<number>>(); // Set up instruments for each channel used in the sequence const channelsUsed = new Set(sortedNotes.map(note => note.instrument || 0)); // Send program changes for each channel for (const channel of channelsUsed) { // Find the first note for this channel to determine the program const firstNoteForChannel = sortedNotes.find(note => (note.instrument || 0) === channel); if (firstNoteForChannel) { const program = firstNoteForChannel.program || 0; this.setProgramForChannel(channel, program); this.activeChannels.add(channel); } } // Start time for calculating relative timing const startTime = Date.now(); // Process all notes and schedule them for (let i = 0; i < sortedNotes.length && this.isPlaying; i++) { const note = sortedNotes[i]; const channel = note.instrument || 0; const pitch = note.pitch; const velocity = note.velocity || 80; // Calculate the time to wait before playing this note const noteStartTime = note.startTime * 1000; // convert to ms const noteEndTime = note.endTime * 1000; // convert to ms // Wait until it's time to play this note await new Promise<void>(resolve => { const timeToWait = Math.max(0, startTime + noteStartTime - Date.now()); this.currentPlaybackTimeout = setTimeout(resolve, timeToWait); }); if (!this.isPlaying) break; // Check if playback was stopped // Send note on message this.virtualOutput.send('noteon', { note: pitch, velocity: velocity, channel: channel }); // Add to active notes if (!activeNotes.has(channel)) { activeNotes.set(channel, new Set()); } activeNotes.get(channel)?.add(pitch); // Schedule note off this.currentPlaybackTimeout = setTimeout(() => { if (this.virtualOutput) { this.virtualOutput.send('noteoff', { note: pitch, velocity: 0, channel: channel }); } // Remove from active notes if (activeNotes.has(channel)) { activeNotes.get(channel)?.delete(pitch); } }, Math.max(0, startTime + noteEndTime - Date.now())); // Send progress updates if (progressCallback && totalDuration > 0) { const progress = note.startTime / totalDuration; progressCallback({ current: note.startTime, total: totalDuration, progress: progress }); } } // Wait for the entire sequence to finish if (this.isPlaying) { await new Promise<void>(resolve => { const timeToWait = Math.max(0, startTime + totalDuration * 1000 - Date.now()) + 100; // Add a small buffer this.currentPlaybackTimeout = setTimeout(resolve, timeToWait); }); } // Ensure all notes are turned off for (const [channel, notes] of activeNotes.entries()) { for (const pitch of notes) { if (this.virtualOutput) { this.virtualOutput.send('noteoff', { note: pitch, velocity: 0, channel: channel }); } } } // Send final progress update if (progressCallback) { progressCallback({ current: totalDuration, total: totalDuration, progress: 1.0 }); } this.isPlaying = false; return { success: true }; } catch (error) { this.isPlaying = false; console.error('Error playing note sequence:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Send a control change message * @param {number} channel - MIDI channel (0-15) * @param {number} controller - Controller number (0-127) * @param {number} value - Controller value (0-127) */ sendControlChange(channel: number, controller: number, value: number): void { if (!this.virtualOutput) return; this.virtualOutput.send('cc', { controller: controller, value: value, channel: channel }); } /** * Send all notes off for a specific channel * @param {number} channel - MIDI channel (0-15) */ allNotesOff(channel: number): void { if (!this.virtualOutput) return; // Send All Notes Off (CC 123) this.sendControlChange(channel, 123, 0); } /** * Send all sound off for a specific channel * @param {number} channel - MIDI channel (0-15) */ allSoundOff(channel: number): void { if (!this.virtualOutput) return; // Send All Sound Off (CC 120) this.sendControlChange(channel, 120, 0); } /** * Reset all controllers for a specific channel * @param {number} channel - MIDI channel (0-15) */ resetAllControllers(channel: number): void { if (!this.virtualOutput) return; // Send Reset All Controllers (CC 121) this.sendControlChange(channel, 121, 0); } /** * Get General MIDI instrument names * @returns {MidiInstrument[]} - Array of instrument information */ getGeneralMidiInstruments(): MidiInstrument[] { return [ // Piano { name: 'Acoustic Grand Piano', family: 'Piano' }, { name: 'Bright Acoustic Piano', family: 'Piano' }, { name: 'Electric Grand Piano', family: 'Piano' }, { name: 'Honky-tonk Piano', family: 'Piano' }, { name: 'Electric Piano 1', family: 'Piano' }, { name: 'Electric Piano 2', family: 'Piano' }, { name: 'Harpsichord', family: 'Piano' }, { name: 'Clavi', family: 'Piano' }, // Chromatic Percussion { name: 'Celesta', family: 'Chromatic Percussion' }, { name: 'Glockenspiel', family: 'Chromatic Percussion' }, { name: 'Music Box', family: 'Chromatic Percussion' }, { name: 'Vibraphone', family: 'Chromatic Percussion' }, { name: 'Marimba', family: 'Chromatic Percussion' }, { name: 'Xylophone', family: 'Chromatic Percussion' }, { name: 'Tubular Bells', family: 'Chromatic Percussion' }, { name: 'Dulcimer', family: 'Chromatic Percussion' }, // Organ { name: 'Drawbar Organ', family: 'Organ' }, { name: 'Percussive Organ', family: 'Organ' }, { name: 'Rock Organ', family: 'Organ' }, { name: 'Church Organ', family: 'Organ' }, { name: 'Reed Organ', family: 'Organ' }, { name: 'Accordion', family: 'Organ' }, { name: 'Harmonica', family: 'Organ' }, { name: 'Tango Accordion', family: 'Organ' }, // Guitar { name: 'Acoustic Guitar (nylon)', family: 'Guitar' }, { name: 'Acoustic Guitar (steel)', family: 'Guitar' }, { name: 'Electric Guitar (jazz)', family: 'Guitar' }, { name: 'Electric Guitar (clean)', family: 'Guitar' }, { name: 'Electric Guitar (muted)', family: 'Guitar' }, { name: 'Overdriven Guitar', family: 'Guitar' }, { name: 'Distortion Guitar', family: 'Guitar' }, { name: 'Guitar harmonics', family: 'Guitar' }, // Bass { name: 'Acoustic Bass', family: 'Bass' }, { name: 'Electric Bass (finger)', family: 'Bass' }, { name: 'Electric Bass (pick)', family: 'Bass' }, { name: 'Fretless Bass', family: 'Bass' }, { name: 'Slap Bass 1', family: 'Bass' }, { name: 'Slap Bass 2', family: 'Bass' }, { name: 'Synth Bass 1', family: 'Bass' }, { name: 'Synth Bass 2', family: 'Bass' }, // Strings { name: 'Violin', family: 'Strings' }, { name: 'Viola', family: 'Strings' }, { name: 'Cello', family: 'Strings' }, { name: 'Contrabass', family: 'Strings' }, { name: 'Tremolo Strings', family: 'Strings' }, { name: 'Pizzicato Strings', family: 'Strings' }, { name: 'Orchestral Harp', family: 'Strings' }, { name: 'Timpani', family: 'Strings' }, // Ensemble { name: 'String Ensemble 1', family: 'Ensemble' }, { name: 'String Ensemble 2', family: 'Ensemble' }, { name: 'SynthStrings 1', family: 'Ensemble' }, { name: 'SynthStrings 2', family: 'Ensemble' }, { name: 'Choir Aahs', family: 'Ensemble' }, { name: 'Voice Oohs', family: 'Ensemble' }, { name: 'Synth Voice', family: 'Ensemble' }, { name: 'Orchestra Hit', family: 'Ensemble' }, // Brass { name: 'Trumpet', family: 'Brass' }, { name: 'Trombone', family: 'Brass' }, { name: 'Tuba', family: 'Brass' }, { name: 'Muted Trumpet', family: 'Brass' }, { name: 'French Horn', family: 'Brass' }, { name: 'Brass Section', family: 'Brass' }, { name: 'SynthBrass 1', family: 'Brass' }, { name: 'SynthBrass 2', family: 'Brass' }, // Reed { name: 'Soprano Sax', family: 'Reed' }, { name: 'Alto Sax', family: 'Reed' }, { name: 'Tenor Sax', family: 'Reed' }, { name: 'Baritone Sax', family: 'Reed' }, { name: 'Oboe', family: 'Reed' }, { name: 'English Horn', family: 'Reed' }, { name: 'Bassoon', family: 'Reed' }, { name: 'Clarinet', family: 'Reed' }, // Pipe { name: 'Piccolo', family: 'Pipe' }, { name: 'Flute', family: 'Pipe' }, { name: 'Recorder', family: 'Pipe' }, { name: 'Pan Flute', family: 'Pipe' }, { name: 'Blown Bottle', family: 'Pipe' }, { name: 'Shakuhachi', family: 'Pipe' }, { name: 'Whistle', family: 'Pipe' }, { name: 'Ocarina', family: 'Pipe' }, // Synth Lead { name: 'Lead 1 (square)', family: 'Synth Lead' }, { name: 'Lead 2 (sawtooth)', family: 'Synth Lead' }, { name: 'Lead 3 (calliope)', family: 'Synth Lead' }, { name: 'Lead 4 (chiff)', family: 'Synth Lead' }, { name: 'Lead 5 (charang)', family: 'Synth Lead' }, { name: 'Lead 6 (voice)', family: 'Synth Lead' }, { name: 'Lead 7 (fifths)', family: 'Synth Lead' }, { name: 'Lead 8 (bass + lead)', family: 'Synth Lead' }, // Synth Pad { name: 'Pad 1 (new age)', family: 'Synth Pad' }, { name: 'Pad 2 (warm)', family: 'Synth Pad' }, { name: 'Pad 3 (polysynth)', family: 'Synth Pad' }, { name: 'Pad 4 (choir)', family: 'Synth Pad' }, { name: 'Pad 5 (bowed)', family: 'Synth Pad' }, { name: 'Pad 6 (metallic)', family: 'Synth Pad' }, { name: 'Pad 7 (halo)', family: 'Synth Pad' }, { name: 'Pad 8 (sweep)', family: 'Synth Pad' }, // Synth Effects { name: 'FX 1 (rain)', family: 'Synth Effects' }, { name: 'FX 2 (soundtrack)', family: 'Synth Effects' }, { name: 'FX 3 (crystal)', family: 'Synth Effects' }, { name: 'FX 4 (atmosphere)', family: 'Synth Effects' }, { name: 'FX 5 (brightness)', family: 'Synth Effects' }, { name: 'FX 6 (goblins)', family: 'Synth Effects' }, { name: 'FX 7 (echoes)', family: 'Synth Effects' }, { name: 'FX 8 (sci-fi)', family: 'Synth Effects' }, // Ethnic { name: 'Sitar', family: 'Ethnic' }, { name: 'Banjo', family: 'Ethnic' }, { name: 'Shamisen', family: 'Ethnic' }, { name: 'Koto', family: 'Ethnic' }, { name: 'Kalimba', family: 'Ethnic' }, { name: 'Bag pipe', family: 'Ethnic' }, { name: 'Fiddle', family: 'Ethnic' }, { name: 'Shanai', family: 'Ethnic' }, // Percussive { name: 'Tinkle Bell', family: 'Percussive' }, { name: 'Agogo', family: 'Percussive' }, { name: 'Steel Drums', family: 'Percussive' }, { name: 'Woodblock', family: 'Percussive' }, { name: 'Taiko Drum', family: 'Percussive' }, { name: 'Melodic Tom', family: 'Percussive' }, { name: 'Synth Drum', family: 'Percussive' }, { name: 'Reverse Cymbal', family: 'Percussive' }, // Sound Effects { name: 'Guitar Fret Noise', family: 'Sound Effects' }, { name: 'Breath Noise', family: 'Sound Effects' }, { name: 'Seashore', family: 'Sound Effects' }, { name: 'Bird Tweet', family: 'Sound Effects' }, { name: 'Telephone Ring', family: 'Sound Effects' }, { name: 'Helicopter', family: 'Sound Effects' }, { name: 'Applause', family: 'Sound Effects' }, { name: 'Gunshot', family: 'Sound Effects' } ]; } /** * Get General MIDI drum names (Channel 10) * @returns {MidiDrum[]} - Array of drum information */ getGeneralMidiDrums(): MidiDrum[] { return [ { name: 'Acoustic Bass Drum' }, // 35 { name: 'Bass Drum 1' }, // 36 { name: 'Side Stick' }, // 37 { name: 'Acoustic Snare' }, // 38 { name: 'Hand Clap' }, // 39 { name: 'Electric Snare' }, // 40 { name: 'Low Floor Tom' }, // 41 { name: 'Closed Hi Hat' }, // 42 { name: 'High Floor Tom' }, // 43 { name: 'Pedal Hi-Hat' }, // 44 { name: 'Low Tom' }, // 45 { name: 'Open Hi-Hat' }, // 46 { name: 'Low-Mid Tom' }, // 47 { name: 'Hi Mid Tom' }, // 48 { name: 'Crash Cymbal 1' }, // 49 { name: 'High Tom' }, // 50 { name: 'Ride Cymbal 1' }, // 51 { name: 'Chinese Cymbal' }, // 52 { name: 'Ride Bell' }, // 53 { name: 'Tambourine' }, // 54 { name: 'Splash Cymbal' }, // 55 { name: 'Cowbell' }, // 56 { name: 'Crash Cymbal 2' }, // 57 { name: 'Vibraslap' }, // 58 { name: 'Ride Cymbal 2' }, // 59 { name: 'Hi Bongo' }, // 60 { name: 'Low Bongo' }, // 61 { name: 'Mute Hi Conga' }, // 62 { name: 'Open Hi Conga' }, // 63 { name: 'Low Conga' }, // 64 { name: 'High Timbale' }, // 65 { name: 'Low Timbale' }, // 66 { name: 'High Agogo' }, // 67 { name: 'Low Agogo' }, // 68 { name: 'Cabasa' }, // 69 { name: 'Maracas' }, // 70 { name: 'Short Whistle' }, // 71 { name: 'Long Whistle' }, // 72 { name: 'Short Guiro' }, // 73 { name: 'Long Guiro' }, // 74 { name: 'Claves' }, // 75 { name: 'Hi Wood Block' }, // 76 { name: 'Low Wood Block' }, // 77 { name: 'Mute Cuica' }, // 78 { name: 'Open Cuica' }, // 79 { name: 'Mute Triangle' }, // 80 { name: 'Open Triangle' } // 81 ]; } /** * Close the MIDI output */ close(): void { if (this.virtualOutput) { this.virtualOutput.close(); this.virtualOutput = null; } } }

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/tezza1971/mcp-midi'

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