前言

        前面的章节我们从认识摄像头开始,逐渐认识的YCbCr,并对其进行了H264的编码以及MP4封装。整个过程中,我们大致使用了V4L2和FFmpeg这两个重量级工具,就像我们前面章节所讲,V4L2只是给图像做服务的,并不参与音频。

        在第3章中,我们说过V4L2这位大哥只管视频,不管音频。那音频谁来管?ALSA大佬管!

        那么这一章,我们重点讨论一下ALSA,并且我们列一个目标:从USB摄像头获取音频流,并且编码成AAC!

一、ALSA介绍

1、ALSA是什么
  • 全称:​​ ​Advanced Linux Sound Architecture​ (高级 Linux 声音架构)
  • 本质:​​ ​Linux 内核的音频子系统和驱动框架。它提供了从底层硬件声卡驱动到上层用户空间应用程序接口(API)的一整套解决方案。
  • 目的:​​ 管理和驱动计算机的声卡硬件,允许应用程序播放和录制声音。
  • 历史:​​ 在 2.6 内核中正式取代了老旧的 ​OSS (Open Sound System)​,成为 Linux 默认的标准声音系统。
2、ALSA 的核心组成部分和功能
  1. 内核驱动:​

    • 这部分包含在内核源代码中 (/sound 目录)。
    • 直接与物理声卡硬件(集成、独立声卡、USB 声卡等)通信,处理中断、DMA、硬件寄存器读写等底层操作。
    • 为每种支持的声卡芯片或型号提供特定的驱动程序模块,等我们有能力后,也可以为一个音频芯片或驱动模块编写驱动程序,现在还是先用起来。
  2. 用户空间库 (libasound.so - ALSA library):​

    • 这是应用程序主要交互的接口。
    • 提供了一组丰富、统一的 API (称为 ​ALSA API​ 或 ​alsa-lib API),让应用程序开发者无需关心底层硬件的细节即可播放或录制音频。
    • 库负责将应用程序的请求(如“播放这个 PCM 数据流”)传递给内核驱动,并处理缓冲区、格式转换、插件等高级功能。
    • 支持多种音频格式(采样率、位深、通道数)、参数设置(缓冲区大小、周期数)。
  3. 设备文件 (/dev/snd/ 目录下):​

    • 内核驱动为用户空间暴露的接口文件。虽然应用程序通常通过 libasound 访问音频功能,但理解这些设备文件有助于调试。
    • 主要设备:
      • /dev/snd/controlC#: 控制设备 (Control device),用于混音器控制(如 alsamixer / amixer 使用)。
      • /dev/snd/pcmC#D#: PCM 播放/录制设备 (Playback/Capture device)。C# 表示声卡号 (Card),D# 表示该声卡上的设备号 (Device)。我们编程程序的时候,会用到这个。

二、ALSA初体验

        为了能够在程序中使用ALSA,需要安装ALSA开发库:

sudo apt-get install libasound2-dev

        下面直接给出通过ALSA获取USB摄像头的PCM音频数据的代码,并根据这份代码进行讲解:

