Add Shift+click for siren and k/K for kill

- Add /api/play-siren endpoint for Shift+click
- Add /api/kill-siren endpoint for k/K keys
- Add set_chords method to OSCSender for dynamic voice count
- Update soft kill to 20Hz in osc_sender.py
- Refactor CLI to use send_kill method
- Fix kill to send to all voices regardless of count
This commit is contained in:
Michael Winter 2026-04-01 13:01:50 +02:00
parent 6ab162003e
commit 94b34b4dc4
3 changed files with 107 additions and 16 deletions

View file

@ -26,7 +26,7 @@ class OSCSender:
self.chords = []
self.current_index = 0
self.preview_mode = True
self.num_voices = 0
self.num_voices = 4 # Default to 4 voices
self.voice_indices = [] # Track position for each voice independently
def load_chords(self, chords_file):
@ -50,6 +50,13 @@ class OSCSender:
print(f"Loaded {len(self.chords)} chords, {self.num_voices} voices")
def set_chords(self, chords):
"""Set chords directly (list of chord pitch arrays)."""
self.chords = chords
if self.chords:
self.num_voices = len(self.chords[0])
self.voice_indices = [0] * self.num_voices
def send_chord(self, index):
"""Send frequencies for a specific chord index."""
if index < 0 or index >= len(self.chords):
@ -98,6 +105,15 @@ class OSCSender:
msg.add_arg(frequency)
self.client.send(msg.build())
def send_kill(self, soft=True):
"""Send kill to all voices (soft=20Hz, hard=0Hz)."""
kill_freq = 20.0 if soft else 0.0
for voice in range(1, self.num_voices + 1):
self.send_single(kill_freq, voice)
print(
f" [Sent kill {'soft' if soft else 'hard'} ({kill_freq}) to {self.num_voices} voices]"
)
def send_current(self):
"""Send all voices at their current positions."""
for voice_idx in range(self.num_voices):
@ -302,20 +318,10 @@ class OSCSender:
self.display_chords()
elif key == "k":
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 {self.num_voices} voices]")
self.send_kill(soft=True)
elif key == "K":
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 {self.num_voices} voices]")
self.send_kill(soft=False)
elif key.isdigit():
num_buffer += key

View file

@ -433,9 +433,14 @@
const chordIndex = node.data('chordIndex');
const localId = node.data('localId');
console.log('Sending play request:', chordIndex, localId);
// Check if Shift key is held - send to siren, otherwise send to SuperCollider
const isShift = evt.originalEvent && evt.originalEvent.shiftKey;
const endpoint = isShift ? '/api/play-siren' : '/api/play-freq';
const destination = isShift ? 'siren' : 'SuperCollider';
fetch('/api/play-freq', {
console.log('Sending play request to', destination, ':', chordIndex, localId);
fetch(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
@ -443,7 +448,7 @@
nodeIndex: localId
})
}).then(r => r.json()).then(data => {
console.log('Playing:', data.frequency.toFixed(2), 'Hz on voice', data.voice);
console.log('Playing on', destination + ':', data.frequency.toFixed(2), 'Hz on voice', data.voice);
}).catch(err => {
console.log('Error playing freq:', err);
});
@ -945,6 +950,28 @@
navigate('prev');
} else if (e.key === "ArrowRight") {
navigate('next');
} else if (e.key === "k") {
// Soft kill - send 20 Hz to stop voices gently
fetch('/api/kill-siren', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ soft: true })
}).then(r => r.json()).then(data => {
console.log('Soft kill sent (20 Hz)');
}).catch(err => {
console.log('Error sending kill:', err);
});
} else if (e.key === "K") {
// Hard kill - send 0 Hz to stop voices immediately
fetch('/api/kill-siren', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ soft: false })
}).then(r => r.json()).then(data => {
console.log('Hard kill sent (0 Hz)');
}).catch(err => {
console.log('Error sending kill:', err);
});
}
});

View file

@ -327,6 +327,64 @@ def play_freq():
)
@app.route("/api/play-siren", methods=["POST"])
def play_siren():
"""Play a single frequency for a node on the siren."""
data = request.json
chord_index = data.get("chordIndex")
node_index = data.get("nodeIndex")
if chord_index < 0 or chord_index >= len(chords):
return jsonify({"error": "Invalid chord index"}), 400
chord = chords[chord_index]
if node_index < 0 or node_index >= len(chord):
return jsonify({"error": "Invalid node index"}), 400
pitch = chord[node_index]
fraction = Fraction(pitch.get("fraction", "1"))
frequency = fundamental * float(fraction)
voice = node_index + 1 # 1-indexed
# Send to siren (192.168.4.200:54001) using current fundamental
siren_sender = OSCSender(ip="192.168.4.200", port=54001, fundamental=fundamental)
siren_sender.set_chords(chords) # Set chords to ensure proper voice count
siren_sender.send_single(frequency, voice)
return jsonify(
{
"frequency": frequency,
"voice": voice,
"fundamental": fundamental,
"fraction": pitch.get("fraction", "1"),
"destination": "siren",
}
)
@app.route("/api/kill-siren", methods=["POST"])
def kill_siren():
"""Send kill message to siren (soft or hard)."""
data = request.json
soft = data.get("soft", True) # default to soft kill
kill_freq = 20.0 if soft else 0.0
kill_type = "soft" if soft else "hard"
# Send kill to all voices on siren using send_kill
siren_sender = OSCSender(ip="192.168.4.200", port=54001, fundamental=fundamental)
siren_sender.set_chords(chords) # Set chords to get correct num_voices
siren_sender.send_kill(soft=soft)
return jsonify(
{
"kill_freq": kill_freq,
"kill_type": kill_type,
"voices": list(range(1, siren_sender.num_voices + 1)),
}
)
if __name__ == "__main__":
print("Starting Path Navigator server...")
print(f"Loading chords from: {get_chords_file()}")