当前位置: 首页 > news >正文

ESP-IDF+vscode开发ESP32第九讲——I2S工程1

目录

前言

一、 准备工作

二、代码编写

2.1 es8311.h

2.2 es8311.c

2.3 mian.c

2.4 工程日志


前言

经过第八章的学习,相信大家已经音频信号的传输的各种方式有了体系的了解。那么本章来实现基于外部音频解码器播放flash中存储的音乐的工程。

开发板是微雪的ESP32-P4-Module-DEV-KIT。ESP-IDF版本是6.0。基于第一章的模板工程。


一、准备工作

音频解码器是ES8311,上一章也简单介绍了这款芯片的构成。

首先我们要先制作一个.wav格式的音乐文件。其他格式当然也可以,不过先以该格式测试。因为WAV 格式本质是PCM 原始数据 + 一个 44 字节的文件头(最接近原生 PCM)。所以解码器可以直接使用。

我这使用软件 “Audacity” 截取了歌曲《错位时空》前一小段得到了《cwsk.wav》,转换时采样频率为48K,数据位为32位,得到大小大约9MB。注意查看在自己flash的大小,我的flash为16MB。

另外,注意去设置中把Flash size大小改为自己的实际大小。

接着去分区设置中类型选择自定义分区表 "Custom partition table CSV",因为系统默认分区表的固件程序烧录分区只有1MB,无法存储音频文件。

在项目根目录下创建自定义分区文件partitions.csv,添加下列内容。关于各种分区的解释可以去看《分区表》,其中factory是固件烧录分区,一定要大于音频文件。注意各分区对齐要求。

