Skip to main content
Glama
advanced_midi_generation.py26.3 kB
"""Advanced MIDI generation and manipulation tools.""" from typing import Dict, Any, List, Optional, Tuple from .bridge_sync import ReaperBridge def create_new_midi_item(track_index: int, start_time: float, end_time: float, start_in_qn: Optional[float] = None) -> Dict[str, Any]: """Create a new MIDI item on a track. Args: track_index: Index of the track start_time: Start position in seconds end_time: End position in seconds start_in_qn: Start position in quarter notes (optional) Returns: Dict containing: - item: Item handle - take: Take handle for the MIDI item - success: Operation status """ # Get track handle track_request = {"action": "GetTrack", "proj": 0, "trackidx": track_index} track_response = ReaperBridge.send_request(track_request) if not track_response.get("result"): return { "success": False, "error": f"Track at index {track_index} not found" } track_handle = track_response.get("track") # Create MIDI item request = { "action": "CreateNewMIDIItemInProj", "track": track_handle, "starttime": start_time, "endtime": end_time, "qnInOptional": start_in_qn is not None } if start_in_qn is not None: request["startInQN"] = start_in_qn response = ReaperBridge.send_request(request) if response.get("result"): item_handle = response.get("item") # Get the active take take_request = { "action": "GetActiveTake", "item": item_handle } take_response = ReaperBridge.send_request(take_request) return { "success": True, "item": item_handle, "take": take_response.get("take"), "track_index": track_index, "start_time": start_time, "end_time": end_time } return { "success": False, "error": "Failed to create MIDI item" } def get_midi_hash(take_handle: Any) -> Dict[str, Any]: """Get hash of MIDI data for comparison/versioning. Args: take_handle: MIDI take handle Returns: Dict containing: - hash: MIDI content hash - notes_only: Whether hash includes only notes """ request = { "action": "MIDI_GetHash", "take": take_handle, "notesonly": True } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "hash": response.get("hash", ""), "notes_only": True } def get_ppq_position_from_time(take_handle: Any, time: float) -> Dict[str, Any]: """Convert time to MIDI PPQ position. Args: take_handle: MIDI take handle time: Time in seconds Returns: Dict containing PPQ position """ request = { "action": "MIDI_GetPPQPosFromProjTime", "take": take_handle, "time": time } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "ppq_pos": response.get("ppqpos", 0.0) } def get_ppq_pos_end_of_measure(take_handle: Any, ppq_pos: float) -> Dict[str, Any]: """Get PPQ position at the end of the measure containing the given position. Args: take_handle: MIDI take handle ppq_pos: PPQ position Returns: Dict containing end of measure PPQ position """ request = { "action": "MIDI_GetPPQPos_EndOfMeasure", "take": take_handle, "ppqpos": ppq_pos } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "ppq_end": response.get("ppq_end", ppq_pos) } def get_ppq_pos_start_of_measure(take_handle: Any, ppq_pos: float) -> Dict[str, Any]: """Get PPQ position at the start of the measure containing the given position. Args: take_handle: MIDI take handle ppq_pos: PPQ position Returns: Dict containing start of measure PPQ position """ request = { "action": "MIDI_GetPPQPos_StartOfMeasure", "take": take_handle, "ppqpos": ppq_pos } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "ppq_start": response.get("ppq_start", ppq_pos) } def get_midi_grid(take_handle: Any) -> Dict[str, Any]: """Get MIDI editor grid settings for a take. Args: take_handle: MIDI take handle Returns: Dict containing: - division: Grid division (e.g., 0.25 for 1/4 note) - swing: Swing amount - notelen: Default note length """ request = { "action": "MIDI_GetGrid", "take": take_handle } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "division": response.get("division", 0.25), "swing": response.get("swingamt", 0.0), "note_length": response.get("notelen", 0.25) } def select_midi_notes(take_handle: Any, start_ppq: float, end_ppq: float, channel: int = -1, pitch_low: int = 0, pitch_high: int = 127) -> Dict[str, Any]: """Select MIDI notes within specified criteria. Args: take_handle: MIDI take handle start_ppq: Start position in PPQ end_ppq: End position in PPQ channel: MIDI channel (-1 for all) pitch_low: Lowest pitch to select pitch_high: Highest pitch to select Returns: Dict containing number of notes selected """ # First deselect all desel_request = { "action": "MIDI_SelectAll", "take": take_handle, "select": False } ReaperBridge.send_request(desel_request) # Count total notes count_request = { "action": "MIDI_CountEvts", "take": take_handle } count_response = ReaperBridge.send_request(count_request) note_count = count_response.get("notes", 0) selected = 0 # Select notes matching criteria for i in range(note_count): note_request = { "action": "MIDI_GetNote", "take": take_handle, "noteidx": i } note_response = ReaperBridge.send_request(note_request) if note_response.get("result"): note_start = note_response.get("startppqpos", 0) note_end = note_response.get("endppqpos", 0) note_pitch = note_response.get("pitch", 60) note_chan = note_response.get("chan", 0) # Check if note matches criteria if (start_ppq <= note_start < end_ppq and pitch_low <= note_pitch <= pitch_high and (channel == -1 or channel == note_chan)): # Select the note set_request = { "action": "MIDI_SetNote", "take": take_handle, "noteidx": i, "selectedInOptional": True, "mutedInOptional": note_response.get("muted", False), "startppqposInOptional": note_start, "endppqposInOptional": note_end, "chanInOptional": note_chan, "pitchInOptional": note_pitch, "velInOptional": note_response.get("vel", 80), "noSortInOptional": True } set_response = ReaperBridge.send_request(set_request) if set_response.get("result"): selected += 1 return { "success": True, "notes_selected": selected, "total_notes": note_count } def get_track_midi_note_range(track_index: int) -> Dict[str, Any]: """Get MIDI note name range for a track. Args: track_index: Track index Returns: Dict containing note range info """ # Get track handle track_request = {"action": "GetTrack", "proj": 0, "trackidx": track_index} track_response = ReaperBridge.send_request(track_request) if not track_response.get("result"): return { "success": False, "error": f"Track at index {track_index} not found" } track_handle = track_response.get("track") # Get note range range_request = { "action": "GetTrackMIDINoteRange", "track": track_handle } range_response = ReaperBridge.send_request(range_request) return { "success": range_response.get("result", False), "note_low": range_response.get("note_lo", 0), "note_high": range_response.get("note_hi", 127) } def set_track_midi_note_range(track_index: int, note_low: int, note_high: int) -> Dict[str, Any]: """Set MIDI note range constraints for a track. Args: track_index: Track index note_low: Lowest allowed note (0-127) note_high: Highest allowed note (0-127) Returns: Dict containing operation result """ # Get track handle track_request = {"action": "GetTrack", "proj": 0, "trackidx": track_index} track_response = ReaperBridge.send_request(track_request) if not track_response.get("result"): return { "success": False, "error": f"Track at index {track_index} not found" } track_handle = track_response.get("track") # Set note range range_request = { "action": "SetTrackMIDINoteRange", "track": track_handle, "note_lo": max(0, min(127, note_low)), "note_hi": max(0, min(127, note_high)) } range_response = ReaperBridge.send_request(range_request) return { "success": range_response.get("result", False), "note_low": note_low, "note_high": note_high } def insert_midi_note_extended(take_handle: Any, pitch: int, velocity: int, start_beats: float, length_beats: float, channel: int = 0, selected: bool = False) -> Dict[str, Any]: """Insert MIDI note with musical timing (beats instead of PPQ). Args: take_handle: MIDI take handle pitch: Note pitch (0-127) velocity: Note velocity (0-127) start_beats: Start position in beats length_beats: Length in beats channel: MIDI channel (0-15) selected: Whether note is selected Returns: Dict containing operation result """ # Convert beats to PPQ (960 PPQ per quarter note) ppq_per_beat = 960 start_ppq = start_beats * ppq_per_beat end_ppq = (start_beats + length_beats) * ppq_per_beat request = { "action": "MIDI_InsertNote", "take": take_handle, "selected": selected, "muted": False, "startppqpos": start_ppq, "endppqpos": end_ppq, "chan": channel, "pitch": pitch, "vel": velocity, "noSortInOptional": False } response = ReaperBridge.send_request(request) return { "success": response.get("result", False), "pitch": pitch, "velocity": velocity, "start_beats": start_beats, "length_beats": length_beats, "channel": channel } def generate_chord_progression(take_handle: Any, progression: List[Dict[str, Any]], start_beat: float = 0.0) -> Dict[str, Any]: """Generate a chord progression in a MIDI take. Args: take_handle: MIDI take handle progression: List of chord dicts with 'root', 'type', 'duration_beats' start_beat: Starting position in beats Returns: Dict containing number of notes created """ # Chord templates (intervals from root) chord_types = { "major": [0, 4, 7], "minor": [0, 3, 7], "dim": [0, 3, 6], "aug": [0, 4, 8], "maj7": [0, 4, 7, 11], "min7": [0, 3, 7, 10], "dom7": [0, 4, 7, 10], "sus2": [0, 2, 7], "sus4": [0, 5, 7] } notes_created = 0 current_beat = start_beat for chord in progression: root = chord.get("root", 60) chord_type = chord.get("type", "major") duration = chord.get("duration_beats", 4.0) velocity = chord.get("velocity", 80) intervals = chord_types.get(chord_type, chord_types["major"]) # Insert each note of the chord for interval in intervals: pitch = root + interval # Keep within MIDI range while pitch > 127: pitch -= 12 while pitch < 0: pitch += 12 result = insert_midi_note_extended( take_handle, pitch, velocity, current_beat, duration ) if result["success"]: notes_created += 1 current_beat += duration # Sort notes after insertion sort_request = { "action": "MIDI_Sort", "take": take_handle } ReaperBridge.send_request(sort_request) return { "success": notes_created > 0, "notes_created": notes_created, "duration_beats": current_beat - start_beat } def generate_scale_run(take_handle: Any, scale_type: str, root: int, start_beat: float, note_duration: float, num_octaves: int = 2, direction: str = "up") -> Dict[str, Any]: """Generate a scale run in a MIDI take. Args: take_handle: MIDI take handle scale_type: Type of scale (major, minor, etc.) root: Root note start_beat: Starting position in beats note_duration: Duration of each note in beats num_octaves: Number of octaves to span direction: "up", "down", or "both" Returns: Dict containing notes created """ # Scale intervals scales = { "major": [0, 2, 4, 5, 7, 9, 11], "minor": [0, 2, 3, 5, 7, 8, 10], "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], "pentatonic": [0, 2, 4, 7, 9], "blues": [0, 3, 5, 6, 7, 10], "chromatic": list(range(12)) } intervals = scales.get(scale_type, scales["major"]) notes_created = 0 current_beat = start_beat # Generate scale notes notes = [] for octave in range(num_octaves): for interval in intervals: pitch = root + (octave * 12) + interval if 0 <= pitch <= 127: notes.append(pitch) # Apply direction if direction == "down": notes.reverse() elif direction == "both": notes = notes + notes[-2::-1] # Up then down, skip repeated top note # Insert notes for pitch in notes: result = insert_midi_note_extended( take_handle, pitch, 80, current_beat, note_duration ) if result["success"]: notes_created += 1 current_beat += note_duration return { "success": notes_created > 0, "notes_created": notes_created, "duration_beats": current_beat - start_beat, "scale_type": scale_type, "direction": direction } def register_advanced_midi_tools(mcp): """Register advanced MIDI generation tools with MCP server.""" from functools import wraps # Helper to wrap sync functions for async def async_wrapper(func): @wraps(func) async def wrapper(**kwargs): return func(**kwargs) return wrapper # Register all advanced MIDI tools tool_functions = [ ("create_new_midi_item", create_new_midi_item), ("get_midi_hash", get_midi_hash), ("get_ppq_position_from_time", get_ppq_position_from_time), ("get_ppq_pos_end_of_measure", get_ppq_pos_end_of_measure), ("get_ppq_pos_start_of_measure", get_ppq_pos_start_of_measure), ("get_midi_grid", get_midi_grid), ("select_midi_notes", select_midi_notes), ("get_track_midi_note_range", get_track_midi_note_range), ("set_track_midi_note_range", set_track_midi_note_range), ("insert_midi_note_extended", insert_midi_note_extended), ("generate_chord_progression", generate_chord_progression), ("generate_scale_run", generate_scale_run), ] # Find the corresponding tool definition and register for tool_name, tool_func in tool_functions: tool_def = next((t for t in tools if t["name"] == tool_name), None) if tool_def: mcp.tool( name=tool_name, description=tool_def["description"] )(async_wrapper(tool_func)) return len(tool_functions) # Tool definitions for MCP tools = [ { "name": "create_new_midi_item", "description": "Make a blank canvas for notes. Use when users want to start writing music from scratch.", "input_schema": { "type": "object", "properties": { "track_index": {"type": "integer", "description": "Track index"}, "start_time": {"type": "number", "description": "Start position in seconds"}, "end_time": {"type": "number", "description": "End position in seconds"}, "start_in_qn": {"type": "number", "description": "Start in quarter notes (optional)"} }, "required": ["track_index", "start_time", "end_time"] } }, { "name": "get_midi_hash", "description": "Get a hash of MIDI content for comparison and versioning. Useful for detecting changes, creating variations, or implementing version control for generated patterns. Hash changes when MIDI content changes.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"} }, "required": ["take_handle"] } }, { "name": "get_ppq_position_from_time", "description": "Convert time in seconds to MIDI PPQ (pulses per quarter note) position. Essential for precise MIDI timing when working with time-based positions. Standard is 960 PPQ per quarter note.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "time": {"type": "number", "description": "Time in seconds"} }, "required": ["take_handle", "time"] } }, { "name": "get_ppq_pos_end_of_measure", "description": "Get the PPQ position at the end of the measure containing the given position. Useful for aligning generated patterns to measure boundaries and ensuring musical phrase completion.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "ppq_pos": {"type": "number", "description": "PPQ position"} }, "required": ["take_handle", "ppq_pos"] } }, { "name": "get_ppq_pos_start_of_measure", "description": "Get the PPQ position at the start of the measure containing the given position. Essential for starting patterns on downbeats and maintaining musical alignment with bar lines.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "ppq_pos": {"type": "number", "description": "PPQ position"} }, "required": ["take_handle", "ppq_pos"] } }, { "name": "get_midi_grid", "description": "Get MIDI editor grid settings including division, swing, and default note length. Use to align generated content with the user's preferred grid settings for consistent timing.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"} }, "required": ["take_handle"] } }, { "name": "select_midi_notes", "description": "Highlight specific notes for editing. Use when users want to modify certain pitches or note ranges.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "start_ppq": {"type": "number", "description": "Start position in PPQ"}, "end_ppq": {"type": "number", "description": "End position in PPQ"}, "channel": {"type": "integer", "description": "MIDI channel (-1 for all)", "default": -1}, "pitch_low": {"type": "integer", "description": "Lowest pitch to select", "default": 0}, "pitch_high": {"type": "integer", "description": "Highest pitch to select", "default": 127} }, "required": ["take_handle", "start_ppq", "end_ppq"] } }, { "name": "get_track_midi_note_range", "description": "Get the MIDI note range constraints for a track. Shows the allowed note range which can be useful for instrument-specific generation (e.g., bass ranges, lead ranges).", "input_schema": { "type": "object", "properties": { "track_index": {"type": "integer", "description": "Track index"} }, "required": ["track_index"] } }, { "name": "set_track_midi_note_range", "description": "Set MIDI note range constraints for a track. Limits the notes that can be played, useful for keeping generated content within realistic instrument ranges (e.g., bass guitar, violin).", "input_schema": { "type": "object", "properties": { "track_index": {"type": "integer", "description": "Track index"}, "note_low": {"type": "integer", "description": "Lowest allowed note (0-127)"}, "note_high": {"type": "integer", "description": "Highest allowed note (0-127)"} }, "required": ["track_index", "note_low", "note_high"] } }, { "name": "insert_midi_note_extended", "description": "Add a single musical note. Use for specific note requests like 'add a C' or 'put a kick drum hit'.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "pitch": {"type": "integer", "description": "Note pitch (0-127)"}, "velocity": {"type": "integer", "description": "Note velocity (0-127)"}, "start_beats": {"type": "number", "description": "Start position in beats"}, "length_beats": {"type": "number", "description": "Length in beats"}, "channel": {"type": "integer", "description": "MIDI channel (0-15)", "default": 0}, "selected": {"type": "boolean", "description": "Whether note is selected", "default": False} }, "required": ["take_handle", "pitch", "velocity", "start_beats", "length_beats"] } }, { "name": "generate_chord_progression", "description": "Create chord sequences automatically. Use when users want harmony, chord progressions, or accompaniment.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "progression": { "type": "array", "description": "List of chords with root, type, duration", "items": { "type": "object", "properties": { "root": {"type": "integer", "description": "Root note (0-127)"}, "type": {"type": "string", "description": "Chord type"}, "duration_beats": {"type": "number", "description": "Duration in beats"}, "velocity": {"type": "integer", "description": "Velocity (0-127)"} } } }, "start_beat": {"type": "number", "description": "Starting position in beats", "default": 0.0} }, "required": ["take_handle", "progression"] } }, { "name": "generate_scale_run", "description": "Create melodic runs and scales. Use for solos, arpeggios, or melodic passages.", "input_schema": { "type": "object", "properties": { "take_handle": {"type": "any", "description": "MIDI take handle"}, "scale_type": { "type": "string", "description": "Scale type", "enum": ["major", "minor", "harmonic_minor", "pentatonic", "blues", "chromatic"] }, "root": {"type": "integer", "description": "Root note (0-127)"}, "start_beat": {"type": "number", "description": "Start position in beats"}, "note_duration": {"type": "number", "description": "Duration per note in beats"}, "num_octaves": {"type": "integer", "description": "Number of octaves", "default": 2}, "direction": { "type": "string", "description": "Direction of scale", "enum": ["up", "down", "both"], "default": "up" } }, "required": ["take_handle", "scale_type", "root", "start_beat", "note_duration"] } } ]

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