Files

211 lines
6.7 KiB
Python

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