AI 语音与工具 / 离线语音发布 内容更新

语音前端AFE做什么?先把降噪和VAD结果看见

用Korvo-2 V3.0采MIC1,独立保存RAW/AFE十秒WAV与VAD索引;明确单M、无AEC、软件提交与声学效果的边界。

读完能做什么
得到完整采样/AFE/保存工程和可执行安静/噪声对照方法;不冒称降噪实测。
开始之前
能按版本激活IDF、完整刷应用和模型分区,识别板修订与供电口;先完成所列前置。
所需器材
官方ESP32-S3-Korvo-2主板V3.0,先核WROOM-1-N16R8丝印和板载ES7210/两模拟麦克风、USB-to-UART数据线、独立USB Power稳压5V/2A适配器与线;不接电池/LCD/摄像头/扬声器、4–32GB FAT32 SDHC与读卡器,先备份再关电插拔
版本组合
ESP-IDF / Python:5.5.5 / 3.12.13(实际作者环境)ESP-SR:2.4.7 / 2f8c4b0459db5bbb39abd77adae27962d6d94bcbcodec / DSP / FFT / cjson:1.5.11 / 1.8.0 / 0.8.0 / 1.7.19~2输入:16kHz stereo PCM16;取MIC1为M;无R/AEC模型条目:无神经模型,4B空包;WebRTC NS/VAD
适用范围

仅官方ESP32-S3-Korvo-2主板V3.0、模块丝印WROOM-1-N16R8;ES7210 MIC1/2采入但仅MIC1送M,PA关闭;IDF5.5.5/SR2.4.7/codec1.5.11。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/audio-speech-sources.json:固定源实际章节、安装/cache比较、V3.0图面核查。版本:IDF5.5.5 / SR2.4.7 / codec1.5.11验证日期:
构建 / 配置已通过
docs/esp32-evidence/audio-speech-builds.json:当前源的实际终态构建与配置/模型/镜像/原日志SHA。对象:esp32s3 / Korvo2 V3.0 N16R8版本:IDF5.5.5 / standard Registry lock验证日期:
仿真已通过
docs/esp32-evidence/audio-speech-host-final.json及audio-speech-model-files.json:实际调用的纯逻辑、A03实际提交函数体与合成文件/故障、模型截断拒绝恢复;非声学仿真。;audio-speech-fault-boundaries.json另含65组完整生产函数体外部故障边界/PA所有权调用顺序检查,非神经/RTOS/物理证明。对象:same-source C11 ASan/UBSan; actual model/file bytes, no neural inference验证日期:
本站实板待验证
没有指定实板、真实麦克风录音/播放、唤醒/命令/LED或电气/声学仪表运行;两观察CSV仅表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 离线语音

先分清本页的产物与边界

本例把板上真实ES7210 MIC1采样送入ESP-SR WebRTC NS/VAD,按串口q/n分别触发安静/背景噪声场景的10秒RAW与AFE单声道WAV、逐fetch VAD CSV。固件没有合成音源;本次未接板,没有产出真实录音。桌面合成样本只测试文件工具,不能填入安静/噪声观察表。

采音、VAD“像说话”、唤醒词和命令识别是不同结果。本项目不开WakeNet/MultiNet,没有文字识别、云端理解或回声消除。与既有小智排错文章不同,这里提供独立可构建的原生IDF采音/AFE/保存工程,不改变小智固件的板配置。

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义

02 / 离线语音

安静/噪声两组操作与文件提交

先备份卡、关电插入4–32GB FAT32 SDHC;程序挂载失败不会自动格式化。核供电/串口后等待pipeline打印。保持板和说话者位置不变,串口发q再回车,在安静环境按预先写下的同一段话说10秒;等待FILES_SW_COMMITTED。第二轮只增加位置固定的背景噪声源,发n再回车并重复同一文本。记录距离/方向/噪声类型和人为估计与仪表读数的区别;本次两组都没有执行,不提供伪录音。

每轮从命令之后分别缓存160000个RAW MIC1样本和160000个AFE fetch输出,放在PSRAM,采满后才写卡。两条流都有自己的首样本软件时刻,AFE还有内部队列/处理延迟,因此两个WAV不是逐样本时间对齐,也不是同一时刻的“无损前后”。比较时只选共同稳定发声区间;若要SNR/延迟数值,须另做明确对齐和参考测试,不能直接相减这两文件。

文件名如A03_q_000_RAW.WAV、A03_q_000_AFE.WAV、A03_q_000_EVENTS.csv;q/n为场景标签,不是测量结论。只选三种目标/part均不存在的新编号000–999,用O_EXCL创建part。两个WAV为16kHz/PCM16/mono/10秒,每个320044字节。CSV记录输出样本位置、样本数、software_fetch_us、VAD状态和库的数字音量字段;该volume不是dB SPL,时间戳不是ADC采样硬件时间。

WAV按完整RAM数据写头/PCM,检查fwrite、fflush、fsync、fclose,再rename;CSV同样检查。每个文件独立提交,三文件不是一个事务:RAW成功后AFE/CSV失败仍会留下已完成RAW和part,不能把不完整三件套标为整轮成功。FAT/卡缓存也没有断电原子性保证。故障后保存现状、复制读取检查;不覆盖part或删除旧录音来“恢复成功”。运行采满超20秒/读错则锁存,不把缺失样本补零后保存为成功。

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现

