Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
a2ac6c2
feat(editor): add audio-track data model for external audio import
Beetix Aug 24, 2026
c542fe0
feat(editor): import external audio files as audio-kind assets
Beetix Aug 24, 2026
b47c0c5
feat(editor): add timeline state ops for imported audio tracks
Beetix Aug 24, 2026
b218f4a
feat(editor): import audio onto the timeline with a lane and inspector
Beetix Aug 24, 2026
5c2afad
feat(editor): drag and trim audio tracks on the timeline lane
Beetix Aug 24, 2026
29beb9f
feat(editor): play imported audio tracks in the preview
Beetix Aug 24, 2026
83eb89b
feat(export): mix imported audio tracks into the exported MP4
Beetix Aug 25, 2026
b223380
feat(editor): fold audio import into the single "Import media" button
Beetix Aug 25, 2026
c96afe8
fix(editor): smooth audio-track preview and stop audio becoming a clip
Beetix Aug 25, 2026
b920e02
feat(editor): add audio from the timeline toolbar, not the media tab
Beetix Aug 25, 2026
2688b28
refine(editor): tidy the audio toolbar button and inspector pane
Beetix Aug 25, 2026
3b6a1cc
refine(editor): reposition Add audio and make Delete track span
Beetix Aug 25, 2026
1e78df9
refine(editor): match the audio-track pane to the global Audio pane
Beetix Aug 25, 2026
d62f862
refactor(editor): drop the superseded audio-track move/resize ops
Beetix Aug 25, 2026
3354280
refactor(editor): remove the mute feature from imported audio tracks
Beetix Aug 25, 2026
cc3e709
docs: list audioTracks[] in the document-model top-level shape
Beetix Aug 25, 2026
6a9c32d
fix(editor): address CodeRabbit review on the audio-import PR (#502)
Beetix Aug 25, 2026
462d682
feat(editor): honor imported-track boost in the preview (CodeRabbit #5)
Beetix Aug 25, 2026
f330d33
feat(editor): always show the audio lane + an "Add audio" shortcut (M)
Beetix Aug 25, 2026
3fe1554
fix(editor): sync imported audio tracks to trims in the export (issue…
Beetix Aug 25, 2026
8e18288
fix(editor): address CodeRabbit follow-up on the audio-track PR (#502)
Beetix Aug 26, 2026
59f6036
docs(editor): correct the timelineStartSec comments — the head is raw…
EtienneLescot Aug 27, 2026
3f8ac2a
fix(editor): play imported audio contiguously in preview to match the…
Beetix Aug 28, 2026
cae60a8
fix(ipc): accept audio paths in the generic media reads so imported w…
Beetix Aug 28, 2026
75ddadd
fix(editor): three more preview↔export audio seam bugs
Beetix Aug 28, 2026
030ccb8
fix(editor): four imported-audio state leaks
Beetix Aug 28, 2026
2fadb52
refactor(editor): three imported-audio cleanups
Beetix Aug 28, 2026
657f5d5
feat(audio): clip-anchor timeline audio tracks
olamide226 Aug 31, 2026
1ad8edd
feat(audio): fades, loop and mute on timeline audio tracks
olamide226 Aug 31, 2026
c260113
feat(audio): record voiceovers against the timeline
olamide226 Aug 31, 2026
d4c8d26
fix(audio): unblock voiceover recording and restore the left-edge trim
olamide226 Sep 1, 2026
80c6d38
fix(audio): make the loop toggle reachable, and give voiceover a button
olamide226 Sep 1, 2026
2aaf9a7
test(timeline): pin the voiceover toolbar button
olamide226 Sep 1, 2026
16b0b3f
feat(audio): make loop fill by itself, and mark where it repeats
olamide226 Sep 1, 2026
6aa8167
feat(timeline): one audio button with a named menu, and real tooltips
olamide226 Sep 1, 2026
82bdfb6
fix(ui): forward refs through the Popover and Tooltip wrappers
olamide226 Sep 1, 2026
90484ec
feat(timeline): stack overlapping audio tracks on their own rows
olamide226 Sep 1, 2026
a4b2646
fix(audio): silence the timeline's own tracks while recording a take
olamide226 Sep 1, 2026
eeecb15
feat(audio): dock the voiceover recorder instead of covering the video
olamide226 Sep 1, 2026
24f736e
fix(audio): stop tracks inside a trimmed stretch from playing
olamide226 Sep 1, 2026
1356244
fix(audio): keep tracks at 1x and in place under speed regions
olamide226 Sep 1, 2026
c6b328c
perf(transcription): extract audio natively, off the UI thread
EtienneLescot Sep 2, 2026
2be8054
fix(timeline): let the keyboard activate a lane pill, not just the po…
EtienneLescot Aug 31, 2026
80e716d
fix(transcription): stop the background pass transcribing music
EtienneLescot Sep 2, 2026
00c1ce7
feat(ai): let the agent see and place audio tracks
EtienneLescot Sep 2, 2026
49d0ac5
feat(timeline): show what an audio pill crops, and let it slip
EtienneLescot Sep 2, 2026
b9224ec
fix(timeline): say that Alt slips, instead of leaving it to be discov…
EtienneLescot Sep 2, 2026
c183b17
fix(timeline): put the slip gesture where it is read without hovering
EtienneLescot Sep 2, 2026
2a9290f
feat(editor): fold captions into the transcript tab
EtienneLescot Sep 2, 2026
8ce6f2e
feat(editor): read the transcript from the voiceover, not just the film
EtienneLescot Sep 2, 2026
1a69314
feat(document): add immutable transcript word edits
sunyuchenyaobo Aug 30, 2026
097684e
fix(document): validate referenced word ownership and join CJK indepe…
sunyuchenyaobo Aug 30, 2026
bfa1dc9
fix(document): read CJK segment edges by code point for non-BMP Han
sunyuchenyaobo Aug 30, 2026
1cc0f55
feat(document): make a corrected word survive its re-transcription
EtienneLescot Aug 31, 2026
3f45eb3
feat(editor): correct a word in the transcript without cutting the film
EtienneLescot Aug 31, 2026
5dcfa6e
feat(editor): add words to the transcript that nobody said
EtienneLescot Aug 31, 2026
4860af4
feat(editor): an added word buys itself time, and the film holds its …
EtienneLescot Aug 31, 2026
d829afc
revert(editor): an added word no longer splits the clip it lands in
EtienneLescot Aug 31, 2026
888f5f1
feat(timeline): mark where words were added, without storing anything…
EtienneLescot Aug 31, 2026
b2fce8c
feat(ai): let the chat correct a word it heard wrong
EtienneLescot Aug 31, 2026
90280da
feat(document): store the pause an added word needs, as a region
EtienneLescot Sep 1, 2026
a8faaad
feat(editor): the readers count the pause an added word bought
EtienneLescot Sep 1, 2026
1dbf94a
feat(timeline): one answer to whether a raw moment is in the film
EtienneLescot Sep 2, 2026
ca66c1d
feat(editor): decide a word by the ruler, so both lanes agree
EtienneLescot Sep 2, 2026
c1633cd
feat(audio): a cut under a voiceover takes the words, not the take's …
EtienneLescot Sep 2, 2026
ffde9fb
feat(editor): a transcript cut names a moment, not an owner
EtienneLescot Sep 2, 2026
3a9b74f
feat(captions): the lane is a document fact, and the captions follow it
EtienneLescot Sep 3, 2026
521d0bd
feat(audio): one voiceover row, one music row, through a single door
EtienneLescot Sep 3, 2026
90d80b8
fix(document): sweep the trims that struck words through without cutting
EtienneLescot Sep 3, 2026
e5ba058
feat(timeline): tell the two insertion lanes apart, and pin the inert…
EtienneLescot Sep 3, 2026
889751b
feat(timeline): one walk over a take, losing time to a cut and gainin…
EtienneLescot Sep 3, 2026
733e515
fix(timeline): the projection counts the pauses, so audio stops landi…
EtienneLescot Sep 3, 2026
8af1b21
fix(export): a pause reaches the compositor, instead of exporting as …
EtienneLescot Sep 3, 2026
1f9801c
feat(audio): the export and the transcript read the take's walk
EtienneLescot Sep 3, 2026
6f45416
feat(preview): the voice parks with the walk, and every take plays once
EtienneLescot Sep 3, 2026
2536a59
feat(editor): add a word on either lane, and draw the time it buys
EtienneLescot Sep 3, 2026
f74ac54
feat(editor): cut the notch into the take, inside one outline
EtienneLescot Sep 3, 2026
47b3fd5
fix(timeline): a scrub names a moment on the ruler, not on the tape
EtienneLescot Sep 3, 2026
8adca43
fix(preview): the picture spends the pause a word bought
EtienneLescot Sep 3, 2026
d2c4740
fix(preview): an insertion is media that plays, not a pause
EtienneLescot Sep 3, 2026
f58a802
fix(preview): park the picture on the insertion, don't re-seek it
EtienneLescot Sep 3, 2026
8866d05
fix(timeline): the scrub release carries its ruler second too
EtienneLescot Sep 3, 2026
156b87b
refactor(timeline): one clock — an insertion is media, so the clip is…
EtienneLescot Sep 3, 2026
eddcbe2
fix(captions): place a cue past an insertion on the source it names
EtienneLescot Sep 3, 2026
ed8216b
fix(document): reconcile clip geometry with the insertions on every load
EtienneLescot Sep 4, 2026
6d5a60f
fix(captions): an added word is subtitled over the media it inserted
EtienneLescot Sep 4, 2026
d610e73
fix(timeline): the insertions are a required argument, and the compil…
EtienneLescot Sep 4, 2026
f4bdb25
fix(transcript): the pane follows the voice across an insertion, and …
EtienneLescot Sep 4, 2026
42d9fd7
fix(captions): an added word and the word after it share a second — o…
EtienneLescot Sep 4, 2026
023f414
refactor(timeline): delete the duplicate answer, route the rest throu…
EtienneLescot Sep 4, 2026
913deb8
revert(insertions): remove the added-word machinery, keep the gesture
EtienneLescot Sep 4, 2026
54ea8f6
feat(media): generate the media an added word is spoken over
EtienneLescot Sep 4, 2026
612e1c6
feat(timeline): the insertion layer — a clip reads a list of parts, n…
EtienneLescot Sep 4, 2026
c9b261f
feat(timeline): the one funnel reads parts, so an extension is a segm…
EtienneLescot Sep 4, 2026
09fa94b
feat(document): the clip grows with the word, at the one funnel every…
EtienneLescot Sep 4, 2026
c67fa22
feat(media): an extension resolves to a file, and the save generates it
EtienneLescot Sep 4, 2026
9a12a27
feat(preview): the DOM preview plays an extension, because it is hand…
EtienneLescot Sep 4, 2026
7622016
fix(transcript): the conversion source second -> ruler second lives i…
EtienneLescot Sep 4, 2026
63f16b7
fix(insertions): an extension is a clip, because that is the only sha…
EtienneLescot Sep 4, 2026
e2c1147
feat(insertions): an insertion is a clip, stored as one
EtienneLescot Sep 4, 2026
10a2bb9
refactor(timeline): the seam a clip leaves closes only under generate…
EtienneLescot Sep 4, 2026
cd6a706
refactor(timeline): two clips whose media continues across the join a…
EtienneLescot Sep 4, 2026
0b7152a
fix(insertions): editing an insertion's text resizes the clip it plays
EtienneLescot Sep 4, 2026
f57326c
test(insertions): shortening an insertion is a second claim, pinned s…
EtienneLescot Sep 4, 2026
c8c8b7c
feat(insertions): an insertion in a voice-over is a track fragment
EtienneLescot Sep 4, 2026
e91bd14
refactor(voiceover): the take walk is a subtraction again
EtienneLescot Sep 4, 2026
3fa5aa7
fix(insertions): a release build cannot retype an inserted word either
EtienneLescot Sep 4, 2026
79cc55d
docs(insertions): the gate is the runtime refusal, not the dead-code …
EtienneLescot Sep 4, 2026
c3c25ab
refactor(insertions): one flag for the dev gate, read where the gestu…
EtienneLescot Sep 4, 2026
c991976
fix(ai): the chat cannot rewrite a word nobody said
EtienneLescot Sep 4, 2026
c748ebc
fix: the six CodeRabbit findings that were still standing and cheap
EtienneLescot Sep 4, 2026
8fb74f9
fix: the four CodeRabbit findings that were left, including the read …
EtienneLescot Sep 4, 2026
8d9411e
fix(rebase): restore what replaying 112 commits dropped
EtienneLescot Sep 4, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ workbench/fixtures/
/aur_ci
/aur_ci.pub
/aur_known_hosts
tmp_handoff.md
330 changes: 329 additions & 1 deletion crates/compositor/src/audio.rs

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions crates/compositor/src/pipeline_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use std::ffi::CString;
use std::ptr;

use crate::audio::{
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio,
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio, mix_external_tracks,
AacEncoder, PlanarPcm,
};
use crate::audio_jobs::{decode_and_stretch_clip_audio, ClipAudioJobs};
Expand Down Expand Up @@ -1002,6 +1002,12 @@ pub fn run_composited_multi(

let scene = comp.scene_snapshot();
let audio_settings = scene.as_ref().map(|scene| scene.audio).unwrap_or_default();
// Imported audio tracks (issue #350), cloned out of the borrowed scene so the
// mix step below owns them. Empty for a project with no imported audio.
let audio_tracks = scene
.as_ref()
.map(|scene| scene.audio_tracks.clone())
.unwrap_or_default();
// Ring de staging a 2 : l'export ne veut que du debit, une frame de latence
// ne se voit pas dans un fichier. Voir `Compositor::set_readback_depth` pour
// la raison pour laquelle la preview, elle, reste a 1.
Expand Down Expand Up @@ -1190,7 +1196,10 @@ pub fn run_composited_multi(
let plan = build_audio_concat_plan(&clip_frame_counts, &declared_audio, out_fps as f64);
let octx = mux.octx;
mux.aac.encode(
&finish_audio(assemble_concatenated_pcm(&clip_pcm, &plan), audio_settings),
&finish_audio(
mix_external_tracks(assemble_concatenated_pcm(&clip_pcm, &plan), &audio_tracks),
audio_settings,
),
octx,
)?;
mux.finish()?;
Expand Down
12 changes: 10 additions & 2 deletions crates/compositor/src/pipeline_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
//! décodeurs, symétrique.

use crate::audio::{
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio,
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio, mix_external_tracks,
AacEncoder, PlanarPcm,
};
use crate::audio_jobs::{decode_and_stretch_clip_audio, ClipAudioJobs};
Expand Down Expand Up @@ -1172,6 +1172,11 @@ pub fn run_composited_multi(
// raconte avoir déjà coûté une fois.
let scene = comp.scene_snapshot();
let audio_settings = scene.as_ref().map(|scene| scene.audio).unwrap_or_default();
// Imported audio tracks (issue #350), cloned out of the borrowed scene.
let audio_tracks = scene
.as_ref()
.map(|scene| scene.audio_tracks.clone())
.unwrap_or_default();
frames = unsafe {
crate::timeline_walk::walk_composited_timeline(
clips,
Expand Down Expand Up @@ -1247,7 +1252,10 @@ pub fn run_composited_multi(
let declared_audio: Vec<bool> = clips.iter().map(|clip| clip.has_audio).collect();
let plan = build_audio_concat_plan(&clip_frame_counts, &declared_audio, out_fps as f64);
audio_encoder.encode(
&finish_audio(assemble_concatenated_pcm(&clip_pcm, &plan), audio_settings),
&finish_audio(
mix_external_tracks(assemble_concatenated_pcm(&clip_pcm, &plan), &audio_tracks),
audio_settings,
),
octx,
)?;

Expand Down
9 changes: 7 additions & 2 deletions crates/compositor/src/pipeline_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! tout le run, deux lectures seulement. Rien dans la boucle ne peut fausser le fps.

use crate::audio::{
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio,
assemble_concatenated_pcm, build_audio_concat_plan, finish_audio, mix_external_tracks,
AacEncoder, PlanarPcm,
};
use crate::audio_jobs::{decode_and_stretch_clip_audio, ClipAudioJobs};
Expand Down Expand Up @@ -1351,6 +1351,11 @@ unsafe fn run_multi_inner(
// fenêtrage par clip ; `walk_composited_timeline` s'en charge.
let scene = comp.scene_snapshot();
let audio_settings = scene.as_ref().map(|scene| scene.audio).unwrap_or_default();
// Imported audio tracks (issue #350), cloned out of the borrowed scene.
let audio_tracks = scene
.as_ref()
.map(|scene| scene.audio_tracks.clone())
.unwrap_or_default();

// ---- encodeur (choisi à l'exécution, cf. ExportCodec::candidates) + mux ----
// Backend CPU : pas de pool D3D11 du tout. `av_hwdevice_ctx_init(D3D11VA)` échoue sur
Expand Down Expand Up @@ -1484,7 +1489,7 @@ unsafe fn run_multi_inner(
out_fps as f64,
);
let assembled_audio = finish_audio(
assemble_concatenated_pcm(&clip_pcm, &audio_plan),
mix_external_tracks(assemble_concatenated_pcm(&clip_pcm, &audio_plan), &audio_tracks),
audio_settings,
);
audio_encoder.encode(&assembled_audio, octx)?;
Expand Down
40 changes: 40 additions & 0 deletions crates/compositor/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,39 @@ pub struct SceneAudio {
pub gain_db: f32,
}

/// One imported audio track (issue #350) mixed over the assembled programme —
/// voiceover / BGM / SFX. Deliberately a SEPARATE `Scene` field rather than a
/// member of `SceneAudio`, so `SceneAudio` stays `Copy` and the pipelines keep
/// copying it out of a borrow unchanged.
///
/// `start_sec` is the track's head on the OUTPUT programme; `trim_start_sec` /
/// `trim_end_sec` window the source file (both source seconds). The renderer
/// resolves `start_sec` from the track's raw timeline position — equal to it when
/// the project has no trims/speed, which is the case this first cut mixes exactly.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SceneAudioTrack {
pub path: String,
#[serde(default)]
pub start_sec: f64,
#[serde(default)]
pub gain_db: f32,
#[serde(default)]
pub trim_start_sec: f64,
#[serde(default)]
pub trim_end_sec: Option<f64>,
/// Ramp lengths at this entry's own edges, in seconds. The app puts them only
/// on the pieces that touch the track's real start and end, so a split or
/// looping track fades once instead of at every cut or repeat.
///
/// `#[serde(default)]` for the usual reason: a payload from a build that
/// predates the field must degrade to "no fade", not fail the whole scene.
#[serde(default)]
pub fade_in_sec: f64,
#[serde(default)]
pub fade_out_sec: f64,
}

#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SceneOutput {
Expand Down Expand Up @@ -500,6 +533,10 @@ pub struct Scene {
/// Global audio finishing. Default keeps old scene payloads bit-for-bit compatible.
#[serde(default)]
pub audio: SceneAudio,
/// Imported audio tracks mixed over the programme (issue #350). `#[serde(default)]`:
/// absent from every scene written before this, and from a project with none.
#[serde(default)]
pub audio_tracks: Vec<SceneAudioTrack>,
/// Crop écran par clip, dans le même ordre que `clips` (`cropByClip` côté TS).
#[serde(default)]
pub crop_by_clip: Vec<Option<SceneCrop>>,
Expand Down Expand Up @@ -576,6 +613,9 @@ impl Scene {
mod tests {
use super::*;

/// lui. Sans ce défaut, ouvrir un projet fait par une version antérieure échouerait au
/// parse au lieu de simplement ne rien tenir (issue #560).

#[test]
fn parses_a_minimal_scene_json() {
let json = r##"{
Expand Down
2 changes: 2 additions & 0 deletions crates/compositor/src/timeline_walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::compositor::Compositor;
use crate::config::Cfg;
use crate::cursor::CursorTrack;
use crate::d3d::Gpu;
use crate::ffi::AVFrame;
use crate::frame_geometry::webcam_is_real;
use crate::pipeline::{ClipSource, Decoder};
use crate::regions::{speed_segments_for_window, SpeedSegment};
Expand Down Expand Up @@ -336,6 +337,7 @@ pub(crate) unsafe fn walk_composited_timeline(
frames += 1;
}
}

on_clip_end(
clip_index,
source_end_sec,
Expand Down
2 changes: 2 additions & 0 deletions crates/poc-d3d/src/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ fn run_bench(args: &[String]) -> Result<()> {
source_end_sec: 6.0, // la fixture entière (§ fixture.json : 6 s, 360 frames)
webcam_offset_sec: 0.0,
has_audio: false,
hold_sec: 0.0,
};
let path = format!("{out}/{}_{:?}.mp4", cfg.name, backend).to_lowercase();
let s = pipeline::run_composited_multi(
Expand Down Expand Up @@ -292,6 +293,7 @@ fn run_gif_bench(
source_end_sec: f64::MAX,
webcam_offset_sec: 0.0,
has_audio: false,
hold_sec: 0.0,
}];
for r in 0..repeat {
// Each run writes to the same path — the last frame wins. The
Expand Down
Loading
Loading