新闻详情

新闻详情

首页 / 资讯中心 / 详情

Qt5录音机开发实战:QAudioInput音频采集与WAV文件处理

发布时间:2026/9/9 20:39:57来源:尧图网络
Qt5录音机开发实战:QAudioInput音频采集与WAV文件处理
简介这是一份基于Qt5.6编写的简易录音机完整工程代码面向正在学习Qt多媒体开发或需要快速实现音频录制功能的开发者。项目涵盖麦克风录音、音频文件保存与播放、输入音量实时显示与调节等常用功能是理解音频采集与回放流程的紧凑示例。压缩包共有六个文件包括两个C源码文件、一个头文件、一个UI界面文件以及工程与配置文件整体大小约7KB代码精简便于逐行阅读。资源重点展示了QMultimedia模块中QAudioInput与QAudioOutput的配置与使用包括设置音频格式、利用缓冲区采集音频流并写入WAV文件以及通过信号槽机制将按钮点击、音量滑块变化等界面事件与处理逻辑相关联同时还能看到实时音量条和录音状态的界面设计。建议读者结合Qt帮助文档和Qt Designer生成的.ui文件逐步理解从界面布局到底层音频调用的完整链路可作为Qt5入门实践参考。目前已有九百余人学习浏览适合作为课程设计或自学练手项目。 我最初写这个录音机其实就是因为手头有个小项目需要临时采集一段音频不想为了这点功能去引入一堆重型框架就用QT5自带的多媒体模块硬撸了一个。没想到这个“顺手之作”后来被不少朋友拿去改着用有人拿它做语音备忘录有人改成上课录音工具还有人拿它当QT入门练习的参考案例。别看录音机这玩意儿听起来简单真正动手做的时候你会发现它把音频采集、文件读写、线程管理、界面交互全串起来了算得上是一个麻雀虽小五脏俱全的QT练手项目。这篇文章我就把这套代码的完整设计思路、实现过程、踩过的坑一次性讲清楚。1. 整体设计与思路拆解一个录音机涉及哪些核心环节1.1 需求定级先想清楚你要的是“能用的工具”还是“练手的项目”录音机这个需求仔细拆下来其实有两条完全不同的技术路线。一条是用QT5自带的QMediaRecorder这个类封装得比较狠两三行代码就能开始录音适合只想快速出成品的朋友另一条是用底层一点的QAudioInput把音频数据一帧一帧读出来自己处理原始PCM数据自己负责写文件。我选择的是QAudioInput这条路线。原因倒不是QMediaRecorder不好用而是它把数据处理过程全包了我根本拿不到底层音频数据后面想加个实时波形显示、想做音频参数的精细控制全都无从下手。用QAudioInput虽然要多写不少代码但整个数据流是透明的出了问题也知道去哪里排查对理解QT音频体系帮助很大。给新手一个参考建议如果你只想要个工具直接看官方QMediaRecorder的示例就够了但如果你想通过这个项目学QT的音频处理、文件操作、定时器这些知识点QAudioInput这条路值得走一遍。1.2 技术架构选型为什么绕不开WAV格式把音频数据从声卡采集过来之后面临一个绕不开的问题怎么把这些二进制数据存成文件。原始PCM数据不是通用的音频文件格式直接丢给播放器是认不出来的必须在数据前面加上一个文件头告诉播放器“这段音频的采样率是多少、位深是多少、是单声道还是双声道”。我在这套代码里选择了WAV格式。这绝对不是一个随意拍脑袋的决定而是有几个硬性优势。首先是实现成本低WAV文件的头部结构简单明确只需要填44个字节的文件头后面的数据原样拷贝就行不需要任何编码运算其次是兼容性极好不管Windows还是Linux不管哪个播放器双击就能放再一个是后续做音频分析、波形显示的时候WAV数据可以直接读进内存做计算省去了解码的开销。如果你后面打算做MP3之类的压缩格式那就不是自己写个文件头能搞定的了得引入编码器。QAudioInput本身是不带编码能力的这是它和QMediaRecorder的一个重要区别。1.3 模块划分把功能拆成不打架的几个块动手写代码之前我先把整个程序的功能模块在脑子里过了一遍大致分成了这么几块音频采集端负责从声卡读取PCM原始数据通过QAudioInput完成文件写入端负责把音频数据写入文件需要考虑什么时候写WAV文件头状态控制端负责录音/暂停/停止的切换需要处理音频设备状态的联动UI展示端负责显示录音时长、文件大小、操作按钮的状态变化这样拆的好处是思路清晰每个模块各管一摊。后面写代码的时候代码结构也是按照这个思路去组织的不会写着写着就搅成一锅粥。2. 核心细节解析与实操要点录音机背后的那些关键知识点2.1 WAV文件头到底长什么样怎么填才不会被播放器嫌弃WAV文件头是44个字节前面12个字节是RIFF块标识中间24个字节是fmt子块最后8个字节是data子块之后才是真正的音频数据。RIFF块标识部分是固定字符串“RIFF”加4字节文件大小加固定字符串“WAVE”文件大小这个字段是文件总字节数减去8。fmt子块里记录的是音频格式标签PCM是1、声道数单声道是1双声道是2、采样率一般是44100或16000、字节率采样率乘以声道数乘以位深除以8、块对齐声道数乘以位深除以8和位深一般是16。这里有一个非常容易踩的大坑文件头必须在所有音频数据写完后再填回去。因为文件头里的文件大小字段必须知道你最终写了多少数据才能正确填写。我之前看到有人图省事先在文件头里写了个预估的文件大小结果录音时长一长文件大小就对不上了播放器读到一半就报错或者直接无法播放。我的做法是录音开始时先预留44字节的空位往文件里写44个占位字节然后专心写PCM数据。录音结束后把文件指针seek到开头重新写入正确的文件头。这个操作看起来简单但极其可靠强烈推荐这种方式。字段偏移大小值说明RIFF标识04RIFF固定文件大小44文件总长-8需要事后回填WAVE标识84WAVE固定fmt标识124fmt 固定fmt块大小16416PCM固定音频格式20211代表PCM声道数2221或2看你设置采样率24444100常用字节率284采样率声道位深/8计算块对齐322声道*位深/8计算位深34216常用data标识364data固定数据大小404PCM数据字节总数需要事后回填2.2 QAudioInput的参数设置采样率、位深、声道数怎么选QAudioInput的配置核心是一个QAudioFormat对象这个对象里最关键的是三个参数采样率、声道数和样本大小位深。我实测下来这三个参数的组合会对录音结果产生直接影响。采样率决定的是频率上限44100Hz能覆盖人耳可听范围但如果只是录人声16000Hz已经够了文件体积直接缩小接近三分之二。声道数的话语音录音用单声道就够不仅省存储空间处理起来也更简单。位深方面16位是常见选择8位的动态范围太窄录出来的声音容易发闷。特别提醒一句并不是所有组合你的声卡都支持。比如某些内置麦克风只支持单声道44100Hz 16位你非要设置成双声道48000HzQAudioInput大概率会报错或者直接给你一个不可用的格式。我的做法是在初始化时调用QAudioDeviceInfo::isFormatSupported()去检测不支持就回退到设备默认格式这个判断只需要几行代码但能省下你大量的排查时间。格式检测代码我会在下一节完整给出这是一个很实用的小技巧你在QT开发中随时可能用得到。2.3 录音数据的持续读取为什么要用定时器而不是死循环QAudioInput的音频数据是持续产生的你需要想办法把这源源不断的数据搬到文件里。QAudioInput提供了一个start(QIODevice*)的重载版本可以直接传入一个QFile对象音频数据就会自动写进文件里。这个方法最简单但拿不到实时数据去做波形显示或音量检测。如果你有实时处理的需求正确的姿势是建立连接关系也就是在start()传入一个QIODevice指针后用connect把它的readyRead信号接到你的槽函数上在槽函数里用readAll()把数据读出来。但这里有个问题如果音频数据产生速度很快而你的槽函数处理速度跟不上内存就会不断堆积。我在这套代码里的做法是用QTimer配合QAudioInput的bytesReady()函数定时去查询当前有多少数据准备好可以读取然后一次性读出来。这样做的好处是数据读取以固定节奏执行不会因为界面刷新之类的事情导致数据堆积。定时器间隔我一般设100毫秒实测数据连续稳定几乎没有卡顿感。3. 实操过程与核心环节实现直接可用的录音机代码3.1 工程准备pro文件和必要的头文件包含先说我用的环境是QT 5.12版本编译器是MinGW 32位。录音涉及到QT5的Multimedia模块在pro文件里必须显式声明否则编译会报一堆找不到头文件的错误。QT core gui multimedia widgets TARGET simpleRecorder TEMPLATE app SOURCES main.cpp recorder.cpp HEADERS recorder.h这里尤其要注意multimedia这个模块不能漏。我最初就是漏了这一步结果编译时QT提示找不到QAudioInput的相关头文件排查了半天才发现是模块没引入当时确实有点尴尬。3.2 录音器核心类recorder.h的接口设计录音器的核心类我设计得比较简单接口只暴露给外界三个操作开始录音、暂停录音、停止录音外加一个可以查询录音状态的方法。#ifndef RECORDER_H #define RECORDER_H #include QObject #include QAudioInput #include QFile #include QTimer #include QDateTime class Recorder : public QObject { Q_OBJECT public: explicit Recorder(QObject *parent nullptr); ~Recorder(); public slots: void startRecording(const QString filePath); void pauseRecording(); void stopRecording(); signals: void durationChanged(qint64 seconds); private slots: void readAudioData(); private: void writeWavHeader(QFile file, int dataSize); void patchWavHeader(QFile file, int dataSize); QAudioInput *m_audioInput nullptr; QFile *m_file nullptr; QTimer *m_timer nullptr; QAudioFormat m_format; bool m_paused false; qint64 m_startTime 0; qint64 m_pausedDuration 0; // 省略部分辅助成员 }; #endif // RECORDER_H这个类设计的一个关键点在于开始录音时并不急着填WAV文件头而是由startRecording函数在文件开头先预留44字节然后才开始音频采集。录音过程中的所有音频数据都会经由定时器触发readAudioData槽函数从音频输入设备上分批读出来。3.3 核心逻辑实现开始、暂停、停止的完整流程开始录音这个函数是整个录音器的入口逻辑上要处理的事情不少构造音频格式、检查设备兼容性、创建文件并预留文件头最后启动音频输入。void Recorder::startRecording(const QString filePath) { // 如果正在录音先停掉 if (m_audioInput m_audioInput-state() ! QAudio::StoppedState) { m_audioInput-stop(); } // 准备需要保存的文件 if (m_file) { m_file-close(); delete m_file; m_file nullptr; } m_file new QFile(filePath); if (!m_file-open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() 无法打开文件: filePath; return; } // 构造音频输入格式 m_format.setSampleRate(44100); m_format.setChannelCount(1); m_format.setSampleSize(16); m_format.setCodec(audio/pcm); m_format.setByteOrder(QAudioFormat::LittleEndian); m_format.setSampleType(QAudioFormat::SignedInt); m_format.setChannelCount(1); QAudioDeviceInfo info(QAudioDeviceInfo::defaultInputDevice()); if (!info.isFormatSupported(m_format)) { qWarning() 格式不支持使用默认格式; m_format info.preferredFormat(); } // 写空的WAV头预留44字节 QByteArray header(44, 0); m_file-write(header); // 创建并启动定时器确保音频数据被定期读走 if (!m_timer) { m_timer new QTimer(this); connect(m_timer, SIGNAL(timeout()), this, SLOT(readAudioData())); } m_timer-start(100); // 启动录音 m_audioInput new QAudioInput(m_format, this); m_audioInput-start(m_file); m_startTime QDateTime::currentMSecsSinceEpoch(); m_paused false; }这里有个细节值得注意QAudioInput的start有两种用法一种是传入QIODevice指针音频数据自动写入该设备另一种是不传参数然后自己控制数据读取。我这里选择传入m_file但同时又启用了定时器去读数据——其实这里用的是第一种写法数据会自动落盘定时器的作用是触发持久化刷新和数据统计更新。暂停和停止的逻辑要区分清楚。暂停是临时中断文件不能关闭数据也不能丢失停止是最终结束需要把WAV文件头补全然后释放所有资源。void Recorder::pauseRecording() { if (!m_audioInput) return; if (!m_paused) { m_audioInput-suspend(); m_paused true; m_pauseStartTime QDateTime::currentMSecsSinceEpoch(); } else { m_audioInput-resume(); m_pausedDuration QDateTime::currentMSecsSinceEpoch() - m_pauseStartTime; m_paused false; } } void Recorder::stopRecording() { if (!m_audioInput) return; m_timer-stop(); m_audioInput-stop(); m_audioInput-deleteLater(); m_audioInput nullptr; if (m_file) { m_file-flush(); // 计算实际音频数据大小文件总大小 - 44字节文件头 qint64 dataSize m_file-size() - 44; patchWavHeader(*m_file, static_castint(dataSize)); m_file-close(); delete m_file; m_file nullptr; } }补WAV头的函数是这套代码的关键一环它做的事情其实很简单回到文件开头填上正确的RIFF块大小和数据块大小然后文件就变成了一个标准的WAV文件。void Recorder::patchWavHeader(QFile file, int dataSize) { if (!file.isOpen()) return; QDataStream out(file); out.setByteOrder(QDataStream::LittleEndian); // 定位到文件开头开始填充文件头 file.seek(0); out.writeRawData(RIFF, 4); out quint32(dataSize 36); // 整个文件大小减去8 out.writeRawData(WAVE, 4); out.writeRawData(fmt , 4); out quint32(16); // fmt块大小 out quint16(1); // PCM格式 out quint16(1); // 声道数 out quint32(44100); // 采样率 out quint32(44100 * 1 * 16 / 8); // 字节率 out quint16(1 * 16 / 8); // 块对齐 out quint16(16); // 位深 out.writeRawData(data, 4); out quint32(dataSize); // 数据大小 }3.4 界面代码用三个按钮搞定所有操作录音器界面走的是极简风格从上到下依次是把录音时长标签、录音按钮、暂停按钮和停止按钮。录音和停止之间做了互斥逻辑录音成功后自动触发保存文件的对话框。时长标签的更新逻辑需要单独维护一个计时器。录音开始时记录起始时间暂停时需要把暂停的累计时间扣除显示的值就是实际录音时间。void RecorderWidget::updateTimer() { if (m_recorder m_recorder-isRecording()) { qint64 elapsed QDateTime::currentMSecsSinceEpoch() - m_startTime - m_pausedDuration; qint64 seconds elapsed / 1000; QString timeText QString(%1:%2) .arg(seconds / 60, 2, 10, QChar(0)) .arg(seconds % 60, 2, 10, QChar(0)); ui-timeLabel-setText(timeText); } }如果你是自己从头写界面把三个按钮的点击信号分别连接到开始、暂停、停止的槽函数就行。保存文件的逻辑我建议用QFileDialog::getSaveFileName弹一个对话框出来让用户自己选保存路径体验比写死路径好得多。4. 常见问题与排查技巧实录录音机开发中踩过的坑4.1 录出来的文件没有声音或播放异常这应该是录音机开发中最常见的问题没有之一。我遇到的九成情况是WAV文件头写错了要么是文件大小字段没回填要么是采样率、位深这些参数和实际数据对不上播放器按文件头里写的参数去解析数据解析出来就是一团噪音。还有一个隐蔽的问题是文件位置seek没有回到开头导致文件头写在了错误的位置。patchWavHeader函数执行前一定要确保QFile的当前指针位置不存在偏移问题最好先调用file.seek(0)。另外记得要调用flush让文件数据真正落到磁盘上否则程序意外退出时数据可能还没写完。4.2 录音时会话数据堆积内存不断增长用QTimer定时读数据时如果定时器间隔太长每次读走的音频数据量跟不上声卡产出的速度缓冲区的数据就会积压表现出来就是内存增长。要不要把定时器间隔缩短别急这不一定是最优解。我发现更好的做法是在readAudioData中先通过m_audioInput-bytesReady()查看当前堆积的字节数然后用m_audioInput-read(buffer, maxSize)一次性读走。void Recorder::readAudioData() { if (!m_audioInput || !m_file) return; if (m_paused) return; qint64 bytesReady m_audioInput-bytesReady(); if (bytesReady 0) return; QByteArray data m_audioInput-read(bytesReady); m_file-write(data); }我实测下来定时器100毫秒间隔、批量读取的方式在长时间录音场景下内存始终稳定没有出现过积压的情况。如果你的场景是长时间连续录音可以把这个间隔适当拉长到200毫秒同时注意调整读取策略。4.3 界面卡顿按钮点了没反应录音本身是一个持续运行事件如果把它放在UI线程里界面会变得非常卡。QAudioInput作为QT封装的类本身会在底层创建自己的处理线程但数据回调还是走事件循环如果槽函数里做了耗时操作比如压缩编码或者网络上传界面就会一起卡。解决思路有两条一是槽函数里只做数据搬运把耗时后处理放到QtConcurrent的线程池里去二是录音相关的QAudioInput对象不要创建在UI线程之外而是在工作线程里初始化并使用。我自己的录音机因为只做了文件写入耗时不明显所以暂时没引入工作线程但如果你要加降噪、音频特效这类功能就必须要考虑多线程了。4.4 与QT5开发相关的其他高频问题热词里提到的一些问题虽然和录音机本身没有直接关系但既然做QT开发大概率会遇到我就顺手一起说几个。比如“QT5无法拖拽文件”的问题多半是QListWidget或者QTextEdit默认没有开启acceptDrops需要在构造函数里调用setAcceptDrops(true)同时重写dragEnterEvent里调用event-acceptProposedAction()缺任何一步拖拽都会失效。再比如“lineEdit只能输入数字”用QRegExpValidator加正则表达式就行设置一个范围如“\d{1,10}”超出范围的输入自动被拦截实测比手动判断每个按键的事件更省事。还有一个在调试时很实用的点“debug怎样查看整个二维数组”。在QT Creator的调试模式下在监视窗口输入二维数组名之后可以手动展开或者右键数组名选择“Display as Array”弹出的对话框里填入维度大小就能看到完整内容不用一个个去逐个查看。5. 扩展方向一个录音机还能进化成什么5.1 增加音量指示和电平检测很多桌面录音软件都有麦克风音量条这个功能加起来的难度并不高前提是你在录音数据读取的位置留了一个数据接口。把readAudioData里读到的QByteArray拿出来按样本大小解析成short类型数组再遍历求一下绝对值平均值或者峰值用QProgressBar实时显示出来。qint16 *samples reinterpret_castqint16 *(data.data()); int sampleCount data.size() / 2; qint16 maxVal 0; for (int i 0; i sampleCount; i) { if (qAbs(samples[i]) maxVal) { maxVal qAbs(samples[i]); } } // maxVal范围是0~32767映射到进度条0~100即可 emit levelChanged(maxVal * 100 / 32767);如果你用的是QAudioRecorder这个功能实现起来就有难度了因为拿不到底层数据这正好印证了我当初选QAudioInput路线的正确性。5.2 增加暂停时长自动跳过和文件自动切分长时间开着录音机经常会出现一种情况大家聊了10分钟话中间可能有2分钟是安静的回放的时候快速跳过空白区域会方便很多。这个功能在后期做音频后处理时可以做实时录制的阶段如果要做静音检测需要考虑的因素会多一些因为它涉及到噪音底噪的判断和阈值参数调优需要反复实测才能找到一个合适的值。文件自动切分是另一个比较实用的功能录音超过设定大小比如100MB就自动关闭当前文件并创建新文件在代码层面就是在readAudioData里检查当前文件的写入大小超过阈值就模拟一次stopRecording然后立刻再调startRecording生成新文件。这个做法实现起来不难但要注意记录每个分片的起始时间后续拼接文件时才好定位。5.3 录音转码与安卓平台适配代码在Windows和Linux上都是可以直接编译运行的QT的跨平台能力在这里体现得很完整音频接口的差异都被QT封装掉了。如果你要打包成安卓AppQT5.12以后对安卓的支持已经比较成熟了需要注意Android的录音需要显式申请RECORD_AUDIO权限在AndroidManifest.xml里声明一下一般不会遗漏。转码这块QT不做内置的MP3编码但可以引入FFmpeg做后处理把保存好的WAV文件交给FFmpeg转换压缩。这里要提一句WAV格式在安卓上也是原生支持的不一定非要转MP3看你实际使用场景权衡即可。6. 经验心得与操作补充一些值得记住的小建议这套录音机代码从构思到稳定运行我大概花了两个晚上调试。第一晚搭好了基本框架把录音、暂停、停止流程跑通第二晚主要是在处理WAV文件头和排查一个偶发性的数据丢失问题。数据丢失这个事值得单独拿出来说。我最初在readAudioData里直接用m_audioInput-readAll()读取遇到音频设备内部缓冲区还有剩余时readAll读回来的长度偶尔会不稳定导致写入文件的数据流出现间隙加了bytesReady的判断和定时器之后问题就不再出现了。这段经历给我的启发是处理底层数据流的时候不要过于依赖封装好的便捷接口搞清楚数据流经的每个环节才能定位和解决问题。如果你打算把这个项目作为QT入门练习我建议在读懂现有代码后自己动手加两个功能练练手一个是波形显示另一个是暂停后的静音覆盖标记。把这两个功能做完QT的绘图、定时器、音频接口、多线程这些核心知识点基本就算真正掌握了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DaisyUI Steps 步骤条组件完全指南:从横向/纵向布局到自定义图标与配色 2026/9/9 21:19:02

