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