feat: reggaeton production system with intelligent sample selection and FLP generation

This commit is contained in:
renato97
2026-05-02 21:40:18 -03:00
commit 4d941f3f90
62 changed files with 8656 additions and 0 deletions

26
mcp/tests/quick_test.py Normal file
View File

@@ -0,0 +1,26 @@
"""Quick validation of the protocol module."""
import sys
sys.path.insert(0, "C:\\Users\\Administrator\\Documents\\fl_control\\mcp")
from protocol.sysex import nibble_encode, nibble_decode, encode_command, decode_command, SYSEX_ID
# Test 1: nibble roundtrip
for original in [b"Hello", b"", b"\x00\x7f", b'{"cmd":"ping"}']:
encoded = nibble_encode(original)
decoded = nibble_decode(encoded)
assert decoded == original, f"Roundtrip failed for {original}"
print("PASS: nibble roundtrip")
# Test 2: encode/decode command
result = encode_command("ping", {"ts": 1})
assert result[0] == 0xF0 and result[1] == 0x7D and result[-1] == 0xF7
decoded = decode_command(result)
assert decoded["cmd"] == "ping"
assert decoded["params"] == {"ts": 1}
print("PASS: encode/decode command")
# Test 3: SYSEX_ID
assert SYSEX_ID == 0x7D
print("PASS: SYSEX_ID == 0x7D")
print("All unit tests passed!")

46
mcp/tests/send_ping.py Normal file
View File

@@ -0,0 +1,46 @@
"""Send a SysEx ping to FL Studio and check if it arrives."""
import mido
import json
import sys
import time
print("=== FL Studio MCP — Send Ping Test ===")
# List ports
print(f"Inputs: {mido.get_input_names()}")
print(f"Outputs: {mido.get_output_names()}")
# Open FL_MCP 1 output
out = mido.open_output("FL_MCP 1")
print("Opened FL_MCP 1 output")
# Encode ping command
payload = json.dumps({"cmd": "ping", "params": {"ts": 1}}).encode("utf-8")
nibbles = []
for b in payload:
nibbles.append((b >> 4) & 0x0F)
nibbles.append(b & 0x0F)
# mido adds F0/F7 automatically, we provide [SYSEX_ID + nibble data]
msg = mido.Message("sysex", data=bytes([0x7D] + nibbles))
hex_str = " ".join(f"{b:02X}" for b in msg.bytes())
print(f"Sending SysEx ({len(msg.bytes())} bytes): {hex_str}")
out.send(msg)
print("Sent! Check FL Studio script console for: [FL-MCP] Ping received")
# Also try a play command
time.sleep(0.5)
payload2 = json.dumps({"cmd": "start_playback", "params": {}}).encode("utf-8")
nibbles2 = []
for b in payload2:
nibbles2.append((b >> 4) & 0x0F)
nibbles2.append(b & 0x0F)
msg2 = mido.Message("sysex", data=bytes([0x7D] + nibbles2))
hex_str2 = " ".join(f"{b:02X}" for b in msg2.bytes())
print(f"\nSending PLAY SysEx ({len(msg2.bytes())} bytes): {hex_str2}")
out.send(msg2)
print("Sent! FL Studio should start playing if controller is loaded.")
time.sleep(0.5)
out.close()
print("\nDone. Check FL Studio console and transport state.")

View File

