#!/usr/bin/python3
import gi, math
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, Pango
import cairo

GST_OK = False
try:
    gi.require_version('Gst', '1.0')
    from gi.repository import Gst
    Gst.init(None)
    GST_OK = True
except Exception:
    pass

NOTES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
A4 = 440.0

def freq_to_note_cents(freq):
    if freq <= 0: return '?', 0, 0
    semitones = 12 * math.log2(freq / A4)
    rounded = round(semitones)
    cents = round((semitones - rounded) * 100)
    note = NOTES[(rounded + 9) % 12]
    octave = (rounded + 9) // 12 + 4
    return f'{note}{octave}', cents, freq

def play_ref_tone(freq):
    if not GST_OK: return None
    try:
        pipe = Gst.parse_launch(f'audiotestsrc freq={freq} ! audioconvert ! autoaudiosink')
        pipe.set_state(Gst.State.PLAYING)
        return pipe
    except Exception: return None

class App(Gtk.Window):
    def __init__(self):
        super().__init__(title='Chromatic Pitch Tuner')
        self.set_default_size(480, 420)
        self.ref_pipeline = None
        self.note = 'A'; self.octave = 4; self.cents = 0
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        vbox.set_margin_top(16); vbox.set_margin_bottom(16)
        vbox.set_margin_start(16); vbox.set_margin_end(16)
        self.add(vbox)
        # note display
        self.note_lbl = Gtk.Label()
        self.note_lbl.override_font(Pango.FontDescription('Bold 64'))
        self.note_lbl.set_text('A4')
        vbox.pack_start(self.note_lbl, False, False, 0)
        self.cents_lbl = Gtk.Label()
        self.cents_lbl.override_font(Pango.FontDescription('Monospace 16'))
        self.cents_lbl.set_text('0 cents')
        vbox.pack_start(self.cents_lbl, False, False, 0)
        # tuner needle drawing
        self.da = Gtk.DrawingArea(); self.da.set_size_request(400, 120)
        self.da.set_halign(Gtk.Align.CENTER)
        self.da.connect('draw', self.draw_needle)
        vbox.pack_start(self.da, False, False, 0)
        # reference tone section
        frame = Gtk.Frame(label='Reference Tone Playback')
        ref_box = Gtk.Grid(column_spacing=10, row_spacing=8)
        ref_box.set_margin_top(8); ref_box.set_margin_bottom(8)
        ref_box.set_margin_start(8); ref_box.set_margin_end(8)
        ref_box.attach(Gtk.Label(label='Note:', xalign=0), 0, 0, 1, 1)
        self.ref_note = Gtk.ComboBoxText()
        for n in NOTES: self.ref_note.append_text(n)
        self.ref_note.set_active(9)  # A
        self.ref_note.connect('changed', self.update_ref_freq)
        ref_box.attach(self.ref_note, 1, 0, 1, 1)
        ref_box.attach(Gtk.Label(label='Octave:', xalign=0), 0, 1, 1, 1)
        self.ref_oct = Gtk.SpinButton.new_with_range(1, 8, 1)
        self.ref_oct.set_value(4)
        self.ref_oct.connect('value-changed', self.update_ref_freq)
        ref_box.attach(self.ref_oct, 1, 1, 1, 1)
        self.ref_freq_lbl = Gtk.Label(label='440.0 Hz', xalign=0)
        ref_box.attach(self.ref_freq_lbl, 2, 0, 1, 2)
        hb = Gtk.Box(spacing=8)
        play_btn = Gtk.Button(label='▶ Play Reference')
        play_btn.connect('clicked', self.play_reference)
        hb.pack_start(play_btn, True, True, 0)
        stop_btn = Gtk.Button(label='⏹ Stop')
        stop_btn.connect('clicked', self.stop_reference)
        hb.pack_start(stop_btn, True, True, 0)
        ref_box.attach(hb, 0, 2, 3, 1)
        frame.add(ref_box)
        vbox.pack_start(frame, False, False, 0)
        # manual tuning input
        hb2 = Gtk.Box(spacing=8)
        hb2.add(Gtk.Label(label='Manual freq (Hz):'))
        self.freq_entry = Gtk.Entry(); self.freq_entry.set_text('440')
        self.freq_entry.connect('activate', self.manual_tune)
        hb2.pack_start(self.freq_entry, True, True, 0)
        tune_btn = Gtk.Button(label='Show Note')
        tune_btn.connect('clicked', self.manual_tune)
        hb2.pack_start(tune_btn, False, False, 0)
        vbox.pack_start(hb2, False, False, 0)
        self.update_ref_freq()
    def get_ref_freq(self):
        note_idx = self.ref_note.get_active()
        oct_num = int(self.ref_oct.get_value())
        semitones = (note_idx - 9) + (oct_num - 4) * 12
        return round(A4 * (2 ** (semitones / 12)), 2)
    def update_ref_freq(self, *_):
        freq = self.get_ref_freq()
        self.ref_freq_lbl.set_text(f'{freq} Hz')
    def play_reference(self, *_):
        self.stop_reference()
        freq = self.get_ref_freq()
        self.ref_pipeline = play_ref_tone(freq)
        note_name = NOTES[self.ref_note.get_active()]
        oct_num = int(self.ref_oct.get_value())
        self.show_tuning(freq)
    def stop_reference(self, *_):
        if self.ref_pipeline:
            self.ref_pipeline.set_state(Gst.State.NULL)
            self.ref_pipeline = None
    def manual_tune(self, *_):
        try:
            freq = float(self.freq_entry.get_text())
            self.show_tuning(freq)
        except ValueError: pass
    def show_tuning(self, freq):
        note_str, cents, _ = freq_to_note_cents(freq)
        self.note_lbl.set_text(note_str)
        color = 'green' if abs(cents) < 5 else ('orange' if abs(cents) < 15 else 'red')
        self.cents_lbl.set_markup(f'<span foreground="{color}">{cents:+d} cents</span>')
        self.cents = cents
        self.da.queue_draw()
    def draw_needle(self, w, cr):
        alloc = w.get_allocation()
        W, H = alloc.width, alloc.height
        cr.set_source_rgb(0.95,0.95,0.95); cr.paint()
        cx = W//2; cy = H - 20
        r = H - 30
        # arc background
        cr.set_source_rgb(0.85,0.85,0.85); cr.set_line_width(20)
        cr.arc(cx, cy, r, math.pi, 2*math.pi); cr.stroke()
        # colored zones
        def arc_col(start, end, col):
            cr.set_source_rgb(*col); cr.set_line_width(20)
            a1 = math.pi + start * math.pi / 100
            a2 = math.pi + end * math.pi / 100
            cr.arc(cx, cy, r, a1, a2); cr.stroke()
        arc_col(0, 50, (0.8,0.2,0.2))  # left = flat
        arc_col(50, 100, (0.2,0.8,0.2))  # center = in tune ... wait
        # green center, red extremes
        cr.set_source_rgb(0.2,0.8,0.2); cr.set_line_width(20)
        mid_start = math.pi + 47 * math.pi/100
        mid_end   = math.pi + 53 * math.pi/100
        cr.arc(cx, cy, r, mid_start, mid_end); cr.stroke()
        # needle
        angle = math.pi + (self.cents + 50) / 100 * math.pi
        nx = cx + r * math.cos(angle)
        ny = cy + r * math.sin(angle)
        cr.set_source_rgb(0.1,0.1,0.1); cr.set_line_width(3)
        cr.move_to(cx, cy); cr.line_to(nx, ny); cr.stroke()
        cr.arc(cx, cy, 6, 0, math.pi*2); cr.fill()
        # labels
        cr.set_source_rgb(0.3,0.3,0.3); cr.set_font_size(11)
        cr.move_to(10, H-5); cr.show_text('-50¢')
        cr.move_to(cx-8, 15); cr.show_text('0')
        cr.move_to(W-42, H-5); cr.show_text('+50¢')

win = App()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()
