WAV to C Header Converter
將WAV音頻文件轉換為C語言頭文件的Python腳本,支持將音頻數據嵌入到C/C++項目中。
功能特性
音頻格式支持
- PCM格式:支持8位、16位、24位、32位PCM音頻
- IEEE Float格式:支持32位浮點音頻
- 多聲道:支持單聲道、立體聲及多聲道音頻
- 自動格式檢測:自動識別WAV文件的編碼格式
智能數組類型選擇
- 16位及以下WAV → 生成
int16_t
數組(緊湊存儲) - 32位WAV → 生成
int32_t
數組(保持完整精度)
生成的頭文件包含
- 包含防護宏定義
- 音頻參數宏定義(采樣率、聲道數、采樣點數)
- 格式化的音頻數據數組
- 符合C語言標準的變量命名
使用方法
基本用法
- 將WAV文件放在腳本同一目錄下
- 運行腳本:
python convert_wav_to_int16_h.py
腳本會自動掃描當前目錄下的所有 .wav
文件并轉換。
輸出示例
對于16位WAV文件 audio.wav
,會生成 audio.h
:
#ifndef AUDIO_H_
#define AUDIO_H_#include <stdint.h>#define AUDIO_SAMPLE_RATE 44100
#define AUDIO_NUM_CHANNELS 2
#define AUDIO_NUM_SAMPLES 88200const int16_t audio_samples[AUDIO_NUM_SAMPLES] = {1234, -5678, 2345, -6789, 3456, -7890, 4567, -8901,5678, -9012, 6789, -1023, 7890, -2134, 8901, -3245,// ... 更多數據
};#endif
對于32位WAV文件,會生成 int32_t
數組:
const int32_t audio_samples[AUDIO_NUM_SAMPLES] = {123456789, -987654321, 234567890, -876543210,// ... 更多數據
};
支持的音頻格式
位深度 | 格式類型 | 輸出數組類型 | 說明 |
---|---|---|---|
8位 | PCM | int16_t | 無符號轉有符號,縮放到16位 |
16位 | PCM | int16_t | 直接使用原始值 |
24位 | PCM | int16_t | 縮放到16位(截斷低8位) |
32位 | PCM | int32_t | 保持完整32位精度 |
32位 | IEEE Float | int32_t | 浮點轉整數,縮放到32位范圍 |
文件命名規則
- 輸出文件名:
原文件名.h
- 變量名:基于文件名生成合法的C標識符
- 宏定義:變量名轉大寫
命名示例
my-audio.wav
→ 變量名:my_audio_samples
,宏前綴:MY_AUDIO_
123sound.wav
→ 變量名:wav_123sound_samples
,宏前綴:WAV_123SOUND_
應用場景
- 嵌入式系統:將音效直接編譯到固件中
- 游戲開發:嵌入音效資源,避免文件IO
- 音頻處理:將測試音頻數據編譯到程序中
- 實時系統:避免運行時文件加載延遲
技術細節
數據轉換
- 所有音頻數據按小端序處理
- 浮點數據范圍限制在 [-1.0, 1.0]
- 超出范圍的整數數據會被裁剪
- NaN浮點值會被轉換為0
內存效率
- 使用生成器避免大文件內存占用
- 分塊讀取音頻數據(4096幀/塊)
- 16位數組每行16個數據,32位數組每行8個數據
錯誤處理
- 跳過不支持的音頻格式
- 顯示詳細的轉換狀態信息
- 繼續處理其他文件即使某個文件出錯
輸出信息
腳本運行時會顯示每個文件的處理狀態:
[ok] audio.wav -> audio.h | fmt=PCM, ch=2, sr=44100, width=2 bytes, samples=88200, array_type=int16_t
[skip] unsupported.wav: unsupported sample width 5 bytes
[error] corrupted.wav: Invalid WAV file format
依賴要求
- Python 3.6+
- 標準庫模塊:
os
,struct
,wave
無需安裝額外的第三方庫。
python代碼
import os
import struct
import wavedef sanitize_name(name: str) -> str:base = ''.join(c if (c.isalnum() or c == '_') else '_' for c in name)if not base or base[0].isdigit():base = 'wav_' + basereturn basedef to_macro(name: str) -> str:return sanitize_name(name).upper()def detect_wav_format(path: str):# Returns ("PCM" or "FLOAT")try:with open(path, 'rb') as f:if f.read(4) != b'RIFF':return "PCM"f.read(4) # sizeif f.read(4) != b'WAVE':return "PCM"# iterate chunkswhile True:hdr = f.read(8)if len(hdr) < 8:breakchunk_id, chunk_size = hdr[:4], struct.unpack('<I', hdr[4:8])[0]if chunk_id == b'fmt ':fmt_data = f.read(chunk_size)if len(fmt_data) < 16:return "PCM"audio_format = struct.unpack('<H', fmt_data[0:2])[0]if audio_format == 1:return "PCM"if audio_format == 3:return "FLOAT"if audio_format == 0xFFFE and len(fmt_data) >= 40:# WAVE_FORMAT_EXTENSIBLE: subformat at offset 24 (16 bytes)subformat = fmt_data[24:40]# First 4 bytes little-endian correspond to PCM(1) or IEEE_FLOAT(3)code = struct.unpack('<I', subformat[0:4])[0]if code == 1:return "PCM"if code == 3:return "FLOAT"return "PCM"else:# skip chunk (with padding byte if size is odd)skip = chunk_size + (chunk_size & 1)f.seek(skip, 1)except Exception:return "PCM"return "PCM"def clip_int16(v: int) -> int:if v > 32767:return 32767if v < -32768:return -32768return vdef convert_sample(sample_bytes: bytes, fmt: str, sampwidth: int, target_width: int) -> int:# Returns int16 or int32 value based on target_widthif sampwidth == 1:# 8-bit unsigned PCM: 0..255 -> -128..127, then scaleu = sample_bytes[0]s = u - 128if target_width == 2:return s << 8 # scale to int16else:return s << 24 # scale to int32elif sampwidth == 2:# 16-bit signed little-endianval = struct.unpack('<h', sample_bytes)[0]if target_width == 2:return valelse:return val << 16 # scale to int32elif sampwidth == 3:# 24-bit signed little-endianb0, b1, b2 = sample_bytes[0], sample_bytes[1], sample_bytes[2]val = b0 | (b1 << 8) | (b2 << 16)if b2 & 0x80:val -= 1 << 24if target_width == 2:# Scale down to 16-bitreturn clip_int16(val >> 8)else:return val << 8 # scale to int32elif sampwidth == 4:if fmt == "PCM":# 32-bit signed little-endianval = struct.unpack('<i', sample_bytes)[0]if target_width == 2:return clip_int16(val >> 16)else:return valelse:# 32-bit IEEE floatf = struct.unpack('<f', sample_bytes)[0]if f != f: # NaNf = 0.0if f > 1.0:f = 1.0elif f < -1.0:f = -1.0if target_width == 2:return clip_int16(int(round(f * 32767.0)))else:return int(round(f * 2147483647.0)) # scale to int32else:# Unsupported width: fallback zeroreturn 0def write_header_start(fp, guard: str, var_base: str, num_samples: int, sample_rate: int, num_channels: int):fp.write(f"#ifndef {guard}\n")fp.write(f"#define {guard}\n\n")fp.write("#include <stdint.h>\n\n")macro_base = to_macro(var_base)fp.write(f"#define {macro_base}_SAMPLE_RATE {sample_rate}\n")fp.write(f"#define {macro_base}_NUM_CHANNELS {num_channels}\n")fp.write(f"#define {macro_base}_NUM_SAMPLES {num_samples}\n\n")fp.write(f"extern const int16_t {var_base}_samples[{macro_base}_NUM_SAMPLES];\n\n")fp.write("#endif\n")def write_header_with_array(path_h: str, var_base: str, sample_iter, total_samples: int, sample_rate: int, num_channels: int, sampwidth: int):guard = to_macro(var_base) + "_H_"# Determine array type based on sample widthif sampwidth == 4:array_type = "int32_t"per_line = 8 # fewer numbers per line for int32else:array_type = "int16_t"per_line = 16with open(path_h, "w", encoding="utf-8") as fp:fp.write(f"#ifndef {guard}\n")fp.write(f"#define {guard}\n\n")fp.write("#include <stdint.h>\n\n")macro_base = to_macro(var_base)fp.write(f"#define {macro_base}_SAMPLE_RATE {sample_rate}\n")fp.write(f"#define {macro_base}_NUM_CHANNELS {num_channels}\n")fp.write(f"#define {macro_base}_NUM_SAMPLES {total_samples}\n\n")fp.write(f"const {array_type} {var_base}_samples[{macro_base}_NUM_SAMPLES] = {{\n ")count = 0first = Truefor s in sample_iter:if not first:fp.write(", ")else:first = Falsefp.write(str(s))count += 1if (count % per_line) == 0:fp.write("\n ")fp.write("\n};\n\n")fp.write("#endif\n")def convert_wav_to_h(path_wav: str):base_name = os.path.splitext(os.path.basename(path_wav))[0]var_base = sanitize_name(base_name)out_h = f"{base_name}.h"fmt = detect_wav_format(path_wav)with wave.open(path_wav, 'rb') as wf:nch = wf.getnchannels()sampwidth = wf.getsampwidth() # bytes per samplefr = wf.getframerate()nframes = wf.getnframes()if sampwidth not in (1, 2, 3, 4):print(f"[skip] {path_wav}: unsupported sample width {sampwidth} bytes")return# Determine target width: 32-bit WAV -> int32, others -> int16target_width = 4 if sampwidth == 4 else 2array_type = "int32_t" if target_width == 4 else "int16_t"total_samples = nframes * nchdef sample_generator():chunk_frames = 4096while True:frames = wf.readframes(chunk_frames)if not frames:breakmv = memoryview(frames)step = sampwidthfor off in range(0, len(mv), step):# mv[off:off+step] is a memoryview, convert to bytes for struct/unpacksb = mv[off:off + step].tobytes()yield convert_sample(sb, fmt, sampwidth, target_width)write_header_with_array(out_h, var_base, sample_generator(), total_samples, fr, nch, sampwidth)print(f"[ok] {path_wav} -> {out_h} | fmt={fmt}, ch={nch}, sr={fr}, width={sampwidth} bytes, samples={total_samples}, array_type={array_type}")def main():wavs = [f for f in os.listdir('.') if f.lower().endswith('.wav')]if not wavs:print("No .wav files found in current directory.")returnwavs.sort()for w in wavs:try:convert_wav_to_h(w)except Exception as e:print(f"[error] {w}: {e}")if __name__ == "__main__":main()