From 585bf7e381e178d708f99419a6e62782669e002d Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 5 Jun 2026 02:39:29 +1000 Subject: [PATCH] Fixed audiobookplayer crowded footer text and filtering out m4b files for now until we get that sorted. Fixed voice message recording playback for the max. --- .../ui-new/Audiobookplayerscreen.h | 21 +- examples/companion_radio/ui-new/ES8311.h | 191 ++++++++++++++++++ .../ui-new/Voicemessagescreen.h | 106 +++++++++- 3 files changed, 302 insertions(+), 16 deletions(-) diff --git a/examples/companion_radio/ui-new/Audiobookplayerscreen.h b/examples/companion_radio/ui-new/Audiobookplayerscreen.h index 7dbfc8c2..ef196ae3 100644 --- a/examples/companion_radio/ui-new/Audiobookplayerscreen.h +++ b/examples/companion_radio/ui-new/Audiobookplayerscreen.h @@ -73,7 +73,9 @@ void meck_audio_codec_init(); static bool isAudiobookFile(const String& name) { String lower = name; lower.toLowerCase(); - return lower.endsWith(".m4b") || lower.endsWith(".m4a") || + // .m4b excluded: these can't currently be played, so they are hidden from + // the file explorer. (.m4a/.mp3/.wav remain listed.) + return lower.endsWith(".m4a") || lower.endsWith(".mp3") || lower.endsWith(".wav"); } @@ -1361,21 +1363,10 @@ private: } // Footer (stays at size 1 for readability) + // Left label is always the file count — the folder path was overflowing and + // colliding with the right-hand nav text, so it is no longer shown here. char leftBuf[32]; - if (_currentPath == String(AUDIOBOOKS_FOLDER)) { - snprintf(leftBuf, sizeof(leftBuf), "%d files", (int)_fileList.size()); - } else { - // Show current subfolder name - int lastSlash = _currentPath.lastIndexOf('/'); - String folderName = (lastSlash >= 0) ? _currentPath.substring(lastSlash + 1) : _currentPath; - snprintf(leftBuf, sizeof(leftBuf), "/%s", folderName.c_str()); - if ((int)strlen(leftBuf) > 16) { - leftBuf[13] = '.'; - leftBuf[14] = '.'; - leftBuf[15] = '.'; - leftBuf[16] = '\0'; - } - } + snprintf(leftBuf, sizeof(leftBuf), "%d files", (int)_fileList.size()); drawFooter(display, leftBuf, "W/S:Nav Enter:Open"); } diff --git a/examples/companion_radio/ui-new/ES8311.h b/examples/companion_radio/ui-new/ES8311.h index 3c580221..e8e13386 100644 --- a/examples/companion_radio/ui-new/ES8311.h +++ b/examples/companion_radio/ui-new/ES8311.h @@ -256,6 +256,197 @@ static inline bool es8311_init_44100_16bit() { return id == 0x83; } +// ============================================================================= +// es8311_init_capture_16k() -- ANALOGUE-MIC CAPTURE (ADC) init, 16 kHz, 16-bit +// +// Recording counterpart to es8311_init_44100_16bit(). Brings up the ES8311 ADC +// path (mic -> ADC -> I2S SDOUT/ASDOUT) so the ESP32 I2S RX can read PCM. It +// does NOT power the DAC; call es8311_init_44100_16bit() afterwards to restore +// the playback path. +// +// Same authority as the DAC init: transcribed from esp_codec_dev es8311.c +// (es8311_open + es8311_set_bits_per_sample(16) + es8311_config_fmt(I2S) + +// es8311_config_sample(16000) + es8311_start) for the configuration: +// master_mode = false (ESP32 I2S is master, drives MCLK/BCLK/LRCK) +// use_mclk = true (real MCLK on BOARD_I2S_MCLK / GPIO38, 256 * fs) +// invert_mclk = false, invert_sclk = false +// codec_mode = ADC, digital_mic = false (analogue mic), no_dac_ref = false +// mclk_div = 256 -> MCLK = 16000 * 256 = 4.096 MHz +// using the {4096000, 16000} coefficient row: +// pre_div=1 pre_multi=1 adc_div=1 dac_div=1 fs_mode=0 +// lrck_h=0x00 lrck_l=0xFF bclk_div=0x04 adc_osr=0x10 dac_osr=0x20 +// +// mic_gain -> REG16 ADC PGA gain (ES8311 mic-gain enum, low 3 bits): +// 0=0dB 1=6dB 2=12dB 3=18dB 4=24dB 5=30dB 6=36dB 7=42dB +// 30 dB (5) is a reasonable starting point for the MAX analogue mic; this is +// the first knob to tune on the bench if recordings are too quiet or clip. +// +// Returns true if the chip ID read back as 0x83. +// ============================================================================= +static inline bool es8311_init_capture_16k(uint8_t mic_gain = 0x05) { + uint8_t id = es8311_read(ES8311_REGFD_CHIPID1); + Serial.printf("[ES8311] (capture) chip ID REGFD = 0x%02X (expect 0x83)\n", id); + + // ---- open(): base register setup (from es8311_open) ---- + es8311_write(ES8311_REG44_GPIO, 0x08); // double write: first I2C write can be unreliable + es8311_write(ES8311_REG44_GPIO, 0x08); + + es8311_write(ES8311_REG01_CLK_MANAGER, 0x30); + es8311_write(ES8311_REG02_CLK_MANAGER, 0x00); + es8311_write(ES8311_REG03_CLK_MANAGER, 0x10); + es8311_write(ES8311_REG16_ADC, 0x24); + es8311_write(ES8311_REG04_CLK_MANAGER, 0x10); + es8311_write(ES8311_REG05_CLK_MANAGER, 0x00); + es8311_write(ES8311_REG0B_SYSTEM, 0x00); + es8311_write(ES8311_REG0C_SYSTEM, 0x00); + es8311_write(ES8311_REG10_SYSTEM, 0x1F); + es8311_write(ES8311_REG11_SYSTEM, 0x7F); + es8311_write(ES8311_REG00_RESET, 0x80); // CSM power up + + // Slave mode: REG00 bit6 = 0 (read-modify-write) + { + uint8_t regv = es8311_read(ES8311_REG00_RESET); + regv &= 0xBF; + es8311_write(ES8311_REG00_RESET, regv); + } + + // REG01 clock source: use_mclk=true (&=0x7F), not inverted (&=~0x40) => 0x3F + { + uint8_t regv = 0x3F; + regv &= 0x7F; + regv &= ~0x40; + es8311_write(ES8311_REG01_CLK_MANAGER, regv); + } + + // SCLK not inverted (REG06) + { + uint8_t regv = es8311_read(ES8311_REG06_CLK_MANAGER); + regv &= ~0x20; + es8311_write(ES8311_REG06_CLK_MANAGER, regv); + } + + es8311_write(ES8311_REG13_SYSTEM, 0x10); + es8311_write(ES8311_REG1B_ADC, 0x0A); + es8311_write(ES8311_REG1C_ADC, 0x6A); + // no_dac_ref == false: internal reference signal (ADCL + DACR) -> REG44 = 0x58 + es8311_write(ES8311_REG44_GPIO, 0x58); + + // ---- set_bits_per_sample(16): 16-bit on both SDP regs (REG09 DAC, REG0A ADC) ---- + { + uint8_t dac_iface = es8311_read(ES8311_REG09_SDPIN); + uint8_t adc_iface = es8311_read(ES8311_REG0A_SDPOUT); + dac_iface |= 0x0C; + adc_iface |= 0x0C; + es8311_write(ES8311_REG09_SDPIN, dac_iface); + es8311_write(ES8311_REG0A_SDPOUT, adc_iface); + } + + // ---- config_fmt(ES_I2S_NORMAL): clear format bits [1:0] on REG09/REG0A ---- + { + uint8_t dac_iface = es8311_read(ES8311_REG09_SDPIN); + uint8_t adc_iface = es8311_read(ES8311_REG0A_SDPOUT); + dac_iface &= 0xFC; + adc_iface &= 0xFC; + es8311_write(ES8311_REG09_SDPIN, dac_iface); + es8311_write(ES8311_REG0A_SDPOUT, adc_iface); + } + + // ---- config_sample(16000): {4096000,16000} coeff row, write order from es8311.c ---- + { + const uint8_t pre_div = 1, datmp = 0; // pre_multi=1, use_mclk=true -> datmp=0 + const uint8_t adc_div = 1, dac_div = 1, fs_mode = 0; + const uint8_t lrck_h = 0x00, lrck_l = 0xFF, bclk_div = 0x04; + const uint8_t adc_osr = 0x10, dac_osr = 0x20; + + uint8_t regv = es8311_read(ES8311_REG02_CLK_MANAGER); + regv &= 0x07; + regv |= (pre_div - 1) << 5; + regv |= datmp << 3; + es8311_write(ES8311_REG02_CLK_MANAGER, regv); + + regv = ((adc_div - 1) << 4) | (dac_div - 1); + es8311_write(ES8311_REG05_CLK_MANAGER, regv); + + regv = es8311_read(ES8311_REG03_CLK_MANAGER); + regv &= 0x80; + regv |= (fs_mode << 6) | adc_osr; + es8311_write(ES8311_REG03_CLK_MANAGER, regv); + + regv = es8311_read(ES8311_REG04_CLK_MANAGER); + regv &= 0x80; + regv |= dac_osr; + es8311_write(ES8311_REG04_CLK_MANAGER, regv); + + regv = es8311_read(ES8311_REG07_CLK_MANAGER); + regv &= 0xC0; + regv |= lrck_h; + es8311_write(ES8311_REG07_CLK_MANAGER, regv); + + es8311_write(ES8311_REG08_CLK_MANAGER, lrck_l); + + regv = es8311_read(ES8311_REG06_CLK_MANAGER); + regv &= 0xE0; + if (bclk_div < 19) regv |= (bclk_div - 1); + else regv |= bclk_div; + es8311_write(ES8311_REG06_CLK_MANAGER, regv); + } + + // ---- start(): ADC-mode power-up (from es8311_start, codec_mode == ADC) ---- + { + uint8_t regv = 0x80; + regv &= 0xBF; // slave + es8311_write(ES8311_REG00_RESET, regv); + + uint8_t r01 = 0x3F; + r01 &= 0x7F; // use_mclk = true + r01 &= ~0x40; // not inverted + es8311_write(ES8311_REG01_CLK_MANAGER, r01); + + uint8_t dac_iface = es8311_read(ES8311_REG09_SDPIN); + uint8_t adc_iface = es8311_read(ES8311_REG0A_SDPOUT); + dac_iface &= 0xBF; + adc_iface &= 0xBF; + adc_iface &= ~(1 << 6); // ADC mode: power up ADC serial-port output + es8311_write(ES8311_REG09_SDPIN, dac_iface); + es8311_write(ES8311_REG0A_SDPOUT, adc_iface); + + es8311_write(ES8311_REG17_ADC, 0xBF); // ADC full-scale / volume + es8311_write(ES8311_REG0E_SYSTEM, 0x02); + // REG12 (DAC enable) intentionally NOT written -- this is the ADC-only path. + es8311_write(ES8311_REG14_SYSTEM, 0x1A); + + // digital_mic == false: clear DMIC-enable bit (0x40) on REG14 (analogue mic) + { + uint8_t r14 = es8311_read(ES8311_REG14_SYSTEM); + r14 &= ~0x40; + es8311_write(ES8311_REG14_SYSTEM, r14); + } + + es8311_write(ES8311_REG0D_SYSTEM, 0x01); + es8311_write(ES8311_REG15_ADC, 0x40); + es8311_write(ES8311_REG37_DAC, 0x08); + es8311_write(ES8311_REG45_GP, 0x00); + } + + // ADC PGA / mic gain (REG16). Overwrites the open() default (0x24), mirroring + // esp_codec_dev es8311_set_mic_gain which writes the raw enum (0..7) to REG16. + es8311_write(ES8311_REG16_ADC, (uint8_t)(mic_gain & 0x07)); + + // DIAG: read back the key ADC/clock/format registers to confirm the writes. + Serial.printf("[ES8311] (capture) readback R00=%02X R01=%02X R02=%02X R09=%02X R0A=%02X R14=%02X R15=%02X R16=%02X R17=%02X\n", + es8311_read(ES8311_REG00_RESET), + es8311_read(ES8311_REG01_CLK_MANAGER), + es8311_read(ES8311_REG02_CLK_MANAGER), + es8311_read(ES8311_REG09_SDPIN), + es8311_read(ES8311_REG0A_SDPOUT), + es8311_read(ES8311_REG14_SYSTEM), + es8311_read(ES8311_REG15_ADC), + es8311_read(ES8311_REG16_ADC), + es8311_read(ES8311_REG17_ADC)); + + return id == 0x83; +} + // DAC volume, 0..255 (0 = mute-ish, 191 = 0 dB, 255 = +32 dB). static inline void es8311_set_dac_volume(uint8_t vol) { es8311_write(ES8311_REG32_DAC, vol); diff --git a/examples/companion_radio/ui-new/Voicemessagescreen.h b/examples/companion_radio/ui-new/Voicemessagescreen.h index ed68e848..2e194012 100644 --- a/examples/companion_radio/ui-new/Voicemessagescreen.h +++ b/examples/companion_radio/ui-new/Voicemessagescreen.h @@ -32,6 +32,12 @@ #include "Audio.h" #include "variant.h" +// MAX (ES8311) capture path: the codec must be configured for ADC over I2C. +// ES8311.h self-guards on HAS_ES8311_AUDIO, so this is a no-op on the Pro V1.1. +#if defined(HAS_ES8311_AUDIO) +#include "ES8311.h" +#endif + // Codec2 low-bitrate voice codec #include @@ -79,6 +85,12 @@ class MyMesh; // stop audio before recording, uninstall driver after, let audio lib reclaim. #define VOICE_I2S_PORT I2S_NUM_0 +// Default DAC sample rate the rest of the firmware (audiobooks, tones) runs at. +// On the MAX, ESP32-audioI2S leaves the hardware I2S clock fixed at this rate +// (its setSampleRate is a no-op under HAS_ES8311_AUDIO), so voice playback has +// to switch the hardware to VOICE_SAMPLE_RATE and switch it back afterwards. +#define VOICE_PLAYBACK_DEFAULT_RATE 44100 + // DMA buffer config for mic capture // E-ink refreshes block the CPU for ~650ms. At 16kHz, that's 10,400 samples. // We need enough DMA buffer to hold audio during those blocks. @@ -258,6 +270,53 @@ private: _i2sInitialized = false; i2s_config_t mic_cfg = {}; +#if defined(HAS_ES8311_AUDIO) + // ---- MAX: standard I2S master RX, MCLK driven, ES8311 ADC as the source ---- + // The MAX has no PDM mic. Audio is captured by the ES8311's ADC, which sits + // on the standard I2S bus in SLAVE mode (ESP32 is master and drives + // MCLK=IO38 / BCLK=IO39 / WS=IO18; mic data returns on ASDOUT=IO17). + // APLL + fixed_mclk forces MCLK = 256 * fs (4.096 MHz at 16 kHz), which the + // ES8311 needs for its ADC clock derivation. + mic_cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX); + mic_cfg.sample_rate = VOICE_SAMPLE_RATE; + mic_cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; + mic_cfg.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; + mic_cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S; + mic_cfg.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; + mic_cfg.dma_buf_count = VOICE_DMA_BUF_COUNT; + mic_cfg.dma_buf_len = VOICE_DMA_BUF_LEN; + mic_cfg.use_apll = true; // clean MCLK for the ES8311 + mic_cfg.tx_desc_auto_clear = false; + mic_cfg.fixed_mclk = VOICE_SAMPLE_RATE * 256; // 4.096 MHz = 256 * fs + + esp_err_t err = i2s_driver_install(VOICE_I2S_PORT, &mic_cfg, 0, NULL); + if (err != ESP_OK) { + Serial.printf("Voice: i2s_driver_install failed: %d\n", err); + return false; + } + + i2s_pin_config_t mic_pins = {}; + mic_pins.mck_io_num = BOARD_ES8311_MCLK; // IO38 — drive MCLK to codec + mic_pins.bck_io_num = BOARD_ES8311_SCLK; // IO39 — BCLK + mic_pins.ws_io_num = BOARD_ES8311_LRCK; // IO18 — LRCK/WS + mic_pins.data_out_num = I2S_PIN_NO_CHANGE; + mic_pins.data_in_num = BOARD_MIC_I2S_DIN; // IO17 — ASDOUT (mic in) + + err = i2s_set_pin(VOICE_I2S_PORT, &mic_pins); + if (err != ESP_OK) { + Serial.printf("Voice: i2s_set_pin failed: %d\n", err); + i2s_driver_uninstall(VOICE_I2S_PORT); + return false; + } + + // MCLK is now live on IO38; bring up the ES8311 ADC capture path over I2C. + delay(10); + es8311_init_capture_16k(); + + _micInitialized = true; + Serial.println("Voice: ES8311 ADC mic initialised (16k I2S RX on I2S_NUM_0)"); + return true; +#else mic_cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); mic_cfg.sample_rate = VOICE_SAMPLE_RATE; mic_cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; @@ -292,15 +351,25 @@ private: _micInitialized = true; Serial.println("Voice: PDM mic initialised on I2S_NUM_0"); return true; +#endif } void deinitMic() { if (!_micInitialized) return; i2s_driver_uninstall(VOICE_I2S_PORT); _micInitialized = false; +#if defined(HAS_ES8311_AUDIO) + // Recording reconfigured the ES8311 for ADC capture. Restore the codec to + // its playback (DAC) state by re-running the same init used at audio start, + // so subsequent voice/audiobook playback works. ESP32-audioI2S only manages + // the I2S stream; it does not touch the codec's registers. + es8311_init_44100_16bit(); + Serial.println("Voice: ES8311 mic deinitialised, codec restored to DAC, I2S_NUM_0 released"); +#else // _i2sInitialized already cleared in initMic() — ESP32-audioI2S // will reconfigure I2S_NUM_0 on next connecttoFS() call. Serial.println("Voice: PDM mic deinitialised, I2S_NUM_0 released"); +#endif } // Allocate PSRAM capture buffer (once, reused across recordings) @@ -584,9 +653,23 @@ private: Serial.println("Voice: Recreating Audio object for clean I2S state"); delete _audio; _audio = new Audio(); +#if defined(HAS_ES8311_AUDIO) + // MAX: the ES8311 runs as I2S slave and needs a real MCLK on BOARD_I2S_MCLK + // (IO38). Use the 5-arg setPinout (4th = DIN, unused; 5th = MCLK) exactly + // as the boot/notification audio bring-up in main.cpp does. The 4-arg form + // used on the Pro leaves MCLK unset, which silences the ES8311 DAC — this + // is why a just-recorded note played silent while received notes (which + // never recreate the Audio object) play fine. + bool ok = _audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, + I2S_PIN_NO_CHANGE, BOARD_I2S_MCLK); + Serial.printf("Voice: DAC setPinout(BCLK=%d LRC=%d DOUT=%d MCK=%d) -> %s\n", + BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, BOARD_I2S_MCLK, + ok ? "OK" : "FAIL"); +#else bool ok = _audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, 0); if (!ok) ok = _audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT); if (!ok) Serial.println("Voice: DAC setPinout FAILED"); +#endif _i2sInitialized = true; } @@ -594,6 +677,14 @@ private: snprintf(fullPath, sizeof(fullPath), "%s/%s", VOICE_FOLDER, filename); _audio->setVolume(21); // Max volume for voice playback +#if defined(HAS_ES8311_AUDIO) + // ESP32-audioI2S's setSampleRate() is a no-op on the MAX (it deliberately + // skips i2s_set_sample_rates to avoid disrupting the APLL MCLK mid-stream), + // so the hardware I2S stays at VOICE_PLAYBACK_DEFAULT_RATE. A 16 kHz voice + // WAV would then clock out ~2.76x too fast (chipmunk). Force the hardware to + // the voice rate here; restored on playback end so audiobooks are unaffected. + i2s_set_sample_rates((i2s_port_t)VOICE_I2S_PORT, VOICE_SAMPLE_RATE); +#endif bool ok = _audio->connecttoFS(SD, fullPath); if (!ok) { Serial.printf("Voice: Failed to open %s for playback\n", fullPath); @@ -608,6 +699,11 @@ private: if (_audio && _i2sInitialized) { _audio->stopSong(); } +#if defined(HAS_ES8311_AUDIO) + // Restore the hardware I2S clock to the default rate so audiobook/tone + // playback (which the library won't re-rate on this platform) is correct. + i2s_set_sample_rates((i2s_port_t)VOICE_I2S_PORT, VOICE_PLAYBACK_DEFAULT_RATE); +#endif _reviewPlaying = false; _listPlaying = false; } @@ -986,7 +1082,10 @@ private: char countStr[16]; snprintf(countStr, sizeof(countStr), "%d files", (int)_fileList.size()); - display.setCursor(display.width() - display.getTextWidth(countStr) - 2, 0); + // Place the count just after the title rather than right-aligned to the + // screen edge. The right-aligned position pushed the trailing 's' of + // "files" off the right edge, where it wrapped onto the next line. + display.setCursor(display.getTextWidth("Voice Messages") + 6, 0); display.print(countStr); display.fillRect(0, 11, display.width(), 1); // horizontal rule @@ -1397,6 +1496,11 @@ public: _reviewPlaying = false; _listPlaying = false; _playbackJustFinished = true; +#if defined(HAS_ES8311_AUDIO) + // Restore the hardware I2S clock to the default rate (see playFile) so the + // next audiobook/tone plays at the correct speed. + i2s_set_sample_rates((i2s_port_t)VOICE_I2S_PORT, VOICE_PLAYBACK_DEFAULT_RATE); +#endif } }