#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>#define SAMPLE_RATE     22050   // 采样率
#define CHANNELS        1       // 单声道
#define PERIOD_SIZE     512     // 周期大小
#define PERIODS        4        // 缓冲区周期数
#define RECORD_SECONDS 5        // 录音时长(秒)int main() {int rc;snd_pcm_t *capture_handle;snd_pcm_hw_params_t *hw_params;FILE *pcm_file;short *buffer;int dir = 0;// 1. 打开音频设备rc = snd_pcm_open(&capture_handle, "plughw:1,0", SND_PCM_STREAM_CAPTURE, 0);if (rc < 0) {fprintf(stderr, "无法打开音频设备: %s\n", snd_strerror(rc));return 1;}// 2. 分配硬件参数结构snd_pcm_hw_params_alloca(&hw_params);// 3. 初始化硬件参数rc = snd_pcm_hw_params_any(capture_handle, hw_params);if (rc < 0) {fprintf(stderr, "无法初始化硬件参数: %s\n", snd_strerror(rc));goto cleanup;}// 4. 设置访问类型(交错模式)rc = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);if (rc < 0) {fprintf(stderr, "无法设置访问类型: %s\n", snd_strerror(rc));goto cleanup;}// 5. 设置采样格式(16位小端)rc = snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16_LE);if (rc < 0) {fprintf(stderr, "无法设置采样格式: %s\n", snd_strerror(rc));goto cleanup;}// 6. 设置采样率unsigned int sample_rate = SAMPLE_RATE;rc = snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &sample_rate, &dir);if (rc < 0) {fprintf(stderr, "无法设置采样率: %s\n", snd_strerror(rc));goto cleanup;}printf("实际采样率: %u Hz\n", sample_rate);// 7. 设置声道数(单声道)rc = snd_pcm_hw_params_set_channels(capture_handle, hw_params, CHANNELS);if (rc < 0) {fprintf(stderr, "无法设置声道数: %s\n", snd_strerror(rc));goto cleanup;}// 8. 设置周期大小snd_pcm_uframes_t period_size = PERIOD_SIZE;rc = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params, &period_size, &dir);if (rc < 0) {fprintf(stderr, "无法设置周期大小: %s\n", snd_strerror(rc));goto cleanup;}printf("实际周期大小: %lu 帧\n", period_size);// 9. 设置周期数(缓冲区大小 = 周期大小 * 周期数)unsigned int periods = PERIODS;rc = snd_pcm_hw_params_set_periods_near(capture_handle, hw_params, &periods, &dir);if (rc < 0) {fprintf(stderr, "无法设置周期数: %s\n", snd_strerror(rc));goto cleanup;}printf("实际周期数: %u\n", periods);// 10. 应用硬件参数rc = snd_pcm_hw_params(capture_handle, hw_params);if (rc < 0) {fprintf(stderr, "无法设置参数: %s\n", snd_strerror(rc));goto cleanup;}// 11. 准备音频缓冲区buffer = malloc(period_size * sizeof(short));if (!buffer) {fprintf(stderr, "无法分配缓冲区\n");goto cleanup;}// 12. 打开输出文件pcm_file = fopen("test.pcm", "wb");if (!pcm_file) {fprintf(stderr, "无法创建输出文件\n");goto cleanup;}printf("开始录音...\n");// 13. 录音循环int frames = 0;const int total_frames = (sample_rate * RECORD_SECONDS) / period_size;while (frames < total_frames) {rc = snd_pcm_readi(capture_handle, buffer, period_size);if (rc == -EPIPE) {fprintf(stderr, "缓冲区溢出,正在恢复\n");snd_pcm_prepare(capture_handle);continue;} else if (rc < 0) {fprintf(stderr, "读取错误: %s\n", snd_strerror(rc));break;} else if (rc != period_size) {fprintf(stderr, "短帧读取,期望 %lu,实际 %d\n", period_size, rc);}// 写入PCM数据到文件fwrite(buffer, sizeof(short), rc, pcm_file);frames++;printf("\r已录制 %.1f 秒... ", (float)frames * period_size / sample_rate);fflush(stdout);}printf("\n录音完成!保存为 test.pcm\n");cleanup:if (capture_handle) {snd_pcm_close(capture_handle);}if (buffer) {free(buffer);}if (pcm_file) {fclose(pcm_file);}return 0;
}

        代码整体上还是比较简单的,逻辑也很清晰,只对新出现的部分做一些补充:

        1、snd_pcm_xxx是ALSA库(alsa-lib)接口,编译的时候,需要链接 -lsound

        2、snd_pcm_open的参数中,有一个“plughw:1,0”,这里的plug指的是插件,比如应用程序想要获取44100采样率的数据,但是硬件只支持22050,那么plug就可以自动将音频数据从22050转成44100给到应用程序,主要是考虑到兼容性问题。但是在嵌入式中,音频硬件和驱动是固定的,不考虑兼容性,所以在打开音频设备时,使用“hw:1,0”即可。毕竟兼容性是要牺牲算力资源和内存资源的。

        3、“hw:1,0“的命名规则如下:

        card如果是0,代表是系统默认声卡。如果是1,一般是外接声卡,比如USB声卡。

        设备号是从0开始的,我们的USB摄像头设备号只有一个,其他的不清楚。

        所以“hw:1,0”表示的是:硬件访问方式,外置声卡,且声卡的设备号为0。

        在Ubuntu中,在插入USB摄像头之前,/dev/snd里面的设备如下:

by-path  controlC0  midiC0D0  pcmC0D0c  pcmC0D0p  pcmC0D1p  seq  timer

        插入USB摄像头后,/dev/snd里面的设备如下:

