#!/usr/bin/env python """ Flask server for Path Navigator with OSC support. """ from flask import Flask, jsonify, request, send_from_directory import json import os from pathlib import Path # Add parent directory to path for imports import sys sys.path.insert(0, str(Path(__file__).parent.parent)) from src.pitch import Pitch from src.osc_sender import OSCSender from fractions import Fraction app = Flask(__name__) # Path to output directory DATA_DIR = Path(__file__).parent.parent / "output" DEFAULT_FILE = "output_chords.json" # default file # State current_index = 0 chords = [] # OSC settings fundamental = 110.0 osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental) def load_chords(filepath=None): global chords, current_index if filepath is None: filepath = DATA_DIR / DEFAULT_FILE if filepath.exists(): with open(filepath) as f: data = json.load(f) chords = data.get("chords", []) current_index = 0 else: chords = [] load_chords() def parse_fraction(frac_str): """Parse a fraction string to float.""" if isinstance(frac_str, (int, float)): return float(frac_str) if "/" in frac_str: num, den = frac_str.split("/") return int(num) / int(den) return float(frac_str) def calculate_cents(fraction_str): """Calculate cents from fraction string.""" import math fr = parse_fraction(fraction_str) if fr <= 0: return 0 return 1200 * math.log2(fr) @app.route("/") def index(): return send_from_directory(".", "path_navigator.html") @app.route("/") def serve_static(filename): return send_from_directory(".", filename) @app.route("/api/chords") def get_chords(): return jsonify({"chords": chords, "total": len(chords)}) @app.route("/api/set-fundamental", methods=["POST"]) def set_fundamental(): """Set the fundamental frequency for OSC playback.""" global fundamental, osc_sender data = request.json fundamental = data.get("fundamental", 110.0) osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental) return jsonify({"fundamental": fundamental}) @app.route("/api/play-freq", methods=["POST"]) def play_freq(): """Play a single frequency for a node.""" data = request.json chord_index = data.get("chordIndex") node_index = data.get("nodeIndex") octave = data.get("octave", 0) 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) * (2**octave) voice = node_index + 1 # 1-indexed osc_sender.send_single(frequency, voice) return jsonify( { "frequency": frequency, "voice": voice, "fundamental": fundamental, "fraction": pitch.get("fraction", "1"), } ) @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)), } ) @app.route("/api/load-file", methods=["POST"]) def load_file(): global current_index, chords data = request.json filepath = data.get("filepath") if not filepath: return jsonify({"error": "No filepath provided"}), 400 try: with open(filepath) as f: chords_data = json.load(f) chords = chords_data.get("chords", []) current_index = 0 return jsonify({"loaded": len(chords), "filepath": filepath}) except FileNotFoundError: return jsonify({"error": f"File not found: {filepath}"}), 404 except json.JSONDecodeError: return jsonify({"error": f"Invalid JSON in: {filepath}"}), 400 @app.route("/api/parse-fraction", methods=["POST"]) def parse_fraction_api(): """Parse a fraction string to float.""" data = request.json fraction = data.get("fraction", "1") try: value = parse_fraction(fraction) return jsonify({"fraction": fraction, "value": value}) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/api/calculate-cents", methods=["POST"]) def calculate_cents_api(): """Calculate cents from fraction string.""" data = request.json fraction = data.get("fraction", "1") try: cents = calculate_cents(fraction) return jsonify({"fraction": fraction, "cents": cents}) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/api/batch-calculate-cents", methods=["POST"]) def batch_calculate_cents_api(): """Calculate cents for multiple fractions at once.""" data = request.json fractions = data.get("fractions", []) try: results = [] for fraction in fractions: cents = calculate_cents(fraction) results.append({"fraction": fraction, "cents": cents}) return jsonify({"results": results}) except Exception as e: return jsonify({"error": str(e)}), 400 if __name__ == "__main__": print("Starting Path Navigator server...") filepath = DATA_DIR / DEFAULT_FILE print(f"Loading chords from: {filepath}") load_chords() print(f"Loaded {len(chords)} chords") app.run(host="0.0.0.0", port=8080, debug=True)