File size: 16,706 Bytes
e67dc56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// ♈ 白羊座 —— 史诗配乐 + 音效(WebAudio 离线合成)
// E 小调英雄进行 Em–C–G–D;太鼓 / 定音鼓 / 弦乐 / 铜管 / 竖琴 / 钟声
// buildScore(TIMELINE) → AudioBuffer(立体声 48k,仅音乐与音效,不含旁白)

const SR = 48000;
const mtof = m => 440 * Math.pow(2, (m - 69) / 12);
const PROG = [ // [bass, chord tones]
  [40, [52, 55, 59, 64]], // Em
  [36, [48, 52, 55, 60]], // C
  [43, [55, 59, 62, 67]], // G
  [38, [50, 54, 57, 62]], // D
];
const LEVEL = { dawn: .55, first: .35, find: .22, names: .25, fleece1: .5, fleece2: .6, glyph: .35, fire: .78, badge: 1, onward: .5 };

function seeded(seed) { let s = seed >>> 0; return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296); }

function impulse(ctx, sec = 2.4, decay = 2.6) {
  const n = Math.floor(SR * sec), b = ctx.createBuffer(2, n, SR), r = seeded(7);
  for (let c = 0; c < 2; c++) { const d = b.getChannelData(c); for (let i = 0; i < n; i++) d[i] = (r() * 2 - 1) * Math.pow(1 - i / n, decay); }
  return b;
}
function noiseBuf(ctx, sec = 3) {
  const n = Math.floor(SR * sec), b = ctx.createBuffer(1, n, SR), d = b.getChannelData(0), r = seeded(99);
  for (let i = 0; i < n; i++) d[i] = r() * 2 - 1; return b;
}

