Add per-voice ramp cancellation and frequency tracking in osc_sender
This commit is contained in:
parent
f3a5dda4bc
commit
f6a98bf8a7
|
|
@ -16,15 +16,6 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
# Module-level flag to cancel any running ramp
|
||||
_ramp_cancelled = threading.Event()
|
||||
|
||||
|
||||
def cancel_ramp():
|
||||
"""Cancel any currently running ramp."""
|
||||
_ramp_cancelled.set()
|
||||
|
||||
|
||||
class OSCSender:
|
||||
"""Send chord frequencies via OSC."""
|
||||
|
||||
|
|
@ -38,6 +29,9 @@ class OSCSender:
|
|||
self.preview_mode = True
|
||||
self.num_voices = 4 # Default to 4 voices
|
||||
self.voice_indices = [] # Track position for each voice independently
|
||||
self._ramp_events = {} # voice_idx -> threading.Event
|
||||
self._ramp_threads = {} # voice_idx -> thread
|
||||
self._current_frequencies = {} # voice_idx -> current frequency
|
||||
|
||||
def load_chords(self, chords_file):
|
||||
"""Load chords from output_chords.json."""
|
||||
|
|
@ -114,6 +108,12 @@ class OSCSender:
|
|||
msg.add_arg(voice) # Voice number (1-indexed)
|
||||
msg.add_arg(frequency)
|
||||
self.client.send(msg.build())
|
||||
# Track current frequency for this voice
|
||||
self._current_frequencies[voice - 1] = frequency
|
||||
|
||||
def get_current_frequency(self, voice_idx):
|
||||
"""Get current frequency for a voice (0-indexed)."""
|
||||
return self._current_frequencies.get(voice_idx, 20)
|
||||
|
||||
def send_kill(self, soft=True):
|
||||
"""Send kill to all voices (soft=20Hz, hard=0Hz)."""
|
||||
|
|
@ -132,7 +132,6 @@ class OSCSender:
|
|||
duration_ms: duration in milliseconds
|
||||
start_freq: starting frequency (single or per-voice list)
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
|
||||
# Handle single frequency vs list
|
||||
|
|
@ -142,31 +141,54 @@ class OSCSender:
|
|||
# Handle single start_freq vs list (per-voice)
|
||||
if isinstance(start_freq, (int, float)):
|
||||
start_freq = [start_freq] * len(frequencies)
|
||||
elif start_freq is None:
|
||||
# Default: use current frequency from sender
|
||||
start_freq = [
|
||||
self.get_current_frequency(i) for i in range(len(frequencies))
|
||||
]
|
||||
|
||||
# Cancel any existing ramp for these voices before starting new ones
|
||||
for voice_idx in range(len(frequencies)):
|
||||
if voice_idx in self._ramp_events:
|
||||
self._ramp_events[voice_idx].set()
|
||||
if voice_idx in self._ramp_threads:
|
||||
old_thread = self._ramp_threads[voice_idx]
|
||||
if old_thread.is_alive():
|
||||
print(f" [Cancelling existing ramp for voice {voice_idx + 1}]")
|
||||
|
||||
# Create new cancel events for each voice
|
||||
for voice_idx in range(len(frequencies)):
|
||||
self._ramp_events[voice_idx] = threading.Event()
|
||||
|
||||
# Fixed interval time for smooth ramp regardless of duration
|
||||
step_interval = 0.02 # 20ms = 50 Hz update rate
|
||||
num_steps = max(1, int(duration_ms / 1000 / step_interval))
|
||||
|
||||
def run_ramp():
|
||||
_ramp_cancelled.clear() # Reset cancellation flag
|
||||
def run_ramp(voice_idx, target_freq, voice_start):
|
||||
cancel_event = self._ramp_events[voice_idx]
|
||||
cancel_event.clear() # Reset cancellation flag
|
||||
for step in range(num_steps + 1):
|
||||
if _ramp_cancelled.is_set():
|
||||
print(" [Ramp cancelled]")
|
||||
if cancel_event.is_set():
|
||||
print(f" [Ramp cancelled for voice {voice_idx + 1}]")
|
||||
break
|
||||
progress = step / num_steps
|
||||
for voice_idx, target_freq in enumerate(frequencies):
|
||||
voice_start = start_freq[voice_idx]
|
||||
current_freq = voice_start + (target_freq - voice_start) * progress
|
||||
self.send_single(current_freq, voice_idx + 1)
|
||||
time.sleep(step_interval)
|
||||
else:
|
||||
# Send final exact target frequencies (only if not cancelled)
|
||||
for voice_idx, target_freq in enumerate(frequencies):
|
||||
# Send final exact target frequency (only if not cancelled)
|
||||
self.send_single(target_freq, voice_idx + 1)
|
||||
|
||||
thread = threading.Thread(target=run_ramp)
|
||||
# Start a thread for each voice
|
||||
for voice_idx, target_freq in enumerate(frequencies):
|
||||
voice_start = start_freq[voice_idx]
|
||||
thread = threading.Thread(
|
||||
target=run_ramp, args=(voice_idx, target_freq, voice_start)
|
||||
)
|
||||
thread.daemon = True
|
||||
self._ramp_threads[voice_idx] = thread
|
||||
thread.start()
|
||||
|
||||
print(f" [Started ramp to {len(frequencies)} voices over {duration_ms}ms]")
|
||||
|
||||
def send_current(self):
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import sys
|
|||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.pitch import Pitch
|
||||
from src.osc_sender import OSCSender, cancel_ramp
|
||||
from src.osc_sender import OSCSender
|
||||
from fractions import Fraction
|
||||
|
||||
app = Flask(__name__)
|
||||
|
|
@ -27,8 +27,16 @@ DEFAULT_FILE = "output_chords.json" # default file
|
|||
current_index = 0
|
||||
chords = []
|
||||
|
||||
# Track last frequency per voice per siren IP (in-memory)
|
||||
last_frequencies = {} # {ip: {voice_index: frequency}}
|
||||
# Cache persistent OSCSenders per IP for the siren
|
||||
_sender_cache = {} # ip -> OSCSender
|
||||
|
||||
|
||||
def get_siren_sender(ip):
|
||||
"""Get or create a persistent OSCSender for the given siren IP."""
|
||||
if ip not in _sender_cache:
|
||||
_sender_cache[ip] = OSCSender(ip=ip, port=54001, fundamental=fundamental)
|
||||
return _sender_cache[ip]
|
||||
|
||||
|
||||
# OSC settings
|
||||
fundamental = 110.0
|
||||
|
|
@ -152,14 +160,11 @@ def play_siren():
|
|||
frequency = fundamental * float(fraction)
|
||||
voice = node_index + 1 # 1-indexed
|
||||
|
||||
# Send to siren using provided or default IP
|
||||
siren_sender = OSCSender(ip=siren_ip, port=54001, fundamental=fundamental)
|
||||
# Send to siren using cached sender
|
||||
siren_sender = get_siren_sender(siren_ip)
|
||||
siren_sender.set_chords(chords) # Set chords to ensure proper voice count
|
||||
siren_sender.send_single(frequency, voice)
|
||||
|
||||
# Track frequency per voice (0-indexed)
|
||||
last_frequencies.setdefault(siren_ip, {})[node_index] = frequency
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"frequency": frequency,
|
||||
|
|
@ -180,19 +185,15 @@ def kill_siren():
|
|||
kill_freq = 20.0 if soft else 0.0
|
||||
kill_type = "soft" if soft else "hard"
|
||||
|
||||
# Cancel any running ramp
|
||||
cancel_ramp()
|
||||
# Cancel any running ramp (for this IP)
|
||||
siren_sender = get_siren_sender(siren_ip)
|
||||
for event in siren_sender._ramp_events.values():
|
||||
event.set()
|
||||
|
||||
# Send kill to all voices on siren using send_kill
|
||||
siren_sender = OSCSender(ip=siren_ip, port=54001, fundamental=fundamental)
|
||||
# Send kill to all voices using cached sender
|
||||
siren_sender.set_chords(chords) # Set chords to get correct num_voices
|
||||
siren_sender.send_kill(soft=soft)
|
||||
|
||||
# Update tracking: set all voices to kill frequency (20 or 0)
|
||||
if siren_ip in last_frequencies:
|
||||
for voice_idx in last_frequencies[siren_ip]:
|
||||
last_frequencies[siren_ip][voice_idx] = kill_freq
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"kill_freq": kill_freq,
|
||||
|
|
@ -222,8 +223,8 @@ def ramp_to_chord():
|
|||
|
||||
chord = chords[chord_index]
|
||||
|
||||
# Create OSC sender with provided or default IP
|
||||
siren_sender = OSCSender(ip=siren_ip, port=54001, fundamental=fundamental)
|
||||
# Use cached sender
|
||||
siren_sender = get_siren_sender(siren_ip)
|
||||
siren_sender.set_chords(chords)
|
||||
|
||||
if node_index is not None:
|
||||
|
|
@ -234,29 +235,15 @@ def ramp_to_chord():
|
|||
fraction = Fraction(pitch.get("fraction", "1"))
|
||||
frequency = fundamental * float(fraction)
|
||||
|
||||
# Get current frequency for this voice (or default to 20)
|
||||
current_freqs = last_frequencies.get(siren_ip, {})
|
||||
voice_start_freq = current_freqs.get(node_index, 20)
|
||||
|
||||
siren_sender.ramp_to_chord(frequency, duration_ms, voice_start_freq)
|
||||
|
||||
# Update tracking with target (will be reached after ramp completes)
|
||||
last_frequencies.setdefault(siren_ip, {})[node_index] = frequency
|
||||
# Ramp single voice - let sender get start frequency from current position
|
||||
siren_sender.ramp_to_chord(frequency, duration_ms, start_freq=None)
|
||||
else:
|
||||
# Ramp entire chord
|
||||
# Ramp entire chord - let sender get start frequencies from current positions
|
||||
frequencies = [
|
||||
fundamental * float(Fraction(p.get("fraction", "1"))) for p in chord
|
||||
]
|
||||
|
||||
# Get current frequencies for each voice (or default to 20)
|
||||
current_freqs = last_frequencies.get(siren_ip, {})
|
||||
start_freqs = [current_freqs.get(i, 20) for i in range(len(frequencies))]
|
||||
|
||||
siren_sender.ramp_to_chord(frequencies, duration_ms, start_freqs)
|
||||
|
||||
# Update tracking with targets (will be reached after ramp completes)
|
||||
for i, freq in enumerate(frequencies):
|
||||
last_frequencies.setdefault(siren_ip, {})[i] = freq
|
||||
siren_sender.ramp_to_chord(frequencies, duration_ms, start_freq=None)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue