Skip to main content
Glama
ComposeWithLLM.pyscript8.76 kB
""" ComposeWithLLM - FL Studio Piano Roll Script for MCP Integration This script enables AI assistants to control FL Studio's piano roll by: 1. Reading note requests from mcp_request.json 2. Adding/deleting/clearing notes in the piano roll 3. Exporting the current state to piano_roll_state.json Trigger: Cmd+Opt+Y (macOS) or Ctrl+Alt+Y (Windows) Based on work from https://github.com/calvinw/fl-studio-mcp """ import flpianoroll as flp import json import os def get_script_dir(): """Get the directory where this script is located""" return os.path.expanduser("~/Documents/Image-Line/FL Studio/Settings/Piano roll scripts") REQUEST_FILE = os.path.join(get_script_dir(), "mcp_request.json") RESPONSE_FILE = os.path.join(get_script_dir(), "mcp_response.json") STATE_FILE = os.path.join(get_script_dir(), "piano_roll_state.json") def export_piano_roll_state(): """Export current piano roll state to JSON file""" notes_data = [] ppq = flp.score.PPQ # Collect all notes for i in range(flp.score.noteCount): note = flp.score.getNote(i) notes_data.append({ "number": note.number, "midi": note.number, # Alias for convenience "time": note.time / ppq if ppq > 0 else 0, # Convert to quarter notes "time_ticks": note.time, "duration": note.length / ppq if ppq > 0 else 0, # Convert to quarter notes "length_ticks": note.length, "velocity": note.velocity, "pan": note.pan, "color": note.color, "fcut": note.fcut, "fres": note.fres, "slide": note.slide, "porta": note.porta, "pitchofs": note.pitchofs, "selected": note.selected, "muted": note.muted, }) # Export metadata export_data = { "ppq": ppq, "noteCount": flp.score.noteCount, "notes": notes_data } # Save to file with open(STATE_FILE, 'w') as f: json.dump(export_data, f, indent=2) return STATE_FILE def process_mcp_request(): """Check for and process all requests from the MCP server""" if not os.path.exists(REQUEST_FILE): return None try: with open(REQUEST_FILE, 'r') as f: content = json.load(f) # Handle both single request (old format) and list of requests (new format) if isinstance(content, list): requests = content elif isinstance(content, dict) and content.get("action"): requests = [content] else: # Empty or invalid return None if not requests: return None # Process all requests in order total_notes = 0 notes_deleted = 0 for request in requests: action = request.get("action") if action == "clear": # Clear all notes count = flp.score.noteCount for i in range(count - 1, -1, -1): flp.score.deleteNote(i) notes_deleted += count elif action == "delete_notes": result = delete_notes_from_piano_roll(request) if result and result.get("status") == "success": notes_deleted += result.get("notes_deleted", 0) elif action == "add_chord": result = add_chord_to_piano_roll(request) if result and result.get("status") == "success": total_notes += result.get("notes_added", 0) elif action == "add_notes": result = add_notes_to_piano_roll(request) if result and result.get("status") == "success": total_notes += result.get("notes_added", 0) # Clear the request queue after processing with open(REQUEST_FILE, 'w') as f: f.write("[]") return { "status": "success", "requests_processed": len(requests), "notes_added": total_notes, "notes_deleted": notes_deleted } except Exception as e: return {"status": "error", "message": str(e)} def add_chord_to_piano_roll(request): """Add a chord's notes to the piano roll from MCP request. Chord format allows setting a base time and duration for all notes, with optional per-note offsets and durations. Request format: { "action": "add_chord", "time": 0, # Base time in quarter notes "duration": 1.0, # Default duration for all notes "notes": [ {"midi": 60}, # Uses base time and duration {"midi": 64, "offset": 0.5}, # Starts 0.5 beats after base time {"midi": 67, "duration": 2.0}, # Custom duration ] } """ try: notes = request.get("notes", []) ppq = flp.score.PPQ chord_time = request.get("time", 0) current_time = int(ppq * chord_time) duration_multiplier = request.get("duration", 1.0) for note_data in notes: midi_note = flp.Note() midi_note.number = note_data["midi"] midi_note.time = current_time + int(ppq * note_data.get("offset", 0)) note_duration = note_data.get("duration", duration_multiplier) midi_note.length = int(ppq * note_duration) midi_note.velocity = note_data.get("velocity", 0.8) flp.score.addNote(midi_note) return { "status": "success", "notes_added": len(notes) } except Exception as e: return {"status": "error", "message": str(e)} def delete_notes_from_piano_roll(request): """Delete specific notes from the piano roll. Request format: { "action": "delete_notes", "notes": [ {"midi": 60, "time": 0}, {"midi": 64, "time": 0}, ] } """ try: notes_to_delete = request.get("notes", []) ppq = flp.score.PPQ deleted_count = 0 # Convert delete criteria to ticks for comparison delete_criteria = [] for note_data in notes_to_delete: delete_criteria.append({ "midi": note_data["midi"], "time": int(ppq * note_data["time"]) }) # Iterate backward through existing notes and delete matches for i in range(flp.score.noteCount - 1, -1, -1): note = flp.score.getNote(i) for criteria in delete_criteria: if note.number == criteria["midi"] and note.time == criteria["time"]: flp.score.deleteNote(i) deleted_count += 1 break return { "status": "success", "notes_deleted": deleted_count } except Exception as e: return {"status": "error", "message": str(e)} def add_notes_to_piano_roll(request): """Add arbitrary notes to the piano roll from MCP request. Request format: { "action": "add_notes", "notes": [ {"midi": 60, "time": 0, "duration": 1.0, "velocity": 0.8}, {"midi": 64, "time": 1.0, "duration": 0.5}, ] } """ try: notes = request.get("notes", []) ppq = flp.score.PPQ for note_data in notes: midi_note = flp.Note() midi_note.number = note_data["midi"] note_time = note_data.get("time", note_data.get("offset", 0)) midi_note.time = int(ppq * note_time) midi_note.length = int(ppq * note_data["duration"]) midi_note.velocity = note_data.get("velocity", 0.8) flp.score.addNote(midi_note) return { "status": "success", "notes_added": len(notes) } except Exception as e: return {"status": "error", "message": str(e)} def apply(form=None): """ AUTO MODE - Processes requests silently without dialog This script is designed to be triggered automatically via Ctrl+Alt+Y (Windows) or Cmd+Opt+Y (macOS). It processes pending requests and updates the state without showing any UI. """ # Step 1: Process any pending requests result = process_mcp_request() # Step 2: Always export state AFTER processing # This ensures Claude sees the updated piano roll state try: export_piano_roll_state() except Exception as e: # Silent error handling - no UI shown pass # Step 3: Write result to response file if result: try: with open(RESPONSE_FILE, 'w') as f: json.dump(result, f, indent=2) except: pass

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/karl-andres/fl-studio-mcp'

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