Skip to main content
Glama
midi.py28 kB
""" MIDI Operations Tools for REAPER MCP This module contains tools for MIDI operations. """ from typing import Optional, List from ..bridge import bridge # ============================================================================ # MIDI Note Operations (5 tools) # ============================================================================ async def insert_midi_note(item_index: int, take_index: int, pitch: int, velocity: int, start_time: float, duration: float, channel: int = 0, selected: bool = False, muted: bool = False) -> str: """Insert a MIDI note into a take""" # Use the combined bridge function that handles all operations in one call result = await bridge.call_lua("InsertMIDINoteToItemTake", [ item_index, take_index, pitch, velocity, start_time, duration, channel, selected, muted, 0, 0 # Changed None to 0 for reserved parameters ]) if result.get("ok"): return f"Inserted MIDI note: pitch={pitch}, velocity={velocity}, start={start_time:.3f}s, duration={duration:.3f}s" else: raise Exception(f"Failed to insert MIDI note: {result.get('error', 'Unknown error')}") async def midi_insert_note(item_index: int, take_index: int, pitch: int, velocity: int, start_time: float, duration: float, channel: int = 0, selected: bool = False, muted: bool = False) -> str: """Insert a MIDI note into a take (alias)""" return await insert_midi_note(item_index, take_index, pitch, velocity, start_time, duration, channel, selected, muted) async def midi_get_note_name(note_number: int) -> str: """Get the name of a MIDI note from its number""" # Note names note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] octave = (note_number // 12) - 1 note_name = note_names[note_number % 12] return f"Note name: {note_name}{octave}" async def get_track_midi_note_name(track_index: int, pitch: int) -> str: """Get the custom name for a MIDI note on a track""" # Get track track_result = await bridge.call_lua("GetTrack", [0, track_index]) if not track_result.get("ok") or not track_result.get("ret"): raise Exception(f"Failed to find track at index {track_index}") result = await bridge.call_lua("GetTrackMIDINoteNameEx", [0, track_result.get("ret"), pitch, 0]) if result.get("ok"): name = result.get("ret", "") if name: return f"Note {pitch} name on track {track_index}: {name}" else: # Fall back to standard note name return await midi_get_note_name(pitch) else: raise Exception(f"Failed to get note name: {result.get('error', 'Unknown error')}") async def midi_sort(item_index: int, take_index: int) -> str: """Sort MIDI events in a take""" # Use a combined bridge function that handles indices result = await bridge.call_lua("SortMIDIInItemTake", [item_index, take_index]) if result.get("ok"): return "MIDI events sorted successfully" else: raise Exception(f"Failed to sort MIDI: {result.get('error', 'Unknown error')}") # ============================================================================ # MIDI Event Operations (3 tools) # ============================================================================ async def midi_insert_evt(item_index: int, take_index: int, ppq_pos: float, event_type: str, data1: int, data2: int = 0, channel: int = 0, selected: bool = False, muted: bool = False) -> str: """Insert a MIDI event at a specific PPQ position""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # For note events, use MIDI_InsertNote which is more reliable if event_type in ["note_on", "note_off"]: # For note_on, we need to find a reasonable end time # For note_off, we just insert a very short note if event_type == "note_on": # Default to 1 quarter note duration ppq_end = ppq_pos + 960 else: # Very short note for note_off ppq_end = ppq_pos + 1 result = await bridge.call_lua("MIDI_InsertNote", [ take_handle, selected, muted, ppq_pos, ppq_end, channel, data1, data2, True ]) if result.get("ok"): return f"Inserted MIDI {event_type} event at PPQ {ppq_pos}: data1={data1}, data2={data2}" else: raise Exception(f"Failed to insert MIDI event: {result.get('error', 'Unknown error')}") # For CC events, use MIDI_InsertCC elif event_type == "cc": # MIDI_InsertCC needs an extra parameter for the "noSort" flag result = await bridge.call_lua("MIDI_InsertCC", [ take_handle, selected, muted, ppq_pos, 0xB0 + channel, channel, data1, data2 ]) if result.get("ok"): return f"Inserted MIDI {event_type} event at PPQ {ppq_pos}: data1={data1}, data2={data2}" else: raise Exception(f"Failed to insert MIDI event: {result.get('error', 'Unknown error')}") # For other events, we'll need to implement them differently # For now, just return a message that they're not yet supported else: return f"MIDI {event_type} events not yet fully supported - PPQ {ppq_pos}: data1={data1}, data2={data2}" async def midi_insert_text_sysex_evt(item_index: int, take_index: int, ppq_pos: float, event_type: str, text: str, selected: bool = False, muted: bool = False) -> str: """Insert a text or sysex event""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Text event type mapping text_types = { "text": 1, "copyright": 2, "track_name": 3, "instrument": 4, "lyric": 5, "marker": 6, "cue": 7, "program_name": 8, "device_name": 9, "sysex": -1 } if event_type not in text_types: raise Exception(f"Invalid text event type: {event_type}. Valid types: {list(text_types.keys())}") type_num = text_types[event_type] result = await bridge.call_lua("MIDI_InsertTextSysexEvt", [ take_handle, selected, muted, ppq_pos, type_num, text ]) if result.get("ok"): return f"Inserted {event_type} event at PPQ {ppq_pos}: '{text}'" else: raise Exception(f"Failed to insert text/sysex event: {result.get('error', 'Unknown error')}") async def midi_delete_event(item_index: int, take_index: int, event_index: int) -> str: """Delete a MIDI event""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Delete the event result = await bridge.call_lua("MIDI_DeleteEvt", [take_handle, event_index]) if result.get("ok"): return f"Deleted MIDI event at index {event_index}" else: raise Exception(f"Failed to delete MIDI event: {result.get('error', 'Unknown error')}") # ============================================================================ # MIDI CC Operations (1 tool) # ============================================================================ async def insert_midi_cc(item_index: int, take_index: int, time: float, channel: int, cc_number: int, value: int, selected: bool = False, muted: bool = False) -> str: """Insert a MIDI CC event""" # Use the combined bridge function that handles all operations in one call result = await bridge.call_lua("InsertMIDICCToItemTake", [ item_index, take_index, time, channel, cc_number, value, selected ]) if result.get("ok"): return f"Inserted MIDI CC: CC{cc_number}={value} at {time:.3f}s on channel {channel}" else: raise Exception(f"Failed to insert MIDI CC: {result.get('error', 'Unknown error')}") # ============================================================================ # MIDI Event Management (5 tools) # ============================================================================ async def midi_count_events(item_index: int = 0, take_index: int = 0) -> str: """Count MIDI events in a take""" # Use the combined bridge function that gets item, take and counts in one call result = await bridge.call_lua("GetItemTakeAndCountMIDI", [item_index, take_index]) if result.get("ok"): notes = result.get("notes", 0) ccs = result.get("cc", 0) text_events = result.get("text", 0) return f"MIDI event counts: notes={notes}, CCs={ccs}, sysex={text_events}" else: raise Exception(f"Failed to count MIDI events: {result.get('error', 'Unknown error')}") async def midi_select_all(item_index: int = 0, take_index: int = 0) -> str: """Select all MIDI events in a take""" # Use the combined bridge function result = await bridge.call_lua("SelectAllMIDIInItemTake", [item_index, take_index]) if result.get("ok"): return "Selected all MIDI events" else: raise Exception(f"Failed to select MIDI events: {result.get('error', 'Unknown error')}") async def midi_get_all_events(item_index: int = 0, take_index: int = 0) -> str: """Get all MIDI events from a take""" # Note: MIDI_GetAllEvts returns binary data that cannot be easily transferred via JSON # For now, we'll just count the events instead result = await bridge.call_lua("GetItemTakeAndCountMIDI", [item_index, take_index]) if result.get("ok"): notes = result.get("notes", 0) ccs = result.get("cc", 0) text_events = result.get("text", 0) total_events = notes + ccs + text_events return f"MIDI events data: {total_events} total events (notes={notes}, CCs={ccs}, text={text_events})" else: raise Exception(f"Failed to get MIDI events: {result.get('error', 'Unknown error')}") async def midi_get_scale(item_index: int = 0, take_index: int = 0) -> str: """Get the scale setting for a MIDI take""" # Note: MIDI_GetScale is not available in the current REAPER API # This is a placeholder implementation return "Scale: root=0, type=0, name= (Note: MIDI scale functions not available in this REAPER version)" async def midi_set_scale(item_index: int = 0, take_index: int = 0, root: int = 0, scale: int = 0, channel: int = 0) -> str: """Set the scale for a MIDI take""" # Note: MIDI_SetScale is not available in the current REAPER API # This is a placeholder implementation scale_names = ["Major", "Minor", "Harmonic minor", "Melodic minor", "Dorian", "Phrygian", "Lydian", "Mixolydian", "Aeolian", "Locrian"] scale_name = scale_names[scale] if 0 <= scale < len(scale_names) else f"Custom ({scale})" return f"Set MIDI scale: root={root}, scale={scale} ({scale_name}) (Note: MIDI scale functions not available in this REAPER version)" # ============================================================================ # MIDI Hardware (4 tools) # ============================================================================ async def get_num_midi_inputs() -> str: """Get the number of MIDI input devices""" result = await bridge.call_lua("GetNumMIDIInputs", []) if result.get("ok"): count = result.get("ret", 0) return f"Number of MIDI inputs: {count}" else: raise Exception(f"Failed to get number of MIDI inputs: {result.get('error', 'Unknown error')}") async def get_num_midi_outputs() -> str: """Get the number of MIDI output devices""" result = await bridge.call_lua("GetNumMIDIOutputs", []) if result.get("ok"): count = result.get("ret", 0) return f"Number of MIDI outputs: {count}" else: raise Exception(f"Failed to get number of MIDI outputs: {result.get('error', 'Unknown error')}") async def get_midi_input_name(input_index: int) -> str: """Get the name of a MIDI input device""" result = await bridge.call_lua("GetMIDIInputName", [input_index, "", 256]) if result.get("ok"): # The function returns success and the name as a string name = result.get("ret", "Unknown") return f"MIDI input {input_index}: {name}" else: raise Exception(f"Failed to get MIDI input name: {result.get('error', 'Unknown error')}") async def get_midi_output_name(output_index: int) -> str: """Get the name of a MIDI output device""" result = await bridge.call_lua("GetMIDIOutputName", [output_index, "", 256]) if result.get("ok"): # The function returns success and the name as a string name = result.get("ret", "Unknown") return f"MIDI output {output_index}: {name}" else: raise Exception(f"Failed to get MIDI output name: {result.get('error', 'Unknown error')}") # ============================================================================ # MIDI Extended Operations (8 tools) # ============================================================================ async def midi_get_evt(item_index: int, take_index: int, event_index: int) -> str: """Get a MIDI event by index""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Get the event result = await bridge.call_lua("MIDI_GetEvt", [take_handle, event_index, False, False, 0, ""]) if result.get("ok"): ret = result.get("ret", []) if isinstance(ret, list) and len(ret) >= 5: retval, selected, muted, ppq_pos, msg = ret[:5] if retval: # Try to decode the message msg_info = "Unknown event" if msg and len(msg) > 0: status = ord(msg[0]) if isinstance(msg, str) else msg[0] event_type = (status & 0xF0) >> 4 channel = status & 0x0F type_names = { 0x8: "Note Off", 0x9: "Note On", 0xA: "Aftertouch", 0xB: "CC", 0xC: "Program Change", 0xD: "Channel Pressure", 0xE: "Pitch Bend" } msg_info = type_names.get(event_type, f"Type {event_type}") return f"Event {event_index}: PPQ={ppq_pos:.1f}, Type={msg_info}, Channel={channel}, Selected={selected}, Muted={muted}" else: return f"Event {event_index} not found" else: return f"Invalid response format for event {event_index}" else: raise Exception(f"Failed to get MIDI event: {result.get('error', 'Unknown error')}") async def midi_get_grid(item_index: int, take_index: int) -> str: """Get MIDI grid settings""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Get grid settings result = await bridge.call_lua("MIDI_GetGrid", [take_handle]) if result.get("ok"): ret = result.get("ret", []) if isinstance(ret, list) and len(ret) >= 2: qn_grid = ret[0] if len(ret) > 0 else 0 swing = ret[1] if len(ret) > 1 else 0 return f"MIDI grid: {qn_grid:.3f} quarter notes, swing={swing:.1f}%" else: return "MIDI grid: default settings" else: raise Exception(f"Failed to get MIDI grid: {result.get('error', 'Unknown error')}") async def midi_get_ppq_pos_from_proj_time(item_index: int, take_index: int, time: float) -> str: """Convert project time to PPQ position""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Convert time to PPQ result = await bridge.call_lua("MIDI_GetPPQPosFromProjTime", [take_handle, time]) if result.get("ok"): ppq = result.get("ret", 0.0) return f"Time {time:.3f}s = PPQ {ppq:.1f}" else: raise Exception(f"Failed to convert time to PPQ: {result.get('error', 'Unknown error')}") async def midi_get_proj_time_from_ppq_pos(item_index: int, take_index: int, ppq_pos: float) -> str: """Convert PPQ position to project time""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Convert PPQ to time result = await bridge.call_lua("MIDI_GetProjTimeFromPPQPos", [take_handle, ppq_pos]) if result.get("ok"): time = result.get("ret", 0.0) return f"PPQ {ppq_pos:.1f} = Time {time:.3f}s" else: raise Exception(f"Failed to convert PPQ to time: {result.get('error', 'Unknown error')}") async def midi_get_ppq_pos_start_of_measure(item_index: int, take_index: int, ppq_pos: float) -> str: """Get PPQ position of the start of measure for a given PPQ position""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Get start of measure result = await bridge.call_lua("MIDI_GetPPQPos_StartOfMeasure", [take_handle, ppq_pos]) if result.get("ok"): measure_start_ppq = result.get("ret", 0.0) return f"Start of measure for PPQ {ppq_pos:.1f} is PPQ {measure_start_ppq:.1f}" else: raise Exception(f"Failed to get start of measure: {result.get('error', 'Unknown error')}") async def midi_get_ppq_pos_end_of_measure(item_index: int, take_index: int, ppq_pos: float) -> str: """Get PPQ position of the end of measure for a given PPQ position""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Get end of measure result = await bridge.call_lua("MIDI_GetPPQPos_EndOfMeasure", [take_handle, ppq_pos]) if result.get("ok"): measure_end_ppq = result.get("ret", 0.0) return f"End of measure for PPQ {ppq_pos:.1f} is PPQ {measure_end_ppq:.1f}" else: raise Exception(f"Failed to get end of measure: {result.get('error', 'Unknown error')}") async def midi_enum_sel_notes(item_index: int, take_index: int) -> str: """Enumerate selected MIDI notes""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Enumerate selected notes selected_notes = [] note_idx = -1 while True: result = await bridge.call_lua("MIDI_EnumSelNotes", [take_handle, note_idx]) if result.get("ok"): next_idx = result.get("ret", -1) if next_idx == -1: break # Get note info note_result = await bridge.call_lua("MIDI_GetNote", [take_handle, next_idx]) if note_result.get("ok"): ret = note_result.get("ret", []) if isinstance(ret, list) and len(ret) >= 7: retval, selected, muted, start_ppq, end_ppq, channel, pitch, velocity = ret[:8] if retval and selected: selected_notes.append({ "index": next_idx, "pitch": pitch, "velocity": velocity, "start_ppq": start_ppq }) note_idx = next_idx else: break if selected_notes: notes_info = ", ".join([f"Note {n['index']}: pitch={n['pitch']}" for n in selected_notes[:5]]) if len(selected_notes) > 5: notes_info += f", ... ({len(selected_notes) - 5} more)" return f"Selected notes ({len(selected_notes)}): {notes_info}" else: return "No notes selected" async def midi_set_item_extents(item_index: int, take_index: int, start_qn: float, end_qn: float) -> str: """Set MIDI item extents in quarter notes""" # Get media item and take item_result = await bridge.call_lua("GetMediaItem", [0, item_index]) if not item_result.get("ok") or not item_result.get("ret"): raise Exception(f"Failed to find media item at index {item_index}") item_handle = item_result.get("ret") take_result = await bridge.call_lua("GetMediaItemTake", [item_handle, take_index]) if not take_result.get("ok") or not take_result.get("ret"): raise Exception(f"Failed to find take at index {take_index}") take_handle = take_result.get("ret") # Set item extents result = await bridge.call_lua("MIDI_SetItemExtents", [item_handle, start_qn, end_qn]) if result.get("ok"): return f"Set MIDI item extents: start={start_qn:.1f} QN, end={end_qn:.1f} QN" else: raise Exception(f"Failed to set MIDI item extents: {result.get('error', 'Unknown error')}") # ============================================================================ # Registration Function # ============================================================================ def register_midi_tools(mcp) -> int: """Register all MIDI tools with the MCP instance""" tools = [ # MIDI Note Operations (insert_midi_note, "Insert a MIDI note into a take"), (midi_insert_note, "Insert a MIDI note into a take (alias)"), (midi_get_note_name, "Get the name of a MIDI note from its number"), (get_track_midi_note_name, "Get the custom name for a MIDI note on a track"), (midi_sort, "Sort MIDI events in a take"), # MIDI Event Operations (midi_insert_evt, "Insert a MIDI event at a specific PPQ position"), (midi_insert_text_sysex_evt, "Insert a text or sysex event"), (midi_delete_event, "Delete a MIDI event"), # MIDI CC Operations (insert_midi_cc, "Insert a MIDI CC event"), # MIDI Event Management (midi_count_events, "Count MIDI events in a take"), (midi_select_all, "Select all MIDI events in a take"), (midi_get_all_events, "Get all MIDI events from a take"), (midi_get_scale, "Get the scale setting for a MIDI take"), (midi_set_scale, "Set the scale for a MIDI take"), # MIDI Hardware (get_num_midi_inputs, "Get the number of MIDI input devices"), (get_num_midi_outputs, "Get the number of MIDI output devices"), (get_midi_input_name, "Get the name of a MIDI input device"), (get_midi_output_name, "Get the name of a MIDI output device"), # MIDI Extended Operations (midi_get_evt, "Get a MIDI event by index"), (midi_get_grid, "Get MIDI grid settings"), (midi_get_ppq_pos_from_proj_time, "Convert project time to PPQ position"), (midi_get_proj_time_from_ppq_pos, "Convert PPQ position to project time"), (midi_get_ppq_pos_start_of_measure, "Get PPQ position of the start of measure"), (midi_get_ppq_pos_end_of_measure, "Get PPQ position of the end of measure"), (midi_enum_sel_notes, "Enumerate selected MIDI notes"), (midi_set_item_extents, "Set MIDI item extents in quarter notes"), ] # Register each tool for func, desc in tools: decorated = mcp.tool()(func) return len(tools)

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/shiehn/total-reaper-mcp'

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