@@ -0,0 +1,125 @@
"""
Integration tests for FL Studio MCP server.
Tests nibble encode/decode roundtrip, command encoding, and tool registration.
"""
from __future__ import annotations
import json
import sys
sys.path.insert(0, "C:\\Users\\Administrator\\Documents\\fl_control\\mcp")
from protocol.sysex import nibble_encode, nibble_decode, encode_command, decode_command, SYSEX_ID
from protocol.transport import MidiTransport
def test_nibble_encode_decode_roundtrip():
"""Test that nibble_encode and nibble_decode are perfect inverses."""
test_cases = [
b"Hello",
b"",
b"\x00\x7f\x80\xff",
b'{"cmd":"ping","params":{"ts":1}}',
b"\xff\xfe\xfd\xfc\xfb",
b"A" * 256, # stress test
]
for original in test_cases:
encoded = nibble_encode(original)
# Each byte becomes 2 nibbles
assert len(encoded) == len(original) * 2, f"Length mismatch for {original!r}"
decoded = nibble_decode(encoded)
assert decoded == original, f"Roundtrip failed for {original!r}: got {decoded!r}"
print("PASS: nibble_encode/decode roundtrip")
def test_encode_command_format():
"""Test that encode_command produces valid SysEx format."""
# Test 1: Basic command
result = encode_command("ping", {"ts": 1})
assert result[0] == 0xF0, "Must start with F0"
assert result[1] == SYSEX_ID, "Must have SYSEX_ID=0x7D"
assert result[-1] == 0xF7, "Must end with F7"
print("PASS: encode_command format (basic)")
# Test 2: Empty params
result2 = encode_command("play")
assert result2[0] == 0xF0
assert result2[1] == SYSEX_ID
assert result2[-1] == 0xF7
print("PASS: encode_command format (no params)")
# Test 3: Command decodes back to original JSON
original_cmd = "set_tempo"
original_params = {"tempo": 140.0}
encoded = encode_command(original_cmd, original_params)
decoded = decode_command(encoded)
assert decoded is not None
assert decoded["cmd"] == original_cmd
assert decoded["params"] == original_params
print("PASS: encode_command → decode_command roundtrip")
def test_decode_command_invalid():
"""Test that decode_command returns None for invalid input."""
assert decode_command([]) is None
assert decode_command([0xF0]) is None # too short
assert decode_command([0xF0, 0x7D]) is None # too short
assert decode_command([0xF0, 0x00, 0xF7]) is None # wrong ID
assert decode_command([0xF0, 0x7D, 0xF7]) is None # empty payload
print("PASS: decode_command rejects invalid input")
def test_sysex_id():
"""Verify SYSEX_ID is the correct non-commercial experimental ID."""
assert SYSEX_ID == 0x7D
print("PASS: SYSEX_ID == 0x7D")
def test_miditransport_list_ports():
"""Test that MidiTransport.list_ports() works without crashing."""
ports = MidiTransport.list_ports()
assert "inputs" in ports
assert "outputs" in ports
assert isinstance(ports["inputs"], list)
assert isinstance(ports["outputs"], list)
print(f"PASS: list_ports — inputs={ports['inputs']}, outputs={ports['outputs']}")
def test_sysex_protocol_complete_roundtrip():
"""Full end-to-end: encode → send模拟 → receive → decode."""
# Simulate a complete conversation
commands = [
("ping", {"ts": 1}),
("set_tempo", {"tempo": 92.0}),
("select_channel", {"channel_index": 3}),
("note_on", {"channel_index": 0, "note": 60, "velocity": 100}),
("stop_all_notes", {}),
]
for cmd, params in commands:
encoded = encode_command(cmd, params)
# Verify format
assert encoded[0] == 0xF0
assert encoded[1] == SYSEX_ID
assert encoded[-1] == 0xF7
# Verify decode
decoded = decode_command(encoded)
assert decoded is not None
assert decoded["cmd"] == cmd
assert decoded["params"] == params
print("PASS: complete command roundtrip")
if __name__ == "__main__":
print("FL Studio MCP — Integration Tests")
print("==================================\n")
test_sysex_id()
test_nibble_encode_decode_roundtrip()
test_encode_command_format()
test_decode_command_invalid()
test_miditransport_list_ports()
test_sysex_protocol_complete_roundtrip()
print("\nAll tests passed!")

View File

