# av.audio

纯代码合成音效、混音并做响度验收，不需要任何素材文件。主要用于给代码生成的视频配声，输出可直接交给 `sys.mediaFoundation.writer` 写入 MP4。

- `av.audio`：音频片段 `av.audio.clip`（合成、滤波、效果、剪辑、输出、分析）与工具函数。
- `av.audio.sfx`：24 种常用音效预设（whoosh、pop、impact、riser……）。
- `av.audio.mixer`：时间线混音器，按时间放置音效和音乐，自动闪避、响度归一化和真峰值限幅。
- `av.audio.music`：程序化配乐，按风格、速度、段落强度和重音现场合成背景音乐。
- `av.audio.dsp`：DSP 内核（C 函数，预编译成 DLL 内嵌在库里、内存加载，运行时不需要 tcc；C 源码与重建脚本在库目录的 `.res/`），一般不直接使用。

音频内部是 float32 平面 PCM（1 或 2 声道），默认 48000Hz。所有随机性来自 `seed`，同样的代码总是得到逐采样相同的声音。生成视频是耗时任务，请放在后台线程运行。

## 快速开始：给视频配音效

同一张 cue 表同时驱动画面和声音。音效的“命中点”（`anchor`）对齐到 cue 时间：pop、click、impact 在起音处，whoosh 在最响处，riser、swell 在结束处。

```aardio
import av.audio.mixer;
import sys.mediaFoundation.writer;
import gdip.graphics;

var W,H,FPS,SR,DUR = 1280,720,30,48000,6;
var cues = {{1.0,"whoosh"};{1.6,"pop"};{3.0,"impact"};{4.5,"ding"}};

var mix = av.audio.mixer(DUR);
for(i,c in cues) mix.sfx(c[1],c[2]);
var sound = mix.render(); //默认归一化到 -16 LUFS、真峰值不超过 -1.5 dBTP

var writer = sys.mediaFoundation.writer("/demo.mp4",{width=W;height=H;fps=FPS;audio={sampleRate=SR;channels=2}});
var bmp = gdip.bitmap(W,H,0xE200B/*PARGB*/);
var g = gdip.graphics(bmp);
for(f=0;DUR * FPS - 1){
	g.clear(0xFF202020);
	//……按 f / FPS 绘制画面
	writer.writeFrame(bmp);
	
	//这一帧对应的采样范围用整数算法，任何帧率都不会累积误差
	var s0 = math.floor(f * SR / FPS);
	var s1 = math.floor((f + 1) * SR / FPS);
	writer.writeAudio(sound.pcm16(s0,s1 - s0,{channels=2}));
}
writer.close();
```

验收时可以直接看 `sound.report`（响度、真峰值、限幅衰减、各总线响度与闪避量），或用 `sound.onsets()` 核对声音起音是否落在 cue 时间。

## 音效预设 av.audio.sfx

`av.audio.sfx(name,opt)` 返回一个 `av.audio.clip`，已按典型用法归一化到固定响度（最大瞬时响度，见 `av.audio.sfx.levels`），峰值不超过 -1dBFS。在混音器里用 `mix.sfx(t,name,opt)`，同名音效每次自动换一个变体。

| 名称 | 命中点 | 用途 | 专用参数 |
|---|---|---|---|
| whoosh | 最响处 | 转场、飞入 | dur(0.8) peak(0.6) dir(1 左→右,-1,0) |
| swish | 最响处 | 滑动、切换 | dur(0.28) dir |
| pop / bubble | 起音 | 元素弹出 / 气泡 | pitch |
| click / tick / blip | 起音 | 界面点击 / 滴答 / 哔 | pitch，blip 可用 note |
| ding / chime / sparkle | 起音 | 铃声 / 琶音 / 闪烁 | note decay；chime: notes gap；sparkle: dur count |
| success / notify / error / coin | 起音 | 状态提示 | note 或 pitch |
| impact / boom / drop | 起音 | 标题落地 / 轰鸣 / 低音下潜 | dur pitch |
| riser / swell | 结束处 | 蓄势 / 反向镲 | dur |
| downer | 起音 | 释放 | dur |
| glitch | 起音 | 数字故障 | dur |
| type / typing | 起音 | 单次击键 / 连续打字 | typing: count interval dur |
| shutter | 起音 | 快门 | pitch |

