Migrate MCP and utilities to CircuitPython, including color GIF and volume fixes

This commit is contained in:
Adolfo Reyna
2026-06-22 10:13:10 -04:00
parent 2863f21459
commit 8f871e499e
53 changed files with 4239 additions and 108 deletions
+90 -10
View File
@@ -5,13 +5,17 @@ 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
@@ -63,20 +67,29 @@ class ES8311:
def set_volume(self, volume):
volume = max(0, min(100, int(volume)))
self._write(0x32, int(volume * 255 / 100))
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):
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=False)
self._amp.switch_to_output(value=not amp_active_level)
self.codec = ES8311(i2c)
def start_mclk(self):
@@ -88,7 +101,7 @@ class BoardAudio:
frequency=12288000,
duty_cycle=32768,
variable_frequency=False,
)
)
def stop_mclk(self):
if self._mclk is not None:
@@ -106,25 +119,92 @@ class BoardAudio:
try:
f = open(filename, "rb")
decoder = audiomp3.MP3Decoder(f)
# Use decoder-derived rate if available, else fall back.
sample_rate = getattr(decoder, "sample_rate", 44100)
audio = audiobusio.I2SOut(
bit_clock=self.bit_clock_pin,
word_select=self.word_select_pin,
data=self.data_pin,
)
self._amp.value = True
self._amp.value = self.amp_active_level
audio.play(decoder)
while audio.playing:
time.sleep(0.05)
return True
finally:
self._amp.value = False
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()
# Leave MCLK running for repeated playback; call stop_mclk() before
# deep sleep if power matters.
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()