by-id  by-path  controlC0  controlC1  midiC0D0  pcmC0D0c  pcmC0D0p  pcmC0D1p  pcmC1D0c  seq  timer

        可以看到多出了“controlC1”和“pcmC1D0c”,后者就是声卡1,设备0。

        4、周期是什么?

        在代码中,音频的采样率(每秒钟采样多少个点)和单声道都好理解,但是PERIODS/PERIODS_SIZE是什么?

        当ALSA从USB摄像头获取到音频数据后,会循环放在P个buffer中,这个P就是就是周期数,也就是PERIODS。每个音频帧的大小都是固定的,也就是PERRIODS_SIZE。其中音频帧又是交错格式(Interleaved)存储的。如果是立体声(左右双声道):L R L R... 我手里的摄像头是单声道的,就算是交错模式,存储方式也是单声道的:L L L L ...

        在snd_pcm_readi读取音频数据时,有一个错误EPIPE处理。

        在播放音频的时候,EPIPE代表的是欠载,表示应用程序填充数据太慢,硬件已经消耗完所有的周期,需要加快数据填充用于播放。

        在录音的时候,EPIPE代表的是超限,意思是应用程序snd_pcm_readi读取的不及时,导致buffer中的数据溢出了,需要及时读取。

        5、编译并运行

gcc uvc_voice_streaming.c -o uvc_voice_streaming -lasound -lavcodec -lavutil

        运行后,就可以生成test.pcm,因为这个是pcm数据,没有头部结构,一般的播放器无法进行播放。笔者使用的是GoldWave,在打开的时候,参数选项要正确,否则无法正确播放录音。

三、使用FFmpeg进行AAC编码

        FFmpeg在前面几章介绍过,虽然一个是视频,一个是音频,但是处理方式都差不多,这里就不再重复。

