在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
相关产品推荐
相关产品推荐

