基于C++实现的用于OpenAL的 .wav音频加载器

Posted IAKSH

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于C++实现的用于OpenAL的 .wav音频加载器相关的知识,希望对你有一定的参考价值。

文章目录

0x00 | 前言

近日学习OpenAL,想从最简单的.wav格式入手,但苦于找不到合适的解析库,最终写下此文。

另,该库已开源

IAKSH/libwavaudio (github.com)

0x01 | .wav格式的标准结构

.wav音频格式按照一定的采样率(通常是44.1KHz,和CD音频一样)保存了音频的波形数据,不进行任何压缩,其数据结构简单易懂,十分便于操作。

标准的.wav音频文件主要由以下部分组成:

如图所示,整个文件大致分为三个部分:

  • RIFF Chunk(文件头)

    这一部分是资源交换档案标准(Resource Interchange File Format)所规定的文件头,Windows操作系统中许多媒体文件格式,如.wav.avi,均遵循这一标准。这一文件头的出现表示该文件是一个标准的RIFF文件,可以按照RIFF标准对其进行解析。

    RIFF Chunk的标准大小为12Byte,由三个4Byte数据组成,它们分别是:

    • ChunkID(4Byte)

      描述该区块的类别,对于RIFF Chunk,其值应为"RIFF"

    • ChunkSize(4Byte)

      块大小,单位Byte,描述ChunkIDChunkSize所占用的8字节外的整个RIFF标准文件的大小

    • Format(4Byte)

      描述该RIFF标准文件内的媒体数据格式,对于我们的.wav文件,其值应该是"WAVE"

  • fmt Chunk

    这一部分是音频数据的格式块,描述了该音频数据的格式以及相关参数,如采样率,码率等。

    在.wav格式中,fmt Chunk的标准大小是24Byte,由4个4Byte数据和4个2Byte数据组成,从头至尾以此为:

    • SubChunk1ID(4Byte)

      标明这是一个子区块,同时描述该区块的类别,对于fmt Chunk,此值应为"fmt"

    • SubChunk1Size(4Byte)

      描述了该子区块(fmt Chunk)SubChunk1IDSubChunk1Size所占用的8字节外的大小(单位Byte)

    • AudioFormat(2Byte)

      描述了音频数据的格式,对于我们的.wav文件,其值通常为1。

    • NumChannels(2Byte)

      描述了音频数据的总通道数

    • SampleRate(4Byte)

      描述了音频数据的采样率(每个通道每秒包含多少帧数据),对于我们的.wav文件,这个值通常是44100

    • ByteRate(4Byte)

      描述音频数据每秒钟的音频包含多少字节的数据

    • BlockAlign(2Byte)

      描述音频数据每帧所有通道总共有多少字节的数据

    • BitsPerSample(2Byte)

      描述每帧包含多少bit数据

  • data Chunk

    • SubChunk2ID(4Byte)

      标明这是一个子区块,同时描述该区块的类别,对于data Chunk,其值应为data

    • SubChunk2Size(4Byte)

      描述音频数据(data)的总大小(单位Byte)

    • data(大小不定)

      音频波形帧数据,即PCM(脉冲编码调制)数据,整个文件中真正保存音频的地方。其大小为SubChunk2Size的值(单位Byte)。

0x02 | .wav格式的非标准结构

和PE格式一样,.wav的各数据块的位置是不定的。对于某些编码实现,各个Chunk之间完全可能被随意插入某些数据,比如,ffmpeg在转换音频格式时会在文件中插入libavformat的版本信息。

使用互联网上的一些在线mp3转wav网页得到的wav文件,通过16进制编辑器打开后可以发现这一现象:

这其实是合乎RIFF标准的,但并不是我们所理解的普适化的”.wav标准格式”。这意味着一个.wav文件中各Chunk的偏移量并不是确定的,我们需要通过ChunkID动态地解析地址。

0x03 | C++按字节读取文件的方法

想要动态解析Chunk,我们就需要按字节读取文件,然后解析。C++标准库中的fstream库提供了相关功能。

#include <fstream>

int main() noexcpet

    std::ifstream fs("test.wav",std::ios::in | std::ios::binary);
    fs.seekg(1,std::ios::beg);
    char c;
    fs.read(&c,1);
    fs.close();
    return 0;

上述代码使用fstream库打开了test.wav文件,并读取了其第2个字节到变量c中。

其中,我们通过std::ifstream::seekg(...)函数对文件流进行了偏移以读取指定地址。使用std::ifstream::read(...)函数在文件流中读取了指定大小的数据到c中。

0x04 | OpenAL播放音频的流程

我们还需要将解码出的波形数据播放出来以验证代码是否按照我们的期望运行,理所当然,这里我选用了OpenAL。

OpenAL API的设计与OpenGL API高度相似,但其主要围绕两种对象进行操作:

  1. Buffer(缓存)

是音频数据的缓存,实际上其内存由OpenAL状态机管理,我们只能拿到其ID。

  1. Source(声源)

声源用于播放音频,其记录了自己的方位,速度属性,以进行混音。

除此之外,还需要建立OpenAL上下文,加载音频设备。我们可以粗略的将其封装为一个类。

class AudioPlayer

private:
    ALCdevice *device = nullptr;
    ALCcontext *context = nullptr;
    ALuint audioSource;
    ALfloat audioSourcePos[3];
    ALfloat audioSourceVel[3];

    void initializeOpenAL()
    
        device = alcOpenDevice(nullptr); // open defeault device
        context = alcCreateContext(device, nullptr);
        alcMakeContextCurrent(context);
    

    void closeOpenAL()
    
        alcMakeContextCurrent(nullptr);
        alcDestroyContext(context);
        alcCloseDevice(device);
    

public:
    AudioPlayer()
    
        initializeOpenAL();
        alGenSources(1, &audioSource);
    

    ~AudioPlayer()
    
        closeOpenAL();
    

    void play(wava::WavAudio &wav, bool loopable, float posX, float posY, float posZ, float velX, float velY, float velZ)
    
        audioSourcePos[0] = posX;
        audioSourcePos[1] = posY;
        audioSourcePos[2] = posZ;

        audioSourceVel[0] = velX;
        audioSourceVel[1] = velY;
        audioSourceVel[2] = velZ;

        alSourcei(audioSource, AL_BUFFER, wav.getBuffer());
        alSourcef(audioSource, AL_PITCH, 1.0f);
        alSourcef(audioSource, AL_GAIN, 1.0f);
        alSourcefv(audioSource, AL_POSITION, audioSourcePos);
        alSourcefv(audioSource, AL_VELOCITY, audioSourceVel);
        alSourcei(audioSource, AL_LOOPING, static_cast<ALboolean>(loopable));

        alSourcePlay(audioSource);
    

    void stop()
    
        alSourceStop(audioSource);
    
;

0x05 | 构建.wav加载器

我们的目标是解析出.wav文件中的PCM数据,在上述分析的基础上,我们可以写出以下代码。

// wavaudio.hpp
#pragma once

#include <AL/al.h>
#include <AL/alc.h>

#include <iostream>
#include <fstream>
#include <array>

#include <cstdint>

namespace wava

    class WavAudio
    
    private:
        uint32_t buffer;
        bool loop = true;
        bool loaded = false;

        // RIFF chunk (main chunk)
        uint32_t chunkSize;
        char format[5] = '\\0';

        // sub-chunk 1 (fmt chunk)
        uint32_t subChunk1Size;
        uint16_t audioFormat;
        uint16_t numChannels;
        uint32_t sampleRate;
        uint32_t byteRate;
        uint16_t blockAlign;
        uint16_t bitsPerSample;

        // sub-chunk 2 (data)
        uint32_t subChunk2Size;
        unsigned char *data;

        int getFileCursorMark(std::ifstream &fs, std::string mark);

    public:
        WavAudio();
        WavAudio(const char *path);
        ~WavAudio();

        void load(const char *path);
        uint32_t getBuffer();
    ;

以及实现:

// wavaudio.cpp
#include "wavaudio.hpp"

int wava::WavAudio::getFileCursorMark(std::ifstream &fs, std::string mark)

    int len = mark.length();
    char buf[len + 1];
    buf[len] = '\\0';
    int i = 0;
    while (!fs.eof())
    
        fs.seekg(i++, std::ios::beg);
        fs.read(buf, sizeof(char) * len);
        if (mark.compare(buf) == 0)
            return i;
    
    std::cerr << "[libwavaudio] ERROR: failed to locate mark (" << mark << ") in moveFileCursorToMark()\\n";
    abort();


wava::WavAudio::WavAudio()



wava::WavAudio::WavAudio(const char *path)

    load(path);


wava::WavAudio::~WavAudio()

    alDeleteBuffers(1, &buffer);


