没有音频设计师、没有素材库、没有版权预算——但你需要四种自然音效。E‑Brufen 的答案是:用数学算法生成一切。本文详解从 WAV 文件格式到完整 Dart 编码器的实现过程。


一、为什么手写 WAV 编码器?

1.1 问题的起点

E‑Brufen 的白噪音功能需要四种自然音效:雨声、海浪、篝火、森林。传统做法:

方案 问题
购买音效素材 版权费用 + 授权复杂
免费素材网站 质量参差不齐 + 使用限制
自己录制 设备门槛 + 环境噪音
网络流媒体 需要 INTERNET 权限(E‑Brufen 是离线应用)

1.2 E‑Brufen 的选择

用 Dart 代码生成音频。优势:

  • 零版权风险(数学算法不涉及著作权)
  • 零网络依赖(完全离线)
  • 零外部依赖(不引入第三方音频库)
  • 可参数化定制(改参数 = 改音效风格)
  • 可复现构建(任何机器运行同样脚本得到同样结果)

二、WAV 文件格式详解

2.1 整体结构

WAV 是微软和 IBM 开发的 RIFF 容器格式,结构极其简单:

偏移量    内容                  大小
0x00     "RIFF"                4 bytes
0x04     文件总大小 - 8         4 bytes (小端)
0x08     "WAVE"                4 bytes
0x0C     "fmt "                4 bytes
0x10     fmt chunk 大小 = 16   4 bytes
0x14     音频格式 = 1 (PCM)     2 bytes
0x16     声道数 = 1 (mono)      2 bytes
0x18     采样率 = 22050         4 bytes
0x1C     字节速率              4 bytes
0x20     块对齐 = 2            2 bytes
0x22     位深度 = 16            2 bytes
0x24     "data"                4 bytes
0x28     数据大小              4 bytes
0x2C     样本数据...            N bytes (16-bit signed LE)

总文件头大小:44 字节(固定)。之后全是原始 PCM 样本数据。

2.2 关键参数的选择

const sampleRate = 22050;  // 22.05 kHz — 自然音效的高频成分少
const durationSec = 30;    // 30 秒 — 循环播放足够
const channels = 1;        // Mono — 白噪音不需要立体声
const bitDepth = 16;       // 16-bit — 标准 CD 音质
参数 理由
采样率 22050 Hz 自然音效频谱主要在低频,22kHz 完全足够(音乐才需要 44.1kHz)
时长 30 秒 循环播放,30 秒的循环无缝感好
声道 1 (Mono) 减小文件体积,自然音效立体声意义不大
位深 16-bit 标准质量,每个样本 2 字节

文件大小计算

22050 样本/秒 × 30 秒 × 2 字节/样本 = 1,323,000 字节 ≈ 1.3 MB
4 种音效 × 1.3 MB ≈ 5.2 MB(完全可接受)

三、完整 Dart 实现

在这里插入图片描述

:上图原为 WAV 文件结构示意图(来自 CSDN 图床)。若无法显示,请参考文末“可视化辅助”章节中的文本结构图。

3.1 主函数

// generate_audio.dart
import 'dart:io';
import 'dart:math';

final _rand = Random();

void main() {
  final dir = Directory('assets/audio');
  if (!dir.existsSync()) dir.createSync(recursive: true);

  print('Generating rain.wav...');
  _generateRain('${dir.path}/rain.wav', 22050, 30);

  print('Generating ocean.wav...');
  _generateOcean('${dir.path}/ocean.wav', 22050, 30);

  print('Generating fire.wav...');
  _generateFire('${dir.path}/fire.wav', 22050, 30);

  print('Generating forest.wav...');
  _generateForest('${dir.path}/forest.wav', 22050, 30);

  print('Done!');
}

3.2 WAV 写入函数

