#
tokens: 14539/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── README.md
├── requirements.txt
├── Test Controller
│   └── device_test.py.py
└── trigger.py
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # flstudio MCP
  2 | 
  3 | # This is an MCP server that connects Claude to FL Studio.
  4 | Made this in 3 days. We're open sourcing it to see what we can actually get out of it. The possibilities are endless.
  5 | 
  6 | ## If you're running to any issues, join our discord and we can setup it for you.
  7 | (also join if you interested in the future of music and AI or want to request features. we're building this with you)
  8 | 
  9 | https://discord.gg/ZjG9TaEhvy
 10 | 
 11 | Check out our AI-Powered DAW for musicians at www.veena.studio
 12 | 
 13 | All in browser. All for free.
 14 | 
 15 | 
 16 | ## Step 1: Download the Files
 17 | You should see two main items.
 18 | 
 19 | - A folder called Test Controller
 20 | - A python file called trigger.py
 21 | The Test Controller folder has a file called device_test.py that receives information from the MCP server.
 22 | trigger.py is the MCP server.
 23 | 
 24 | Place the Test Controller folder in Image-Line/FL Studio/Settings/Hardware (Don't change the name of this file or folder)
 25 | 
 26 | ## Step 2: Set up MCP for Claude
 27 | Follow this tutorial to see how to setup MCP servers in Claude by edyting the claude_desktop_config files.
 28 | 
 29 | https://modelcontextprotocol.io/quickstart/server
 30 | 
 31 | If you followed this process, make sure to change whatever mentions of weather.py to trigger.py
 32 | 
 33 | If the Hammer icon doesn't show up, open Task Manager and force close the Claude process.
 34 | 
 35 | It should then show up.
 36 | 
 37 | This is what my config file looks like
 38 | 
 39 | ![mcp](https://github.com/user-attachments/assets/e8e609f7-eaa4-469b-9140-c05b5a9bf242)
 40 | 
 41 | ## Step 3: Set Up Virtual MIDI Ports
 42 | 
 43 | ### For Windows
 44 | For Windows, download LoopMIDI from here.
 45 | 
 46 | https://www.tobias-erichsen.de/software/loopmidi.html
 47 | 
 48 | Install LoopMIDI and add a port using the + button.
 49 | 
 50 | This is what mine looks like:
 51 | ![loopmidi2](https://github.com/user-attachments/assets/fdc2770f-e07a-4b19-824b-56de8a4aa2c3)
 52 | 
 53 | ### For Mac
 54 | Your MIDI Ports would be automatically setup to receive data.
 55 | 
 56 | ## Step 4: Setup MIDI Controller
 57 | Open FL Studio.
 58 | 
 59 | Go To Options > MIDI Settings.
 60 | 
 61 | In the Input Tab, click the MIDI Input you just created with LoopMIDI.
 62 | 
 63 | Change controller type from (generic controller) to Test Controller.
 64 | 
 65 | ## Step 5: Download Packages
 66 | Go to the folder with the trigger.py file. (This is the MCP Server file)
 67 | 
 68 | Activate the conda environment (like you learned in the Claude MCP Setup Tutorial)
 69 | 
 70 | Run this command to download the necessary packages: uv pip install httpx mido python-rtmidi typing fastmcp FL-Studio-API-Stubs
 71 | (uv should be installed from the Claude MCP setup)
 72 | 
 73 | ## Step 6: Verify MCP Connection
 74 | Tell Claude to get available MIDI ports.
 75 | 
 76 | This should use the MCP to get the ports from FL Studio.
 77 | 
 78 | If Windows, copy the port you created with LoopMIDI and the number in front of it.
 79 | 
 80 | If Mac, copy the default port.
 81 | 
 82 | ![loopmidi](https://github.com/user-attachments/assets/a14b0aaa-5127-47c9-b041-fcb5a70339d9)
 83 | 
 84 | In my case, I copy loopMIDI Port 2
 85 | 
 86 | Open trigger.py in a text editor and replace the default port with the name of the port you just copied.
 87 | output_port = mido.open_output('loopMIDI Port 2') 
 88 | 
 89 | 
 90 | ## Step 7: Make Music
 91 | Use the MCP to send melodies, chords, drums, etc.
 92 | 
 93 | Click on the instrument you want to record to and it will live record to the piano roll of that instrument.
 94 | 
 95 | 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)
 96 | 
 97 | ## Step 8: Share what you made
 98 | Share what you made on our Discord: https://discord.gg/ZjG9TaEhvy
 99 | 
100 | ## Credits
101 | FL Studio API Stubs: https://github.com/IL-Group/FL-Studio-API-Stubs
102 | Ableton MCP: https://github.com/ahujasid/ableton-mcp
103 | 
104 | ## Nerd Stuff
105 | If you want to contribute please go ahead. 
106 | 
107 | The way this works is that device_test.py behaves as a virtual MIDI Controller.
108 | The MCP server (trigger.py) communicates with this MIDI Controller by opening a Virtual Port and sending MIDI messages through a library called MIDO.
109 | 
110 | The issue with MIDI messages is that its only 7 bits so we can only send in number from 0-127.
111 | 
112 | So we encrypt all of our MIDI data like note position, etc in multiple MIDI notes that the device knows how to read.
113 | 
114 | Hopefully, Image Line can give us more access to their DAW via their API so we don't have to do this MIDI nonsense.
115 | 
116 | 
117 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | mido==1.3.3
2 | python-rtmidi==1.5.8
3 | fl-studio-api-stubs==37.0.1
4 | 
```

--------------------------------------------------------------------------------
/trigger.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import Any
  2 | import httpx
  3 | from mcp.server.fastmcp import FastMCP
  4 | import mido
  5 | from mido import Message
  6 | import time
  7 | 
  8 | # Initialize FastMCP server
  9 | mcp = FastMCP("flstudio")
 10 | output_port = mido.open_output('loopMIDI Port 2') 
 11 | 
 12 | # MIDI Note mappings for FL Studio commands
 13 | NOTE_PLAY = 60          # C3
 14 | NOTE_STOP = 61          # C#3
 15 | NOTE_RECORD = 62        # D3
 16 | NOTE_NEW_PROJECT = 63   # D#3
 17 | NOTE_SET_BPM = 64       # E3
 18 | NOTE_NEW_PATTERN = 65   # F3
 19 | NOTE_SELECT_PATTERN = 66  # F#3
 20 | NOTE_ADD_CHANNEL = 67   # G3
 21 | NOTE_NAME_CHANNEL = 68  # G#3
 22 | NOTE_ADD_NOTE = 69      # A3
 23 | NOTE_ADD_TO_PLAYLIST = 70  # A#3
 24 | NOTE_SET_PATTERN_LEN = 71  # B3
 25 | NOTE_CHANGE_TEMPO = 72   
 26 | 
 27 | # Define custom MIDI CC messages for direct step sequencer grid control
 28 | CC_SELECT_CHANNEL = 100  # Select which channel to edit
 29 | CC_SELECT_STEP = 110     # Select which step to edit
 30 | CC_TOGGLE_STEP = 111     # Toggle the selected step on/off
 31 | CC_STEP_VELOCITY = 112   # Set velocity for the selected step
 32 | 
 33 | # Drum sound MIDI notes
 34 | KICK = 36      # C1
 35 | SNARE = 38     # D1
 36 | CLAP = 39      # D#1
 37 | CLOSED_HAT = 42  # F#1
 38 | OPEN_HAT = 46  # A#1
 39 | 
 40 | 
 41 | 
 42 | @mcp.tool()
 43 | def list_midi_ports():
 44 |     """List all available MIDI input ports"""
 45 |     print("\nAvailable MIDI Input Ports:")
 46 |     input_ports = mido.get_output_names()
 47 |     if not input_ports:
 48 |         print("  No MIDI input ports found")
 49 |     else:
 50 |         for i, port in enumerate(input_ports):
 51 |             print(f"  {i}: {port}")
 52 |     
 53 |     return input_ports
 54 | 
 55 | @mcp.tool()
 56 | def play():
 57 |     """Send MIDI message to start playback in FL Studio"""
 58 |     # Send Note On for C3 (note 60)
 59 |     output_port.send(mido.Message('note_on', note=60, velocity=100))
 60 |     time.sleep(0.1)  # Small delay
 61 |     output_port.send(mido.Message('note_off', note=60, velocity=0))
 62 |     print("Sent Play command")
 63 | 
 64 | @mcp.tool()
 65 | def stop():
 66 |     """Send MIDI message to stop playback in FL Studio"""
 67 |     # Send Note On for C#3 (note 61)
 68 |     output_port.send(mido.Message('note_on', note=61, velocity=100))
 69 |     time.sleep(0.1)  # Small delay
 70 |     output_port.send(mido.Message('note_off', note=61, velocity=0))
 71 |     print("Sent Stop command")
 72 | 
 73 | def int_to_midi_bytes(value):
 74 |     """
 75 |     Convert an integer value into an array of MIDI-compatible bytes (7-bit values)
 76 |     
 77 |     Args:
 78 |         value (int): The integer value to convert
 79 |         
 80 |     Returns:
 81 |         list: Array of MIDI bytes (each 0-127)
 82 |     """
 83 |     if value < 0:
 84 |         print("Warning: Negative values not supported, converting to positive")
 85 |         value = abs(value)
 86 |     
 87 |     # Special case for zero
 88 |     if value == 0:
 89 |         return [0]
 90 |     
 91 |     # Convert to MIDI bytes (7-bit values, MSB first)
 92 |     midi_bytes = []
 93 |     while value > 0:
 94 |         # Extract the lowest 7 bits and prepend to array
 95 |         midi_bytes.insert(0, value & 0x7F)  # 0x7F = 127 (binary: 01111111)
 96 |         # Shift right by 7 bits
 97 |         value >>= 7
 98 |     
 99 |     return midi_bytes
100 | 
101 | def change_tempo(bpm):
102 |     """
103 |     Change the tempo in FL Studio using a sequence of MIDI notes
104 |     
105 |     This function converts a BPM value to an array of MIDI notes,
106 |     sends a start marker, the notes, and an end marker to trigger
107 |     a tempo change in FL Studio.
108 |     
109 |     Args:
110 |         bpm (float): The desired tempo in beats per minute
111 |     """
112 |     # Ensure BPM is within a reasonable range
113 |     if bpm < 20 or bpm > 999:
114 |         print(f"Warning: BPM value {bpm} is outside normal range (20-999)")
115 |         bpm = max(20, min(bpm, 999))
116 |     
117 |     # Convert BPM to integer
118 |     bpm_int = int(bpm)
119 |     
120 |     # Convert to MIDI bytes
121 |     midi_notes = int_to_midi_bytes(bpm_int)
122 |     
123 |     print(f"Setting tempo to {bpm_int} BPM using note array: {midi_notes}")
124 |     
125 |     # Send start marker (note 72)
126 |     send_midi_note(72)
127 |     time.sleep(0.2)
128 |     
129 |     # Send each note in the array
130 |     for note in midi_notes:
131 |         send_midi_note(note)
132 |         time.sleep(0.1)
133 |     
134 |     # Send end marker (note 73)
135 |     send_midi_note(73)
136 |     time.sleep(0.2)
137 |     
138 |     print(f"Tempo change to {bpm_int} BPM sent successfully using {len(midi_notes)} notes")
139 | 
140 | @mcp.tool()
141 | def send_melody(notes_data):
142 |     """
143 |     Send a sequence of MIDI notes with timing information to FL Studio for recording
144 |     
145 |     Args:
146 |         notes_data (str): String containing note data in format "note,velocity,length,position"
147 |                          with each note on a new line
148 |     """
149 |     # Parse the notes_data string into a list of note tuples
150 |     notes = []
151 |     for line in notes_data.strip().split('\n'):
152 |         if not line.strip():
153 |             continue
154 |             
155 |         parts = line.strip().split(',')
156 |         if len(parts) != 4:
157 |             print(f"Warning: Skipping invalid line: {line}")
158 |             continue
159 |             
160 |         try:
161 |             note = min(127, max(0, int(parts[0])))
162 |             velocity = min(127, max(0, int(parts[1])))
163 |             length = max(0, float(parts[2]))
164 |             position = max(0, float(parts[3]))
165 |             notes.append((note, velocity, length, position))
166 |         except ValueError:
167 |             print(f"Warning: Skipping line with invalid values: {line}")
168 |             continue
169 |     
170 |     if not notes:
171 |         return "No valid notes found in input data"
172 |     
173 |     # Create the MIDI data array (5 values per note)
174 |     midi_data = []
175 |     for note, velocity, length, position in notes:
176 |         # 1. Note value (0-127)
177 |         midi_data.append(note)
178 |         
179 |         # 2. Velocity value (0-127)
180 |         midi_data.append(velocity)
181 |         
182 |         # 3. Length whole part (0-127)
183 |         length_whole = min(127, int(length))
184 |         midi_data.append(length_whole)
185 |         
186 |         # 4. Length decimal part (0-9)
187 |         length_decimal = int(round((length - length_whole) * 10)) % 10
188 |         midi_data.append(length_decimal)
189 |         
190 |         # 5. Position whole part (0-127)
191 |         position_whole = min(127, int(position))
192 |         midi_data.append(position_whole)
193 |         
194 |         # 6. Position decimal part (0-9)
195 |         position_decimal = int(round((position - position_whole) * 10)) % 10
196 |         midi_data.append(position_decimal)
197 |     
198 |     # Start MIDI transfer
199 |     print(f"Transferring {len(notes)} notes ({len(midi_data)} MIDI values)...")
200 |     
201 |     # Initial toggle signal (note 0)
202 |     send_midi_note(0)
203 |     time.sleep(0.01)
204 |     
205 |     # Send total count of notes
206 |     send_midi_note(min(127, len(notes)))
207 |     time.sleep(0.01)
208 |     
209 |     # Send all MIDI data values
210 |     for i, value in enumerate(midi_data):
211 |         send_midi_note(value)
212 |         #time.sleep(0.1)
213 |         #print(f"Sent MIDI value {i+1}/{len(midi_data)}: {value}")
214 |     
215 |     send_midi_note(127)
216 | 
217 |     #print(f"Melody transfer complete: {len(notes)} notes sent")
218 |     return f"Melody successfully transferred: {len(notes)} notes ({len(midi_data)} MIDI values) sent to FL Studio"
219 | 
220 | # Send a MIDI note message
221 | @mcp.tool()
222 | def send_midi_note(note, velocity=1, duration=0.01):
223 |     """Send a MIDI note on/off message with specified duration"""
224 |     note_on = Message('note_on', note=note, velocity=velocity)
225 |     output_port.send(note_on)
226 |     #print(f"Sent MIDI note {note} (on), velocity {velocity}")
227 |     time.sleep(duration)
228 |     note_off = Message('note_off', note=note, velocity=0)
229 |     output_port.send(note_off)
230 |     print(f"Sent MIDI note {note} (off)")
231 |     #time.sleep(0.1)  # Small pause between messages
232 |     
233 | if __name__ == "__main__":
234 |     # Initialize and run the server
235 |     mcp.run(transport='stdio')
236 | 
```

--------------------------------------------------------------------------------
/Test Controller/device_test.py.py:
--------------------------------------------------------------------------------

```python
  1 | # device_test.py
  2 | # name=Test Controller
  3 | 
  4 | import transport
  5 | import ui
  6 | import midi
  7 | import channels
  8 | import playlist
  9 | import patterns
 10 | import arrangement
 11 | import general
 12 | import device
 13 | import time
 14 | import sys
 15 | 
 16 | # Global variables
 17 | running = True
 18 | command_history = []
 19 | current_pattern = 0
 20 | current_channel = 0
 21 | terminal_active = False
 22 | 
 23 | collecting_tempo_notes = False
 24 | tempo_note_array = []
 25 | NOTE_TEMPO_START = 72  # C4 starts tempo note collection
 26 | NOTE_TEMPO_END = 73    # C#4 ends collection and applies tempo change
 27 | 
 28 | channel_to_edit = 0
 29 | step_to_edit = 0
 30 | 
 31 | def midi_notes_to_int(midi_notes):
 32 |     """
 33 |     Convert an array of MIDI note values (7 bits each) into a single integer
 34 |     
 35 |     This function takes a list of MIDI notes and combines them into a single
 36 |     integer value, with the first note being the most significant.
 37 |     
 38 |     Args:
 39 |         midi_notes (list): A list of MIDI note values (each 0-127)
 40 |         
 41 |     Returns:
 42 |         int: The combined integer value
 43 |     """
 44 |     result = 0
 45 |     for note in midi_notes:
 46 |         # Ensure each value is within MIDI range (0-127)
 47 |         note_value = min(127, max(0, note))
 48 |         # Shift left by 7 bits (MIDI values use 7 bits) and combine
 49 |         result = (result << 7) | note_value
 50 |     return result
 51 | 
 52 | def OnInit():
 53 |     """Called when the script is loaded by FL Studio"""
 54 |     print("FL Studio Terminal Beat Builder initialized")
 55 |     print("Type 'help' for a list of commands")
 56 |     
 57 |     return
 58 | 
 59 | def OnDeInit():
 60 |     """Called when the script is unloaded by FL Studio"""
 61 |     global running
 62 |     running = False  # Signal the terminal thread to exit
 63 |     print("FL Studio Terminal Beat Builder deinitialized")
 64 |     return
 65 | 
 66 | def OnRefresh(flags):
 67 |     """Called when FL Studio's state changes or when a refresh is needed"""
 68 |     # Update terminal with current state if needed
 69 |     return
 70 | 
 71 | def OnMidiIn(event):
 72 |     """Called whenever the device sends a MIDI message to FL Studio"""
 73 |     #print(f"MIDI In - Status: {event.status}, Data1: {event.data1}, Data2: {event.data2}")
 74 |     return
 75 | 
 76 | def change_tempo(bpm):
 77 |     """
 78 |     Change the tempo in FL Studio to the specified BPM value
 79 |     
 80 |     Args:
 81 |         bpm (float): The desired tempo in beats per minute
 82 |     """
 83 |     # FL Studio stores tempo as BPM * 1000
 84 |     tempo_value = int(bpm * 1000)
 85 |     
 86 |     # Use processRECEvent to set the tempo
 87 |     # REC_Tempo is the event ID for tempo
 88 |     # REC_Control | REC_UpdateControl flags ensure the value is set and the UI updates
 89 |     general.processRECEvent(
 90 |         midi.REC_Tempo,
 91 |         tempo_value,
 92 |         midi.REC_Control | midi.REC_UpdateControl
 93 |     )
 94 | 
 95 | def process_received_midi(note, velocity):
 96 | 
 97 |     global current_note, current_velocity, current_length, current_position
 98 |     global decimal_state, decimal_value, decimal_target
 99 |     
100 |     # Special MIDI commands
101 |     DECIMAL_MARKER = 100   # Indicates next value is a decimal part
102 |     LENGTH_MARKER = 101    # Next value affects length
103 |     POSITION_MARKER = 102  # Next value affects position
104 |     
105 |     # Process based on message type
106 |     if note == DECIMAL_MARKER:
107 |         # Next value will be a decimal
108 |         decimal_state = 1
109 |         return False
110 |         
111 |     elif note == LENGTH_MARKER:
112 |         # Next value affects length
113 |         decimal_target = "length"
114 |         decimal_state = 0
115 |         decimal_value = 0
116 |         return False
117 |         
118 |     elif note == POSITION_MARKER:
119 |         # Next value affects position
120 |         decimal_target = "position"
121 |         decimal_state = 0
122 |         decimal_value = 0
123 |         return False
124 |         
125 |     elif decimal_state == 1:
126 |         # This is a decimal part value
127 |         decimal_value = note / 10.0  # Convert to decimal (0-9 becomes 0.0-0.9)
128 |         
129 |         # Apply to the correct parameter
130 |         if decimal_target == "length":
131 |             current_length = (current_length or 0) + decimal_value
132 |             print(f"Set length decimal: {current_length:.2f}")
133 |         elif decimal_target == "position":
134 |             current_position = (current_position or 0) + decimal_value
135 |             print(f"Set position decimal: {current_position:.2f}")
136 |             
137 |         decimal_state = 0
138 |         return False
139 |         
140 |     elif decimal_target is not None:
141 |         # This is a whole number part for a specific parameter
142 |         if decimal_target == "length":
143 |             current_length = float(note)
144 |             print(f"Set length whole: {current_length:.2f}")
145 |         elif decimal_target == "position":
146 |             current_position = float(note)
147 |             print(f"Set position whole: {current_position:.2f}")
148 |         return False
149 |         
150 |     else:
151 |         # This is a note value and velocity
152 |         # Check if we have a complete previous note to add
153 |         add_note = (current_note is not None and 
154 |                    current_velocity is not None and 
155 |                    current_length is not None and 
156 |                    current_position is not None)
157 |         
158 |         # Start a new note
159 |         current_note = note
160 |         current_velocity = velocity
161 |         # Use default values if not specified
162 |         if current_length is None:
163 |             current_length = 1.0
164 |         if current_position is None:
165 |             current_position = 0.0
166 |         print(f"Started new note: {current_note}, velocity: {current_velocity}")
167 |         
168 |         return add_note
169 | 
170 | 
171 | def OnMidiMsg(event, timestamp=0):
172 |     """Called when a processed MIDI message is received"""
173 |     
174 |     global receiving_mode, message_count, messages_received
175 |     global current_note, current_velocity, current_length, current_position
176 |     global decimal_state, decimal_target, midi_notes_array
177 |     
178 |     if 'receiving_mode' not in globals():
179 |         global receiving_mode
180 |         receiving_mode = False
181 |     
182 |     if 'note_count' not in globals():
183 |         global note_count
184 |         note_count = 0
185 |     
186 |     if 'values_received' not in globals():
187 |         global values_received
188 |         values_received = 0
189 |     
190 |     if 'midi_data' not in globals():
191 |         global midi_data
192 |         midi_data = []
193 |     
194 |     if 'midi_notes_array' not in globals():
195 |         global midi_notes_array
196 |         midi_notes_array = []
197 |     
198 |     # Only process Note On messages with velocity > 0
199 |     if event.status >= midi.MIDI_NOTEON and event.status < midi.MIDI_NOTEON + 16 and event.data2 > 0:
200 |         note_value = event.data1
201 |         
202 |         # Toggle receiving mode with note 0
203 |         if note_value == 0 and not receiving_mode:
204 |             receiving_mode = True
205 |             print("Started receiving MIDI notes")
206 |             midi_data = []
207 |             note_count = 0
208 |             values_received = 0
209 |             midi_notes_array = []
210 |             event.handled = True
211 |             return
212 |         
213 |         # Only process further messages if in receiving mode
214 |         if not receiving_mode:
215 |             return
216 |         
217 |         # Second message is the note count
218 |         if note_count == 0:
219 |             note_count = note_value
220 |             print(f"Expecting {note_count} notes")
221 |             event.handled = True
222 |             return
223 |         
224 |         # All subsequent messages are MIDI values (6 per note)
225 |         midi_data.append(note_value)
226 |         values_received += 1
227 |         
228 |         # Process completed notes (every 6 values)
229 |         if len(midi_data) >= 6 and len(midi_data) % 6 == 0:
230 |             # Process the last complete note
231 |             i = len(midi_data) - 6
232 |             note = midi_data[i]
233 |             velocity = midi_data[i+1]
234 |             length_whole = midi_data[i+2]
235 |             length_decimal = midi_data[i+3]
236 |             position_whole = midi_data[i+4]
237 |             position_decimal = midi_data[i+5]
238 |             
239 |             # Calculate full values
240 |             length = length_whole + (length_decimal / 10.0)
241 |             position = position_whole + (position_decimal / 10.0)
242 |             
243 |             # Add to notes array
244 |             midi_notes_array.append((note, velocity, length, position))
245 |             print(f"Added note: note={note}, velocity={velocity}, length={length:.1f}, position={position:.1f}")
246 |             print(f"Current array size: {len(midi_notes_array)}")
247 | 
248 |             if len(midi_notes_array) >= note_count or note_value == 127:
249 |                 print(f"Received all {len(midi_notes_array)} notes or termination signal")
250 |                 receiving_mode = False
251 |                 
252 |                 # Only process if we have actual notes
253 |                 if midi_notes_array:
254 |                     # Print all collected notes
255 |                     print(f"Collected {len(midi_notes_array)} notes:")
256 |                     for i, (note, vel, length, pos) in enumerate(midi_notes_array):
257 |                         print(f"  Note {i+1}: note={note}, velocity={vel}, length={length:.1f}, position={pos:.1f}")
258 |                     
259 |                     print("\nFinal array:")
260 |                     print(midi_notes_array)
261 |                     
262 |                     # Process the notes using the record_notes_batch function
263 |                     record_notes_batch(midi_notes_array)
264 |                 
265 |                 event.handled = True
266 |                 return
267 |         
268 |         # Check if we've received all expected notes
269 |         # if len(midi_notes_array) >= note_count:
270 |         #     print(f"Received all {note_count} notes")
271 |         #     receiving_mode = False
272 |             
273 |         #     # Print all collected notes
274 |         #     print(f"Collected {len(midi_notes_array)} notes:")
275 |         #     for i, (note, vel, length, pos) in enumerate(midi_notes_array):
276 |         #         print(f"  Note {i+1}: note={note}, velocity={vel}, length={length:.1f}, position={pos:.1f}")
277 |             
278 |         #     print("\nFinal array:")
279 |         #     print(midi_notes_array)
280 | 
281 |         #     record_notes_batch(midi_notes_array)
282 |             
283 |         #     # Process the notes here if needed
284 |         #     # record_notes_batch(midi_notes_array)
285 |         
286 |         # event.handled = True
287 |         
288 | 
289 | 
290 |     # elif note == 72:
291 |     #         collecting_tempo_notes = True
292 |     #         tempo_note_array = []
293 |     #         print("Started collecting notes for tempo change")
294 |     #         event.handled = True
295 |             
296 |     #     # End collection and apply tempo change
297 |     # elif note == 73:
298 |     #         if collecting_tempo_notes and tempo_note_array:
299 |     #             bpm = change_tempo_from_notes(tempo_note_array)
300 |     #             print(f"Tempo changed to {bpm} BPM from collected notes: {tempo_note_array}")
301 |     #             collecting_tempo_notes = False
302 |     #             tempo_note_array = []
303 |     #         else:
304 |     #             print("No tempo notes collected, tempo unchanged")
305 |     #         event.handled = True
306 |             
307 |     #     # Collect notes for tempo if in collection mode
308 |     # elif collecting_tempo_notes:
309 |     #         tempo_note_array.append(note)
310 |     #         print(f"Added note {note} to tempo collection, current array: {tempo_note_array}")
311 |     #         event.handled = True
312 |     
313 |     # Handle Control Change messages
314 |     # elif event.status >= midi.MIDI_CONTROLCHANGE and event.status < midi.MIDI_CONTROLCHANGE + 16:
315 |     #     # CC 100: Select channel to edit
316 |     #     if event.data1 == 100:
317 |     #         channel_to_edit = event.data2
318 |     #         channels.selectOneChannel(channel_to_edit)
319 |     #         print(f"Selected channel {channel_to_edit} for grid editing")
320 |     #         event.handled = True
321 |             
322 |     #     # CC 110: Select step to edit
323 |     #     elif event.data1 == 110:
324 |     #         step_to_edit = event.data2
325 |     #         print(f"Selected step {step_to_edit} for grid editing")
326 |     #         event.handled = True
327 |             
328 |     #     # CC 111: Toggle step on/off
329 |     #     elif event.data1 == 111:
330 |     #         enabled = event.data2 > 0
331 |     #         channels.setGridBit(channel_to_edit, step_to_edit, enabled)
332 |     #         print(f"Set grid bit for channel {channel_to_edit}, step {step_to_edit} to {enabled}")
333 |     #         commit_pattern_changes()  # Force UI update
334 |     #         event.handled = True
335 |             
336 |     #     # CC 112: Set step velocity/level
337 |     #     elif event.data1 == 112:
338 |     #         velocity = event.data2
339 |     #         channels.setStepLevel(channel_to_edit, step_to_edit, velocity)
340 |     #         print(f"Set step level for channel {channel_to_edit}, step {step_to_edit} to {velocity}")
341 |     #         commit_pattern_changes()  # Force UI update
342 |     #         event.handled = True
343 |         
344 |     #     # Process other CC messages
345 |     #     else:
346 |     #         # Handle other CC messages with your existing code...
347 |     #         pass
348 |     
349 |     # # Handle other MIDI message types if needed
350 |     # else:
351 |     #     # Process other MIDI message types
352 |     #     pass
353 |     
354 |     # Handle Note On messages with your existing code...
355 |     # [rest of your existing OnMidiMsg function]
356 |     #record_notes_batch(midi_notes_array)
357 | 
358 | # Make sure your commit_pattern_changes function is defined:
359 | def commit_pattern_changes(pattern_num=None):
360 |     """Force FL Studio to update the pattern data visually"""
361 |     if pattern_num is None:
362 |         pattern_num = patterns.patternNumber()
363 |     
364 |     # Force FL Studio to redraw and commit changes
365 |     ui.crDisplayRect()
366 |     
367 |     # Force channel rack to update
368 |     ui.setFocused(midi.widChannelRack)
369 |     
370 |     # Update playlist if needed
371 |     playlist.refresh()
372 | def OnTransport(isPlaying):
373 |     """Called when the transport state changes (play/stop)"""
374 |     print(f"Transport state changed: {'Playing' if isPlaying else 'Stopped'}")
375 |     return
376 | 
377 | def OnTempoChange(tempo):
378 |     """Called when the tempo changes"""
379 |     print(f"Tempo changed to: {tempo} BPM")
380 |     return
381 | 
382 | 
383 | # # Terminal interface functions
384 | # def start_terminal_thread():
385 | #     """Start a thread to handle terminal input"""
386 | #     global terminal_active
387 | #     if not terminal_active:
388 | #         terminal_active = True
389 | #         thread = threading.Thread(target=terminal_loop)
390 | #         thread.daemon = True
391 | #         thread.start()
392 | 
393 | # def terminal_loop():
394 | #     """Main terminal input loop"""
395 | #     global running, terminal_active
396 |     
397 | #     print("\n===== FL STUDIO TERMINAL BEAT BUILDER =====")
398 | #     print("Enter commands to build your beat (type 'help' for commands)")
399 |     
400 | #     while running:
401 | #         try:
402 | #             command = input("\nFLBEAT> ")
403 | #             command_history.append(command)
404 | #             process_command(command)
405 | #         except Exception as e:
406 | #             print(f"Error processing command: {e}")
407 |     
408 | #     terminal_active = False
409 | 
410 | def record_note(note=60, velocity=100, length_beats=1.0, position_beats=0.0, quantize=True):
411 |     """
412 |     Records a single note to the piano roll synced with project tempo
413 |     
414 |     Args:
415 |         note (int): MIDI note number (60 = middle C)
416 |         velocity (int): Note velocity (0-127)
417 |         length_beats (float): Length of note in beats (1.0 = quarter note)
418 |         position_beats (float): Position to place note in beats from start
419 |         quantize (bool): Whether to quantize the recording afterward
420 |     """
421 |     # Make sure transport is stopped first
422 |     if transport.isPlaying():
423 |         transport.stop()
424 |     
425 |     # Get the current channel
426 |     channel = channels.selectedChannel()
427 |     
428 |     # Get the project's PPQ (pulses per quarter note)
429 |     ppq = general.getRecPPQ()
430 |     
431 |     # Calculate ticks based on beats
432 |     length_ticks = int(length_beats * ppq)
433 |     position_ticks = int(position_beats * ppq)
434 |     
435 |     # Set playback position
436 |     transport.setSongPos(position_ticks, 2)  # 2 = SONGLENGTH_ABSTICKS
437 |     
438 |     # Toggle recording mode if needed
439 |     if not transport.isRecording():
440 |         transport.record()
441 |     
442 |     print(f"Recording note {note} to channel {channel}")
443 |     print(f"Position: {position_beats} beats, Length: {length_beats} beats")
444 |     
445 |     # Calculate the exact tick positions where we need to place note and note-off
446 |     start_tick = position_ticks
447 |     end_tick = start_tick + length_ticks
448 |     
449 |     # Start playback to begin recording
450 |     transport.start()
451 |     
452 |     # Record the note at the exact start position
453 |     channels.midiNoteOn(channel, note, velocity)
454 |     
455 |     # Get the current tempo (BPM)
456 |     tempo = 120  # Default fallback
457 |     
458 |     # Try to get actual tempo from the project
459 |     try:
460 |         import mixer
461 |         tempo = mixer.getCurrentTempo()
462 |         tempo = tempo/1000
463 |         print(f"Using project tempo: {tempo} BPM")
464 |     except (ImportError, AttributeError):
465 |         print("Using default tempo: 120 BPM")
466 |     
467 |     # Calculate the time to wait in seconds
468 |     seconds_to_wait = (length_beats * 60) / tempo
469 |     
470 |     print(f"Waiting for {seconds_to_wait:.2f} seconds...")
471 |     
472 |     # Wait the calculated time
473 |     time.sleep(seconds_to_wait)
474 |     
475 |     # Send note-off event
476 |     channels.midiNoteOn(channel, note, 0)
477 |     
478 |     # Stop playback
479 |     transport.stop()
480 |     
481 |     # Exit recording mode if it was active
482 |     if transport.isRecording():
483 |         transport.record()
484 |     
485 |     # Quantize if requested
486 |     if quantize:
487 |         channels.quickQuantize(channel)
488 |         print("Recording quantized")
489 |     
490 |     print(f"Note {note} recorded to piano roll")
491 |     
492 |     # Return to beginning
493 |     transport.setSongPos(0, 2)
494 | 
495 | def rec_hihat_pattern():
496 |     """
497 |     Records a predefined hi-hat pattern to the piano roll using record_notes_batch
498 |     
499 |     This creates a 4-bar hi-hat pattern with variations in velocity, rhythm, and types of hats
500 |     """
501 |     # Stop playback and rewind to beginning first
502 |     if transport.isPlaying():
503 |         transport.stop()
504 |     
505 |     transport.setSongPos(0, 2)  # Go to the beginning
506 |     
507 |     print("Recording hi-hat pattern...")
508 |     
509 |     # Common hi-hat MIDI notes:
510 |     # 42 = Closed hi-hat
511 |     # 44 = Pedal hi-hat
512 |     # 46 = Open hi-hat
513 |     
514 |     # Define the pattern as a list of notes
515 |     # Each tuple contains (note, velocity, length_beats, position_beats)
516 |     hihat_pattern = [
517 |         # BAR 1 - Basic pattern
518 |         (42, 90, 0.1, 0.0),     # Closed hat on beat 1
519 |         (42, 65, 0.1, 0.5),     # Closed hat on off-beat
520 |         (42, 90, 0.1, 1.0),     # Closed hat on beat 2
521 |         (42, 65, 0.1, 1.5),     # Closed hat on off-beat
522 |         (42, 90, 0.1, 2.0),     # Closed hat on beat 3
523 |         (42, 65, 0.1, 2.5),     # Closed hat on off-beat
524 |         (42, 90, 0.1, 3.0),     # Closed hat on beat 4
525 |         (42, 65, 0.1, 3.5),     # Closed hat on off-beat
526 |         
527 |         # BAR 2 - Adding 16th notes
528 |         (42, 90, 0.1, 4.0),     # Closed hat on beat 1
529 |         (42, 60, 0.1, 4.25),    # Closed hat on 16th
530 |         (42, 70, 0.1, 4.5),     # Closed hat on off-beat
531 |         (42, 60, 0.1, 4.75),    # Closed hat on 16th
532 |         (42, 90, 0.1, 5.0),     # Closed hat on beat 2
533 |         (42, 60, 0.1, 5.25),    # Closed hat on 16th
534 |         (42, 70, 0.1, 5.5),     # Closed hat on off-beat
535 |         (42, 60, 0.1, 5.75),    # Closed hat on 16th
536 |         (42, 90, 0.1, 6.0),     # Closed hat on beat 3
537 |         (42, 60, 0.1, 6.25),    # Closed hat on 16th
538 |         (42, 70, 0.1, 6.5),     # Closed hat on off-beat
539 |         (42, 60, 0.1, 6.75),    # Closed hat on 16th
540 |         (42, 90, 0.1, 7.0),     # Closed hat on beat 4
541 |         (46, 80, 0.2, 7.5),     # Open hat on off-beat
542 |         
543 |         # BAR 3 - Mixing closed and open hats
544 |         (42, 100, 0.1, 8.0),    # Closed hat on beat 1
545 |         (42, 70, 0.1, 8.5),     # Closed hat on off-beat
546 |         (46, 85, 0.2, 9.0),     # Open hat on beat 2
547 |         (42, 70, 0.1, 9.5),     # Closed hat on off-beat
548 |         (42, 95, 0.1, 10.0),    # Closed hat on beat 3
549 |         (42, 70, 0.1, 10.5),    # Closed hat on off-beat
550 |         (46, 85, 0.2, 11.0),    # Open hat on beat 4
551 |         
552 |         # Triplet fill at the end of bar 3
553 |         (42, 80, 0.08, 11.33),  # Closed hat - triplet 1
554 |         (42, 85, 0.08, 11.66),  # Closed hat - triplet 2
555 |         (42, 90, 0.08, 11.99),  # Closed hat - triplet 3
556 |         
557 |         # BAR 4 - Complex pattern with pedal hats
558 |         (42, 100, 0.1, 12.0),   # Closed hat on beat 1
559 |         (44, 75, 0.1, 12.25),   # Pedal hat on 16th
560 |         (42, 80, 0.1, 12.5),    # Closed hat on off-beat
561 |         (44, 70, 0.1, 12.75),   # Pedal hat on 16th
562 |         (42, 90, 0.1, 13.0),    # Closed hat on beat 2
563 |         (46, 85, 0.3, 13.5),    # Open hat on off-beat
564 |         
565 |         # Beat 3-4: Building intensity
566 |         (42, 95, 0.1, 14.0),    # Closed hat on beat 3
567 |         (42, 75, 0.1, 14.25),   # Closed hat on 16th
568 |         (42, 85, 0.1, 14.5),    # Closed hat on off-beat
569 |         (42, 80, 0.1, 14.75),   # Closed hat on 16th
570 |         
571 |         # Final fill
572 |         (42, 85, 0.05, 15.0),   # Closed hat - 32nd note 1
573 |         (42, 90, 0.05, 15.125), # Closed hat - 32nd note 2
574 |         (42, 95, 0.05, 15.25),  # Closed hat - 32nd note 3
575 |         (42, 100, 0.05, 15.375),# Closed hat - 32nd note 4
576 |         (42, 105, 0.05, 15.5),  # Closed hat - 32nd note 5
577 |         (42, 110, 0.05, 15.625),# Closed hat - 32nd note 6
578 |         (42, 115, 0.05, 15.75), # Closed hat - 32nd note 7
579 |         (46, 120, 0.25, 15.875),# Open hat - final accent
580 |     ]
581 |     
582 |     # Record the hi-hat pattern using the batch recording function
583 |     record_notes_batch(hihat_pattern)
584 |     
585 |     print("Hi-hat pattern recording complete!")
586 |     
587 |     # Quantize the hi-hat pattern
588 |     channel = channels.selectedChannel()
589 |     channels.quickQuantize(channel)
590 |     
591 |     # Return to beginning
592 |     transport.setSongPos(0, 2)
593 | 
594 | def record_notes_batch(notes_array):
595 |     """
596 |     Records a batch of notes to FL Studio, handling simultaneous notes properly
597 |     
598 |     Args:
599 |         notes_array: List of tuples, each containing (note, velocity, length_beats, position_beats)
600 |     """
601 |     # Sort notes by their starting position
602 |     sorted_notes = sorted(notes_array, key=lambda x: x[3])
603 |     
604 |     # Group notes by their starting positions
605 |     position_groups = {}
606 |     for note in sorted_notes:
607 |         position = note[3]  # position_beats is the 4th element (index 3)
608 |         if position not in position_groups:
609 |             position_groups[position] = []
610 |         position_groups[position].append(note)
611 |     
612 |     # Process each position group
613 |     positions = sorted(position_groups.keys())
614 |     for position in positions:
615 |         notes_at_position = position_groups[position]
616 |         
617 |         # Find the longest note in this group to determine recording length
618 |         max_length = max(note[2] for note in notes_at_position)
619 |         
620 |         # Make sure transport is stopped first
621 |         if transport.isPlaying():
622 |             transport.stop()
623 |         
624 |         # Get the current channel
625 |         channel = channels.selectedChannel()
626 |         
627 |         # Get the project's PPQ (pulses per quarter note)
628 |         ppq = general.getRecPPQ()
629 |         
630 |         # Calculate ticks based on beats
631 |         position_ticks = int(position * ppq)
632 |         
633 |         # Set playback position
634 |         transport.setSongPos(position_ticks, 2)  # 2 = SONGLENGTH_ABSTICKS
635 |         
636 |         # Toggle recording mode if needed
637 |         if not transport.isRecording():
638 |             transport.record()
639 |         
640 |         print(f"Recording {len(notes_at_position)} simultaneous notes at position {position}")
641 |         
642 |         # Start playback to begin recording
643 |         transport.start()
644 |         
645 |         # Record all notes at this position simultaneously
646 |         for note, velocity, length, _ in notes_at_position:
647 |             channels.midiNoteOn(channel, note, velocity)
648 |         
649 |         # Get the current tempo
650 |         try:
651 |             import mixer
652 |             tempo = mixer.getCurrentTempo()
653 |             tempo = tempo/1000
654 |         except (ImportError, AttributeError):
655 |             tempo = 120  # Default fallback
656 |             
657 |         print(f"Using tempo: {tempo} BPM")
658 |         
659 |         # Calculate the time to wait in seconds based on the longest note
660 |         seconds_to_wait = (max_length * 60) / tempo
661 |         
662 |         print(f"Waiting for {seconds_to_wait:.2f} seconds...")
663 |         
664 |         # Wait the calculated time
665 |         time.sleep(seconds_to_wait)
666 |         
667 |         # Send note-off events for all notes
668 |         for note, _, _, _ in notes_at_position:
669 |             channels.midiNoteOn(channel, note, 0)
670 |         
671 |         # Stop playback
672 |         transport.stop()
673 |         
674 |         # Exit recording mode if it was active
675 |         if transport.isRecording():
676 |             transport.record()
677 |         
678 |         # Small pause between recordings to avoid potential issues
679 |         time.sleep(0.2)
680 |     
681 |     print("All notes recorded successfully")
682 |     
683 |     # Return to beginning
684 |     transport.setSongPos(0, 2)
685 | 
686 | 
687 | 
688 | def rec_melody():
689 |     """
690 |     Records a predefined melody to the piano roll by calling record_notes_batch
691 |     
692 |     The melody is a robust 4-bar composition with melody notes and chord accompaniment
693 |     """
694 |     # Stop playback and rewind to beginning first
695 |     if transport.isPlaying():
696 |         transport.stop()
697 |     
698 |     transport.setSongPos(0, 2)  # Go to the beginning
699 |     
700 |     print("Recording melody...")
701 |     
702 |     # Define the melody as a list of notes
703 |     # Each tuple contains (note, velocity, length_beats, position_beats)
704 |     melody = [
705 |         # BAR 1
706 |         # Beat 1: C major chord
707 |         (60, 100, 1.0, 0.0),    # C4 - root note
708 |         (64, 85, 1.0, 0.0),     # E4 - chord tone
709 |         (67, 80, 1.0, 0.0),     # G4 - chord tone
710 |         
711 |         # Beat 1.5: Melody note
712 |         (72, 110, 0.5, 0.5),    # C5 - melody
713 |         
714 |         # Beat 2: G7 chord
715 |         (55, 90, 1.0, 1.0),     # G3 - bass note
716 |         (59, 75, 1.0, 1.0),     # B3 - chord tone
717 |         (62, 75, 1.0, 1.0),     # D4 - chord tone
718 |         (65, 75, 1.0, 1.0),     # F4 - chord tone
719 |         
720 |         # Beat 2.5-3: Melody phrase
721 |         (71, 105, 0.25, 1.5),   # B4 - melody
722 |         (69, 95, 0.25, 1.75),   # A4 - melody
723 |         (67, 90, 0.5, 2.0),     # G4 - melody
724 |         
725 |         # Beat 3: C major chord
726 |         (48, 95, 1.0, 2.0),     # C3 - bass note
727 |         (64, 75, 1.0, 2.0),     # E4 - chord tone
728 |         (67, 75, 1.0, 2.0),     # G4 - chord tone
729 |         
730 |         # Beat 4: Melody fill
731 |         (64, 100, 0.5, 3.0),    # E4 - melody
732 |         (65, 90, 0.25, 3.5),    # F4 - melody
733 |         (67, 95, 0.25, 3.75),   # G4 - melody
734 |         
735 |         # BAR 2
736 |         # Beat 1: Am chord
737 |         (57, 95, 1.0, 4.0),     # A3 - bass note
738 |         (60, 80, 1.0, 4.0),     # C4 - chord tone
739 |         (64, 80, 1.0, 4.0),     # E4 - chord tone
740 |         
741 |         # Beat 1-2: Melody note
742 |         (69, 110, 0.75, 4.0),   # A4 - melody
743 |         (67, 90, 0.25, 4.75),   # G4 - melody
744 |         
745 |         # Beat 2: F major chord
746 |         (53, 90, 1.0, 5.0),     # F3 - bass note
747 |         (57, 75, 1.0, 5.0),     # A3 - chord tone
748 |         (60, 75, 1.0, 5.0),     # C4 - chord tone
749 |         
750 |         # Beat 2.5-3: Melody
751 |         (65, 100, 0.5, 5.5),    # F4 - melody
752 |         (64, 90, 0.5, 6.0),     # E4 - melody
753 |         
754 |         # Beat 3: G7 chord
755 |         (55, 95, 1.0, 6.0),     # G3 - bass note
756 |         (59, 80, 1.0, 6.0),     # B3 - chord tone
757 |         (62, 80, 1.0, 6.0),     # D4 - chord tone
758 |         
759 |         # Beat 3.5-4: Melody fill
760 |         (62, 100, 0.25, 6.5),   # D4 - melody
761 |         (64, 95, 0.25, 6.75),   # E4 - melody
762 |         (65, 90, 0.25, 7.0),    # F4 - melody
763 |         (67, 105, 0.75, 7.25),  # G4 - melody
764 |         
765 |         # BAR 3
766 |         # Beat 1: C major chord
767 |         (48, 100, 1.0, 8.0),    # C3 - bass note
768 |         (60, 85, 1.0, 8.0),     # C4 - chord tone
769 |         (64, 85, 1.0, 8.0),     # E4 - chord tone
770 |         (67, 85, 1.0, 8.0),     # G4 - chord tone
771 |         
772 |         # Beat 1-2: Melody
773 |         (72, 110, 1.0, 8.0),    # C5 - melody
774 |         
775 |         # Beat 2: Em chord
776 |         (52, 90, 1.0, 9.0),     # E3 - bass note
777 |         (59, 75, 1.0, 9.0),     # B3 - chord tone
778 |         (64, 75, 1.0, 9.0),     # E4 - chord tone
779 |         
780 |         # Beat 2.5-3.5: Melody run
781 |         (71, 105, 0.25, 9.5),   # B4 - melody
782 |         (72, 100, 0.25, 9.75),  # C5 - melody
783 |         (74, 110, 0.5, 10.0),   # D5 - melody
784 |         (76, 115, 0.5, 10.5),   # E5 - melody
785 |         
786 |         # Beat 3: Am chord
787 |         (57, 95, 1.0, 10.0),    # A3 - bass note
788 |         (60, 80, 1.0, 10.0),    # C4 - chord tone
789 |         (64, 80, 1.0, 10.0),    # E4 - chord tone
790 |         
791 |         # Beat 4: Descending run
792 |         (74, 100, 0.25, 11.0),  # D5 - melody
793 |         (72, 95, 0.25, 11.25),  # C5 - melody
794 |         (71, 90, 0.25, 11.5),   # B4 - melody
795 |         (69, 85, 0.25, 11.75),  # A4 - melody
796 |         
797 |         # BAR 4
798 |         # Beat 1: F major chord
799 |         (53, 95, 1.0, 12.0),    # F3 - bass note
800 |         (60, 80, 1.0, 12.0),    # C4 - chord tone
801 |         (65, 80, 1.0, 12.0),    # F4 - chord tone
802 |         
803 |         # Beat 1-2: Melody
804 |         (67, 100, 1.0, 12.0),   # G4 - melody
805 |         
806 |         # Beat 2: G7 chord
807 |         (55, 90, 1.0, 13.0),    # G3 - bass note
808 |         (59, 75, 1.0, 13.0),    # B3 - chord tone
809 |         (62, 75, 1.0, 13.0),    # D4 - chord tone
810 |         
811 |         # Beat 2-3: Melody
812 |         (65, 95, 0.5, 13.0),    # F4 - melody
813 |         (64, 90, 0.5, 13.5),    # E4 - melody
814 |         
815 |         # Beat 3-4: Final C major chord
816 |         (48, 110, 2.0, 14.0),   # C3 - bass note
817 |         (60, 95, 2.0, 14.0),    # C4 - chord tone
818 |         (64, 95, 2.0, 14.0),    # E4 - chord tone
819 |         (67, 95, 2.0, 14.0),    # G4 - chord tone
820 |         
821 |         # Final melody note
822 |         (72, 120, 2.0, 14.0),   # C5 - melody final note
823 |     ]
824 |     
825 |     # Record the melody using the batch recording function
826 |     record_notes_batch(melody)
827 |     
828 |     print("Melody recording complete!")
829 |     
830 | def change_tempo_from_notes(note_array):
831 |     """
832 |     Change the tempo in FL Studio based on an array of MIDI notes
833 |     
834 |     This function converts an array of MIDI notes to a single integer value
835 |     and uses that value as the new tempo.
836 |     
837 |     Args:
838 |         note_array (list): A list of MIDI note values (each 0-127)
839 |     """
840 |     # Convert note array to integer
841 |     bpm_value = midi_notes_to_int(note_array)
842 |     
843 |     # Limit to a reasonable BPM range
844 |     if bpm_value < 20:
845 |         bpm_value = 20  # Minimum reasonable tempo
846 |     elif bpm_value > 999:
847 |         bpm_value = 999  # Maximum reasonable tempo
848 |     
849 |     # Change the tempo
850 |     print(f"Changing tempo to {bpm_value} BPM from note array {note_array}")
851 |     change_tempo(bpm_value)
852 |     
853 |     return bpm_value
854 | 
855 | # Start the terminal interface when loaded in FL Studio
856 | # No need to call this explicitly as OnInit will be called by FL Studio
857 | 
```