export async function buildScore(TL, opts = {}) {
  const len = TL.length + .5;
  const ctx = new OfflineAudioContext(2, Math.ceil(len * SR), SR);
  const master = ctx.createGain(); master.gain.value = .56;
  const comp = ctx.createDynamicsCompressor(); comp.threshold.value = -14; comp.ratio.value = 3.5; comp.attack.value = .01; comp.release.value = .25;
  master.connect(comp); comp.connect(ctx.destination);
  const rev = ctx.createConvolver(); rev.buffer = impulse(ctx); const revG = ctx.createGain(); revG.gain.value = .32; rev.connect(revG); revG.connect(master);
  const music = ctx.createGain(); music.connect(master); music.connect(rev);
  const sfx = ctx.createGain(); sfx.gain.value = .9; sfx.connect(master); const sfxRev = ctx.createGain(); sfxRev.gain.value = .5; sfx.connect(sfxRev); sfxRev.connect(rev);
  // 旁白时音乐自动让位(ducking)
  music.gain.setValueAtTime(0, 0); music.gain.linearRampToValueAtTime(1, 2.5);
  for (const a of TL.acts) { const s = a.vo.at, e = a.vo.at + a.vo.len; music.gain.setTargetAtTime(.45, s - .3, .25); music.gain.setTargetAtTime(1, e, .6); }
  music.gain.setTargetAtTime(0, TL.length - 4, 1.2);
  const NB = noiseBuf(ctx);
  const actAt = t => { for (const a of TL.acts) if (t < a.t1) return a; return TL.acts[TL.acts.length - 1]; };
  const cue = (i, n) => { const a = TL.acts[i]; return (a.lines[n] || a.lines[a.lines.length - 1]).a; };

  /* ---------- 乐器 ---------- */
  const env = (g, t, a, peak, d, rel) => { g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(peak, t + a); g.gain.setTargetAtTime(0, t + a + d, rel); };
  const pan = (node, p, dest) => { const pn = ctx.createStereoPanner(); pn.pan.value = p; node.connect(pn); pn.connect(dest); return pn; };
  function pad(t, dur, notes, vol) {
    const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 1100; f.Q.value = .6;
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + dur * .35); g.gain.setValueAtTime(vol, t + dur * .7); g.gain.linearRampToValueAtTime(0, t + dur + .4);
    f.connect(g); g.connect(music);
    notes.forEach((m, i) => { for (const det of [-7, 7]) { const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = mtof(m); o.detune.value = det; const og = ctx.createGain(); og.gain.value = .25; o.connect(og); pan(og, (i % 2 ? .4 : -.4) * Math.sign(det), f); o.start(t); o.stop(t + dur + .5); } });
  }
  function bass(t, dur, m, vol) {
    const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = mtof(m);
    const s = ctx.createOscillator(); s.type = 'sine'; s.frequency.value = mtof(m - 12);
    const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 320;
    const g = ctx.createGain(); env(g, t, .05, vol, dur * .6, dur * .3);
    o.connect(f); s.connect(f); f.connect(g); g.connect(music); o.start(t); s.start(t); o.stop(t + dur + 1); s.stop(t + dur + 1);
  }
  function harp(t, m, vol, p = 0) {
    const o = ctx.createOscillator(); o.type = 'triangle'; o.frequency.value = mtof(m);
    const o2 = ctx.createOscillator(); o2.type = 'sine'; o2.frequency.value = mtof(m) * 2;
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + .006); g.gain.setTargetAtTime(0, t + .01, .45);
    const g2 = ctx.createGain(); g2.gain.value = .3; o2.connect(g2); g2.connect(g); o.connect(g); pan(g, p, music); o.start(t); o2.start(t); o.stop(t + 2.5); o2.stop(t + 2.5);
  }
  function str(t, dur, m, vol, p = 0) {
    const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = mtof(m);
    const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 1800;
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + .02); g.gain.setTargetAtTime(0, t + dur * .7, .06);
    o.connect(f); f.connect(g); pan(g, p, music); o.start(t); o.stop(t + dur + .4);
  }
  function brass(t, dur, notes, vol) {
    notes.forEach((m, i) => {
      const o = ctx.createOscillator(); o.type = 'sawtooth'; o.frequency.value = mtof(m); o.detune.value = (i - 1) * 4;
      const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.Q.value = 2; f.frequency.setValueAtTime(300, t); f.frequency.linearRampToValueAtTime(2400, t + .18); f.frequency.setTargetAtTime(1200, t + .25, .4);
      const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + .08); g.gain.setValueAtTime(vol * .8, t + dur * .7); g.gain.linearRampToValueAtTime(0, t + dur + .3);
      o.connect(f); f.connect(g); pan(g, (i - 1) * .3, music); o.start(t); o.stop(t + dur + .4);
    });
  }
  function drum(t, vol, f0 = 140, f1 = 48, dest = music, decay = .35) {
    const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.setValueAtTime(f0, t); o.frequency.exponentialRampToValueAtTime(f1, t + .18);
    const g = ctx.createGain(); g.gain.setValueAtTime(vol, t); g.gain.setTargetAtTime(0, t + .02, decay);
    o.connect(g); g.connect(dest); o.start(t); o.stop(t + 2.5);
    const n = ctx.createBufferSource(); n.buffer = NB; const nf = ctx.createBiquadFilter(); nf.type = 'lowpass'; nf.frequency.value = 900;
    const ng = ctx.createGain(); ng.gain.setValueAtTime(vol * .5, t); ng.gain.setTargetAtTime(0, t, .05);
    n.connect(nf); nf.connect(ng); ng.connect(dest); n.start(t, Math.random() * 1.5, .5);
  }
  function noise(t, dur, vol, type, f0, f1, dest = sfx, q = 1, att = .02) {
    const n = ctx.createBufferSource(); n.buffer = NB; n.loop = true;
    const f = ctx.createBiquadFilter(); f.type = type; f.Q.value = q; f.frequency.setValueAtTime(f0, t); f.frequency.exponentialRampToValueAtTime(Math.max(20, f1), t + dur);
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + att); g.gain.setTargetAtTime(0, t + att, dur / 3);
    n.connect(f); f.connect(g); g.connect(dest); n.start(t); n.stop(t + dur + 1.5);
  }
  function bell(t, m, vol, dest = sfx, p = 0) {
    [[1, 1], [2.76, .45], [5.4, .25], [8.9, .12]].forEach(([r, a]) => {
      const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = mtof(m) * r;
      const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol * a, t + .004); g.gain.setTargetAtTime(0, t + .01, 1.1 / r);
      o.connect(g); pan(g, p, dest); o.start(t); o.stop(t + 5);
    });
  }
  const sparkle = (t, n = 6, vol = .06) => { const r = seeded(Math.floor(t * 100)); for (let i = 0; i < n; i++) bell(t + i * .06 + r() * .03, 88 + Math.floor(r() * 12), vol * (1 - i / n * .5), sfx, r() * 1.6 - .8); };
  const cymbal = (t, vol = .22, dur = 3) => noise(t, dur, vol, 'highpass', 6000, 4000, sfx, .5, .005);
  const riser = (t, dur, vol = .16) => { const n = ctx.createBufferSource(); n.buffer = NB; n.loop = true; const f = ctx.createBiquadFilter(); f.type = 'bandpass'; f.Q.value = 3; f.frequency.setValueAtTime(300, t); f.frequency.exponentialRampToValueAtTime(5000, t + dur);
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(vol, t + dur); g.gain.linearRampToValueAtTime(0, t + dur + .08); n.connect(f); f.connect(g); g.connect(sfx); n.start(t); n.stop(t + dur + .2); };
  const whoosh = (t, vol = .2, dur = .9) => noise(t, dur, vol, 'bandpass', 400, 3000, sfx, 1.4, dur * .4);
  const boom = (t, vol = .9) => { drum(t, vol, 90, 32, sfx, .7); noise(t, 1.6, vol * .3, 'lowpass', 400, 60, sfx, .7, .005); };
  const thunder = (t, vol = .5) => { noise(t, .25, vol, 'highpass', 3000, 1500, sfx, .6, .002); noise(t + .05, 4, vol * .9, 'lowpass', 600, 80, sfx, .5, .03); drum(t + .1, vol * .8, 70, 30, sfx, 1); };
  const anvil = (t, vol = .2) => { [[2093, 1], [2637, .6], [4186, .4], [5588, .3]].forEach(([f, a]) => { const o = ctx.createOscillator(); o.frequency.value = f; const g = ctx.createGain(); g.gain.setValueAtTime(vol * a, t); g.gain.setTargetAtTime(0, t, .25); o.connect(g); g.connect(sfx); o.start(t); o.stop(t + 2); }); drum(t, vol * 2, 200, 80, sfx, .1); };
  const splash = t => { noise(t, 1.2, .25, 'bandpass', 1200, 300, sfx, .8, .01); noise(t + .1, 2, .1, 'highpass', 4000, 2000, sfx, .5, .2); };
  const pop = (t, vol = .25) => { drum(t, vol, 400, 120, sfx, .05); noise(t + .02, 1.4, vol * .25, 'highpass', 5000, 3000, sfx, .5, .01); };

  /* ---------- 音乐编排 ---------- */
  const beat = 60 / 84, bar = beat * 4;
  for (let k = 0, t = 0; t < TL.length - 3; k++, t = k * bar) {
    const a = actAt(t + .01), L = LEVEL[a.id], [b, ch] = PROG[k % 4], tail = TL.length - t;
    const fade = Math.min(1, tail / 10);
    pad(t, bar, ch.slice(0, 3), (.022 + .02 * L) * fade);
    if (L >= .3) bass(t, bar * .95, b, .1 * L * fade);
    if (['first', 'find', 'names', 'glyph', 'onward', 'dawn'].includes(a.id)) {
      const arp = [0, 1, 2, 3, 2, 1, 2, 3]; arp.forEach((j, i) => harp(t + i * beat / 2, ch[j] + (a.id === 'find' || a.id === 'names' ? 12 : 0), (.05 + .03 * L) * fade, Math.sin(i) * .5));
    }
    if (L >= .5 && a.id !== 'dawn') { for (let i = 0; i < 8; i++) str(t + i * beat / 2, beat / 2, (i % 2 ? ch[2] : ch[0]) - 12, .045 * L * fade, i % 2 ? .3 : -.3); }
    if (L >= .6) { drum(t, .55 * L); drum(t + beat * 1.5, .35 * L); drum(t + beat * 2, .5 * L); if (L > .9) { drum(t + beat * 3, .4 * L); drum(t + beat * 3.5, .3 * L); } }
    if ((a.id === 'badge' || a.id === 'fire') && k % 2 === 0) brass(t, bar * (a.id === 'badge' ? 1.8 : 1.2), ch.slice(0, 3).map(m => m - 12), a.id === 'badge' ? .05 : .03);
    if (a.id === 'badge' || (a.id === 'onward' && t < a.t0 + 8)) for (let i = 0; i < 4; i++) str(t + i * beat, beat, ch[(i + 1) % 4] + 12, .02 * fade, i % 2 ? .5 : -.5);
  }
  // 序章:低频持续音 + 晨光
  pad(0, 12, [28, 40, 47], .05);
  const A = i => TL.acts[i];
  // —— 1 dawn
  riser(cue(0, 3) - 1.5, 1.8, .1); boom(cue(0, 4) + .25, .8); whoosh(cue(0, 4) + .2, .18);
  riser(cue(0, 5) - 1.2, 1.25, .14); boom(cue(0, 5) + .1, 1); cymbal(cue(0, 5) + .1, .2, 3.5); brass(cue(0, 5) + .1, 3, [40, 47, 52, 55], .06);
  bell(cue(0, 5) + 1.5, 76, .12); sparkle(cue(0, 5) + 1.5, 5); whoosh(cue(0, 6) - .2, .14);
  // —— 2 first
  sparkle(cue(1, 3) - .1, 8, .05); bell(cue(1, 4) + .4, 71, .14); boom(cue(1, 4) + .3, .35);
  for (let i = 0; i < 12; i++) bell(cue(1, 5) + (cue(1, 6) + 1.2 - cue(1, 5)) * i / 12, 76 + [0, 2, 3, 5, 7, 8, 10, 12, 14, 15, 17, 19][i], .05, sfx, (i / 11 - .5));
  whoosh(cue(1, 7) - .4, .12, 1.2); bell(cue(1, 7) + 1, 64, .14); bell(cue(1, 7) + 1, 71, .1); sparkle(cue(1, 7) + 2, 6);
  // —— 3 find
  for (let i = 0; i < 3; i++) bell(cue(2, 2) + i * .45, 83 + i * 2, .09, sfx, i * .4 - .4);
  sparkle(cue(2, 4) - .1, 10, .045); bell(cue(2, 6) - .2, 59, .1); bell(cue(2, 6) + .1, 66, .08);
  // —— 4 names
  boom(A(3).t0 + .3, .35); bell(cue(3, 2) - .1, 69, .09); whoosh(cue(3, 4) - .2, .12, 2);
  whoosh(cue(3, 5) - .6, .16, 1); bell(cue(3, 7) - .1, 88, .1, sfx, -.4); bell(cue(3, 7) + .1, 91, .1, sfx, .4); bell(cue(3, 8) - .1, 64, .12); sparkle(cue(3, 10), 5);
  // —— 5 fleece1
  boom(A(4).t0 + .4, .5); cymbal(A(4).t0 + .4, .1, 2);
  thunder(cue(4, 3) + .4, .35); thunder(cue(4, 4) - .3, .25);
  riser(cue(4, 4) - .5, 1.2, .1); sparkle(cue(4, 4) + 1, 8, .06); whoosh(cue(4, 5), .22, 1.4); whoosh(cue(4, 6), .14, 1.6);
  whoosh(cue(4, 7), .1, 1.6); splash(cue(4, 7) + 1.8); bell(cue(4, 8) + .2, 59, .1);
  // —— 6 fleece2
  whoosh(A(5).t0 + .5, .12, 2.2); bell(cue(5, 1), 79, .08); noise(cue(5, 2), 2.5, .08, 'bandpass', 200, 120, sfx, 2, .5);
  for (let i = 0; i < 6; i++) drum(cue(5, 3) + i * beat / 2, .12, 120, 60, sfx, .1);
  riser(cue(5, 4) - .3, 2.5, .1); sparkle(cue(5, 6), 10, .06); bell(cue(5, 6) + .4, 64, .12); boom(cue(5, 6) + .3, .4);
  // —— 7 glyph
  sparkle(cue(6, 1), 5, .04); bell(cue(6, 2) + .2, 81, .07);
  riser(cue(6, 5) - 1.2, 1.4, .1); boom(cue(6, 5) + .2, .45); bell(cue(6, 5) + .2, 64, .14); bell(cue(6, 5) + .2, 71, .1); sparkle(cue(6, 5) + .4, 10, .05);
  // —— 8 fire
  noise(cue(7, 1) - .2, 1.2, .2, 'lowpass', 2000, 300, sfx, .8, .08); boom(cue(7, 2), .4); bell(cue(7, 2) + .1, 52, .08);
  [cue(7, 3) + .5, cue(7, 3) + 1.3, cue(7, 3) + 2.1, cue(7, 4)].forEach((t, i) => { anvil(t, .08); drum(t, .35, 160, 60, sfx, .15); });
  whoosh(cue(7, 5) - .2, .3, 1.2); for (let i = 0; i < 4; i++) drum(cue(7, 5) + i * .18, .25, 100, 50, sfx, .08);
  sparkle(cue(7, 7) + .4, 6, .05);
  thunder(cue(7, 8) + .2, .7); cymbal(cue(7, 9) - .1, .12, 3);
  // —— 9 badge(高潮)
  const b0 = cue(8, 0);
  riser(b0 - 2, 1.6, .16); boom(b0 - .3, .7);
  noise(b0 - .3, 3, .14, 'lowpass', 1500, 400, sfx, .7, .3);  // 熔炉轰鸣
  anvil(b0 + .3, .14); anvil(b0 + 1.0, .14); anvil(cue(8, 1) - .1, .18);
  boom(cue(8, 1) + .2, 1); cymbal(cue(8, 1) + .2, .25, 4); brass(cue(8, 1) + .2, 4, [40, 47, 52, 55, 59], .06);
  for (let i = 0; i < 4; i++) sparkle(cue(8, 2) + i * .7, 4, .04);
  whoosh(cue(8, 4) - .3, .2, 1); bell(cue(8, 4) + .1, 76, .14); bell(cue(8, 4) + .1, 83, .1);
  pop(cue(8, 4) + .4, .3); pop(cue(8, 4) + .7, .3); sparkle(cue(8, 4) + .5, 12, .05);
  bell(cue(8, 5) + .1, 64, .16); bell(cue(8, 5) + .1, 71, .12); bell(cue(8, 5) + .1, 76, .1);
  boom(cue(8, 5) + 1.4, .9); cymbal(cue(8, 5) + 1.4, .25, 4); pop(cue(8, 5) + 1.5, .3); pop(cue(8, 5) + 1.9, .25); pop(cue(8, 5) + 2.3, .25);
  brass(cue(8, 5) + 1.4, 3.5, [40, 47, 52, 56, 59], .06); // E 大调终止 —— 皮卡第三度
  // —— 10 onward
  pad(A(9).t0, 8, [52, 56, 59, 64], .04);
  bell(cue(9, 2) + .3, 76, .1); sparkle(cue(9, 2) + .3, 5, .04);
  riser(cue(9, 4) - 1.4, 1.5, .1); boom(cue(9, 4), .6); cymbal(cue(9, 4), .14, 3.5);
  brass(cue(9, 4), 5, [40, 47, 52, 56, 59], .05); bell(cue(9, 4) + .1, 64, .14); bell(cue(9, 4) + .1, 68, .1); bell(cue(9, 4) + .1, 71, .1);
  pad(cue(9, 4), TL.length - cue(9, 4), [40, 52, 56, 59, 64], .05);
  sparkle(cue(9, 5) + .3, 8, .04);

  return await ctx.startRendering();
}

