Add ramp exponent UI and split ramp_to_pitch for single voice
This commit is contained in:
parent
f6a98bf8a7
commit
30675756c9
|
|
@ -124,13 +124,16 @@ class OSCSender:
|
||||||
f" [Sent kill {'soft' if soft else 'hard'} ({kill_freq}) to {self.num_voices} voices]"
|
f" [Sent kill {'soft' if soft else 'hard'} ({kill_freq}) to {self.num_voices} voices]"
|
||||||
)
|
)
|
||||||
|
|
||||||
def ramp_to_chord(self, frequencies, duration_ms=3000, start_freq=20):
|
def ramp_to_chord(
|
||||||
"""Ramp each voice from start_freq to target frequency(s) linearly.
|
self, frequencies, duration_ms=3000, start_freq=None, exponent=1.0
|
||||||
|
):
|
||||||
|
"""Ramp each voice from start_freq to target frequency(s).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frequencies: single frequency (int/float) or list of frequencies
|
frequencies: single frequency (int/float) or list of frequencies
|
||||||
duration_ms: duration in milliseconds
|
duration_ms: duration in milliseconds
|
||||||
start_freq: starting frequency (single or per-voice list)
|
start_freq: starting frequency (single or per-voice list, or None to use current)
|
||||||
|
exponent: exponent for curve (1.0 = linear, 2.0 = quadratic ease, etc.)
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -172,7 +175,9 @@ class OSCSender:
|
||||||
print(f" [Ramp cancelled for voice {voice_idx + 1}]")
|
print(f" [Ramp cancelled for voice {voice_idx + 1}]")
|
||||||
break
|
break
|
||||||
progress = step / num_steps
|
progress = step / num_steps
|
||||||
current_freq = voice_start + (target_freq - voice_start) * progress
|
current_freq = voice_start + (target_freq - voice_start) * (
|
||||||
|
progress**exponent
|
||||||
|
)
|
||||||
self.send_single(current_freq, voice_idx + 1)
|
self.send_single(current_freq, voice_idx + 1)
|
||||||
time.sleep(step_interval)
|
time.sleep(step_interval)
|
||||||
else:
|
else:
|
||||||
|
|
@ -191,6 +196,65 @@ class OSCSender:
|
||||||
|
|
||||||
print(f" [Started ramp to {len(frequencies)} voices over {duration_ms}ms]")
|
print(f" [Started ramp to {len(frequencies)} voices over {duration_ms}ms]")
|
||||||
|
|
||||||
|
def ramp_to_pitch(
|
||||||
|
self, frequency, voice, duration_ms=3000, start_freq=None, exponent=1.0
|
||||||
|
):
|
||||||
|
"""Ramp a single voice from start_freq to target frequency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frequency: target frequency (float)
|
||||||
|
voice: voice number (1-indexed, e.g., 1-4)
|
||||||
|
duration_ms: duration in milliseconds
|
||||||
|
start_freq: starting frequency (or None to use current)
|
||||||
|
exponent: exponent for curve (1.0 = linear, 2.0 = quadratic ease, etc.)
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
voice_idx = voice - 1 # Convert to 0-indexed
|
||||||
|
|
||||||
|
# Get start frequency from sender if not provided
|
||||||
|
if start_freq is None:
|
||||||
|
start_freq = self.get_current_frequency(voice_idx)
|
||||||
|
|
||||||
|
# Cancel any existing ramp for this voice
|
||||||
|
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}]")
|
||||||
|
|
||||||
|
# Create new cancel event
|
||||||
|
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():
|
||||||
|
cancel_event = self._ramp_events[voice_idx]
|
||||||
|
cancel_event.clear() # Reset cancellation flag
|
||||||
|
for step in range(num_steps + 1):
|
||||||
|
if cancel_event.is_set():
|
||||||
|
print(f" [Ramp cancelled for voice {voice}]")
|
||||||
|
break
|
||||||
|
progress = step / num_steps
|
||||||
|
current_freq = start_freq + (frequency - start_freq) * (
|
||||||
|
progress**exponent
|
||||||
|
)
|
||||||
|
self.send_single(current_freq, voice)
|
||||||
|
time.sleep(step_interval)
|
||||||
|
else:
|
||||||
|
# Send final exact target frequency (only if not cancelled)
|
||||||
|
self.send_single(frequency, voice)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_ramp)
|
||||||
|
thread.daemon = True
|
||||||
|
self._ramp_threads[voice_idx] = thread
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
print(f" [Started ramp for voice {voice} over {duration_ms}ms]")
|
||||||
|
|
||||||
def send_current(self):
|
def send_current(self):
|
||||||
"""Send all voices at their current positions."""
|
"""Send all voices at their current positions."""
|
||||||
for voice_idx in range(self.num_voices):
|
for voice_idx in range(self.num_voices):
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,8 @@
|
||||||
<input type="number" id="octaveInput" value="2" style="width: 40px;">
|
<input type="number" id="octaveInput" value="2" style="width: 40px;">
|
||||||
<span style="margin-left: 15px;">Ramp (s):</span>
|
<span style="margin-left: 15px;">Ramp (s):</span>
|
||||||
<input type="number" id="rampDuration" value="3" step="0.1" style="width: 60px;">
|
<input type="number" id="rampDuration" value="3" step="0.1" style="width: 60px;">
|
||||||
|
<span style="margin-left: 10px;">Exponent:</span>
|
||||||
|
<input type="number" id="rampExponent" value="1" min="0.1" max="5" step="0.1" style="width: 50px;">
|
||||||
<span style="margin-left: 15px;">Siren IP:</span>
|
<span style="margin-left: 15px;">Siren IP:</span>
|
||||||
<input type="text" id="sirenIp" value="192.168.4.200" style="width: 100px;">
|
<input type="text" id="sirenIp" value="192.168.4.200" style="width: 100px;">
|
||||||
<button id="toggleUnitBtn" style="margin-left: 15px;">Show: Cents</button>
|
<button id="toggleUnitBtn" style="margin-left: 15px;">Show: Cents</button>
|
||||||
|
|
@ -369,7 +371,7 @@
|
||||||
function rampToChord(chordIdx, nodeIdx = null) {
|
function rampToChord(chordIdx, nodeIdx = null) {
|
||||||
const durationSeconds = parseFloat(document.getElementById('rampDuration').value) || 3;
|
const durationSeconds = parseFloat(document.getElementById('rampDuration').value) || 3;
|
||||||
const durationMs = durationSeconds * 1000;
|
const durationMs = durationSeconds * 1000;
|
||||||
const startFreq = 20; // Default start frequency
|
const exponent = parseFloat(document.getElementById('rampExponent').value) || 1.0;
|
||||||
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
const sirenIp = document.getElementById('sirenIp').value || "192.168.4.200";
|
||||||
|
|
||||||
// Find nodes in this chord (exclude label nodes)
|
// Find nodes in this chord (exclude label nodes)
|
||||||
|
|
@ -401,7 +403,7 @@
|
||||||
chordIndex: chordIdx,
|
chordIndex: chordIdx,
|
||||||
nodeIndex: nodeIdx,
|
nodeIndex: nodeIdx,
|
||||||
duration: durationMs,
|
duration: durationMs,
|
||||||
startFreq: startFreq,
|
exponent: exponent,
|
||||||
ip: sirenIp
|
ip: sirenIp
|
||||||
})
|
})
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ def ramp_to_chord():
|
||||||
chord_index = data.get("chordIndex")
|
chord_index = data.get("chordIndex")
|
||||||
node_index = data.get("nodeIndex") # Optional - if present, ramp single voice
|
node_index = data.get("nodeIndex") # Optional - if present, ramp single voice
|
||||||
duration_ms = data.get("duration", 3000)
|
duration_ms = data.get("duration", 3000)
|
||||||
start_freq = data.get("startFreq", 20)
|
exponent = data.get("exponent", 1.0)
|
||||||
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
||||||
|
|
||||||
if chord_index is None:
|
if chord_index is None:
|
||||||
|
|
@ -234,16 +234,21 @@ def ramp_to_chord():
|
||||||
pitch = chord[node_index]
|
pitch = chord[node_index]
|
||||||
fraction = Fraction(pitch.get("fraction", "1"))
|
fraction = Fraction(pitch.get("fraction", "1"))
|
||||||
frequency = fundamental * float(fraction)
|
frequency = fundamental * float(fraction)
|
||||||
|
voice = node_index + 1 # 1-indexed
|
||||||
|
|
||||||
# Ramp single voice - let sender get start frequency from current position
|
# Ramp single voice - let sender get start frequency from current position
|
||||||
siren_sender.ramp_to_chord(frequency, duration_ms, start_freq=None)
|
siren_sender.ramp_to_pitch(
|
||||||
|
frequency, voice, duration_ms, start_freq=None, exponent=exponent
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Ramp entire chord - let sender get start frequencies from current positions
|
# Ramp entire chord - let sender get start frequencies from current positions
|
||||||
frequencies = [
|
frequencies = [
|
||||||
fundamental * float(Fraction(p.get("fraction", "1"))) for p in chord
|
fundamental * float(Fraction(p.get("fraction", "1"))) for p in chord
|
||||||
]
|
]
|
||||||
|
|
||||||
siren_sender.ramp_to_chord(frequencies, duration_ms, start_freq=None)
|
siren_sender.ramp_to_chord(
|
||||||
|
frequencies, duration_ms, start_freq=None, exponent=exponent
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue