Skip to content

Commit 98145c6

Browse files
CopilotRohansi
andauthored
Fix audio pops, end-of-playback delay, and idle speaker ring on Freenove board
Agent-Logs-Url: https://github.com/Rohansi/MicroPythonOS/sessions/243a7904-8df1-4ea2-889a-32d216524054 Co-authored-by: Rohansi <[email protected]>
1 parent 39f4001 commit 98145c6

5 files changed

Lines changed: 94 additions & 5 deletions

File tree

internal_filesystem/lib/drivers/codec/es8311.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def const(x): return x
4646
_REG14_MIC = const(0x14) # microphone: DMIC select, analog PGA gain
4747
_REG17_ADC_VOL = const(0x17) # ADC volume / gain
4848
_REG1C_ADC_EQ = const(0x1C) # ADC equalizer bypass + DC-offset cancel
49+
_REG31_DAC_MUTE = const(0x31) # DAC soft-mute control (bits[6:5] = 11 → muted)
4950
_REG32_DAC_VOL = const(0x32) # DAC output volume (0x00=muted, 0xFF=max)
5051
_REG37_DAC_EQ = const(0x37) # DAC equalizer / ramp-rate control
5152

@@ -123,8 +124,28 @@ def _init(self):
123124
self._wr(_REG32_DAC_VOL, _DEFAULT_VOL_REG) # set output volume (~85%)
124125
self._wr(_REG37_DAC_EQ, 0x08) # bypass DAC equalizer
125126

127+
# Soft-mute the DAC at boot — unmuted by on_open callback when playback starts
128+
self.dac_mute(True)
129+
126130
print("ES8311: codec initialised")
127131

132+
def dac_mute(self, mute=True):
133+
"""
134+
Soft-mute or unmute the DAC output.
135+
136+
Uses the ES8311's built-in ramp so the transition is pop-free.
137+
Does not affect the DAC power state or volume register.
138+
139+
Args:
140+
mute: True to mute, False to unmute
141+
"""
142+
val = self._rd(_REG31_DAC_MUTE)
143+
if mute:
144+
val |= 0x60 # bits[6:5] = 11 → soft mute on
145+
else:
146+
val &= ~0x60 # bits[6:5] = 00 → soft mute off
147+
self._wr(_REG31_DAC_MUTE, val)
148+
128149
def set_dac_volume(self, percent):
129150
"""
130151
Set DAC (speaker) volume.

internal_filesystem/lib/mpos/audio/audiomanager.py

Lines changed: 12 additions & 0 deletions
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:
@@ -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: 18 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,12 +38,16 @@ 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
@@ -152,6 +157,13 @@ def record(self):
152157
except Exception as e:
153158
print(f"RecordStream: MCLK PWM init failed: {e}")
154159

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+
155167
# Use sck_in if available (separate clock for mic), otherwise fall back to sck
156168
sck_pin = self.i2s_pins.get('sck_in', self.i2s_pins.get('sck'))
157169
print(f"RecordStream: Initializing I2S RX with sck={sck_pin}, ws={self.i2s_pins['ws']}, sd={self.i2s_pins['sd_in']}")
@@ -260,6 +272,11 @@ def record(self):
260272

261273
finally:
262274
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}")
263280
if self._i2s:
264281
self._i2s.deinit()
265282
self._i2s = None

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

internal_filesystem/lib/mpos/board/freenove_esp32s3_display.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,14 +225,33 @@ def adc_to_voltage(adc_millivolts):
225225

226226
# Initialise the ES8311 codec over the shared I2C bus.
227227
# machine_i2c is already open on SDA=16, SCL=15 from the touch init above.
228+
_es8311 = None
228229
try:
229230
import drivers.codec.es8311 as es8311_drv
230-
es8311_drv.ES8311(machine_i2c)
231+
_es8311 = es8311_drv.ES8311(machine_i2c)
231232
except Exception as e:
232233
print(f"ES8311 init failed: {e}")
233234

234-
# Enable the FM8002E speaker amplifier (GPIO1 LOW = enabled).
235-
amp_enable = Pin(1, Pin.OUT, value=0) # LOW = FM8002E amplifier enabled
235+
# FM8002E speaker amplifier enable pin (GPIO1: LOW=enabled, HIGH=disabled).
236+
# Start disabled at boot — enabled only around active playback to prevent ring noise.
237+
_amp_enable = Pin(1, Pin.OUT, value=1) # HIGH = FM8002E amplifier disabled
238+
239+
240+
def _audio_on_open():
241+
"""Called after MCLK starts and before I2S init. Enables amp and unmutes DAC."""
242+
_amp_enable.value(0) # LOW = enable FM8002E amplifier
243+
if _es8311:
244+
time.sleep_ms(10) # let amp rail settle before unmuting
245+
_es8311.dac_mute(False) # release DAC soft-mute
246+
247+
248+
def _audio_on_close():
249+
"""Called before I2S deinit. Mutes DAC then disables amp to suppress pops."""
250+
if _es8311:
251+
_es8311.dac_mute(True) # soft-mute DAC first (ramp prevents click)
252+
time.sleep_ms(20) # wait for ramp to complete
253+
_amp_enable.value(1) # HIGH = disable FM8002E amplifier
254+
236255

237256
# Register I2S audio devices with AudioManager.
238257
# Both output and input share MCLK (GPIO4), BCLK (GPIO5), and WS (GPIO7).
@@ -250,6 +269,8 @@ def adc_to_voltage(adc_millivolts):
250269
'ws': 7, # LRCK
251270
'sd': 8, # I2S TX (ESP32 → ES8311 DAC)
252271
},
272+
on_open=_audio_on_open,
273+
on_close=_audio_on_close,
253274
)
254275
)
255276

0 commit comments

Comments
 (0)