@@ -0,0 +1,199 @@
"""
Phase 0: SysEx Loopback Validation Test.
Tests whether MIDI SysEx messages can travel through the Windows MIDI Services
loopback port "FL_MCP" from the MCP server to the FL Studio controller script.
Usage:
python test_sysex_loopback.py
Requires:
- FL Studio running with FL_MCP controller script loaded
- Windows MIDI Services loopback ports "FL_MCP 0" (input) and "FL_MCP 1" (output)
"""
from __future__ import annotations
import sys
import time
try:
import mido
except ImportError:
print("ERROR: mido not installed. Run: pip install mido")
sys.exit(1)
def send_sysex_raw(output, data):
"""Send raw SysEx bytes via mido."""
msg = mido.Message("sysex", data=bytes(data))
output.send(msg)
def test_simple_sysex():
"""Test 1: Send F0 7D 48 69 F7 ("Hi") and expect it back."""
print("\n=== Test 1: Simple SysEx Echo ===")
output_name = None
input_name = None
for name in mido.get_output_names():
if "FL_MCP" in name and "1" in name:
output_name = name
break
for name in mido.get_input_names():
if "FL_MCP" in name and "0" in name:
input_name = name
break
if not output_name:
print("FAIL: FL_MCP output port (FL_MCP 1) not found")
print("Available outputs:", mido.get_output_names())
return False
if not input_name:
print("FAIL: FL_MCP input port (FL_MCP 0) not found")
print("Available inputs:", mido.get_input_names())
return False
print(f"Output: {output_name}")
print(f"Input: {input_name}")
try:
output = mido.open_output(output_name)
input_port = mido.open_input(input_name)
except Exception as e:
print(f"FAIL: Cannot open ports: {e}")
return False
# Send "Hi" ping: F0 7D 48 69 F7
# (0x48='H', 0x69='i')
ping_data = [0xF0, 0x7D, 0x48, 0x69, 0xF7]
print(f"Sending: {' '.join(f'{b:02X}' for b in ping_data)}")
send_sysex_raw(output, ping_data[1:-1]) # mido adds F0/F7
print("Waiting 5s for echo...")
timeout = 5.0
start = time.time()
received = None
while time.time() - start < timeout:
msg = input_port.receive(timeout=0.1)
if msg and msg.type == "sysex":
received = list(msg.bytes())
break
output.close()
input_port.close()
if received:
print(f"Received: {' '.join(f'{b:02X}' for b in received)}")
if received[0] == 0xF0 and received[-1] == 0xF7 and received[1] == 0x7D:
print("PASS: Simple SysEx loopback works!")
return True
print(f"UNKNOWN: Received but unexpected bytes: {received}")
return False
else:
print("FAIL: No message received within timeout")
return False
def test_json_command():
"""Test 2: Send a proper JSON command via nibble encoding."""
print("\n=== Test 2: JSON Command SysEx ===")
import json
output_name = None
input_name = None
for name in mido.get_output_names():
if "FL_MCP" in name and "1" in name:
output_name = name
break
for name in mido.get_input_names():
if "FL_MCP" in name and "0" in name:
input_name = name
break
if not output_name or not input_name:
print("SKIP: FL_MCP ports not available for JSON test")
return False
try:
output = mido.open_output(output_name)
input_port = mido.open_input(input_name)
except Exception as e:
print(f"SKIP: Cannot open ports: {e}")
return False
# Encode ping command: {"cmd":"ping","params":{"ts":1}}
payload = json.dumps({"cmd": "ping", "params": {"ts": 1}}).encode("utf-8")
print(f"JSON payload: {payload}")
# Nibble encode
nibbles = []
for byte in payload:
nibbles.append((byte >> 4) & 0x0F)
nibbles.append(byte & 0x0F)
sysex_data = [0xF0, 0x7D] + nibbles + [0xF7]
print(f"SysEx: {' '.join(f'{b:02X}' for b in sysex_data)}")
send_sysex_raw(output, sysex_data[1:-1])
print("Waiting 5s for response...")
timeout = 5.0
start = time.time()
received = None
while time.time() - start < timeout:
msg = input_port.receive(timeout=0.1)
if msg and msg.type == "sysex":
received = list(msg.bytes())
break
output.close()
input_port.close()
if received:
print(f"Received: {' '.join(f'{b:02X}' for b in received)}")
# Decode nibbles
if received[1] == 0x7D:
nibble_data = received[2:-1]
result = bytearray()
for i in range(0, len(nibble_data), 2):
if i + 1 < len(nibble_data):
result.append(((nibble_data[i] & 0x0F) << 4) | (nibble_data[i + 1] & 0x0F))
decoded = result.decode("utf-8")
print(f"Decoded JSON: {decoded}")
print("PASS: JSON command roundtrip works!")
return True
else:
print("FAIL: No response within timeout")
return False
def list_ports():
"""List all available MIDI ports."""
print("\n=== Available MIDI Ports ===")
print("Inputs:")
for p in mido.get_input_names():
print(f" {p}")
print("Outputs:")
for p in mido.get_output_names():
print(f" {p}")
if __name__ == "__main__":
print("FL Studio MCP — SysEx Loopback Validation")
print("=========================================")
list_ports()
test1 = test_simple_sysex()
test2 = test_json_command()
print("\n=== Summary ===")
print(f"Simple SysEx: {'PASS' if test1 else 'FAIL'}")
print(f"JSON Command: {'PASS' if test2 else 'FAIL'}")
if test1 and test2:
print("\nSysEx loopback is functional. Phase 1-4 can proceed.")
sys.exit(0)
else:
print("\nSysEx loopback FAILED. Phase 1-4 ABORTED — pivot required.")
sys.exit(1)