FFmpeg开发笔记(十二):ffmpeg音频处理、采集麦克风音频录音为WAV

若该文为原创文章,转载请注明原文出处

本文章博客地址:https://blog.csdn.net/qq21497936/article/details/152651085

各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究

长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中...

FFmpeg和SDL开发专栏(点击传送门)

上一篇:《FFmpeg开发笔记(十一):ffmpeg移植到海思HI35xx平台之将ffmpeg库引入到sample的demo中

下一篇:敬请期待...

前言

ffmpeg采集音频是一块很重要也复杂的,本篇描述了录音麦克风为pcm封装成wav。

使用ffmpeg命令行获取设备列表

ffmpeg 会列出所有可用的视频和音频设备。其中音频设备会在 "DirectShow audio devices""标题下显示

cmd 复制代码
ffmpeg -list_devices true -f dshow -i dummy

发现输出的是乱码(中文在cmd上的输出),

cmd 复制代码
chcp 56001

65001 是 UTF-8 编码的代码页

以上2个没有麦(系统自己的,没插入麦),使用usb的:

音频

音频源(Audio Source)

音频源指FFmpeg获取声音数据的来源,是录制的 "起点"。常见的音频源主要分为两类:

  • 硬件音频源:直接从计算机硬件设备采集声音,如麦克风(通过声卡输入接口)、线路输入(Line-In,用于连接外部设备如录音机、吉他等)。在不同操作系统中,硬件音频源的标识方式不同,例如 Windows 下通过 "麦克风阵列""线路输入" 等设备名识别,Linux 下通过 ALSA(Advanced Linux Sound Architecture)或 PulseAudio 的设备节点(如hw:0,0)标识,macOS 则依赖 Core Audio 架构的设备 ID。
  • 软件音频源:从系统内部或其他软件中捕获音频,如录制浏览器播放的音乐、视频会议的声音等。这种场景需要依赖 "虚拟音频设备"(如 Windows 的 "立体声混音"、macOS 的 Soundflower、Linux 的 PulseAudio Loopback),将系统输出的音频重新作为输入源提供给 FFmpeg。

音频编解码器(Audio Codec)

编解码器(Codec,即 Coder-Decoder)是处理音频数据的核心组件,负责将原始音频采样数据压缩(编码)为特定格式的文件,或解压(解码)为可播放的原始数据。在录制场景中,我们主要关注 "编码器",它直接影响录制文件的体积、音质与兼容性。

FFmpeg 支持几乎所有主流音频编码器,常见的包括:

  • PCM(脉冲编码调制):无压缩编码,直接存储原始音频采样数据,音质最佳但文件体积极大(例如 44.1kHz、16 位、立体声的 PCM 音频,每分钟约 10MB),常见于 WAV 格式文件。
  • MP3(MPEG-1 Audio Layer III):有损压缩编码,通过舍弃人耳不敏感的音频频段实现高压缩比,是目前最普及的音频格式之一,比特率通常在 128-320kbps 之间(320kbps 接近无损音质)。
  • AAC(Advanced Audio Coding):有损压缩编码,性能优于 MP3,在相同比特率下音质更优,广泛用于 MP4、MOV 等视频文件及流媒体场景(如 YouTube、抖音)。
  • FLAC(Free Lossless Audio Codec):无损压缩编码,在保留原始音质的前提下压缩文件体积(压缩比约 1:2),适合对音质要求极高的场景(如音乐制作、无损音乐收藏)。

音频采样参数

采样参数决定了音频的 "精度" 与 "范围",是影响音质的核心指标,主要包括以下三个:

  • 采样率(Sample Rate):单位时间内对音频信号的采样次数,单位为赫兹(Hz)。采样率越高,越能还原高频声音,音质越细腻。常见的采样率有: 44.1kHz:CD 音质的标准采样率,能覆盖人耳可听范围(20Hz-20kHz); 48kHz:专业音频制作与视频配套音频的常用采样率; 96kHz/192kHz:高解析度音频(Hi-Res)的采样率,适合高端音频设备。
  • 采样位深(Sample Bit Depth):每个采样点用多少位二进制数表示,决定了音频的动态范围(即最大音量与最小音量的差值)。位深越大,动态范围越广,声音的层次感越强。常见的位深有 16 位(CD 标准,动态范围约 96dB)、24 位(专业制作标准,动态范围约 144dB)。
  • 声道数(Channels):音频信号的通道数量,决定了声音的空间感。常见的声道模式有: 单声道(Mono,1 声道):适合语音录制(如 podcasts、语音备忘录); 立体声(Stereo,2 声道):左右声道分别传输不同信号,营造空间感,适合音乐、影视音频; 多声道(如 5.1、7.1 声道):用于环绕声系统,常见于电影音频。

容器格式(Container Format)

容器格式(也称封装格式)是用于存储音频、视频、字幕等多种数据流的文件格式,它不负责数据压缩,仅规定数据的存储结构。在音频录制中,容器格式需与编码器匹配,常见的组合如下:

  • WAV + PCM:无压缩音频的标准组合,兼容性强但体积大;
  • MP3 + MP3:有损音频的主流组合,仅支持 MP3 编码;
  • MP4 + AAC:视频配套音频或纯音频的常用组合,兼容性好且体积小;
  • FLAC + FLAC:无损音频的主流组合,保留音质的同时压缩体积。

录音流程

FFmpeg 通过 "输入设备→采集原始压缩数据包AVPacket→封装Wav"三个步骤,将音频源的信号转化为目标音频文件,具体流程如下:

步骤一:设备探测与选择

FFmpeg 首先通过操作系统的音频接口(如 Windows 的 DirectSound、Linux 的 ALSA、macOS 的 Core Audio)探测可用的音频输入设备,用户通过命令行参数指定要使用的设备(如-f dshow -i audio="麦克风阵列")。

步骤二:音频采集与原始压缩数据获取AVPacket

选定设备后,FFmpeg 按照指定的采样参数(采样率、位深、声道数)从设备中读取原始 PCM 压缩数据。这一步是 "无损" 的,数据直接来自硬件或虚拟设备的输出。

步骤三:pcm压缩数据支持.wav格式封装,直接存为目标文件

编码后的音频数据流会被写入指定的容器格式(如 MP3、MP4)中,同时生成文件头、索引等元数据,最终形成可播放的音频文件。

注意:本篇没有重采样和压缩,直接存储的WAV+PCM。

Demo源码

