User story
As a coding agent implementing the Rust FFT spike (tracked in #93), I want a validate_peak function in spike/audio-sync/src/lib.rs that exactly replicates the JS SNR reliability check, so that the Rust binary surfaces a "LOW CONFIDENCE" flag for unreliable offsets in the same cases the JS would.
Background
RFC 0001 (#90) proposes rewriting the editing pipeline in Rust. To validate the agent-driven Rust dev loop before committing, the team is running a spike: port the FFT cross-correlation algorithm from scripts/sync/AudioSyncer.js to a standalone Rust binary.
This issue ports validatePeak (scripts/sync/AudioSyncer.js L218–235). It takes the correlation array and the lag already computed by find_best_lag (#97) and computes an SNR metric: how many standard deviations above the mean is the peak value? If the SNR is below RELIABILITY_SNR_THRESHOLD = 3.0, the result is flagged as unreliable. This is a quality-of-result signal, not an error — the lag is still returned and used; the flag tells the operator to verify the sync manually.
Acceptance criteria
Happy path
Given a correlation array with a sharp, high-amplitude peak and a lag computed by find_best_lag
When validate_peak(correlation, lag_seconds, sample_rate) is called
Then it returns (snr, true) where snr >= 3.0
Given a near-silence correlation array (all values close to zero with tiny noise) and any lag
When validate_peak is called
Then it returns (snr, false) where snr < 3.0
Error path / edge case
Given a correlation array where all values are exactly equal (std = 0)
When validate_peak is called
Then it returns (0.0, false) without a division-by-zero panic — matching the JS snr = std > 0 ? ... : 0 guard
Out of scope
Technical context
JS source to replicate (scripts/sync/AudioSyncer.js L218–235):
validatePeak(correlation, lagSeconds) {
const N = correlation.length;
let sum = 0, sumSq = 0;
for (let i = 0; i < N; i++) { sum += correlation[i]; sumSq += correlation[i] ** 2; }
const mean = sum / N;
const std = Math.sqrt(sumSq / N - mean ** 2);
// convert frame-quantized lag back to sample index for lookup
const lagFrames = Math.round(lagSeconds * SYNC_FRAME_RATE);
const lagSamples = Math.round(lagFrames * this.sampleRate / SYNC_FRAME_RATE);
const idx = ((lagSamples % N) + N) % N; // modular wrap to handle negative lags
const signalValue = correlation[idx];
const snr = std > 0 ? Math.abs(signalValue - mean) / std : 0;
return { snr, isReliable: snr >= RELIABILITY_SNR_THRESHOLD };
}
Constants (already declared in lib.rs from #97):
const SYNC_FRAME_RATE: f64 = 30.0;
const RELIABILITY_SNR_THRESHOLD: f64 = 3.0;
Signature:
pub fn validate_peak(correlation: &[f64], lag_seconds: f64, sample_rate: u32) -> (f64, bool)
Returns (snr, is_reliable).
Index reconstruction subtlety: the JS reconstructs the sample index from the already-quantized lagSeconds (not from raw lag samples) using lagFrames = round(lagSeconds * SYNC_FRAME_RATE) then lagSamples = round(lagFrames * sampleRate / SYNC_FRAME_RATE). The modular wrap ((lagSamples % N) + N) % N handles negative lags correctly — replicate this exactly.
Variance formula: the JS uses the biased population variance (sumSq / N - mean²), not Bessel-corrected. Rust's iterator .map(|x| x*x).sum::<f64>() / n - mean² replicates this.
Implementation details
- Add
pub fn validate_peak(correlation: &[f64], lag_seconds: f64, sample_rate: u32) -> (f64, bool) to spike/audio-sync/src/lib.rs.
- Add
#[test] for high-SNR case: build a correlation array that is all zeros except one large value at a known index; assert is_reliable == true.
- Add
#[test] for low-SNR case: build a flat correlation array (all same value); assert is_reliable == false and snr == 0.0.
- Add
#[test] for zero-std guard: all-equal values; assert no panic and snr == 0.0.
- Run
cargo test --manifest-path spike/audio-sync/Cargo.toml — all tests pass.
- Commit
spike/audio-sync/src/lib.rs.
Additional test scenarios
- Negative lag (index reconstructed via modular wrap): assert result is the same as positive lag of equal magnitude against a symmetric correlation array
- Correlation array of length 1: assert function does not panic
Hard constraints
RELIABILITY_SNR_THRESHOLD = 3.0 must not be changed
- Population variance (divide by N, not N-1) must be used — the JS uses this form
- No changes to files outside
spike/audio-sync/src/lib.rs
Dependency issues
Depends on #95 (Cargo scaffold) and that SYNC_FRAME_RATE / RELIABILITY_SNR_THRESHOLD constants are declared (introduced in #97 — can be added here instead if #97 is not yet merged, but only one issue should own the constants).
Can be developed in parallel with #96 and #97.
#99 (CLI + round-trip test) depends on this issue being complete.
User story
As a coding agent implementing the Rust FFT spike (tracked in #93), I want a
validate_peakfunction inspike/audio-sync/src/lib.rsthat exactly replicates the JS SNR reliability check, so that the Rust binary surfaces a "LOW CONFIDENCE" flag for unreliable offsets in the same cases the JS would.Background
RFC 0001 (#90) proposes rewriting the editing pipeline in Rust. To validate the agent-driven Rust dev loop before committing, the team is running a spike: port the FFT cross-correlation algorithm from
scripts/sync/AudioSyncer.jsto a standalone Rust binary.This issue ports
validatePeak(scripts/sync/AudioSyncer.jsL218–235). It takes the correlation array and the lag already computed byfind_best_lag(#97) and computes an SNR metric: how many standard deviations above the mean is the peak value? If the SNR is belowRELIABILITY_SNR_THRESHOLD = 3.0, the result is flagged as unreliable. This is a quality-of-result signal, not an error — the lag is still returned and used; the flag tells the operator to verify the sync manually.Acceptance criteria
Happy path
Given a correlation array with a sharp, high-amplitude peak and a lag computed by
find_best_lagWhen
validate_peak(correlation, lag_seconds, sample_rate)is calledThen it returns
(snr, true)wheresnr >= 3.0Given a near-silence correlation array (all values close to zero with tiny noise) and any lag
When
validate_peakis calledThen it returns
(snr, false)wheresnr < 3.0Error path / edge case
Given a correlation array where all values are exactly equal (std = 0)
When
validate_peakis calledThen it returns
(0.0, false)without a division-by-zero panic — matching the JSsnr = std > 0 ? ... : 0guardOut of scope
compute_cross_correlation(spike(rust/fft): port compute_cross_correlation to Rust #96)find_best_lag(spike(rust/lag): port find_best_lag to Rust #97)Technical context
JS source to replicate (
scripts/sync/AudioSyncer.jsL218–235):Constants (already declared in
lib.rsfrom #97):Signature:
Returns
(snr, is_reliable).Index reconstruction subtlety: the JS reconstructs the sample index from the already-quantized
lagSeconds(not from raw lag samples) usinglagFrames = round(lagSeconds * SYNC_FRAME_RATE)thenlagSamples = round(lagFrames * sampleRate / SYNC_FRAME_RATE). The modular wrap((lagSamples % N) + N) % Nhandles negative lags correctly — replicate this exactly.Variance formula: the JS uses the biased population variance (
sumSq / N - mean²), not Bessel-corrected. Rust's iterator.map(|x| x*x).sum::<f64>() / n - mean²replicates this.Implementation details
pub fn validate_peak(correlation: &[f64], lag_seconds: f64, sample_rate: u32) -> (f64, bool)tospike/audio-sync/src/lib.rs.#[test]for high-SNR case: build a correlation array that is all zeros except one large value at a known index; assertis_reliable == true.#[test]for low-SNR case: build a flat correlation array (all same value); assertis_reliable == falseandsnr == 0.0.#[test]for zero-std guard: all-equal values; assert no panic andsnr == 0.0.cargo test --manifest-path spike/audio-sync/Cargo.toml— all tests pass.spike/audio-sync/src/lib.rs.Additional test scenarios
Hard constraints
RELIABILITY_SNR_THRESHOLD = 3.0must not be changedspike/audio-sync/src/lib.rsDependency issues
Depends on #95 (Cargo scaffold) and that
SYNC_FRAME_RATE/RELIABILITY_SNR_THRESHOLDconstants are declared (introduced in #97 — can be added here instead if #97 is not yet merged, but only one issue should own the constants).Can be developed in parallel with #96 and #97.
#99 (CLI + round-trip test) depends on this issue being complete.