#!/usr/bin/env python3 """Standalone MIDI test tool — loopback test, clock monitor, and CC debug. Tests MIDI UART and/or USB ports without requiring the full pedal application. Works headless (no display needed). Usage: # Default (UART on /dev/ttyAMA0) python scripts/midi_test.py # Custom UART port python scripts/midi_test.py --uart /dev/ttyUSB0 # USB-MIDI only python scripts/midi_test.py --no-uart --usb # Display clock BPM in real-time python scripts/midi_test.py --clock-monitor # Check all available MIDI ports (discovery mode) python scripts/midi_test.py --discover """ from __future__ import annotations import argparse import logging import sys import time from typing import NoReturn # ── Add src to path ───────────────────────────────────────────────── from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) from midi.handler import ( MIDIHandler, UARTMIDI, USBMIDI, CC_EXPRESSION, CC_VOLUME, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) logger = logging.getLogger("midi-test") def discover_ports() -> None: """List available MIDI ports without opening them.""" print("=" * 50) print("MIDI Port Discovery") print("=" * 50) # USBMIDI port listing try: import rtmidi # type: ignore[import-untyped] midi_in = rtmidi.MidiIn() midi_out = rtmidi.MidiOut() midi_in.ignore_types(timing=True, sysex=False) ports_in = midi_in.get_ports() ports_out = midi_out.get_ports() print(f"\nUSB-MIDI Input Ports ({len(ports_in)}):") for i, p in enumerate(ports_in): print(f" {i}: {p}") print(f"\nUSB-MIDI Output Ports ({len(ports_out)}):") for i, p in enumerate(ports_out): print(f" {i}: {p}") midi_in.close_port() except ImportError: print("\npython-rtmidi not installed — cannot scan USB ports") # UART port check print("\nUART MIDI Ports:") for port in ["/dev/ttyAMA0", "/dev/ttyUSB0", "/dev/ttyS0"]: p = Path(port) if p.exists(): print(f" {port} — EXISTS") else: print(f" {port} — not found") print() print("On Raspberry Pi, also check:") print(" /dev/serial0 (symlink to active UART)") print(" dmesg | grep tty") print(" dtoverlay=disable-bt (if using UART for MIDI)") print(" core_freq=250 (for stable UART baud rate)") def clock_monitor(handler: MIDIHandler) -> None: """Monitor MIDI clock BPM in real-time.""" def on_bpm(bpm: float) -> None: logger.info("MIDI Clock BPM: %.1f", bpm) handler.set_clock_callback(on_bpm) logger.info("Clock monitor active — send MIDI clock to see BPM") def dispatch_example(handler: MIDIHandler) -> None: """Subscribe to all MIDI events and print them.""" def on_pc(channel: int, program: int) -> None: logger.info(" PC: ch=%d → program=%d", channel, program) def on_cc(cc_number: int, value: int, channel: int) -> None: logger.info(" CC: ch=%d, cc=%d → %d", channel, cc_number, value) def on_note(note: int, velocity: int, channel: int) -> None: logger.info(" Note: ch=%d, note=%d, vel=%d (on=%s)", channel, note, velocity, velocity > 0) def on_learn(mapping: object) -> None: logger.info(" MIDI Learned: %s", mapping) handler.set_pc_callback(on_pc) handler.register_cc(CC_EXPRESSION, lambda v, c: on_cc(CC_EXPRESSION, v, c)) handler.register_cc(CC_VOLUME, lambda v, c: on_cc(CC_VOLUME, v, c)) handler.set_note_callback(on_note) handler.set_midi_learn_callback(on_learn) def send_test_messages(handler: MIDIHandler) -> None: """Send test MIDI messages out all open ports.""" logger.info("Sending test MIDI messages...") handler.send(MIDIHandler.send_note_on(60, velocity=100)) time.sleep(0.1) handler.send(MIDIHandler.send_note_off(60)) time.sleep(0.1) handler.send(MIDIHandler.send_cc(7, 100)) time.sleep(0.1) handler.send(MIDIHandler.send_pc(1)) time.sleep(0.1) handler.send(MIDIHandler.send_pitch_bend(8192)) logger.info("Test messages sent (5 messages)") def main() -> None: parser = argparse.ArgumentParser( description="MIDI test tool for Pi Multi-FX Pedal", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument("--uart", default="/dev/ttyAMA0", help="UART device path (default: /dev/ttyAMA0)") parser.add_argument("--no-uart", action="store_true", help="Skip UART MIDI") parser.add_argument("--no-usb", action="store_true", help="Skip USB MIDI") parser.add_argument("--usb-port", default="", help="Filter for USB-MIDI port name") parser.add_argument("--clock-monitor", action="store_true", help="Display MIDI clock BPM in real-time") parser.add_argument("--discover", action="store_true", help="Discover available MIDI ports and exit") parser.add_argument("--send-test", action="store_true", help="Send test MIDI messages after startup") parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging") args = parser.parse_args() if args.verbose: logging.getLogger("midi").setLevel(logging.DEBUG) if args.discover: discover_ports() return handler = MIDIHandler() uart_port = None if args.no_uart else args.uart usb_enabled = not args.no_usb print("=" * 50) print("Pi Multi-FX Pedal — MIDI Test Tool") print("=" * 50) print(f" UART MIDI: {uart_port or 'disabled'}") print(f" USB MIDI: {'enabled' if usb_enabled else 'disabled'}") print(f" Clock mon: {'yes' if args.clock_monitor else 'no'}") print(f" Send test: {'yes' if args.send_test else 'no'}") print("=" * 50) if args.clock_monitor: clock_monitor(handler) dispatch_example(handler) print("\nStarting MIDI handler...") handler.start(uart_port=uart_port, usb=usb_enabled, usb_port_name=args.usb_port) print(f"\nActive interfaces: {handler.interface_names or 'none (software-only)'}") if args.send_test: send_test_messages(handler) print("\nListening for MIDI messages...") print("Press Ctrl+C to stop.\n") try: while True: time.sleep(0.1) except KeyboardInterrupt: print("\nShutting down...") finally: handler.stop() print("MIDI handler stopped.") if __name__ == "__main__": main()