03 / 离线语音

检查真实文件,不替它们编造来源

在读者自己完成现场录音后,关电取卡,用本地工具分别检查两WAV,再由操作者手动用本机播放器回放同一稳定语句区间,记录噪声/语音失真与VAD漏/误触发。本文作者没有播放现实音频;工具不上传、不自动播放。文件合法不能证明录音来自指定MIC,也不能证明降噪变好。

check_recording.py从当前main.c原样抽取le16/le32和完整wav_commit函数,与明确stdio故障包装一起编译运行,验证正常提交、短写、fsync失败、fclose失败和已有PART拒覆盖。用Python wave/struct读真正写出的合成320044B WAV;此夹具没有执行AFE推理、SD硬件或麦克风。源/实际体/产物SHA和原日志用于定位软件故障。

代码 / 配置
python3 tools/inspect_wav.py A03_q_000_RAW.WAV
python3 tools/inspect_wav.py A03_q_000_AFE.WAV
python3 tools/check_recording.py --output /tmp/a03-commit-check
python3 tools/inspect_models.py build/srmodels/srmodels.bin --expect '' --model-root managed_components/espressif__esp-sr/model

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现

04 / 离线语音

完整应用、板级音频与同源状态代码

以下四个文件与下载目录逐字一致。项目另含正常Registry依赖锁、构建配置、测试工具、许可和空观察表。没有隐藏平台服务、自动上传音频或桌面合成音冒充传感器的分支。

