diff --git a/apps/cli/src/selftest/playback.rs b/apps/cli/src/selftest/playback.rs index 83845d08aa..2bb0230844 100644 --- a/apps/cli/src/selftest/playback.rs +++ b/apps/cli/src/selftest/playback.rs @@ -903,6 +903,7 @@ mod fixture { timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }], transitions: Vec::new(), zoom_segments: Vec::new(), diff --git a/apps/desktop-gpui/src/editor_clips.rs b/apps/desktop-gpui/src/editor_clips.rs index 73fe939a16..1ac266d338 100644 --- a/apps/desktop-gpui/src/editor_clips.rs +++ b/apps/desktop-gpui/src/editor_clips.rs @@ -3192,6 +3192,7 @@ fn full_timeline_for_segments( end: duration, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect() @@ -3768,6 +3769,7 @@ fn full_timeline_for_source_segments( end: duration, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect() @@ -3857,6 +3859,7 @@ fn source_timeline_segments_for_import( end, name: None, speed_audio_mode: None, + audio_muted: segment.audio_muted, }); } @@ -4087,6 +4090,7 @@ pub(crate) fn append_cap_project_to_editor( end: source_segment.end, name: None, speed_audio_mode: source_segment.speed_audio_mode, + audio_muted: source_segment.audio_muted, }); } } @@ -4116,6 +4120,7 @@ mod tests { end, name: None, speed_audio_mode: None, + audio_muted: false, } } diff --git a/apps/desktop-gpui/src/editor_edits.rs b/apps/desktop-gpui/src/editor_edits.rs index c4e23b8e61..163e00b8f2 100644 --- a/apps/desktop-gpui/src/editor_edits.rs +++ b/apps/desktop-gpui/src/editor_edits.rs @@ -1262,6 +1262,7 @@ pub fn ensure_timeline(project: &mut ProjectConfiguration, clip_display_duration end: *duration, name: None, speed_audio_mode: None, + audio_muted: false, }) .collect(), transitions: Vec::new(), @@ -1847,28 +1848,22 @@ pub fn set_clip_segment_timescale( } pub fn clip_is_muted(segment: &TimelineSegment) -> bool { - if (segment.timescale - 1.0).abs() < f64::EPSILON { + let speed_mode_muted = if (segment.timescale - 1.0).abs() < f64::EPSILON { segment.speed_audio_mode == Some(ClipSpeedAudioMode::Mute) } else { segment.speed_audio_mode.unwrap_or(ClipSpeedAudioMode::Mute) == ClipSpeedAudioMode::Mute - } + }; + segment.audio_muted || speed_mode_muted } pub fn set_clip_muted(timeline: &mut TimelineConfiguration, index: usize, muted: bool) -> bool { let Some(segment) = timeline.segments.get_mut(index) else { return false; }; - let next = if muted { - Some(ClipSpeedAudioMode::Mute) - } else if (segment.timescale - 1.0).abs() < f64::EPSILON { - None - } else { - Some(ClipSpeedAudioMode::MaintainPitch) - }; - if segment.speed_audio_mode == next { + if segment.audio_muted == muted { return false; } - segment.speed_audio_mode = next; + segment.audio_muted = muted; true } @@ -2775,18 +2770,17 @@ mod tests { } #[test] - fn muting_a_1x_clip_sets_speed_audio_mode() { + fn muting_a_1x_clip_sets_audio_muted() { let mut project = zoom_fixture(); let timeline = project.timeline.as_mut().unwrap(); assert!(!clip_is_muted(&timeline.segments[0])); assert!(set_clip_muted(timeline, 0, true)); assert!(clip_is_muted(&timeline.segments[0])); - assert_eq!( - timeline.segments[0].speed_audio_mode, - Some(ClipSpeedAudioMode::Mute) - ); + assert!(timeline.segments[0].audio_muted); + assert_eq!(timeline.segments[0].speed_audio_mode, None); assert!(set_clip_muted(timeline, 0, false)); assert!(!clip_is_muted(&timeline.segments[0])); + assert!(!timeline.segments[0].audio_muted); assert_eq!(timeline.segments[0].speed_audio_mode, None); } diff --git a/apps/desktop-gpui/src/editor_window.rs b/apps/desktop-gpui/src/editor_window.rs index bdca2a1cb3..bb30087fa9 100644 --- a/apps/desktop-gpui/src/editor_window.rs +++ b/apps/desktop-gpui/src/editor_window.rs @@ -355,6 +355,7 @@ pub fn preflight(path: &std::path::Path) -> Result { end: segment.duration(), name: None, speed_audio_mode: None, + audio_muted: false, }) .collect(), // `TimelineConfiguration` has no `Default`, so the eight other @@ -6115,7 +6116,7 @@ impl EditorWindow { .as_ref() .and_then(|timeline| timeline.segments.get(menu.index))?; let timescale = segment.timescale; - let muted = edits::clip_is_muted(segment); + let muted = segment.audio_muted; let audio_mode = segment.speed_audio_mode.unwrap_or_default(); let speeds = [0.25, 0.5, 1.0, 1.5, 2.0, 4.0, 8.0]; let audio_modes = [ @@ -6203,7 +6204,52 @@ impl EditorWindow { })) })), ) - .child(if normal_speed { + .when(!normal_speed, |popover| { + popover.child( + div() + .flex() + .flex_row() + .items_center() + .gap(px(4.)) + .rounded(px(8.)) + .bg(Hsla::from(theme.gray_2)) + .p(px(4.)) + .children(audio_modes.into_iter().map(|(mode, label)| { + let selected = audio_mode == mode; + div() + .id(SharedString::from(format!( + "clip-speed-audio-{label}" + ))) + .rounded(px(6.)) + .px(px(8.)) + .py(px(4.)) + .text_size(px(12.)) + .cursor_pointer() + .bg(if selected { + Hsla::from(theme.gray_4) + } else { + gpui::transparent_black() + }) + .text_color(Hsla::from(if selected { + theme.gray_12 + } else { + theme.gray_10 + })) + .hover(|this| { + this.text_color(Hsla::from(theme.gray_12)) + }) + .child(label) + .on_click(cx.listener( + move |this, _, window, cx| { + this.set_clip_speed_audio_mode( + index, mode, window, cx, + ); + }, + )) + })), + ) + }) + .child( div() .flex() .flex_row() @@ -6235,45 +6281,8 @@ impl EditorWindow { .on_click(cx.listener(move |this, _, window, cx| { this.set_clip_muted(index, !muted, window, cx); })), - ) - .into_any_element() - } else { - div() - .flex() - .flex_row() - .items_center() - .gap(px(4.)) - .rounded(px(8.)) - .bg(Hsla::from(theme.gray_2)) - .p(px(4.)) - .children(audio_modes.into_iter().map(|(mode, label)| { - let selected = audio_mode == mode - || (mode == ClipSpeedAudioMode::Mute && muted); - div() - .id(SharedString::from(format!("clip-speed-audio-{label}"))) - .rounded(px(6.)) - .px(px(8.)) - .py(px(4.)) - .text_size(px(12.)) - .cursor_pointer() - .bg(if selected { - Hsla::from(theme.gray_4) - } else { - gpui::transparent_black() - }) - .text_color(Hsla::from(if selected { - theme.gray_12 - } else { - theme.gray_10 - })) - .hover(|this| this.text_color(Hsla::from(theme.gray_12))) - .child(label) - .on_click(cx.listener(move |this, _, window, cx| { - this.set_clip_speed_audio_mode(index, mode, window, cx); - })) - })) - .into_any_element() - }), + ), + ), ) .into_any_element(), ) diff --git a/apps/desktop-gpui/src/transcription.rs b/apps/desktop-gpui/src/transcription.rs index 4f27fe5943..614a9d445a 100644 --- a/apps/desktop-gpui/src/transcription.rs +++ b/apps/desktop-gpui/src/transcription.rs @@ -2288,6 +2288,7 @@ pub fn apply_caption_result( end: recording_duration, name: None, speed_audio_mode: None, + audio_muted: false, }], transitions: Vec::new(), zoom_segments: Vec::new(), diff --git a/apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs b/apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs index 47cac690d8..94e26ac9f2 100644 --- a/apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs +++ b/apps/desktop/src-tauri/examples/desktop-display-transport-benchmark.rs @@ -133,6 +133,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }] } StudioRecordingMeta::MultipleSegments { inner } => inner @@ -152,6 +153,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(), diff --git a/apps/desktop/src-tauri/src/import.rs b/apps/desktop/src-tauri/src/import.rs index 3d543b216e..a5a3f8799b 100644 --- a/apps/desktop/src-tauri/src/import.rs +++ b/apps/desktop/src-tauri/src/import.rs @@ -324,6 +324,7 @@ fn full_timeline_for_segments( end: duration, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect() @@ -358,6 +359,7 @@ fn full_timeline_for_source_segments( end: duration, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect() @@ -920,6 +922,7 @@ fn source_timeline_segments_for_import( end, name: None, speed_audio_mode: None, + audio_muted: segment.audio_muted, }); } @@ -1729,6 +1732,7 @@ async fn append_mp4_to_editor_project( end: duration, name: None, speed_audio_mode: None, + audio_muted: false, }); add_clip_configs( &mut config, @@ -1778,13 +1782,21 @@ async fn append_cap_project_to_editor_project( }; }; + append_studio_project_to_editor_project(target_project_path, &source_meta, source_studio_meta) +} + +fn append_studio_project_to_editor_project( + target_project_path: PathBuf, + source_meta: &RecordingMeta, + source_studio_meta: &StudioRecordingMeta, +) -> Result { let source_segments = studio_segments_for_import(source_studio_meta); if source_segments.is_empty() { return Err("Source Cap project has no recording segments".to_string()); } - let source_timeline = source_timeline_segments_for_import(&source_meta, &source_segments)?; - let source_cursors = match source_studio_meta.as_ref() { + let source_timeline = source_timeline_segments_for_import(source_meta, &source_segments)?; + let source_cursors = match source_studio_meta { StudioRecordingMeta::MultipleSegments { inner } => Some(&inner.cursors), StudioRecordingMeta::SingleSegment { .. } => None, }; @@ -1860,6 +1872,7 @@ async fn append_cap_project_to_editor_project( end: source_segment.end, name: None, speed_audio_mode: source_segment.speed_audio_mode, + audio_muted: source_segment.audio_muted, }); } } @@ -2094,6 +2107,126 @@ pub async fn check_import_ready(project_path: PathBuf) -> Result { mod tests { use super::*; + #[test] + fn cap_project_import_preserves_segment_mute() { + let _ = ffmpeg::init(); + let source_project = tempfile::tempdir().unwrap(); + let target_project = tempfile::tempdir().unwrap(); + let display_path = RelativePathBuf::from("content/segments/segment-0/display.mp4"); + let absolute_display_path = display_path.to_path(source_project.path()); + std::fs::create_dir_all(absolute_display_path.parent().unwrap()).unwrap(); + std::fs::write( + &absolute_display_path, + include_bytes!("../../../media-server/src/__tests__/fixtures/test-no-audio.mp4"), + ) + .unwrap(); + + let source_segment = MultipleSegment { + display: VideoMeta { + path: display_path, + fps: 30, + start_time: Some(0.0), + device_id: None, + }, + camera: None, + mic: None, + system_audio: None, + cursor: None, + keyboard: None, + display_notch: None, + }; + let source_meta = RecordingMeta { + platform: Some(Platform::default()), + project_path: source_project.path().to_path_buf(), + pretty_name: "Muted import fixture".to_string(), + sharing: None, + inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments { + inner: MultipleSegments { + segments: vec![source_segment], + cursors: Cursors::default(), + status: Some(StudioRecordingStatus::Complete), + }, + })), + upload: None, + }; + source_meta.save_for_project().unwrap(); + ProjectConfiguration { + timeline: Some(TimelineConfiguration { + segments: vec![TimelineSegment { + recording_clip: 0, + timescale: 1.0, + start: 0.0, + end: 1.0, + name: None, + speed_audio_mode: None, + audio_muted: true, + }], + transitions: Vec::new(), + zoom_segments: Vec::new(), + scene_segments: Vec::new(), + mask_segments: Vec::new(), + text_segments: Vec::new(), + caption_segments: Vec::new(), + keyboard_segments: Vec::new(), + audio_segments: Vec::new(), + camera3d_segments: Vec::new(), + }), + ..Default::default() + } + .write(source_project.path()) + .unwrap(); + let source_meta = RecordingMeta::load_for_project(source_project.path()).unwrap(); + + RecordingMeta { + platform: Some(Platform::default()), + project_path: target_project.path().to_path_buf(), + pretty_name: "Target import fixture".to_string(), + sharing: None, + inner: RecordingMetaInner::Studio(Box::new(StudioRecordingMeta::MultipleSegments { + inner: MultipleSegments { + segments: Vec::new(), + cursors: Cursors::default(), + status: Some(StudioRecordingStatus::Complete), + }, + })), + upload: None, + } + .save_for_project() + .unwrap(); + + let RecordingMetaInner::Studio(source_studio) = &source_meta.inner else { + panic!("expected Studio source metadata"); + }; + let imported = append_studio_project_to_editor_project( + target_project.path().to_path_buf(), + &source_meta, + source_studio, + ) + .unwrap(); + + let target_config = ProjectConfiguration::load(target_project.path()).unwrap(); + let target_timeline = target_config.timeline.unwrap(); + let target_meta = RecordingMeta::load_for_project(target_project.path()).unwrap(); + let RecordingMetaInner::Studio(target_studio) = target_meta.inner else { + panic!("expected Studio recording metadata"); + }; + let StudioRecordingMeta::MultipleSegments { inner } = *target_studio else { + panic!("expected multiple recording segments"); + }; + + assert_eq!(imported, 1); + assert_eq!(target_timeline.segments.len(), 1); + assert!(target_timeline.segments[0].audio_muted); + assert_eq!(inner.segments.len(), 1); + assert!( + inner.segments[0] + .display + .path + .to_path(target_project.path()) + .is_file() + ); + } + #[test] fn imported_video_frames_share_reference_counted_pixel_storage() { let mut source = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, 16, 12); diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 1e0743a6b3..817d7ac5da 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -3936,6 +3936,7 @@ fn project_config_from_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) .collect::>(); diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 0d02e93015..b4d3ba7a42 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -356,11 +356,13 @@ function WaveformCanvas(props: { function ClipSpeedControl(props: { timescale: number; speedAudioMode?: ClipSpeedAudioMode | null; + audioMuted?: boolean; open: boolean; onOpenChange: (open: boolean) => void; triggerClass?: string; onSetTimescale: (timescale: number) => void; onSetSpeedAudioMode: (mode: ClipSpeedAudioMode) => void; + onSetAudioMuted: (audioMuted: boolean) => void; }) { return ( + @@ -1239,6 +1254,7 @@ export function ClipTrack( projectActions.setClipSegmentSpeedAudioMode(i(), mode) } + onSetAudioMuted={(audioMuted) => + projectActions.setClipSegmentAudioMuted( + i(), + audioMuted, + ) + } /> ); diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 30e55787a5..f85d159fde 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -1200,6 +1200,9 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( speedAudioMode, ); }, + setClipSegmentAudioMuted: (index: number, audioMuted: boolean) => { + setProject("timeline", "segments", index, "audioMuted", audioMuted); + }, }; let projectSaveTimeout: number | undefined; diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index fda893d199..8519915240 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1235,7 +1235,7 @@ letterSpacing?: number; lineHeight?: number; opacity?: number; shadow?: number; */ layoutTransition?: number } export type TimelineConfiguration = { segments: TimelineSegment[]; transitions: ClipTransition[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[]; maskSegments?: MaskSegment[]; textSegments?: TextSegment[]; captionSegments?: CaptionTrackSegment[]; keyboardSegments?: KeyboardTrackSegment[]; audioSegments?: AudioTrackSegment[]; camera3dSegments?: Camera3DSegment[] } -export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number; name?: string | null; speedAudioMode?: ClipSpeedAudioMode | null } +export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number; name?: string | null; speedAudioMode?: ClipSpeedAudioMode | null; audioMuted?: boolean } export type TranscriptionEngine = "Whisper" | "Parakeet" export type Trigger = "screenshotTaken" | "studioRecordingFinished" | "instantRecordingFinished" | "recordingStarted" | "uploadCompleted" | "videoImported" | "recordingDeleted" export type UpdateChannel = "stable" | "nightly" diff --git a/crates/editor/examples/editor-playback-benchmark.rs b/crates/editor/examples/editor-playback-benchmark.rs index b143280a94..53aa449212 100644 --- a/crates/editor/examples/editor-playback-benchmark.rs +++ b/crates/editor/examples/editor-playback-benchmark.rs @@ -353,6 +353,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }] } StudioRecordingMeta::MultipleSegments { inner } => inner @@ -372,6 +373,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(), diff --git a/crates/editor/examples/playback-pipeline-benchmark.rs b/crates/editor/examples/playback-pipeline-benchmark.rs index 5601ac3231..ae30fb7394 100644 --- a/crates/editor/examples/playback-pipeline-benchmark.rs +++ b/crates/editor/examples/playback-pipeline-benchmark.rs @@ -276,6 +276,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }] } StudioRecordingMeta::MultipleSegments { inner } => inner @@ -298,6 +299,7 @@ async fn load_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(), diff --git a/crates/editor/src/audio.rs b/crates/editor/src/audio.rs index 0a8ecdca8e..d8367c1da1 100644 --- a/crates/editor/src/audio.rs +++ b/crates/editor/src/audio.rs @@ -496,6 +496,10 @@ impl AudioRenderer { out_offset: usize, out: &mut [f32], ) -> usize { + if samples == 0 || project.audio.mute || source.segment.audio_muted { + return 0; + } + if source.segment.timescale == 1.0 { if source.segment.speed_audio_mode == Some(ClipSpeedAudioMode::Mute) { return 0; @@ -515,9 +519,7 @@ impl AudioRenderer { out_offset: usize, out: &mut [f32], ) -> usize { - if samples == 0 - || project.audio.mute - || source.segment.speed_audio_mode.unwrap_or_default() == ClipSpeedAudioMode::Mute + if source.segment.speed_audio_mode.unwrap_or_default() == ClipSpeedAudioMode::Mute || !source.segment.timescale.is_finite() || !(0.25..=8.0).contains(&source.segment.timescale) { @@ -1903,6 +1905,7 @@ mod tests { end: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 0, @@ -1911,6 +1914,7 @@ mod tests { end: 2.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 0, @@ -1919,6 +1923,7 @@ mod tests { end: 3.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 1, @@ -1927,6 +1932,7 @@ mod tests { end: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 1, @@ -1935,6 +1941,7 @@ mod tests { end: 2.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 1, @@ -1943,6 +1950,7 @@ mod tests { end: 3.0, name: None, speed_audio_mode: None, + audio_muted: false, }, ], transitions: Vec::new(), @@ -2237,6 +2245,32 @@ mod tests { } } + #[test] + fn segment_mute_does_not_mute_adjacent_segments() { + let mut muted = segment(0, 0.0, 1.0, 1.0); + muted.audio_muted = true; + let (_dir, mut renderer, project) = + single_clip_fixture(&[8_000, 16_000], vec![muted, segment(0, 1.0, 2.0, 1.0)]); + + let stream = render_export_audio(&mut renderer, &project, 30, 60); + + assert!(left_at_second(&stream, 0).abs() < 0.0001); + assert!((left_at_second(&stream, 1) - expected(16_000)).abs() < 0.01); + } + + #[test] + fn segment_mute_silences_retimed_audio() { + let mut muted = segment(0, 0.0, 2.0, 2.0); + muted.speed_audio_mode = Some(ClipSpeedAudioMode::MaintainPitch); + muted.audio_muted = true; + let (_dir, mut renderer, project) = single_clip_fixture(&[8_000, 16_000], vec![muted]); + + let stream = render_export_audio(&mut renderer, &project, 30, 30); + + assert!(mean_abs(&stream) < 0.0001); + assert!(renderer.speed_audio_processors.iter().all(Option::is_none)); + } + /// One clip per second `section_values`, on a timeline made of `segments`. fn single_clip_fixture( section_values: &[i16], @@ -2288,6 +2322,7 @@ mod tests { end, name: None, speed_audio_mode: None, + audio_muted: false, } } diff --git a/crates/editor/src/editor_instance.rs b/crates/editor/src/editor_instance.rs index e75b7cd4e9..688d78e20d 100644 --- a/crates/editor/src/editor_instance.rs +++ b/crates/editor/src/editor_instance.rs @@ -256,6 +256,7 @@ impl EditorInstance { timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }], _ => { warn!( @@ -289,6 +290,7 @@ impl EditorInstance { timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(), diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index b4857ff2a3..58247d0c2a 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -118,6 +118,7 @@ impl ExporterBuilder { timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(); diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs index 8114d15523..cb56706191 100644 --- a/crates/project/src/configuration.rs +++ b/crates/project/src/configuration.rs @@ -906,6 +906,8 @@ pub struct TimelineSegment { pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub speed_audio_mode: Option, + #[serde(default)] + pub audio_muted: bool, } #[derive(Type, Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -2627,6 +2629,7 @@ mod tests { end: 4.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 1, @@ -2635,6 +2638,7 @@ mod tests { end: 16.0, name: None, speed_audio_mode: None, + audio_muted: false, }, ], transitions, @@ -2775,6 +2779,19 @@ mod tests { ); } + #[test] + fn timeline_segment_defaults_audio_muted_for_existing_projects() { + let segment: TimelineSegment = serde_json::from_value(serde_json::json!({ + "recordingSegment": 0, + "timescale": 1.0, + "start": 0.0, + "end": 1.0 + })) + .unwrap(); + + assert!(!segment.audio_muted); + } + #[test] fn timeline_maps_both_sources_inside_transition() { let timeline = timeline_with_transitions(vec![ClipTransition { diff --git a/crates/recording/src/recovery.rs b/crates/recording/src/recovery.rs index 60e92c15d6..9cc548f20a 100644 --- a/crates/recording/src/recovery.rs +++ b/crates/recording/src/recovery.rs @@ -1537,6 +1537,7 @@ impl RecoveryManager { timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(); diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index 909f23369f..ccaa3eb11e 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -1120,6 +1120,7 @@ async fn stop_recording( timescale: 1.0, name: None, speed_audio_mode: None, + audio_muted: false, }) }) .collect(); diff --git a/crates/recording/src/track_heal.rs b/crates/recording/src/track_heal.rs index 2534ec7cda..8a42d5bcd2 100644 --- a/crates/recording/src/track_heal.rs +++ b/crates/recording/src/track_heal.rs @@ -1052,6 +1052,7 @@ mod tests { end: 202.220711, name: None, speed_audio_mode: None, + audio_muted: false, }], transitions: Vec::new(), zoom_segments: vec![ZoomSegment { @@ -1106,6 +1107,7 @@ mod tests { end: 202.220711, name: None, speed_audio_mode: None, + audio_muted: false, }], transitions: Vec::new(), zoom_segments: vec![], @@ -1180,6 +1182,7 @@ mod tests { end: 100.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 1, @@ -1188,6 +1191,7 @@ mod tests { end: 50.0, name: None, speed_audio_mode: None, + audio_muted: false, }, ], transitions: Vec::new(), diff --git a/crates/rendering/src/zoom_spring.rs b/crates/rendering/src/zoom_spring.rs index 7e42d01079..4ad05cb10c 100644 --- a/crates/rendering/src/zoom_spring.rs +++ b/crates/rendering/src/zoom_spring.rs @@ -1549,6 +1549,7 @@ mod tests { end: 20.0, name: None, speed_audio_mode: None, + audio_muted: false, }], transitions: vec![], zoom_segments: vec![], @@ -1606,6 +1607,7 @@ mod tests { end: 4.0, name: None, speed_audio_mode: None, + audio_muted: false, }, TimelineSegment { recording_clip: 0, @@ -1614,6 +1616,7 @@ mod tests { end: 14.0, name: None, speed_audio_mode: None, + audio_muted: false, }, ], transitions: vec![ClipTransition {