cpp 复制代码
void FFmpegManager::testCaptureAudio()
{
    // 命令行,查看本地可用的音频设备列表
    // linux  :  ffmpeg -list_devices true -f alsa -i dummy
    //
    // windows:  ffmpeg -list_devices true -f dshow -i dummy
    //           Windows 系统下通过 DirectShow 接口访问音频设备的场景。
    //  "麦克风 (Realtek(R) Audio)"
    //  "麦克风 (USB Audio Device)" 使用本设备
    //  "立体声混音 (Realtek(R) Audio)"
    //
    // windows录制音频测试: ffmpeg -f dshow -i audio="麦克风 (USB Audio Device)" output.wav
    //

    // ffmpeg相关变量预先定义与分配
    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全局上下文,所有ffmpeg操作都需要
    AVInputFormat * pAVInputFormat = 0;             // ffmpeg输入类型格式

    AVStream *pAVStream = 0;                        // ffmpeg流信息
    AVCodecParameters *pAVCodecParameters;          // ffmpeg解码器参数
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg编码上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg编码器
    AVPacket *pAVPacket = 0;                        // ffmpag单帧数据包


    int ret = 0;                                    // 函数执行结果
    int audioIndex = -1;                            // 音频流所在的序号

    // 步骤一: 注册ffmpeg所有组件
    av_register_all();                              // 初始化所有组件(只使用这个,找不到dshow)
    avdevice_register_all();                        // 显示注册所有设备
    avcodec_register_all();                         // 显式注册所有编解码器

    // 步骤二:设置设备输入格式未dshow
    pAVInputFormat = av_find_input_format("dshow");
    if(!pAVInputFormat)
    {
        LOG << "Failed to av_find_input_format(\"dshow\")";
        return;
    }
    {
#if 0
        // 探测输入设备代码: 使用代码探测所有设备并输出
//        AVInputFormat * pAVInputFormat = av_find_input_format("dshow");
        AVDeviceInfoList * pAVDeviceInfoList = 0;
        ret = avdevice_list_input_sources(pAVInputFormat, 0, 0, &pAVDeviceInfoList);
        if(ret < 0)
        {
            char err_buf[1024];
            av_strerror(ret, err_buf, sizeof(err_buf));
            LOG << QSTRING("无法列出设备: ") << QSTRING(err_buf) << QSTRING("(错误代码:") << ret << ")";
            return;
        }
        for(int index = 0; index < pAVDeviceInfoList->nb_devices; index++)
        {
            std::string deviceName = pAVDeviceInfoList->devices[index]->device_name;
            LOG << QString(deviceName.data());
        }
        avdevice_free_list_devices(&pAVDeviceInfoList);
#endif
    }
    // 步骤三: 设置输入设备
#if 1
    QString deviceStr = QSTRING("audio=%1").arg(QSTRING("麦克风 (USB Audio Device)"));
    // 步骤四: 打开输入设备
    ret = avformat_open_input(&pAVFormatContext, deviceStr.toUtf8().constData(), pAVInputFormat, 0);
    if(ret < 0)
    {
        LOG << "Failed to open avformat_open_input:" << deviceStr;
        return;
    }
    LOG << "Suceed to open avformat_open_input:" << deviceStr;
#else
    std::string deviceName = "麦克风 (USB Audio Device)";
    std::string deviceArg = "audio=" + deviceName;
    // 步骤四: 打开输入设备
    ret = avformat_open_input(&pAVFormatContext, deviceArg.c_str(), pAVInputFormat, 0);
    if(ret < 0)
    {
        LOG << "Failed to open avformat_open_input:" << QString(deviceName.data());
        return;
    }
#endif
    // 步骤五: 查找流信息, 提取音频
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        pAVStream = pAVFormatContext->streams[index];
        switch (pAVCodecContext->codec_type)
        {
        case AVMEDIA_TYPE_UNKNOWN:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_UNKNOWN").arg(index);
            break;
        case AVMEDIA_TYPE_VIDEO:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_VIDEO").arg(index);
            break;
        case AVMEDIA_TYPE_AUDIO:
            audioIndex = index;
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_AUDIO").arg(index);
            break;
        case AVMEDIA_TYPE_DATA:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_DATA").arg(index);
            break;
        case AVMEDIA_TYPE_SUBTITLE:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_SUBTITLE").arg(index);
            break;
        case AVMEDIA_TYPE_ATTACHMENT:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_ATTACHMENT").arg(index);
            break;
        case AVMEDIA_TYPE_NB:
            LOG << QSTRING("流序号: %1 类型为: AVMEDIA_TYPE_NB").arg(index);
            break;
        default:
            break;
        }
        // 已经找打视频品流
        if(audioIndex != -1)
        {
            break;
        }
    }

    if(audioIndex == -1 || !pAVCodecContext)
    {
        LOG << "Failed to find video stream";
        return;
    }
    LOG << "Succeed to find audio stream";

    // 步骤六:获取解码器参数
    pAVCodecParameters = pAVFormatContext->streams[audioIndex]->codecpar;
    // 步骤七:查找解码器
    LOG << "AVCodecID" << pAVCodecParameters->codec_id << pAVCodecContext->codec_id;
    LOG << "AV_CODEC_ID_PCM_S16LE =" << AV_CODEC_ID_PCM_S16LE;
    pAVCodec = avcodec_find_decoder(pAVCodecParameters->codec_id);
    if(!pAVCodec)
    {
        LOG << "Failed to avcodec_find_decoder(pAVCodecContext->codec_id):"
            << pAVCodecContext->codec_id;
        return;
    }
    // 步骤八: 打开解码器
    ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
    if(ret < 0)
    {
        LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, NULL);";
        return;
    }
#if 1
    // 打印音频信息
    LOG << QSTRING("音频信息 采样率: %1Hz  声道数: %2  采样格式: %3")
           .arg(pAVCodecContext->sample_rate)
           .arg(pAVCodecContext->channels)
           .arg(av_get_sample_fmt_name(pAVCodecContext->sample_fmt));
#endif

    // 只能wav格式,这个demo
    QString fileName = "1.wav";