代码 / 配置
// ---- main/main.c ----
// Copyright2026 hixinwu. SPDX-License-Identifier: MIT
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_afe_sr_iface.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include "board_audio.h"
#include "speech_state.h"
#define PROJECT_ID "A03"
#define COMMANDS 0
static const char *TAG=PROJECT_ID;
static const esp_afe_sr_iface_t *afe;
static esp_afe_sr_data_t *ad;
static atomic_bool failed;
static int feed_n;
static uint64_t now_us(void){return (uint64_t)esp_timer_get_time();}
static void fail(const char *why){atomic_store(&failed,true);ESP_LOGE(TAG,"FAULT %s; no inferred hardware result; RESET after repair",why);}

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#define SAMPLES 160000u
#define MAX_EVENTS 2000u
static int16_t *raw_capture,*out_capture;
typedef struct{uint32_t sample;uint32_t samples;uint64_t software_us;int vad;float volume;} event_t;
static event_t *events;
static atomic_bool capturing;
static atomic_uint generation,raw_done,out_done,event_done;
static atomic_ullong raw_first_us,out_first_us;
static uint32_t raw_pos,out_pos,event_pos,raw_gen,out_gen;
static void raw_save(const int16_t *data,size_t n){
    if(atomic_load(&failed)||!atomic_load(&capturing)){return;}
    unsigned gen=atomic_load(&generation);if(raw_gen!=gen){raw_gen=gen;raw_pos=0;}
    if(raw_pos==SAMPLES){return;}
    if(raw_pos==0)atomic_store(&raw_first_us,now_us());
    if(n>SAMPLES-raw_pos){n=SAMPLES-raw_pos;}
    memcpy(raw_capture+raw_pos,data,n*2);raw_pos+=n;if(!atomic_load(&failed))atomic_store(&raw_done,raw_pos);
}
static void processed_save(const afe_fetch_result_t *res,uint64_t t){
    if(atomic_load(&failed)||!atomic_load(&capturing)){return;}
    unsigned gen=atomic_load(&generation);if(out_gen!=gen){out_gen=gen;out_pos=0;event_pos=0;}
    if(out_pos==SAMPLES){return;}
    if(out_pos==0)atomic_store(&out_first_us,t);
    size_t n=res->data_size/2;if(n>SAMPLES-out_pos)n=SAMPLES-out_pos;
    if(event_pos==MAX_EVENTS){fail("event capture capacity");return;}
    events[event_pos++]=(event_t){.sample=out_pos,.samples=n,.software_us=t,.vad=res->vad_state,.volume=res->data_volume};
    memcpy(out_capture+out_pos,res->data,n*2);out_pos+=n;if(!atomic_load(&failed)){atomic_store(&event_done,event_pos);atomic_store(&out_done,out_pos);}
}
static void le16(uint8_t*p,uint16_t n){p[0]=n;p[1]=n>>8;}
static void le32(uint8_t*p,uint32_t n){le16(p,n);le16(p+2,n>>16);}
static bool wav_commit(const char *part,const char *done,const int16_t *data){
    int fd=open(part,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd<0)return false;FILE*f=fdopen(fd,"wb");if(!f){close(fd);return false;}
    uint8_t h[44]={0};memcpy(h,"RIFF",4);le32(h+4,36+SAMPLES*2);memcpy(h+8,"WAVEfmt ",8);le32(h+16,16);le16(h+20,1);le16(h+22,1);le32(h+24,16000);le32(h+28,32000);le16(h+32,2);le16(h+34,16);memcpy(h+36,"data",4);le32(h+40,SAMPLES*2);
    bool ok=fwrite(h,1,44,f)==44&&fwrite(data,2,SAMPLES,f)==SAMPLES;
    if(ok){ok=fflush(f)==0;}
    if(ok){ok=fsync(fileno(f))==0;}
    if(fclose(f)!=0){ok=false;}
    if(ok){ok=rename(part,done)==0;}
    return ok;
}
static bool file_absent(const char*p){struct stat st;if(stat(p,&st)==0)return false;return errno==ENOENT;}
static bool save_record(char condition){
    if(atomic_load(&failed)){return false;}
    char rp[64],rw[64],op[64],ow[64],cp[64],cw[64];int seq;
    for(seq=0;seq<1000;seq++){snprintf(rp,sizeof(rp),"/sd/A03_%c_%03d_RAW.part",condition,seq);snprintf(rw,sizeof(rw),"/sd/A03_%c_%03d_RAW.WAV",condition,seq);snprintf(op,sizeof(op),"/sd/A03_%c_%03d_AFE.part",condition,seq);snprintf(ow,sizeof(ow),"/sd/A03_%c_%03d_AFE.WAV",condition,seq);snprintf(cp,sizeof(cp),"/sd/A03_%c_%03d_EVENTS.part",condition,seq);snprintf(cw,sizeof(cw),"/sd/A03_%c_%03d_EVENTS.csv",condition,seq);if(file_absent(rp)&&file_absent(rw)&&file_absent(op)&&file_absent(ow)&&file_absent(cp)&&file_absent(cw))break;}
    if(seq==1000)return false;
    // Each file commits independently. This is NOT a transaction across the three files.
    if(atomic_load(&failed)||!wav_commit(rp,rw,raw_capture)){return false;}
    if(atomic_load(&failed)||!wav_commit(op,ow,out_capture)){return false;}
    if(atomic_load(&failed)){return false;} // completed earlier files are retained, not rolled back.
    int fd=open(cp,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd<0)return false;FILE*f=fdopen(fd,"w");if(!f){close(fd);return false;}
    bool ok=fprintf(f,"output_sample,output_samples,software_fetch_us,vad_state,volume_library_db\n")>0;
    for(unsigned i=0;i<atomic_load(&event_done)&&ok&&!atomic_load(&failed);i++){event_t *a=events+i;ok=fprintf(f,"%u,%u,%llu,%d,%f\n",(unsigned)a->sample,(unsigned)a->samples,(unsigned long long)a->software_us,a->vad,(double)a->volume)>0;}
    if(ok){ok=fflush(f)==0;}
    if(ok){ok=fsync(fileno(f))==0;}
    if(fclose(f)!=0){ok=false;}
    if(atomic_load(&failed)){ok=false;}
    if(ok){ok=rename(cp,cw)==0;}
    if(atomic_load(&failed)){ok=false;}
    if(ok)ESP_LOGI(TAG,"FILES_SW_COMMITTED raw=%s afe=%s events=%s raw_first_us=%llu fetch_first_us=%llu; not aligned/acoustic proof",rw,ow,cw,atomic_load(&raw_first_us),atomic_load(&out_first_us));
    return ok;
}
static void feed_task(void *arg){
    (void)arg;int16_t *stereo=heap_caps_malloc(feed_n*4,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),*mono=heap_caps_malloc(feed_n*2,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);
    if(!stereo||!mono){fail("feed allocation");vTaskDelete(NULL);return;}
    size_t discarded=0;while(discarded<8000&&!atomic_load(&failed)){if(board_audio_read(stereo,feed_n)!=ESP_OK){fail("startup read/short read");break;}discarded+=feed_n;}
    // Driver runs during model creation. Discard startup samples and fence its old overflows.
    const uint32_t baseline=board_audio_lost();
    while(!atomic_load(&failed)){
        if(board_audio_read(stereo,feed_n)!=ESP_OK||board_audio_lost()!=baseline){fail("read/short read/RX queue overflow");break;}
        if(atomic_load(&failed)){break;}
        for(int i=0;i<feed_n;i++)mono[i]=mic1(stereo,i);
        raw_save(mono,feed_n);
        if(atomic_load(&failed)){break;}
        if(afe->feed(ad,mono)<0){fail("AFE feed");break;}
    }
    free(stereo);free(mono);vTaskDelete(NULL);
}
static void fetch_task(void *arg){
    (void)arg;uint64_t last=now_us();
    while(!atomic_load(&failed)){afe_fetch_result_t*res=afe->fetch_with_delay(ad,pdMS_TO_TICKS(100));uint64_t t=now_us();
        if(atomic_load(&failed)){break;} // fetch may have waited while feed failed.
        if(!res){if(t-last>2000000ULL)fail("AFE no fetch >2s");continue;}
        if(res->ret_value!=ESP_OK||!res->data||res->data_size<=0||res->data_size%2){fail("AFE fetch/size");break;}last=t;processed_save(res,t);
    }board_leds(true,false);vTaskDelete(NULL);
}
void app_main(void){
    ESP_LOGI(TAG,"IDF5.5.5 / SR2.4.7 / Korvo2 MAIN V3.0 N16R8; no WiFi/cloud; PA stays OFF");
    ESP_ERROR_CHECK(board_audio_init());srmodel_list_t *models=esp_srmodel_init("model");if(!models){fail("model partition init");return;}
    afe_config_t *cfg=afe_config_init("M",models,AFE_TYPE_SR,AFE_MODE_LOW_COST);if(!cfg){fail("AFE config");return;}
    cfg->aec_init=false;cfg->se_init=false;cfg->ns_init=true;cfg->vad_init=true;cfg->agc_init=false;cfg->wakenet_init=false;
    cfg->afe_ns_mode=AFE_NS_MODE_WEBRTC;cfg->vad_model_name=NULL;cfg->afe_linear_gain=1.0f;cfg->memory_alloc_mode=AFE_MEMORY_ALLOC_MORE_PSRAM;
    
    cfg=afe_config_check(cfg);if(cfg->aec_init||cfg->pcm_config.total_ch_num!=1||cfg->pcm_config.ref_num!=0){fail("unexpected AFE reference format");return;}
    afe=esp_afe_handle_from_config(cfg);ad=afe?afe->create_from_config(cfg):NULL;afe_config_free(cfg);if(!ad){fail("AFE create");return;}
    feed_n=afe->get_feed_chunksize(ad);if(feed_n<=0||feed_n>4096||afe->get_feed_channel_num(ad)!=1||afe->get_samp_rate(ad)!=16000){fail("AFE shape");return;}
    afe->print_pipeline(ad);ESP_LOGI(TAG,"feed samples/chunk=%d; ES7210 MIC1/MIC2 stereo16 → first MIC only M; no R/AEC",feed_n);
    ESP_ERROR_CHECK(board_sd_mount());raw_capture=heap_caps_malloc(SAMPLES*2,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);out_capture=heap_caps_malloc(SAMPLES*2,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);events=heap_caps_malloc(MAX_EVENTS*sizeof(event_t),MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);if(!raw_capture||!out_capture||!events){fail("capture PSRAM allocation");return;}fcntl(STDIN_FILENO,F_SETFL,O_NONBLOCK);
    if(xTaskCreatePinnedToCore(feed_task,"mic-feed",6144,NULL,5,NULL,0)!=pdPASS||xTaskCreatePinnedToCore(fetch_task,"afe-fetch",8192,models,4,NULL,1)!=pdPASS){fail("task create");return;}
    char condition=0;uint64_t began=0;ESP_LOGI(TAG,"type q then Enter for quiet, n then Enter for noise;10s each; never synthetic capture");
    while(!atomic_load(&failed)){
        int c=getchar();if(c==EOF)clearerr(stdin);
        if(atomic_load(&failed)){break;} // no new capture/commit after an observed feed/fetch fault.
        if((c=='q'||c=='n')&&!atomic_load(&capturing)){condition=c;atomic_store(&raw_done,0);atomic_store(&out_done,0);atomic_store(&event_done,0);atomic_fetch_add(&generation,1);began=now_us();atomic_store(&capturing,true);ESP_LOGI(TAG,"CAPTURE %c started; independent first-sample fences, not sample aligned",condition);}
        if(atomic_load(&capturing)&&atomic_load(&raw_done)==SAMPLES&&atomic_load(&out_done)==SAMPLES){atomic_store(&capturing,false);if(atomic_load(&failed)){break;}if(!save_record(condition)){fail("file commit; keep existing .part/independent completed files; do not retry overwrite");break;}}
        if(atomic_load(&capturing)&&now_us()-began>20000000ULL){atomic_store(&capturing,false);fail("capture incomplete20s");break;}vTaskDelay(pdMS_TO_TICKS(20));
    }atomic_store(&capturing,false);ESP_LOGE(TAG,"latched fault; partial RAM not saved as a successful recording; RESET after repair");
}

