如何用C语言将两个数组写入16位96kHz双声道WAV文件?
实现方案:将双声道int数组写入16位96kHz WAV文件
核心思路
要高效生成符合要求的WAV文件,需解决三个关键问题:正确构造WAV文件头、将int采样转换为16位PCM格式、批量交错写入双声道数据以减少IO开销。
1. WAV文件头结构定义
WAV采用RIFF容器格式,PCM编码的标准头大小为44字节。为避免编译器字节对齐导致的结构大小错误,需使用编译指令强制按1字节对齐:
#include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> // 强制1字节对齐,保证结构体大小为44字节 #pragma pack(push, 1) typedef struct { // RIFF块 char riff_id[4]; // 固定为"RIFF" uint32_t riff_size; // 文件总大小 - 8 char wave_id[4]; // 固定为"WAVE" // fmt子块 char fmt_id[4]; // 固定为"fmt " uint32_t fmt_size; // fmt块大小,PCM为16 uint16_t audio_format; // 编码格式,PCM为1 uint16_t num_channels; // 声道数,双声道为2 uint32_t sample_rate; // 采样率,96000 uint32_t byte_rate; // 字节率 = 采样率 * 声道数 * 采样位深/8 uint16_t block_align; // 块对齐 = 声道数 * 采样位深/8 uint16_t bits_per_sample;// 采样位深,16 // data子块 char data_id[4]; // 固定为"data" uint32_t data_size; // 数据总字节数 = 采样数 * 声道数 * 采样位深/8 } WavHeader; #pragma pack(pop)
2. 初始化文件头
根据需求填充头字段,注意多字节字段需采用小端字节序(WAV标准要求),跨平台环境下可使用htole16/htole32转换(Linux/macOS)或手动转换:
void init_wav_header(WavHeader* header, uint32_t sample_count) { memcpy(header->riff_id, "RIFF", 4); memcpy(header->wave_id, "WAVE", 4); memcpy(header->fmt_id, "fmt ", 4); memcpy(header->data_id, "data", 4); header->fmt_size = 16; header->audio_format = 1; // PCM header->num_channels = 2; header->sample_rate = 96000; header->bits_per_sample = 16; header->block_align = header->num_channels * (header->bits_per_sample / 8); header->byte_rate = header->sample_rate * header->block_align; header->data_size = sample_count * header->block_align; header->riff_size = 36 + header->data_size; // 36 = 44 - 8(RIFF块的id和size占8字节) // 若运行在大端系统,需将多字节字段转为小端 // 示例:header->sample_rate = htole32(header->sample_rate); }
3. 数据转换与交错
你的channel1和channel2是int类型(通常为32位),需转换为16位有符号整数(int16_t),同时要避免数据溢出。转换后需将左右声道采样交错排列(左、右、左、右...),这是WAV双声道的存储格式:
// 将int数组转换为交错的16位PCM数据 int16_t* convert_and_interleave(int* ch1, int* ch2, uint32_t sample_count) { int16_t* pcm_data = malloc(sample_count * 2 * sizeof(int16_t)); if (!pcm_data) return NULL; for (uint32_t i = 0; i < sample_count; i++) { // 将int缩放到int16_t范围(-32768 ~ 32767),根据实际采样范围调整缩放逻辑 int32_t sample1 = ch1[i]; int32_t sample2 = ch2[i]; // 防止溢出,截断到int16_t范围 sample1 = sample1 > 32767 ? 32767 : (sample1 < -32768 ? -32768 : sample1); sample2 = sample2 > 32767 ? 32767 : (sample2 < -32768 ? -32768 : sample2); pcm_data[2*i] = (int16_t)sample1; pcm_data[2*i + 1] = (int16_t)sample2; } return pcm_data; }
4. 高效写入文件
最优写入方式是先写入文件头,再一次性写入所有交错后的PCM数据,减少IO系统调用次数(单次大写入比多次小写入效率高)。同时要做好错误检查:
int write_wav_file(const char* filename, int* ch1, int* ch2, uint32_t sample_count) { FILE* fp = fopen(filename, "wb"); if (!fp) { perror("Failed to open file"); return -1; } WavHeader header; init_wav_header(&header, sample_count); // 写入文件头 if (fwrite(&header, sizeof(WavHeader), 1, fp) != 1) { perror("Failed to write header"); fclose(fp); return -1; } // 转换并交错数据 int16_t* pcm_data = convert_and_interleave(ch1, ch2, sample_count); if (!pcm_data) { perror("Failed to allocate PCM data"); fclose(fp); return -1; } // 一次性写入所有数据 size_t written = fwrite(pcm_data, sizeof(int16_t), sample_count * 2, fp); if (written != sample_count * 2) { perror("Failed to write PCM data"); free(pcm_data); fclose(fp); return -1; } free(pcm_data); fclose(fp); return 0; }
5. 使用示例
int main() { uint32_t sample_count = 4800; int* channel1 = malloc(sizeof(int)*sample_count); int* channel2 = malloc(sizeof(int)*sample_count); // 假设这里已经填充了channel1和channel2的数据 if (write_wav_file("output.wav", channel1, channel2, sample_count) == 0) { printf("WAV file generated successfully\n"); } free(channel1); free(channel2); return 0; }
关键优化点
- 减少IO操作:一次性写入所有PCM数据,避免多次调用
fwrite带来的系统开销 - 内存高效:交错数据时直接分配所需内存,避免临时缓冲区浪费
- 跨平台兼容:处理字节对齐和字节序问题,保证在不同架构下生成的WAV文件可正常播放
- 数据安全:添加溢出保护,避免采样转换时出现非法值
内容的提问来源于stack exchange,提问作者Jake A
相关产品推荐
相关产品推荐

