From 8302db03db0c2da34eae13269329e3887b5eca53 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Mon, 1 Jun 2026 13:26:05 -0400 Subject: [PATCH] Expose speaker playback and play_tone tool via MCP --- audio_util.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ mcp_server.py | 26 +++++++++ 2 files changed, 183 insertions(+) diff --git a/audio_util.py b/audio_util.py index f7073a9..aa9fdaf 100644 --- a/audio_util.py +++ b/audio_util.py @@ -126,3 +126,160 @@ def record_audio(duration_seconds=5, filename='recording.pcm'): # Always release the I2S peripheral resources i2s.deinit() print("I2S receiver deinitialized.") + + +class ES8311: + """MicroPython driver for the ES8311 Audio Codec (Speaker DAC). + + Controls the ES8311 chip over I2C to configure clocks, audio format, and volume. + """ + ADDR = 0x18 + + def __init__(self, i2c): + self.i2c = i2c + + def init(self, sample_rate=16000): + """Initializes the ES8311 registers for audio playback. + + Args: + sample_rate (int): Audio sample rate (typically 16000). + + Returns: + bool: True if initialization was successful, False otherwise. + """ + print("Initializing ES8311 Speaker DAC...") + try: + # 1. Reset the chip + self._write(0x00, 0x1F) + time.sleep_ms(10) + self._write(0x00, 0x00) + time.sleep_ms(10) + + # Clock Configuration (16kHz sample rate, MCLK=12.288MHz) + self._write(0x01, 0x3F) # Enable all clocks, use MCLK pin + self._write(0x02, 0x48) # pre_div=3, pre_mult=1 + self._write(0x03, 0x10) # fs_mode=0, adc_osr=16 + self._write(0x04, 0x20) # dac_osr=32 + self._write(0x05, 0x00) # adc_div=1, dac_div=1 + self._write(0x06, 0x03) # bclk_div=4 (4-1=3) + self._write(0x07, 0x00) # lrck_h=0 + self._write(0x08, 0xFF) # lrck_l=255 + + # Audio Format Configuration (I2S standard format, 16-bit) + self._write(0x09, 0x0C) # SDP in: 16-bit I2S + self._write(0x0A, 0x0C) # SDP out: 16-bit I2S + + # System / DAC Power Up + self._write(0x0D, 0x01) # Power up analog circuitry + self._write(0x0E, 0x02) # Enable analog PGA, enable ADC modulator + self._write(0x12, 0x00) # Power up DAC + self._write(0x13, 0x10) # Enable output to HP drive (speaker/hp output) + self._write(0x1C, 0x6A) # ADC Equalizer bypass + self._write(0x37, 0x08) # Bypass DAC equalizer + + # Set Volume (0xBF = 0dB) + self._write(0x32, 0xBF) + + # Unmute DAC + self._write(0x31, 0x00) + + # Power On + self._write(0x00, 0x80) + + print("ES8311 initialization complete.") + return True + except Exception as e: + print(f"Failed to initialize ES8311: {e}") + return False + + def set_volume(self, val): + """Sets DAC digital volume (0-100 scale).""" + # Volume register 0x32 accepts values from 0 (mute) to 255 (+0dB / max volume). + reg_val = int((val / 100.0) * 255.0) + reg_val = max(0, min(255, reg_val)) + try: + self._write(0x32, reg_val) + except Exception as e: + print(f"Failed to set volume: {e}") + + def _write(self, reg, val): + self.i2c.writeto_mem(self.ADDR, reg, bytes([val])) + + +def play_tone(frequency=440, duration_ms=1000, volume=50): + """Plays a pure sine wave tone on the board speaker. + + Args: + frequency (int): Tone frequency in Hz (e.g. 440 for A4). + duration_ms (int): Tone duration in milliseconds. + volume (int): Volume level from 0 to 100. + """ + import math + import struct + + print(f"Playing tone: {frequency}Hz for {duration_ms}ms (vol={volume})...") + + # 1. Setup Master Clock (MCLK) on GPIO 16 using PWM + mclk_pin = Pin(16, Pin.OUT) + mclk_pwm = machine.PWM(mclk_pin) + mclk_pwm.freq(12288000) + mclk_pwm.duty_u16(32768) + + # 2. Start I2C Control Bus (SDA=13, SCL=14) + i2c = I2C(0, sda=Pin(13), scl=Pin(14)) + + # 3. Initialize the ES8311 DAC + dac = ES8311(i2c) + if not dac.init(sample_rate=16000): + mclk_pwm.deinit() + return False + + dac.set_volume(volume) + + # 4. Configure I2S TX + # Pins: sck=BCLK (GPIO 9), ws=WS/LRCK (GPIO 45), sd=DOUT (GPIO 8) + i2s = I2S(1, + sck=Pin(9), + ws=Pin(45), + sd=Pin(8), + mode=I2S.TX, + ibuf=8000, + rate=16000, + bits=16, + format=I2S.STEREO) + + # 5. Enable Speaker Amplifier (GPIO 46) + amp_pin = Pin(46, Pin.OUT, value=1) + + # 6. Generate sine wave cycle + # Approximate frequency to make integer number of samples per cycle + # (avoiding phase clicking) + N = int(16000 / frequency) + N = max(4, N) # prevent division by zero or extremely high frequencies + + volume_scale = int((volume / 100.0) * 32767) + + cycle_data = bytearray() + for i in range(N): + val = int(volume_scale * math.sin(2 * math.pi * i / N)) + cycle_data.extend(struct.pack("