Docs/Skills and code/skill video-visual-production
.claude/skills/video-visual-production/scripts/sfx.js
What the file says about itself
V7 functional sounds, built in code from the recipes in research/video-production/03-sound.md section 4. Library: sounds(name, opts) -> Float32Array at 48 kHz, peak 1. CLI: audition reel of every sound at its default level.
#!/usr/bin/env node
// V7 functional sounds, built in code from the recipes in research/video-production/03-sound.md section 4.
// Library: sounds(name, opts) -> Float32Array at 48 kHz, peak 1. CLI: audition reel of every sound at its default level.
// node sfx.js --reel <out.wav>
const path = require('path');
const D = require('./lib/dsp');
const { SR } = D;
// Peak level relative to the voice peak (dB). From the sound plan; the user auditions and changes these.
const LEVEL_DB = { tick: -14, thud: -9, stamp: -9, whoosh: -16, swipe: -18, paper: -20, pivot: -14, clink: -14, clock: -30 };
const N = (sec) => Math.round(sec * SR);
function tick(step = 0) {
const f = 1500 * Math.pow(2, step / 12), n = N(0.045), o = new Float32Array(n);
for (let i = 0; i < n; i++) { const t = i / SR; o[i] = Math.sin(2 * Math.PI * f * t) * Math.min(1, t / 0.002) * Math.exp(-t / 0.006); }
o[0] += 0.8; // a one-sample click
return D.normalizePeak(o);
}
function thud() {
const n = N(0.4), o = new Float32Array(n), r = D.rng(11), lp = D.onePoleLP(500);
let ph = 0;
for (let i = 0; i < n; i++) {
const t = i / SR, f = 45 + 25 * Math.exp(-t / 0.05);
ph += (2 * Math.PI * f) / SR;
o[i] = Math.sin(ph) * Math.exp(-t / 0.09) + lp(r() * 2 - 1) * Math.exp(-t / 0.004) * 0.7;
}
return D.normalizePeak(o);
}
function stamp() {
const base = thud(), n = N(0.25), o = new Float32Array(n), r = D.rng(23), bp = D.bandpass(0.8);
for (let i = 0; i < n; i++) {
const t = i / SR;
o[i] = (i < base.length ? base[i] * Math.exp(-t / 0.07) : 0) + bp(r() * 2 - 1, 1200) * Math.exp(-t / 0.02) * 0.9;
}
return D.normalizePeak(o);
}
function sweepNoise(dur, f0, f1, Q, seed) {
const n = N(dur), o = new Float32Array(n), r = D.rng(seed), bp = D.bandpass(Q);
for (let i = 0; i < n; i++) {
const k = i / n, f = f0 * Math.pow(f1 / f0, k), e = Math.pow(Math.sin(Math.PI * k), 2);
o[i] = bp(r() * 2 - 1, f) * e;
}
return D.normalizePeak(o);
}
const whoosh = () => sweepNoise(0.36, 300, 3000, 1.2, 31);
const paper = () => sweepNoise(0.2, 500, 1800, 0.8, 37);
function swipe() { // marker: band-passed noise, short rise, quick fall
const n = N(0.22), o = new Float32Array(n), r = D.rng(41), bp = D.bandpass(1.5);
for (let i = 0; i < n; i++) { const t = i / SR; o[i] = bp(r() * 2 - 1, 3200) * Math.min(1, t / 0.02) * Math.exp(-t / 0.07); }
return D.normalizePeak(o);
}
function pivot() { // two notes a fourth apart, rising
const n = N(0.6), o = new Float32Array(n);
[[392, 0], [523.25, 0.12]].forEach(([f, d]) => {
for (let i = N(d); i < n; i++) { const t = (i - N(d)) / SR; o[i] += (Math.sin(2 * Math.PI * f * t) + 0.3 * Math.sin(4 * Math.PI * f * t)) * Math.min(1, t / 0.005) * Math.exp(-t / 0.13); }
});
return D.normalizePeak(o);
}
function clink() { // glass ping: three inharmonic partials
const n = N(0.7), o = new Float32Array(n);
[[3100, 1, 0.35], [4650, 0.6, 0.25], [6200, 0.35, 0.18]].forEach(([f, w, d]) => {
for (let i = 0; i < n; i++) { const t = i / SR; o[i] += w * Math.sin(2 * Math.PI * f * t) * Math.min(1, t / 0.001) * Math.exp(-t / d); }
});
return D.normalizePeak(o);
}
function clockTick(k = 0) { // 1 Hz tick, alternating pitch, very soft
const f = k % 2 ? 900 : 1200, n = N(0.05), o = new Float32Array(n), lp = D.onePoleLP(3500);
for (let i = 0; i < n; i++) { const t = i / SR; o[i] = lp(Math.sin(2 * Math.PI * f * t)) * Math.min(1, t / 0.001) * Math.exp(-t / 0.006); }
return D.normalizePeak(o);
}
function make(name, e = {}) {
switch (name) {
case 'tick': return tick(e.step || 0);
case 'thud': return thud();
case 'stamp': return stamp();
case 'whoosh': return whoosh();
case 'swipe': return swipe();
case 'paper': return paper();
case 'pivot': return pivot();
case 'clink': return clink();
case 'clock': return clockTick(e.k || 0);
default: throw new Error('unknown sound ' + name);
}
}
// Place every event of a timeline into a track of `samples` samples. Returns the track and a table of onsets.
function placeEvents(events, samples, voicePeak, levels = {}) {
const track = new Float32Array(samples);
const table = [];
for (const e of events) {
const name = e.sound === 'clock' ? 'clock' : e.sound;
const snd = make(name, { step: e.step, k: e.k || 0 });
const gain = voicePeak * D.db((levels[name] != null ? levels[name] : (LEVEL_DB[name] || -14)) + (e.level_db || 0));
const at = e.frame * (SR / 30);
for (let i = 0; i < snd.length && at + i < samples; i++) track[at + i] += snd[i] * gain;
table.push({ ...e, sample: at, time_s: at / SR, peak_dbfs: +(20 * Math.log10(gain)).toFixed(1) });
}
return { track, table };
}
module.exports = { make, placeEvents, LEVEL_DB };
if (require.main === module) {
const U = require('./lib/util');
const out = U.arg('reel', 'sfx-reel.wav');
const names = ['tick', 'thud', 'stamp', 'whoosh', 'swipe', 'paper', 'pivot', 'clink', 'clock'];
const gap = N(0.6);
const total = names.length * (N(0.8) + gap);
const reel = new Float32Array(total);
names.forEach((nm, i) => { const s = make(nm), g = D.db(LEVEL_DB[nm]) * 0.5, at = i * (N(0.8) + gap); for (let k = 0; k < s.length; k++) reel[at + k] += s[k] * g; });
D.writeWav(out, reel, SR, 16);
U.say('wrote ' + out + ' (order: ' + names.join(', ') + ')');
}
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.