Spaces:
Running
Running
Download aries/src/score.js from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/score.js
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/aries/src/score.js
-
curl -L -o score.js https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/aries/src/score.js
16.7 kB
| // ♈ 白羊座 —— 史诗配乐 + 音效(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 }; | |