feat: reggaeton production system with intelligent sample selection and FLP generation
This commit is contained in:
199
mcp/tests/test_sysex_loopback.py
Normal file
199
mcp/tests/test_sysex_loopback.py
Normal 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)
|
||||
Reference in New Issue
Block a user