Skip to content

Commit 5e7ee2d

Browse files
2 parents 74a9400 + 2998345 commit 5e7ee2d

7 files changed

Lines changed: 546 additions & 3 deletions

File tree

internal_filesystem/lib/drivers/codec/__init__.py

Whitespace-only changes.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# ES8311 mono audio codec driver
2+
# Initialises the ES8311 over I2C so that the ESP32 I2S peripheral can route
3+
# audio to/from the on-board speaker/microphone.
4+
#
5+
# Register layout and initialisation sequence are taken directly from the
6+
# Espressif reference driver shipped with the Freenove ESP32-S3 Display
7+
# tutorial sketches (Sketch_07.1_Music / Sketch_07.2_Echo, es8311.cpp).
8+
#
9+
# Clock configuration (MCLK_MULTIPLE = 256, 16-bit I2S, two slots per frame):
10+
# MCLK = sample_rate × 256 (driven by MCK PWM pin)
11+
# BCLK = MCLK / 4 (bclk_div = 4 → REG06 = bclk_div−1 = 3)
12+
# LRCK = MCLK / 256 = sample_rate (lrck_h=0x00, lrck_l=0xFF → REG07/08)
13+
# ADC/DAC oversampling rate = 0x10 (REG03 / REG04)
14+
# These divider values are identical for every standard sample rate when
15+
# MCLK = rate × 256 (verified against Espressif coeff_div[] table).
16+
#
17+
# The codec runs as I2S slave (ESP32-S3 drives BCLK and LRCK).
18+
19+
import time
20+
21+
try:
22+
from micropython import const
23+
except ImportError:
24+
def const(x): return x
25+
26+
I2C_ADDR = const(0x18)
27+
28+
# ---------------------------------------------------------------------------
29+
# Register addresses (from es8311_reg.h, Espressif reference driver)
30+
# ---------------------------------------------------------------------------
31+
_REG00_RESET = const(0x00) # reset + power control
32+
_REG01_CLK_SRC = const(0x01) # clock source select, all-clock enable
33+
_REG02_CLK_DIV = const(0x02) # pre-divider / pre-multiplier
34+
_REG03_ADC_OSR = const(0x03) # ADC fs-mode and oversampling rate
35+
_REG04_DAC_OSR = const(0x04) # DAC oversampling rate
36+
_REG05_CLKDIV = const(0x05) # ADC and DAC clock dividers
37+
_REG06_BCLKDIV = const(0x06) # BCLK (SCLK) inverter and divider
38+
_REG07_LRCK_H = const(0x07) # LRCK divider high byte
39+
_REG08_LRCK_L = const(0x08) # LRCK divider low byte
40+
_REG09_SDP_IN = const(0x09) # serial data port for DAC (playback input to codec)
41+
_REG0A_SDP_OUT = const(0x0A) # serial data port for ADC (recording output from codec)
42+
_REG0D_SYS = const(0x0D) # system: power-up analog circuitry
43+
_REG0E_SYS = const(0x0E) # system: enable analog PGA + ADC modulator
44+
_REG12_DAC_EN = const(0x12) # system: power-up DAC
45+
_REG13_SYS = const(0x13) # system: enable HP output driver
46+
_REG14_MIC = const(0x14) # microphone: DMIC select, analog PGA gain
47+
_REG16_ADC_GAIN = const(0x16) # ADC digital gain (separate from volume; default 4 = 24 dB)
48+
_REG17_ADC_VOL = const(0x17) # ADC volume / gain
49+
_REG1C_ADC_EQ = const(0x1C) # ADC equalizer bypass + DC-offset cancel
50+
_REG31_DAC_MUTE = const(0x31) # DAC soft-mute control (bits[6:5] = 11 → muted)
51+
_REG32_DAC_VOL = const(0x32) # DAC output volume (0x00=muted, 0xFF=max)
52+
_REG37_DAC_EQ = const(0x37) # DAC equalizer / ramp-rate control
53+
54+
# SDP format word: slave mode (bit7=0), 16-bit resolution (bits[4:2]=011)
55+
_SDP_16BIT_SLAVE = const(0x0C)
56+
57+
# Default DAC volume at init: 85% using Espressif formula (volume*256/100)−1
58+
_DEFAULT_VOL_REG = const(0xD8) # = (85*256//100) - 1 ≈ 85% output volume
59+
60+
61+
class ES8311:
62+
"""
63+
ES8311 codec initialiser.
64+
65+
Usage::
66+
67+
i2c = machine.I2C(0, sda=Pin(16), scl=Pin(15), freq=400_000)
68+
codec = ES8311(i2c)
69+
"""
70+
71+
def __init__(self, i2c):
72+
self._i2c = i2c
73+
self._init()
74+
75+
# ------------------------------------------------------------------
76+
def _wr(self, reg, val):
77+
self._i2c.writeto_mem(I2C_ADDR, reg, bytes([val]))
78+
79+
def _rd(self, reg):
80+
buf = bytearray(1)
81+
self._i2c.readfrom_mem_into(I2C_ADDR, reg, buf)
82+
return buf[0]
83+
84+
# ------------------------------------------------------------------
85+
def _init(self):
86+
# --- Reset sequence (matches Espressif es8311_init) ---
87+
self._wr(_REG00_RESET, 0x1F) # assert reset
88+
time.sleep_ms(20)
89+
self._wr(_REG00_RESET, 0x00) # release reset
90+
self._wr(_REG00_RESET, 0x80) # power-on command (required)
91+
92+
# --- Clock configuration ---
93+
# REG01: enable all internal clocks; select MCLK from MCLK pin (bit7=0)
94+
self._wr(_REG01_CLK_SRC, 0x3F)
95+
# REG02: pre_div=1 (bits[7:5]=000), pre_multi=×1 (bits[4:3]=00)
96+
self._wr(_REG02_CLK_DIV, 0x00)
97+
# REG03: ADC fs_mode=single-speed (bit6=0), ADC OSR=0x10
98+
self._wr(_REG03_ADC_OSR, 0x10)
99+
# REG04: DAC OSR=0x10
100+
self._wr(_REG04_DAC_OSR, 0x10)
101+
# REG05: ADC clk_div=1 (bits[7:4]=0000), DAC clk_div=1 (bits[3:0]=0000)
102+
self._wr(_REG05_CLKDIV, 0x00)
103+
# REG06: BCLK divider = bclk_div−1 = 4−1 = 3 (MCLK/4 = BCLK for 16-bit stereo)
104+
self._wr(_REG06_BCLKDIV, 0x03)
105+
# REG07/08: LRCK divider = 0x00FF = 255+1 = 256 (MCLK/256 = sample_rate)
106+
self._wr(_REG07_LRCK_H, 0x00)
107+
self._wr(_REG08_LRCK_L, 0xFF)
108+
109+
# --- I2S serial data format: 16-bit, standard I2S, slave mode ---
110+
self._wr(_REG09_SDP_IN, _SDP_16BIT_SLAVE) # DAC (playback)
111+
self._wr(_REG0A_SDP_OUT, _SDP_16BIT_SLAVE) # ADC (recording)
112+
113+
# --- System / analog power-up ---
114+
self._wr(_REG0D_SYS, 0x01) # power up analog circuitry
115+
self._wr(_REG0E_SYS, 0x02) # enable analog PGA + ADC modulator
116+
self._wr(_REG12_DAC_EN, 0x00) # power up DAC
117+
self._wr(_REG13_SYS, 0x10) # enable output to HP driver
118+
self._wr(_REG14_MIC, 0x1A) # enable analog mic input, max PGA gain
119+
120+
# --- ADC (microphone) ---
121+
self._wr(_REG16_ADC_GAIN, 0x04) # ADC digital gain = 24 dB (default)
122+
self._wr(_REG17_ADC_VOL, 0xC8) # ADC gain/volume (Espressif default)
123+
self._wr(_REG1C_ADC_EQ, 0x6A) # ADC equalizer bypass, cancel DC offset
124+
125+
# --- DAC (speaker) ---
126+
self._wr(_REG32_DAC_VOL, _DEFAULT_VOL_REG) # set output volume (~85%)
127+
self._wr(_REG37_DAC_EQ, 0x08) # bypass DAC equalizer
128+
129+
# Soft-mute the DAC at boot — unmuted by on_open callback when playback starts
130+
self.dac_mute(True)
131+
132+
print("ES8311: codec initialised")
133+
134+
def dac_mute(self, mute=True):
135+
"""
136+
Soft-mute or unmute the DAC output.
137+
138+
Uses the ES8311's built-in ramp so the transition is pop-free.
139+
Does not affect the DAC power state or volume register.
140+
141+
Args:
142+
mute: True to mute, False to unmute
143+
"""
144+
val = self._rd(_REG31_DAC_MUTE)
145+
if mute:
146+
val |= 0x60 # bits[6:5] = 11 → soft mute on
147+
else:
148+
val &= ~0x60 # bits[6:5] = 00 → soft mute off
149+
self._wr(_REG31_DAC_MUTE, val)
150+
151+
def set_dac_volume(self, percent):
152+
"""
153+
Set DAC (speaker) volume.
154+
155+
Args:
156+
percent: 0 (mute) … 100 (maximum)
157+
"""
158+
percent = max(0, min(100, percent))
159+
if percent == 0:
160+
val = 0
161+
else:
162+
val = (percent * 256 // 100) - 1
163+
self._wr(_REG32_DAC_VOL, val)
164+
165+
def set_adc_volume(self, percent):
166+
"""
167+
Set ADC (microphone) gain.
168+
169+
Args:
170+
percent: 0 (minimum) … 100 (maximum, 0xC8 default)
171+
"""
172+
percent = max(0, min(100, percent))
173+
val = percent * 0xC8 // 100
174+
self._wr(_REG17_ADC_VOL, val)

internal_filesystem/lib/mpos/audio/audiomanager.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def __init__(
4242
i2s_pins=None,
4343
buzzer_pin=None,
4444
preferred_sample_rate=None,
45+
on_open=None,
46+
on_close=None,
4547
):
4648
if kind not in ("i2s", "buzzer"):
4749
raise ValueError("Output.kind must be 'i2s' or 'buzzer'")
@@ -52,6 +54,8 @@ def __init__(
5254
self.kind = kind
5355
self.channels = channels
5456
self.preferred_sample_rate = preferred_sample_rate
57+
self.on_open = on_open
58+
self.on_close = on_close
5559

5660
if kind == "i2s":
5761
if not i2s_pins:
@@ -88,6 +92,8 @@ def __init__(
8892
adc_mic_pin=None,
8993
pdm_pins=None,
9094
preferred_sample_rate=None,
95+
on_open=None,
96+
on_close=None,
9197
):
9298
if kind not in ("i2s", "adc", "pdm"):
9399
raise ValueError("Input.kind must be 'i2s', 'adc', or 'pdm'")
@@ -98,6 +104,8 @@ def __init__(
98104
self.kind = kind
99105
self.channels = channels
100106
self.preferred_sample_rate = preferred_sample_rate
107+
self.on_open = on_open
108+
self.on_close = on_close
101109

102110
if kind == "i2s":
103111
if not i2s_pins:
@@ -122,7 +130,7 @@ def __init__(
122130

123131
@staticmethod
124132
def _validate_i2s_pins(i2s_pins):
125-
allowed = {"sck_in", "sck", "ws", "sd_in"}
133+
allowed = {"sck_in", "sck", "ws", "sd_in", "mck"}
126134
for key in i2s_pins:
127135
if key not in allowed:
128136
raise ValueError("Invalid i2s_pins key for input: %s" % key)
@@ -726,6 +734,8 @@ def _play_wav(self):
726734
i2s_pins=self.output.i2s_pins,
727735
on_complete=self.on_complete,
728736
requested_sample_rate=self.sample_rate,
737+
on_open=getattr(self.output, "on_open", None),
738+
on_close=getattr(self.output, "on_close", None),
729739
)
730740
self._stream.play()
731741

@@ -810,6 +820,8 @@ def _record_i2s(self):
810820
sample_rate=self.sample_rate,
811821
i2s_pins=self.input_device.i2s_pins,
812822
on_complete=self.on_complete,
823+
on_open=getattr(self.input_device, "on_open", None),
824+
on_close=getattr(self.input_device, "on_close", None),
813825
)
814826
self._stream.record()
815827

internal_filesystem/lib/mpos/audio/stream_record_i2s.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class RecordStream:
2727
DEFAULT_MAX_DURATION_MS = 60000 # 60 seconds max
2828
DEFAULT_FILESIZE = 1024 * 1024 * 1024 # 1GB data size because it can't be quickly set after recording
2929

30-
def __init__(self, file_path, duration_ms, sample_rate, i2s_pins, on_complete):
30+
def __init__(self, file_path, duration_ms, sample_rate, i2s_pins, on_complete,
31+
on_open=None, on_close=None):
3132
"""
3233
Initialize recording stream.
3334
@@ -37,15 +38,20 @@ def __init__(self, file_path, duration_ms, sample_rate, i2s_pins, on_complete):
3738
sample_rate: Sample rate in Hz
3839
i2s_pins: Dict with 'sck', 'ws', 'sd_in' pin numbers
3940
on_complete: Callback function(message) when recording finishes
41+
on_open: Optional callable invoked after MCLK starts, before I2S init
42+
on_close: Optional callable invoked before I2S deinit
4043
"""
4144
self.file_path = file_path
4245
self.duration_ms = duration_ms if duration_ms else self.DEFAULT_MAX_DURATION_MS
4346
self.sample_rate = sample_rate if sample_rate else self.DEFAULT_SAMPLE_RATE
4447
self.i2s_pins = i2s_pins
4548
self.on_complete = on_complete
49+
self.on_open = on_open
50+
self.on_close = on_close
4651
self._keep_running = True
4752
self._is_recording = False
4853
self._i2s = None
54+
self._mck_pwm = None
4955
self._bytes_recorded = 0
5056
self._start_time_ms = 0
5157

@@ -138,6 +144,26 @@ def record(self):
138144
if not use_simulation:
139145
# Initialize I2S in RX mode with correct pins for microphone
140146
try:
147+
# Start MCLK on mck pin if provided (required for I2S codecs such as ES8311)
148+
if 'mck' in self.i2s_pins:
149+
try:
150+
from machine import Pin, PWM
151+
mck_pin = Pin(self.i2s_pins['mck'], Pin.OUT)
152+
self._mck_pwm = PWM(mck_pin)
153+
mck_freq = self.sample_rate * 256
154+
self._mck_pwm.freq(mck_freq)
155+
self._mck_pwm.duty_u16(32768) # 50% duty cycle
156+
print(f"RecordStream: MCLK PWM started at {mck_freq} Hz")
157+
except Exception as e:
158+
print(f"RecordStream: MCLK PWM init failed: {e}")
159+
160+
# Notify codec to prepare for recording (e.g. mute DAC, configure ADC)
161+
if self.on_open:
162+
try:
163+
self.on_open()
164+
except Exception as e:
165+
print(f"RecordStream: on_open failed: {e}")
166+
141167
# Use sck_in if available (separate clock for mic), otherwise fall back to sck
142168
sck_pin = self.i2s_pins.get('sck_in', self.i2s_pins.get('sck'))
143169
print(f"RecordStream: Initializing I2S RX with sck={sck_pin}, ws={self.i2s_pins['ws']}, sd={self.i2s_pins['sd_in']}")
@@ -246,9 +272,20 @@ def record(self):
246272

247273
finally:
248274
self._is_recording = False
275+
if self.on_close:
276+
try:
277+
self.on_close()
278+
except Exception as e:
279+
print(f"RecordStream: on_close failed: {e}")
249280
if self._i2s:
250281
self._i2s.deinit()
251282
self._i2s = None
283+
if self._mck_pwm:
284+
try:
285+
self._mck_pwm.deinit()
286+
except Exception:
287+
pass
288+
self._mck_pwm = None
252289
print(f"RecordStream: Recording thread finished")
253290

254291
def get_duration_ms(self):

internal_filesystem/lib/mpos/audio/stream_wav.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def __init__(
177177
i2s_pins,
178178
on_complete,
179179
requested_sample_rate=None,
180+
on_open=None,
181+
on_close=None,
180182
):
181183
"""
182184
Initialize WAV stream.
@@ -188,13 +190,17 @@ def __init__(
188190
i2s_pins: Dict with 'sck', 'ws', 'sd' pin numbers
189191
on_complete: Callback function(message) when playback finishes
190192
requested_sample_rate: Optional negotiated sample rate for shared clocks
193+
on_open: Optional callable invoked after MCLK starts, before I2S init
194+
on_close: Optional callable invoked before I2S deinit (after audio drains)
191195
"""
192196
self.file_path = file_path
193197
self.stream_type = stream_type
194198
self.volume = volume
195199
self.i2s_pins = i2s_pins
196200
self.on_complete = on_complete
197201
self.requested_sample_rate = requested_sample_rate
202+
self.on_open = on_open
203+
self.on_close = on_close
198204
self._keep_running = True
199205
self._is_playing = False
200206
self._i2s = None
@@ -449,7 +455,7 @@ def play(self):
449455

450456
self._playback_rate = playback_rate
451457
# ibuf = playback_rate # doesnt account for stereo vs mono...
452-
ibuf = 32000
458+
ibuf = 8192
453459

454460
print(f"WAVStream: {original_rate} Hz, {bits_per_sample}-bit, {channels}-ch")
455461
print(f"WAVStream: Playback at {playback_rate} Hz (factor {upsample_factor})")
@@ -495,6 +501,13 @@ def play(self):
495501
print(f"MCLK PWM init failed: {e}")
496502
# fallback or error handling
497503

504+
# Notify codec/amp to prepare for playback (enable amp, unmute DAC, etc.)
505+
if self.on_open:
506+
try:
507+
self.on_open()
508+
except Exception as e:
509+
print(f"WAVStream: on_open failed: {e}")
510+
498511
if self.i2s_pins.get("sck"):
499512
self._i2s = machine.I2S(
500513
0,
@@ -627,6 +640,11 @@ def play(self):
627640

628641
finally:
629642
self._is_playing = False
643+
if self.on_close:
644+
try:
645+
self.on_close()
646+
except Exception as e:
647+
print(f"WAVStream: on_close failed: {e}")
630648
if self._i2s:
631649
print("Done playing, doing i2s deinit")
632650
self._i2s.deinit() # disabling this does not fix the "play just once" issue

0 commit comments

Comments
 (0)