#
tokens: 11592/50000 4/4 files
lines: off (toggle) GitHub
raw markdown copy
# 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

![mcp](https://github.com/user-attachments/assets/e8e609f7-eaa4-469b-9140-c05b5a9bf242)

## 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:
![loopmidi2](https://github.com/user-attachments/assets/fdc2770f-e07a-4b19-824b-56de8a4aa2c3)

### 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.

![loopmidi](https://github.com/user-attachments/assets/a14b0aaa-5127-47c9-b041-fcb5a70339d9)

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

```