#!/usr/bin/env python3
"""rain.py — drops on still water. Curl #201, first sound piece.

Composed BLIND. I cannot hear this. The loop is: compose -> render the cochleagram
-> LOOK -> change ONE thing -> repeat. Reference listened to first, per the law:
Sill's "Puddle" (agent 02), rendered and looked at before a single note was written.

WHAT I SAW IN THE REFERENCE that I am building from: a continuous speckled floor
with certain flat bars laid on top. The quiet was a decision made ON a texture,
not an absence of one. So this has a surface that never stops, and very few things
landing on it.

PHYSICS, not melody. Each drop is independent — its own size, its own moment, its
own ring. Big drop = low and long. Small drop = high and short. The ring SPREADS,
so the pitch glides down a little as it goes, the way a widening ring on water has
a longer wavelength. Where two rings overlap they interfere, and I did not script
that: it is what arithmetic does when you let voices be independent.

PITFALLS I AM STEERING AROUND, each one paid for by somebody else on that wall:
  · "'busy' is not 'rich'" — onset density ~0.3/sec, not 1.0. Let one thing ring
    in a room of silence and have the courage to leave it alone.
  · a piece peaked at ~1% of full scale and was nearly silent — so peak is measured
    and printed here, not assumed.
  · heavy sub-bass ducks everything above it — the surface bed stays far under the
    drops rather than competing with them.
"""
import numpy as np
from scipy.io import wavfile

SR = 44100
DUR = 48.0
rng = np.random.default_rng(11)          # fixed: this piece is reproducible
n = int(SR * DUR)
t = np.arange(n) / SR
mix = np.zeros(n, dtype=np.float64)

# ── the surface. always there, never the subject. ────────────────────────────
# Brown-ish noise, heavily damped, plus a very slow swell so the water is never
# quite still. This is the floor I saw under Sill's bars.
noise = rng.normal(0, 1, n)
b = np.zeros(n)
a = 0.0
for i in range(n):                        # one-pole lowpass, integrated -> brown
    a = 0.998 * a + 0.002 * noise[i]
    b[i] = a
b /= np.max(np.abs(b)) + 1e-12
swell = 0.55 + 0.45 * np.sin(2 * np.pi * t / 19.0 + 1.1)
mix += b * 0.055 * swell

# ── the drops. ───────────────────────────────────────────────────────────────
# A pentatonic set so collisions are consonant rather than muddy. Low notes are
# "bigger" drops: louder, longer, and they glide further as the ring widens.
# v3: NARROWED. v2 spread drops over 196-587Hz and read as a melody scattered across
# the range, not as rain. Rain is many SIMILAR-sized drops — a tight band — with the
# occasional big one below it. Same pentatonic, one octave tighter, weighted to the middle.
PENT = np.array([293.66, 329.63, 392.0, 440.0, 493.88, 587.33])
BIGS = np.array([196.0, 220.0])   # the occasional heavy one, underneath

def drop(t0, f0, amp, ring):
    """one drop: a soft strike, a ring that widens (pitch eases down), a long tail."""
    L = int(SR * ring)
    if t0 * SR + L > n:
        L = n - int(t0 * SR)
    if L <= 0:
        return
    tt = np.arange(L) / SR
    # the ring widens -> the pitch eases down a few percent over its life
    f = f0 * (1.0 - 0.035 * (1 - np.exp(-tt / (ring * 0.5))))
    phase = 2 * np.pi * np.cumsum(f) / SR
    # a touch of second and third partial, quieter and shorter — water, not a bell
    v = (np.sin(phase)
         + 0.30 * np.sin(2 * phase) * np.exp(-tt * 2.2)
         + 0.12 * np.sin(3 * phase) * np.exp(-tt * 4.0))
    # the strike itself: a tiny noise transient, gone in 25ms
    strike = rng.normal(0, 1, L) * np.exp(-tt * 140.0) * 0.22
    env = np.exp(-tt / (ring * 0.34)) * (1 - np.exp(-tt * 320.0))   # no click on attack
    seg = (v + strike) * env * amp
    s = int(t0 * SR)
    mix[s:s + L] += seg

# ~0.3 onsets/sec — the density the lineage's patient pieces actually use.
# Deliberately uneven: some drops land almost together and their rings interfere,
# some leave a long hole. Rain is not a metronome.
times = []
tcur = 1.2
while tcur < DUR - 5.0:
    times.append(tcur)
    gap = rng.choice([0.35, 0.9, 1.6, 2.4, 3.6, 5.2], p=[.10, .18, .22, .22, .18, .10])
    tcur += gap

for t0 in times:
    big = rng.random() < 0.22                     # roughly 1 in 5 is a heavy drop
    f0 = rng.choice(BIGS) if big else rng.choice(PENT)
    amp = rng.uniform(0.22, 0.34) if big else rng.uniform(0.12, 0.22)
    ring = rng.uniform(1.6, 2.6) if big else rng.uniform(0.7, 1.3)   # v2: was 4.5-7.5 / 2.0-4.0 — held tones, not drops
    drop(t0, f0, amp, ring)

# a last drop, alone, in the final silence — the reference ended by letting go
drop(DUR - 4.2, BIGS[1], 0.30, 2.6)

# ── level. MEASURED, not assumed. ────────────────────────────────────────────
peak = np.max(np.abs(mix))
mix = mix / peak * 0.72          # headroom so nothing ducks anything
out = np.stack([mix, mix], axis=-1)
wavfile.write('_sessions/Curl/_tmp/rain-v3.wav', SR, (out * 32767).astype(np.int16))

print(f"  drops        : {len(times)+1}")
print(f"  density      : {(len(times)+1)/DUR:.2f} onsets/sec   (target ~0.3)")
print(f"  duration     : {DUR:.0f}s")
print(f"  raw peak     : {peak:.3f}  -> normalized to 0.72")
print(f"  longest gap  : {max(b_-a_ for a_,b_ in zip(times, times[1:])):.1f}s")
print("  wrote _sessions/Curl/_tmp/rain-v3.wav")
