FFmpeg之三 录制音频并保存, API编解码从理论到实战
在学习FFmpeg的时候,想拿demo来练习,官方虽有示例,但更像是工具演示,新手不好掌握,在网上找不到有文章,能给出完整的示例和关键点的分析说明,一步一个错误,慢慢啃过来的,本文就把重要经验和完整代码全部分享出来。
文章目录
- 音频的基本概念
- 1. 采样率 (Sample Rate)
- 解释
- 单位
- 示例
- 2. 声道数 (Channel Count)
- 解释
- 示例
- 3. 采样位数 / 位深度 (Bit Depth)
- 解释
- 单位
- 示例
- 4、音频帧
- 解释
- 为什么需要知道 frame_size?
- 分片(plane)和打包(packed)
- 重采样
- AVAudioFifo
- PTS
- 代码实现:
音频的基本概念
1. 采样率 (Sample Rate)
解释
采样率是指在将连续的模拟音频信号转换为数字信号时,每秒钟对其幅度进行测量的次数(样本数)。可以将其想象为给连续的声音波形拍摄快照,采样率就是每秒拍摄快照的数量。采样率越高,意味着捕捉到的声音信息越精细,尤其是在高频部分,能够还原的声音频率上限也越高(根据奈奎斯特理论,最高可还原频率约为采样率的一半)。
单位
赫兹 (Hz) 或千赫兹 (kHz)。
示例
- 8000 Hz (8 kHz): 电话音质,足以识别人声,但听起来比较模糊。
- 16000 Hz (16 kHz): 广泛用于 VoIP(网络电话)和一些语音识别应用,比电话音质好。
- 44100 Hz (44.1 kHz): CD 音质标准。可以很好地覆盖人耳能听到的绝大部分频率范围(约 20 Hz - 20 kHz)。
- 48000 Hz (48 kHz): 专业音频、DVD 和蓝光视频、数字电视广播中常用的标准。
- 96000 Hz (96 kHz) / 192000 Hz (192 kHz): 高解析度音频(Hi-Res Audio)标准,理论上能提供超越 CD 的音质细节和频率响应,但文件体积也更大。
2. 声道数 (Channel Count)
解释
声道数是指音频信号中包含的独立声轨的数量。它决定了声音的空间感和来源方向。
示例
- 1 (Mono / 单声道): 所有声音混合在一个声道中,没有方向感。适用于语音录制、一些老式录音或 AM 广播。
- 2 (Stereo / 立体声 / 双声道): 包含左、右两个声道,可以营造出声音从不同方向传来的空间感。
- 5.1 声道: 包含 5 个全频带声道(前左、前中、前右、后左、后右)和 1 个低频效果声道(LFE,即 “.1”),用于家庭影院环绕声。
- 7.1 声道: 在 5.1 的基础上增加了两个侧环绕声道。
3. 采样位数 / 位深度 (Bit Depth)
解释
位深度(Bit Depth)描述了用来表示每个音频样本(采样点)的振幅(响度)的二进制位数(bits)。它决定了音频信号的动态范围(最响和最轻声音之间的范围)和量化噪声的大小。位数越多,表示振幅的精度就越高,动态范围越大,声音细节越丰富
单位
比特 (bit)。
示例
- 8 bit: 动态范围较小,量化噪声明显。常见于早期的游戏、一些电话系统或特定效果。
- 16 bit: CD 音质标准。提供了约 96 dB 的动态范围,对大多数听音环境和音乐类型来说已经足够好。
- 24 bit: 专业音频录制和处理中广泛使用。提供了约 144 dB 的巨大动态范围,可以记录非常细微的声音细节,并在后期处理中有更大裕量。
- 32 bit float (浮点): 主要在音频制作和处理软件内部使用。它提供了极大的动态范围,并且可以避免在处理过程中因信号过载而产生削波失真(clipping)。最终成品通常会转换回 16 bit 或 24 bit 整数。
4、音频帧
解释
“音频帧”(Audio Frame)是指编码器处理和输出的一个基本单元。它包含了一定数量的连续音频样本(每个声道)。
与前面提到的单个“样本”(Sample)不同,编码器(如 AAC, MP3, Opus 等)为了提高压缩效率和利用心理声学模型,通常会把一小段时间的音频样本打包在一起进行处理和压缩。这个“包”就是一个编码后的音频帧。
在 FFmpeg 的 AVCodecContext 结构中,有一个名为 frame_size 的成员。对于大多数有损压缩编码器,这个 frame_size 指的是该编码器每个输出帧所包含的单个声道的样本数量。对于特定编码器(或其特定配置)来说这个值是固定的
为什么需要知道 frame_size?
-
缓冲管理: 在使用 libavcodec 进行编码时,你需要确保提供给编码器的 PCM 样本数量通常是 frame_size 的整数倍(或者至少满足编码器的最小输入要求)。
-
时间戳计算: 音频帧的持续时间可以通过 frame_size / sample_rate 计算得出,这对于精确控制播放时间和同步至关重要。
-
理解编码器行为: 知道帧大小有助于理解编码器的内部工作方式和潜在的延迟(通常至少是一个帧的长度)。
-
重要区别: 对于未压缩的 PCM 数据,"帧"的概念不那么严格,你可以按任意数量的样本进行处理。但一旦涉及压缩编码器,它们通常强制要求以特定的 frame_size 来组织数据。
FFmpeg 中常见编码器的 frame_size 示例,frame_size 的具体值取决于所使用的编码器:
-
AAC (Advanced Audio Coding):
常见的 FFmpeg 内置 aac 编码器或 libfdk_aac 编码器,其 frame_size 通常是 1024 个样本/声道。
某些 AAC 变种,如 AAC-LD (Low Delay),frame_size 可能是 512 或 480。 -
MP3 (MPEG-1 Audio Layer III):
使用 libmp3lame 编码器时,frame_size 通常是 1152 个样本/声道。 -
Opus:
Opus 编码器比较灵活,它工作在不同的帧持续时间上(如 2.5ms, 5ms, 10ms, 20ms, 40ms, 60ms)。其 frame_size(样本数)等于 sample_rate * frame_duration_in_seconds。
例如,在 48 kHz 采样率下,一个 20ms 的 Opus 帧包含 48000 * 0.020 = 960 个样本/声道。FFmpeg 的 frame_size 常常报告这个常用的 960 值。 -
PCM (Pulse Code Modulation - 未压缩):
对于 PCM 这种未压缩格式,frame_size 的概念不那么适用。FFmpeg 可能会报告 frame_size 为 1,表示可以按样本处理,或者在某些封装上下文中可能有不同的表示。但编码意义上的固定帧大小通常不存在。
分片(plane)和打包(packed)
在ffmepeg 的AVCodecContext 有成员变量 sample_fmt,表示采样格式, 可选择位深度和存储样式。 位深度,例如:8bit、16bit,float 等等, 每种位深度都有两种类型,例如float型的有: AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_FLTP
-
带P(plane)的数据格式在存储时,其左声道和右声道的数据是分开存储的,左声道的数据存储在data[0],右声道的数据存储在data[1],每个声道的所占用的字节数为linesize[0]和linesize[1],frame.data[i]或者frame.extended_data[i]表示第i个声道的数据;
-
不带P(packed)的音频数据在存储时,是按照LRLRLR…的格式交替存储在data[0]中,linesize[0]表示总的数据量,frame.data[0]或frame.extended_data[0]包含所有的音频数据中。
重采样
在这篇文章 FFmpeg之一——常用命令 ,有提到数据处理流程, 那里并没有说重采样。重采样流程是:decoder解析 得到frame -> 对frame重采样 得到frame1 -> 对frame1 进行encoder。 为什么还要经过重采样?
在理想情况下是可行的。不用重采样也是可以的,例如这种情况:
- 音频源(录音设备)输出的音频参数(采样率、位深度/样本格式、声道布局)与
- 你选择的编码器所支持(或你希望最终文件拥有)的音频参数完全一致。
在实际应用中,这种情况并不总是发生,因此需要进行重采样 (Resampling) 或其他格式转换。以下是主要原因:
- 采样率不匹配 (Sample Rate Mismatch):
来源: 你的录音设备(如麦克风)可能以一个特定的采样率工作,比如 44100 Hz 或 16000 Hz。
目标: 保存的文件是标准的 48000 Hz,或者你选用的编码器在 48000 Hz 时效果最好/效率最高
解决: 重采样,将音频数据的采样率从来源(如 44.1k)转换到目标(如 48k)。FFmpeg 的 libavresample 或 libswresample 库就是做这个的(通常通过 -ar 参数或 aresample filter 隐式或显式调用)。
- 样本格式不匹配 (Sample Format Mismatch):
来源: 音频设备或解码器可能输出一种特定的样本格式,例如 16 位有符号整数 (s16)、32 位浮点数 (flt)、或者平面格式的 32 位浮点数 (fltp)。
目标: 但你选择的编码器可能只接受(或优化于)特定的样本格式。例如,很多 AAC 编码器内部处理或接受 fltp 格式效率更高。有些旧编码器可能只接受 s16。
解决: 这就需要进行样本格式转换,将音频数据的表示方式从一种格式转为另一种。FFmpeg 通过 -sample_fmt 参数或 aformat filter 来处理。这严格来说不叫“重采样”,但属于格式转换,通常和重采样一起讨论,因为都是预处理步骤。
- 声道布局不匹配 (Channel Layout Mismatch):
来源: 你可能从单声道麦克风录音 (mono)。
目标: 但你想保存为标准的双声道文件 (stereo),即使两个声道内容一样。或者反过来,从立体声源录制但只想保存单声道。
解决: 这就需要进行声道布局转换,比如将单声道复制成双声道,或将双声道混合为单声道。FFmpeg 通过 -ac 参数或 channelmap/pan 等 filter 来处理。
编码器要求/优化:
某些编码器对特定的输入参数组合有优化,或者干脆不支持某些组合。为了获得最佳压缩效率或兼容性,开发者可能会选择将输入音频转换到编码器最适合的格式。
AVAudioFifo
每个AVCodecContext (编解码器)都有对应的frame_size,在编码时,需要读取到frame_size 个sample后,才能编码一个音频帧。但是在音频转换过程中, 解码器对应的音频帧的采样数量 和 编码器对样的音频帧的采样数量可能不一样, 此时就需要累计音频采样数据到一定量后(编码器的音频帧大小)后,再写入一帧数据。否则会报错。
这个累计可以自己手动实现,也可以直接使用AVAudioFifo,它是一个缓存队列,可以把音频的采样先存入(av_audio_fifo_write),到底目标数量后(av_audio_fifo_size), 取出数据(av_audio_fifo_read)
PTS
一个音频帧的AVFrame有nb_samples个sample (和AVCodecContext 的frame_size值对应),
一个AVFrame 时长= nb_samples / sample_rate 秒
用frameIndex 表示当前帧的索引,PTS = frameIndex * 一个AVFrame 时长
代码实现:
实现步骤:
1、打开设备并初始化解码器
2、打开音频编码器
3、创建输出上下文并初始化 流、写入头文件
4、初始化重采样上下文
5、创建重采样上下文
6、使用av_read_frame 循环读取音频PCM数据,重复以下步骤:
6.1、音频解码 avcodec_send_packet-> avcodec_receive_frame, 得到解码帧decoded_frame
6.2、swr_convert_frame音频帧重采样 ,得到重采样后的音频帧 swr_frame
6.3、av_audio_fifo_write写入 FIFO队列
6.4、判断FIFO队列中的数据大小 av_audio_fifo_size,是否满足编码器帧大小
6.5、av_audio_fifo_read 读取FIFO中音频sample数据
6.6、对音频sample数据 进行编码
6.7、对编码后的音频帧 写文件
完整代码在 Github,下面贴出两个段的代码,
获取到音频帧的循环处理过程:
static void process_frame_use_decode(AVPacket* pkt, AVFrame* decoded_frame, AVFrame* swr_frame, AVAudioFifo* fifo,AVCodecContext* decoder_ctx, AVCodecContext* encoder_ctx,AVFormatContext* ofmtCtx, struct SwrContext* swr_ctx, AVPacket* out_packet, int* frameIndex)
{int ret = avcodec_send_packet(decoder_ctx, pkt);if (ret < 0){char error[1024] = {0};av_strerror(ret, error, 1024);fprintf(stderr, "Decode error: %s\n", error);return;}int received_frame = 0;while (avcodec_receive_frame(decoder_ctx, decoded_frame) == 0){received_frame++;// swr_frame->ch_layout = encoder_ctx->ch_layout;//// swr_frame->format = encoder_ctx->sample_fmt;// swr_frame->sample_rate = 48000;// swr_frame->nb_samples = encoder_ctx->frame_size; //与编码器帧大小保持一致ret = swr_convert_frame(swr_ctx, swr_frame, decoded_frame);// 打印pts,dtsprintf("decoded_frame pts: %ld, dts: %ld\n", decoded_frame->pts, decoded_frame->pkt_dts);printf("swr_frame pts: %ld, dts: %ld\n", swr_frame->pts, swr_frame->pkt_dts);if (ret < 0){char error[1024] = {0};av_strerror(ret, error, 1024);fprintf(stderr, "Error while resampling: %s\n", error);return;}// Add resampled samples to FIFOret = av_audio_fifo_write(fifo, (void**)swr_frame->data, swr_frame->nb_samples);if (ret < swr_frame->nb_samples){fprintf(stderr, "Failed to write all samples to FIFO\n");return;}printf(" frame received this time num: %d, Added %d samples to FIFO, current size: %d\n", received_frame, swr_frame->nb_samples, av_audio_fifo_size(fifo));// Encode when enough samples are availablewhile (av_audio_fifo_size(fifo) >= encoder_ctx->frame_size){printf("FIFO size: %d, frame size: %d\n", av_audio_fifo_size(fifo), encoder_ctx->frame_size);swr_frame->nb_samples = encoder_ctx->frame_size;ret = av_audio_fifo_read(fifo, (void**)swr_frame->data, encoder_ctx->frame_size);if (ret < encoder_ctx->frame_size){fprintf(stderr, "Failed to read enough samples from FIFO\n");return;}swr_frame->pts = *frameIndex * encoder_ctx->frame_size;printf("in fifo swr_frame pts: %ld, dts: %ld\n", swr_frame->pts, swr_frame->pkt_dts);encode_and_write_frame(encoder_ctx, swr_frame, ofmtCtx, out_packet, *frameIndex);(*frameIndex)++;}}
}
对重采样的音频sample进行编码、写文件:
static void encode_and_write_frame(AVCodecContext* encoder_ctx, AVFrame* swr_frame, AVFormatContext* ofmtCtx, AVPacket* out_packet, int frameIndex)
{int ret = avcodec_send_frame(encoder_ctx, swr_frame);if (ret < 0){// 获取错误信息char error[1024] = {0};av_strerror(ret, error, 1024);}// 将重采样后的帧发送给编码器if (ret == 0){while (avcodec_receive_packet(encoder_ctx, out_packet) == 0){// 正确设置数据包中的流索引out_packet->stream_index = ofmtCtx->streams[0]->index;// 调整时间戳,使其基于输出流的时间基av_packet_rescale_ts(out_packet, encoder_ctx->time_base, ofmtCtx->streams[0]->time_base);// 写入一个编码的数据包到输出文件if (av_interleaved_write_frame(ofmtCtx, out_packet) < 0){fprintf(stderr, "Error while writing output packet\n");break;}av_packet_unref(out_packet);}}
}
相关文章:
FFmpeg之三 录制音频并保存, API编解码从理论到实战
在学习FFmpeg的时候,想拿demo来练习,官方虽有示例,但更像是工具演示,新手不好掌握,在网上找不到有文章,能给出完整的示例和关键点的分析说明,一步一个错误,慢慢啃过来的,…...
Kaamel白皮书:2025版COPPA落地实操指南
COPPA简介 《儿童在线隐私保护法案》(COPPA)于1998年在美国颁布,其最初的动因源于人们日益增长的对互联网上收集儿童个人信息的担忧。为了响应这一问题,联邦贸易委员会(FTC)被授权制定并执行相关法规。COP…...

Jenkins Pipeline 构建 CI/CD 流程
文章目录 jenkins 安装jenkins 配置jenkins 快速上手在 jenkins 中创建一个新的 Pipeline 作业配置Pipeline运行 Pipeline 作业 Pipeline概述Declarative PipelineScripted Pipeline jenkins 安装 安装环境: Linux CentOS 10:Linux CentOS9安装配置Jav…...
蓝桥杯 8. 移动距离
移动距离 原题目链接 题目描述 X 星球居民小区的楼房全是一样的,并且按矩阵样式排列。楼房的编号为 1, 2, 3, ⋯⋯。 当排满一行时,从下一行相邻的楼往反方向排号。 例如,当小区排号宽度为 6 时,排列如下: 1 2 …...

AJAX 介绍
一、什么是AJAX ? AJAX 是 异步的 JavaScript 和 XML(Asynchronous JavaScript And XML) 的缩写,是一种实现浏览器与服务器进行数据通信的技术。其核心是通过 XMLHttpRequest 对象在不重新刷新页面的前提下,与服务器交换数据并更…...
硬盘损坏数据恢复后对python程序的影响
最近硬盘突然间坏掉了,让数据商恢复了2个月今天终于拿到了恢复后的数据。 但是一测试问题就来了: PS E:\geosystem> python manage.py runserver 0.0.0.0:5000 Unhandled exception in thread started by <function check_errors.<locals>.…...

promis(resolve,reject)入门级别
JavaScript Promise 的定义 Promise 是一种用于处理异步操作的对象,表示一个可能已经完成或者尚未完成的操作的结果。它的核心作用在于简化复杂的回调嵌套问题(即所谓的“回调地狱”),使异步代码更加清晰易读。 Promise 的状态 …...

w~嵌入式C语言~合集6
我自己的原文哦~ https://blog.51cto.com/whaosoft/13870384 一、开源MCU简易数字示波器项目 这是一款采用STC8A8K MCU制造的简单示波器,只有零星组件,易于成型。这些功能可以涵盖简单的测量: 该作品主要的规格如下: 单片机…...

学习海康VisionMaster之路径提取
一:进一步学习了 今天学习下VisionMaster中的路径提取:可在绘制的路径上等间隔取点或查找边缘点 二:开始学习 1:什么是路径提取? 相当于事先指定一段路径,然后在对应的路径上查找边缘,这个也是…...
第十二章-PHP文件上传
第十二章-PHP文件上传 一,文件上传原理 一、HTTP协议与文件上传 1. 请求体结构 当表单设置enctype"multipart/form-data"时,浏览器会将表单数据编码为多部分(multipart)格式。 Boundary分隔符:随机生成的…...

第35课 常用快捷操作——用“鼠标左键”拖动图元
概述 拖动某个图元,是设计过程中常需要用到的操作,我们可以在原理图中拖动某个元器件符号,也可以在PCB图中拖动某个焊盘。 和常用的软件类似,用按住鼠标左键的方式来完成拖动操作。 用鼠标左键拖动图元 在想要拖动的图元上&…...
qt.qpa.plugin: Could not find the Qt platform plugin “cocoa“ in “ “
开发的pyqt项目在Windows运行时没啥问题,移植到Mac中时,发现一直报错qt.qpa.plugin: Could not find the Qt platform plugin “cocoa” in " ",一开始认为是pyqt版本问题,换了版本依旧不行。后续按照网上pip install o…...

二、Web服务常用的I/O操作
一、单个或者批量上传文件 前端: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>文件…...

「Mac畅玩AIGC与多模态04」开发篇01 - 创建第一个 LLM 对话应用
一、概述 本篇介绍如何在 macOS 环境下,基于已部署完成的 Dify 平台和本地 LLM 模型(如 DeepSeek),创建并测试第一个基础对话应用,实现快速验证推理服务与平台交互功能。 二、应用创建流程 1. 通过首页创建应用 打…...

深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
在电商行业竞争日益激烈的今天,淘宝商品数据如同蕴藏巨大价值的宝藏,无论是商家进行竞品分析、优化商品策略,还是数据分析师挖掘市场趋势,都离不开对这些数据的获取与分析。本文将深入探讨获取淘宝商品数据的多种途径,…...

马哥教育Linux云计算运维课程
课程大小:19.1G 课程下载:https://download.csdn.net/download/m0_66047725/90640128 更多资源下载:关注我 你是否找了很多资料看了很多视频聊了很多群友,却发现自己技术仍然原地踏步?本教程联合BAT一线导师倾囊相授…...

FPGA与边缘AI:计算革命的前沿力量
在数字化转型浪潮中,边缘计算和人工智能正引领着技术革命。而FPGA(现场可编程门阵列)作为一种独特的硬件架构,正逐渐成为边缘AI领域的关键推动力。本文将探讨FPGA与边缘AI的结合如何重塑我们的数字世界,以及这一技术融…...

Kafka 架构设计和组件介绍
什么是Apache Kafka? Apache Kafka 是一个强大的开源分布式事件流平台。它最初由 LinkedIn 开发,最初是一个消息队列,后来发展成为处理各种场景数据流的工具。 Kafka 的分布式系统架构支持水平扩展,使消费者能够按照自己的节奏检…...
虚函数表的设计和多态的实现
虚函数表 1.包含虚函数的类会有对应的虚函数表,这个表在编译时就初始化好了 2.本质是一个函数指针数组,里面是虚函数的指针 3.该类实例化的对象共用一张虚函数表 4.子类的虚函数表会继承父类的虚函数,如果继承多个父类那就把父类的虚函数…...

【Node.js 】在Windows 下搭建适配 DPlayer 的轻量(简陋)级弹幕后端服务
一、引言 DPlayer官网:DPlayer 官方弹幕后端服务:DPlayer-node MoePlayer/DPlayer-node:使用 Docker for DPlayer Node.js 后端(https://github.com/DIYgod/DPlayer) 本来想直接使用官网提供的DPlayer-node直接搭建…...

OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理
OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理 目录 OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理 一、MS ODBC驱动 1.1、安装到Windows后的表现形式 1.2、版本的互斥性 1.3、安装程序 1.4、配置后才可用 二、Navicat数据库管理工具 2.1、安…...
织梦dedecms调用会员详细字段信息
织梦如何调用会员详细信息: 在include/extend.func.php function GetMemberInfos($fields,$mid){ global $dsql; if($mid < 0){ $revalue "Error"; } else{ $row$dsql->GetOne("sele ct * fr…...
MySQL 8.0 忘记登录密码 mysqld --init-file重置
看到了很多跳过授权表的办法,这里通过mysqld --init-file办法。 适用情况: 服务器可以启动但无法登录/忘记登录密码。 一、首先停止 MySQL 服务: 按下 Win R 组合键,输入 services.msc 并点击“确定”,打开“服务”…...
Python 学习路线与笔记跳转(持续更新笔记链接)
这里写目录标题 Python 学习路线与笔记Python 简介学习路线第一阶段:Python 基础第二阶段:Python 进阶第三阶段:实用库与框架第四阶段:DevOps 与 Python第五阶段:最佳实践与高级技巧 学习资源官方资源在线学习平台书籍…...

操作系统:计算机世界的基石与演进
一、操作系统的本质与核心功能 操作系统如同计算机系统的"总管家",在硬件与应用之间架起关键桥梁。从不同视角观察,其核心功能呈现多维价值: 硬件视角的双重使命: 硬件管理者:通过内存管理、进程调度和设…...

Codeium 免费的AI编程助手
Codeium 由 Exafunction 团队(主要也是美国华人)开发的一款免费AI编程助手,是一个建立在顶尖AI技术上的代码加速工具,其背后的老板非常厉害,据说投资过马斯克的SpaceX。Codeium 本身具有颇多的亮点,支持70种…...

在MySQL Shell里 重启MySQL 8.4实例
前一段时间看到MySQL官方视频的Oracle工程师在mysql shell里面重启mysql实例,感觉这个操作很方便,所以来试试,下面为该工程师的操作截图 1.MySQL Shell 通过root用户连上mysql,shutdown mysql实例 [rootmysql8_3 bin]# mysqlshMy…...

FANUC机器人GI与GO位置数据传输设置
FANUC机器人GI与GO位置数据传输设置(整数小数分开发) 一、概述 在 Fanuc 机器人应用中,如果 IO 点位足够,可以利用机器人 IO 传输位置数据及偏移位置数据等。 二、操作步骤 1、确认通讯软件安装 首先确认机器人控制柜已经安装…...

LeetCode 24 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。 示例 1: 输入:head [1,2,3,4] 输出:[2,1…...

低代码平台开发手机USB-HID调试助手
项目介绍 USB-HID调试助手是一种专门用于调试和测试USB-HID设备的软件工具。USB-HID设备是一类通过USB接口与计算机通信的设备,常见的HID设备包括键盘、鼠标、游戏控制器、以及一些专用的工业控制设备等。 主要功能包括: 数据监控:实时监控和…...