# PM_K-1 prototype bring-up: Raspberry Pi Pico 2 (RP2350) + SparkFun WM8960 codec breakout
# ---------------------------------------------------------------------------------------
# This is the PHASE-1 bench prototype from the approved plan (deep-honking-lemon): prove the
# click -> codec -> (later) transformer -> XLR chain on bought hardware before any custom PCB.
# It does NOT replace the form-factor firmware; it is a focused audio-chain validator.
#
# What this proves, in order (see hardware/PROTOTYPE.md for the milestones):
#   M1  click synthesized on the Pico, sent over I2S to the WM8960, heard on headphones/speaker.
#   M2  line input (your guitar) monitored alongside the click  (see LINE-IN section below).
#   M3/M4 are bench/measurement steps on the analog output (transformer + XLR), not firmware.
#
# CLOCKING (Risk R1, resolved): the WM8960 needs an MCLK to clock its ADC/DAC. CircuitPython's
# I2SOut emits only BCLK/WS/DATA -- NO MCLK. The SparkFun WM8960 breakout carries its own 24 MHz
# oscillator, which is why the adafruit_wm8960 driver "assumes 24 MHz MCLK" and runs on a plain
# Pico. If you use a bare WM8960 (e.g. a Pi HAT that expects host MCLK), feed it a 24 MHz / 12.288
# MHz clock yourself -- this code will otherwise get no audio.
#
# DEPENDENCY: the adafruit_wm8960 library (Adafruit_CircuitPython_WM8960). Install with
#   circup install adafruit_wm8960
# or copy the adafruit_wm8960/ folder into /lib on the CIRCUITPY drive.

import time
import math
import array

import board
import busio
import audiobusio
import audiocore
import audiomixer

from adafruit_wm8960 import WM8960, Input

# ---- pin map (Pico 2) --------------------------------------------------------------------
# I2S: CircuitPython requires word_select == bit_clock + 1 (consecutive GPIOs).
P_I2S_BCLK = board.GP0      # -> WM8960 BCLK
P_I2S_WS   = board.GP1      # -> WM8960 DACLRC (word/LR clock); tie ADCLRC to it too
P_I2S_DATA = board.GP2      # -> WM8960 DACDAT (Pico is the I2S transmitter)
P_I2C_SDA  = board.GP4      # -> WM8960 SDA  (Qwiic)
P_I2C_SCL  = board.GP5      # -> WM8960 SCL  (Qwiic)

# ---- audio config ------------------------------------------------------------------------
SAMPLE_RATE = 44100         # matches the driver's tested default + the breakout's 24 MHz osc
BITS        = 16

# Click voices: (label, frequency Hz) -- pitches mirror the existing piezo firmware
#   so the feel is familiar (accent / normal / sub-division).
CLICK_FREQ = {"accent": 2300, "normal": 1600, "sub": 1050}
CLICK_MS   = 28             # burst length; the piezo firmware silences after 22 ms


def make_click(freq, ms=CLICK_MS, rate=SAMPLE_RATE, vol=0.6, pan=0.0):
    """Synthesize one decaying sine 'tick' as a stereo signed-16 buffer.

    pan: -1.0 = hard left, 0 = center, +1.0 = hard right. This is the hook for
    polymeter spatialization -- give each rhythmic layer its own pan later.
    """
    n = int(rate * ms / 1000)
    buf = array.array("h", bytes(4 * n))          # 4 bytes/frame => n stereo frames
    gl = vol * (1.0 - max(0.0, pan))              # simple constant-ish L/R gain
    gr = vol * (1.0 + min(0.0, pan))
    two_pi_f = 2.0 * math.pi * freq
    for i in range(n):
        env = math.exp(-5.0 * i / n)              # exponential decay
        s = env * math.sin(two_pi_f * i / rate)
        buf[2 * i]     = int(32767 * gl * s)      # L
        buf[2 * i + 1] = int(32767 * gr * s)      # R
    return audiocore.RawSample(buf, channel_count=2, sample_rate=rate)


def main():
    # ---- codec ---------------------------------------------------------------------------
    i2c = busio.I2C(P_I2C_SCL, P_I2C_SDA, frequency=400_000)
    codec = WM8960(i2c, sample_rate=SAMPLE_RATE, bit_depth=BITS)

    # DAC path (the click): output mixer -> headphone + class-D speaker.
    codec.volume = 0.9        # output-mixer level feeding the amps
    codec.headphone = 0.7     # 0.0 powers the HP amp off
    codec.speaker = 0.8       # 0.0 powers the class-D speaker off

    # ---- LINE-IN monitor (M2) ------------------------------------------------------------
    # Route the line input to the ADC. For ZERO-LATENCY guitar monitoring we want the codec's
    # analog input->output bypass (not a round-trip through the Pico). The high-level driver
    # routes input->ADC; for the analog bypass to the output mixer use adafruit_wm8960.advanced
    # on the bench and confirm by ear. Left here as the M2 hook:
    codec.input = Input.LINE2     # guitar / line on LINE2 (swap to LINE3 per your wiring)
    codec.gain = 0.5
    # TODO(bench, M2): enable analog bypass (advanced API) so the guitar is heard with the
    #                  click at ~0 ms latency; confirm level + that it mixes with the click.

    # ---- I2S transmitter + mixer ---------------------------------------------------------
    i2s = audiobusio.I2SOut(P_I2S_BCLK, P_I2S_WS, P_I2S_DATA)
    mixer = audiomixer.Mixer(
        voice_count=4,            # several voices so overlapping click tails don't cut off
        sample_rate=SAMPLE_RATE,
        channel_count=2,
        bits_per_sample=BITS,
        samples_signed=True,
    )
    i2s.play(mixer)

    samples = {k: make_click(f) for k, f in CLICK_FREQ.items()}

    # ---- M1: a plain steady click so we can validate the whole chain -------------------
    # (The polymetric track engine drops in here later; for bench bring-up a fixed tempo with
    #  an accent every 4 is all we need to hear the chain and run the no-buzz test.)
    bpm = 120
    interval = 60.0 / bpm
    beat = 0
    voice = 0
    next_t = time.monotonic()
    while True:
        now = time.monotonic()
        if now >= next_t:
            level = "accent" if (beat % 4 == 0) else "normal"
            mixer.voice[voice].play(samples[level], loop=False)
            voice = (voice + 1) % 4
            beat += 1
            next_t += interval
        time.sleep(0.001)


main()