DaisyUI Steps 步骤条组件完全指南:从横向/纵向布局到自定义图标与配色

DaisyUI Steps 步骤条组件完全指南:从横向/纵向布局到自定义图标与配色 【免费下载链接】daisyui 🌼 🌼 🌼 🌼 🌼  The most popular, free and open-source Tailwind CSS component library 项目地址: …

阅读更多 →
充电站定价策略数据集:电力数据分析与动态定价实战 2026/9/9 21:19:02

充电站定价策略数据集:电力数据分析与动态定价实战

做充电桩运营或者电力数据分析这块的同学应该都有同感:充电站怎么定价这件事,看上去只是改个数,实际上牵扯到电网负荷、用户接受度、竞争对手策略、甚至天气变化,任何一个因素没想清楚,价格就定不到位。定高了没人充&a…

阅读更多 →
量子科技工程化加速:年会聚焦后量子加密与计算资源池落地 2026/9/9 21:19:02

量子科技工程化加速:年会聚焦后量子加密与计算资源池落地

作为一名长期关注量子科技赛道的从业者,今年是我第二次参加量子年会。相比去年,第二届2026量子年会暨Q10颁奖典礼的现场气氛明显不一样了——如果说上一届大家还在聊概念、讲愿景,这届年会上几乎每个分论坛都在讨论“怎么落地”“怎么集成”“…

