frameyard 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -14
- package/GLOSSARY.md +112 -2
- package/README.md +302 -265
- package/dist/assets/integrations.d.ts +8 -4
- package/dist/assets/integrations.d.ts.map +1 -1
- package/dist/assets/integrations.js +8 -4
- package/dist/assets/integrations.js.map +1 -1
- package/dist/assets/provenance.d.ts +2 -2
- package/dist/assets/provenance.d.ts.map +1 -1
- package/dist/assets/provenance.js +5 -3
- package/dist/assets/provenance.js.map +1 -1
- package/dist/assets/tts-supertonic.d.ts +28 -0
- package/dist/assets/tts-supertonic.d.ts.map +1 -0
- package/dist/assets/tts-supertonic.js +156 -0
- package/dist/assets/tts-supertonic.js.map +1 -0
- package/dist/assets/tts.d.ts +18 -6
- package/dist/assets/tts.d.ts.map +1 -1
- package/dist/assets/tts.js +219 -97
- package/dist/assets/tts.js.map +1 -1
- package/dist/cli.js +1685 -93
- package/dist/cli.js.map +1 -1
- package/dist/composition/format.d.ts +1 -1
- package/dist/composition/format.d.ts.map +1 -1
- package/dist/composition/format.js +1 -1
- package/dist/composition/format.js.map +1 -1
- package/dist/context.d.ts +82 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +277 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +25 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +14 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +166 -7
- package/dist/doctor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fonts/add.d.ts +41 -0
- package/dist/fonts/add.d.ts.map +1 -0
- package/dist/fonts/add.js +259 -0
- package/dist/fonts/add.js.map +1 -0
- package/dist/fonts/binary.d.ts +4 -0
- package/dist/fonts/binary.d.ts.map +1 -0
- package/dist/fonts/binary.js +21 -0
- package/dist/fonts/binary.js.map +1 -0
- package/dist/fonts/css.d.ts +4 -0
- package/dist/fonts/css.d.ts.map +1 -0
- package/dist/fonts/css.js +50 -0
- package/dist/fonts/css.js.map +1 -0
- package/dist/fonts/fontsource.d.ts +107 -0
- package/dist/fonts/fontsource.d.ts.map +1 -0
- package/dist/fonts/fontsource.js +565 -0
- package/dist/fonts/fontsource.js.map +1 -0
- package/dist/fonts/index.d.ts +11 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +11 -0
- package/dist/fonts/index.js.map +1 -0
- package/dist/fonts/install.d.ts +24 -0
- package/dist/fonts/install.d.ts.map +1 -0
- package/dist/fonts/install.js +145 -0
- package/dist/fonts/install.js.map +1 -0
- package/dist/fonts/manifest.d.ts +21 -0
- package/dist/fonts/manifest.d.ts.map +1 -0
- package/dist/fonts/manifest.js +96 -0
- package/dist/fonts/manifest.js.map +1 -0
- package/dist/fonts/options.d.ts +9 -0
- package/dist/fonts/options.d.ts.map +1 -0
- package/dist/fonts/options.js +66 -0
- package/dist/fonts/options.js.map +1 -0
- package/dist/fonts/schema.d.ts +4 -0
- package/dist/fonts/schema.d.ts.map +1 -0
- package/dist/fonts/schema.js +316 -0
- package/dist/fonts/schema.js.map +1 -0
- package/dist/fonts/summary.d.ts +16 -0
- package/dist/fonts/summary.d.ts.map +1 -0
- package/dist/fonts/summary.js +22 -0
- package/dist/fonts/summary.js.map +1 -0
- package/dist/fonts/types.d.ts +56 -0
- package/dist/fonts/types.d.ts.map +1 -0
- package/dist/fonts/types.js +8 -0
- package/dist/fonts/types.js.map +1 -0
- package/dist/fonts/verify.d.ts +33 -0
- package/dist/fonts/verify.d.ts.map +1 -0
- package/dist/fonts/verify.js +200 -0
- package/dist/fonts/verify.js.map +1 -0
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +59 -0
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +325 -0
- package/dist/inspect.js.map +1 -0
- package/dist/lint.d.ts +63 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +265 -0
- package/dist/lint.js.map +1 -0
- package/dist/policy.d.ts +69 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +361 -0
- package/dist/policy.js.map +1 -0
- package/dist/preview/fonts/LICENSE-figtree.txt +93 -0
- package/dist/preview/fonts/LICENSE-space-mono.txt +93 -0
- package/dist/preview/fonts/README.md +23 -0
- package/dist/preview/fonts/figtree-latin-wght-normal.woff2 +0 -0
- package/dist/preview/fonts/space-mono-latin-400-normal.woff2 +0 -0
- package/dist/preview/fonts/space-mono-latin-700-normal.woff2 +0 -0
- package/dist/preview/main.js +5 -2
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +17 -69
- package/dist/preview/timeline-data.d.ts.map +1 -1
- package/dist/preview/timeline-host.d.ts.map +1 -1
- package/dist/preview/timeline-host.js +70 -45
- package/dist/preview/timeline-host.js.map +1 -1
- package/dist/preview/timeline-main.d.ts.map +1 -1
- package/dist/preview/timeline-main.js +88 -27
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +10 -3
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -17
- package/dist/project.js.map +1 -1
- package/dist/renderer/audio.d.ts +6 -0
- package/dist/renderer/audio.d.ts.map +1 -1
- package/dist/renderer/audio.js +3 -1
- package/dist/renderer/audio.js.map +1 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +13 -0
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/concurrency.d.ts +6 -0
- package/dist/renderer/concurrency.d.ts.map +1 -0
- package/dist/renderer/concurrency.js +27 -0
- package/dist/renderer/concurrency.js.map +1 -0
- package/dist/renderer/ffmpeg.d.ts +104 -5
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +580 -67
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.js +1 -1
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyframe-expressions.d.ts +39 -0
- package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
- package/dist/renderer/keyframe-expressions.js +180 -0
- package/dist/renderer/keyframe-expressions.js.map +1 -0
- package/dist/renderer/session-pool.d.ts +28 -0
- package/dist/renderer/session-pool.d.ts.map +1 -0
- package/dist/renderer/session-pool.js +91 -0
- package/dist/renderer/session-pool.js.map +1 -0
- package/dist/renderer/timeline.d.ts +2 -0
- package/dist/renderer/timeline.d.ts.map +1 -1
- package/dist/renderer/timeline.js +600 -147
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/renderer/timings.d.ts +13 -0
- package/dist/renderer/timings.d.ts.map +1 -0
- package/dist/renderer/timings.js +29 -0
- package/dist/renderer/timings.js.map +1 -0
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +4 -0
- package/dist/runtime/host.js.map +1 -1
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +48 -320
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +2 -0
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +7 -0
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +74 -188
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/preview-style.d.ts +11 -0
- package/dist/server/preview-style.d.ts.map +1 -0
- package/dist/server/preview-style.js +298 -0
- package/dist/server/preview-style.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +31 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts.map +1 -1
- package/dist/server/timeline-preview-data.js +12 -34
- package/dist/server/timeline-preview-data.js.map +1 -1
- package/dist/server/timeline-preview-html.d.ts.map +1 -1
- package/dist/server/timeline-preview-html.js +140 -148
- package/dist/server/timeline-preview-html.js.map +1 -1
- package/dist/templates.d.ts +16 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +186 -0
- package/dist/templates.js.map +1 -0
- package/dist/timeline/easing.d.ts +36 -0
- package/dist/timeline/easing.d.ts.map +1 -0
- package/dist/timeline/easing.js +225 -0
- package/dist/timeline/easing.js.map +1 -0
- package/dist/timeline/keyframes.d.ts +23 -0
- package/dist/timeline/keyframes.d.ts.map +1 -0
- package/dist/timeline/keyframes.js +95 -0
- package/dist/timeline/keyframes.js.map +1 -0
- package/dist/timeline/plan.d.ts +6 -12
- package/dist/timeline/plan.d.ts.map +1 -1
- package/dist/timeline/plan.js +17 -30
- package/dist/timeline/plan.js.map +1 -1
- package/dist/timeline/resolve.d.ts +12 -0
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +105 -9
- package/dist/timeline/resolve.js.map +1 -1
- package/dist/timeline/scene.d.ts +126 -0
- package/dist/timeline/scene.d.ts.map +1 -0
- package/dist/timeline/scene.js +109 -0
- package/dist/timeline/scene.js.map +1 -0
- package/dist/timeline/schema.d.ts +31 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +240 -9
- package/dist/timeline/schema.js.map +1 -1
- package/dist/timeline/transitions.d.ts +21 -0
- package/dist/timeline/transitions.d.ts.map +1 -0
- package/dist/timeline/transitions.js +23 -0
- package/dist/timeline/transitions.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
- package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
- package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +2 -1
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +2 -2
- package/docs/adr/0016-author-compositions-as-html-fragments.md +1 -1
- package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
- package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
- package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
- package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
- package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
- package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
- package/docs/adr/0024-stream-composition-captures-and-skip-trivial-composites.md +86 -0
- package/docs/adr/0025-capture-timeline-layers-in-parallel-with-a-global-limit.md +67 -0
- package/docs/adr/0026-reuse-loaded-composition-sessions-across-segments.md +70 -0
- package/docs/adr/0027-encode-two-full-frame-layers-through-dual-pipes.md +76 -0
- package/docs/adr/0028-encode-full-frame-video-segments-directly.md +65 -0
- package/docs/migration-0.4.md +2 -2
- package/docs/preview-testing.md +27 -6
- package/docs/{v0.1-design.md → v0.x-design.md} +57 -32
- package/examples/blend/README.md +3 -3
- package/examples/demo/README.md +2 -2
- package/examples/intro/README.md +71 -0
- package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
- package/examples/intro/assets/grain.png +0 -0
- package/examples/intro/assets/score.ogg +0 -0
- package/examples/intro/compositions/brand.html +412 -0
- package/examples/intro/compositions/chrome.controller.js +63 -0
- package/examples/intro/compositions/chrome.html +256 -0
- package/examples/intro/compositions/lattice.controller.js +148 -0
- package/examples/intro/compositions/lattice.html +308 -0
- package/examples/intro/compositions/opening.html +504 -0
- package/examples/intro/compositions/studio.html +930 -0
- package/examples/intro/compositions/theme.css +218 -0
- package/examples/intro/fy.config.js +13 -0
- package/examples/intro/fy.fonts.json +100 -0
- package/examples/intro/scripts/make-grain.mjs +79 -0
- package/examples/intro/scripts/make-score.mjs +702 -0
- package/examples/intro/timelines/intro.timeline.json +60 -0
- package/examples/keyframes/README.md +76 -0
- package/examples/keyframes/compositions/backdrop.html +60 -0
- package/examples/keyframes/compositions/card.html +99 -0
- package/examples/keyframes/fy.config.js +10 -0
- package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
- package/examples/nested/README.md +112 -0
- package/examples/nested/assets/pulse.wav +0 -0
- package/examples/nested/compositions/backdrop.html +84 -0
- package/examples/nested/compositions/card.html +275 -0
- package/examples/nested/compositions/overlay.html +307 -0
- package/examples/nested/fy.config.js +13 -0
- package/examples/nested/scripts/make-pulse.sh +20 -0
- package/examples/nested/timelines/chapter-a.timeline.json +39 -0
- package/examples/nested/timelines/chapter-b.timeline.json +39 -0
- package/examples/nested/timelines/nested-reel.timeline.json +52 -0
- package/examples/static/README.md +2 -2
- package/examples/transitions/README.md +128 -0
- package/examples/transitions/assets/alpha.wav +0 -0
- package/examples/transitions/assets/beta.wav +0 -0
- package/examples/transitions/assets/gamma.wav +0 -0
- package/examples/transitions/compositions/alpha.html +56 -0
- package/examples/transitions/compositions/beta.html +56 -0
- package/examples/transitions/compositions/gamma.html +56 -0
- package/examples/transitions/fy.config.js +12 -0
- package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
- package/examples/transitions/timelines/transitions.timeline.json +31 -0
- package/examples/video/README.md +4 -4
- package/examples/webgl/README.md +1 -1
- package/package.json +13 -6
- package/scaffolds/init/AGENTS.md +131 -0
- package/scaffolds/init/README.md +56 -0
- package/scaffolds/init/compositions/hello.html +79 -0
- package/scaffolds/init/fy.config.js +9 -0
- package/scaffolds/init/gitignore.template +1 -0
- package/scaffolds/init/package.json +11 -0
- package/scaffolds/init/props/hello.json +4 -0
- package/scaffolds/init/props/main.json +6 -0
- package/scaffolds/init/timelines/main.timeline.json +20 -0
- package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
- package/scaffolds/local-models/fy.policy.json +5 -0
- package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
- package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
- package/scaffolds/local-models/pi/sandbox.json +15 -0
- package/scaffolds/local-models/pi/settings.json +15 -0
- package/skills/frameyard/README.md +1 -0
- package/skills/frameyard/SKILL.md +59 -61
- package/skills/frameyard/references/audio-assets.md +32 -3
- package/skills/frameyard/references/common-mistakes.md +53 -2
- package/skills/frameyard/references/deterministic-authoring.md +10 -3
- package/skills/frameyard/references/diagnostics.md +31 -1
- package/skills/frameyard/references/failure-recovery.md +32 -6
- package/skills/frameyard/references/fonts.md +132 -0
- package/skills/frameyard/references/json-results.md +119 -6
- package/skills/frameyard/references/quick-reference.md +222 -0
- package/skills/frameyard/references/timelines.md +97 -7
- /package/examples/blend/{video.config.js → fy.config.js} +0 -0
- /package/examples/demo/{video.config.js → fy.config.js} +0 -0
- /package/examples/static/{video.config.js → fy.config.js} +0 -0
- /package/examples/video/{video.config.js → fy.config.js} +0 -0
- /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Renders the 20-second original score for the Frameyard intro.
|
|
3
|
+
//
|
|
4
|
+
// The score is synthesized from scratch so the example stays self-contained:
|
|
5
|
+
// no samples, no model downloads, no licensing questions. Every random choice
|
|
6
|
+
// comes from a fixed-seed LCG, so regenerating the WAV is deterministic.
|
|
7
|
+
//
|
|
8
|
+
// Structure (120 BPM, D minor -> D major), aligned with the scene cuts:
|
|
9
|
+
// 0.0 – 4.5 author: sub drone, heartbeat kick, air
|
|
10
|
+
// 4.5 – 9.5 compose: Bb pad, plucked arpeggio, hats
|
|
11
|
+
// 9.5 – 14.5 render: F pad, four-on-the-floor, riser
|
|
12
|
+
// 14.5 – 20.0 frameyard: D major, impact, shimmer, tail
|
|
13
|
+
//
|
|
14
|
+
// The WAV synthesis is deterministic. The default Ogg Vorbis encode uses
|
|
15
|
+
// ffmpeg's libvorbis at q6 with bitexact flags, so the same toolchain
|
|
16
|
+
// reproduces the same bytes.
|
|
17
|
+
//
|
|
18
|
+
// Run from anywhere:
|
|
19
|
+
// node examples/intro/scripts/make-score.mjs
|
|
20
|
+
// node examples/intro/scripts/make-score.mjs /tmp/score.ogg
|
|
21
|
+
// node examples/intro/scripts/make-score.mjs /tmp/score.wav
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const out = process.argv[2] ? path.resolve(process.argv[2]) : path.join(here, "..", "assets", "score.ogg");
|
|
31
|
+
|
|
32
|
+
const SR = 48000;
|
|
33
|
+
const DURATION = 20;
|
|
34
|
+
const TAIL = 2.5;
|
|
35
|
+
const N = Math.ceil((DURATION + TAIL) * SR);
|
|
36
|
+
const OUT = Math.round(DURATION * SR);
|
|
37
|
+
const TAU = Math.PI * 2;
|
|
38
|
+
const BPM = 120;
|
|
39
|
+
const BEAT = 60 / BPM;
|
|
40
|
+
|
|
41
|
+
let seed = 0x51ed270b;
|
|
42
|
+
function random() {
|
|
43
|
+
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
|
44
|
+
return seed / 0x100000000;
|
|
45
|
+
}
|
|
46
|
+
function noise() {
|
|
47
|
+
return random() * 2 - 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hz(midi) {
|
|
51
|
+
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
52
|
+
}
|
|
53
|
+
function smoothstep(value) {
|
|
54
|
+
const x = Math.max(0, Math.min(1, value));
|
|
55
|
+
return x * x * (3 - 2 * x);
|
|
56
|
+
}
|
|
57
|
+
function envelope(t, duration, attack, release) {
|
|
58
|
+
if (t < 0 || t > duration) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
return smoothstep(t / attack) * smoothstep((duration - t) / release);
|
|
62
|
+
}
|
|
63
|
+
function panGains(pan) {
|
|
64
|
+
const p = (Math.max(-1, Math.min(1, pan)) + 1) / 2;
|
|
65
|
+
return [Math.cos(p * Math.PI * 0.5), Math.sin(p * Math.PI * 0.5)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Buses. Pad and bass are filtered after the notes are rendered; percussion,
|
|
69
|
+
// arpeggio, and effects stay mostly dry with selected reverb sends.
|
|
70
|
+
const padL = new Float64Array(N);
|
|
71
|
+
const padR = new Float64Array(N);
|
|
72
|
+
const bassL = new Float64Array(N);
|
|
73
|
+
const bassR = new Float64Array(N);
|
|
74
|
+
const arpL = new Float64Array(N);
|
|
75
|
+
const arpR = new Float64Array(N);
|
|
76
|
+
const percL = new Float64Array(N);
|
|
77
|
+
const percR = new Float64Array(N);
|
|
78
|
+
const fxL = new Float64Array(N);
|
|
79
|
+
const fxR = new Float64Array(N);
|
|
80
|
+
const revSend = new Float64Array(N);
|
|
81
|
+
const delaySend = new Float64Array(N);
|
|
82
|
+
const duck = new Float64Array(N).fill(1);
|
|
83
|
+
|
|
84
|
+
function addPadNote(start, duration, midi, gain, pan, brightness = 1) {
|
|
85
|
+
const frequency = hz(midi);
|
|
86
|
+
const length = Math.round(duration * SR);
|
|
87
|
+
const startIndex = Math.round(start * SR);
|
|
88
|
+
const [gainL, gainR] = panGains(pan);
|
|
89
|
+
const detunes = [-0.0055, 0, 0.0055];
|
|
90
|
+
const harmonics = brightness > 1 ? 6 : 5;
|
|
91
|
+
|
|
92
|
+
for (const detune of detunes) {
|
|
93
|
+
const voiceFrequency = frequency * (1 + detune);
|
|
94
|
+
let phase = random() * TAU;
|
|
95
|
+
let lfoPhase = random() * TAU;
|
|
96
|
+
const voicePan = pan + (detune < 0 ? -0.08 : detune > 0 ? 0.08 : 0);
|
|
97
|
+
const [voiceL, voiceR] = panGains(voicePan);
|
|
98
|
+
for (let i = 0; i < length; i += 1) {
|
|
99
|
+
const index = startIndex + i;
|
|
100
|
+
if (index < 0 || index >= N) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const t = i / SR;
|
|
104
|
+
const env = envelope(t, duration, 0.85, 1.35);
|
|
105
|
+
const breath = 0.84 + 0.16 * Math.sin(lfoPhase + t * 0.62);
|
|
106
|
+
phase += (TAU * voiceFrequency) / SR;
|
|
107
|
+
if (phase > TAU) {
|
|
108
|
+
phase -= TAU;
|
|
109
|
+
}
|
|
110
|
+
let sample = 0;
|
|
111
|
+
for (let harmonic = 1; harmonic <= harmonics; harmonic += 1) {
|
|
112
|
+
sample += Math.sin(phase * harmonic) / Math.pow(harmonic, 1.72);
|
|
113
|
+
}
|
|
114
|
+
sample *= gain * env * breath;
|
|
115
|
+
padL[index] += sample * gainL * voiceL;
|
|
116
|
+
padR[index] += sample * gainR * voiceR;
|
|
117
|
+
revSend[index] += sample * 0.05;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function addSub(start, duration, midi, gain) {
|
|
123
|
+
const frequency = hz(midi);
|
|
124
|
+
const length = Math.round(duration * SR);
|
|
125
|
+
const startIndex = Math.round(start * SR);
|
|
126
|
+
let phase = 0;
|
|
127
|
+
for (let i = 0; i < length; i += 1) {
|
|
128
|
+
const index = startIndex + i;
|
|
129
|
+
if (index < 0 || index >= N) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const t = i / SR;
|
|
133
|
+
const env = envelope(t, duration, 0.9, 1.6);
|
|
134
|
+
phase += (TAU * frequency) / SR;
|
|
135
|
+
if (phase > TAU) {
|
|
136
|
+
phase -= TAU;
|
|
137
|
+
}
|
|
138
|
+
const sample = (Math.sin(phase) + 0.18 * Math.sin(phase * 2)) * env * gain;
|
|
139
|
+
bassL[index] += sample;
|
|
140
|
+
bassR[index] += sample;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function addBassNote(start, duration, midi, gain) {
|
|
145
|
+
const frequency = hz(midi);
|
|
146
|
+
const length = Math.round(duration * SR);
|
|
147
|
+
const startIndex = Math.round(start * SR);
|
|
148
|
+
let phase = random() * TAU;
|
|
149
|
+
for (let i = 0; i < length; i += 1) {
|
|
150
|
+
const index = startIndex + i;
|
|
151
|
+
if (index < 0 || index >= N) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
const t = i / SR;
|
|
155
|
+
const env = envelope(t, duration, 0.008, 0.06) * Math.exp(-t * 3.2);
|
|
156
|
+
phase += (TAU * frequency) / SR;
|
|
157
|
+
if (phase > TAU) {
|
|
158
|
+
phase -= TAU;
|
|
159
|
+
}
|
|
160
|
+
const sample = (Math.sin(phase) + 0.42 * Math.sin(phase * 2) + 0.16 * Math.sin(phase * 3)) * env * gain;
|
|
161
|
+
bassL[index] += sample;
|
|
162
|
+
bassR[index] += sample;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addKick(start, gain, decay = 0.42, startPitch = 112, endPitch = 42) {
|
|
167
|
+
const length = Math.round((decay * 5.5 + 0.08) * SR);
|
|
168
|
+
const startIndex = Math.round(start * SR);
|
|
169
|
+
let phase = 0;
|
|
170
|
+
for (let i = 0; i < length; i += 1) {
|
|
171
|
+
const index = startIndex + i;
|
|
172
|
+
if (index < 0 || index >= N) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const t = i / SR;
|
|
176
|
+
const frequency = endPitch + (startPitch - endPitch) * Math.exp(-t * 17);
|
|
177
|
+
phase += (TAU * frequency) / SR;
|
|
178
|
+
if (phase > TAU) {
|
|
179
|
+
phase -= TAU;
|
|
180
|
+
}
|
|
181
|
+
const body = Math.sin(phase) * Math.exp(-t / decay) * (1 - Math.exp(-t * 320));
|
|
182
|
+
const click = noise() * Math.exp(-t * 95) * 0.08;
|
|
183
|
+
const sample = (body + click) * gain;
|
|
184
|
+
percL[index] += sample;
|
|
185
|
+
percR[index] += sample;
|
|
186
|
+
revSend[index] += sample * 0.04;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const duckStart = Math.round(start * SR);
|
|
190
|
+
for (let i = 0; i < Math.round(0.42 * SR); i += 1) {
|
|
191
|
+
const index = duckStart + i;
|
|
192
|
+
if (index < 0 || index >= N) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
const t = i / SR;
|
|
196
|
+
duck[index] = Math.max(0.4, duck[index] * (1 - 0.24 * Math.exp(-t * 10) * (1 - Math.exp(-t * 110))));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function addHat(start, gain, decay = 0.035, pan = 0.25, tone = 0.8) {
|
|
201
|
+
const length = Math.round((decay * 6 + 0.01) * SR);
|
|
202
|
+
const startIndex = Math.round(start * SR);
|
|
203
|
+
const [gainL, gainR] = panGains(pan);
|
|
204
|
+
let previousInput = 0;
|
|
205
|
+
let previousOutput = 0;
|
|
206
|
+
const coefficient = 0.58;
|
|
207
|
+
for (let i = 0; i < length; i += 1) {
|
|
208
|
+
const index = startIndex + i;
|
|
209
|
+
if (index < 0 || index >= N) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
const t = i / SR;
|
|
213
|
+
const input = noise();
|
|
214
|
+
const highpassed = coefficient * (previousOutput + input - previousInput);
|
|
215
|
+
previousInput = input;
|
|
216
|
+
previousOutput = highpassed;
|
|
217
|
+
const metallic = Math.sin(TAU * 9100 * t) * 0.18 * tone;
|
|
218
|
+
const sample = (highpassed + metallic) * Math.exp(-t / decay) * gain;
|
|
219
|
+
percL[index] += sample * gainL;
|
|
220
|
+
percR[index] += sample * gainR;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function addClap(start, gain, pan = 0) {
|
|
225
|
+
const [gainL, gainR] = panGains(pan);
|
|
226
|
+
const bursts = [0, 0.012, 0.026];
|
|
227
|
+
for (const offset of bursts) {
|
|
228
|
+
const startIndex = Math.round((start + offset) * SR);
|
|
229
|
+
const length = Math.round(0.22 * SR);
|
|
230
|
+
let low = 0;
|
|
231
|
+
let band = 0;
|
|
232
|
+
for (let i = 0; i < length; i += 1) {
|
|
233
|
+
const index = startIndex + i;
|
|
234
|
+
if (index < 0 || index >= N) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
const t = i / SR;
|
|
238
|
+
const f = 2 * Math.sin((Math.PI * 1650) / SR);
|
|
239
|
+
low += f * band;
|
|
240
|
+
const high = noise() - low - 1.1 * band;
|
|
241
|
+
band += f * high;
|
|
242
|
+
const sample = band * Math.exp(-t * 34) * gain;
|
|
243
|
+
percL[index] += sample * gainL;
|
|
244
|
+
percR[index] += sample * gainR;
|
|
245
|
+
revSend[index] += sample * 0.12;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function addPluck(start, duration, midi, gain, pan) {
|
|
251
|
+
const frequency = hz(midi);
|
|
252
|
+
const length = Math.round(duration * SR);
|
|
253
|
+
const startIndex = Math.round(start * SR);
|
|
254
|
+
const [gainL, gainR] = panGains(pan);
|
|
255
|
+
let phase = random() * TAU;
|
|
256
|
+
for (let i = 0; i < length; i += 1) {
|
|
257
|
+
const index = startIndex + i;
|
|
258
|
+
if (index < 0 || index >= N) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
const t = i / SR;
|
|
262
|
+
const env = (1 - Math.exp(-t * 260)) * Math.exp(-t * 9.5);
|
|
263
|
+
phase += (TAU * frequency) / SR;
|
|
264
|
+
if (phase > TAU) {
|
|
265
|
+
phase -= TAU;
|
|
266
|
+
}
|
|
267
|
+
const sample = (Math.sin(phase) + 0.36 * Math.sin(phase * 2) + 0.13 * Math.sin(phase * 3)) * env * gain;
|
|
268
|
+
arpL[index] += sample * gainL;
|
|
269
|
+
arpR[index] += sample * gainR;
|
|
270
|
+
delaySend[index] += sample * 0.8;
|
|
271
|
+
revSend[index] += sample * 0.16;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function addRiser(start, duration, gain) {
|
|
276
|
+
const length = Math.round(duration * SR);
|
|
277
|
+
const startIndex = Math.round(start * SR);
|
|
278
|
+
let low = 0;
|
|
279
|
+
let band = 0;
|
|
280
|
+
let sweepPhase = 0;
|
|
281
|
+
for (let i = 0; i < length; i += 1) {
|
|
282
|
+
const index = startIndex + i;
|
|
283
|
+
if (index < 0 || index >= N) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
const t = i / SR;
|
|
287
|
+
const progress = t / duration;
|
|
288
|
+
const cutoff = 240 * Math.pow(28, progress);
|
|
289
|
+
const f = Math.min(1.6, 2 * Math.sin((Math.PI * cutoff) / SR));
|
|
290
|
+
low += f * band;
|
|
291
|
+
const high = noise() - low - 1.35 * band;
|
|
292
|
+
band += f * high;
|
|
293
|
+
const sweepFrequency = 180 * Math.pow(7, progress);
|
|
294
|
+
sweepPhase += (TAU * sweepFrequency) / SR;
|
|
295
|
+
if (sweepPhase > TAU) {
|
|
296
|
+
sweepPhase -= TAU;
|
|
297
|
+
}
|
|
298
|
+
const env = Math.pow(progress, 2.2) * Math.min(1, (duration - t) / 0.035);
|
|
299
|
+
const sample = (band * 0.8 + Math.sin(sweepPhase) * 0.18) * env * gain;
|
|
300
|
+
const pan = -0.55 + progress * 1.1;
|
|
301
|
+
const [gainL, gainR] = panGains(pan);
|
|
302
|
+
fxL[index] += sample * gainL;
|
|
303
|
+
fxR[index] += sample * gainR;
|
|
304
|
+
revSend[index] += sample * 0.3;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function addImpact(start, gain) {
|
|
309
|
+
addKick(start, gain, 1.15, 86, 28);
|
|
310
|
+
const length = Math.round(1.4 * SR);
|
|
311
|
+
const startIndex = Math.round(start * SR);
|
|
312
|
+
let phase = 0;
|
|
313
|
+
for (let i = 0; i < length; i += 1) {
|
|
314
|
+
const index = startIndex + i;
|
|
315
|
+
if (index < 0 || index >= N) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
const t = i / SR;
|
|
319
|
+
const env = Math.exp(-t * 2.6) * (1 - Math.exp(-t * 180));
|
|
320
|
+
phase += (TAU * 58) / SR;
|
|
321
|
+
if (phase > TAU) {
|
|
322
|
+
phase -= TAU;
|
|
323
|
+
}
|
|
324
|
+
const ring = Math.sin(phase) * 0.5 + Math.sin(phase * 3) * 0.08;
|
|
325
|
+
const sample = ring * env * gain * 0.4;
|
|
326
|
+
fxL[index] += sample;
|
|
327
|
+
fxR[index] += sample;
|
|
328
|
+
revSend[index] += sample * 0.5;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const noiseLength = Math.round(0.5 * SR);
|
|
332
|
+
for (let i = 0; i < noiseLength; i += 1) {
|
|
333
|
+
const index = startIndex + i;
|
|
334
|
+
if (index < 0 || index >= N) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
const t = i / SR;
|
|
338
|
+
const sample = noise() * Math.exp(-t * 16) * gain * 0.16;
|
|
339
|
+
fxL[index] += sample;
|
|
340
|
+
fxR[index] += sample;
|
|
341
|
+
revSend[index] += sample * 0.6;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function addAirNote(start, duration, midi, gain, pan) {
|
|
346
|
+
const frequency = hz(midi);
|
|
347
|
+
const length = Math.round(duration * SR);
|
|
348
|
+
const startIndex = Math.round(start * SR);
|
|
349
|
+
const [gainL, gainR] = panGains(pan);
|
|
350
|
+
let phase = random() * TAU;
|
|
351
|
+
let phase2 = random() * TAU;
|
|
352
|
+
for (let i = 0; i < length; i += 1) {
|
|
353
|
+
const index = startIndex + i;
|
|
354
|
+
if (index < 0 || index >= N) {
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
const t = i / SR;
|
|
358
|
+
const env = envelope(t, duration, 1.15, 3.4);
|
|
359
|
+
phase += (TAU * frequency) / SR;
|
|
360
|
+
phase2 += (TAU * frequency * 1.004) / SR;
|
|
361
|
+
if (phase > TAU) phase -= TAU;
|
|
362
|
+
if (phase2 > TAU) phase2 -= TAU;
|
|
363
|
+
const sample = (Math.sin(phase) * 0.72 + Math.sin(phase2) * 0.28) * env * gain;
|
|
364
|
+
arpL[index] += sample * gainL;
|
|
365
|
+
arpR[index] += sample * gainR;
|
|
366
|
+
revSend[index] += sample * 0.4;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Arrangement
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
const chords = [
|
|
375
|
+
{ start: 0, end: 5.0, notes: [38, 45, 50, 53, 57], root: 26, gain: 0.009 },
|
|
376
|
+
{ start: 4.2, end: 9.8, notes: [34, 41, 46, 50, 53], root: 22, gain: 0.01 },
|
|
377
|
+
{ start: 9.2, end: 14.8, notes: [29, 36, 41, 45, 48], root: 17, gain: 0.011 },
|
|
378
|
+
{ start: 14.2, end: 20.5, notes: [38, 45, 50, 54, 57, 62], root: 26, gain: 0.011 },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
for (const chord of chords) {
|
|
382
|
+
const duration = chord.end - chord.start + 0.9;
|
|
383
|
+
chord.notes.forEach((note, index) => {
|
|
384
|
+
const pan = chord.notes.length === 1 ? 0 : (index / (chord.notes.length - 1) - 0.5) * 0.62;
|
|
385
|
+
addPadNote(chord.start, duration, note, chord.gain, pan, 1.2);
|
|
386
|
+
});
|
|
387
|
+
addSub(chord.start, chord.end - chord.start + 0.35, chord.root, 0.025);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Heartbeat and groove.
|
|
391
|
+
addImpact(0.02, 0.4);
|
|
392
|
+
for (const time of [1.0, 2.0, 3.0, 4.0]) {
|
|
393
|
+
addKick(time, 0.24, 0.28, 92, 46);
|
|
394
|
+
}
|
|
395
|
+
for (const time of [4.5, 5.5, 6.5, 7.5, 8.5]) {
|
|
396
|
+
addKick(time, 0.55, 0.34, 108, 44);
|
|
397
|
+
}
|
|
398
|
+
for (let time = 9.5; time < 14.5; time += 0.5) {
|
|
399
|
+
addKick(time, time === 9.5 || time === 13.5 ? 0.72 : 0.56, 0.34, 108, 44);
|
|
400
|
+
}
|
|
401
|
+
addImpact(4.5, 0.5);
|
|
402
|
+
addImpact(9.5, 0.68);
|
|
403
|
+
addImpact(14.5, 0.95);
|
|
404
|
+
addImpact(15.15, 0.32);
|
|
405
|
+
|
|
406
|
+
// Hats: sparse before the lift, driving in the middle.
|
|
407
|
+
for (let time = 4.5, index = 0; time < 14.5; time += 0.25, index += 1) {
|
|
408
|
+
const accent = index % 4 === 2 ? 0.32 : 0.2;
|
|
409
|
+
addHat(time, accent, index % 4 === 2 ? 0.07 : 0.035, index % 2 === 0 ? -0.28 : 0.3, 0.9);
|
|
410
|
+
}
|
|
411
|
+
for (let time = 9.5; time < 14.5; time += 0.5) {
|
|
412
|
+
addClap(time + 0.25, 0.3, 0);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Bass pulses follow the chord roots. The offbeats are softer.
|
|
416
|
+
const bassPattern = [
|
|
417
|
+
{ at: 0, length: 0.24, gain: 0.06 },
|
|
418
|
+
{ at: 0.25, length: 0.2, gain: 0.038 },
|
|
419
|
+
{ at: 0.375, length: 0.12, gain: 0.024 },
|
|
420
|
+
];
|
|
421
|
+
for (let time = 4.5; time < 14.5; time += 0.5) {
|
|
422
|
+
const chord = chords.find((entry) => time >= entry.start && time < entry.end) ?? chords[chords.length - 1];
|
|
423
|
+
for (const pulse of bassPattern) {
|
|
424
|
+
const at = time + pulse.at;
|
|
425
|
+
if (at < 14.5) {
|
|
426
|
+
addBassNote(at, pulse.length, chord.root + 12, pulse.gain);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Plucked arpeggio. The pattern changes register at the lift.
|
|
432
|
+
const arpPattern = [0, 2, 4, 3, 2, 4, 1, 3];
|
|
433
|
+
for (let time = 4.5, index = 0; time < 14.5; time += 0.25, index += 1) {
|
|
434
|
+
const chord = chords.find((entry) => time >= entry.start && time < entry.end) ?? chords[chords.length - 1];
|
|
435
|
+
const note = chord.notes[arpPattern[index % arpPattern.length] % chord.notes.length] + 12;
|
|
436
|
+
const lift = time >= 9.5 ? 1 : 0;
|
|
437
|
+
addPluck(time, 0.62, note + lift * 12, time >= 9.5 ? 0.15 : 0.13, index % 2 === 0 ? -0.34 : 0.34);
|
|
438
|
+
if (time >= 9.5 && index % 4 === 3) {
|
|
439
|
+
addPluck(time + 0.125, 0.3, note + 24, 0.05, 0.1);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Risers and transition air.
|
|
444
|
+
addRiser(2.4, 2.0, 0.16);
|
|
445
|
+
addRiser(7.1, 2.3, 0.2);
|
|
446
|
+
addRiser(12.0, 2.4, 0.28);
|
|
447
|
+
|
|
448
|
+
// Resolution: high air chord, sparkles, and a low final swell.
|
|
449
|
+
const airNotes = [74, 78, 81, 86, 90];
|
|
450
|
+
airNotes.forEach((note, index) => {
|
|
451
|
+
addAirNote(14.55, 5.1, note, 0.026, (index / (airNotes.length - 1) - 0.5) * 1.2);
|
|
452
|
+
});
|
|
453
|
+
const sparkleNotes = [98, 93, 90, 98, 95, 102];
|
|
454
|
+
const sparkleTimes = [15.4, 16.1, 16.9, 17.7, 18.4, 19.0];
|
|
455
|
+
sparkleTimes.forEach((time, index) => {
|
|
456
|
+
addPluck(time, 1.3, sparkleNotes[index], 0.05, index % 2 === 0 ? -0.66 : 0.66);
|
|
457
|
+
revSend[Math.round(time * SR)] += 0.02;
|
|
458
|
+
});
|
|
459
|
+
addSub(17.6, 2.5, 26, 0.055);
|
|
460
|
+
addAirNote(17.9, 2.4, 62, 0.022, 0);
|
|
461
|
+
addAirNote(17.9, 2.4, 69, 0.018, -0.4);
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Filters and mixing
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
function lowpassBus(input, cutoffAt, q = 1.0) {
|
|
468
|
+
const output = new Float64Array(N);
|
|
469
|
+
let low = 0;
|
|
470
|
+
let band = 0;
|
|
471
|
+
for (let i = 0; i < N; i += 1) {
|
|
472
|
+
const cutoff = Math.max(40, Math.min(0.42 * SR, cutoffAt(i / SR)));
|
|
473
|
+
const f = 2 * Math.sin((Math.PI * cutoff) / SR);
|
|
474
|
+
low += f * band;
|
|
475
|
+
const high = input[i] - low - q * band;
|
|
476
|
+
band += f * high;
|
|
477
|
+
output[i] = low;
|
|
478
|
+
}
|
|
479
|
+
return output;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function highpassBus(input, cutoff) {
|
|
483
|
+
const output = new Float64Array(N);
|
|
484
|
+
const rc = 1 / (TAU * cutoff);
|
|
485
|
+
const alpha = rc / (rc + 1 / SR);
|
|
486
|
+
let previousInput = 0;
|
|
487
|
+
let previousOutput = 0;
|
|
488
|
+
for (let i = 0; i < N; i += 1) {
|
|
489
|
+
const value = input[i];
|
|
490
|
+
previousOutput = alpha * (previousOutput + value - previousInput);
|
|
491
|
+
previousInput = value;
|
|
492
|
+
output[i] = previousOutput;
|
|
493
|
+
}
|
|
494
|
+
return output;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function padCutoff(t) {
|
|
498
|
+
if (t < 4.5) {
|
|
499
|
+
return 420 + (t / 4.5) * 380;
|
|
500
|
+
}
|
|
501
|
+
if (t < 9.5) {
|
|
502
|
+
return 800 + ((t - 4.5) / 5) * 1000;
|
|
503
|
+
}
|
|
504
|
+
if (t < 14.5) {
|
|
505
|
+
return 1800 + ((t - 9.5) / 5) * 3000;
|
|
506
|
+
}
|
|
507
|
+
return 4800 * Math.pow(0.22, Math.min(1, (t - 14.5) / 5.2));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function bassCutoff(t) {
|
|
511
|
+
return 180 + 60 * Math.sin(Math.min(1, t / 20) * Math.PI);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const padFilteredL = highpassBus(lowpassBus(padL, padCutoff, 0.9), 75);
|
|
515
|
+
const padFilteredR = highpassBus(lowpassBus(padR, padCutoff, 0.9), 75);
|
|
516
|
+
const bassFilteredL = lowpassBus(bassL, bassCutoff, 0.7);
|
|
517
|
+
const bassFilteredR = lowpassBus(bassR, bassCutoff, 0.7);
|
|
518
|
+
|
|
519
|
+
// Simple stereo Schroeder reverb. Four damped combs feed two allpasses per
|
|
520
|
+
// channel; different delay lengths keep the tail wide without a convolver.
|
|
521
|
+
function reverb(input) {
|
|
522
|
+
const left = new Float64Array(N);
|
|
523
|
+
const right = new Float64Array(N);
|
|
524
|
+
|
|
525
|
+
function channel(target, lengths, spread) {
|
|
526
|
+
const combs = lengths.map((seconds, index) => ({
|
|
527
|
+
buffer: new Float64Array(Math.max(1, Math.round(seconds * SR))),
|
|
528
|
+
index: 0,
|
|
529
|
+
damp: 0,
|
|
530
|
+
feedback: 0.76 - index * 0.025,
|
|
531
|
+
}));
|
|
532
|
+
const allpasses = [0.0051 * spread, 0.0017 * spread].map((seconds) => ({
|
|
533
|
+
buffer: new Float64Array(Math.max(1, Math.round(seconds * SR))),
|
|
534
|
+
index: 0,
|
|
535
|
+
}));
|
|
536
|
+
const combMix = new Float64Array(N);
|
|
537
|
+
for (let i = 0; i < N; i += 1) {
|
|
538
|
+
let sum = 0;
|
|
539
|
+
for (const comb of combs) {
|
|
540
|
+
const delayed = comb.buffer[comb.index];
|
|
541
|
+
comb.damp = delayed * 0.62 + comb.damp * 0.38;
|
|
542
|
+
comb.buffer[comb.index] = input[i] + comb.damp * comb.feedback;
|
|
543
|
+
comb.index = (comb.index + 1) % comb.buffer.length;
|
|
544
|
+
sum += delayed;
|
|
545
|
+
}
|
|
546
|
+
combMix[i] = sum / combs.length;
|
|
547
|
+
}
|
|
548
|
+
let signal = combMix;
|
|
549
|
+
for (const allpass of allpasses) {
|
|
550
|
+
const next = new Float64Array(N);
|
|
551
|
+
for (let i = 0; i < N; i += 1) {
|
|
552
|
+
const delayed = allpass.buffer[allpass.index];
|
|
553
|
+
next[i] = -signal[i] + delayed;
|
|
554
|
+
allpass.buffer[allpass.index] = signal[i] + delayed * 0.5;
|
|
555
|
+
allpass.index = (allpass.index + 1) % allpass.buffer.length;
|
|
556
|
+
}
|
|
557
|
+
signal = next;
|
|
558
|
+
}
|
|
559
|
+
target.set(signal);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
channel(left, [0.0297, 0.0371, 0.0411, 0.0437], 1);
|
|
563
|
+
channel(right, [0.0311, 0.0389, 0.0427, 0.0451], 1.03);
|
|
564
|
+
return { left, right };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Ping-pong delay for the arpeggio: dotted eighth, filtered feedback.
|
|
568
|
+
function pingPong(input, delaySeconds, feedback = 0.34) {
|
|
569
|
+
const left = new Float64Array(N);
|
|
570
|
+
const right = new Float64Array(N);
|
|
571
|
+
const delay = Math.max(1, Math.round(delaySeconds * SR));
|
|
572
|
+
const bufferLeft = new Float64Array(N);
|
|
573
|
+
const bufferRight = new Float64Array(N);
|
|
574
|
+
for (let i = 0; i < N; i += 1) {
|
|
575
|
+
const readLeft = i >= delay ? bufferRight[i - delay] : 0;
|
|
576
|
+
const readRight = i >= delay ? bufferLeft[i - delay] : 0;
|
|
577
|
+
bufferLeft[i] = input[i] + readLeft * feedback;
|
|
578
|
+
bufferRight[i] = readRight * feedback;
|
|
579
|
+
left[i] = i >= delay ? bufferLeft[i - delay] * 0.9 : 0;
|
|
580
|
+
right[i] = i >= delay ? bufferRight[i - delay] * 0.9 : 0;
|
|
581
|
+
}
|
|
582
|
+
return { left, right };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const reverbInput = new Float64Array(N);
|
|
586
|
+
for (let i = 0; i < N; i += 1) {
|
|
587
|
+
reverbInput[i] = revSend[i] + (fxL[i] + fxR[i]) * 0.22;
|
|
588
|
+
}
|
|
589
|
+
const wet = reverb(reverbInput);
|
|
590
|
+
const delay = pingPong(delaySend, BEAT * 0.75, 0.36);
|
|
591
|
+
|
|
592
|
+
const mixL = new Float64Array(OUT);
|
|
593
|
+
const mixR = new Float64Array(OUT);
|
|
594
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
595
|
+
const padGain = duck[i];
|
|
596
|
+
const bassGain = duck[i];
|
|
597
|
+
const dryL = padFilteredL[i] * padGain + bassFilteredL[i] * bassGain + arpL[i] + percL[i] + fxL[i];
|
|
598
|
+
const dryR = padFilteredR[i] * padGain + bassFilteredR[i] * bassGain + arpR[i] + percR[i] + fxR[i];
|
|
599
|
+
mixL[i] = dryL + wet.left[i] * 0.72 + delay.left[i] * 0.24;
|
|
600
|
+
mixR[i] = dryR + wet.right[i] * 0.72 + delay.right[i] * 0.24;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Master: DC/rumble removal, gentle saturation, normalization, fades.
|
|
604
|
+
function highpass(input, cutoff) {
|
|
605
|
+
const rc = 1 / (TAU * cutoff);
|
|
606
|
+
const alpha = rc / (rc + 1 / SR);
|
|
607
|
+
let previousInput = 0;
|
|
608
|
+
let previousOutput = 0;
|
|
609
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
610
|
+
const value = input[i];
|
|
611
|
+
previousOutput = alpha * (previousOutput + value - previousInput);
|
|
612
|
+
previousInput = value;
|
|
613
|
+
input[i] = previousOutput;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
highpass(mixL, 24);
|
|
618
|
+
highpass(mixR, 24);
|
|
619
|
+
|
|
620
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
621
|
+
mixL[i] = Math.tanh(mixL[i] * 1.08);
|
|
622
|
+
mixR[i] = Math.tanh(mixR[i] * 1.08);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let peak = 0;
|
|
626
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
627
|
+
peak = Math.max(peak, Math.abs(mixL[i]), Math.abs(mixR[i]));
|
|
628
|
+
}
|
|
629
|
+
const normalize = peak > 0 ? 0.72 / peak : 1;
|
|
630
|
+
const fadeIn = Math.round(0.04 * SR);
|
|
631
|
+
const fadeOutStart = Math.round(19.25 * SR);
|
|
632
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
633
|
+
const fade = smoothstep(i / fadeIn) * smoothstep((OUT - i) / (OUT - fadeOutStart));
|
|
634
|
+
mixL[i] *= normalize * fade;
|
|
635
|
+
mixR[i] *= normalize * fade;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 16-bit PCM WAV with TPDF dither.
|
|
639
|
+
const bytes = 44 + OUT * 4;
|
|
640
|
+
const wav = Buffer.alloc(bytes);
|
|
641
|
+
wav.write("RIFF", 0, "ascii");
|
|
642
|
+
wav.writeUInt32LE(36 + OUT * 4, 4);
|
|
643
|
+
wav.write("WAVE", 8, "ascii");
|
|
644
|
+
wav.write("fmt ", 12, "ascii");
|
|
645
|
+
wav.writeUInt32LE(16, 16);
|
|
646
|
+
wav.writeUInt16LE(1, 20);
|
|
647
|
+
wav.writeUInt16LE(2, 22);
|
|
648
|
+
wav.writeUInt32LE(SR, 24);
|
|
649
|
+
wav.writeUInt32LE(SR * 4, 28);
|
|
650
|
+
wav.writeUInt16LE(4, 32);
|
|
651
|
+
wav.writeUInt16LE(16, 34);
|
|
652
|
+
wav.write("data", 36, "ascii");
|
|
653
|
+
wav.writeUInt32LE(OUT * 4, 40);
|
|
654
|
+
|
|
655
|
+
let offset = 44;
|
|
656
|
+
for (let i = 0; i < OUT; i += 1) {
|
|
657
|
+
for (const value of [mixL[i], mixR[i]]) {
|
|
658
|
+
const dither = (random() - random()) / 65536;
|
|
659
|
+
const sample = Math.max(-1, Math.min(1, value + dither));
|
|
660
|
+
wav.writeInt16LE(Math.round(sample * 32767), offset);
|
|
661
|
+
offset += 2;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
mkdirSync(path.dirname(out), { recursive: true });
|
|
666
|
+
if (out.endsWith(".wav")) {
|
|
667
|
+
writeFileSync(out, wav);
|
|
668
|
+
} else {
|
|
669
|
+
const work = mkdtempSync(path.join(tmpdir(), "frameyard-score-"));
|
|
670
|
+
const master = path.join(work, "score.wav");
|
|
671
|
+
try {
|
|
672
|
+
writeFileSync(master, wav);
|
|
673
|
+
execFileSync(
|
|
674
|
+
"ffmpeg",
|
|
675
|
+
[
|
|
676
|
+
"-y",
|
|
677
|
+
"-v",
|
|
678
|
+
"error",
|
|
679
|
+
"-fflags",
|
|
680
|
+
"+bitexact",
|
|
681
|
+
"-i",
|
|
682
|
+
master,
|
|
683
|
+
"-map_metadata",
|
|
684
|
+
"-1",
|
|
685
|
+
"-c:a",
|
|
686
|
+
"libvorbis",
|
|
687
|
+
"-q:a",
|
|
688
|
+
"6",
|
|
689
|
+
"-fflags",
|
|
690
|
+
"+bitexact",
|
|
691
|
+
out,
|
|
692
|
+
],
|
|
693
|
+
{ stdio: "inherit" },
|
|
694
|
+
);
|
|
695
|
+
} finally {
|
|
696
|
+
rmSync(work, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const size = statSync(out).size;
|
|
700
|
+
console.log(
|
|
701
|
+
`wrote ${path.relative(process.cwd(), out)} (${(size / 1024).toFixed(0)} KiB, peak ${(20 * Math.log10(0.72)).toFixed(1)} dBFS)`,
|
|
702
|
+
);
|