#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <time.h>#define SAMPLE_RATE     22050   // 采样率
#define CHANNELS        1       // 单声道
#define PERIOD_SIZE     512     // 周期大小
#define PERIODS        4        // 缓冲区周期数
#define RECORD_SECONDS 5        // 录音时长(秒)// ADTS头部长度为7个字节
static uint8_t *adts_header = NULL;// 生成ADTS头
static void add_adts_header(uint8_t *header, int packet_size, int sample_rate_index, int channels) {// Sync Pointheader[0] = 0xFF;header[1] = 0xF1;// Profile(2), Sampling Freq(4), Private(1), Channel Config(1)header[2] = ((2 - 1) << 6)  // AAC-LC = 2| (sample_rate_index << 2)| ((channels & 4) >> 2);// Channel Config(2), Original(1), Home(1), Copyright ID(1), Copyright Start(1), Frame Length(2)header[3] = ((channels & 3) << 6)| ((packet_size + 7) >> 11);// Frame Length(8)header[4] = ((packet_size + 7) >> 3) & 0xFF;// Frame Length(3), Buffer Fullness(5)header[5] = (((packet_size + 7) & 0x07) << 5)| 0x1F;// Buffer Fullness(6), Raw Data Blocks(2)header[6] = 0xFC;
}// 获取采样率索引
static int get_sample_rate_index(int sample_rate) {int sample_rates[] = {96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000};for (int i = 0; i < 12; i++) {if (sample_rate == sample_rates[i]) {return i;}}return 7; // 默认使用22050Hz的索引
}int main() {int rc;snd_pcm_t *capture_handle;snd_pcm_hw_params_t *hw_params;FILE *aac_file;short *buffer;int dir = 0;// FFmpeg变量AVCodec *codec = NULL;AVCodecContext *codec_ctx = NULL;AVFrame *frame = NULL;AVPacket *pkt = NULL;// 1. 打开音频设备rc = snd_pcm_open(&capture_handle, "plughw:1,0", SND_PCM_STREAM_CAPTURE, 0);if (rc < 0) {fprintf(stderr, "无法打开音频设备: %s\n", snd_strerror(rc));return 1;}// 2. 分配硬件参数结构snd_pcm_hw_params_alloca(&hw_params);// 3. 初始化硬件参数rc = snd_pcm_hw_params_any(capture_handle, hw_params);if (rc < 0) {fprintf(stderr, "无法初始化硬件参数: %s\n", snd_strerror(rc));goto cleanup;}// 4. 设置访问类型(交错模式)rc = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);if (rc < 0) {fprintf(stderr, "无法设置访问类型: %s\n", snd_strerror(rc));goto cleanup;}// 5. 设置采样格式(16位小端)rc = snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16_LE);if (rc < 0) {fprintf(stderr, "无法设置采样格式: %s\n", snd_strerror(rc));goto cleanup;}// 6. 设置采样率unsigned int sample_rate = SAMPLE_RATE;rc = snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &sample_rate, &dir);if (rc < 0) {fprintf(stderr, "无法设置采样率: %s\n", snd_strerror(rc));goto cleanup;}printf("实际采样率: %u Hz\n", sample_rate);// 7. 设置声道数(单声道)rc = snd_pcm_hw_params_set_channels(capture_handle, hw_params, CHANNELS);if (rc < 0) {fprintf(stderr, "无法设置声道数: %s\n", snd_strerror(rc));goto cleanup;}// 8. 设置周期大小snd_pcm_uframes_t period_size = PERIOD_SIZE;rc = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params, &period_size, &dir);if (rc < 0) {fprintf(stderr, "无法设置周期大小: %s\n", snd_strerror(rc));goto cleanup;}printf("实际周期大小: %lu 帧\n", period_size);// 9. 设置周期数(缓冲区大小 = 周期大小 * 周期数)unsigned int periods = PERIODS;rc = snd_pcm_hw_params_set_periods_near(capture_handle, hw_params, &periods, &dir);if (rc < 0) {fprintf(stderr, "无法设置周期数: %s\n", snd_strerror(rc));goto cleanup;}printf("实际周期数: %u\n", periods);// 10. 应用硬件参数rc = snd_pcm_hw_params(capture_handle, hw_params);if (rc < 0) {fprintf(stderr, "无法设置参数: %s\n", snd_strerror(rc));goto cleanup;}// 初始化FFmpeg编码器codec = avcodec_find_encoder(AV_CODEC_ID_AAC);if (!codec) {fprintf(stderr, "找不到AAC编码器\n");goto cleanup;}codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx) {fprintf(stderr, "无法分配编码器上下文\n");goto cleanup;}// 设置AAC编码器参数codec_ctx->bit_rate = 64000;  // 64 kbpscodec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;  // AAC需要浮点平面格式codec_ctx->sample_rate = SAMPLE_RATE;codec_ctx->channel_layout = AV_CH_LAYOUT_MONO;  // 单声道codec_ctx->channels = CHANNELS;codec_ctx->profile = FF_PROFILE_AAC_LOW;  // AAC-LCrc = avcodec_open2(codec_ctx, codec, NULL);if (rc < 0) {char errbuf[AV_ERROR_MAX_STRING_SIZE];av_strerror(rc, errbuf, AV_ERROR_MAX_STRING_SIZE);fprintf(stderr, "无法打开编码器: %s\n", errbuf);goto cleanup;}// 分配音频帧frame = av_frame_alloc();if (!frame) {fprintf(stderr, "无法分配音频帧\n");goto cleanup;}frame->nb_samples = codec_ctx->frame_size;frame->format = codec_ctx->sample_fmt;frame->channel_layout = codec_ctx->channel_layout;frame->sample_rate = codec_ctx->sample_rate;rc = av_frame_get_buffer(frame, 0);if (rc < 0) {fprintf(stderr, "无法分配帧缓冲区\n");goto cleanup;}// 分配数据包pkt = av_packet_alloc();if (!pkt) {fprintf(stderr, "无法分配数据包\n");goto cleanup;}// 分配ADTS头部缓冲区adts_header = (uint8_t *)malloc(7);if (!adts_header) {fprintf(stderr, "无法分配ADTS头部缓冲区\n");goto cleanup;}// 准备音频缓冲区buffer = malloc(period_size * sizeof(short));if (!buffer) {fprintf(stderr, "无法分配缓冲区\n");goto cleanup;}// 打开输出文件aac_file = fopen("test.aac", "wb");if (!aac_file) {fprintf(stderr, "无法创建输出文件\n");goto cleanup;}printf("开始录音...\n");// 录音循环int frames = 0;const int total_frames = (SAMPLE_RATE * RECORD_SECONDS) / period_size;float *samples = (float *)frame->data[0];int samples_index = 0;while (frames < total_frames) {rc = snd_pcm_readi(capture_handle, buffer, period_size);if (rc == -EPIPE) {fprintf(stderr, "缓冲区溢出,正在恢复\n");snd_pcm_prepare(capture_handle);continue;} else if (rc < 0) {fprintf(stderr, "读取错误: %s\n", snd_strerror(rc));break;}// 将PCM数据转换为浮点格式并填充到framefor (int i = 0; i < rc; i++) {samples[samples_index++] = buffer[i] / 32768.0f;if (samples_index >= frame->nb_samples) {// 帧满了,进行编码rc = avcodec_send_frame(codec_ctx, frame);if (rc < 0) {fprintf(stderr, "发送帧失败\n");goto cleanup;}while (rc >= 0) {rc = avcodec_receive_packet(codec_ctx, pkt);if (rc == AVERROR(EAGAIN) || rc == AVERROR_EOF) {break;} else if (rc < 0) {fprintf(stderr, "接收包失败\n");goto cleanup;}// 添加ADTS头add_adts_header(adts_header, pkt->size, get_sample_rate_index(SAMPLE_RATE), CHANNELS);// 写入ADTS头和AAC数据fwrite(adts_header, 1, 7, aac_file);fwrite(pkt->data, 1, pkt->size, aac_file);av_packet_unref(pkt);}samples_index = 0;}}frames++;printf("\r已录制 %.1f 秒... ", (float)frames * period_size / SAMPLE_RATE);fflush(stdout);}// 刷新编码器avcodec_send_frame(codec_ctx, NULL);while (1) {rc = avcodec_receive_packet(codec_ctx, pkt);if (rc == AVERROR_EOF) {break;} else if (rc < 0) {fprintf(stderr, "刷新编码器失败\n");break;}// 添加ADTS头并写入最后的数据add_adts_header(adts_header, pkt->size, get_sample_rate_index(SAMPLE_RATE), CHANNELS);fwrite(adts_header, 1, 7, aac_file);fwrite(pkt->data, 1, pkt->size, aac_file);av_packet_unref(pkt);}printf("\n录音完成!保存为 test.aac\n");cleanup:if (capture_handle) {snd_pcm_close(capture_handle);}if (buffer) {free(buffer);}if (aac_file) {fclose(aac_file);}if (codec_ctx) {avcodec_free_context(&codec_ctx);}if (frame) {av_frame_free(&frame);}if (pkt) {av_packet_free(&pkt);}if (adts_header) {free(adts_header);}return 0;
}

        代码的逻辑关系如下:

        该代码是在上一节代码基础上修改的。snd_pcm_readi之后,使用FFmpeg处理。这里只讲新的知识点。

        1、重采样:

        USB摄像头传过来的音频数据是交错模式(interleaved),即立体声的时候排列方式是:L R L R...

        但是FFmpeg要求的是平面格式(Plannar),即每个声道单独存放:LLLL...RRR...

        因为我们只有单通道,所以存储格式不需要更改,后面我们见到重采样就知道怎么回事的。代码里面只是对音频数据进行了浮点重采样,因为FFmpeg是要求浮点的。

        2、AAC

        AAC(Advanced Audio Coding)是现代音频压缩技术的巅峰之作,代表了心理声学模型应用的最高水平。作为MPEG-4标准的核心音频技术,AAC在效率、质量和灵活性方面都超越了前代MP3标准。笔者并没有对AAC做过多研究,有兴趣的道友可以稍微深入一下。

