wav音频转C语言样点数组

发布于:2025-09-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

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语言标准的变量命名

使用方法

基本用法

  1. 将WAV文件放在脚本同一目录下
  2. 运行脚本:
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 88200

const 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 wave

def 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_' + base
    return base

def 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)  # size
            if f.read(4) != b'WAVE':
                return "PCM"
            # iterate chunks
            while True:
                hdr = f.read(8)
                if len(hdr) < 8:
                    break
                chunk_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 32767
    if v < -32768:
        return -32768
    return v

def convert_sample(sample_bytes: bytes, fmt: str, sampwidth: int, target_width: int) -> int:
    # Returns int16 or int32 value based on target_width
    if sampwidth == 1:
        # 8-bit unsigned PCM: 0..255 -> -128..127, then scale
        u = sample_bytes[0]
        s = u - 128
        if target_width == 2:
            return s << 8  # scale to int16
        else:
            return s << 24  # scale to int32
    elif sampwidth == 2:
        # 16-bit signed little-endian
        val = struct.unpack('<h', sample_bytes)[0]
        if target_width == 2:
            return val
        else:
            return val << 16  # scale to int32
    elif sampwidth == 3:
        # 24-bit signed little-endian
        b0, b1, b2 = sample_bytes[0], sample_bytes[1], sample_bytes[2]
        val = b0 | (b1 << 8) | (b2 << 16)
        if b2 & 0x80:
            val -= 1 << 24
        if target_width == 2:
            # Scale down to 16-bit
            return clip_int16(val >> 8)
        else:
            return val << 8  # scale to int32
    elif sampwidth == 4:
        if fmt == "PCM":
            # 32-bit signed little-endian
            val = struct.unpack('<i', sample_bytes)[0]
            if target_width == 2:
                return clip_int16(val >> 16)
            else:
                return val
        else:
            # 32-bit IEEE float
            f = struct.unpack('<f', sample_bytes)[0]
            if f != f:  # NaN
                f = 0.0
            if f > 1.0:
                f = 1.0
            elif f < -1.0:
                f = -1.0
            if target_width == 2:
                return clip_int16(int(round(f * 32767.0)))
            else:
                return int(round(f * 2147483647.0))  # scale to int32
    else:
        # Unsupported width: fallback zero
        return 0

def 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 width
    if sampwidth == 4:
        array_type = "int32_t"
        per_line = 8  # fewer numbers per line for int32
    else:
        array_type = "int16_t"
        per_line = 16
    
    with 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 = 0
        first = True
        for s in sample_iter:
            if not first:
                fp.write(", ")
            else:
                first = False
            fp.write(str(s))
            count += 1
            if (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 sample
        fr = 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 -> int16
        target_width = 4 if sampwidth == 4 else 2
        array_type = "int32_t" if target_width == 4 else "int16_t"
        
        total_samples = nframes * nch

        def sample_generator():
            chunk_frames = 4096
            while True:
                frames = wf.readframes(chunk_frames)
                if not frames:
                    break
                mv = memoryview(frames)
                step = sampwidth
                for off in range(0, len(mv), step):
                    # mv[off:off+step] is a memoryview, convert to bytes for struct/unpack
                    sb = 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.")
        return
    wavs.sort()
    for w in wavs:
        try:
            convert_wav_to_h(w)
        except Exception as e:
            print(f"[error] {w}: {e}")

if __name__ == "__main__":
    main()

网站公告

今日签到

点亮在社区的每一天
去签到