通用参数：`seed`（变体）、`gain`（dB，在预设响度基础上调整）、`pan`（-1~1）、`pitch`（半音）、`reverb`（湿声量，0 关闭）、`normalize=false`（不归一化）。

自定义预设：给 `av.audio.sfx.presets` 添加 `function(opt){ return clip }`，并在 `av.audio.sfx.levels` 里设置目标响度。

## 混音器 av.audio.mixer

```aardio
import av.audio.mixer;
var mix = av.audio.mixer(10);                    //总时长 10 秒
mix.sfx(2.0,"riser",{dur=1.5});               //在 2.0 秒结束的蓄势
mix.sfx(2.0,"impact");                        //蓄势结束处落地
var voice = av.audio.load("/voice.mp3");         //读取音频文件（旁白等），失败返回 null
if(voice) mix.add(5.0,voice,{bus="voice";gain=-2});

var bed = av.audio.clip(10,2).osc("saw","A2",{amp=0.1;unison=5}).lp(700);
mix.music(bed,{gain=-6;fadeOut=2});           //music 总线：其他总线有声音时自动压低 4dB

var out = mix.render({lufs=-14});             //渲染：响度 -14 LUFS
out.saveWav("/mix.wav");
```

- `mix.bus(name,{gain;duck})` 设置总线。`duck` 为 `{amount=最大衰减 dB;by={触发总线};threshold=-45;range=15;attack=0.01;release=0.3}`，`false` 关闭。
- `render` 顺序：总线增益 → 闪避 → 汇总 → 低切 25Hz → 响度归一化 → 真峰值限幅（限幅使响度不足时补偿，最多衰减 `maxLimit`=6dB）→ 结尾淡出 0.05 秒。
- 默认真峰值上限是 -1.5 dBTP：实测 AAC 编码后真峰值会升高约 0.7dB。
- `render` 可重复调用（不修改已放置的内容），`mix.events` 记录所有放置事件。
- `sound.report` 还会给出 `eventsBySource`、每个总线的 `lufs/afterDuckLufs/duckReduction/duckDrop`。需要做声音预检时可写 `mix.render({bar=2;barOffset=0.1;eventMetrics=true})`，得到每小节响度与每个非配乐事件的局部窗口指标；`eventMetrics` 在 duck 后、母带低切/响度归一/限幅之前计算，`delta` 是该窗口混合总线相对音乐总线的增量，不含母带统一增益；这些是编码前 PCM 指标，不是 AAC 回读结果。

## 配乐 av.audio.music

按风格、速度（bpm）、段落强度（energy 0~3）和重音现场合成背景音乐，不需要任何素材。返回立体声 `av.audio.clip`，响度 -16 LUFS，可直接交给 `mix.music`。在 `av.film` 里写 `music="tech"` 即可，配乐会按时间线自动编排。

```aardio
import av.audio.music;
var m = av.audio.music("tech",{
	duration = 20;                //秒（必填）
	bpm = 120;                    //省略则用风格默认值
	sections = {{t=0;energy=1};{t=4;energy=3};{t=12;energy=2}}; //段落强度 0~3
	hits = {{t=4}};               //重音：加镲和底鼓
	endAt = 18;                   //终止和弦（主和弦长音一直响到结尾）
});
m.saveWav("/music.wav");
return {bpm = m.bpm;key = m.key;endAt = m.endAt;bars = table.slice(m.bars,1,3)}; //bars、beats 供画面卡点
```

| 风格 | 默认速度与调 | 特点 |
|---|---|---|
| tech | 124 BPM，A 小调 | 四拍底鼓、离拍贝斯、琶音，垫音随底鼓抽吸（i–VI–III–VII） |
| upbeat | 112 BPM，C 大调 | 电钢琴和弦、拨弦琶音、沙锤、拍手（I–V–vi–IV） |
| ambient | 76 BPM，D 大调 | 慢起圆润垫音、铃、木琴琶音、低音，几乎没有鼓（大七和弦，垫音开放排法） |
| lofi | 84 BPM，F 大调 | 摇摆节奏、暖音电钢琴七和弦、黑胶底噪（ii–V–I–vi） |