//    QString fileName = "1.aac"; // 录制无法播放
    // 步骤九: 创建输出上下文
    AVFormatContext *pAVFormatContextOut = 0;
    ret = avformat_alloc_output_context2(&pAVFormatContextOut, 0, 0, fileName.toUtf8().data());
    if(ret < 0)
    {
        LOG << QSTRING("无法创建输出上下文");
        return;
    }
    // 步骤十: 创建输出流
    AVStream *pAVStreamOut = 0;                     // ffmpeg流信息(输出)
    pAVStreamOut = avformat_new_stream(pAVFormatContextOut, 0);
    if(!pAVStreamOut)
    {
        LOG << QSTRING("无法创建输出流");
        return;
    }
    // 步骤十:复制编码器信息
    ret = avcodec_parameters_copy(pAVStreamOut->codecpar, pAVCodecParameters);
    if(ret < 0)
    {
        LOG << QSTRING("复制编码器失败");
        return;
    }
#if 0
    // 步骤十一: 设置输出编码器参数(想要别的就修改)
    pAVStreamOut->codecpar->codec_id = AV_CODEC_ID_PCM_S16LE;
    pAVStreamOut->codecpar->sample_rate = 44100;
//    pAVStreamOut->codecpar->sample_rate = 22050;    // 声音会变得慢低,时间翻倍,
//    pAVStreamOut->codecpar->sample_rate = 88200;    // 生意会变快尖,时间减半
    pAVStreamOut->codecpar->channels = 2;
//    pAVStreamOut->codecpar->channels = 1;   // 声音会变的慢低,时间翻倍
//    pAVStreamOut->codecpar->channels = 4;   // 声音会变的快尖,时间减半
#endif
    // 步骤十二: 创建输出文件
    ret = avio_open(&pAVFormatContextOut->pb, fileName.toUtf8().data(), AVIO_FLAG_WRITE);
    if(ret < 0)
    {
        LOG << QSTRING("无法开输出文件");
        return;
    }
    LOG;
    // 步骤十三:写入头文件
    ret = avformat_write_header(pAVFormatContextOut, 0);
    LOG;
    if(ret < 0)
    {
        LOG << QSTRING("写入头文件失败");
        return;
    }
    LOG;
    // 步骤十四:录制循环
    LOG << QSTRING("开始录制...");
    int frames = 0;
    pAVPacket = av_packet_alloc();
    QElapsedTimer elapsedTimer;
    elapsedTimer.start();
    while(elapsedTimer.elapsed() < 10 * 1000)
    {
        LOG;
        // 步骤十五:读取一帧音频数据
        ret = av_read_frame(pAVFormatContext, pAVPacket);
        LOG;
        if(ret < 0)
        {
            LOG << QSTRING("读取数据包失败");
            if(ret == AVERROR_EOF)
            {
                break;
            }
            return;
        }
        // 步骤十六:是音频流则进行时间戳调整
        if(pAVPacket->stream_index == audioIndex)
        {
            // 调整时间戳
            pAVPacket->stream_index = 0;
            av_packet_rescale_ts(pAVPacket,
                                 pAVFormatContext->streams[audioIndex]->time_base,
                                 pAVStreamOut->time_base);
            pAVPacket->pos = -1;

            // 写入数据包
            ret = av_interleaved_write_frame(pAVFormatContextOut, pAVPacket);
            if (ret < 0)
            {
                LOG << QSTRING("写入数据包失败:") << ret;
                break;
            }

            frames++;
        }

        av_packet_unref(pAVPacket);
    }
    // 步骤十五: 写入文件尾巴
    av_write_trailer(pAVFormatContextOut);

    av_packet_free(&pAVPacket);

    avformat_close_input(&pAVFormatContext);

    avio_closep(&pAVFormatContextOut->pb);

    avformat_free_context(pAVFormatContext);

    LOG << QSTRING("录制完成! 已保存到") << fileName;
    LOG << QSTRING("共写入 %1 个音频帧").arg(frames);
}

工程模板v1.6.0

入坑

入坑一:ffmpeg命令行输出中文设备乱码

问题

原因

在 Windows 的命令提示符(CMD)中使用 ffmpeg 时出现中文乱码,通常是由于编码不匹配导致的,测试改成uft-8即可。

解决

cmd 复制代码
chcp 65001

入坑二:ffmpeg代码获取设备失败

问题

Ffmpeg代码获取失败,为0。

原因

代码exe获取设备在win10上是需要管理员权限的,无效;

