Skip to main content
Glama
analysis_tools.py16.1 kB
""" Analysis Tools for REAPER MCP This module contains analysis tools particularly useful for AI agents to understand project structure, content, and patterns. """ from typing import List, Dict, Any from ..bridge import bridge # ============================================================================ # Project Analysis # ============================================================================ async def analyze_project_structure() -> str: """Analyze overall project structure""" # Get basic counts track_count_result = await bridge.call_lua("CountTracks", [0]) item_count_result = await bridge.call_lua("CountMediaItems", [0]) marker_count_result = await bridge.call_lua("CountProjectMarkers", [0]) tracks = track_count_result.get("ret", 0) if track_count_result.get("ok") else 0 items = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 markers = marker_count_result.get("num_markers", 0) if marker_count_result.get("ok") else 0 # Get project length length_result = await bridge.call_lua("GetProjectLength", [0]) length = length_result.get("ret", 0) if length_result.get("ok") else 0 minutes = int(length // 60) seconds = length % 60 return (f"Project structure: {tracks} tracks, {items} items, {markers} markers/regions, " f"length: {minutes}:{seconds:05.2f}") async def get_track_hierarchy() -> str: """Get track folder hierarchy information""" result = await bridge.call_lua("CountTracks", [0]) track_count = result.get("ret", 0) if result.get("ok") else 0 hierarchy = [] folder_stack = [] for i in range(track_count): # Get track track_result = await bridge.call_lua("GetTrack", [0, i]) if not track_result.get("ok"): continue # Get folder info folder_result = await bridge.call_lua("GetMediaTrackInfo_Value", [track_result.get("ret"), "I_FOLDERDEPTH"]) folder_depth = int(folder_result.get("ret", 0)) if folder_result.get("ok") else 0 # Get track name name_result = await bridge.call_lua("GetTrackName", [i]) name = name_result.get("ret", f"Track {i+1}") if name_result.get("ok") else f"Track {i+1}" indent = " " * len(folder_stack) if folder_depth == 1: # Folder start hierarchy.append(f"{indent}📁 {name}") folder_stack.append(i) elif folder_depth < 0: # Folder end hierarchy.append(f"{indent}🎵 {name}") for _ in range(abs(folder_depth)): if folder_stack: folder_stack.pop() else: # Normal track hierarchy.append(f"{indent}🎵 {name}") return "Track hierarchy:\n" + "\n".join(hierarchy) async def analyze_tempo_map() -> str: """Analyze tempo changes in the project""" # Count tempo markers count_result = await bridge.call_lua("CountTempoTimeSigMarkers", [0]) count = count_result.get("ret", 0) if count_result.get("ok") else 0 tempo_changes = [] for i in range(min(count, 10)): # Limit to first 10 for brevity marker_result = await bridge.call_lua("GetTempoTimeSigMarker", [0, i]) if marker_result.get("ok"): time = marker_result.get("timepos", 0) bpm = marker_result.get("bpm", 120) ts_num = marker_result.get("timesig_num", 4) ts_denom = marker_result.get("timesig_denom", 4) tempo_changes.append(f" {time:.1f}s: {bpm:.1f} BPM, {ts_num}/{ts_denom}") if tempo_changes: return f"Tempo map ({count} changes):\n" + "\n".join(tempo_changes) else: return "No tempo changes in project (constant tempo)" # ============================================================================ # Content Analysis # ============================================================================ async def analyze_track_content(track_index: int) -> str: """Analyze content of a specific track""" # Get track track_result = await bridge.call_lua("GetTrack", [0, track_index]) if not track_result.get("ok"): raise Exception(f"Failed to get track {track_index}") track = track_result.get("ret") # Get track name name_result = await bridge.call_lua("GetTrackName", [track_index]) track_name = name_result.get("ret", f"Track {track_index + 1}") if name_result.get("ok") else f"Track {track_index + 1}" # Count items item_count_result = await bridge.call_lua("CountTrackMediaItems", [track]) item_count = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 # Count envelopes env_count_result = await bridge.call_lua("CountTrackEnvelopes", [track]) env_count = env_count_result.get("ret", 0) if env_count_result.get("ok") else 0 # Count FX fx_count_result = await bridge.call_lua("TrackFX_GetCount", [track]) fx_count = fx_count_result.get("ret", 0) if fx_count_result.get("ok") else 0 # Check if armed armed_result = await bridge.call_lua("GetMediaTrackInfo_Value", [track, "I_RECARM"]) is_armed = bool(armed_result.get("ret", 0)) if armed_result.get("ok") else False # Get total item length total_length = 0 for i in range(item_count): item_result = await bridge.call_lua("GetTrackMediaItem", [track, i]) if item_result.get("ok"): length_result = await bridge.call_lua("GetMediaItemInfo_Value", [item_result.get("ret"), "D_LENGTH"]) if length_result.get("ok"): total_length += length_result.get("ret", 0) analysis = [ f"Track '{track_name}' analysis:", f" Items: {item_count}", f" Total content length: {total_length:.1f}s", f" Envelopes: {env_count}", f" FX: {fx_count}", f" Armed: {'Yes' if is_armed else 'No'}" ] return "\n".join(analysis) async def find_silent_regions(threshold_db: float = -60.0, min_length: float = 1.0) -> str: """Find silent regions in the project""" # This is a simplified version - full implementation would analyze audio return (f"Silent region detection (threshold: {threshold_db}dB, min length: {min_length}s) " "requires audio analysis - use render and analyze workflow") async def analyze_item_overlaps() -> str: """Find overlapping media items""" item_count_result = await bridge.call_lua("CountMediaItems", [0]) item_count = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 overlaps = [] items_data = [] # Collect all items with their positions for i in range(item_count): item_result = await bridge.call_lua("GetMediaItem", [0, i]) if not item_result.get("ok"): continue item = item_result.get("ret") # Get position and length pos_result = await bridge.call_lua("GetMediaItemInfo_Value", [item, "D_POSITION"]) length_result = await bridge.call_lua("GetMediaItemInfo_Value", [item, "D_LENGTH"]) track_result = await bridge.call_lua("GetMediaItem_Track", [item]) if pos_result.get("ok") and length_result.get("ok") and track_result.get("ok"): pos = pos_result.get("ret", 0) length = length_result.get("ret", 0) track = track_result.get("ret") items_data.append((i, pos, pos + length, track)) # Find overlaps on same track for i in range(len(items_data)): for j in range(i + 1, len(items_data)): if items_data[i][3] == items_data[j][3]: # Same track # Check if items overlap if (items_data[i][1] < items_data[j][2] and items_data[j][1] < items_data[i][2]): overlaps.append(f"Items {items_data[i][0]} and {items_data[j][0]} overlap") if overlaps: return f"Found {len(overlaps)} overlapping items:\n" + "\n".join(overlaps[:10]) # Limit to 10 else: return "No overlapping items found" # ============================================================================ # Pattern Detection # ============================================================================ async def detect_loop_regions() -> str: """Detect potential loop regions based on item patterns""" item_count_result = await bridge.call_lua("CountMediaItems", [0]) item_count = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 # Collect items by track tracks_items = {} for i in range(item_count): item_result = await bridge.call_lua("GetMediaItem", [0, i]) if not item_result.get("ok"): continue item = item_result.get("ret") # Get track track_result = await bridge.call_lua("GetMediaItem_Track", [item]) if not track_result.get("ok"): continue track = track_result.get("ret") # Get position and length pos_result = await bridge.call_lua("GetMediaItemInfo_Value", [item, "D_POSITION"]) length_result = await bridge.call_lua("GetMediaItemInfo_Value", [item, "D_LENGTH"]) if pos_result.get("ok") and length_result.get("ok"): if track not in tracks_items: tracks_items[track] = [] tracks_items[track].append({ 'pos': pos_result.get("ret", 0), 'length': length_result.get("ret", 0) }) # Look for repeating patterns patterns = [] for track, items in tracks_items.items(): if len(items) < 2: continue # Sort by position items.sort(key=lambda x: x['pos']) # Check for regular spacing gaps = [] for i in range(1, len(items)): gap = items[i]['pos'] - (items[i-1]['pos'] + items[i-1]['length']) gaps.append(gap) # If gaps are consistent, we might have a loop if gaps and all(abs(g - gaps[0]) < 0.01 for g in gaps): patterns.append(f"Potential loop pattern on track (gap: {gaps[0]:.2f}s)") if patterns: return "Detected patterns:\n" + "\n".join(patterns[:5]) # Limit to 5 else: return "No obvious loop patterns detected" async def analyze_project_rhythm() -> str: """Analyze rhythmic structure based on item positions""" # Get tempo tempo_result = await bridge.call_lua("Master_GetTempo", []) tempo = tempo_result.get("ret", 120) if tempo_result.get("ok") else 120 # Get time signature ts_result = await bridge.call_lua("GetTempoTimeSigMarker", [0, 0]) ts_num = ts_result.get("timesig_num", 4) if ts_result.get("ok") else 4 ts_denom = ts_result.get("timesig_denom", 4) if ts_result.get("ok") else 4 # Calculate beat length beat_length = 60.0 / tempo measure_length = beat_length * ts_num # Count items on downbeats item_count_result = await bridge.call_lua("CountMediaItems", [0]) item_count = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 on_beat_count = 0 near_beat_count = 0 for i in range(item_count): item_result = await bridge.call_lua("GetMediaItem", [0, i]) if not item_result.get("ok"): continue pos_result = await bridge.call_lua("GetMediaItemInfo_Value", [item_result.get("ret"), "D_POSITION"]) if pos_result.get("ok"): pos = pos_result.get("ret", 0) # Check if near a beat beat_offset = pos % beat_length if beat_offset < 0.05 or beat_offset > beat_length - 0.05: near_beat_count += 1 # Check if on a downbeat measure_offset = pos % measure_length if measure_offset < 0.05 or measure_offset > measure_length - 0.05: on_beat_count += 1 return (f"Rhythm analysis: {tempo:.1f} BPM, {ts_num}/{ts_denom}\n" f" Items on downbeats: {on_beat_count}\n" f" Items on beats: {near_beat_count}\n" f" Beat-aligned: {near_beat_count/item_count*100:.1f}%" if item_count > 0 else "No items") # ============================================================================ # MIDI Analysis # ============================================================================ async def analyze_midi_content_summary() -> str: """Get summary of all MIDI content in project""" item_count_result = await bridge.call_lua("CountMediaItems", [0]) item_count = item_count_result.get("ret", 0) if item_count_result.get("ok") else 0 total_notes = 0 total_midi_items = 0 pitch_range = [127, 0] # min, max for i in range(item_count): item_result = await bridge.call_lua("GetMediaItem", [0, i]) if not item_result.get("ok"): continue # Get active take take_result = await bridge.call_lua("GetActiveTake", [item_result.get("ret")]) if not take_result.get("ok") or not take_result.get("ret"): continue take = take_result.get("ret") # Check if MIDI is_midi_result = await bridge.call_lua("TakeIsMIDI", [take]) if not is_midi_result.get("ok") or not is_midi_result.get("ret"): continue total_midi_items += 1 # Count events count_result = await bridge.call_lua("MIDI_CountEvts", [take]) if count_result.get("ok"): notes = count_result.get("notes", 0) total_notes += notes # Sample first few notes for pitch range for n in range(min(notes, 20)): note_result = await bridge.call_lua("MIDI_GetNote", [take, n]) if note_result.get("ok"): pitch = note_result.get("pitch", 60) pitch_range[0] = min(pitch_range[0], pitch) pitch_range[1] = max(pitch_range[1], pitch) if total_midi_items > 0: note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] low_note = f"{note_names[pitch_range[0] % 12]}{pitch_range[0] // 12 - 1}" high_note = f"{note_names[pitch_range[1] % 12]}{pitch_range[1] // 12 - 1}" return (f"MIDI content summary:\n" f" MIDI items: {total_midi_items}\n" f" Total notes: {total_notes}\n" f" Pitch range: {low_note} to {high_note}") else: return "No MIDI content found in project" # ============================================================================ # Registration Function # ============================================================================ def register_analysis_tools(mcp) -> int: """Register all analysis tools with the MCP instance""" tools = [ # Project Analysis (analyze_project_structure, "Analyze overall project structure"), (get_track_hierarchy, "Get track folder hierarchy information"), (analyze_tempo_map, "Analyze tempo changes in the project"), # Content Analysis (analyze_track_content, "Analyze content of a specific track"), (find_silent_regions, "Find silent regions in the project"), (analyze_item_overlaps, "Find overlapping media items"), # Pattern Detection (detect_loop_regions, "Detect potential loop regions"), (analyze_project_rhythm, "Analyze rhythmic structure"), # MIDI Analysis (analyze_midi_content_summary, "Get summary of all MIDI content"), ] # 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