- src/main.cpp: Add serial command interface (A/B/C values, s=status, 0=off) - src/main.cpp: Serial control disables MIDI timeout for persistent values - serial_control.py: Streamlit GUI with sliders and quick action buttons - serial_control.py: Filter out ttyS* ports, prioritize USB serial - test_midi.py: Update for new CC control mapping (CC 20/21/22) - README.md: Complete project documentation
269 lines
8.6 KiB
Python
Executable File
269 lines
8.6 KiB
Python
Executable File
#!/usr/bin/env -S uv run
|
|
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = [
|
|
# "mido",
|
|
# "python-rtmidi",
|
|
# ]
|
|
# ///
|
|
"""
|
|
Test MIDI receiver on Arduino with CC and Note control.
|
|
|
|
CC Mapping (Arduino pins 9, 10, 11):
|
|
- CC 20 -> PWM Pin 1 (pin 9)
|
|
- CC 21 -> PWM Pin 2 (pin 10)
|
|
- CC 22 -> PWM Pin 3 (pin 11)
|
|
|
|
Note Mapping (optional, enabled in Arduino):
|
|
- Note 60 (C4) -> PWM Pin 1
|
|
- Note 62 (D4) -> PWM Pin 2
|
|
- Note 64 (E4) -> PWM Pin 3
|
|
"""
|
|
|
|
import mido
|
|
import time
|
|
import argparse
|
|
import math
|
|
|
|
# CC numbers matching Arduino configuration
|
|
CC_PWM_1 = 20
|
|
CC_PWM_2 = 21
|
|
CC_PWM_3 = 22
|
|
|
|
# Note numbers matching Arduino configuration
|
|
NOTE_CH1 = 60 # C4
|
|
NOTE_CH2 = 62 # D4
|
|
NOTE_CH3 = 64 # E4
|
|
|
|
|
|
def find_midi_port():
|
|
"""Find a suitable MIDI output port."""
|
|
outputs = mido.get_output_names()
|
|
print("Available MIDI outputs:")
|
|
for i, name in enumerate(outputs):
|
|
print(f" {i}: {name}")
|
|
|
|
if not outputs:
|
|
return None
|
|
|
|
# Find CH345 or use first available
|
|
for name in outputs:
|
|
if "CH345" in name or "MIDI" in name:
|
|
return name
|
|
|
|
return outputs[0]
|
|
|
|
|
|
def demo_cc_sweep(output, duration=5.0):
|
|
"""Sweep all three CC channels from 0 to 127 and back."""
|
|
print("\n--- CC Sweep Demo ---")
|
|
print("Ramping all channels 0->127->0")
|
|
|
|
steps = 128
|
|
delay = duration / (steps * 2)
|
|
|
|
# Ramp up
|
|
for val in range(0, 128):
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
msg = mido.Message('control_change', control=cc, value=val, channel=0)
|
|
output.send(msg)
|
|
if val % 16 == 0:
|
|
print(f" CC value: {val}")
|
|
time.sleep(delay)
|
|
|
|
# Ramp down
|
|
for val in range(127, -1, -1):
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
msg = mido.Message('control_change', control=cc, value=val, channel=0)
|
|
output.send(msg)
|
|
if val % 16 == 0:
|
|
print(f" CC value: {val}")
|
|
time.sleep(delay)
|
|
|
|
print("Sweep complete.\n")
|
|
|
|
|
|
def demo_cc_individual(output):
|
|
"""Test each CC channel individually."""
|
|
print("\n--- Individual CC Channel Test ---")
|
|
|
|
channels = [
|
|
(CC_PWM_1, "PWM Pin 1 (pin 9)"),
|
|
(CC_PWM_2, "PWM Pin 2 (pin 10)"),
|
|
(CC_PWM_3, "PWM Pin 3 (pin 11)"),
|
|
]
|
|
|
|
for cc, name in channels:
|
|
print(f"Testing CC {cc} - {name}")
|
|
|
|
# Turn on
|
|
msg = mido.Message('control_change', control=cc, value=127, channel=0)
|
|
output.send(msg)
|
|
print(f" CC {cc} = 127")
|
|
time.sleep(1.0)
|
|
|
|
# Turn off
|
|
msg = mido.Message('control_change', control=cc, value=0, channel=0)
|
|
output.send(msg)
|
|
print(f" CC {cc} = 0")
|
|
time.sleep(0.5)
|
|
|
|
print("Individual test complete.\n")
|
|
|
|
|
|
def demo_cc_wave(output, duration=10.0):
|
|
"""Create a sine wave pattern on all channels with phase offset."""
|
|
print("\n--- CC Sine Wave Demo ---")
|
|
print(f"Running for {duration} seconds (Ctrl+C to stop early)")
|
|
|
|
start = time.time()
|
|
freq = 0.5 # Hz
|
|
|
|
try:
|
|
while time.time() - start < duration:
|
|
t = time.time() - start
|
|
|
|
# Phase-shifted sine waves for each channel
|
|
val1 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t))
|
|
val2 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t + 2 * math.pi / 3))
|
|
val3 = int(63.5 + 63.5 * math.sin(2 * math.pi * freq * t + 4 * math.pi / 3))
|
|
|
|
output.send(mido.Message('control_change', control=CC_PWM_1, value=val1, channel=0))
|
|
output.send(mido.Message('control_change', control=CC_PWM_2, value=val2, channel=0))
|
|
output.send(mido.Message('control_change', control=CC_PWM_3, value=val3, channel=0))
|
|
|
|
time.sleep(0.02)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
# Turn off all
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
|
|
|
|
print("Wave demo complete.\n")
|
|
|
|
|
|
def demo_notes(output):
|
|
"""Test note-based control (legacy mode)."""
|
|
print("\n--- Note Control Demo ---")
|
|
print("Testing notes: C4 (60), D4 (62), E4 (64)")
|
|
|
|
notes = [
|
|
(NOTE_CH1, "C4 - PWM Pin 1"),
|
|
(NOTE_CH2, "D4 - PWM Pin 2"),
|
|
(NOTE_CH3, "E4 - PWM Pin 3"),
|
|
]
|
|
|
|
for note, name in notes:
|
|
print(f" Note ON: {name} vel=100")
|
|
output.send(mido.Message('note_on', note=note, velocity=100, channel=0))
|
|
time.sleep(0.5)
|
|
|
|
print(f" Note OFF: {name}")
|
|
output.send(mido.Message('note_off', note=note, velocity=0, channel=0))
|
|
time.sleep(0.3)
|
|
|
|
print("Note demo complete.\n")
|
|
|
|
|
|
def interactive_mode(output):
|
|
"""Interactive CC control mode."""
|
|
print("\n--- Interactive Mode ---")
|
|
print("Commands:")
|
|
print(" 1/2/3 <value> - Set CC channel 1/2/3 to value (0-127)")
|
|
print(" all <value> - Set all channels to value")
|
|
print(" sweep - Run sweep demo")
|
|
print(" wave - Run wave demo")
|
|
print(" off - Turn all channels off")
|
|
print(" q/quit - Exit")
|
|
print()
|
|
|
|
try:
|
|
while True:
|
|
cmd = input("> ").strip().lower()
|
|
|
|
if cmd in ('q', 'quit', 'exit'):
|
|
break
|
|
elif cmd == 'off':
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
|
|
print("All channels off")
|
|
elif cmd == 'sweep':
|
|
demo_cc_sweep(output)
|
|
elif cmd == 'wave':
|
|
demo_cc_wave(output)
|
|
elif cmd.startswith('all '):
|
|
try:
|
|
val = int(cmd.split()[1])
|
|
val = max(0, min(127, val))
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
output.send(mido.Message('control_change', control=cc, value=val, channel=0))
|
|
print(f"All channels set to {val}")
|
|
except (ValueError, IndexError):
|
|
print("Usage: all <value>")
|
|
elif cmd[0] in '123' and ' ' in cmd:
|
|
try:
|
|
ch = int(cmd[0])
|
|
val = int(cmd.split()[1])
|
|
val = max(0, min(127, val))
|
|
cc = [CC_PWM_1, CC_PWM_2, CC_PWM_3][ch - 1]
|
|
output.send(mido.Message('control_change', control=cc, value=val, channel=0))
|
|
print(f"Channel {ch} (CC {cc}) set to {val}")
|
|
except (ValueError, IndexError):
|
|
print("Usage: 1/2/3 <value>")
|
|
elif cmd:
|
|
print("Unknown command. Type 'q' to quit.")
|
|
except (KeyboardInterrupt, EOFError):
|
|
pass
|
|
|
|
# Turn off on exit
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
|
|
print("\nExiting interactive mode.")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Test MIDI CC control for Arduino PWM")
|
|
parser.add_argument('--mode', '-m', choices=['sweep', 'individual', 'wave', 'notes', 'interactive', 'all'],
|
|
default='all', help='Demo mode to run (default: all)')
|
|
parser.add_argument('--duration', '-d', type=float, default=5.0,
|
|
help='Duration for sweep/wave demos (default: 5.0)')
|
|
args = parser.parse_args()
|
|
|
|
port_name = find_midi_port()
|
|
|
|
if port_name is None:
|
|
print("No MIDI output ports found!")
|
|
return
|
|
|
|
print(f"\nUsing: {port_name}")
|
|
print("Press Ctrl+C to stop\n")
|
|
|
|
with mido.open_output(port_name) as output:
|
|
try:
|
|
if args.mode == 'interactive':
|
|
interactive_mode(output)
|
|
elif args.mode == 'sweep':
|
|
demo_cc_sweep(output, args.duration)
|
|
elif args.mode == 'individual':
|
|
demo_cc_individual(output)
|
|
elif args.mode == 'wave':
|
|
demo_cc_wave(output, args.duration)
|
|
elif args.mode == 'notes':
|
|
demo_notes(output)
|
|
else: # all
|
|
demo_cc_individual(output)
|
|
demo_cc_sweep(output, args.duration)
|
|
demo_notes(output)
|
|
print("All demos complete!")
|
|
|
|
except KeyboardInterrupt:
|
|
# Ensure all outputs are off
|
|
for cc in [CC_PWM_1, CC_PWM_2, CC_PWM_3]:
|
|
output.send(mido.Message('control_change', control=cc, value=0, channel=0))
|
|
print("\nStopped. All channels turned off.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|