// ---- main/board_audio.c ----
// Copyright 2026 hixinwu. SPDX-License-Identifier: MIT
#include "board_audio.h"
#include "driver/i2c_master.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "driver/sdmmc_host.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "esp_codec_dev.h"
#include "esp_codec_dev_defaults.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static i2c_master_bus_handle_t bus;
static i2c_master_dev_handle_t io;
static i2s_chan_handle_t rx;
static uint32_t lost;
static const char *TAG="BOARD_V3_0";
static bool IRAM_ATTR overflow(i2s_chan_handle_t h,i2s_event_data_t *e,void *u){
    (void)h;(void)e;(void)u;__atomic_fetch_add(&lost,1,__ATOMIC_RELAXED);return false;
}
static esp_err_t reg_read(uint8_t reg,uint8_t *value){return i2c_master_transmit_receive(io,&reg,1,value,1,100);}
static esp_err_t reg_write(uint8_t reg,uint8_t value){uint8_t b[]={reg,value};return i2c_master_transmit(io,b,2,100);}
esp_err_t board_leds(bool red,bool blue){
    uint8_t out;ESP_RETURN_ON_ERROR(reg_read(1,&out),TAG,"TCA output read");
    out=(out&~0xc0)|(red?0x40:0)|(blue?0x80:0);return reg_write(1,out);
}
uint32_t board_audio_lost(void){return __atomic_load_n(&lost,__ATOMIC_RELAXED);}
/* P0 and GPIO48 share PA_CTRL. Only MCU reset does not reset TCA latches.
 * First release MCU drive, then release the expander, preload LOW, finally drive.
 * Speaker is disconnected in this recipe; no seamless mute/electrical guarantee. */
