From dfdc0497a083cedca5b9d599258b12dec5274463 Mon Sep 17 00:00:00 2001 From: Michael Winter Date: Sat, 28 Mar 2026 17:13:51 +0100 Subject: [PATCH] Add independent voice control to OSC sender with improved display --- src/osc_sender.py | 219 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 165 insertions(+), 54 deletions(-) diff --git a/src/osc_sender.py b/src/osc_sender.py index 214245d..c022efb 100644 --- a/src/osc_sender.py +++ b/src/osc_sender.py @@ -26,6 +26,8 @@ class OSCSender: self.chords = [] self.current_index = 0 self.preview_mode = True + self.num_voices = 0 + self.voice_indices = [] # Track position for each voice independently def load_chords(self, chords_file): """Load chords from output_chords.json.""" @@ -38,7 +40,15 @@ class OSCSender: else: self.chords = chords_data - print(f"Loaded {len(self.chords)} chords") + # Initialize voice indices to 0 for all voices + if self.chords: + self.num_voices = len(self.chords[0]) + self.voice_indices = [0] * self.num_voices + else: + self.num_voices = 0 + self.voice_indices = [] + + print(f"Loaded {len(self.chords)} chords, {self.num_voices} voices") def send_chord(self, index): """Send frequencies for a specific chord index.""" @@ -59,9 +69,33 @@ class OSCSender: self.client.send(msg.build()) print(f" [Sent chord {index + 1}/{len(self.chords)}]") + def send_voice(self, voice_idx): + """Send frequency for a single voice at its current position.""" + if voice_idx < 0 or voice_idx >= len(self.voice_indices): + return + + chord_idx = self.voice_indices[voice_idx] + if chord_idx < 0 or chord_idx >= len(self.chords): + return + + chord = self.chords[chord_idx] + pitch = chord[voice_idx] + + from fractions import Fraction + + frac = Fraction(pitch["fraction"]) + freq = self.fundamental * float(frac) + + msg = osc_message_builder.OscMessageBuilder(address="/freq") + msg.add_arg(voice_idx + 1) # Voice number (1-indexed) + msg.add_arg(freq) + self.client.send(msg.build()) + def send_current(self): - """Send the current chord.""" - self.send_chord(self.current_index) + """Send all voices at their current positions.""" + for voice_idx in range(self.num_voices): + self.send_voice(voice_idx) + print(f" [Sent all {self.num_voices} voices at their positions]") def _get_chord_preview(self, index): """Get a one-line preview of a chord.""" @@ -82,48 +116,76 @@ class OSCSender: return self.chords[index] def display_chords(self): - """Display previous, current, and next chords.""" + """Display previous, current, and next chords with per-voice positions.""" 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): + # Build prev/next info for each voice independently + prev_freqs = [] + prev_positions = [] + next_freqs = [] + next_positions = [] + current_freqs = [] + + for voice_idx in range(self.num_voices): + curr_idx = self.voice_indices[voice_idx] + + # Previous for this voice + prev_idx = max(0, curr_idx - 1) + chord = self.chords[prev_idx] + pitch = chord[voice_idx] frac = Fraction(pitch["fraction"]) freq = self.fundamental * float(frac) - print(f" Voice {voice_idx + 1}: {freq:.2f}") + prev_freqs.append(f"{freq:.2f}") + prev_positions.append(prev_idx + 1) - print("\nHS Arrays:") - for voice_idx, pitch in enumerate(chord): - print(f" Voice {voice_idx + 1}: {pitch['hs_array']}") + # Current for this voice + chord = self.chords[curr_idx] + pitch = chord[voice_idx] + frac = Fraction(pitch["fraction"]) + freq = self.fundamental * float(frac) + current_freqs.append((freq, pitch["hs_array"])) + + # Next for this voice + next_idx = min(len(self.chords) - 1, curr_idx + 1) + chord = self.chords[next_idx] + pitch = chord[voice_idx] + frac = Fraction(pitch["fraction"]) + freq = self.fundamental * float(frac) + next_freqs.append(f"{freq:.2f}") + next_positions.append(next_idx + 1) + + # Get current positions for display + curr_positions = [v + 1 for v in self.voice_indices] + + print("\n" + "=" * 60) + + # PREVIOUS line + print(f"PREVIOUS ({prev_positions}/{total})") + print(f"{', '.join(prev_freqs)} Hz") print("-" * 60) + + # CURRENT line with positions + print(f"CURRENT ({curr_positions}/{total})") + for voice_idx in range(self.num_voices): + freq, hs = current_freqs[voice_idx] + print(f" Voice {voice_idx + 1}: {freq:.2f} Hz {hs}") + + print("-" * 60) + + # NEXT line + print(f"Next ({next_positions}/{total})") + print(f"{', '.join(next_freqs)} Hz") + + print("-" * 60) + + mode_str = "[PREVIEW]" if self.preview_mode else "[SEND]" 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}" + f"Fundamental: {self.fundamental} Hz | Destination: {self.ip}:{self.port} {mode_str}" ) print( - "Controls: <-|-> navigate | p=preview | k/K=kill | Enter=send | [n]=jump | Esc=quit" + "Controls: <-|-> all | [n]<-/->=voice n | p=preview | k/K=kill | Enter=send all | Esc=quit" ) def play(self): @@ -167,52 +229,101 @@ class OSCSender: break elif key == "\x1b[D": - if self.current_index > 0: - self.current_index -= 1 - self.display_chords() - if not self.preview_mode: - self.send_current() + # Back arrow + if num_buffer: + # Individual voice: [n]<- move voice n back + try: + voice_num = int(num_buffer) - 1 + if 0 <= voice_num < self.num_voices: + if self.voice_indices[voice_num] > 0: + self.voice_indices[voice_num] -= 1 + self.display_chords() + if not self.preview_mode: + self.send_voice(voice_num) + else: + print(f"\n Voice {voice_num + 1} at start") + else: + print(f"\n Invalid voice: {num_buffer}") + except ValueError: + pass + num_buffer = "" + else: + # All voices: move all voice_indices back + can_move = any(vi > 0 for vi in self.voice_indices) + if can_move: + for voice_idx in range(self.num_voices): + if self.voice_indices[voice_idx] > 0: + self.voice_indices[voice_idx] -= 1 + 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.display_chords() - if not self.preview_mode: - self.send_current() + # Forward arrow + if num_buffer: + # Individual voice: [n]-> move voice n forward + try: + voice_num = int(num_buffer) - 1 + if 0 <= voice_num < self.num_voices: + if self.voice_indices[voice_num] < len(self.chords) - 1: + self.voice_indices[voice_num] += 1 + self.display_chords() + if not self.preview_mode: + self.send_voice(voice_num) + else: + print(f"\n Voice {voice_num + 1} at end") + else: + print(f"\n Invalid voice: {num_buffer}") + except ValueError: + pass + num_buffer = "" + else: + # All voices: move all voice_indices forward + can_move = any( + vi < len(self.chords) - 1 for vi in self.voice_indices + ) + if can_move: + for voice_idx in range(self.num_voices): + if self.voice_indices[voice_idx] < len(self.chords) - 1: + self.voice_indices[voice_idx] += 1 + 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): + for voice_idx in range(self.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]") + print(f" [Sent kill soft (15.0) to {self.num_voices} voices]") elif key == "K": - num_voices = len(self.chords[self.current_index]) - for voice_idx in range(num_voices): + for voice_idx in range(self.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]") + print(f" [Sent kill hard (0.0) to {self.num_voices} voices]") elif key.isdigit(): num_buffer += key - print(f"\rJump to: {num_buffer}", end="", flush=True) + print(f"\rVoice: {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 + # Set all voices to this chord index + self.voice_indices = [idx] * self.num_voices self.display_chords() + if not self.preview_mode: + self.send_current() else: print(f"\nIndex must be 1-{len(self.chords)}") except ValueError: @@ -225,9 +336,9 @@ class OSCSender: if num_buffer: num_buffer = num_buffer[:-1] if num_buffer: - print(f"\rJump to: {num_buffer}", end="", flush=True) + print(f"\rVoice: {num_buffer}", end="", flush=True) else: - print("\r ", end="", flush=True) + print("\r ", end="", flush=True) print("\r", end="", flush=True) except KeyboardInterrupt: