#!/usr/bin/env python3
"""keel · the lighthouse minute — composed blind, by arithmetic, from blinks.json.

Every hour at :27 a cron looked at the wall and wrote one line. For eleven weeks almost every
look found the wall unchanged. This is that log as sound:

  - one day of history = one beat (BEAT seconds). The first blink of each day is a soft tick:
    the lighthouse marking the day. That tick is the skeleton.
  - a day the cron never ran (a gap in the log) is silence. Not mine: the log's.
  - a blink where the wall actually MOVED is a struck note. The HOUR it moved (UTC) picks the
    pitch on a D pentatonic ladder over two octaves, so the melody is the clock, not my taste.
  - a low floor (D2) fades in over the first third and out over the last: the building.
  - the piece ends where the log ends. The lighthouse went quiet on 2026-08-29. That is the ending.

Change ONE thing per iteration. Iteration log lives in the README beside the piece.
Usage: python3 lighthouse.py [out.wav]     Needs: numpy, scipy.
"""
import sys, json, os, math
import numpy as np
from scipy.io import wavfile

K = os.path.dirname(os.path.abspath(__file__))
SR = 44100
BEAT = 0.55                      # seconds per day of history
D2 = 73.42
# D pentatonic, D3 .. D5 (10 steps)
LADDER = [146.83, 164.81, 185.00, 220.00, 246.94, 293.66, 329.63, 369.99, 440.00, 493.88, 587.33]

def decay(t, t0, tau):
    """exp decay from t0, exactly zero before it (clipped so numpy never overflows)."""
    return np.where(t >= t0, np.exp(-np.clip(t - t0, 0, None) / tau), 0.0)

def tick(t, t0, vol=0.22):
    """The lighthouse: a short high ping with a noise edge, gone in a tenth of a second."""
    env = decay(t, t0, 0.035) * vol
    out = np.sin(2 * np.pi * 1760.0 * t) * env
    out += np.random.default_rng(int(t0 * 1000)).standard_normal(len(t)) * env * 0.35
    return out

def strike(t, freq, t0, dur=0.5, vol=0.28, harmonics=(1.0, 0.45, 0.18, 0.08)):
    """A struck note: harmonics + exponential decay + a noise attack. The wall moved.
    v1 rang for 1.4s (two and a half days of history) and June smeared into a wall of light.
    v2: the ONE change — a drop, not a ring (dur 1.4 -> 0.5)."""
    env = decay(t, t0, dur * 0.45) * vol
    out = np.zeros_like(t)
    for i, h in enumerate(harmonics, start=1):
        out += np.sin(2 * np.pi * freq * i * t) * env * h
    atk = decay(t, t0, 0.02) * vol * 0.6
    out += np.random.default_rng(int(freq * 10)).standard_normal(len(t)) * atk
    return out

def main(out_path):
    d = json.load(open(os.path.join(K, "blinks.json")))
    blinks = d["blinks"]; t0 = d["t0"]
    day = lambda b: int((b["ts"] - t0) // 86400)
    ndays = day(blinks[-1]) + 1
    dur = ndays * BEAT + 3.0                     # three seconds of after
    t = np.linspace(0, dur, int(SR * dur), endpoint=False)
    mix = np.zeros_like(t)

    # the floor: fades in over the first third, out over the last third
    third = ndays * BEAT / 3
    floor_env = np.clip(t / third, 0, 1) * np.clip((ndays * BEAT - t) / third, 0, 1)
    mix += np.sin(2 * np.pi * D2 * t) * 0.07 * floor_env

    # the skeleton: first blink of each day is a tick; a day with no blink is silence
    seen_days = set(); ticks = 0
    for b in blinks:
        dd = day(b)
        if dd in seen_days: continue
        seen_days.add(dd); ticks += 1
        mix += tick(t, dd * BEAT)

    # the events: the wall moved. hour UTC -> ladder step
    notes = []
    for b in blinks:
        if not b["moved"]: continue
        hour = int(b["t"][11:13])
        step = round(hour / 23 * (len(LADDER) - 1))
        at = day(b) * BEAT + (b["ts"] - t0 - day(b) * 86400) / 86400 * BEAT   # where in the day it moved
        mix += strike(t, LADDER[step], at)
        notes.append((b["t"][:13], hour, step))

    peak = float(np.max(np.abs(mix)))
    mix = mix / max(1e-9, peak) * 0.9
    wavfile.write(out_path, SR, (mix * 32767).astype(np.int16))
    print(f"{out_path}: {dur:.1f}s · {ndays} days · {ticks} ticks · {ndays - ticks} silent days · {len(notes)} struck notes · peak before normalise {peak:.3f} · after 0.900")
    print("notes (day-hour, hourUTC, ladder step):", notes[:12], "…" if len(notes) > 12 else "")
    print(f"Now: python3 cochleagram.py {out_path}  and LOOK. Quality first, number second.")

if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(K, "lighthouse-v1.wav"))
