Simple test

Ensure your device works with this simple test.

examples/nau88l21_simpletest.py
 1# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
 2#
 3# SPDX-License-Identifier: MIT
 4"""
 5Play a one-octave scale out the headphone jack with synthio, output only.
 6
 7There is no microphone capture here, so the codec only needs its DAC side
 8brought up. The one ordering rule that still applies is the clock one: the FLL
 9locks to the incoming bit clock, so BCLK has to be running before
10``configure_clocks`` is called. Playing the synth is what starts BCLK, so it
11goes first; the codec is silent until ``headphone_output`` is enabled a few
12lines later, which is why starting playback before configuring is harmless.
13
14Pin names are the Teenage Engineering EP-2350's; adjust for other boards.
15"""
16
17import time
18
19import audiobusio
20import board
21import synthio
22
23import adafruit_nau88l21
24
25RATE = 48000  # frame rate on the wire; also what the codec's FLL locks to
26
27# DAC digital gain, dB. Raising this does not reliably make the jack louder:
28# the analog output stage runs out of headroom first, so extra digital gain
29# turns into distortion. Measure before trusting a larger number.
30DAC_VOLUME = 0
31# Headphone analog volume, dB. Only 0, -3, -6 and -9 exist.
32HEADPHONE_VOLUME = 0
33
34# MIDI note numbers for a C-major scale, C4 up to C5.
35SCALE = (60, 62, 64, 65, 67, 69, 71, 72)
36NOTE_SECONDS = 0.4
37
38# CAUTION: this drives the headphone amplifier. Take the headphones off before
39# the first run and confirm the level is comfortable before wearing them.
40
41i2s = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DOUT)
42
43synth = synthio.Synthesizer(sample_rate=RATE)
44
45# Playing the synth starts BCLK, which the codec's FLL needs before it can lock.
46# The synth streams silence until a note is pressed, so nothing is audible yet.
47i2s.play(synth)
48
49codec = adafruit_nau88l21.NAU88L21(board.I2C())
50codec.configure_clocks()
51
52# headphone_output is a quickstart that deliberately ends quiet, at -20 dB DAC
53# and -9 dB headphone. Set both volumes afterward rather than leaving the defaults
54codec.headphone_output = True
55codec.dac_volume = DAC_VOLUME
56codec.headphone_volume = HEADPHONE_VOLUME
57
58print("playing scale")
59try:
60    while True:
61        for note in SCALE:
62            synth.press(note)
63            time.sleep(0.25)
64            synth.release(note)
65            time.sleep(0.125)
66finally:
67    i2s.stop()
68    i2s.deinit()

Microphone passthrough

Full duplex: the microphone feeds the headphone amplifier through a high-pass filter and a gain stage.

examples/nau88l21_passthrough.py
  1# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
  2#
  3# SPDX-License-Identifier: MIT
  4"""
  5Mic -> headphone passthrough for the EP-2350, with a DSP chain in between.
  6"""
  7
  8import time
  9
 10import audiobusio
 11import audiodelays
 12import audiofilters
 13import audioi2sin
 14import board
 15import synthio
 16
 17import adafruit_nau88l21
 18
 19RATE = 48000  # frame rate on the wire; also what the codec's FLL expects
 20
 21# --- Hardware effects config ---
 22
 23# Analog mic gain, dB, -1 .. 36.
 24MIC_GAIN = 24
 25# ADC digital gain, dB.
 26ADC_VOLUME = 0
 27# DAC digital gain, dB.
 28DAC_VOLUME = 6
 29# Headphone analog volume, dB. Only 0/-3/-6/-9 exist.
 30HEADPHONE_VOLUME = 0
 31
 32# --- Software effects config ---
 33
 34# Gain of the software stage, in dB.
 35PRE_GAIN = 4.0
 36# High-pass corner, Hz.
 37HPF_HZ = 120
 38
 39
 40# The internal clock mode object has to exist first: it generates BCLK/WS, and
 41# everything in external clock mode syncs to the WS edges it sees. Constructing
 42# it starts BCLK, which the codec's FLL then has something to lock to.
 43i2s = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DOUT)
 44
 45codec = adafruit_nau88l21.NAU88L21(board.I2C())
 46codec.configure_clocks()
 47
 48# enable headphone output and set hardware volume
 49codec.headphone_output = True
 50codec.dac_volume = DAC_VOLUME
 51codec.headphone_volume = HEADPHONE_VOLUME
 52
 53codec.configure_microphone_input(gain_db=MIC_GAIN)
 54codec.adc_volume = ADC_VOLUME
 55
 56# left_justified=True is what matches this codec's ADCDAT, which it
 57# drives one BCLK ahead of what our own internal clock mode TX program emits.
 58mic = audioi2sin.I2SIn(
 59    board.I2S_BIT_CLOCK,
 60    board.I2S_WORD_SELECT,
 61    board.I2S_DIN,
 62    sample_rate=RATE,
 63    bit_depth=16,
 64    mono=True,
 65    external_clock=True,
 66    left_justified=True,
 67)
 68
 69common = dict(
 70    buffer_size=1024, sample_rate=RATE, bits_per_sample=16, samples_signed=True, channel_count=1
 71)
 72hpf = audiofilters.Filter(
 73    filter=synthio.Biquad(synthio.FilterMode.HIGH_PASS, frequency=HPF_HZ, Q=0.7), mix=1.0, **common
 74)
 75amp = audiofilters.Distortion(
 76    pre_gain=PRE_GAIN,
 77    drive=0.0,
 78    mode=audiofilters.DistortionMode.LOFI,
 79    soft_clip=True,
 80    mix=1.0,
 81    **common,
 82)
 83
 84# WIRING ORDER MATTERS. Work from the output backwards, so the call that
 85# hands the mic to something is the LAST one:
 86#
 87#     i2s.play(amp) -> amp.play(hpf) -> hpf.play(mic)
 88#
 89# i2s.play() restarts the internal clock mode state machine, which knocks the
 90# external clock mode I2SIn off the frame it locked to. The firmware re-syncs
 91# that I2SIn from its reset_buffer(), which only runs when something
 92# calls play() *on the mic*. The effects do not propagate reset_buffer() down
 93# to their own source. So wiring the mic in first (hpf.play(mic) before
 94# i2s.play(amp)) syncs the mic and then immediately de-syncs it,
 95# the output is silence or hiss.
 96
 97i2s.play(amp, loop=True)
 98amp.play(hpf)
 99hpf.play(mic)
100
101print(
102    f"passthrough running: mic gain {codec.mic_gain:.0f} dB, +{PRE_GAIN:.1f} dB, HPF {HPF_HZ:d} Hz"
103)
104
105try:
106    while True:
107        time.sleep(1)
108        # Reading .overflow clears it. It trips if the playback side falls
109        # behind the mic, which should not happen here -- both run off the
110        # same BCLK -- so a report means something is wrong.
111        if mic.overflow:
112            print("overflow")
113except KeyboardInterrupt:
114    i2s.stop()
115    i2s.deinit()
116    mic.deinit()