static esp_err_t board_io_claim(void){
    ESP_RETURN_ON_ERROR(gpio_set_direction(GPIO_NUM_48,GPIO_MODE_INPUT),TAG,"PA release MCU");
    ESP_RETURN_ON_ERROR(gpio_set_pull_mode(GPIO_NUM_48,GPIO_FLOATING),TAG,"PA no MCU pull");
    i2c_master_bus_config_t b={.i2c_port=I2C_NUM_0,.sda_io_num=17,.scl_io_num=18,.clk_source=I2C_CLK_SRC_DEFAULT,.glitch_ignore_cnt=7,.flags.enable_internal_pullup=true};
    ESP_RETURN_ON_ERROR(i2c_new_master_bus(&b,&bus),TAG,"I2C");
    // V3.0 TCA9554 has A2:A0=000. No blind fallback to different board revisions.
    ESP_RETURN_ON_ERROR(i2c_master_probe(bus,0x20,100),TAG,"TCA9554 absent");
    i2c_device_config_t dc={.dev_addr_length=I2C_ADDR_BIT_LEN_7,.device_address=0x20,.scl_speed_hz=100000};
    ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(bus,&dc,&io),TAG,"TCA add");
    uint8_t out,dir;ESP_RETURN_ON_ERROR(reg_read(1,&out),TAG,"TCA latch");
    // P5 LOW powers peripherals (SD), P6/P7 initially OFF; preserve other latches.
    ESP_RETURN_ON_ERROR(reg_write(1,out&~0xe0),TAG,"TCA initial levels");
    ESP_RETURN_ON_ERROR(reg_read(3,&dir),TAG,"TCA direction");
    ESP_RETURN_ON_ERROR(reg_write(3,(dir&~0xe0)|1),TAG,"TCA outputs; P0 input");
    ESP_RETURN_ON_ERROR(reg_read(3,&dir),TAG,"TCA verify P0 input");
    if(!(dir&1)){return ESP_ERR_INVALID_STATE;}
    ESP_RETURN_ON_ERROR(gpio_set_level(GPIO_NUM_48,0),TAG,"PA preload OFF");
    ESP_RETURN_ON_ERROR(gpio_set_direction(GPIO_NUM_48,GPIO_MODE_OUTPUT),TAG,"PA claim MCU output LOW");
    return ESP_OK;
}
esp_err_t board_audio_init(void){
    ESP_RETURN_ON_ERROR(board_io_claim(),TAG,"PA/TCA ownership");
    ESP_RETURN_ON_ERROR(i2c_master_probe(bus,0x40,100),TAG,"ES7210 absent");
    i2s_chan_config_t cc=I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0,I2S_ROLE_MASTER);cc.dma_desc_num=8;cc.dma_frame_num=256;
    ESP_RETURN_ON_ERROR(i2s_new_channel(&cc,NULL,&rx),TAG,"I2S channel");
    i2s_std_config_t sc={.clk_cfg=I2S_STD_CLK_DEFAULT_CONFIG(16000),.slot_cfg=I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,I2S_SLOT_MODE_STEREO),.gpio_cfg={.mclk=16,.bclk=9,.ws=45,.dout=I2S_GPIO_UNUSED,.din=10}};
    ESP_RETURN_ON_ERROR(i2s_channel_init_std_mode(rx,&sc),TAG,"STD stereo16");
    i2s_event_callbacks_t cb={.on_recv_q_ovf=overflow};ESP_RETURN_ON_ERROR(i2s_channel_register_event_callback(rx,&cb,NULL),TAG,"overflow callback");
    audio_codec_i2s_cfg_t data_cfg={.port=I2S_NUM_0,.rx_handle=rx};
    const audio_codec_data_if_t *data=audio_codec_new_i2s_data(&data_cfg);
    // Codec API uses 8-bit control address 0x80; raw master probe above uses 7-bit0x40.
    audio_codec_i2c_cfg_t ctrl_cfg={.port=I2C_NUM_0,.addr=ES7210_CODEC_DEFAULT_ADDR,.bus_handle=bus};
    const audio_codec_ctrl_if_t *ctrl=audio_codec_new_i2c_ctrl(&ctrl_cfg);
    es7210_codec_cfg_t adc_cfg={.ctrl_if=ctrl,.mic_selected=ES7210_SEL_MIC1|ES7210_SEL_MIC2,.master_mode=false};
    const audio_codec_if_t *adc=es7210_codec_new(&adc_cfg);
    if(!data||!ctrl||!adc)return ESP_ERR_NO_MEM;
    esp_codec_dev_cfg_t dev_cfg={.dev_type=ESP_CODEC_DEV_TYPE_IN,.codec_if=adc,.data_if=data};
    esp_codec_dev_handle_t dev=esp_codec_dev_new(&dev_cfg);if(!dev)return ESP_ERR_NO_MEM;
    esp_codec_dev_sample_info_t fs={.sample_rate=16000,.channel=2,.bits_per_sample=16,.channel_mask=3};
    if(esp_codec_dev_open(dev,&fs)!=ESP_CODEC_DEV_OK)return ESP_FAIL;
    if(esp_codec_dev_set_in_gain(dev,24.0f)!=ESP_CODEC_DEV_OK)return ESP_FAIL;
    // Direct channel read below checks actual byte count, not only codec wrapper status.
    return ESP_OK;
}
esp_err_t board_audio_read(int16_t *stereo,size_t frames){
    size_t bytes=0,want=frames*2*sizeof(int16_t);esp_err_t e=i2s_channel_read(rx,stereo,want,&bytes,500);
    return e==ESP_OK&&bytes==want?ESP_OK:(e==ESP_OK?ESP_ERR_INVALID_SIZE:e);
}
esp_err_t board_sd_mount(void){
    sdmmc_host_t host=SDMMC_HOST_DEFAULT();host.max_freq_khz=SDMMC_FREQ_DEFAULT;
    sdmmc_slot_config_t slot=SDMMC_SLOT_CONFIG_DEFAULT();slot.width=1;slot.clk=15;slot.cmd=7;slot.d0=4;slot.d1=GPIO_NUM_NC;slot.d2=GPIO_NUM_NC;slot.d3=GPIO_NUM_NC;
    esp_vfs_fat_sdmmc_mount_config_t cfg={.format_if_mount_failed=false,.max_files=4,.allocation_unit_size=16384};sdmmc_card_t *card=NULL;
    return esp_vfs_fat_sdmmc_mount("/sd",&host,&slot,&cfg,&card);
}