- 强度：0 只有垫音，1 前奏（垫音 + 低音 + 轻打击），2 铺垫（底鼓、贝斯节奏、琶音进入），3 高潮（全部声部、更亮）。强度升两级的段落前，鼓和贝斯停一拍。
- 省略 `sections` 时：前奏 2 小节后进入高潮；较长的曲子中段回落 2 小节。省略 `endAt` 时，终止和弦落在距结尾至少 1.5 秒的最后一个小节线上。
- 其他参数：`key`（如 `"A minor"`、`"Bb"`、`"F# dorian"`）、`seed`（变体）、`lufs`、`stems=true`（返回值的 `stems` 为各总线片段、`events` 为音符事件表，便于检查）。
- 返回值附带 `beats`、`bars`（节拍、小节时刻）、`sections`、`bpm`、`key`、`endAt`、`notes`（音符数）。
- 短页面蒙太奇不要让每个场景都换一套 groove：用 `mode="continuous"` 固定连续配乐床，`energy` 指定固定层级；`autoAccents=false` 关闭段落自动重音，`hits` 保留手动命中点。`hitMergeWindow` 合并同一命中点附近的重复重音，手动命中优先。`beatOffset` 同时影响 `beats/bars` 与 film 的拍点检查。
- 各总线按风格的 `mix` 表自动平衡到相对响度（测门限整合响度，与乐器实现无关），再做侧链抽吸、总线压缩、响度归一与真峰值限幅。20 秒配乐约 3 秒。

自定义风格：复制 `av.audio.music.styles.tech` 改成新表，放进 `av.audio.music.styles` 即可。节奏型是 16 步字符串（每步 1/16 拍；`X` 重音、`x` 正常、`o` 轻、`.` 休止；贝斯的 `"S"` 表示整小节长音），按强度 0~3 分别写；`pad` 的值是亮度 0~1，`bell` 的值是每个八分音符出现的概率。乐器在 `av.audio.music.instruments` 里，每个函数返回一个单声道片段；`sounds` 表给乐器传音色参数，例如垫音 `{voices=3;spread=8;poles=4;cutoff={400;1400}}`（齐奏数、失谐音分、24dB 低通、截止频率），琶音 `{tone="mallet"}`（木琴类，最圆润）或 `{tone="ks"}`（拨弦）。

配乐的音色规则（来自试听反馈与客观测量，自定义风格时也适用）：
- 持续的低音不要低于约 55Hz（A1）。手机和笔记本的小喇叭放不出这么低的声音，只会让振膜大幅振动而失真，耳机里则有压迫感；短促的轰隆声（音效里的 boom、impact）不受影响。
- 中音区的持续和弦避免相邻小二度（大七和弦的七音挨着根音）。泛音会打出 30~80Hz 的拍，听起来沙哑发糙；垫音默认用开放排法避开它（`padVoicing="close"` 关闭）。
- 同时响的铃不要相差半音。两只铃差半音时粗糙度是差三度的约 38 倍，内置的铃声部只用三和弦音，并避开仍在响的铃。

## 音频片段 av.audio.clip

`av.audio.clip(秒数,声道数=1)` 创建静音片段，多数方法返回片段自身，可以链式调用。滤波、包络等处理作用于整个片段，要分别处理的声音层先各自生成再 `mix` 到一起：

```aardio
import av.audio;
//一个电影感冲击：下潜正弦 + 噪声瞬态 + 混响
var hit = av.audio.clip(1.2);
hit.osc("sine",{{0,120};{0.07,50,"exp"};{0.8,38,"exp"}},{amp=av.audio.perc(0.002,0.9)});
var crack = av.audio.clip(0.3).noise("white").lp({{0,8000};{0.2,400,"exp"}}).amp(av.audio.perc(0.001,0.25,-6));
hit.mix(crack,0,{gain=-6}).drive(1.8).reverb({mix=0.2;room=0.8});
hit.normalize(-3).saveWav("/hit.wav");
```

**可自动化的参数**（频率、幅度、截止频率、声像、FM 指数、dB 增益）可写：

- 数值，频率还可写音名 `"A4"`、`"C#5"`、`"Bb3"`；
- 曲线点表 `{{时间,值},{时间,值,形状},...}`，形状写在段末点上：省略为线性，`"exp"` 指数（适合频率），`"hold"` 阶跃，`"sine"` S 形，数值为曲率（负数先快后慢，适合衰减；正数先慢后快，适合渐强）；
- 函数 `function(t){ return 值 }`，每 32 个采样求值一次后线性插值，例如 `av.audio.lfo(6,0.3)`。

