"""CircuitPython audio helpers for Waveshare ESP32-S3-RLCD-4.2. Ports the ES8311 setup sequence from MicroPython audio_util.py and adds MP3 playback using CircuitPython's audiomp3 + audiobusio stack. """ import time import array import math import audiobusio import audiomp3 import audiocore import digitalio import pwmio class ES8311: ADDR = 0x18 def __init__(self, i2c): self.i2c = i2c def _write(self, register, value): # CircuitPython I2CDevice is not used to keep this dependency-free. while not self.i2c.try_lock(): pass try: self.i2c.writeto(self.ADDR, bytes([register & 0xFF, value & 0xFF])) finally: self.i2c.unlock() def init(self, sample_rate=44100): """Initialize ES8311 for I2S DAC playback. The register sequence is the existing MicroPython project's known-good sequence. The original comments targeted 16 kHz WAV playback, but the I2S peripheral is configured with the MP3 decoder sample rate at play time. If MP3 output is silent, hardware-test this sequence first with a 16 kHz WAV/tone and then tune codec dividers for the MP3 sample rate. """ del sample_rate # Kept for API clarity; sequence currently fixed. self._write(0x00, 0x1F) time.sleep(0.01) self._write(0x00, 0x00) time.sleep(0.01) self._write(0x01, 0x3F) self._write(0x02, 0x48) self._write(0x03, 0x10) self._write(0x04, 0x20) self._write(0x05, 0x00) self._write(0x06, 0x03) self._write(0x07, 0x00) self._write(0x08, 0xFF) self._write(0x09, 0x0C) self._write(0x0A, 0x0C) self._write(0x0D, 0x01) self._write(0x0E, 0x02) self._write(0x12, 0x00) self._write(0x13, 0x10) self._write(0x1C, 0x6A) self._write(0x37, 0x08) self._write(0x32, 0xBF) self._write(0x31, 0x00) self._write(0x00, 0x80) def set_volume(self, volume): volume = max(0, min(100, int(volume))) if volume <= 0: reg_val = 0 elif volume <= 50: # 1..50 maps to 0..191 (0xBF = 0dB) reg_val = int((volume / 50.0) * 191) else: # 51..100 maps to 192..255 (0xFF = +32dB) reg_val = 191 + int(((volume - 50) / 50.0) * (255 - 191)) self._write(0x32, reg_val) class BoardAudio: def __init__(self, i2c, *, bit_clock, word_select, data, mclk, amp, amp_active_level=1): self.i2c = i2c self.bit_clock_pin = bit_clock self.word_select_pin = word_select self.data_pin = data self.mclk_pin = mclk self.amp_pin = amp self.amp_active_level = amp_active_level self._mclk = None self._amp = digitalio.DigitalInOut(amp) self._amp.switch_to_output(value=not amp_active_level) self.codec = ES8311(i2c) def start_mclk(self): if self._mclk is None: # External MCLK for ES8311. CircuitPython I2SOut on ESP32-S3 does # not accept a main_clock parameter, so keep this independent. self._mclk = pwmio.PWMOut( self.mclk_pin, frequency=12288000, duty_cycle=32768, variable_frequency=False, ) def stop_mclk(self): if self._mclk is not None: self._mclk.deinit() self._mclk = None def play_mp3(self, filename, volume=60): """Play an MP3 file from CIRCUITPY storage over the onboard speaker.""" self.start_mclk() self.codec.init() self.codec.set_volume(volume) decoder = None audio = None f = None try: f = open(filename, "rb") decoder = audiomp3.MP3Decoder(f) audio = audiobusio.I2SOut( bit_clock=self.bit_clock_pin, word_select=self.word_select_pin, data=self.data_pin, ) self._amp.value = self.amp_active_level audio.play(decoder) while audio.playing: time.sleep(0.05) return True finally: self._amp.value = not self.amp_active_level if audio is not None: audio.deinit() if decoder is not None: decoder.deinit() if f is not None: f.close() def play_wav(self, filename, volume=60): """Play a WAV file from CIRCUITPY storage over the onboard speaker.""" self.start_mclk() self.codec.init() self.codec.set_volume(volume) wav = None audio = None f = None try: f = open(filename, "rb") wav = audiocore.WaveFile(f) audio = audiobusio.I2SOut( bit_clock=self.bit_clock_pin, word_select=self.word_select_pin, data=self.data_pin, ) self._amp.value = self.amp_active_level audio.play(wav) while audio.playing: time.sleep(0.05) return True except Exception as e: print(f"Error playing WAV: {e}") return False finally: self._amp.value = not self.amp_active_level if audio is not None: audio.deinit() if wav is not None: wav.deinit() if f is not None: f.close() def play_tone(self, frequency=440, duration_ms=1000, volume=50): """Generate and play a pure sine wave tone on the speaker.""" self.start_mclk() self.codec.init() self.codec.set_volume(volume) sample_rate = 16000 length = int(sample_rate / frequency) if length < 4: length = 4 sine_wave = array.array("h", [0] * length) for i in range(length): sine_wave[i] = int(math.sin(2 * math.pi * i / length) * 32767) sample = audiocore.RawSample(sine_wave, sample_rate=sample_rate) audio = None try: audio = audiobusio.I2SOut( bit_clock=self.bit_clock_pin, word_select=self.word_select_pin, data=self.data_pin, ) self._amp.value = self.amp_active_level audio.play(sample, loop=True) time.sleep(duration_ms / 1000.0) audio.stop() return True except Exception as e: print(f"Error playing tone: {e}") return False finally: self._amp.value = not self.amp_active_level if audio is not None: audio.deinit() sample.deinit()