Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 121 additions & 20 deletions apps/desktop-gpui/src/editor_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,19 @@ impl ExportResolution {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExportProgress {
Frames(u32, u32),
Normalizing,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportPhase {
Idle,
ChoosingFile,
Starting,
Rendering,
Normalizing,
Copying,
Uploading,
Done,
Expand All @@ -166,7 +173,12 @@ impl ExportPhase {
pub(crate) fn is_busy(self) -> bool {
matches!(
self,
Self::ChoosingFile | Self::Starting | Self::Rendering | Self::Copying | Self::Uploading
Self::ChoosingFile
| Self::Starting
| Self::Rendering
| Self::Normalizing
| Self::Copying
| Self::Uploading
)
}

Expand Down Expand Up @@ -920,7 +932,7 @@ impl EditorWindow {
}

let notify_file_save = save_path.is_some();
let (progress_tx, progress_rx) = flume::unbounded::<(u32, u32)>();
let (progress_tx, progress_rx) = flume::unbounded::<ExportProgress>();
let export_cancel = cancel.clone();
let export = gpui_tokio::Tokio::spawn(cx, async move {
let stopped = export_cancel.clone();
Expand All @@ -943,16 +955,24 @@ impl EditorWindow {
});

loop {
let mut latest: Option<(u32, u32)> = None;
while let Ok((rendered, total)) = progress_rx.try_recv() {
latest = Some((rendered, total));
let mut latest: Option<ExportProgress> = None;
while let Ok(update) = progress_rx.try_recv() {
latest = Some(update);
}
if let Some((rendered, total)) = latest {
if let Some(update) = latest {
let _ = this.update(cx, |this, cx| {
if let Some(ui) = this.export.as_mut() {
ui.rendered = rendered;
ui.total_frames = total;
ui.phase = ExportPhase::Rendering;
match update {
ExportProgress::Frames(rendered, total) => {
ui.rendered = rendered;
ui.total_frames = total;
ui.phase = ExportPhase::Rendering;
}
ExportProgress::Normalizing => {
ui.total_frames = 0;
ui.phase = ExportPhase::Normalizing;
}
}
}
cx.notify();
});
Expand Down Expand Up @@ -1160,7 +1180,7 @@ impl EditorWindow {
cx.notify();
});

let (progress_tx, progress_rx) = flume::unbounded::<(u32, u32)>();
let (progress_tx, progress_rx) = flume::unbounded::<ExportProgress>();
let export_cancel = cancel.clone();
let export_path = project_path.clone();
let export = gpui_tokio::Tokio::spawn(cx, async move {
Expand Down Expand Up @@ -1189,16 +1209,24 @@ impl EditorWindow {
});

loop {
let mut latest: Option<(u32, u32)> = None;
while let Ok((rendered, total)) = progress_rx.try_recv() {
latest = Some((rendered, total));
let mut latest: Option<ExportProgress> = None;
while let Ok(update) = progress_rx.try_recv() {
latest = Some(update);
}
if let Some((rendered, total)) = latest {
if let Some(update) = latest {
let _ = this.update(cx, |this, cx| {
if let Some(ui) = this.export.as_mut() {
ui.rendered = rendered;
ui.total_frames = total;
ui.phase = ExportPhase::Rendering;
match update {
ExportProgress::Frames(rendered, total) => {
ui.rendered = rendered;
ui.total_frames = total;
ui.phase = ExportPhase::Rendering;
}
ExportProgress::Normalizing => {
ui.total_frames = 0;
ui.phase = ExportPhase::Normalizing;
}
}
}
cx.notify();
});
Expand Down Expand Up @@ -2461,6 +2489,7 @@ impl EditorWindow {
ExportPhase::Rendering if ui.cursor_only => "Rendering cursor track",
ExportPhase::Rendering if ui.format == ExportFormatKind::Gif => "Rendering GIF",
ExportPhase::Rendering => "Rendering video",
ExportPhase::Normalizing => "Normalizing audio",
ExportPhase::Copying if ui.destination == ExportDestination::Clipboard => {
"Copying to clipboard"
}
Expand Down Expand Up @@ -2913,12 +2942,83 @@ async fn run_export(
optimize: bool,
force: bool,
save_path: Option<PathBuf>,
progress_tx: flume::Sender<(u32, u32)>,
progress_tx: flume::Sender<ExportProgress>,
cancel: Arc<AtomicBool>,
) -> Result<PathBuf, String> {
if cancel.load(Ordering::Relaxed) {
return Err("Export cancelled".into());
}
let normalize_loudness =
project.audio.normalize_loudness && !cursor_only && format != ExportFormatKind::Gif;
let loudness_progress = progress_tx.clone();
let loudness_cancel = cancel.clone();
let path = render_export(
project_path,
project,
format,
cursor_only,
fps,
width,
height,
compression,
custom_bpp,
optimize,
force,
save_path,
progress_tx,
cancel,
)
.await?;
if normalize_loudness {
let _ = loudness_progress.send(ExportProgress::Normalizing);
let target = path.clone();
let cancel = loudness_cancel.clone();
match tokio::task::spawn_blocking(move || {
cap_export::loudness::normalize_file_loudness(
&target,
cap_export::loudness::SOCIAL_TARGET_LUFS,
&cancel,
)
})
.await
{
Ok(Ok(_)) => {}
// Cancelled mid-pass: the export file is untouched, but the user
// asked to stop, so it ends like any other cancelled export.
Ok(Err(cap_export::loudness::LoudnessError::Cancelled)) => {
let _ = std::fs::remove_file(&path);
return Err("Export cancelled".into());
}
Ok(Err(error)) => tracing::warn!(
%error,
"Loudness normalization failed, keeping the unnormalized export"
),
Err(error) => tracing::warn!(
%error,
"Loudness normalization task failed, keeping the unnormalized export"
),
}
}
Ok(path)
}

#[allow(clippy::too_many_arguments)]
async fn render_export(
project_path: PathBuf,
project: cap_project::ProjectConfiguration,
format: ExportFormatKind,
cursor_only: bool,
fps: u32,
width: u32,
height: u32,
compression: ExportCompression,
custom_bpp: Option<f32>,
optimize: bool,
force: bool,
save_path: Option<PathBuf>,
progress_tx: flume::Sender<ExportProgress>,
cancel: Arc<AtomicBool>,
) -> Result<PathBuf, String> {
let mut builder = ExporterBase::builder(project_path).with_force_ffmpeg_decoder(force);
if cursor_only {
builder = builder.with_config(make_cursor_only_project(project));
Expand Down Expand Up @@ -2948,7 +3048,7 @@ async fn run_export(
let total = base.total_frames(fps);
(PreparedBase::Other(base), total)
};
let _ = progress_tx.send((0, total));
let _ = progress_tx.send(ExportProgress::Frames(0, total));

let progress = {
let progress_tx = progress_tx.clone();
Expand All @@ -2958,7 +3058,7 @@ async fn run_export(
return false;
}
progress_tx
.send(((frame_index + 1).min(total), total))
.send(ExportProgress::Frames((frame_index + 1).min(total), total))
.is_ok()
}
};
Expand Down Expand Up @@ -3201,6 +3301,7 @@ mod tests {
for phase in [
ExportPhase::Starting,
ExportPhase::Rendering,
ExportPhase::Normalizing,
ExportPhase::Copying,
ExportPhase::Uploading,
ExportPhase::Done,
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop-gpui/src/editor_tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,29 @@ impl EditorWindow {
}),
),
))
.child(
ui::Subfield::plain(&theme, "Normalize Loudness").child(
ui::Toggle::plain(
&theme,
"audio-normalize-loudness",
audio.normalize_loudness,
)
.on_click(cx.listener(
|this, _, window, cx| {
let next = !this.project.audio.normalize_loudness;
this.edit_project(
"audio-normalize-loudness",
window,
cx,
move |p| {
p.audio.normalize_loudness = next;
true
},
);
},
)),
),
)
// Only a two-channel microphone gets the stereo row
// (`:709-711`).
.children(stereo_mic.then(|| {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-gpui/src/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ mod tests {
for phase in [
ExportPhase::Starting,
ExportPhase::Rendering,
ExportPhase::Normalizing,
ExportPhase::Copying,
ExportPhase::Uploading,
] {
Expand Down
1 change: 1 addition & 0 deletions crates/export/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod estimates;
pub mod gif;
pub mod loudness;
pub mod mov;
pub mod mp4;
pub mod preview;
Expand Down
Loading
Loading