前言
ffmpeg录制下来的音频为pcm格式(内部存储着十六进制数据),但pcm格式的音频无法直接播放
本文先将pcm转换成wav格式(提要提前了解音频知识)
首先分析wav文件格式(wav的本质是在pcm数据前加上文件头),即在pcm的十六进制数据前加上文件头(文件头也是十六进制数据,但有些内容是固定的,有些内容是变化的)
pcm转换成wav基本思路:
首先封装一个方法,该方法需要实现在传入wav文件头后把源pcm文件转为wav文件。具体功能是先将文件头的十六进制数据写入文件(需要记录下变化的地方,等待读取pcm的数据之后才能确定),然后将pcm中的十六进制数据写入wav文件。这些思路都是有wav文件格式确定的
难点:
文件头到底怎么写?(以下是wav文件头的格式,第二张图为文件头十六进制存储的样子,一个十六进制为一个字节,一个ASCII编码占一个字节,文件头总长为44字节)
注意:这里细节的地方太多了,无法每一处都提及
通过结构的方式来理清文件头
typedef struct {
// RIFF chunk的id
uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'};
// RIFF chunk的data大小,即文件总长度减去8字节
uint32_t riffChunkDataSize;
// "WAVE"
uint8_t format[4] = {'W', 'A', 'V', 'E'};
/* fmt chunk */
// fmt chunk的id
uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '};
// fmt chunk的data大小:存储PCM数据时,是16
uint32_t fmtChunkDataSize = 16;
// 音频编码,1表示PCM,3表示Floating Point
uint16_t audioFormat = AUDIO_FORMAT_PCM;
// 声道数
uint16_t numChannels;
// 采样率
uint32_t sampleRate;
// 字节率 = sampleRate * blockAlign
uint32_t byteRate;
// 一个样本的字节数 = bitsPerSample * numChannels >> 3
uint16_t blockAlign;
// 位深度
uint16_t bitsPerSample;
/* data chunk */
// data chunk的id
uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'};
// data chunk的data大小:音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
uint32_t dataChunkDataSize;
} WAVHeader;
直接把结构体写入文件的方式很巧妙,既确定了写入顺序,又记录下了哪些是会变化的地方
注意:结构体中的某些值是根据音频相关数据决定的,而非一成不变。那相关的值到底如何填,可以直接去搜wav的头文件或者自行下载一个wav文件,将其拖入qt中查看其中的头文件中的数据
比如audioFormat可能填1可能填3,需要找到相应的含义,按情况而定
具体代码:
FFmpegs.h
#ifndef FFMPEGS_H
#define FFMPEGS_H
#include <stdint.h>
#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3
// WAV文件头(44字节)
typedef struct {
// RIFF chunk的id
uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'};
// RIFF chunk的data大小,即文件总长度减去8字节
uint32_t riffChunkDataSize;
// "WAVE"
uint8_t format[4] = {'W', 'A', 'V', 'E'};
/* fmt chunk */
// fmt chunk的id
uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '};
// fmt chunk的data大小:存储PCM数据时,是16
uint32_t fmtChunkDataSize = 16;
// 音频编码,1表示PCM,3表示Floating Point
uint16_t audioFormat = AUDIO_FORMAT_PCM;
// 声道数
uint16_t numChannels;
// 采样率
uint32_t sampleRate;
// 字节率 = sampleRate * blockAlign
uint32_t byteRate;
// 一个样本的字节数 = bitsPerSample * numChannels >> 3
uint16_t blockAlign;
// 位深度
uint16_t bitsPerSample;
/* data chunk */
// data chunk的id
uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'};
// data chunk的data大小:音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
uint32_t dataChunkDataSize;
} WAVHeader;
class FFmpegs
{
public:
FFmpegs();
static void pcm2wav(WAVHeader &header,const char *pcmFilename,const char *wavFilename);
};
#endif // FFMPEGS_H
FFmpegs.cpp
#include "ffmpegs.h"
#include <QFile>
#include <QDebug>
FFmpegs::FFmpegs()
{
}
void FFmpegs::pcm2wav(WAVHeader &header, const char *pcmFilename, const char *wavFilename)
{
header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
header.byteRate = header.sampleRate * header.blockAlign;
// 打开pcm文件
QFile pcmFile(pcmFilename);
if (!pcmFile.open(QFile::ReadOnly)) {
qDebug() << "文件打开失败" << pcmFilename;
return;
}
header.dataChunkDataSize = pcmFile.size();
header.riffChunkDataSize = header.dataChunkDataSize
+ sizeof (WAVHeader) - 8;
// 打开wav文件
QFile wavFile(wavFilename);
if (!wavFile.open(QFile::WriteOnly)) {
qDebug() << "文件打开失败" << wavFilename;
pcmFile.close();
return;
}
// 写入头部
wavFile.write((const char *) &header, sizeof (WAVHeader));
// 写入pcm数据
char buf[1024];
int size;
while ((size = pcmFile.read(buf, sizeof (buf))) > 0) {
wavFile.write(buf, size);
}
// 关闭文件
pcmFile.close();
wavFile.close();
}
封装的代码中也可以看出,基本上头文件中的数据都已经初始化了,还剩下声道数numChannels、采样率sampleRate、位深度bitsPerSample来自行赋值
调用封装的函数:
void MainWindow::on_pushButton_pcm_to_wav_clicked()
{
// 封装WAV的头部,此处我所有的值是大多数情况下音频都是这些数据
WAVHeader header;
header.numChannels = 2;
header.sampleRate = 44100;
header.bitsPerSample = 16;
// 调用函数
FFmpegs::pcm2wav(header, "E:/media/out.pcm", "E:/media/out.wav");
QFile file("E:/media/out.wav");
if(file.exists()){
qDebug()<<"文件转换成功,wav文件已生成";
}
}
pcm和wav文件大小对比,wav多了44字节的文件头
注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用
码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方