// 把配乐 + 各幕旁白合成为一条完整音轨(播放与导出共用,保证完全同步)
export async function mixTrack(TL, scoreBuf, voBufs) {
  const ctx = new OfflineAudioContext(2, Math.ceil(TL.length * SR), SR);
  const s = ctx.createBufferSource(); s.buffer = scoreBuf; const sg = ctx.createGain(); sg.gain.value = 1; s.connect(sg); sg.connect(ctx.destination); s.start(0);
  TL.acts.forEach((a, i) => { const b = voBufs[i]; if (!b) return; const v = ctx.createBufferSource(); v.buffer = b; const g = ctx.createGain(); g.gain.value = 1.05; v.connect(g); g.connect(ctx.destination); v.start(a.vo.at); });
  return await ctx.startRendering();
}

export function toWav(buf) {
  const n = buf.length, ch = buf.numberOfChannels, out = new DataView(new ArrayBuffer(44 + n * ch * 2));
  const w = (o, s) => { for (let i = 0; i < s.length; i++) out.setUint8(o + i, s.charCodeAt(i)); };
  w(0, 'RIFF'); out.setUint32(4, 36 + n * ch * 2, true); w(8, 'WAVE'); w(12, 'fmt '); out.setUint32(16, 16, true); out.setUint16(20, 1, true); out.setUint16(22, ch, true);
  out.setUint32(24, buf.sampleRate, true); out.setUint32(28, buf.sampleRate * ch * 2, true); out.setUint16(32, ch * 2, true); out.setUint16(34, 16, true); w(36, 'data'); out.setUint32(40, n * ch * 2, true);
  const data = [...Array(ch)].map((_, c) => buf.getChannelData(c));
  let o = 44; for (let i = 0; i < n; i++) for (let c = 0; c < ch; c++) { const v = Math.max(-1, Math.min(1, data[c][i])); out.setInt16(o, v < 0 ? v * 0x8000 : v * 0x7fff, true); o += 2; }
  return new Blob([out], { type: 'audio/wav' });
}
export { SR };