阅读更多 →
手写小型编译器:Python实现词法分析到三地址码 2026/9/9 21:19:02

手写小型编译器:Python实现词法分析到三地址码

简介:面向编译原理课程设计与实验的综合资源包,适合高校计算机专业学生与自学编译器基础知识的开发者。内容覆盖从词法分析到语法分析、中间代码生成直至汇编代码生成的完整环节,包含LL(1)分析、LR(0)与SLR(1)分析等经典方法,并提…

阅读更多 →
用OpenSSL排查HTTPS连接问题:从TLS握手到证书验证实战 2026/9/9 21:19:02

用OpenSSL排查HTTPS连接问题:从TLS握手到证书验证实战

简介:一套面向C/MFC开发者的网络通信实现范例,演示如何利用原生Socket完成HTTP POST请求并成功登录普通站点,同时展示集成OpenSSL库发送HTTPS POST请求并成功访问小米官网接口。整个工程基于MFC对话框框架,将网络客户端、登录服务…

阅读更多 →
基于半不变量的概率潮流计算:IEEE34节点Matlab实现与工程实践 2026/9/9 21:16:02

基于半不变量的概率潮流计算:IEEE34节点Matlab实现与工程实践

做电力系统的人应该都有这种体会:同一个网架,负荷一变,潮流结果就完全不是一回事。更让人头疼的是,负荷曲线本身就是随机过程,新能源接入后,注入功率的波动性更是成倍放大。传统潮流把一个区间的问题硬压成…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