#1417: WAV markers show on the audio layer

A WAV's markers were never read, so its audio layer showed none. The importer
now reads the file's cue points (`cue ` chunk named by `LIST/adtl` `labl` and
`note`, sized by `ltxt`) and Adobe's XMP markers (`_PMX` chunk, `xmpDM:markers`
entries as elements or attributes, in frames of the track's `xmpDM:frameRate`).
A marker present in both encodings is listed once. The reader is bounds-checked
and caps the marker count and XMP size.

- `Footage.markers` (optional in the file format) holds them at footage time; a
  layer made from the footage starts with them.
- Layer ▸ Markers ▸ Update Markers From Source also works on footage layers.
- Markers from a file have no label colour.

Tests: reader unit tests (cue, note, ltxt, both XMP forms, both encodings,
truncated and hostile files), a probe test, and an end-to-end test through the
fully wired session. Not checked against a WAV written by Adobe's tools.

Claude-Session: https://claude.ai/code/session_013fTnVxnXTv2MBW6uj2PPyU
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-18 17:38:44 +02:00
committed by Kristian Myram
parent f3f24ef189
commit bccbc666a2
9 changed files with 455 additions and 6 deletions
+10 -5
View File
@@ -296,8 +296,9 @@ fn convert(s: &mut Session, p: &Value) -> Result<Value> {
Ok(Value::Null)
}
/// Layer ▸ Markers ▸ Update Markers From Source: copy a precomp's composition markers onto the
/// layer (layer time is the nested comp's time), skipping ones already there.
/// Layer ▸ Markers ▸ Update Markers From Source: copy a precomp's composition markers, or the
/// markers a footage file carries (a WAV's cue points), onto the layer (layer time is the source's
/// time), skipping ones already there.
fn update_from_source(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
let src: Vec<(LayerId, Vec<Marker>)> = {
@@ -307,13 +308,17 @@ fn update_from_source(s: &mut Session, p: &Value) -> Result<Value> {
let l = comp.layer(*id)?;
match &l.source {
LayerSource::Comp { item } => Some((*id, s.project.comp(*item)?.markers.clone())),
LayerSource::Footage { item } => match &s.project.item(*item)?.kind {
effectcraft_project::ItemKind::Footage(f) => Some((*id, f.markers.clone())),
_ => None,
},
_ => None,
}
})
.collect()
};
if src.is_empty() {
return Err(bad("layer.updateMarkersFromSource", "select a precomposition layer"));
return Err(bad("layer.updateMarkersFromSource", "select a precomposition or footage layer"));
}
let n = s.edit("Update Markers From Source", None, |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
@@ -339,10 +344,10 @@ fn update_from_source(s: &mut Session, p: &Value) -> Result<Value> {
fn has_precomp_layer(s: &Session) -> std::result::Result<(), String> {
has_layers(s)?;
let c = s.active_comp().ok_or("no composition")?;
if s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| matches!(l.source, LayerSource::Comp { .. })) {
if s.state.selected_layers.iter().filter_map(|l| c.layer(*l)).any(|l| matches!(l.source, LayerSource::Comp { .. } | LayerSource::Footage { .. })) {
Ok(())
} else {
Err("select a precomposition layer".into())
Err("select a precomposition or footage layer".into())
}
}