// ---- main/board_audio.h ----
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
esp_err_t board_audio_init(void);
esp_err_t board_audio_read(int16_t *stereo,size_t frames);
uint32_t board_audio_lost(void);
esp_err_t board_leds(bool red,bool blue);
esp_err_t board_sd_mount(void);

// ---- main/speech_state.h ----
// Copyright 2026 hixinwu. SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct{bool active;bool led_on;uint64_t until;} command_gate_t;
static inline void command_wake(command_gate_t *g,uint64_t now){if(!g->active){g->active=true;g->until=now+8000000ULL;}}
static inline void command_tick(command_gate_t *g,uint64_t now){if(g->active&&now>=g->until)g->active=false;}
static inline bool command_accept(command_gate_t *g,int id,uint64_t now){command_tick(g,now);if(!g->active||!(id==1||id==2))return false;g->led_on=id==1;g->active=false;return true;}
static inline int16_t mic1(const int16_t *stereo,unsigned frame){return stereo[2*frame];}

参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[17] IDF5.5.5标准I2S新channel宏[21] IDF5.5.5 GPIO API:输入/方向/拉电阻[22] IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置

05 / 离线语音

固定硬件和电源边界

只适用于官方ESP32-S3-Korvo-2主板V3.0,且实际模块丝印为ESP32-S3-WROOM-1-N16R8:16MB Flash、8MB Octal PSRAM。官方主站列Korvo-2为16/8MB,旧V3.0手册只写WROOM-1,没有保证每块后缀;先拍下自己的模块/主板丝印,不能套V3.1、Korvo-1、仿板、N16R8V或A01的DevKitC。本文没有持有这块实板,不能替读者确认出厂装配。

使用板载ES7210四路ADC中的MIC1和MIC2,板图04_AUDIO的U23/U31为MSM381A3729H9BPC模拟麦克风。它们不是A01的ICS43434数字麦克风;不要外接A01杜邦线。ES7210经I2C配置,输出到S3的标准I2S,程序选择MIC1/2、stereo PCM16;仅取交错数组第0路送AFE,input_format固定“M”。MIC2采入但未送算法,MIC3的板载DAC回声参考未选。AEC关闭、SE阵列处理关闭,不用虚构R/全零R冒充真正参考。NS选择WebRTC、VAD选择WebRTC,线性增益1、AGC关闭;ES7210前端增益固定24dB。这里不宣称双麦波束形成、声压级或回声消除性能。

取下LCD/摄像头扩展,电池座留空。关板上电源开关后,按官方手册分别接USB-to-UART数据口与独立USB Power口;后者用稳压5V/2A适配器和合适线材,再打开开关。不把两个Micro-USB口当作原生USB19/20;bridge/UART0用于115200日志。观察供电/温升异常立即关电排线,不靠提高麦克风增益处理供电故障。不接裸市电、外部功放或电池充电实验。

端点固定连接/程序选择断电核对
I2C SDA/SCLGPIO17/18;主机100kHz板载总线,勿接5V上拉
ES7210控制主机7位0x40;codec API参数0x80为8位地址两种地址写法不能混用
I2S MCLK/BCLK/WS/ADC SDOUTGPIO16/9/45/10;RX only16kHz、2×16bit,名义MCLK4.096MHz/BCLK512kHz;必须用仪表核实实际波形
ES7210 MIC1/MIC216位有符号交错;每帧选stereo[2*i]不把声道编号直接称左右空间位置
PA_CTRL先释放GPIO48;TCA P0输入读回后,GPIO48才输出LOW不初始化ES8311播放、不接扬声器;软件OFF不是物理断电证明
TCA95547位0x20,V3.0固定A2:A0=000P5=外设电源,LOW使能;P6=红灯,P7=蓝灯,高亮
LED虚拟编号BSP的55/56是扩展器映射不是S3 GPIO55/56,项目直接读写TCA寄存器并保留其他位
MicroSD(仅A03使用)1bit:CLK15/CMD7/D0=4关机插拔,4–32GB FAT32 SDHC;不自动格式化

