录制PCM之后,我在命令行播放成功之后,也用了代码去播放,最终也都是成功的,然后我就想能否直接用播放器直接播放呢?
我尝试了一下,结果发现并不成功,经过资料收集,发现播放器是无法播放原始数据的,因为播放器是不知道PCM的采样率、声道数、位深度等参数的,所以需要用到前面音频基础学到的知识来实现这种效果,所以这里打算使用PCM,转化成其他文件格式。例如WAV等
1.WAV文件格式
在PCM需要转WAV文件格式之前,我们先要了解WAV文件格式1,文件格式2
WAV 文件格式来源于微软Microsoft,遵循RIFF标准的文件格式,每个块由块标识符,块长度和块数据组成,官方解释
Wave files have a master RIFF chunk which includes a WAVE identifier followed by sub-chunks. The data is stored in little-endian byte order.
通过WAV介绍的链接,截取其中的相关图片,结合分析,可以得出一个通俗易懂的完整图片,如下图所示:
参数解释
每一chunk数据块包含3部分,前面也提及到
- ckID: 占4个字节,chunk的标识
- chsize: chunk的数据部分大小,占用4+n个字节,后面n个字节为data size
- data: chunk的数据部分
整一个WAV chunks文件由3部分组成,WAVEID,fmt chunk,data chunk - WAVEID: 文件类型
- fmt chunk
音频参数相关的chunk,包含采样率、声道数、位深度等参数信息
- data chunk
音频数据相关的chunk,包含真正的音频数据,比如PCM数据
2. 命令操作
songlin@feng-sl ~/audio/pcm_to_wav master ± ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm out.wav
ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
built with Apple clang version 12.0.0 (clang-1200.0.32.29)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.2_4 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox
libavutil 56. 51.100 / 56. 51.100
libavcodec 58. 91.100 / 58. 91.100
libavformat 58. 45.100 / 58. 45.100
libavdevice 58. 10.100 / 58. 10.100
libavfilter 7. 85.100 / 7. 85.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100
[s16le @ 0x7fa6a8008200] Estimating duration from bitrate, this may be inaccurate
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, s16le, from 'out.pcm':
Duration: 00:00:51.94, bitrate: 1411 kb/s
Stream #0:0: Audio: pcm_s16le, 44100 Hz, stereo, s16, 1411 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'out.wav':
Metadata:
ISFT : Lavf58.45.100
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, stereo, s16, 1411 kb/s
Metadata:
encoder : Lavc58.91.100 pcm_s16le
size= 8948kB time=00:00:51.94 bitrate=1411.2kbits/s speed= 389x
video:0kB audio:8948kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000851%
这里使用s16le 的格式,是因为我的pcm音频是这个s161e格式的,所以需要使用相同的格式,要是使用f32le 这个格式去转换,得出来的数据大小就会相差很远,这个是不正确的
Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native))
这个就代表转换的格式是没有变化的。
然后查看大小有没有变化
从图上面可以看出来WAV的字节大小对比PCM的字节大小大了78个字节,从我们前面学到的只是来看,应该是44个字节才对,那么剩下的34个字节是什么东西么,我们来看看文件的二进制文件:
What is a “LIST” chunk in a RIFF/Wav header?
或者
List chunk(of a RIFF file)
另外我们也可以通过加上一个输出文件参数-bitexact 可以去掉List Chunk
ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm -bitexact out1.wav
从图结果可以看到,完美去除list trunk
3.代码编程
结合之前的录音例子,这里最重要的事情就是补充WAV头文件即可,所以关键代码如下
wavheader.h
// 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;
wavheader.cpp
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)
- sizeof (header.riffChunkId)
- sizeof (header.riffChunkDataSize);
// 打开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();
}
3.1函数调用
// 获取输入流
AVStream *stream = ctx->streams[0];
// 获取音频参数
AVCodecParameters *params = stream->codecpar;
// pcm转wav文件
WAVHeader header;
header.sampleRate = params->sample_rate;
header.bitsPerSample = av_get_bits_per_sample(params->codec_id);
header.numChannels = params->channels;
if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
header.audioFormat = AUDIO_FORMAT_FLOAT;
}
FFmpegs::pcm2wav(header,
filename.toUtf8().data(),
wavFilename.toUtf8().data());
debug运行程序之后就发现在保存的目录中有两个文件,一个是pcm,一个是wav,用播放器播放wav,发现是可以成功播放的!