Add interactive OSC sender with preview mode and chord navigation
- Add preview mode (p key) to toggle between send/preview behavior - Arrow keys for navigation with optional send - Enter sends current chord (only when not in preview mode) - Jump to chord by number + Enter - k/K keys for kill soft (15.0) and kill hard (0.0) commands - Display prev/current/next chords with frequencies to 2 decimals - Add OSC test receiver for debugging - Use arrow key escape sequences for left/right navigation
This commit is contained in:
parent
4a1e1f7ec2
commit
8326fc1d11
|
|
@ -25,6 +25,7 @@ class OSCSender:
|
|||
self.client = SimpleUDPClient(ip, port)
|
||||
self.chords = []
|
||||
self.current_index = 0
|
||||
self.preview_mode = True
|
||||
|
||||
def load_chords(self, chords_file):
|
||||
"""Load chords from output_chords.json."""
|
||||
|
|
@ -44,39 +45,79 @@ class OSCSender:
|
|||
|
||||
frac = Fraction(pitch["fraction"])
|
||||
freq = self.fundamental * float(frac)
|
||||
addr = f"/freq"
|
||||
addr = "/freq"
|
||||
msg = osc_message_builder.OscMessageBuilder(address=addr)
|
||||
msg.add_arg(voice_idx + 1) # Voice number (1-indexed)
|
||||
msg.add_arg(freq) # Frequency
|
||||
self.client.send(msg.build())
|
||||
print(f" [Sent chord {index + 1}/{len(self.chords)}]")
|
||||
|
||||
def display_chord(self, index):
|
||||
"""Display current chord info."""
|
||||
def send_current(self):
|
||||
"""Send the current chord."""
|
||||
self.send_chord(self.current_index)
|
||||
|
||||
def _get_chord_preview(self, index):
|
||||
"""Get a one-line preview of a chord."""
|
||||
if index < 0 or index >= len(self.chords):
|
||||
return
|
||||
|
||||
return None
|
||||
chord = self.chords[index]
|
||||
from fractions import Fraction
|
||||
|
||||
print(f"\r{'=' * 50}")
|
||||
print(f"Chord {index + 1}/{len(self.chords)}")
|
||||
print(f"{'=' * 50}")
|
||||
freqs = [
|
||||
f"{self.fundamental * float(Fraction(p['fraction'])):.2f}" for p in chord
|
||||
]
|
||||
return f"Chord {index + 1}: {', '.join(freqs)} Hz"
|
||||
|
||||
def _get_chord_full(self, index):
|
||||
"""Get full details of a chord."""
|
||||
if index < 0 or index >= len(self.chords):
|
||||
return []
|
||||
return self.chords[index]
|
||||
|
||||
def display_chords(self):
|
||||
"""Display previous, current, and next chords."""
|
||||
total = len(self.chords)
|
||||
prev_idx = self.current_index - 1
|
||||
next_idx = self.current_index + 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(
|
||||
f"PREVIOUS: Chord {prev_idx + 1}/{total}"
|
||||
if prev_idx >= 0
|
||||
else "PREVIOUS: None"
|
||||
)
|
||||
if prev_idx >= 0:
|
||||
print(f" {self._get_chord_preview(prev_idx)}")
|
||||
print("-" * 60)
|
||||
mode_str = "[PREVIEW]" if self.preview_mode else "[SEND]"
|
||||
print(f">>> CURRENT: Chord {self.current_index + 1}/{total} {mode_str} <<<")
|
||||
|
||||
chord = self._get_chord_full(self.current_index)
|
||||
print("Frequencies (Hz):")
|
||||
from fractions import Fraction
|
||||
|
||||
for voice_idx, pitch in enumerate(chord):
|
||||
frac = Fraction(pitch["fraction"])
|
||||
freq = self.fundamental * float(frac)
|
||||
print(f" Voice {voice_idx + 1}: {freq:.1f}")
|
||||
print(f" Voice {voice_idx + 1}: {freq:.2f}")
|
||||
|
||||
print("\nHS Arrays:")
|
||||
for voice_idx, pitch in enumerate(chord):
|
||||
print(f" Voice {voice_idx + 1}: {pitch['hs_array']}")
|
||||
|
||||
print(f"\nFundamental: {self.fundamental} Hz")
|
||||
print(f"Destination: {self.ip}:{self.port}")
|
||||
print(f"{'=' * 50}")
|
||||
print("← Previous | Next → | Escape to quit")
|
||||
print("-" * 60)
|
||||
print(
|
||||
f"NEXT: Chord {next_idx + 1}/{total}" if next_idx < total else "NEXT: None"
|
||||
)
|
||||
if next_idx < total:
|
||||
print(f" {self._get_chord_preview(next_idx)}")
|
||||
print("=" * 60)
|
||||
print(
|
||||
f"Fundamental: {self.fundamental} Hz | Destination: {self.ip}:{self.port}"
|
||||
)
|
||||
print(
|
||||
"Controls: <-|-> navigate | p=preview | k/K=kill | Enter=send | [n]=jump | Esc=quit"
|
||||
)
|
||||
|
||||
def play(self):
|
||||
"""Interactive playback with keyboard control."""
|
||||
|
|
@ -85,49 +126,105 @@ class OSCSender:
|
|||
return
|
||||
|
||||
try:
|
||||
import threading
|
||||
import tty
|
||||
import termios
|
||||
import os
|
||||
|
||||
# Send and display first chord
|
||||
self.send_chord(self.current_index)
|
||||
self.display_chord(self.current_index)
|
||||
self.preview_mode = True
|
||||
self.current_index = 0
|
||||
self.display_chords()
|
||||
|
||||
def get_key():
|
||||
"""Get a single keypress."""
|
||||
"""Get a keypress. Returns full escape sequences for arrow keys."""
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
if ch == "\x1b":
|
||||
next1 = sys.stdin.read(1)
|
||||
if next1 == "[":
|
||||
next2 = sys.stdin.read(1)
|
||||
return "\x1b[" + next2
|
||||
else:
|
||||
return ch
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
print("\nUse arrow keys to navigate. Press Escape to quit.")
|
||||
num_buffer = ""
|
||||
|
||||
while True:
|
||||
key = get_key()
|
||||
|
||||
# Escape key - quit
|
||||
if key == "\x1b":
|
||||
break
|
||||
|
||||
# Alternative: use < and > keys
|
||||
elif key == "," or key == "<":
|
||||
elif key == "\x1b[D":
|
||||
if self.current_index > 0:
|
||||
self.current_index -= 1
|
||||
self.send_chord(self.current_index)
|
||||
self.display_chord(self.current_index)
|
||||
elif key == "." or key == ">":
|
||||
self.display_chords()
|
||||
if not self.preview_mode:
|
||||
self.send_current()
|
||||
|
||||
elif key == "\x1b[C":
|
||||
if self.current_index < len(self.chords) - 1:
|
||||
self.current_index += 1
|
||||
self.send_chord(self.current_index)
|
||||
self.display_chord(self.current_index)
|
||||
self.display_chords()
|
||||
if not self.preview_mode:
|
||||
self.send_current()
|
||||
|
||||
elif key == "p":
|
||||
self.preview_mode = not self.preview_mode
|
||||
self.display_chords()
|
||||
|
||||
elif key == "k":
|
||||
num_voices = len(self.chords[self.current_index])
|
||||
for voice_idx in range(num_voices):
|
||||
msg = osc_message_builder.OscMessageBuilder(address="/freq")
|
||||
msg.add_arg(voice_idx + 1)
|
||||
msg.add_arg(15.0)
|
||||
self.client.send(msg.build())
|
||||
print(f" [Sent kill soft (15.0) to {num_voices} voices]")
|
||||
|
||||
elif key == "K":
|
||||
num_voices = len(self.chords[self.current_index])
|
||||
for voice_idx in range(num_voices):
|
||||
msg = osc_message_builder.OscMessageBuilder(address="/freq")
|
||||
msg.add_arg(voice_idx + 1)
|
||||
msg.add_arg(0.0)
|
||||
self.client.send(msg.build())
|
||||
print(f" [Sent kill hard (0.0) to {num_voices} voices]")
|
||||
|
||||
elif key.isdigit():
|
||||
num_buffer += key
|
||||
print(f"\rJump to: {num_buffer}", end="", flush=True)
|
||||
|
||||
elif key == "\r":
|
||||
if num_buffer:
|
||||
try:
|
||||
idx = int(num_buffer) - 1
|
||||
if 0 <= idx < len(self.chords):
|
||||
self.current_index = idx
|
||||
self.display_chords()
|
||||
else:
|
||||
print(f"\nIndex must be 1-{len(self.chords)}")
|
||||
except ValueError:
|
||||
print(f"\nInvalid number: {num_buffer}")
|
||||
num_buffer = ""
|
||||
elif not self.preview_mode:
|
||||
self.send_current()
|
||||
|
||||
elif key == "\x7f" or key == "\x08":
|
||||
if num_buffer:
|
||||
num_buffer = num_buffer[:-1]
|
||||
if num_buffer:
|
||||
print(f"\rJump to: {num_buffer}", end="", flush=True)
|
||||
else:
|
||||
print("\r ", end="", flush=True)
|
||||
print("\r", end="", flush=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass # Clean exit
|
||||
pass
|
||||
|
||||
print("\n\nPlayback stopped.")
|
||||
|
||||
|
|
@ -135,7 +232,6 @@ class OSCSender:
|
|||
"""Send all chords in sequence (for testing)."""
|
||||
for i in range(len(self.chords)):
|
||||
self.send_chord(i)
|
||||
print(f"Sent chord {i + 1}/{len(self.chords)}")
|
||||
print("All chords sent!")
|
||||
|
||||
|
||||
|
|
|
|||
17
src/test_receiver.py
Normal file
17
src/test_receiver.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env python
|
||||
"""Simple OSC receiver for testing."""
|
||||
|
||||
import socket
|
||||
from pythonosc.osc_message import OscMessage
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("0.0.0.0", 54001))
|
||||
print("Listening on port 54001...")
|
||||
|
||||
while True:
|
||||
data, addr = sock.recvfrom(4096)
|
||||
msg = OscMessage(dgram=data)
|
||||
print(f"From {addr}: {msg.address}", end="")
|
||||
for p in msg.params:
|
||||
print(f" {p}", end="")
|
||||
print()
|
||||
Loading…
Reference in a new issue