// Reproduce typing clicks from the user's local reference video. // node scripts/extract-typing-audio.mjs path/to/ffmpeg.exe // These short excerpts retain the source recording's provenance, not the score's license. import fs from "node:fs"; import crypto from "node:crypto"; import { execFileSync } from "node:child_process"; const ffmpeg = process.argv[2], video = fs.readdirSync(".").find((x) => x.endsWith(".mp4")); if (!ffmpeg || !video) throw Error("A local reference MP4 and FFmpeg path are required"); const rate = 48000, start = 6.84, seconds = 0.55; const bytes = execFileSync( ffmpeg, [ "-v", "error", "-ss", String(start), "-i", video, "-t", String(seconds), "-vn", "-af", "pan=mono|c0=0.5*c0+0.5*c1", "-ar", String(rate), "-f", "f32le", "pipe:1", ], { maxBuffer: 1024 * 1024 }, ); const source = new Float32Array( bytes.buffer, bytes.byteOffset, bytes.length / 4, ); const cuts = [6.864, 6.906, 6.948], duration = 0.038; const samples = cuts.map((time) => source.slice( Math.round((time - start) * rate), Math.round((time - start + duration) * rate), ), ); let peak = 0; for (const sample of samples) { const dc = sample.reduce((sum, x) => sum + x, 0) / sample.length; for (let i = 0; i < sample.length; i++) { const fadeIn = Math.min(1, i / (rate * 0.00035)), fadeOut = Math.min(1, (sample.length - 1 - i) / (rate * 0.0035)); sample[i] = (sample[i] - dc) * fadeIn * fadeOut; peak = Math.max(peak, Math.abs(sample[i])); } } // Shared gain preserves the three original clicks' relative accents. const gain = 0.3 / peak; const pcm = samples.map((sample) => { const out = Buffer.alloc(sample.length * 2); for (let i = 0; i < sample.length; i++) out.writeInt16LE(Math.round(sample[i] * gain * 32767), i * 2); return out; }); fs.mkdirSync("public/audio", { recursive: true }); function wav(data, file) { const out = Buffer.alloc(44 + data.length); out.write("RIFF"); out.writeUInt32LE(out.length - 8, 4); out.write("WAVEfmt ", 8); out.writeUInt32LE(16, 16); out.writeUInt16LE(1, 20); out.writeUInt16LE(1, 22); out.writeUInt32LE(rate, 24); out.writeUInt32LE(rate * 2, 28); out.writeUInt16LE(2, 32); out.writeUInt16LE(16, 34); out.write("data", 36); out.writeUInt32LE(data.length, 40); data.copy(out, 44); fs.writeFileSync(file, out); } // Review-only source excerpt. Uniform gain makes its level comparable to the rebuilt sequence. const referencePcm = Buffer.alloc(source.length * 2); for (let i = 0; i < source.length; i++) referencePcm.writeInt16LE( Math.round(Math.max(-1, Math.min(1, source[i] * gain * 0.2)) * 32767), i * 2, ); wav(referencePcm, "reference/typing-original.wav"); const preview = Buffer.alloc(rate * 2 * 2); const pulseTimes = [ 0.02, 0.064, 0.108, 0.152, 0.2, 0.244, 0.288, 0.332, 0.38, 0.424, 0.468, 0.516, 1.02, 1.068, 1.116, 1.16, 1.208, 1.256, 1.304, 1.352, ]; pulseTimes.forEach((t, n) => { const click = pcm[n % pcm.length], offset = Math.round(t * rate); for (let i = 0; i < click.length / 2; i++) preview.writeInt16LE( Math.round(click.readInt16LE(i * 2) * 0.2), (offset + i) * 2, ); }); wav(preview, "public/audio/typing-preview.wav"); fs.writeFileSync( "src/typing-samples.ts", `// Generated by scripts/extract-typing-audio.mjs. Source provenance: public/audio/typing-source.json.\nexport const TYPING_SAMPLE_RATE = ${rate};\nexport const TYPING_PCM = ${JSON.stringify( pcm.map((x) => x.toString("base64")), null, 2, )} as const;\n`, ); const report = { source: video, sourceSha256: crypto .createHash("sha256") .update(fs.readFileSync(video)) .digest("hex"), sampleRate: rate, channels: 1, cuts: cuts.map((start) => ({ start, end: +(start + duration).toFixed(3) })), processing: { dcRemoval: true, fadeInMs: 0.35, fadeOutMs: 3.5, sharedGain: gain, denoising: false, pitchChange: false, }, scope: "Three short excerpts from user-provided reference. Not original synthesis; source rights remain with the original creators.", }; fs.writeFileSync( "public/audio/typing-source.json", JSON.stringify(report, null, 2) + "\n", ); console.log( JSON.stringify( { cuts: report.cuts, bytes: pcm.reduce((n, b) => n + b.length, 0), gain }, null, 2, ), );