void _writeWav(String path, int sampleRate, List<int> samples) {
  final bytes = <int>[];

  // 辅助函数:写入多字节整数(小端字节序)
  void w32(int v) {
    bytes.add(v & 0xFF);
    bytes.add((v >> 8) & 0xFF);
    bytes.add((v >> 16) & 0xFF);
    bytes.add((v >> 24) & 0xFF);
  }
  void w16(int v) {
    bytes.add(v & 0xFF);
    bytes.add((v >> 8) & 0xFF);
  }

  final dataSize = samples.length * 2;  // 16-bit = 2 bytes/sample
  final fileSize = 36 + dataSize;       // 36 = header after "RIFF" size field

  // ── RIFF Header ──
  bytes.addAll('RIFF'.codeUnits);
  w32(fileSize);
  bytes.addAll('WAVE'.codeUnits);

  // ── fmt chunk ──
  bytes.addAll('fmt '.codeUnits);
  w32(16);              // fmt chunk size (PCM = 16)
  w16(1);               // audio format (1 = PCM)
  w16(1);               // channels (1 = mono)
  w32(sampleRate);      // sample rate (22050)
  w32(sampleRate * 2);  // byte rate (sampleRate * channels * bitsPerSample/8)
  w16(2);               // block align (channels * bitsPerSample/8 = 2)
  w16(16);              // bits per sample (16)

  // ── data chunk ──
  bytes.addAll('data'.codeUnits);
  w32(dataSize);

  // ── 样本数据(16-bit signed integer, little-endian) ──
  for (final s in samples) {
    final v = s.clamp(-32768, 32767);  // 16-bit 有符号范围
    if (v < 0) {
      w16(0x10000 + v);  // 补码转换:-1 → 0xFFFF
    } else {
      w16(v);
    }
  }

  File(path).writeAsBytesSync(bytes);
}

★ Insight ─────────────────────────────────────
0x10000 + v 是有符号整数转无符号小端的技巧。Dart 的 int 是任意精度,而 WAV 格式要求 16-bit 补码。当 v = -1 时,0x10000 + (-1) = 0xFFFF——恰好是 -1 的 16-bit 补码表示。这个一行代码的技巧避免了手动处理补码的复杂度。
─────────────────────────────────────────────────


四、WAV vs 其他格式

格式 复杂度 压缩 文件大小 (30s) 是否需要解码库
WAV 极低 ~1.3 MB ❌ 不需要
MP3 极高 有损 ~0.3 MB ✅ 需要
OGG 有损 ~0.2 MB ✅ 需要
FLAC 无损 ~0.7 MB ✅ 需要

WAV 的优势:编码器只需 60 行代码,任何播放器都能直接打开。对于 30 秒的短音频,1.3MB 的文件大小完全可以接受。


五、验证生成的 WAV

# 检查文件头
xxd rain.wav | head -3
# 00000000: 5249 4646 ... 5741 5645  RIFF....WAVE
# 00000010: 666d 7420 1000 0000 0100  fmt ........
# 00000020: 0100 2256 0000 44ac 0000  .."V..D...

# 用任意播放器测试
# Windows: start rain.wav
# macOS: open rain.wav

六、可视化辅助(补充)

虽然原文中的图片链接可能来自外部图床,但为了便于您理解 WAV 的内存布局,这里提供两种可替代的可视化方案,您可以直接嵌入文章或文档中使用。

6.1 Mermaid 代码(渲染后为流程图)

RIFF Header
0x00-0x03

Chunk Size
0x04-0x07

WAVE ID
0x08-0x0B

fmt 块
0x0C-0x23

data 块头
0x24-0x2B

PCM 样本数据
0x2C ~ EOF

6.2 纯文本 ASCII 结构表

偏移量(Hex)  字段名称         大小(字节)   值/说明
0x00        "RIFF"          4          固定标识
0x04        文件大小-8       4          小端整数
0x08        "WAVE"          4          格式标识
-----------------------------------------------
0x0C        "fmt "          4          块标识(带空格)
0x10        fmt块长度        4          0x10 (16)
0x14        音频格式         2          0x01 (PCM)
0x16        声道数           2          0x01 (Mono)
0x18        采样率           4          0x5622 (22050Hz)
0x1C        字节速率         4          0xAC44 (44100)
0x20        块对齐           2          0x02
0x22        位深度           2          0x10 (16bit)
-----------------------------------------------
0x24        "data"          4          数据块标识
0x28        数据大小         4          N字节
0x2C        样本数据         N          16-bit 小端有符号

这些辅助内容与原文图片相辅相成,保证读者即便无法加载外部图片也能完全掌握 WAV 格式。


七、后续与扩展

本文聚焦于 WAV 编码器的纯 Dart 实现,而四种音效的具体生成算法(粉红噪声、分形噪声、幅度调制等)将在下一篇中详细展开。届时您将看到:

  • 白噪声 → 粉红噪声的滤波器设计
  • 海浪的低频振荡包络
  • 篝火爆裂事件的泊松分布模拟
  • 森林风噪与鸟鸣的 FM 合成

所有算法均不依赖任何外部资源,仅靠 dart:math 完成。


下一篇高斯噪声模拟自然音效的数学原理


作者简介:E‑Brufen Dev,Flutter & 鸿蒙开发者。WAV 编码器的实现参考了 WAV PCM soundfile format 规范,项目地址:AtomGit - E‑Brufen

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