AI小智源码分析——音频部分(一)

 一、源码跳转

这里采用了函数重载来进行代码复用,当需要对I2S接口的数据进行配置,比如左右音道切换,可以使用第二个构造函数,这里小智使用的是第一个构造函数,即只传递I2S相关的引脚参数(不带slot mask),适用于常规的I2S单工音频通信场景

 二、音频模块函数的具体实现

1、构造函数

NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate,gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout,gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din)
{duplex_ = false; // 标记为单工模式input_sample_rate_ = input_sample_rate;output_sample_rate_ = output_sample_rate;// 1. 创建扬声器(Speaker)I2S通道(TX)i2s_chan_config_t chan_cfg = {.id = (i2s_port_t)0,                // 通道ID 0,代表TX(发送/播放).role = I2S_ROLE_MASTER,            // 主模式.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, // DMA描述符数量.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, // DMA帧数量.auto_clear_after_cb = true,        // 回调后自动清除.auto_clear_before_cb = false,      // 回调前不清除.intr_priority = 0,                 // 中断优先级};ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); // 创建TX通道(播放)// 2. 配置扬声器I2S标准参数i2s_std_config_t std_cfg = {.clk_cfg = {.sample_rate_hz = (uint32_t)output_sample_rate_, // 扬声器采样率.clk_src = I2S_CLK_SRC_DEFAULT,                  // 时钟源.mclk_multiple = I2S_MCLK_MULTIPLE_256,          // 主时钟倍频#ifdef   I2S_HW_VERSION_2.ext_clk_freq_hz = 0,#endif},.slot_cfg = {.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,      // 数据位宽32bit.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,       // 自动槽位宽度.slot_mode = I2S_SLOT_MODE_MONO,                 // 单声道.slot_mask = I2S_STD_SLOT_LEFT,                  // 只用左声道.ws_width = I2S_DATA_BIT_WIDTH_32BIT,            // WS宽度.ws_pol = false,                                 // WS极性.bit_shift = true,                               // 位移#ifdef   I2S_HW_VERSION_2.left_align = true,.big_endian = false,.bit_order_lsb = false#endif},.gpio_cfg = {.mclk = I2S_GPIO_UNUSED,         // 不用MCLK.bclk = spk_bclk,                // BCLK引脚(扬声器).ws = spk_ws,                    // WS引脚(扬声器).dout = spk_dout,                // DOUT数据输出(扬声器).din = I2S_GPIO_UNUSED,          // 不用数据输入.invert_flags = {.mclk_inv = false,.bclk_inv = false,.ws_inv = false}}};ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); // 初始化TX通道// 3. 创建麦克风(MIC)I2S通道(RX)chan_cfg.id = (i2s_port_t)1; // 通道ID 1,代表RX(接收/录音)ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); // 创建RX通道(录音)// 4. 配置麦克风I2S参数std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; // 麦克风采样率std_cfg.gpio_cfg.bclk = mic_sck;   // BCLK引脚(麦克风)std_cfg.gpio_cfg.ws = mic_ws;      // WS引脚(麦克风)std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; // 不用数据输出std_cfg.gpio_cfg.din = mic_din;    // DIN数据输入(麦克风)ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); // 初始化RX通道ESP_LOGI(TAG, "Simplex channels created"); // 日志输出,表示单工通道创建完成
}

 2、Write函数(播放)

int NoAudioCodec::Write(const int16_t* data, int samples) {std::vector<int32_t> buffer(samples);// output_volume_: 0-100// volume_factor_: 0-65536
1int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;for (int i = 0; i < samples; i++) {
2int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算if (temp > INT32_MAX) {buffer[i] = INT32_MAX;} else if (temp < INT32_MIN) {buffer[i] = INT32_MIN;} else {buffer[i] = static_cast<int32_t>(temp);}}
3size_t bytes_written;ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));return bytes_written / sizeof(int32_t);
}

3、Read(录音)

int NoAudioCodec::Read(int16_t* dest, int samples) {size_t bytes_read;写入到这个bit32_buffer中std::vector<int32_t> bit32_buffer(samples);i2s_channel_read是乐鑫官方的驱动库函数if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) {ESP_LOGE(TAG, "Read Failed!");return 0;}samples = bytes_read / sizeof(int32_t);for (int i = 0; i < samples; i++) {int32_t value = bit32_buffer[i] >> 12;dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value;}return samples;
}

 4、音频总结

  • Write:16位音频数据 → 音量缩放 → 转32位 → 写入I2S(播放)。
  • Read:从I2S读32位数据 → 右移还原16位 → 裁剪 → 输出(录音)。

这两个函数实现了音频数据与I2S硬件之间的格式转换和音量控制,是AI小智音频输入输出的核心桥梁。