AAC常见容器格式

格式特点使用场景
​.aac原始ADTS流简单存储
​.m4a​MP4容器iTunes标准
.mp4​视频容器视频伴音
.3gp​移动设备手机录制
​.ts​传输流数字电视

        3、ADTS头结构

// ADTS头示例
uint8_t adts[7] = {0xFF, // Sync byte 10xF1, // Sync byte 2 + 保护位0x50, // 配置信息 (AAC-LC, 44.1kHz)0x80, // 声道配置 + 帧长度高位0x1F, // 帧长度中位0xFC, // 帧长度低位 + 缓冲区0x70  // 帧计数器
};

        有了ADTS头,现代一般的播放器就能识别。

        4、编译和运行

gcc -o uvc_voice_streaming uvc_voice_streaming_aac+adts.c -lasound -lavcodec -lavutil -lm

        运行后,可以得到test.aac文件,使用VLC或者其他播放器一般是可以播放的。

四、总结

        本章节主要讨论了ALSA框架下的音频获取的过程,可以看到应用程序还是比较简单的,不需要对底层有过多的关注。

        并使用FFmpeg对音频数据进行了AAC编码,有了之前的章节,我们对FFmpeg使用基本上已经得心应手了。

        下一章将面临关键的挑战:如何精确同步来自V4L2的视频帧和来自ALSA的音频包的时间戳,并使用FFmpeg将它们无缝地封装进MP4文件,实现真正的音视频录制。

        再之后我们就要进入真正运动相机硬件方面的探讨了。

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

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