参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[3] 乐鑫官方S3产品页:Korvo2 16MB/8MB配置[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏

06 / 离线语音

锁定工程、构建与刷机

使用ESP-IDF 5.5.5,ESP-SR 2.4.7(commit2f8c4b0459db5bbb39abd77adae27962d6d94bcb),esp_codec_dev 1.5.11、esp-dsp 1.8.0、dl_fft 0.8.0、cjson 1.7.19~2。这是一组实际可构建基线,不声称最新版本。main/idf_component.yml与正常Registry service源dependencies.lock逐个锁定;无需Arduino core,也没有第二套IDF。源码直接用IDF新I2S channel与I2C master API,不混装legacy driver。

本次作者从既有、已核Registry component_hash及每文件CHECKSUMS的缓存,用APFS clone预填标准managed_components,随后运行正常resolver,得到可迁移service lock;没有override_path、私有SDK路径写进公共manifest,也没有改上游源。普通机器由组件管理器按锁下载一次即可。离线机器须准备同版本缓存;不能静默换成另一版SR或模型。首次下载占用显著,空间不足先停止构建并核已有缓存,不能删最终固件/模型/配置/日志冒充恢复。

sdkconfig.defaults固定16MB/QIO、80MHz Flash与Octal PSRAM;分区factory=4MiB、model=10MiB,应用和模型的实际大小见作者构建JSON。model子类型写spiffs只是分区标签,本项目由ESP-SR读取其专用打包格式,不要拿SPIFFS格式化它。全部flash参数/偏移以当前build/flasher_args.json为准,SR模型会在0x410000写入;换旧固件也要按那一版的完整分区/模型恢复。错误模型不能仅凭“应用烧成功”排除。

刷机失败先核PORT/数据线/串口占用;关闭监视器,按住BOOT点RESET进入下载,重新flash全部当前输出,松开BOOT再RESET。重启后核项目ID、固定版本、pipeline与模型打印,不拿旧终端内容当新固件结果。PSRAM不识别或模块后缀不同先停,不能删PSRAM设置强行运行大型模型。

代码 / 配置
idf.py --version
idf.py -D IDF_TARGET=esp32s3 build
idf.py -p PORT flash monitor

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义

07 / 离线语音

软件运行与错误边界

I2S在codec初始化时已经启用;AFE/模型初始化期间的队列可能产生旧溢出。真正采样任务先丢弃至少8000个stereo帧(名义至少0.5秒),之后快照RX overflow计数作为本轮边界;任何后续计数变化、读错或不足字节立即锁存FAULT。启动丢弃是本项目保守处理,不是麦克风有效时间或音质实测。feed chunk由AFE运行时查询,16kHz PCM16、一个M通道;fetch数据长度/状态均检查,不能假设feed与fetch帧数相同。

feed任务与fetch任务分开,AFE可能内部阻塞,100ms fetch等待不是硬实时总延迟保证。2秒无fetch或运行读错会锁存故障,要求修复后RESET;没有断线后自动伪造PCM恢复路径。全部应用计时用esp_timer_get_time的64位微秒,未用32位millis让旧窗口在完整周期后复活。没有Wi-Fi/云服务和音频上传;本项目也没有音频播放路径。

GPIO48与TCA9554 P0在本板共用PA_CTRL。只复位MCU不保证TCA输出锁存被复位:初始化先将GPIO48设INPUT/FLOATING,确认0x20的P0为输入并读回,再预装GPIO48低电平、切OUTPUT;探测/写入/读回失败保持MCU高阻,不强行接管。此前状态可能仍使PA使能,本例不接扬声器,不承诺上电到接管之间无声或无缝静音;这只是按原理图避免相反强驱动的调用顺序,电平/时序仍待实板。

运行路径在阻塞fetch返回后立即重查故障标志;A05还在detect、get_results/时钟返回后以及新的正常LED调用前检查,已观察故障则丢弃本轮结果,收尾尝试故障红灯。A03也在采样发布、开始新录音和逐文件提交边界检查,不把已观察故障后的完整录音标为成功。检查与下一次调用之间仍有跨核调度窗口,已经进入的I2C/文件写入不能撤销;不承诺FAULT日志出现瞬间全核硬实时停止或已写文件自动回滚。

初始化故障的ESP_ERROR_CHECK可能让固件中止/重启,运行故障则打印锁存FAULT;二者不能混称“仍在正常识别”。红灯/日志为诊断输出,未实测前不保证灯真的亮。ADC/I2C探测只证明地址有应答;即使流正常,静音/错误通道也不能靠软件日志证明麦克风和声学路径正常。

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义

08 / 离线语音

故障输入、恢复与记录

observations.csv、electrical-observations.csv均仅表头。按指定模块实际填供电/电流条件、MCLK/BCLK/WS、固件/模型SHA、现场声音与结果,不能填入构建机的合成值。本交付没有实板、麦克风录音、声学/电气仪表或实际灯光观察;所有硬件状态pending。

输入/现象可执行检查与恢复
0x20或0x40探测失败关电检查V3.0/模块/两口供电和板损伤;不向不同地址盲写寄存器。修好后RESET
PSRAM/AFE/model初始化失败核N16R8与当前sdkconfig、应用+model完整烧录、锁定版本;保留原日志后重新build/flash,不格式化model
RX overflow/短读/无fetch保留首个FAULT和固件SHA,核供电、I2S波形/任务负载;修复后RESET,不能把旧音频拼成连续新数据
启动或运行静音ADC地址应答不等于麦克风有效;按原理图核MIC1支路/增益/实际PCM统计,再定位算法,勿提高增益掩盖问题
LED日志变化但没灯先核TCA P6/P7和板修订;GPIO55/56不是可接实体端点;断电复核后恢复
断电/RESET应用窗口与RAM录音消失,LED初始化OFF;保留卡上已完成文件及.part,不能据旧记录宣称新会话有效

参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义

09 / 离线语音

桌面检查与许可

同源C11 ASan/UBSan只验证main实际调用的纯逻辑:MIC1交错提取;A05另测命令窗口/有效ID/超时/64位时刻。它们不在桌面运行ESP-SR二进制推理,也不证明真实说话能唤醒/识别。

原创固件/工具/图为MIT,见LICENSE;ESP-IDF与codec等依组件原许可。ESP-SR的LICENSE为ESPRESSIF MIT,授权条件限用于乐鑫产品,不能简写成无限制普通MIT;库和模型由官方组件按原许可提供,未随下载包复制大二进制。第三方资料版权归原厂;固定来源、实际章节和比较记录见正文sources及作者证据,不用URL可达替代阅读。

代码 / 配置
clang -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -fno-omit-frame-pointer -I main tests/state.c -o /tmp/speech-state
/tmp/speech-state

参考:[20] ESP-SR2.4.7原许可:限乐鑫产品的ESPRESSIF MIT

10 / 离线语音

复现故障边界与PA所有权顺序

输出目录必须未存在。前者原样提取当前完整fetch_task,A03另提取完整save_record,以明确外部AFE/MN/时钟/LED或stdio桩注入故障;后者原样提取完整board_io_claim,覆盖有电TCA P0保留高/低、MCU旧锁存与I2C探测/写入/读回失败16组。它们验证生产调用边界和顺序,不运行神经识别、真实RTOS调度、SD/I2C/麦克风或电气实测。LED用例另保留“已进入调用后才发布故障”的允许边界,不能把桩结果当灯已亮/PA已静音。

PA接管使用的固定API与实现另见[IDF5.5.5 gpio.h](https://github.com/espressif/esp-idf/blob/v5.5.5/components/esp_driver_gpio/include/driver/gpio.h)及[gpio.c](https://github.com/espressif/esp-idf/blob/v5.5.5/components/esp_driver_gpio/src/gpio.c),已与实际SDK逐字核对。

代码 / 配置
python tools/check_fault_boundaries.py --out /tmp/NEW-speech-fault-boundaries
python tools/check_pa_ownership.py --out /tmp/NEW-speech-pa-order

参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[17] IDF5.5.5标准I2S新channel宏[21] IDF5.5.5 GPIO API:输入/方向/拉电阻[22] IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置

11 / 离线语音

保留软件证据,等待现场结果

本次实际IDF5.5.5构建、源/配置/模型/镜像SHA、固定上游比较和主机/真实文件检查归档于audio-speech-builds.json、audio-speech-host-final.json、audio-speech-model-files.json与audio-speech.md。模型包检查不运行神经推理,编译通过不替代声音、灯光、电源或时钟测量。

下载包中的observations.csv和electrical-observations.csv仅表头。请记录自己的板型、应用/模型SHA与实验条件后再填入现场数据,不能把本文构建机的合成WAV、注入命令ID或数字统计写成录音与识别结果。

做到这里,你应该看到
能拿出当前源与真实编译/工具证据;未执行的物理项目保持pending。

参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[20] ESP-SR2.4.7原许可:限乐鑫产品的ESPRESSIF MIT

BACK TO THE SOURCE

资料来源与版本说明

固定SR/codec源码与校验缓存、IDF5.5.5实际安装源及官方板图已读核;编译/主机/模型字节检查不替代麦克风、灯光或仪表实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. Korvo2 V3.0固定用户手册:供电、针脚、AEC参考
  2. Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO
  3. 乐鑫官方S3产品页:Korvo2 16MB/8MB配置
  4. BSP5.0.0固定音频实现:ES7210/端点
  5. BSP5.0.0固定头:虚拟LED映射和硬件引脚
  6. ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则
  7. ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待
  8. ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en
  9. ESP-SR2.4.7打包脚本:MultiNet7附带fst
  10. ESP-SR2.4.7模型布局打包器
  11. ESP-SR2.4.7模型加载:名称和文件索引
  12. ESP-SR2.4.7 MultiNet接口:chunk/timeout/结果
  13. ESP-SR2.4.7命令图:英文g2p/add/update
  14. ESP-SR2.4.7 wn9_hiesp实际模型信息
  15. codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟
  16. codec1.5.11固定I2S数据接口:open/reconfigure/read
  17. IDF5.5.5标准I2S新channel宏
  18. IDF5.5.5 I2S读/等待的实际实现
  19. IDF5.5.5 esp_timer:64位微秒语义
  20. ESP-SR2.4.7原许可:限乐鑫产品的ESPRESSIF MIT
  21. IDF5.5.5 GPIO API:输入/方向/拉电阻
  22. IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置