为什么要来回裁剪?

1. I2S硬件与软件的数据位宽不同

  • I2S硬件接口(特别是ESP32/ESP-IDF的I2S驱动)通常以32位(int32_t)为一个采样点进行数据传输。
  • 音频算法/文件/协议常用16位(int16_t)作为音频采样的标准格式(比如WAV、PCM等)。

举例

  • 你录音时,I2S硬件会给你32位的数据,但实际有效音频信息只有16位,剩下的高位/低位是补零或对齐。
  • 你播放时,算法/文件给你16位数据,但I2S要求你送32位数据。

2. 音量缩放需要更高精度

  • 音量缩放时,为了避免精度损失,通常会把16位数据先提升到32位甚至64位,做乘法后再裁剪回目标位宽。
  • 这样可以保证音量调整后不会出现溢出或失真。

3. 数据对齐与移位

  • I2S驱动通常要求数据左对齐(高位有效),而16位音频是右对齐的。
  • 所以录音时要右移(比如 >> 12),播放时要左移或补零,保证数据在I2S总线上的格式正确。

4. 防止溢出

  • 在缩放和转换过程中,可能会出现超出目标类型范围的数值(比如乘以音量后超出int16_t范围)。
  • 所以要做裁剪(clamp),防止溢出导致杂音或硬件异常。

5. 总结

  • 来回裁剪是为了在不同的数据格式(16位/32位)、不同的处理需求(音量缩放、I2S对齐)之间安全、准确地转换数据。
  • 这样既能保证音频质量,又能兼容硬件和软件的不同要求。

三、采用的单工通讯,那为什么在小智说话的时候,我可以使用唤醒词打断AI小智?

你的问题非常好,涉及到“单工通信”与“唤醒词打断”之间的实际工程实现。

虽然AI小智大多数板卡采用的是I2S单工(Simplex)音频通信,但你依然可以在AI小智说话时用唤醒词打断它,原因如下:


1. “单工”是指I2S硬件通道的物理能力,不等于软件不能“同时”处理

  • 单工I2S:指的是I2S硬件在同一时刻只能“录音”或“播放”其中之一,不能同时进行。
  • 但在实际应用中,软件可以通过“快速切换”或“分时复用”来实现“看起来同时”的效果。

2. 唤醒词检测通常是“软件VAD/AFE”模块,和TTS播放分离

  • AI小智的唤醒词检测(如AFE/ESP/Custom Wake Word)并不一定依赖I2S实时采集,而是可以在TTS播放的间隙、或者通过软件算法在播放数据流中“偷听”。
  • 很多板卡的麦克风和扬声器虽然共用I2S,但软件可以在TTS播放的每一帧之间,短暂切换到录音模式,采集一小段音频用于唤醒词检测,再切回播放。
  • 这种“分时复用”在人耳听觉上几乎无感,但足以让唤醒词检测模块及时响应。

3. 代码层面的支持

  • 在 application.cc 的 SetDeviceState(kDeviceStateSpeaking) 里有如下逻辑:

    cpp

    Apply to no_audio_cod...

      if (listening_mode_ != kListeningModeRealtime) {

          audio_service_.EnableVoiceProcessing(false);

          // 只有AFE唤醒词可以在说话时检测

      #if CONFIG_USE_AFE_WAKE_WORD

          audio_service_.EnableWakeWordDetection(true);

      #else

          audio_service_.EnableWakeWordDetection(false);

      #endif

      }

  • 也就是说,如果你用的是AFE唤醒词(硬件算法或高性能算法),在AI说话时依然可以检测唤醒词,从而实现“说话时打断”。
  • 这也是为什么有些板卡/算法支持说话时打断,有些则不支持(取决于唤醒词算法和I2S复用能力)。

4. 实际体验

  • 当AI小智在说话时,软件会周期性地切换到录音模式,采集一小段音频,进行唤醒词检测。
  • 一旦检测到唤醒词,主流程会立刻中断TTS播放,切换到“监听”或“待机”状态,实现“打断”效果。

总结

  • 单工I2S ≠ 不能“边说边听”,而是“不能物理上同时说和听”,但可以“分时复用”实现“几乎同时”。
  • 唤醒词打断AI说话,靠的是软件的巧妙调度和高效的唤醒词检测算法(如AFE)。
  • 这也是AI小智工程设计的一个亮点

 

四、音频模块继承体系

  • AudioCodec 是抽象基类,只定义接口,不实现。
  • NoAudioCodec 是具体实现类,实现了 Read 和 Write,可以被实例化。
  • NoAudioCodecSimplex 等是更具体的实现,可以继承和扩展 NoAudioCodec 的功能。

1. 基类 AudioCodec

cpp