相关文章

Linux 命令:useradd

Linux useradd 命令详细教程 useradd 是 Linux 系统中用于创建新用户账户的基础命令&#xff0c;它通过配置文件&#xff08;如 /etc/passwd、/etc/shadow&#xff09;和默认设置自动完成用户创建流程。本文将详细介绍其用法、参数及相关配置。资料已经分类整理好&#xff1a;h…

Pytest之收集用例规则与运行指定用例

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 小伙伴们大家好呀&#xff0c;今天笔者会给大家讲解一下pytest是如何收集我们写好的用例&#xff1f;我们又有哪些方式来运行单个用例或者批量运行用例呢&#xff…

qt 使用memcpy进行内存拷贝时注意的问题

int offset sizeof(st_target_data);// 预先分配足够空间this->featureData.resize(offsetsize);// 再执行拷贝memcpy(this->featureData.data()offset, dataa, size);注意 一定要在mencpy之前 使用resize分配足够的空间&#xff0c;否则在方法退出时候会闪退&#xff…

微调性能赶不上提示工程怎么办?Can Gradient Descent Simulate Prompting?——论文阅读笔记

今天速读一篇文章 Can Gradient Descent Simulate Prompting? 一句话总结 针对【新知识应用的场景里&#xff0c;FT效果往往追不上ICL】这个情况&#xff0c;作者引入MAML的思想↓ 内圈让模型学习新知识形成知识FT模型&#xff1b; 外圈通过最小化ICL和知识FT模型的KL散度&…

从“直觉抢答”到“深度思考”:大模型的“慢思考”革命,思维链、树、图如何让AI越来越像人?

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《GPT多模态大模型与AI Agent智能体》&#xff08;跟我一起学人工智能&#xff09;【陈敬雷编著】【清华大学出版社】 GPT多模态大模型与AI Agent智能体书籍本章配套视频课程【陈敬雷】 文…

Android系统的问题分析笔记 - Android上的调试方式 debuggerd

debuggerd 是 Android 系统中的一个重要调试工具&#xff0c;主要用于生成进程崩溃时的核心转储&#xff08;core dump&#xff09;和调试信息&#xff08;如堆栈跟踪&#xff09;。以下是关于 debuggerd 的详细说明&#xff1a; 1. 基本功能 崩溃分析&#xff1a;当 Native 进…

python 双下划线开头函数

在 Python 里&#xff0c;双下划线开头的函数&#xff08;准确地说是方法&#xff09;有着特殊的用途和意义。下面为你详细介绍相关内容&#xff1a; 1. 类的特殊方法&#xff08;魔术方法&#xff09; 以双下划线开头和结尾的方法&#xff0c;被称为特殊方法或者魔术方法&…

VyOS起步指南:用Docker快速搭建网络实验环境

文章目录1. VyOS是什么&#xff1f;为什么选择它&#xff1f;2. 五分钟快速部署&#xff1a;Docker方案3. 进入容器&#xff1a;初探VyOS世界4. 核心操作&#xff1a;像开发者一样思考5. 踩坑提醒&#xff1a;新手常见问题6. 结语&#xff1a;网络即代码的未来1. VyOS是什么&am…

动态规划理论基础,LeetCode 509. 斐波那契数 LeetCode 70. 爬楼梯 LeetCode 746. 使用最小花费爬楼梯

动态规划理论基础动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就区分于贪心&#xff…

暑期自学嵌入式——Day02(C语言阶段)

点关注不迷路哟。你的点赞、收藏&#xff0c;一键三连&#xff0c;是我持续更新的动力哟&#xff01;&#xff01;&#xff01; 主页&#xff1a; 一位搞嵌入式的 genius-CSDN博客https://blog.csdn.net/m0_73589512?spm1000.2115.3001.5343 目录 Day02→数据类型&#xf…

