免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Tasmota 中 Shine MP3 编码器(mp3_shine_esp32)的 ESP32 移植与实战集成指南

Tasmota 中 Shine MP3 编码器(mp3_shine_esp32)的 ESP32 移植与实战集成指南 Tasmota 中 Shine MP3 编码器mp3_shine_esp32的 ESP32 移植与实战集成指南【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota导读本文围绕 Tasmota 固件仓库中随附的mp3_shine_esp32库展开它是在 ESP32 上运行的老牌 Shine MP3 编码器的移植版负责把 I²S 麦克风采集的 PCM 音频实时编码为 MP3 流。你将了解到该库的起源与移植重点、完整的 API 与配置参数采样率/码率/声道模式、内存管理与性能优化要点以及它在 Tasmota 的 I²S 音频驱动xdrv_42_1_i2s_mp3mic_idf51.ino中如何被真实调用最终能独立写出在 ESP32 上录音并输出 MP3 到文件或网络流的代码。库的定位与背景mp3_shine_esp32位于 lib/lib_audio/mp3_shine_esp32其 README.md 开篇即说明这是老 Shine MP3 编码器向 ESP32 的移植最初为 ARM 或 MIPS 架构设备编写距今已有相当年头README 标注 Last updated FEB 2019并提醒与当前 IDF 的兼容性未知。从 library.properties 与 library.json 可以看到它的元数据名称为mp3_shine_esp32版本 1.0类别为信号处理器Signal processor目标平台为 espressif32 / ESP32使用 Arduino 框架。按 README 的自述移植工作集中在三方面内存分配针对 ESP32 优化编码器初始化时通过heap_caps_malloc系列接口分配 SPIRAM/32-bit 内存详见下文内存策略加入部分汇编提升性能针对 ESP32 的 Xtensa 架构加入了汇编乘法指令见 mult_xtensa_gcc.h保留了双核实现的雏形README 提到代码中留有双核实现的开端的残迹最终并未采用代码也未清理。changelog.txt记录的最后一次修改为 0.1.4 版本better input wav file handling (from Yichin Lin) / code cleanup即改进了 WAV 输入文件的处理并做了代码清理。注意该库的 README 仅十余行其完整的技术细节API、参数表、内存分配需要结合 layer3.h 与 layer3.cpp 等源码来理解下文即以此为据展开。编码算法的特性与局限README 明确列出该编码器最重要的Limitations局限The encoding algorithm is rather simple. In particular, it does not have any Psychoacoustic Model.即编码算法相当简单不含心理声学模型Psychoacoustic Model。这意味着它不像现代商业编码器那样根据人耳掩蔽效应动态分配比特而是采用相对直接的量化/比特分配策略因此低码率下的听感可能不如带心理声学模型的编码器。对应用场景如语音录音、监控音频、对延迟敏感的流式传输而言这通常是可以接受的取舍——换来的是实现简单、占用资源少、在 MCU 上可实时运行。从 layer3.cpp 的shine_encode_buffer_internal可以看到编码一帧的三个主要阶段这也是理解简单算法的入口MDCT/子带分析shine_mdct_sub(config, stride)——对多相滤波器组输出应用 MDCT源码注释为 apply mdct to the polyphase output且原注释put on core 1暗示曾计划放到 core 1 执行比特与噪声分配shine_iteration_loop(config)——即量化与比特分配的迭代循环原注释put on core 0这是编码质量的核心写帧到比特流shine_format_bitstream(config)——把量化结果按 MP3 帧格式打包。帧率与每帧样本数由 MPEG 版本决定granules_per_frame数组layer3.cpp#L19-L24表明 MPEG-I 每帧 2 个 granule、MPEG-II 与 MPEG-2.5 每帧 1 个 granule每个 granule 为 576 个样本GRANULE_SIZE见 types.h因此shine_samples_per_pass返回 1152MPEG-I或 576。一段简短的历史README 用bit of history交代了代码血统这段代码来自互联网与 GitHub 出现之前的故纸堆最初由 Gabriel BouvigneLAME 项目创始人之一在 20 世纪末创作约 2001 年由 Pete Everett 为 RISC OS 转换为定点算术实现约 2006 年 Patrick Roberts 将其改造为跨平台、更库化的形式。mp3_shine_esp32正是这条谱系在 ESP32 上的延续。这段历史解释了代码中保留的 MIPS/ARM 汇编文件mult_mips_gcc.h、mult_sarm_gcc.h以及无架构兜底实现mult_noarch_gcc.h的原因。核心 API 与配置参数版本、声道与模式枚举layer3.h 定义了完整的公开 API。首先是输入与输出格式的枚举enum channels { PCM_MONO 1, PCM_STEREO 2 }; enum mpeg_versions { MPEG_I 3, MPEG_II 2, MPEG_25 0 }; enum modes { STEREO 0, JOINT_STEREO 1, DUAL_CHANNEL 2, MONO 3 }; enum emph { NONE 0, MU50_15 1, CITT 3 };其中 MPEG 版本不是用户手动选择的而是由采样率自动推导见下文支持的采样率与码率。配置结构体typedef struct { enum channels channels; int samplerate; } shine_wave_t; /* 输入 PCM 格式 */ typedef struct { enum modes mode; /* 立体声模式 */ int bitr; /* 码率须符合已知码率表 */ enum emph emph; /* 去加重 */ int copyright; int original; } shine_mpeg_t; /* 输出 MP3 格式 */ typedef struct { shine_wave_t wave; shine_mpeg_t mpeg; } shine_config_t;shine_config_t将输入 PCM 波形与输出 MPEG 参数合并为一次初始化所需的所有配置。注意 layer3.h 的注释强调配置数据在shine_initialise时会被拷贝进编码器初始化后无法再修改参数合法性校验留给调用方可用shine_find_bitrate_index/shine_find_samplerate_index或下面的shine_check_config完成且wave 与 mpeg 的单声道/立体声设置应保持一致。支持的采样率与码率参数表layer3.h 的注释中直接给出了完整的合法参数表这是该库最关键的配置依据务必对照使用合法采样率按 MPEG 版本分组共 9 个MPEG 版本采样率HzMPEG-I44100、48000、32000MPEG-II22050、24000、16000MPEG-2.511025、12000、8000合法码率表bitrates[16][4]单位 kbps四列分别为 MPEG 2.5、保留、MPEG II、MPEG I索引2.5保留III0-1-1-1-118-1832216-11640324-12448432-13256540-14064648-14880756-15696864-164112980-1801281096-19616011112-111219212128-112822413144-114425614160-116032015-1-1-1-1注意MPEG 版本那一列索引 3对应 MPEG-I编码器只实现 Layer IIILAYER_III 1码率/采样率的匹配由shine_check_config统一校验。默认配置与参数校验shine_set_config_mpeg_defaultslayer3.cpp#L27-L32为shine_mpeg_t填充默认值mpeg-bitr 128; /* 默认 128 kbps */ mpeg-emph NONE; /* 默认不去加重 */ mpeg-copyright 0; mpeg-original 1;参数校验函数shine_find_samplerate_index(int freq)在 9 个合法采样率中查找返回索引非法返回 -1shine_mpeg_version(int samplerate_index)采样率索引 3 为 MPEG-I6 为 MPEG-II否则 MPEG-2.5shine_find_bitrate_index(int bitr, int mpeg_version)在码率表中按版本查找非法返回 -1shine_check_config(int freq, int bitr)综合校验采样率与码率成功返回 mpeg_version失败返回 -1。编码器生命周期初始化、编码、冲刷与关闭初始化shine_initialiseshine_t shine_initialise(shine_config_t *config);返回不透明句柄shine_t实际为struct shine_global_flags *。失败如内存分配失败、配置非法返回 NULL。README 特别强调应尽早初始化编码器因为它需要大块连续 RAM——这一建议在源码中得到印证初始化阶段会一次性分配大量缓冲区见下文内存策略。初始化内部layer3.cpp#L87-L206完成的工作包括shine_check_config校验采样率/码率非法直接返回 NULL用heap_caps_malloc(..., MALLOC_CAP_SPIRAM)分配shine_global_config主体为每声道每 granule 分配l3_enc量化谱值与mdct_freqMDCT 频域值各4*GRANULE_SIZE字节用heap_caps_malloc_prefer(..., MALLOC_CAP_32BIT, MALLOC_CAP_SPIRAM|MALLOC_CAP_32BIT)优先放在 32-bit 可寻址内存分配l3loop_t含xrsq、xrabs等迭代中间量至 SPIRAM调用shine_subband_initialise、shine_mdct_initialise、shine_loop_initialise完成子带、MDCT、量化循环的查找表初始化依据采样率/码率计算每帧 slot 数、填充位padding、side info 长度、mean_bits等帧级参数并调用shine_open_bit_stream打开 4096 字节的比特流缓冲BUFFER_SIZE见 bitstream.h。每帧样本数shine_samples_per_passint shine_samples_per_pass(shine_t s);返回每次编码调用所需消耗的 PCM 样本数granules_per_frame * GRANULE_SIZE即 MPEG-I 为 1152MPEG-II/2.5 为 576。调用方必须按此数量喂入数据。编码两种输入方式unsigned char *shine_encode_buffer(shine_t s, int16_t **data, int *written); unsigned char *shine_encode_buffer_interleaved(shine_t s, int16_t *data, int *written);shine_encode_buffer输入为每声道一个独立缓冲的指针数组int16_t **datadata[0] 为左声道data[1] 为右声道shine_encode_buffer_interleaved输入为交织interleaved缓冲L/R 交替的单个int16_t *。两者的内部实现layer3.cpp#L261-L275都是把缓冲指针挂到config-buffer[0]/[1]再以stride1 或声道数调用shine_encode_buffer_internal。返回值指向编码后的 MP3 数据written返回其字节数。该指针由库内部管理仅在下次调用shine_encode_buffer/shine_encode_buffer_interleaved或shine_close之前有效可能为 NULL本帧无输出。调用方应在拿到数据后立即写出写文件或发送到网络。冲刷与关闭unsigned char *shine_flush(shine_t s, int *written); void shine_close(shine_t s);shine_flush把比特流缓冲区中残留的数据全部取出*written为剩余字节数应在关闭前调用确保所有已编码数据都被写出见 layer3.cpp#L277-L281shine_close关闭比特流、逐项释放l3_enc、mdct_freq、l3loop、xrsq、xrabs及主结构体。调用后句柄失效见 layer3.cpp#L283-L308。性能计数器可选uint32_t *shine_get_counters();返回 5 个运行时计数器基于 FreeRTOS 的portGET_RUN_TIME_COUNTER_VALUE()见 layer3.cpp#L210-L225分别记录帧开始、MDCT 结束、迭代循环结束、比特流写入结束等阶段的时间戳可用于分析各编码阶段的耗时分布源码注释中保留了历史实测数据如 68 个周期级计数差值。内存策略与性能优化要点SPIRAM 优先的大块分配这是本移植相对原版最核心的改动。shine_initialise中所有大块内存都通过heap_caps_malloc系函数分配shine_global_config主体 →MALLOC_CAP_SPIRAMlayer3.cpp#L95l3_enc、mdct_freq→heap_caps_malloc_prefer(4*GRANULE_SIZE, MALLOC_CAP_32BIT, MALLOC_CAP_SPIRAM|MALLOC_CAP_32BIT)优先放在可 DMA/32-bit 寻址的内存退而求其次才用 SPIRAMlayer3.cpp#L106-L119l3loop含xrsq、xrabs→MALLOC_CAP_SPIRAMlayer3.cpp#L124-L137。源码注释还给出了选型依据l3_enc放 IRAM 会带来约 4% 的性能下降、mdct_freq放 IRAM 约 1% 性能下降因此选择放在外部 RAM 以节约宝贵的内部 RAM。同时types.h中对subband_t的fl[SBLIMIT][64]、x[MAX_CHANNELS][HAN_SIZE]等大数组也体现了对连续内存的占用需求。这解释了 README 那句You should init the encoder ASAP in your code as the encoder needs large contiguous chunks of RAM——尽早初始化才能在堆碎片化之前拿到大块连续 SPIRAM/32-bit RAM。在没有外挂 PSRAM 的 ESP32 上使用时务必核算内部 RAM 余量。Xtensa 汇编乘法types.h 中按架构条件包含乘法实现#if defined(__mips__) (__mips 32) #include mult_mips_gcc.h #elif defined(__arm__) !defined(__thumb__) #include mult_sarm_gcc.h #elif defined(__XTENSA__) #include mult_xtensa_gcc.h #endif #include mult_noarch_gcc.h /* 兜底 */ESP32 使用 Xtensa 架构因此走 mult_xtensa_gcc.h。其中用 GCC 内联汇编实现了 32×32→64 位有符号乘法的取高 32 位操作#define asm_mul(x,y) \ ({ \ int result; \ asm (mulsh %0, %2, %1 : r (result) : r (x), r (y)); \ result ;\ })mulsh是 Xtensa 的乘-移位高位指令可一条指令完成定点乘法中的乘后取高字操作避免 64 位中间量与右移的开销。此外还提供了asm_muls再左移一位等价于 31、asm_mulr取高字、asm_mulsr等变体分别对应定点算法中不同移位/舍入需求的乘法。非 Xtensa 架构则回退到mult_noarch_gcc.h的通用实现保证代码可移植。编译优化选项README 给出明确指导要达到单核上完整的 48000Hz Joint Stereo 实时编码需要用-Os优化选项编译。这提醒我们实时性依赖编译优化级别若使用默认优化出现丢帧/欠载应检查构建脚本中的-Os是否生效Tasmota 的 PlatformIO 环境通常已包含-Os可参考仓库根目录 platformio.ini 中build_flags的相关配置。在 Tasmota 中的真实集成AudioEncoderShineMP3mp3_shine_esp32并非孤立存在它被 Tasmota 的 I²S 音频驱动 xdrv_42_1_i2s_mp3mic_idf51.ino 以AudioEncoderShineMP3类的形式集成在#ifdef MP3_MIC_STREAM编译开关下启用负责把 I²S 麦克风采集的 PCM 实时编码为 MP3输出到文件File *rec_file或 WiFi 客户端WiFiClient *wifi。这是该库在真实固件中的权威用法范例。初始化流程beginshine_set_config_mpeg_defaults(config.mpeg); if (inputChannels 1) { config.mpeg.mode MONO; } else { config.mpeg.mode STEREO; } config.wave.samplerate samplingRate; config.wave.channels (channels)inputChannels; if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) 0) {return 3;} s shine_initialise(config); if (!s) {return 4; } samplesPerPass shine_samples_per_pass(s); byteSize samplesPerPass * 2 * inputChannels; inBuffer (int16_t*)malloc(byteSize); // byteSize samplesPerPass * sizeof(int16_t) if (!inBuffer) {return 5; }要点先填默认值 → 按输入声道数选MONO/STEREO→ 设采样率与声道 →shine_check_config校验失败返回 3→shine_initialise失败返回 4→ 用shine_samples_per_pass计算并分配输入缓冲失败返回 5。各步骤返回非零错误码便于上层诊断。编码与输出encode / writeoutFrame shine_encode_buffer_interleaved(s, inBuffer, written); return write(written);write(int len)把编码输出写入目标if (file ! nullptr) { written file-write(outFrame, len); /* 写入文件系统 */ } else if (client ! nullptr) { if (client-connected()) { written client-write(outFrame, len); /* 推送到网络流 */ } } return (written ! (size_t)len);这演示了shine_encode_buffer_interleaved返回缓冲的即时有效特性拿到written字节后立即写出绝不跨调用保存该指针。结束与资源释放stop / 析构virtual size_t stop() { outFrame shine_flush(s, written); return written; } ~AudioEncoderShineMP3() { if (s) { shine_close(s); } if (inBuffer) { free(inBuffer); } if (file) { file-close(); } if (client) { client-stop(); } }停止录音时先shine_flush取走缓冲区残留数据析构时再shine_close释放编码器、free输入缓冲并关闭文件/网络连接——与 API 文档要求的生命周期完全一致。一个通用的最小使用模板参照上述集成脱离 Tasmota 场景在裸 Arduino-ESP32 上使用该库的最小流程为#include layer3.h shine_config_t config; shine_set_config_mpeg_defaults(config.mpeg); config.mpeg.mode MONO; /* 或 STEREO / JOINT_STEREO */ config.mpeg.bitr 128; /* kbps须在码率表内 */ config.wave.samplerate 48000; /* 须在 9 个合法采样率内 */ config.wave.channels PCM_MONO; /* 与 mpeg.mode 保持一致 */ if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) 0) { // 采样率/码率组合非法及早返回 } shine_t s shine_initialise(config); if (!s) { // 内存不足或配置非法及早返回 } int samples shine_samples_per_pass(s); /* MPEG-I: 1152 */ int16_t *pcm (int16_t*)malloc(samples * 2 * config.wave.channels); // 循环采集 PCM 填充 pcm → 编码 → 立即写出 int written; unsigned char *out shine_encode_buffer_interleaved(s, pcm, written); if (out written 0) { // fwrite(out, 1, written, fp); 或 client-write(out, written); } // 结束 out shine_flush(s, written); // 写出剩余数据... shine_close(s); free(pcm);使用建议与注意事项综合 README、源码与 Tasmota 集成代码给出以下实操建议尽早初始化在程序启动早期、堆尚未碎片化时调用shine_initialise否则大块连续 RAM尤其 SPIRAM/32-bit 内存可能分配失败严格按参数表配置采样率必须是 8000/11025/12000/16000/22050/24000/32000/44100/48000 之一码率必须落在对应 MPEG 版本的码率表内组合需通过shine_check_config校验否则初始化直接失败声道模式一致性wave.channels与mpeg.mode的单/双声道语义要保持一致参考 Tasmota 中 1 声道配MONO、2 声道配STEREO的写法每帧喂足样本每次编码调用必须提供恰好shine_samples_per_pass(s)个样本/声道不足或超量都会破坏帧对齐及时消费输出返回的 MP3 缓冲在下一次编码调用即失效务必立即写入文件或网络Tasmota 中AudioEncoderShineMP3::write正是这样做的关闭前必须 flushshine_flush取走比特流缓冲区残留数据避免 MP3 尾部丢失实时性依赖-Os48000Hz Joint Stereo 单核实时编码需要-Os编译选项若现场有 PSRAM可参考源码的 SPIRAM 分配策略把大缓冲放到外部 RAM关注质量上限该编码器无心理声学模型适合语音、监控等场景对高保真音乐编码需求应评估其听感是否达标兼容性前提README 自述与当前 IDF 的兼容性未知Last updated FEB 2019但 Tasmota 仓库中它仍被 ESP-IDF 5.x 的 I²S 音频驱动所使用见 xdrv_42_1_i2s_mp3mic_idf51.ino 顶部ESP_IDF_VERSION_MAJOR 5的编译条件说明在 Tasmota 的构建体系中该库仍可正常工作。相关源码导航主题路径库元数据library.properties、library.json公开 API 与参数表layer3.h编码器实现初始化/编码/关闭/计数器layer3.cpp内部数据结构与架构分派types.hXtensa 汇编乘法mult_xtensa_gcc.h比特流缓冲bitstream.hTasmota 集成示例I²S 麦克风 → MP3xdrv_42_1_i2s_mp3mic_idf51.ino【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表