# 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()