Apply to no_audio_cod...class AudioCodec {public:virtual int Read(int16_t* dest, int samples) = 0;   // 纯虚函数,必须由子类实现virtual int Write(const int16_t* data, int samples) = 0; // 纯虚函数,必须由子类实现// ... 其他接口};
  • 作用:定义了音频编解码器的统一接口。
  • 纯虚函数:= 0 表示没有实现,所有子类都必须实现,否则子类也会变成抽象类,不能实例化。

2. 派生类 NoAudioCodec

cpp

Apply to no_audio_cod...class NoAudioCodec : public AudioCodec {private:virtual int Write(const int16_t* data, int samples) override; // 实现父类的纯虚函数virtual int Read(int16_t* dest, int samples) override;        // 实现父类的纯虚函数// ... 其他成员};
  • 作用:继承自 AudioCodec,实现了 Read 和 Write 的具体逻辑(比如“无编解码器”方案)。
  • override:明确告诉编译器“我在重写父类的虚函数”。
  • 实现了纯虚函数:所以 NoAudioCodec 变成了“可实例化的具体类”。

3. 进一步的子类(如 NoAudioCodecSimplex)

cpp

Apply to no_audio_cod...class NoAudioCodecSimplex : public NoAudioCodec {public:NoAudioCodecSimplex(...); // 构造函数// 可以选择重写Read/Write,也可以直接用NoAudioCodec的实现};
  • 作用:针对不同硬件场景(如单工I2S)做进一步适配。
  • 继承链:NoAudioCodecSimplex → NoAudioCodec → AudioCodec
  • Read/Write:如果没有重写,会直接用 NoAudioCodec 的实现。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91114.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91114.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【GNSS原理】【LAMBDA】Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法[2025年7月]

Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法 作者&#xff1a;齐花Guyc(CAUC) 文章目录Chapter.12 GNSS定位算法——模糊度固定LAMBDA算法一.整周模糊度理论1.LAMBDA算法干了一件什么事情&#xff1f;2.LAMBDA算法步骤&#xff08;1&#xff09;去相关&#xff08;Z变换…

计算机毕业设计java在线二手系统的设计与实现 基于Java的在线二手交易平台开发 Java技术驱动的二手物品管理系统

计算机毕业设计java在线二手系统的设计与实现z2n189&#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。随着互联网技术的飞速发展&#xff0c;二手交易市场也逐渐从传统的线下模式转…

如何进行项目复盘?核心要点分析

进行项目复盘需要明确复盘目标、确定复盘参与人员、选择合适的复盘方法、梳理项目过程与关键节点、分析成功与失败的原因、总结经验教训并制定改进计划。其中&#xff0c;选择合适的复盘方法尤其关键&#xff0c;常见的复盘方法包括鱼骨图分析法、SWOT分析法、PDCA循环法&#…

LeetCode 923.多重三数之和

给定一个整数数组 arr &#xff0c;以及一个整数 target 作为目标值&#xff0c;返回满足 i < j < k 且 arr[i] arr[j] arr[k] target 的元组 i, j, k 的数量。 由于结果会非常大&#xff0c;请返回 109 7 的模。 示例 1&#xff1a; 输入&#xff1a;arr [1,1,2,2,…

.Net日志系统Logging-五

日志概念 日志级别 NET (Microsoft.Extensions.Logging) 中定义的 6 个标准日志级别&#xff0c;按严重性从低到高排列&#xff1a; 日志级别数值描述典型使用场景Trace0最详细的信息&#xff0c;包含敏感数据&#xff08;如请求体、密码哈希等&#xff09;。仅在开发或深度故…

中国贸促会融媒体中心出海活动负责人、出海星球创始人莅临绿算技术

近日&#xff0c;中国贸促会融媒体中心出海活动负责人、出海星球创始人王思诺一行莅临广东省绿算技术有限公司&#xff0c;深入考察其核心技术产品与全球化布局。双方围绕绿算技术全栈产品体系、创新出海模式及生态共建展开深度对话。绿算技术作为国内智算基础设施领域的领军企…

【算法专题训练】06、数组双指针

1、数组 数组是由相同类型的元素组成的数据集合&#xff0c;并且占据一块连续的内存&#xff0c;按照顺序存储数据。 1.1、数组的特性&#xff1a; 数组元素通过下标获取数据数组对象初始化时&#xff0c;需要先指定数组容量大小&#xff0c;并根据容量大小分配内存。缺点&…

操作系统-lecture2(操作系统结构)

回顾下lecture1 swap区域不可以马上执行&#xff0c;即虚拟内存的数据和指令不可以被执行&#xff0c;得交换回到内存区域 操作系统的服务 主要提供两种服务 面向普通用户&#xff1a;user interface面向程序员&#xff1a;应用级程序代码 为用户 为用户提供了操作包括但不…

内网服务器实现从公网穿透

从6月份tplink的ddns失效之后&#xff0c;对于部分在内网运行的服务器&#xff0c;从公网访问就收到了部分影响。有好几个朋友找来&#xff0c;寻求帮助&#xff0c;看看怎么恢复原来的机制&#xff0c;可以从公网互联网访问内网服务器。方案一&#xff1a;如果有动态公网的客户…

vcs-编译+仿真+dump波形【IMP】

VCS仿真分为两步式(编译/compilation仿真/simulation)和三步式(分析/analysis细化/elaborationsimulation/仿真);注2:analysis/分析是三步式flow中仿真design的第一步&#xff0c;在此阶段将使用vhdlan或vlogan分析VHDL、Verilog、SystemVerilog和OpenVera文件。下面的部分包括…

程序代码篇---python向http界面发送数据

在 Python 中向 HTTP 界面发送数据&#xff0c;本质上是模拟用户在网页上填写表单、点击提交按钮的过程。这在自动化测试、数据上报、接口调用等场景中非常常用。下面用通俗易懂的方式介绍具体方法、实例代码和解析。核心原理网页上的数据发送&#xff08;比如提交表单&#xf…

mybatis-plus由mysql改成达梦数据库

前置条件: 达梦数据库设置了大小写敏感,我比较菜,改不动!先这么凑合着用吧; 因为设置了大小写敏感,所以所有的sql语句都要加 引号; 这样是会报错的: SELECT remark,createDept,createBy,createTime,updateBy,updateTime FROM sys_oss_config这样才可以 SELECT "create_…

设计模式:外观模式 Facade

目录前言问题解决方案结构代码前言 外观是一种结构型设计模式&#xff0c;能为程序库、框架或其他复杂类提供一个简单的接口。 问题 假设你必须在代码中使用某个复杂的库或框架中的众多对象。正常情况下&#xff0c; 你需要负责所有对象的初始化工作、 管理其依赖关系并按正确…

【数据结构初阶】--二叉树(四)

&#x1f525;个人主页&#xff1a;草莓熊Lotso &#x1f3ac;作者简介&#xff1a;C研发方向学习者 &#x1f4d6;个人专栏&#xff1a; 《C语言》 《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》 ⭐️人生格言&#xff1a;生活是默默的坚持&#xff0c;毅力是永久的…

三、平面度检测-差值法

方法一: dev_get_window (WindowHandle) *读取3通道彩色融合图 read_image (Image, ./XYZ彩色融合图.tiff) *拆分3个通道 decompose3 (Image, x, y, z) *将3个通道图像转换为3D模型 xyz_to_object_model_3d (x,y, z, ObjectModel3D) *显示动态3D模型 threshold (z, Regions,…

什么是数据编排?数据编排的流程、优势、挑战及工具有哪些?

目录 一、数据编排的定义与概念 1.数据编排的基本含义 2.数据编排与相关概念的区别 3.数据编排的重要性 二、数据编排的流程 1.需求分析&#xff1a; 2.数据源识别与连接&#xff1a; 3.数据抽取&#xff1a; 4.数据转换&#xff1a; 5.数据加载&#xff1a; 6.监控…

【C++算法】82.BFS解决FloodFill算法_被围绕的区域

文章目录题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;题目链接&#xff1a; 130. 被围绕的区域 题目描述&#xff1a; 解法 BFS一层层剥开。 C 算法代码&#xff1a; class Solution {// 定义四个方向的偏移量&#xff1a;右、左、下、上int dx[4] …

商汤发布具身智能平台,让机器人像人一样和现实世界交互

7月27日&#xff0c;在“大爱无疆模塑未来”WAIC 2025大模型论坛上&#xff0c;商汤科技重磅发布「悟能」具身智能平台。「悟能」具身智能平台以商汤具身世界模型为核心引擎&#xff0c;依托商汤大装置提供端侧和云侧算力支持&#xff0c;能够为机器人、智能设备提供强大的感知…

MCP工作原理

在谈MCP原理前&#xff0c;我们先谈谈MCP的技术前身—Function Calling。1.Function Calling技术在FunctionCalling技术出现之前&#xff0c;大语言模型虽然拥有强大的知识储备和语言理解能力&#xff0c;但是只能提供自身数据库已有的信息&#xff0c;无法和外界进行信息交互。…

VSCode手动版本更新

技术背景 使用VSCode的的过程中&#xff0c;如果打开了自动更新功能&#xff0c;每隔一段时间就会有更新提示。为了保持版本的稳定性&#xff0c;我们可以在设置中将Update: Mode设置为none&#xff0c;这样就不会触发自动更新。但有时又有版本更新的需求&#xff0c;可能是版本…