void wava::WavAudio::load(const char *path)

    int i;
    std::ifstream fs(path, std::ios::in | std::ios::binary);
    if(!fs)
    
        std::cerr << "[libwavaudio] ERROR: can't open file (" << path << ")\\n";
        abort(); 
    
    
    i = getFileCursorMark(fs, "RIFF") - 1;
    fs.seekg(i + 4, std::ios::beg);
    fs.read((char *)&chunkSize, 4);
    fs.seekg(i + 8, std::ios::beg);
    fs.read((char *)&format, 4);

    if (std::string(format).compare("WAVE") != 0)
    
        std::cerr << "[libwavaudio] ERROR: trying to load a none-wav format file (" << path << ")\\n";
        abort();
    

    i = getFileCursorMark(fs, "fmt") - 1;
    fs.seekg(i + 4, std::ios::beg);
    fs.read((char *)&subChunk1Size, 4);
    fs.seekg(i + 8, std::ios::beg);
    fs.read((char *)&audioFormat, 2);
    fs.seekg(i + 10, std::ios::beg);
    fs.read((char *)&numChannels, 2);
    fs.seekg(i + 12, std::ios::beg);
    fs.read((char *)&sampleRate, 4);
    fs.seekg(i + 16, std::ios::beg);
    fs.read((char *)&byteRate, 4);
    fs.seekg(i + 20, std::ios::beg);
    fs.read((char *)&blockAlign, 2);
    fs.seekg(i + 22, std::ios::beg);
    fs.read((char *)&bitsPerSample, 2);
    fs.seekg(i + 24, std::ios::beg);

    i = getFileCursorMark(fs, "data") - 1;
    fs.seekg(i + 4, std::ios::beg);
    fs.read((char *)&subChunk2Size, 4);
    fs.seekg(i + 8, std::ios::beg);
    data = new unsigned char[subChunk2Size];
    fs.read((char *)data, subChunk2Size);

    // load data to OpenAL buffer
    alGenBuffers(1, &buffer);
    if (bitsPerSample == 16)
    
        if (numChannels == 1)
            alBufferData(buffer, AL_FORMAT_MONO16, data, subChunk2Size, sampleRate);
        else if (numChannels == 2)
            alBufferData(buffer, AL_FORMAT_STEREO16, data, subChunk2Size, sampleRate);
        else
            abort();
    
    else if (bitsPerSample == 8)
    
        if (numChannels == 1)
            alBufferData(buffer, AL_FORMAT_MONO8, data, subChunk2Size, sampleRate);
        else if (numChannels == 2)
            alBufferData(buffer, AL_FORMAT_STEREO8, data, subChunk2Size, sampleRate);
        else
            abort();
    
    else
        abort();

    // release data
    delete[] data;
    fs.close();

    loaded = true;


uint32_t wava::WavAudio::getBuffer()

    if (loaded)
        return buffer;
    else
    
        std::cerr << "[libwavaudio] ERROR: called getBuffer() from an unloaded WavAudio\\n";
        abort();
    

0x06 | 测试

我使用上述AudioPlayerWavAudio类编写了简单的测试代码。

#include "../wavaudio.hpp"
#include <thread>
#include <chrono>

class AudioPlayer

private:
    ALCdevice *device = nullptr;
    ALCcontext *context = nullptr;
    ALuint audioSource;
    ALfloat audioSourcePos[3];
    ALfloat audioSourceVel[3];

    void initializeOpenAL()
    
        device = alcOpenDevice(nullptr); // open defeault device
        context = alcCreateContext(device, nullptr);
        alcMakeContextCurrent(context);
    

    void closeOpenAL()
    
        alcMakeContextCurrent(nullptr);
        alcDestroyContext(context);
        alcCloseDevice(device);
    

public:
    AudioPlayer()
    
        initializeOpenAL();
        alGenSources(1, &audioSource);
    

    ~AudioPlayer()
    
        closeOpenAL();
    

    void play(wava::WavAudio &wav, bool loopable, float posX, float posY, float posZ, float velX, float velY, float velZ)
    
        audioSourcePos[0] = posX;
        audioSourcePos[1] = posY;
        audioSourcePos[2] = posZ;

        audioSourceVel[0] = velX;
        audioSourceVel[1] = velY;
        audioSourceVel[2] = velZ;

        alSourcei(audioSource, AL_BUFFER, wav.getBuffer());
        alSourcef(audioSource, AL_PITCH, 1.0f);
        alSourcef(audioSource, AL_GAIN, 1.0f);
        alSourcefv(audioSource, AL_POSITION, audioSourcePos);
        alSourcefv(audioSource, AL_VELOCITY, audioSourceVel);
        alSourcei(audioSource, AL_LOOPING, static_cast<ALboolean>(loopable));

        alSourcePlay(audioSource);
    

    void stop()
    
        alSourceStop(audioSource);
    
;

int main() noexcept

    std::cout << "hello world!\\n";

    AudioPlayer ap;
    wava::WavAudio wa("../test/sounds/heart.wav");

    ap.play(wa, false, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
    std::cout << "finished!\\n";
    while (true)
        std::this_thread::sleep_for(std::chrono::seconds(1));

    return 0;

上述代码尝试加载位于../test/sounds/heart.wav的.wav文件,并使用AudioPlayer调用OpenAL进行播放。如果你使用我的这个.wav文件,运行

以上是关于基于C++实现的用于OpenAL的 .wav音频加载器的主要内容,如果未能解决你的问题,请参考以下文章

用于比较两个音频文件的 openAL c++ 库 [关闭]

使用openal播放WAV音频

如何使用openAL将实时音频输入从麦克风录制到文件中? (里面有C++代码)

用于播放/录制音频(.wav、.ogg)的 C++ 多平台库

我的 OpenAL C++ 音频流缓冲区故障

用c或c++播放wav文件,怎么实现