diff --git a/src/osc_sender.py b/src/osc_sender.py index b1c67ce..5b7676a 100644 --- a/src/osc_sender.py +++ b/src/osc_sender.py @@ -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 diff --git a/webapp/path_navigator.html b/webapp/path_navigator.html index 10555a6..f67f6dc 100644 --- a/webapp/path_navigator.html +++ b/webapp/path_navigator.html @@ -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); + }); } }); diff --git a/webapp/server.py b/webapp/server.py index 6cf6ee8..b42229d 100644 --- a/webapp/server.py +++ b/webapp/server.py @@ -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()}")