Synthesized SFX for motion graphics
hn4a has no sound-effect model, so effects are made in Python and mixed by the compositor. They come out clean and a little cartoony, which suits kinetic type; if the person wants realistic foley, ask them for sound files and place those at the same cues.
1. Cues from the spec, not by ear
Read every event time from the compose JSON so a re-timed spec re-cues the sound:
a layer's start (entrance), end (exit), the key where a box height reaches its
minimum (flat), and the contact time where x keys start moving (hit). One sound per
visible event; leave space between them.
| Event | Sound | Recipe |
|---|---|---|
| Title pops in | pop | sine sweep 650-900 Hz up to 1500-1700 Hz, 90 ms, exp decay ~38/s, plus a 2-6 kHz noise tick |
| Squash under a hand | rubbery squeeze | sawtooth sweep 260 Hz down to 70 Hz with 18 Hz wobble, low-passed at 1.4 kHz, plus band-passed noise gated at 22-52 Hz (creak), getting louder as it flattens |
| Pressed flat | splat | 90 Hz falling thump decaying ~14/s plus low-passed noise smack |
| Blinks out | down-pop | sine 900 Hz down to 300 Hz, 120 ms, quieter |
| Hand hits it | thwack | 140 Hz falling thump plus 0.8-5 kHz noise burst, 250 ms |
| Flies off | whoosh | noise through a band-pass whose centre sweeps 400 Hz to 4 kHz over ~0.45 s, sin envelope, panned from the hit side to the exit side |
Pan each sound toward where its title sits (left title about -0.4, right about +0.5). Start transients about 10 ms before the visual event.
2. Build the track
48 kHz stereo, the comp's exact duration. scipy.signal.butter(..., output='sos') with
sosfilt for the filters; equal-power pan (sqrt((1-p)/2), sqrt((1+p)/2)); for a moving
pan, add the whoosh in 10 ms slices with the pan ramping. Seed the noise generator so a
rebuild sounds the same. Normalize the mix peak to about -1 dBFS. Write it to
audio/sfx.wav and keep the script in scripts/make_sfx.py, taking the spec path as its
argument.
3. Replace the audio
In the spec: "audio": [{"src": "../audio/sfx.wav", "start": 0, "trim": 0, "volume": 1}].
To keep the original sound under the effects, list the footage as a second audio entry
at a lower volume instead of removing it. Changing audio needs a whole render, not
--into.
4. Check
- After the render, run
volumedetecton a stretch with no events: about -91 dB means the original audio is really gone. - You can't hear: put the render on a review page, each sound and its moment in the
row's
body, send the MP4 into the chat, and ask the person to listen. Adjust gain or swap a recipe per their notes, then rebuild from the script.


