# Directory Structure ``` ├── README.md ├── requirements.txt ├── Test Controller │ └── device_test.py.py └── trigger.py ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # flstudio MCP # This is an MCP server that connects Claude to FL Studio. Made this in 3 days. We're open sourcing it to see what we can actually get out of it. The possibilities are endless. ## If you're running to any issues, join our discord and we can setup it for you. (also join if you interested in the future of music and AI or want to request features. we're building this with you) https://discord.gg/ZjG9TaEhvy Check out our AI-Powered DAW for musicians at www.veena.studio All in browser. All for free. ## Step 1: Download the Files You should see two main items. - A folder called Test Controller - A python file called trigger.py The Test Controller folder has a file called device_test.py that receives information from the MCP server. trigger.py is the MCP server. Place the Test Controller folder in Image-Line/FL Studio/Settings/Hardware (Don't change the name of this file or folder) ## Step 2: Set up MCP for Claude Follow this tutorial to see how to setup MCP servers in Claude by edyting the claude_desktop_config files. https://modelcontextprotocol.io/quickstart/server If you followed this process, make sure to change whatever mentions of weather.py to trigger.py If the Hammer icon doesn't show up, open Task Manager and force close the Claude process. It should then show up. This is what my config file looks like  ## Step 3: Set Up Virtual MIDI Ports ### For Windows For Windows, download LoopMIDI from here. https://www.tobias-erichsen.de/software/loopmidi.html Install LoopMIDI and add a port using the + button. This is what mine looks like:  ### For Mac Your MIDI Ports would be automatically setup to receive data. ## Step 4: Setup MIDI Controller Open FL Studio. Go To Options > MIDI Settings. In the Input Tab, click the MIDI Input you just created with LoopMIDI. Change controller type from (generic controller) to Test Controller. ## Step 5: Download Packages Go to the folder with the trigger.py file. (This is the MCP Server file) Activate the conda environment (like you learned in the Claude MCP Setup Tutorial) Run this command to download the necessary packages: uv pip install httpx mido python-rtmidi typing fastmcp FL-Studio-API-Stubs (uv should be installed from the Claude MCP setup) ## Step 6: Verify MCP Connection Tell Claude to get available MIDI ports. This should use the MCP to get the ports from FL Studio. If Windows, copy the port you created with LoopMIDI and the number in front of it. If Mac, copy the default port.  In my case, I copy loopMIDI Port 2 Open trigger.py in a text editor and replace the default port with the name of the port you just copied. output_port = mido.open_output('loopMIDI Port 2') ## Step 7: Make Music Use the MCP to send melodies, chords, drums, etc. Click on the instrument you want to record to and it will live record to the piano roll of that instrument. I tend to use this prompt when I start a new chat: Here is format for notes: note(0-127),velocity(0-100),length in beats(decimal),position in beats(decimal) ## Step 8: Share what you made Share what you made on our Discord: https://discord.gg/ZjG9TaEhvy ## Credits FL Studio API Stubs: https://github.com/IL-Group/FL-Studio-API-Stubs Ableton MCP: https://github.com/ahujasid/ableton-mcp ## Nerd Stuff If you want to contribute please go ahead. The way this works is that device_test.py behaves as a virtual MIDI Controller. The MCP server (trigger.py) communicates with this MIDI Controller by opening a Virtual Port and sending MIDI messages through a library called MIDO. The issue with MIDI messages is that its only 7 bits so we can only send in number from 0-127. So we encrypt all of our MIDI data like note position, etc in multiple MIDI notes that the device knows how to read. Hopefully, Image Line can give us more access to their DAW via their API so we don't have to do this MIDI nonsense. ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` mido==1.3.3 python-rtmidi==1.5.8 fl-studio-api-stubs==37.0.1 ``` -------------------------------------------------------------------------------- /trigger.py: -------------------------------------------------------------------------------- ```python from typing import Any import httpx from mcp.server.fastmcp import FastMCP import mido from mido import Message import time # Initialize FastMCP server mcp = FastMCP("flstudio") output_port = mido.open_output('loopMIDI Port 2') # MIDI Note mappings for FL Studio commands NOTE_PLAY = 60 # C3 NOTE_STOP = 61 # C#3 NOTE_RECORD = 62 # D3 NOTE_NEW_PROJECT = 63 # D#3 NOTE_SET_BPM = 64 # E3 NOTE_NEW_PATTERN = 65 # F3 NOTE_SELECT_PATTERN = 66 # F#3 NOTE_ADD_CHANNEL = 67 # G3 NOTE_NAME_CHANNEL = 68 # G#3 NOTE_ADD_NOTE = 69 # A3 NOTE_ADD_TO_PLAYLIST = 70 # A#3 NOTE_SET_PATTERN_LEN = 71 # B3 NOTE_CHANGE_TEMPO = 72 # Define custom MIDI CC messages for direct step sequencer grid control CC_SELECT_CHANNEL = 100 # Select which channel to edit CC_SELECT_STEP = 110 # Select which step to edit CC_TOGGLE_STEP = 111 # Toggle the selected step on/off CC_STEP_VELOCITY = 112 # Set velocity for the selected step # Drum sound MIDI notes KICK = 36 # C1 SNARE = 38 # D1 CLAP = 39 # D#1 CLOSED_HAT = 42 # F#1 OPEN_HAT = 46 # A#1 @mcp.tool() def list_midi_ports(): """List all available MIDI input ports""" print("\nAvailable MIDI Input Ports:") input_ports = mido.get_output_names() if not input_ports: print(" No MIDI input ports found") else: for i, port in enumerate(input_ports): print(f" {i}: {port}") return input_ports @mcp.tool() def play(): """Send MIDI message to start playback in FL Studio""" # Send Note On for C3 (note 60) output_port.send(mido.Message('note_on', note=60, velocity=100)) time.sleep(0.1) # Small delay output_port.send(mido.Message('note_off', note=60, velocity=0)) print("Sent Play command") @mcp.tool() def stop(): """Send MIDI message to stop playback in FL Studio""" # Send Note On for C#3 (note 61) output_port.send(mido.Message('note_on', note=61, velocity=100)) time.sleep(0.1) # Small delay output_port.send(mido.Message('note_off', note=61, velocity=0)) print("Sent Stop command") def int_to_midi_bytes(value): """ Convert an integer value into an array of MIDI-compatible bytes (7-bit values) Args: value (int): The integer value to convert Returns: list: Array of MIDI bytes (each 0-127) """ if value < 0: print("Warning: Negative values not supported, converting to positive") value = abs(value) # Special case for zero if value == 0: return [0] # Convert to MIDI bytes (7-bit values, MSB first) midi_bytes = [] while value > 0: # Extract the lowest 7 bits and prepend to array midi_bytes.insert(0, value & 0x7F) # 0x7F = 127 (binary: 01111111) # Shift right by 7 bits value >>= 7 return midi_bytes def change_tempo(bpm): """ Change the tempo in FL Studio using a sequence of MIDI notes This function converts a BPM value to an array of MIDI notes, sends a start marker, the notes, and an end marker to trigger a tempo change in FL Studio. Args: bpm (float): The desired tempo in beats per minute """ # Ensure BPM is within a reasonable range if bpm < 20 or bpm > 999: print(f"Warning: BPM value {bpm} is outside normal range (20-999)") bpm = max(20, min(bpm, 999)) # Convert BPM to integer bpm_int = int(bpm) # Convert to MIDI bytes midi_notes = int_to_midi_bytes(bpm_int) print(f"Setting tempo to {bpm_int} BPM using note array: {midi_notes}") # Send start marker (note 72) send_midi_note(72) time.sleep(0.2) # Send each note in the array for note in midi_notes: send_midi_note(note) time.sleep(0.1) # Send end marker (note 73) send_midi_note(73) time.sleep(0.2) print(f"Tempo change to {bpm_int} BPM sent successfully using {len(midi_notes)} notes") @mcp.tool() def send_melody(notes_data): """ Send a sequence of MIDI notes with timing information to FL Studio for recording Args: notes_data (str): String containing note data in format "note,velocity,length,position" with each note on a new line """ # Parse the notes_data string into a list of note tuples notes = [] for line in notes_data.strip().split('\n'): if not line.strip(): continue parts = line.strip().split(',') if len(parts) != 4: print(f"Warning: Skipping invalid line: {line}") continue try: note = min(127, max(0, int(parts[0]))) velocity = min(127, max(0, int(parts[1]))) length = max(0, float(parts[2])) position = max(0, float(parts[3])) notes.append((note, velocity, length, position)) except ValueError: print(f"Warning: Skipping line with invalid values: {line}") continue if not notes: return "No valid notes found in input data" # Create the MIDI data array (5 values per note) midi_data = [] for note, velocity, length, position in notes: # 1. Note value (0-127) midi_data.append(note) # 2. Velocity value (0-127) midi_data.append(velocity) # 3. Length whole part (0-127) length_whole = min(127, int(length)) midi_data.append(length_whole) # 4. Length decimal part (0-9) length_decimal = int(round((length - length_whole) * 10)) % 10 midi_data.append(length_decimal) # 5. Position whole part (0-127) position_whole = min(127, int(position)) midi_data.append(position_whole) # 6. Position decimal part (0-9) position_decimal = int(round((position - position_whole) * 10)) % 10 midi_data.append(position_decimal) # Start MIDI transfer print(f"Transferring {len(notes)} notes ({len(midi_data)} MIDI values)...") # Initial toggle signal (note 0) send_midi_note(0) time.sleep(0.01) # Send total count of notes send_midi_note(min(127, len(notes))) time.sleep(0.01) # Send all MIDI data values for i, value in enumerate(midi_data): send_midi_note(value) #time.sleep(0.1) #print(f"Sent MIDI value {i+1}/{len(midi_data)}: {value}") send_midi_note(127) #print(f"Melody transfer complete: {len(notes)} notes sent") return f"Melody successfully transferred: {len(notes)} notes ({len(midi_data)} MIDI values) sent to FL Studio" # Send a MIDI note message @mcp.tool() def send_midi_note(note, velocity=1, duration=0.01): """Send a MIDI note on/off message with specified duration""" note_on = Message('note_on', note=note, velocity=velocity) output_port.send(note_on) #print(f"Sent MIDI note {note} (on), velocity {velocity}") time.sleep(duration) note_off = Message('note_off', note=note, velocity=0) output_port.send(note_off) print(f"Sent MIDI note {note} (off)") #time.sleep(0.1) # Small pause between messages if __name__ == "__main__": # Initialize and run the server mcp.run(transport='stdio') ``` -------------------------------------------------------------------------------- /Test Controller/device_test.py.py: -------------------------------------------------------------------------------- ```python # device_test.py # name=Test Controller import transport import ui import midi import channels import playlist import patterns import arrangement import general import device import time import sys # Global variables running = True command_history = [] current_pattern = 0 current_channel = 0 terminal_active = False collecting_tempo_notes = False tempo_note_array = [] NOTE_TEMPO_START = 72 # C4 starts tempo note collection NOTE_TEMPO_END = 73 # C#4 ends collection and applies tempo change channel_to_edit = 0 step_to_edit = 0 def midi_notes_to_int(midi_notes): """ Convert an array of MIDI note values (7 bits each) into a single integer This function takes a list of MIDI notes and combines them into a single integer value, with the first note being the most significant. Args: midi_notes (list): A list of MIDI note values (each 0-127) Returns: int: The combined integer value """ result = 0 for note in midi_notes: # Ensure each value is within MIDI range (0-127) note_value = min(127, max(0, note)) # Shift left by 7 bits (MIDI values use 7 bits) and combine result = (result << 7) | note_value return result def OnInit(): """Called when the script is loaded by FL Studio""" print("FL Studio Terminal Beat Builder initialized") print("Type 'help' for a list of commands") return def OnDeInit(): """Called when the script is unloaded by FL Studio""" global running running = False # Signal the terminal thread to exit print("FL Studio Terminal Beat Builder deinitialized") return def OnRefresh(flags): """Called when FL Studio's state changes or when a refresh is needed""" # Update terminal with current state if needed return def OnMidiIn(event): """Called whenever the device sends a MIDI message to FL Studio""" #print(f"MIDI In - Status: {event.status}, Data1: {event.data1}, Data2: {event.data2}") return def change_tempo(bpm): """ Change the tempo in FL Studio to the specified BPM value Args: bpm (float): The desired tempo in beats per minute """ # FL Studio stores tempo as BPM * 1000 tempo_value = int(bpm * 1000) # Use processRECEvent to set the tempo # REC_Tempo is the event ID for tempo # REC_Control | REC_UpdateControl flags ensure the value is set and the UI updates general.processRECEvent( midi.REC_Tempo, tempo_value, midi.REC_Control | midi.REC_UpdateControl ) def process_received_midi(note, velocity): global current_note, current_velocity, current_length, current_position global decimal_state, decimal_value, decimal_target # Special MIDI commands DECIMAL_MARKER = 100 # Indicates next value is a decimal part LENGTH_MARKER = 101 # Next value affects length POSITION_MARKER = 102 # Next value affects position # Process based on message type if note == DECIMAL_MARKER: # Next value will be a decimal decimal_state = 1 return False elif note == LENGTH_MARKER: # Next value affects length decimal_target = "length" decimal_state = 0 decimal_value = 0 return False elif note == POSITION_MARKER: # Next value affects position decimal_target = "position" decimal_state = 0 decimal_value = 0 return False elif decimal_state == 1: # This is a decimal part value decimal_value = note / 10.0 # Convert to decimal (0-9 becomes 0.0-0.9) # Apply to the correct parameter if decimal_target == "length": current_length = (current_length or 0) + decimal_value print(f"Set length decimal: {current_length:.2f}") elif decimal_target == "position": current_position = (current_position or 0) + decimal_value print(f"Set position decimal: {current_position:.2f}") decimal_state = 0 return False elif decimal_target is not None: # This is a whole number part for a specific parameter if decimal_target == "length": current_length = float(note) print(f"Set length whole: {current_length:.2f}") elif decimal_target == "position": current_position = float(note) print(f"Set position whole: {current_position:.2f}") return False else: # This is a note value and velocity # Check if we have a complete previous note to add add_note = (current_note is not None and current_velocity is not None and current_length is not None and current_position is not None) # Start a new note current_note = note current_velocity = velocity # Use default values if not specified if current_length is None: current_length = 1.0 if current_position is None: current_position = 0.0 print(f"Started new note: {current_note}, velocity: {current_velocity}") return add_note def OnMidiMsg(event, timestamp=0): """Called when a processed MIDI message is received""" global receiving_mode, message_count, messages_received global current_note, current_velocity, current_length, current_position global decimal_state, decimal_target, midi_notes_array if 'receiving_mode' not in globals(): global receiving_mode receiving_mode = False if 'note_count' not in globals(): global note_count note_count = 0 if 'values_received' not in globals(): global values_received values_received = 0 if 'midi_data' not in globals(): global midi_data midi_data = [] if 'midi_notes_array' not in globals(): global midi_notes_array midi_notes_array = [] # Only process Note On messages with velocity > 0 if event.status >= midi.MIDI_NOTEON and event.status < midi.MIDI_NOTEON + 16 and event.data2 > 0: note_value = event.data1 # Toggle receiving mode with note 0 if note_value == 0 and not receiving_mode: receiving_mode = True print("Started receiving MIDI notes") midi_data = [] note_count = 0 values_received = 0 midi_notes_array = [] event.handled = True return # Only process further messages if in receiving mode if not receiving_mode: return # Second message is the note count if note_count == 0: note_count = note_value print(f"Expecting {note_count} notes") event.handled = True return # All subsequent messages are MIDI values (6 per note) midi_data.append(note_value) values_received += 1 # Process completed notes (every 6 values) if len(midi_data) >= 6 and len(midi_data) % 6 == 0: # Process the last complete note i = len(midi_data) - 6 note = midi_data[i] velocity = midi_data[i+1] length_whole = midi_data[i+2] length_decimal = midi_data[i+3] position_whole = midi_data[i+4] position_decimal = midi_data[i+5] # Calculate full values length = length_whole + (length_decimal / 10.0) position = position_whole + (position_decimal / 10.0) # Add to notes array midi_notes_array.append((note, velocity, length, position)) print(f"Added note: note={note}, velocity={velocity}, length={length:.1f}, position={position:.1f}") print(f"Current array size: {len(midi_notes_array)}") if len(midi_notes_array) >= note_count or note_value == 127: print(f"Received all {len(midi_notes_array)} notes or termination signal") receiving_mode = False # Only process if we have actual notes if midi_notes_array: # Print all collected notes print(f"Collected {len(midi_notes_array)} notes:") for i, (note, vel, length, pos) in enumerate(midi_notes_array): print(f" Note {i+1}: note={note}, velocity={vel}, length={length:.1f}, position={pos:.1f}") print("\nFinal array:") print(midi_notes_array) # Process the notes using the record_notes_batch function record_notes_batch(midi_notes_array) event.handled = True return # Check if we've received all expected notes # if len(midi_notes_array) >= note_count: # print(f"Received all {note_count} notes") # receiving_mode = False # # Print all collected notes # print(f"Collected {len(midi_notes_array)} notes:") # for i, (note, vel, length, pos) in enumerate(midi_notes_array): # print(f" Note {i+1}: note={note}, velocity={vel}, length={length:.1f}, position={pos:.1f}") # print("\nFinal array:") # print(midi_notes_array) # record_notes_batch(midi_notes_array) # # Process the notes here if needed # # record_notes_batch(midi_notes_array) # event.handled = True # elif note == 72: # collecting_tempo_notes = True # tempo_note_array = [] # print("Started collecting notes for tempo change") # event.handled = True # # End collection and apply tempo change # elif note == 73: # if collecting_tempo_notes and tempo_note_array: # bpm = change_tempo_from_notes(tempo_note_array) # print(f"Tempo changed to {bpm} BPM from collected notes: {tempo_note_array}") # collecting_tempo_notes = False # tempo_note_array = [] # else: # print("No tempo notes collected, tempo unchanged") # event.handled = True # # Collect notes for tempo if in collection mode # elif collecting_tempo_notes: # tempo_note_array.append(note) # print(f"Added note {note} to tempo collection, current array: {tempo_note_array}") # event.handled = True # Handle Control Change messages # elif event.status >= midi.MIDI_CONTROLCHANGE and event.status < midi.MIDI_CONTROLCHANGE + 16: # # CC 100: Select channel to edit # if event.data1 == 100: # channel_to_edit = event.data2 # channels.selectOneChannel(channel_to_edit) # print(f"Selected channel {channel_to_edit} for grid editing") # event.handled = True # # CC 110: Select step to edit # elif event.data1 == 110: # step_to_edit = event.data2 # print(f"Selected step {step_to_edit} for grid editing") # event.handled = True # # CC 111: Toggle step on/off # elif event.data1 == 111: # enabled = event.data2 > 0 # channels.setGridBit(channel_to_edit, step_to_edit, enabled) # print(f"Set grid bit for channel {channel_to_edit}, step {step_to_edit} to {enabled}") # commit_pattern_changes() # Force UI update # event.handled = True # # CC 112: Set step velocity/level # elif event.data1 == 112: # velocity = event.data2 # channels.setStepLevel(channel_to_edit, step_to_edit, velocity) # print(f"Set step level for channel {channel_to_edit}, step {step_to_edit} to {velocity}") # commit_pattern_changes() # Force UI update # event.handled = True # # Process other CC messages # else: # # Handle other CC messages with your existing code... # pass # # Handle other MIDI message types if needed # else: # # Process other MIDI message types # pass # Handle Note On messages with your existing code... # [rest of your existing OnMidiMsg function] #record_notes_batch(midi_notes_array) # Make sure your commit_pattern_changes function is defined: def commit_pattern_changes(pattern_num=None): """Force FL Studio to update the pattern data visually""" if pattern_num is None: pattern_num = patterns.patternNumber() # Force FL Studio to redraw and commit changes ui.crDisplayRect() # Force channel rack to update ui.setFocused(midi.widChannelRack) # Update playlist if needed playlist.refresh() def OnTransport(isPlaying): """Called when the transport state changes (play/stop)""" print(f"Transport state changed: {'Playing' if isPlaying else 'Stopped'}") return def OnTempoChange(tempo): """Called when the tempo changes""" print(f"Tempo changed to: {tempo} BPM") return # # Terminal interface functions # def start_terminal_thread(): # """Start a thread to handle terminal input""" # global terminal_active # if not terminal_active: # terminal_active = True # thread = threading.Thread(target=terminal_loop) # thread.daemon = True # thread.start() # def terminal_loop(): # """Main terminal input loop""" # global running, terminal_active # print("\n===== FL STUDIO TERMINAL BEAT BUILDER =====") # print("Enter commands to build your beat (type 'help' for commands)") # while running: # try: # command = input("\nFLBEAT> ") # command_history.append(command) # process_command(command) # except Exception as e: # print(f"Error processing command: {e}") # terminal_active = False def record_note(note=60, velocity=100, length_beats=1.0, position_beats=0.0, quantize=True): """ Records a single note to the piano roll synced with project tempo Args: note (int): MIDI note number (60 = middle C) velocity (int): Note velocity (0-127) length_beats (float): Length of note in beats (1.0 = quarter note) position_beats (float): Position to place note in beats from start quantize (bool): Whether to quantize the recording afterward """ # Make sure transport is stopped first if transport.isPlaying(): transport.stop() # Get the current channel channel = channels.selectedChannel() # Get the project's PPQ (pulses per quarter note) ppq = general.getRecPPQ() # Calculate ticks based on beats length_ticks = int(length_beats * ppq) position_ticks = int(position_beats * ppq) # Set playback position transport.setSongPos(position_ticks, 2) # 2 = SONGLENGTH_ABSTICKS # Toggle recording mode if needed if not transport.isRecording(): transport.record() print(f"Recording note {note} to channel {channel}") print(f"Position: {position_beats} beats, Length: {length_beats} beats") # Calculate the exact tick positions where we need to place note and note-off start_tick = position_ticks end_tick = start_tick + length_ticks # Start playback to begin recording transport.start() # Record the note at the exact start position channels.midiNoteOn(channel, note, velocity) # Get the current tempo (BPM) tempo = 120 # Default fallback # Try to get actual tempo from the project try: import mixer tempo = mixer.getCurrentTempo() tempo = tempo/1000 print(f"Using project tempo: {tempo} BPM") except (ImportError, AttributeError): print("Using default tempo: 120 BPM") # Calculate the time to wait in seconds seconds_to_wait = (length_beats * 60) / tempo print(f"Waiting for {seconds_to_wait:.2f} seconds...") # Wait the calculated time time.sleep(seconds_to_wait) # Send note-off event channels.midiNoteOn(channel, note, 0) # Stop playback transport.stop() # Exit recording mode if it was active if transport.isRecording(): transport.record() # Quantize if requested if quantize: channels.quickQuantize(channel) print("Recording quantized") print(f"Note {note} recorded to piano roll") # Return to beginning transport.setSongPos(0, 2) def rec_hihat_pattern(): """ Records a predefined hi-hat pattern to the piano roll using record_notes_batch This creates a 4-bar hi-hat pattern with variations in velocity, rhythm, and types of hats """ # Stop playback and rewind to beginning first if transport.isPlaying(): transport.stop() transport.setSongPos(0, 2) # Go to the beginning print("Recording hi-hat pattern...") # Common hi-hat MIDI notes: # 42 = Closed hi-hat # 44 = Pedal hi-hat # 46 = Open hi-hat # Define the pattern as a list of notes # Each tuple contains (note, velocity, length_beats, position_beats) hihat_pattern = [ # BAR 1 - Basic pattern (42, 90, 0.1, 0.0), # Closed hat on beat 1 (42, 65, 0.1, 0.5), # Closed hat on off-beat (42, 90, 0.1, 1.0), # Closed hat on beat 2 (42, 65, 0.1, 1.5), # Closed hat on off-beat (42, 90, 0.1, 2.0), # Closed hat on beat 3 (42, 65, 0.1, 2.5), # Closed hat on off-beat (42, 90, 0.1, 3.0), # Closed hat on beat 4 (42, 65, 0.1, 3.5), # Closed hat on off-beat # BAR 2 - Adding 16th notes (42, 90, 0.1, 4.0), # Closed hat on beat 1 (42, 60, 0.1, 4.25), # Closed hat on 16th (42, 70, 0.1, 4.5), # Closed hat on off-beat (42, 60, 0.1, 4.75), # Closed hat on 16th (42, 90, 0.1, 5.0), # Closed hat on beat 2 (42, 60, 0.1, 5.25), # Closed hat on 16th (42, 70, 0.1, 5.5), # Closed hat on off-beat (42, 60, 0.1, 5.75), # Closed hat on 16th (42, 90, 0.1, 6.0), # Closed hat on beat 3 (42, 60, 0.1, 6.25), # Closed hat on 16th (42, 70, 0.1, 6.5), # Closed hat on off-beat (42, 60, 0.1, 6.75), # Closed hat on 16th (42, 90, 0.1, 7.0), # Closed hat on beat 4 (46, 80, 0.2, 7.5), # Open hat on off-beat # BAR 3 - Mixing closed and open hats (42, 100, 0.1, 8.0), # Closed hat on beat 1 (42, 70, 0.1, 8.5), # Closed hat on off-beat (46, 85, 0.2, 9.0), # Open hat on beat 2 (42, 70, 0.1, 9.5), # Closed hat on off-beat (42, 95, 0.1, 10.0), # Closed hat on beat 3 (42, 70, 0.1, 10.5), # Closed hat on off-beat (46, 85, 0.2, 11.0), # Open hat on beat 4 # Triplet fill at the end of bar 3 (42, 80, 0.08, 11.33), # Closed hat - triplet 1 (42, 85, 0.08, 11.66), # Closed hat - triplet 2 (42, 90, 0.08, 11.99), # Closed hat - triplet 3 # BAR 4 - Complex pattern with pedal hats (42, 100, 0.1, 12.0), # Closed hat on beat 1 (44, 75, 0.1, 12.25), # Pedal hat on 16th (42, 80, 0.1, 12.5), # Closed hat on off-beat (44, 70, 0.1, 12.75), # Pedal hat on 16th (42, 90, 0.1, 13.0), # Closed hat on beat 2 (46, 85, 0.3, 13.5), # Open hat on off-beat # Beat 3-4: Building intensity (42, 95, 0.1, 14.0), # Closed hat on beat 3 (42, 75, 0.1, 14.25), # Closed hat on 16th (42, 85, 0.1, 14.5), # Closed hat on off-beat (42, 80, 0.1, 14.75), # Closed hat on 16th # Final fill (42, 85, 0.05, 15.0), # Closed hat - 32nd note 1 (42, 90, 0.05, 15.125), # Closed hat - 32nd note 2 (42, 95, 0.05, 15.25), # Closed hat - 32nd note 3 (42, 100, 0.05, 15.375),# Closed hat - 32nd note 4 (42, 105, 0.05, 15.5), # Closed hat - 32nd note 5 (42, 110, 0.05, 15.625),# Closed hat - 32nd note 6 (42, 115, 0.05, 15.75), # Closed hat - 32nd note 7 (46, 120, 0.25, 15.875),# Open hat - final accent ] # Record the hi-hat pattern using the batch recording function record_notes_batch(hihat_pattern) print("Hi-hat pattern recording complete!") # Quantize the hi-hat pattern channel = channels.selectedChannel() channels.quickQuantize(channel) # Return to beginning transport.setSongPos(0, 2) def record_notes_batch(notes_array): """ Records a batch of notes to FL Studio, handling simultaneous notes properly Args: notes_array: List of tuples, each containing (note, velocity, length_beats, position_beats) """ # Sort notes by their starting position sorted_notes = sorted(notes_array, key=lambda x: x[3]) # Group notes by their starting positions position_groups = {} for note in sorted_notes: position = note[3] # position_beats is the 4th element (index 3) if position not in position_groups: position_groups[position] = [] position_groups[position].append(note) # Process each position group positions = sorted(position_groups.keys()) for position in positions: notes_at_position = position_groups[position] # Find the longest note in this group to determine recording length max_length = max(note[2] for note in notes_at_position) # Make sure transport is stopped first if transport.isPlaying(): transport.stop() # Get the current channel channel = channels.selectedChannel() # Get the project's PPQ (pulses per quarter note) ppq = general.getRecPPQ() # Calculate ticks based on beats position_ticks = int(position * ppq) # Set playback position transport.setSongPos(position_ticks, 2) # 2 = SONGLENGTH_ABSTICKS # Toggle recording mode if needed if not transport.isRecording(): transport.record() print(f"Recording {len(notes_at_position)} simultaneous notes at position {position}") # Start playback to begin recording transport.start() # Record all notes at this position simultaneously for note, velocity, length, _ in notes_at_position: channels.midiNoteOn(channel, note, velocity) # Get the current tempo try: import mixer tempo = mixer.getCurrentTempo() tempo = tempo/1000 except (ImportError, AttributeError): tempo = 120 # Default fallback print(f"Using tempo: {tempo} BPM") # Calculate the time to wait in seconds based on the longest note seconds_to_wait = (max_length * 60) / tempo print(f"Waiting for {seconds_to_wait:.2f} seconds...") # Wait the calculated time time.sleep(seconds_to_wait) # Send note-off events for all notes for note, _, _, _ in notes_at_position: channels.midiNoteOn(channel, note, 0) # Stop playback transport.stop() # Exit recording mode if it was active if transport.isRecording(): transport.record() # Small pause between recordings to avoid potential issues time.sleep(0.2) print("All notes recorded successfully") # Return to beginning transport.setSongPos(0, 2) def rec_melody(): """ Records a predefined melody to the piano roll by calling record_notes_batch The melody is a robust 4-bar composition with melody notes and chord accompaniment """ # Stop playback and rewind to beginning first if transport.isPlaying(): transport.stop() transport.setSongPos(0, 2) # Go to the beginning print("Recording melody...") # Define the melody as a list of notes # Each tuple contains (note, velocity, length_beats, position_beats) melody = [ # BAR 1 # Beat 1: C major chord (60, 100, 1.0, 0.0), # C4 - root note (64, 85, 1.0, 0.0), # E4 - chord tone (67, 80, 1.0, 0.0), # G4 - chord tone # Beat 1.5: Melody note (72, 110, 0.5, 0.5), # C5 - melody # Beat 2: G7 chord (55, 90, 1.0, 1.0), # G3 - bass note (59, 75, 1.0, 1.0), # B3 - chord tone (62, 75, 1.0, 1.0), # D4 - chord tone (65, 75, 1.0, 1.0), # F4 - chord tone # Beat 2.5-3: Melody phrase (71, 105, 0.25, 1.5), # B4 - melody (69, 95, 0.25, 1.75), # A4 - melody (67, 90, 0.5, 2.0), # G4 - melody # Beat 3: C major chord (48, 95, 1.0, 2.0), # C3 - bass note (64, 75, 1.0, 2.0), # E4 - chord tone (67, 75, 1.0, 2.0), # G4 - chord tone # Beat 4: Melody fill (64, 100, 0.5, 3.0), # E4 - melody (65, 90, 0.25, 3.5), # F4 - melody (67, 95, 0.25, 3.75), # G4 - melody # BAR 2 # Beat 1: Am chord (57, 95, 1.0, 4.0), # A3 - bass note (60, 80, 1.0, 4.0), # C4 - chord tone (64, 80, 1.0, 4.0), # E4 - chord tone # Beat 1-2: Melody note (69, 110, 0.75, 4.0), # A4 - melody (67, 90, 0.25, 4.75), # G4 - melody # Beat 2: F major chord (53, 90, 1.0, 5.0), # F3 - bass note (57, 75, 1.0, 5.0), # A3 - chord tone (60, 75, 1.0, 5.0), # C4 - chord tone # Beat 2.5-3: Melody (65, 100, 0.5, 5.5), # F4 - melody (64, 90, 0.5, 6.0), # E4 - melody # Beat 3: G7 chord (55, 95, 1.0, 6.0), # G3 - bass note (59, 80, 1.0, 6.0), # B3 - chord tone (62, 80, 1.0, 6.0), # D4 - chord tone # Beat 3.5-4: Melody fill (62, 100, 0.25, 6.5), # D4 - melody (64, 95, 0.25, 6.75), # E4 - melody (65, 90, 0.25, 7.0), # F4 - melody (67, 105, 0.75, 7.25), # G4 - melody # BAR 3 # Beat 1: C major chord (48, 100, 1.0, 8.0), # C3 - bass note (60, 85, 1.0, 8.0), # C4 - chord tone (64, 85, 1.0, 8.0), # E4 - chord tone (67, 85, 1.0, 8.0), # G4 - chord tone # Beat 1-2: Melody (72, 110, 1.0, 8.0), # C5 - melody # Beat 2: Em chord (52, 90, 1.0, 9.0), # E3 - bass note (59, 75, 1.0, 9.0), # B3 - chord tone (64, 75, 1.0, 9.0), # E4 - chord tone # Beat 2.5-3.5: Melody run (71, 105, 0.25, 9.5), # B4 - melody (72, 100, 0.25, 9.75), # C5 - melody (74, 110, 0.5, 10.0), # D5 - melody (76, 115, 0.5, 10.5), # E5 - melody # Beat 3: Am chord (57, 95, 1.0, 10.0), # A3 - bass note (60, 80, 1.0, 10.0), # C4 - chord tone (64, 80, 1.0, 10.0), # E4 - chord tone # Beat 4: Descending run (74, 100, 0.25, 11.0), # D5 - melody (72, 95, 0.25, 11.25), # C5 - melody (71, 90, 0.25, 11.5), # B4 - melody (69, 85, 0.25, 11.75), # A4 - melody # BAR 4 # Beat 1: F major chord (53, 95, 1.0, 12.0), # F3 - bass note (60, 80, 1.0, 12.0), # C4 - chord tone (65, 80, 1.0, 12.0), # F4 - chord tone # Beat 1-2: Melody (67, 100, 1.0, 12.0), # G4 - melody # Beat 2: G7 chord (55, 90, 1.0, 13.0), # G3 - bass note (59, 75, 1.0, 13.0), # B3 - chord tone (62, 75, 1.0, 13.0), # D4 - chord tone # Beat 2-3: Melody (65, 95, 0.5, 13.0), # F4 - melody (64, 90, 0.5, 13.5), # E4 - melody # Beat 3-4: Final C major chord (48, 110, 2.0, 14.0), # C3 - bass note (60, 95, 2.0, 14.0), # C4 - chord tone (64, 95, 2.0, 14.0), # E4 - chord tone (67, 95, 2.0, 14.0), # G4 - chord tone # Final melody note (72, 120, 2.0, 14.0), # C5 - melody final note ] # Record the melody using the batch recording function record_notes_batch(melody) print("Melody recording complete!") def change_tempo_from_notes(note_array): """ Change the tempo in FL Studio based on an array of MIDI notes This function converts an array of MIDI notes to a single integer value and uses that value as the new tempo. Args: note_array (list): A list of MIDI note values (each 0-127) """ # Convert note array to integer bpm_value = midi_notes_to_int(note_array) # Limit to a reasonable BPM range if bpm_value < 20: bpm_value = 20 # Minimum reasonable tempo elif bpm_value > 999: bpm_value = 999 # Maximum reasonable tempo # Change the tempo print(f"Changing tempo to {bpm_value} BPM from note array {note_array}") change_tempo(bpm_value) return bpm_value # Start the terminal interface when loaded in FL Studio # No need to call this explicitly as OnInit will be called by FL Studio ```