Multi-camera video synchronization using inaudible acoustic markers — no timecode hardware, no clapper board, no HDMI sync cable. You press a button; every camera in the room hears a short ultrasonic tone burst; post-processing aligns the recordings by matching the tone.
cheepsync-mark emits a ~200ms FSK-encoded burst at 18 kHz (adjustable) when you press a button. Each press broadcasts a unique marker ID (0–255 per session) and logs the exact wall-clock time. Think of it as a digital slate clap.
cheepsync-sync finds those markers in your recorded video files and either:
--find— prints a table of which markers appear in which files and when- default — prepends blank video to earlier-starting cameras and low-pass filters the sync tone out of all tracks, producing aligned files you can drop into any editor
- Python 3.9+
ffmpegandffprobeon PATH (forcheepsync-sync)- A speaker within ~3 m of all cameras (Pimoroni Pirate Audio 3W hat works well on the Pi)
pip install cheepsync # mac/linux
pip install cheepsync[rpi] # Raspberry Pi (adds gpiozero)
1. Start recording on all cameras.
2. Press the cheepsync-mark button (or spacebar) — one press per sync point.
3. Stop cameras.
4. Run cheepsync-sync to align the files.
Pressing the button multiple times creates redundant sync points — useful if one recording was too noisy or the signal was blocked. You only need one common marker between cameras.
Plays a marker burst on each button press and writes a session log.
cheepsync-mark [OPTIONS]
| Option | Default | Description |
|---|---|---|
--base-freq |
18000 | Carrier frequency in Hz. Reduce if cameras can't pick up 18 kHz. |
--fsk-shift |
500 | Frequency shift for FSK bit-1 (Hz). |
--sample-rate |
48000 | Audio sample rate. |
--device |
system default | sounddevice output device index or name substring. |
--list-devices |
— | Print available output devices and exit. |
Trigger:
- Raspberry Pi — physical button on BCM 5 (Pimoroni Pirate Audio A button, detected automatically via
gpiozero) - Mac / Linux — press SPACEBAR; Q or Ctrl+C to end session
Session log — written to the current directory as cheepsync_YYYYMMDD_HHMMSS.json:
{
"session_id": "20260406_142315",
"started": "2026-04-06T14:23:15.000Z",
"base_freq": 18000.0,
"fsk_shift": 500.0,
"markers": [
{"id": 0, "time": "2026-04-06T14:23:45.123Z"},
{"id": 1, "time": "2026-04-06T14:24:12.456Z"}
]
}Detects markers in video files and produces aligned output.
cheepsync-sync [OPTIONS] VIDEO [VIDEO ...]
cheepsync-sync cam_a.MOV cam_b.MOV --find
Prints a table of every detected marker, which files contain it, and the time it appears in each:
Marker cam_a.MOV cam_b.MOV
------ ---------- ----------
0 ✓ 10.000s 6.300s
1 ✓ 25.000s 21.300s
3 89.001s —
2 of 3 marker(s) present in all files.
Pass --session-log cheepsync_*.json to annotate the table with wall-clock times.
cheepsync-sync cam_a.MOV cam_b.MOV --output-dir ./synced
- Detects markers in each file
- Picks the common markers and computes per-camera time offsets
- Generates a
_STARToffset clip for each non-reference camera - Place each
_STARTclip before its video on the NLE timeline to align
Optionally pass --lowpass-hz 17500 to also generate _filtered copies with the sync tones removed from audio.
| Option | Default | Description |
|---|---|---|
--base-freq |
18000 | Must match what cheepsync-mark used. |
--fsk-shift |
500 | Must match what cheepsync-mark used. |
--sample-rate |
48000 | Audio analysis sample rate. |
--session-log |
— | Optional .json from cheepsync-mark for wall-clock annotation. |
--find |
— | Report only; no output files. |
--output-dir |
. |
Where to write offset clips and filtered files. |
--lowpass-hz |
0 | Low-pass cutoff for filtered copies (0 = disabled). Set to 17500 to remove sync tones. |
--threshold |
10.0 | Goertzel magnitude threshold factor (noise_floor × factor). Raise if too many false positives; lower if markers are missed. |
--debug |
— | Print detection details for each candidate onset (verbose). |
--verbose |
— | Print ffmpeg progress and metadata. |
Each marker burst is 200 ms total:
[30ms sync pulse @ base_freq] [10ms silence] [8 FSK bits × 20ms each]
The 30ms sync pulse followed by 10ms of silence acts as a structural header: the FSK decoder skips past it to find the 8 data bits that follow.
Detection pipeline:
- Goertzel filter at base_freq in 5ms windows — computes magnitude at the exact sync tone frequency (single DFT bin, no broadband pre-filter needed)
- Threshold onset detection — Goertzel magnitude must exceed noise_floor × threshold_factor, with a 250ms refractory period between detections
- FSK decode — at each onset, skip the sync pulse + gap, then compare Goertzel magnitudes at the two FSK frequencies across 8 bit windows
18 kHz is the default — above the hearing range of most adults, below the recording cutoff of common consumer cameras. Some cameras roll off above 15–16 kHz.
If markers aren't detected: open a recording in Audacity, switch to Spectrogram view (range 14–22 kHz), and look for a short bright stripe at 18 kHz at the moment of the button press. If no stripe is visible, lower --base-freq in 500 Hz steps until it appears.
If you hear the tone: lower --base-freq toward 18000–19000 Hz to move it higher. Young ears can sometimes hear above 17 kHz; test with your youngest participants.
If false positives persist: try a different --base-freq that doesn't coincide with camera electronics noise. Common interference bands: 15.6 kHz (CRT horizontal scan, rare now), 17–19 kHz (some LED drivers), 20 kHz (some camera DSPs).
pip install cheepsync[rpi]
cheepsync-mark --device "sndrpihifiberry" # find exact name with --list-devicesThe MAX98357A I²S DAC on the Pirate Audio hat appears as an ALSA device. Use --list-devices to find its name on your system. Output is always stereo (L+R identical) as required by the I²S DAC.
The A button (BCM 5) is used automatically when gpiozero is installed. No configuration needed.