打印错误代码:

ENOSYS(错误码 40)本质上是 FFmpeg 告诉你:"我不认识这个设备类型,因为编译时没加支持"。解决的核心是确保 FFmpeg 包含对应平台的设备模块,并在代码中使用正确的设备格式。

思考

可能是编译的时候没编译dshow进去?但是通过dshow去获取输入接口又是可以,只是拿列表不行。

查找编译参数:

cpp 复制代码
ffmpeg -buildconf

Windows:寻找 --enable-dshow(DirectShow 设备支持),Linux:寻找 --enable-alsa(ALSA 音频设备)或 --enable-v4l2(视频设备)。

没有编译进去,那就是。

解决

未解决,不深究,有兴趣读者可以深入尝试并交流结果。

入坑三:ffmpeg打开音频获取解码器失败

问题

找不到编码器。

尝试一:列出设备

cpp 复制代码
ffmpeg -f dshow -i audio="麦克风 (USB Audio Device)" -v debug -t 1 NUL

尝试二:切换msvc版本

尝试切换版本后,打开设备都失败:

列出设备也失败,可能就不是版本问题,应该要能列出来。

使用ffmpeg录音测试,发现msvc版本的ffmpeg(预编译)可以录音,而mingw32的ffmpeg(自编译)不可以录音:

录音后打开,发现正常录制了,下面的是mingw32都无法录音:

所以怀疑还是版本问题,再次切换至msvc,上面的问题有可能是编码问题?

搭建号环境后,再次ffmpeg命令行录音并播放测试:

确认没有问题,库是没问题的,查看代码运行:

无法列出设备:

编码切换可以打开设备,也有数据流,但是拿不到解码器还是:

定位解码器:

shell 复制代码
ffmpeg -codecs

是有的,但是无法通过设备去拿到这个id?

id是对的,闹乌龙,以为65535是越界,但是更加奇怪,都已经有这个id了,为什么打开其编码器是失败的呢?

测试获取其编码器和解码器都失败:

就好像根本找不到这个一样。

cpp 复制代码
ffmpeg -encoders
cpp 复制代码
ffmpeg -decoders

原地蒙了?没注册,试了下,是没注册:

测试是否可以获取设备列表了:

还是不行,查阅细化:

功能未实现。

解决方式

注册即可`

cpp 复制代码
// 步骤一: 注册ffmpeg所有组件
av_register_all();                              // 初始化所有组件(只使用这个,找不到dshow)
avdevice_register_all();                        // 显示注册所有设备
avcodec_register_all();                        // 显式注册所有编解码器

上一篇:《FFmpeg开发笔记(十一):ffmpeg移植到海思HI35xx平台之将ffmpeg库引入到sample的demo中

下一篇:敬请期待...

本文章博客地址:https://blog.csdn.net/qq21497936/article/details/152651085

相关推荐
aqi0013 小时前
FFmpeg开发笔记(八十一)FFmpeg代码对RTSP和RTMP的推流区别
ffmpeg·音视频·直播·流媒体
奔跑吧邓邓子2 天前
【C++实战(71)】解锁C++音视频开发:FFmpeg从入门到实战
c++·ffmpeg·实战·音视频
骄傲的心别枯萎2 天前
项目1:FFMPEG推流器讲解(一):FFMPEG重要结构体讲解
linux·ffmpeg·音视频·视频编解码·rv1126
骄傲的心别枯萎3 天前
项目1:FFMPEG推流器讲解(二):FFMPEG输出模块初始化
linux·ffmpeg·音视频·视频编解码·rv1126
筏.k3 天前
FFmpeg 核心 API 系列:av_read_frame / avcodec_send_packet / avcodec_receive_frame
ffmpeg
humors2214 天前
批量M3U8转MP4工具
ffmpeg·视频·mp4·多媒体·转换·m3u8
神洛华4 天前
FFmpeg 全面教程:从安装到高级应用
ffmpeg
筏.k4 天前
FFmpeg 核心 API 系列:avcodec_find_decoder / avcodec_alloc_context3 / avcodec_open2
ffmpeg
Everbrilliant894 天前
Xcode上编译调试ffmpeg
macos·ffmpeg·xcode·ffmpeg源码编译工具·xcode调试ffmpeg源码·ffmpeg工具环境变量配置