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