211 lines
6.7 KiB
Python
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()
|
|
|