#!/usr/bin/env python3
"""
phitune13.py
Golden Recursive Algebra Cymatic Visualizer + Audio
"""

import numpy as np
import sympy as sp
import threading
import sounddevice as sd
from vispy import app, scene
from vispy.scene import widgets

# -----------------------------
# Golden Recursive Algebra Core
# -----------------------------
phi = (1 + sp.sqrt(5)) / 2

def fibonacci(n):
    return sp.fibonacci(n)

def prime(n):
    return sp.prime(max(1, n))  # ensure >= 1

def D_n(n, r, k=1.0, Omega=1.0):
    """Golden Recursive Algebra mapping"""
    F_n = fibonacci(n)
    P_n = prime(n)
    return float(sp.sqrt(phi * F_n * (2**n) * P_n * Omega)) * (r**k)

def midi_to_freq(note):
    """Convert MIDI note number to frequency (Hz). A4=440Hz at note=69"""
    return 440.0 * (2.0 ** ((note - 69) / 12.0))

# -----------------------------
# Cymatic Window
# -----------------------------
class CymaticWindow(scene.SceneCanvas):
    def __init__(self):
        super().__init__(keys='interactive', size=(900, 900), show=True)

        # --- Sliders / Dials ---
        self.note_slider = widgets.Slider(min=0, max=72, value=36)
        self.omega_slider = widgets.Slider(min=0.1, max=5.0, value=1.0)
        self.k_slider = widgets.Slider(min=0.1, max=5.0, value=1.0)
        self.resolution_slider = widgets.Slider(min=128, max=4096, value=1024)

        self.grid = widgets.Grid()
        self.grid.add_widget(self.note_slider, 0, 0)
        self.grid.add_widget(self.omega_slider, 0, 1)
        self.grid.add_widget(self.k_slider, 0, 2)
        self.grid.add_widget(self.resolution_slider, 0, 3)
        self.central_widget.add_widget(self.grid)

        # --- Particle cloud ---
        self.view = self.central_widget.add_view()
        self.scatter = scene.visuals.Markers(parent=self.view.scene)

        # Timer for animation
        self.timer = app.Timer('auto', connect=self.refresh_scene, start=True)

        # --- Audio ---
        self.audio_thread = threading.Thread(target=self.play_audio, daemon=True)
        self.audio_thread.start()

    def play_audio(self):
        """Continuously play tone based on current note slider"""
        samplerate = 44100
        blocksize = 1024
        t = np.arange(blocksize) / samplerate
        phase = 0.0

        def callback(outdata, frames, time, status):
            nonlocal phase
            n = int(self.note_slider.value)
            freq = midi_to_freq(n)
            samples = np.sin(2 * np.pi * freq * (t + phase))
            phase += frames / samplerate
            outdata[:, 0] = samples  # mono left
            if outdata.shape[1] > 1:
                outdata[:, 1] = samples  # duplicate right

        with sd.OutputStream(channels=2, callback=callback,
                             samplerate=samplerate, blocksize=blocksize):
            app.run()  # keep audio alive

    def refresh_scene(self, event=None):
        """Update visuals each frame"""
        n = int(self.note_slider.value)
        Omega = float(self.omega_slider.value)
        k = float(self.k_slider.value)
        resolution = int(self.resolution_slider.value)

        # radial parameter
        theta = np.linspace(0, 2*np.pi, resolution)
        r = np.linspace(0.05, 1.0, resolution)

        # Apply D_n mapping
        R_vals = [D_n(n, ri, k, Omega) for ri in r]

        # Convert to Cartesian coords
        x = np.array(R_vals) * np.cos(theta)
        y = np.array(R_vals) * np.sin(theta)
        pts = np.column_stack([x, y, np.zeros_like(x)])

        self.scatter.set_data(pts, face_color='cyan', size=3)

# -----------------------------
if __name__ == "__main__":
    win = CymaticWindow()
    app.run()
