Skip to content

Add speed audio modes for retimed clip segments#2016

Merged
richiemcilroy merged 14 commits into
mainfrom
speedup-sound
Jul 16, 2026
Merged

Add speed audio modes for retimed clip segments#2016
richiemcilroy merged 14 commits into
mainfrom
speedup-sound

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Jul 16, 2026

Copy link
Copy Markdown
Member

Clip segments with non-1x speed no longer always mute audio. Each segment can use Mute (default), Maintain pitch, or Match speed (pitch follows playback speed).

  • Adds ClipSpeedAudioMode to the project schema with backward-compatible serde defaults
  • Implements FFmpeg-based retiming in the editor audio renderer (atempo / asetrate)
  • Adds a speed audio picker in the clip config sidebar when speed ≠ 1x
  • Wires the setting through import, recording, export, and playback benchmarks

Legacy projects default to mute, preserving existing behavior.

Greptile Summary

This PR lifts the blanket audio mute for retimed clip segments and replaces it with three user-selectable modes — Mute (default, preserves existing behavior), Maintain pitch (pitch-corrected via an FFmpeg atempo chain), and Match speed (pitch follows rate via asetrate+aresample). The schema change is fully backward-compatible (None serialises as absent; legacy files default to Mute). A 2-slot LRU SpeedAudioProcessor cache wraps per-segment FFmpeg filter graphs with preroll/discard seek support and a contiguity check for gapless playback.

  • Schema & Tauri bindings (configuration.rs, tauri.ts): adds ClipSpeedAudioMode enum and optional speed_audio_mode field to TimelineSegment; all new literal sites across recording, import, export, and benchmark code are initialised to None.
  • Audio renderer (audio.rs): render_speed_audio_chunk replaces the old silent-bypass; the SpeedAudioProcessor feeds raw audio data into an FFmpeg filter graph and drains output into a VecDeque, with preroll to reduce seek latency; set_playhead clears both slots.
  • Sidebar UI (ConfigSidebar.tsx, context.ts): adds the audio-mode radio group below the speed picker, gated on timescale !== 1; the mode is persisted to the project store without requiring a seek.

Confidence Score: 5/5

Safe to merge; new speed-audio paths are additive and isolated, legacy projects default to silent behavior, and the schema change is backward-compatible.

FFmpeg filter dispatch, LRU processor cache, preroll/discard seek logic, and serialization round-trip are all covered by new tests. No existing audio rendering path is broken.

No files require special attention; the two suggestions in audio.rs are minor quality improvements, not blockers.

Important Files Changed

Filename Overview
crates/editor/src/audio.rs Core change: adds SpeedAudioProcessor with 2-slot LRU cache, FFmpeg filter-graph dispatch (atempo chain for MaintainPitch, asetrate+aresample for MatchSpeed), preroll/discard for seek, and contiguity check. Logic is sound and well-tested.
crates/project/src/configuration.rs Adds ClipSpeedAudioMode enum and speed_audio_mode field to TimelineSegment with serde defaults; serialization round-trip test verifies backward compatibility.
apps/desktop/src/routes/editor/ConfigSidebar.tsx Adds speed audio picker behind a Show guard for timescale !== 1; removes the old static mute warning.
apps/desktop/src/routes/editor/context.ts Adds setClipSegmentSpeedAudioMode action writing speedAudioMode into the SolidJS store.
apps/desktop/src/utils/tauri.ts Exports ClipSpeedAudioMode union type and extends TimelineSegment with the optional speedAudioMode field.
apps/desktop/src-tauri/src/import.rs Adds speed_audio_mode: None to import-path literals; append_cap_project correctly propagates source_segment.speed_audio_mode.
crates/export/src/lib.rs Adds speed_audio_mode: None when constructing export segments.
crates/recording/src/studio_recording.rs Adds speed_audio_mode: None to new recording segments.
crates/recording/src/track_heal.rs Adds speed_audio_mode: None to test fixtures and refactors config init to use struct update syntax.
crates/recording/src/recovery.rs Adds speed_audio_mode: None to recovery-path segment construction.
crates/editor/src/editor_instance.rs Adds speed_audio_mode: None when synthesising timeline segments from recording metadata.
crates/rendering/src/zoom_spring.rs Adds speed_audio_mode: None to zoom-spring test fixtures only.

Reviews (2): Last reviewed commit: "fix: harden speed audio filter construct..." | Re-trigger Greptile

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Comment on lines +745 to +748
let requested_source_sample = requested_source_sample.clamp(
key.segment_start_samples as f64,
key.segment_end_samples as f64,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f64::clamp will panic if segment_end_samples < segment_start_samples (e.g. if a segment gets corrupted / has end < start). Might be worth guarding here so audio rendering can fail soft instead of crashing.

Suggested change
let requested_source_sample = requested_source_sample.clamp(
key.segment_start_samples as f64,
key.segment_end_samples as f64,
);
if key.segment_end_samples <= key.segment_start_samples {
return Err(ffmpeg::Error::InvalidData);
}
let requested_source_sample = requested_source_sample.clamp(
key.segment_start_samples as f64,
key.segment_end_samples as f64,
);

Comment thread crates/editor/src/audio.rs Outdated
Comment thread crates/editor/src/audio.rs Outdated
Comment on lines +784 to +808
while self.pending.len() / 2 < target_samples {
if self.input_samples < self.segment_end_samples {
self.feed_input(data, project)?;
} else if !self.flushed {
let mut source = self.graph.get("in").ok_or(ffmpeg::Error::InvalidData)?;
source.source().flush()?;
self.flushed = true;
} else {
break;
}
self.drain_output()?;
}

let discard = self.discard_output_samples.min(self.pending.len() / 2);
for _ in 0..discard * 2 {
self.pending.pop_front();
}
self.discard_output_samples -= discard;

let available_output = out.len().saturating_sub(out_offset) / 2;
let written = samples.min(available_output).min(self.pending.len() / 2);
for target in &mut out[out_offset..out_offset + written * 2] {
*target = self.pending.pop_front().unwrap();
}
self.expected_source_sample = requested_source_sample + samples as f64 * self.timescale;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Seek latency on first call with large preroll

When a new SpeedAudioProcessor is created at a position far from the segment start, discard_output_samples can reach SPEED_AUDIO_PREROLL_SAMPLES (2 400) output frames. The inner while loop then pumps SPEED_AUDIO_INPUT_BLOCK_SAMPLES (4 096) source frames per iteration until the pending queue covers both discard and samples. For MaintainPitch at 0.25× speed each source block only produces ~1 024 output frames, so a cold seek may require ≥3 pump iterations on the first call — this is bounded and acceptable. Worth keeping in mind if users report seek latency at extreme slow-motion values.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/editor/src/audio.rs
Line: 784-808

Comment:
**Seek latency on first call with large preroll**

When a new `SpeedAudioProcessor` is created at a position far from the segment start, `discard_output_samples` can reach `SPEED_AUDIO_PREROLL_SAMPLES` (2 400) output frames. The inner `while` loop then pumps `SPEED_AUDIO_INPUT_BLOCK_SAMPLES` (4 096) source frames per iteration until the pending queue covers both `discard` and `samples`. For `MaintainPitch` at 0.25× speed each source block only produces ~1 024 output frames, so a cold seek may require ≥3 pump iterations on the first call — this is bounded and acceptable. Worth keeping in mind if users report seek latency at extreme slow-motion values.

How can I resolve this? If you propose a fix, please make it concise.

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy
richiemcilroy merged commit 6c131a5 into main Jul 16, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant