您好,欢迎来到汇意旅游网。
搜索
您的当前位置:首页基于C++实现的用于OpenAL的 .wav音频加载器

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

来源:汇意旅游网

0x00 | 前言

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

后面添的:
这篇里的代码写的很烂,特别是那一堆 fs.seekg,fs.read。我的建议是直接创建一个对应结构的struct来读。但是很明显我懒得改了 ,能回来写这一段话已经很对得起各位了。

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进制编辑器打开后可以发现这一现象:

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中。

0x04 | OpenAL播放音频的流程

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

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

是音频数据的缓存,实际上其内存由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文件,运行该代码你应该可以听见不间断的心跳声。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- hids.cn 版权所有 赣ICP备2024042780号-1

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务