PCM转WAV

录制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.
WavFormat.png

fmt Chunk.png
data Chunk.png

通过WAV介绍的链接,截取其中的相关图片,结合分析,可以得出一个通俗易懂的完整图片,如下图所示:


WAV.png

参数解释

每一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.png

从图上面可以看出来WAV的字节大小对比PCM的字节大小大了78个字节,从我们前面学到的只是来看,应该是44个字节才对,那么剩下的34个字节是什么东西么,我们来看看文件的二进制文件:
WAV包含list trunk.png
,发现34个字节是一个list trunk的东西,这个list trunk是什么东西呢,具体可以参考
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
WAC-PCM-NOList.png

从图结果可以看到,完美去除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,发现是可以成功播放的!

你可能感兴趣的:(PCM转WAV)