如何单独安装设置包域名

前言 在 npm 中&#xff0c;直接通过 package-lock.json 无法单独设置包的安装地址&#xff0c;因为该文件是自动生成的依赖关系锁定文件。但你可以通过以下方法间接实现&#xff1a; 一、在 package.json 中指定包来源&#xff08;推荐&#xff09; 在 package.json 的 depend…

存储过程探秘:数据库编程的艺术

文章目录存储过程语法格式BEGIN...END语句块DECLARE&#xff08;声明局部变量&#xff09;流控制语句if函数批处理操作测试2测试3存储过程与函数的关系存储过程 MYSQL的存储过程是一组预处理的SQL语句&#xff0c;可以像函数一样在数据库中进行存储和调用。 它们允许在数据库…

非阻塞写入核心:asyncio.StreamWriter 的流量控制与数据推送之道

在 asyncio 的异步编程框架中&#xff0c;如果说 asyncio.StreamReader 是你异步应用的数据输入管道&#xff0c;那么 asyncio.StreamWriter 就是你异步应用的数据输出管道。它是一个至关重要的组件&#xff0c;让你能够方便、高效且非阻塞地向连接的另一端&#xff08;如 TCP …

控制台打开mysql服务报错解决办法

控制台打开mysql服务报错解决办法这个MySQL错误表示访问被拒绝&#xff0c;通常是因为没有提供正确的用户名和密码。以下是几种解决方法&#xff1a; 方法1&#xff1a;指定用户名和密码连接 mysql -u root -p然后输入root用户的密码。 方法2&#xff1a;如果忘记了root密码&am…

Unsloth 实战:DeepSeek-R1 模型高效微调指南(下篇)

食用指南 本系列因篇幅原因拆分为上下两篇&#xff1a; 上篇以基础环境搭建为主&#xff0c;介绍了 Unsloth 框架、基座模型下载、导入基座模型、数据集下载/加载/清洗、SwanLab 平台账号注册。 下篇&#xff08;本文&#xff09;以实战微调为主&#xff0c;介绍预训练、全量…

Ubuntu安装Jenkins

Ubuntu安装Jenkins方法1&#xff1a;使用官方的Jenkins仓库1. 添加Jenkins仓库2. 更新软件包列表3. 安装Jenkins4. 启动Jenkins服务5. 设置Jenkins开机启动6. 查找初始管理员密码7. 访问Jenkins方法2&#xff1a;使用Snap包&#xff08;适用于较新的Ubuntu版本&#xff09;1. 安…

ubuntu22.04下配置qt5.15.17开发环境

自从qt5.15版本开始&#xff0c;不再提供免费的离线安装包&#xff0c;只能通过源码自行编译。刚好最近需要在ubuntu22.04下配置qt开发环境&#xff0c;于是写篇文章记录配置的过程。 其实一开始是想配置qt5.15.2的&#xff0c;但是在编译配置参数这一步骤中出现如下报错 em…

S7-1200 与 S7-300 CPS7-400 CP UDP 通信 Step7 项目编程

S7-1200 CPU 与S7-300 CP STEP7 UDP通信S7-1200 与 S7-300 CP 之间的以太网通信可以通过 UDP 协议来实现&#xff0c;使用的通信指令是在S7-1200 CPU 侧调用通信-开放式用户通信TSEND_C&#xff0c;TRCV_C指令或TCON&#xff0c;TDISCON&#xff0c;TUSEND&#xff0c;TURCV 指…

基于YOLOv11的无人机目标检测实战(Windows环境)

1. 环境搭建 1.1 硬件与操作系统 操作系统&#xff1a;Windows 11 CPU&#xff1a;Intel i7-9700 GPU&#xff1a;NVIDIA RTX 2080&#xff08;8GB显存&#xff09; 1.2 安装CUDA和cuDNN 由于YOLOv11依赖PyTorch的GPU加速&#xff0c;需要安装CUDA和cuDNN&#xff1a; 安…

Spring Cloud分布式配置中心:架构设计与技术实践

从单体到微服务&#xff1a;Spring Cloud 开篇与微服务设计 Spring Cloud服务注册与发现&#xff1a;架构设计与技术实践深度分析 在以往分享中&#xff0c;码友们已经掌握了微服务的设计和注册中心的设计&#xff0c;部分聪明的码友已经察觉了&#xff0c;已经到了需要设计一个…