You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在C语言中优雅处理WAV结构体的多字节采样格式问题

问题

我正在用C语言编写.wav PCM文件头,目前仅支持8位和16位PCM格式(分别用int8_t*和int16_t*类型的byteArray存储音频采样数据)。我希望能在将数据加载到内存前读取文件的采样大小,但为每种字节长度编写单独的处理函数(如writeToWav8()、writeToWav16()、writeToWav32())显得非常繁琐。

我曾尝试过两种解决方案,但都存在问题:

  • 使用固定的1字节整数数组,动态将大采样值打包到数组中(例如将01000110拆分为0100和0110),但出现了无法排查根源的精度丢失问题,且仍需为不同字节长度编写多个处理逻辑;
  • 将程序限制为仅支持8、16、32位PCM格式,这又显得过于敷衍。

请问是否有更简洁的处理方式?

当前定义的结构体如下:

typedef struct RiffHeader {
    // basic formatting info for the file, not relevant
} RIFF_CHUNK;

typedef struct FmtHeader {
    uint32_t Subchunk1ID;
    // all of these determine how to read the audio
    uint32_t Subchunk1Size;
    uint16_t AudioFormat;
    uint16_t NumChannels;
    uint32_t SampleRate;
    uint32_t ByteRate;
    uint16_t BlockAlign;
    uint16_t BitsPerSample; // bits per sample, determines how large each bit is in the data array
} FMT_CHUNK;

typedef struct DataChunk8 {
    uint32_t Subchunk2ID;
    uint32_t Subchunk2Size;
    int8_t *byteArray; // pointer to start of data
    // data size is equal to subchunk2size
} DATA_CHUNK_8;

typedef struct DataChunk16 {
    uint32_t Subchunk2ID;
    uint32_t Subchunk2Size;
    int16_t *byteArray; // pointer to start of data
    // data size is equal to subchunk2size

} DATA_CHUNK_16;

typedef struct WaveFile8 {
    RIFF_CHUNK RIFF;
    FMT_CHUNK  FMT;
    DATA_CHUNK_8 DATA;
} WAV_FILE_8;

typedef struct WaveFile_16 {
    RIFF_CHUNK RIFF;
    FMT_CHUNK  FMT;
    DATA_CHUNK_16 DATA;
} WAV_FILE_16;

后续还有一系列输入或输出WAV_FILE_8*或WAV_FILE_16*的辅助函数。

此外,文件读取部分也令我困惑:目前我使用两个函数

WAV_FILE_8* readFromFile8(const char* path)
WAV_FILE_16* readFromFile16(const char* path)

但读取未知文件时,无法确定应返回哪种尺寸的WAV结构体及调用哪个函数。

解决方案

1. 用通用结构体统一管理不同采样深度的数据

放弃为每种采样深度单独定义结构体,改用包含通用数据指针的结构体,保留BitsPerSample字段标识采样深度。所有处理逻辑复用,仅在读写数据时根据采样深度做分支处理。

示例结构体定义:

typedef struct DataChunk {
    uint32_t Subchunk2ID;
    uint32_t Subchunk2Size;
    void *byteArray; // 通用指针,指向任意类型的采样数据
} DATA_CHUNK;

typedef struct WaveFile {
    RIFF_CHUNK RIFF;
    FMT_CHUNK  FMT;
    DATA_CHUNK DATA;
} WAV_FILE;

2. 拆分文件读取流程:先读头,再加载数据

将文件读取拆分为两步,解决未知文件的读取问题:

  • 第一步:只读RIFF和FMT块,获取BitsPerSample等关键信息;
  • 第二步:根据采样深度分配对应类型的内存,再读取数据块内容。

示例实现:

// 读取文件头信息,不加载数据
int readWavHeader(const char* path, WAV_FILE *wav) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return -1;

    // 读取RIFF块
    fread(&wav->RIFF, sizeof(RIFF_CHUNK), 1, fp);
    // 读取FMT块
    fread(&wav->FMT, sizeof(FMT_CHUNK), 1, fp);

    // 定位到DATA块(跳过可能存在的其他块)
    uint32_t chunkID;
    uint32_t chunkSize;
    while (fread(&chunkID, sizeof(uint32_t), 1, fp) == 1) {
        fread(&chunkSize, sizeof(uint32_t), 1, fp);
        if (chunkID == 0x61746164) { // "data"的ASCII码
            wav->DATA.Subchunk2ID = chunkID;
            wav->DATA.Subchunk2Size = chunkSize;
            break;
        } else {
            fseek(fp, chunkSize, SEEK_CUR);
        }
    }

    fclose(fp);
    return 0;
}

// 根据头信息加载数据
int loadWavData(const char* path, WAV_FILE *wav) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return -1;

    // 定位到DATA块数据起始位置(实际项目建议用readWavHeader中记录的偏移)
    fseek(fp, sizeof(RIFF_CHUNK) + sizeof(FMT_CHUNK) + 8, SEEK_SET);

    size_t sampleSize = wav->FMT.BitsPerSample / 8;
    wav->DATA.byteArray = malloc(wav->DATA.Subchunk2Size);
    if (!wav->DATA.byteArray) {
        fclose(fp);
        return -1;
    }

    // 读取数据
    fread(wav->DATA.byteArray, sampleSize, wav->DATA.Subchunk2Size / sampleSize, fp);

    fclose(fp);
    return 0;
}

3. 通用读写函数,按采样深度分支处理

编写单个读写函数,内部根据BitsPerSample判断采样深度,执行对应操作,无需为每种深度单独写函数。

示例写入函数:

int writeWavFile(const char* path, WAV_FILE *wav) {
    FILE *fp = fopen(path, "wb");
    if (!fp) return -1;

    // 写入RIFF、FMT块
    fwrite(&wav->RIFF, sizeof(RIFF_CHUNK), 1, fp);
    fwrite(&wav->FMT, sizeof(FMT_CHUNK), 1, fp);
    // 写入DATA块的ID和Size
    fwrite(&wav->DATA.Subchunk2ID, sizeof(uint32_t), 1, fp);
    fwrite(&wav->DATA.Subchunk2Size, sizeof(uint32_t), 1, fp);

    // 根据采样深度写入数据
    size_t sampleSize = wav->FMT.BitsPerSample / 8;
    fwrite(wav->DATA.byteArray, sampleSize, wav->DATA.Subchunk2Size / sampleSize, fp);

    fclose(fp);
    return 0;
}

4. 类型安全访问宏(可选)

为避免直接操作void*的类型隐患,可定义宏根据采样深度获取对应类型指针:

#define GET_WAV_DATA_8(wav) ((int8_t*)((wav)->DATA.byteArray))
#define GET_WAV_DATA_16(wav) ((int16_t*)((wav)->DATA.byteArray))
#define GET_WAV_DATA_32(wav) ((int32_t*)((wav)->DATA.byteArray))

使用时先判断wav->FMT.BitsPerSample,再调用对应宏获取类型安全的指针。

内容的提问来源于stack exchange,提问作者opl23590

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:34:55