曲线时间从处理区域起点算起。常用包络：`av.audio.perc(起音,衰减,曲率)`、`av.audio.adsr(a,d,s,r,总时长)`。

| 类别 | 方法 |
|---|---|
| 发声（叠加） | `osc(wave,freq,opt)` wave: sine/tri/saw/square，opt: amp pw detune unison spread seed phase；`noise("white"/"pink"/"brown",opt)`；`fm(freq,{ratio;index;feedback})`；`pluck(freq,{decay;bright})` 拨弦；`modal(freq,{{倍频,幅度,衰减秒},...})` 钟/木琴；`dust(每秒个数)` |
| 增益包络 | `amp(值或曲线)`、`gain(dB)`、`fade(in,out)`、`normalize(目标,"peak"/"lufs"/"momentary")` |
| 滤波 | `lp` `hp` `bp` `notch`（freq,q）、`eq(freq,dB,q)`、`lowShelf/highShelf(freq,dB)`，12dB/倍频，频率可自动化 |
| 失真 | `drive(量,"tanh"/"clip"/"fold"/"tube",mix)`、`crush(位数,保持采样数)` |
| 声像 | `pan(-1~1 或曲线)`（单声道转立体声，恒定功率，居中时两声道等于原声）、`stereo()`、`mono()`、`width(w)` |
| 空间（会延长片段） | `reverb({mix;room;damp;predelay;lowcut;highcut;tail})`、`delay(秒,{feedback;mix;damp;pingpong})`、`chorus({rate;depth;mix})` |
| 动态 | `compress({threshold;ratio;knee;attack;release;makeup;sidechain})`、`limit({ceiling;truePeak;lookahead;release})` |
| 剪辑 | `mix(src,时间,{gain;pan})`（src.anchor 对齐到时间）、`copy()`、`slice(t0,t1)`、`resize(帧)`、`extend(秒)`、`pad(前,后)`、`trim()`、`reverse()`、`clear()` |
| 输出 | `pcm16(起始帧,帧数,{channels;dither})`、`saveWav(path,{bits=16/32})`；`av.audio.load(音频文件[,秒数])`、`av.audio.fromPcm16(pcm,声道数,采样率)` |

`av.audio.load` 读 WAV、MP3、M4A/AAC、WMA 和 MP4/MOV 的音轨，返回的片段总是 `av.audio.sampleRate`（默认 48000）、1 或 2 声道：同采样率的 WAV 直接解析（逐采样不变），其他交给系统 Media Foundation 解码并重采样，可以直接进混音器。可选第 2 个参数只读开头若干秒（长音乐只取用得到的部分）。
| 分析 | `analyze()`、`loudness()`、`peak()`、`truePeak()`、`rms()`、`bands()`、`onsets()` |

大多数处理方法的最后一个参数 `opt` 可用 `start`、`dur`（秒）限定处理区域。

## 分析与验收

- `loudness()` 按 ITU-R BS.1770-4 返回 整合响度 LUFS（绝对/相对门限）、最大瞬时响度（400ms）、最大短期响度（3s）。单声道片段按居中放置（两声道相同）计算；不足 400ms 的片段整段作为一个块。
- `truePeak()` 为 4 倍过采样真峰值（dBTP）。突然开始或截断的声音在边界处会有真实的过冲。
- `onsets()` 返回起音时间（秒），精确到采样附近，用于核对音画同步；对安静背景上的瞬态声音最可靠。
- `analyze()` 汇总 peak、truePeak、rms、lufs、momentaryMax、shortTermMax、dc、clipped（满幅采样数）、start/end（首尾超过 -60dBFS 的时间）。
- `bands()` 返回低（<200Hz）、中（200~2k）、高（2k~8k）、极高（>8k）频段能量占比，可粗查音色是否合理。

实测（本机）：混音逐帧写入 MP4 后回读，AAC 起音误差 ≤ 0.5ms，响度变化 < 0.05 LU，画面闪白帧与声音起音在同一帧；30 秒、含 24 个音效和音乐床的混音渲染约 3 秒。

## 工具函数

`av.audio.freq("A4")` 音名转频率，`av.audio.midi("C4")`、`av.audio.mtof(60)`，`av.audio.dbToAmp(db)`、`av.audio.ampToDb(amp)`，`av.audio.perc`、`av.audio.adsr`、`av.audio.lfo`，`av.audio.random01(a,b,c)` 确定性随机数。
