synthi-probe

synthi-probe

Headless measurement for synthi/sos-dj.html. Built while fixing the oud, the guitar and the arp, because “sounds gutted”, “too loud” and “it clips” are all checkable claims, and tuning gain constants by ear through a browser is slow and gets the wrong answer.

engines.mjs lifts playMel() straight out of the page and renders it through an OfflineAudioContext, so it always measures the shipped code — there is no second copy of the synth to drift out of date. It keys off two markers, function driveCurve and function trig(; if those are renamed it says so.

bun add -d node-web-audio-api          # once

bun scripts/synthi-probe/engines.mjs levels
bun scripts/synthi-probe/engines.mjs spectrum --ref master
bun scripts/synthi-probe/engines.mjs arp --amp 0.45 --gate 1
bun scripts/synthi-probe/engines.mjs render
bun scripts/synthi-probe/styles.mjs

Reading the numbers

Seed every render. ksBuf’s excitation and every noiseBuf call are Math.random(). Unseeded, the same engine measures differently each run and band ratios swing by tens of percent — enough to “confirm” a change that did nothing. engines.mjs seeds a deterministic PRNG before each render.

Peak lies about a held voice. An organ that sustains flat and a pluck that decays can share a peak and sit 6 dB apart to the ear. levels reports the loudest 50 ms (onset) and the loudest 1.2 s (sustained energy) instead, and ranks against the median. That is what caught ORGAN sitting +5.5 dB above everything while its peak looked modest.

Band ratios confound level with balance. Summing raw FFT bins reports broadband noise as “94 % treble” purely because the 1.5–6 kHz band holds 450 bins against the low band’s 25. spectrum reports spectral centroid and the harmonic profile in dB relative to the fundamental instead. A 4th harmonic above the fundamental is the signature of a hollow, nasal voice — that is exactly what the old oud measured (-9 -6 +10 -1).

The arp plays a stream, not a note. arp renders 24 overlapping notes. ringAfterLast much larger than the gate means the engine is discarding the note length it was handed: GT does nothing for it and its notes pile up. That single column explained both “the oud is too long” and most of the clipping.

--wav <dir> writes each render out so you can listen instead of reading a table.

reframe.swift

Reframes a square clip to 9:16 (or any size) and remuxes it properly, using AVFoundation — no ffmpeg install.

swiftc -O scripts/synthi-probe/reframe.swift -o /tmp/reframe
/tmp/reframe in.mp4 out.mp4 1080 1920

The generators record via MediaRecorder, which emits a fragmented MP4 with zero duration in its header. Players cope; upload APIs sometimes do not. The export writes a normal moov with faststart and keeps the audio track. It prints the source’s real duration and bitrate — worth checking, since X re-encodes everything and dense line-fields want 8–15 Mb/s to survive it.