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.

This commit is contained in:
pelgraine
2026-06-05 02:39:29 +10:00
parent 9080af8037
commit 585bf7e381
3 changed files with 302 additions and 16 deletions
@@ -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");
}
+191
View File
@@ -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);
@@ -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 <codec2.h>
@@ -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
}
}