# ESP-IDF Partition Table # Name , Type, SubType, Offset , Size , Flags nvs , data, nvs , 0x9000 , 0x6000, phy_init , data, phy , 0xf000 , 0x1000, factory , app , factory, 0x10000, 10M , ,
分区类型对齐要求原因
data(如 nvs、phy_init)4KB(0x1000存储配置数据,Flash 擦除最小单位是 4KB
app(如 factory、ota_0/ota_1)64KB(0x10000ESP32 的 Flash MMU(内存管理单元)映射粒度是 64KB,代码加载时必须按 64KB 建立地址映射

二、代码编写

首先添加官方音频解码器驱动,这里面包含很多音频解码芯片的底层驱动。其中就有我使用的ES8311。

打开终端,输入idf.py add-dependency "espressif/esp_codec_dev^1.5.8",重新编译组件就添加成功了。

接着自定义组件es8311,把上面准备的音频文件cwsk.wav粘贴组件es8311根目录下,在组件中创建组件描述文件idf_component.yml,其中添加对上esp_codec_dev组件的依赖。这样就可以在自定义组件es8311中使用组件esp_codec_dev了。

## IDF Component Manager Manifest File dependencies: espressif/esp_codec_dev: ">=1.5.8"

完整工程如图所示

2.1 es8311.h

#ifndef __ES8311_H__ #define __ES8311_H__ #include "esp_log.h" // ESP32日志函数 #include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数 #include "FreeRTOS/task.h" // FreeRTOS任务管理函数 #include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数 #define I2S_num 0 // I2S端口号 #define role I2S_ROLE_MASTER // I2S工作模式,设置为主机模式 #define I2S_MCLK GPIO_NUM_13 // I2S主时钟引脚,连接到ES8311的MCLK引脚 #define I2S_BCLK GPIO_NUM_12 // I2S位时钟引脚,连接到ES8311的BCLK引脚 #define I2S_DOUT GPIO_NUM_9 // I2S数据输出引脚,连接到ES8311的SDOUT引脚 #define I2S_WS GPIO_NUM_10 // I2S帧同步引脚,连接到ES8311的LRCK引脚 #define I2S_DIN GPIO_NUM_11 // I2S数据输入引脚,连接到ES8311的SDIN引脚 #define sample_hz 48000 // 采样率,设置为48000Hz #define I2S_bits 32 // 每个采样的位数,设置为32位以兼容ES8311的24位数据格式 #define MCLK_multiple 256 // MCLK倍频,设置为256倍以满足ES8311的时钟要求 #define I2C_SDA GPIO_NUM_7 // I2C数据引脚,连接到ES8311的SDA引脚 #define I2C_SCL GPIO_NUM_8 // I2C时钟引脚,连接到ES8311的SCL引脚 #define NS4150_PA GPIO_NUM_53 // NS4150功放使能引脚,连接到NS4150的EN引脚 #define CONFIG_MODE_MUSIC 1 // 1表示启用音乐模式,0表示关闭 #define CONFIG_MODE_ECHO 0 // 1表示启用回声模式,0表示关闭 void audio_start(void); #endif

2.2 es8311.c

#include <stdio.h> #include "es8311.h" #include "driver/i2s_std.h" #include "driver/i2c_master.h" #include "driver/gpio.h" #include "esp_codec_dev_defaults.h" #include "esp_codec_dev.h" static char *TAG = "es8311"; static i2s_chan_handle_t tx_handle = NULL; static i2s_chan_handle_t rx_handle = NULL; void I2S_driver_init(void) { i2s_chan_config_t i2s_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_num, role); ESP_ERROR_CHECK(i2s_new_channel(&i2s_chan_config, &tx_handle, &rx_handle)); // 创建I2S通道 i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_hz), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_bits, I2S_SLOT_MODE_STEREO), .gpio_cfg = { .mclk = I2S_MCLK, .bclk = I2S_BCLK, .ws = I2S_WS, .dout = I2S_DOUT, .din = I2S_DIN, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false, }, }, }; std_cfg.clk_cfg.clk_src = I2S_CLK_SRC_APLL; std_cfg.clk_cfg.mclk_multiple = MCLK_multiple; ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_enable(tx_handle)); ESP_ERROR_CHECK(i2s_channel_enable(rx_handle)); } i2c_master_bus_handle_t I2C_driver_init(void) { // I2C主机初始化代码 i2c_master_bus_handle_t i2c_bus_handle = NULL; i2c_master_bus_config_t bus_config = { .i2c_port = I2C_NUM_0, .sda_io_num = I2C_SDA, .scl_io_num = I2C_SCL, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .flags.enable_internal_pullup = true, }; ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus_handle)); return i2c_bus_handle; }

首先进行I2S设备初始化,各API可以看我整理的《ESP32实用API指南3》上面代码实现了I2S通道的创建和I2S标准模式的创建。

查看ES8311,发现支持所有标准模式下所有格式的传输,所以我这用了最广泛使用的Philips 格式。

接着初始化ES8311控制接口I2C设备的初始化。

完成后会有以下日志(我在设置中开启了I2S调试日志),

D (439) i2s_common: tx channel is registered on I2S0 successfully Desp> ) i2s_common: rx channel is registered on I2S0 successfully D (441) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920 D (443) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (444) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz D (445) i2s_common: MCLK is pinned to GPIO13 on I2S0 D (446) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully D (447) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 1920 W (448) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz W (449) i2s_common: Trying to work at 24575996 Hz... D (450) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (451) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 4 [bclk] 3072000 Hz D (452) i2s_common: MCLK is pinned to GPIO13 on I2S0 D (452) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully D (453) i2s_common: i2s tx channel enabled D (454) i2s_common: i2s rx channel enabled

从日志中可以看出:

I2S0 的发送 (TX)接收 (RX)通道已成功注册到硬件控制器

开启了6 个 DMA 描述符,每个DMA缓冲区大小 1920 字节

音频时钟APLL期望 24.576MHz,由采样率自动计算得到,实际 24575996Hz(仅4Hz 误差)ESP32 APLL 是小数分频硬件,微小误差是正常硬件特性,完全不影响音频功能。

MCLK期望值:48000*256 = 12288000,实际值:12287998

BCLK期望值:48000*32*2 = 3072000,实际值:3072000

void ES8311_coder_init(void) { i2c_master_bus_handle_t i2c_bus_handle = I2C_driver_init(); // es8311 控制接口设置 audio_codec_i2c_cfg_t i2c_cfg = { .port = I2C_NUM_0, .addr = ES8311_CODEC_DEFAULT_ADDR, .bus_handle = i2c_bus_handle, }; const audio_codec_ctrl_if_t *ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg); assert(ctrl_if); // 判断控制接口是否创建成功 // es8311 数据接口设置 audio_codec_i2s_cfg_t i2s_cfg = { .port = I2S_num, .tx_handle = tx_handle, .rx_handle = rx_handle, .clk_src = I2S_CLK_SRC_APLL, }; const audio_codec_data_if_t *data_if = audio_codec_new_i2s_data(&i2s_cfg); assert(data_if); // es8311 GPIO接口设置 const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio(); assert(gpio_if); // es8311 编码器配置 es8311_codec_cfg_t codec_cfg = { .codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH, .ctrl_if = ctrl_if, .gpio_if = gpio_if, .pa_pin = NS4150_PA, .pa_reverted = false, .master_mode = false, .digital_mic = false, .use_mclk = true, // 查看实际电路获得 .hw_gain = { .pa_voltage = 5.0, .codec_dac_voltage = 3.3, .pa_gain = 1.6, }, .mclk_div = MCLK_multiple, }; const audio_codec_if_t *es8311_if = es8311_codec_new(&codec_cfg); assert(es8311_if); // es8311 配置 esp_codec_dev_cfg_t codec_dev_cfg = { .codec_if = es8311_if, .data_if = data_if, .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT, }; esp_codec_dev_handle_t codec_handle = esp_codec_dev_new(&codec_dev_cfg); assert(codec_handle); // 启动es8311 esp_codec_dev_sample_info_t sample_info = { .bits_per_sample = I2S_bits, .channel = I2S_SLOT_MODE_STEREO, .channel_mask =I2S_STD_SLOT_BOTH, .sample_rate = sample_hz, .mclk_multiple = MCLK_multiple, }; esp_codec_dev_open(codec_handle, &sample_info); // 设置输出音量 esp_codec_dev_set_out_vol(codec_handle, 30); // 设置输入音量 //esp_codec_dev_set_in_gain(codec_handle, 30); }

assert(_e); 断言,判断_e是不是非空指针

该组件将硬件行为抽象如下:

通讯通道抽象为两种接口:

  • audio_codec_ctrl_if_t控制接口: 主要提供read_regwrite_regAPI 来配置编解码器设备 常用控制通道包括 I2C, SPI 等
  • audio_codec_data_if_t数据接口: 主要提供readwriteAPI 用来交换音频数据 常用数据通道包括 I2S, SPI 等

esp_codec_dev为用户提供便捷的上层 API 来实现播放和录音功能。我们这使用了esp_codec_dev_new和esp_codec_dev_open。

音量统一通过 APIesp_codec_dev_set_out_vol进行设定。 默认的音量调节区间是 0 - 100,音量 100 对应为 0 dB,每个刻度对应 0.5 dB,音量 0 被特殊映射为 -96 dB。

#if CONFIG_MODE_MUSIC extern const uint8_t* music_start asm("_binary_cwsk_wav_start"); extern const uint8_t* music_end asm("_binary_cwsk_wav_end"); #endif void music_task(void* args); void audio_start(void) { I2S_driver_init(); ES8311_coder_init(); #if CONFIG_MODE_MUSIC xTaskCreate(music_task, "music_task", 4096, NULL, 5, NULL); #elif CONFIG_MODE_ECHO xTaskCreate(echo_task, "echo_task", 4096, NULL, 5, NULL); #endif } #if CONFIG_MODE_MUSIC void music_task(void* args) { size_t bytes_write = 0; uint8_t* data_ptr = (uint8_t *)music_start+44; ESP_ERROR_CHECK(i2s_channel_disable(tx_handle)); ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_end - data_ptr, &bytes_write)); data_ptr += bytes_write; ESP_LOGI(TAG, "music len: %d", music_end - data_ptr); ESP_ERROR_CHECK(i2s_channel_enable(tx_handle)); while(1) { i2s_channel_write(tx_handle, data_ptr, music_end - data_ptr, &bytes_write, portMAX_DELAY); data_ptr = (uint8_t *)music_start+44; ESP_LOGI(TAG, "i2s music played, %d bytes are written", bytes_write); } } #endif

添加依赖访问如下:

idf_component_register(SRCS "es8311.c" INCLUDE_DIRS "include" PRIV_REQUIRES esp_driver_i2s esp_driver_i2c esp_driver_gpio EMBED_FILES "cwsk.wav")

EMBED_FILES "cwsk.wav"是 ESP-IDF 工程中专门用于嵌入二进制文件到固件的 CMake 配置。表示将 cwsk.wav 音频文件以原始二进制数据的形式烧录到芯片 Flash 中。并自动生成链接器符号:

_binary_cwsk_wav_start(文件起始地址)

_binary_cwsk_wav_end(文件结束地址)

_binary_cwsk_wav_size(文件总大小)

注意:链接器生成的这些不是一个 "存储地址的指针变量",而是一个直接代表 Flash 地址的标签(常量地址)。

在开头也有两个很特殊的用法

extern const uint8_t music_start[] asm("_binary_canon_pcm_start"); extern const uint8_t music_end[] asm("_binary_canon_pcm_end");

extern表示:这两个符号不在该 C 文件中定义,由外部提供。

const:强制只读,避免修改

asm(x):GCC 特殊扩展语法,修改变量x在链接层的符号名。

这是固定搭配操作,相当于给_binary_cwsk_wav_start重命名为music_start ,给_binary_cwsk_wav_end重命名为music_end,所以必须加extern,因为不是新建指针。

注意:只有数组名能直接映射这个地址标签,如果换成指针变量会因为「间接寻址」出错。

因为I2S只传输PCM 原始数据,所以首地址data_ptr为music_start+44,避开文件头。

在music任务内,首先先关闭通道,将通道状态变为就绪态,方便调用函数i2s_channel_preload_data,该函数会提前储存一段音频信息在DMA缓冲区,接着启动通道,通道状态变为运行态。

注意:此时首地址data_ptr要下移,覆盖提前储存的数据

作用:启动播放时无静音、无爆音,无缝开始。可以删除

在while循环中,会不断地重复播放音乐,注意此时预存储的音频数据,所以首地址要恢复过来。另外函数i2s_channel_write是DMA阻塞,阻塞过程可以运行其他任务,对CPU负担小。

I2S 的数据传输(包括数据发送和接收)由 DMA 实现。在传输数据之前,请调用 i2s_channel_enable()来启用特定的通道。发送或接收的数据达到 DMA 缓冲区的大小时,将触发I2S_OUT_EOFI2S_IN_SUC_EOF中断。注意,DMA 缓冲区的大小等于dma_buffer_size = dma_frame_num * slot_num * slot_bit_width / 8

传输数据时,可以调用i2s_channel_write() 来输入数据,并把数据从源缓冲区复制到 DMA TX 缓冲区等待传输完成。此过程将重复进行,直到发送的字节数达到配置的大小。接收数据时,用户可以调用函数 i2s_channel_read()来等待接收包含 DMA 缓冲区地址的消息队列,从而将数据从 DMA RX 缓冲区复制到目标缓冲区。

i2s_channel_write() 和 i2s_channel_read()都是阻塞函数,在源缓冲区的数据发送完毕前,或是整个目标缓冲区都被加载数据占用时,它们会一直保持等待状态。在等待时间达到最大阻塞时间时,返回ESP_ERR_TIMEOUT错误。

#include <stdio.h> #include "user.h" #include "es8311.h" void app_main(void) { CONSOLE_REPL_INIT(); // 初始化控制台REPL环境 //ESP_LDOV4_SET(3300); audio_start(); while(1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }

2.4 工程日志

D (909) i2s_common: tx channel is registered on I2S0 successfully D (909) i2s_common: rx channel is registered on I2S0 successfully D (910) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960 D (912) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (913) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz D (914) i2s_common: MCLK is pinned to GPIO13 on I2S0 D (915) i2s_std: The tx channel on I2S0 has been initialized to STD mode successfully D (916) i2s_common: DMA malloc info: dma_desc_num = 6, dma_desc_buf_size = dma_frame_num * slot_num * data_bit_width = 960 W (917) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz W (919) i2s_common: Trying to work at 24575996 Hz... D (919) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (920) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz D (921) i2s_common: MCLK is pinned to GPIO13 on I2S0 D (922) i2s_std: The rx channel on I2S0 has been initialized to STD mode successfully esp> D (923) i2s_common: i2s tx channel enabled D (924) i2s_common: i2s rx channel enabled I (931) ES8311: Work in Slave mode D (934) i2s_common: i2s tx channel disabled D (935) i2s_common: i2s rx channel disabled W (935) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz W (936) i2s_common: Trying to work at 24575996 Hz... D (937) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (938) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz I (939) I2S_IF: STD: TX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3 I (940) I2S_IF: STD: TX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20 W (941) i2s_common: APLL is occupied already, it is working at 24575996 Hz while the expected frequency is 24576000 Hz W (942) i2s_common: Trying to work at 24575996 Hz... D (943) i2s_common: APLL expected frequency is 24576000 Hz, real frequency is 24575996 Hz D (944) i2s_std: Clock division info: [sclk] 24575996 Hz [mdiv] 2 0/2 [mclk] 12287998 Hz [bdiv] 8 [bclk] 1536000 Hz I (945) I2S_IF: STD: RX, data_bit: 16, slot_bit: 16, ws_width: 16, slot_mode: STEREO, slot_mask: 0x3 I (946) I2S_IF: STD: RX, sample_rate_hz: 48000, mclk_multiple: 256, clk_src: 20 D (947) i2s_common: i2s tx channel enabled D (947) i2s_common: i2s rx channel enabled I (963) Adev_Codec: Open codec device OK D (964) i2s_common: i2s tx channel disabled I (965) es8311: music len: 3731254 D (965) i2s_common: i2s tx channel enabled I (20401) es8311: i2s music played, 3731254 bytes are written I (39865) es8311: i2s music played, 3737014 bytes are written I (59330) es8311: i2s music played, 3737014 bytes are written I (78791) es8311: i2s music played, 3737014 bytes are written I (98255) es8311: i2s music played, 3737014 bytes are written I (117720) es8311: i2s music played, 3737014 bytes are written

三、拓展

再稍微加一点代码就能实现回声播放的功能。改变如下:

3.1 es8311.h

#define CONFIG_MODE_MUSIC 0 // 1表示启用音乐模式,0表示关闭 #define CONFIG_MODE_ECHO 1 // 1表示启用回声模式,0表示关闭

3.2 es8311.c

增加下列代码

#if CONFIG_MODE_ECHO void echo_task(void* args) { int read_buf_size = 1024*4; // 4KB的缓冲区大小,可以根据需要调整 int* mic_buf = malloc(read_buf_size); // 分配麦克风数据缓冲区 size_t bytes_read = 0; size_t bytes_written = 0; while(1) { memset(mic_buf, 0, read_buf_size); // 清空麦克风数据缓冲区 i2s_channel_read(rx_handle, mic_buf, read_buf_size, &bytes_read, pdMS_TO_TICKS(1000)); ESP_LOGI(TAG, "i2s echo played, %d bytes are read", bytes_read); i2s_channel_write(tx_handle, mic_buf, bytes_read, &bytes_written, pdMS_TO_TICKS(1000)); ESP_LOGI(TAG, "i2s echo played, %d bytes are written", bytes_written); } } #endif
http://www.jsqmd.com/news/720924/

相关文章:

  • 开源数据备份实战:如何高效永久保存微信聊天记录
  • 终极免费Switch模拟器Ryujinx:5分钟快速上手指南
  • 2026年3月网带生产商推荐,不锈钢链板/非标链条/平顶链板/金属网带/滚筒输送机/爬坡输送机,网带制造企业如何选 - 品牌推荐师
  • 论文降AI选错工具会怎样?从90%降到4%中间踩了哪些坑全公开! - 我要发一区
  • 终极Windows更新修复指南:如何用Reset Windows Update Tool快速解决更新问题
  • 如何实现微信聊天记录永久保存:WeChatMsg技术解析与应用指南
  • 【App Service】查看Application Insights自身SDK日志的方法示例
  • 如何掌握Undecimus的5个高效调试技巧:从问题诊断到完美解决
  • 2026最权威的六大AI写作助手推荐
  • geopanda库GIS地理分析
  • 2026年厦门专升本公司最新TOP实力排行:专升本辅导中心/专升本培训辅导班/专升本考试培训班升本/专升 - 品牌策略师
  • 20240429
  • 跟着 MDN 学 HTML day_3:(表单CSS美化实战与盒子模型三大核心属性详解)
  • 保姆级教程:用MQTT.fx 1.7.1连接OneNET平台,从设备创建到数据收发全流程
  • Winhance:你的Windows性能加速器,3大核心功能让电脑重获新生
  • 研途从容落笔,Paperxie 智能撰写赋能毕业论文全阶创作
  • P4592 [TJOI2018] 异或 - Link
  • 20254121 2025-2026-2 《Python程序设计》实验3报告
  • 开源色彩管理革命:OpenColorIO配置为ACES的终极指南
  • 别再只抄代码了!手把手教你用逻辑分析仪调试STM32与DS1302的SPI时序
  • LongWayToGo
  • 终极风扇控制指南:告别噪音与过热的专业解决方案
  • 成都二手上下铁床供应商|十年工厂,员工宿舍高低床/工地双层床/可定制 - 企业推荐师
  • 降AI怎么花钱才不冤枉?按学校要求+预算4种情况分类推荐工具! - 我要发一区
  • 萌宝成长助手,轻松带娃
  • 嘎嘎降的1000字免费试用够不够看出效果?万字论文实测拆解! - 我要发一区
  • 成都二手上下铁床厂家|自有工厂,全新二手上下铺铁架床 批量供货 - 企业推荐师
  • 如何用Faster-Whisper-GUI实现高效音频视频转文字
  • 为什么你的Swoole-LLM服务上线3天就OOM?揭秘内存管理、协程调度、流控熔断的4层防护架构
  • ChatGPT机器人集成实战:从API调用到生产级对话系统构建