diff --git a/crates/interchange/src/fcp7.rs b/crates/interchange/src/fcp7.rs
index 962508a5..e0e9075d 100644
--- a/crates/interchange/src/fcp7.rs
+++ b/crates/interchange/src/fcp7.rs
@@ -517,6 +517,14 @@ impl<'a, 'i> Imp<'a, 'i, '_> {
&& let Ok(ch) = u16::try_from(idx - 1)
{
ti.source_channels = vec![ch];
+ // The two halves of a split stereo file play on their own side, as the stereo clip did (#371):
+ // a mono clip is centred (both sides at unity), so the halves would otherwise sum on both
+ // sides, +3 dB for uncorrelated channels, +6 dB for dual mono. An `audiopan` filter
+ // (read below) still overrides this.
+ let stereo = matches!(&self.b.p.item(item).map(|i| &i.kind), Some(ItemKind::Media(m)) if m.info.audio().is_some_and(|a| a.channels == 2));
+ if stereo && ch <= 1 {
+ set_param(&mut ti, "panner", "balance", Param::new(ParamValue::Float(if ch == 0 { -100.0 } else { 100.0 })));
+ }
}
if let Some(mode) = child_text(c, "compositemode") {
match BLEND_XML.iter().find(|(n, _)| n.eq_ignore_ascii_case(mode)) {
@@ -1212,7 +1220,9 @@ impl Exp<'_, '_> {
self.float_param(c, "volume", "level", "Level", 0.0, 3.98109, rate, move |v| db_to_gain(v + g), "level");
self.close_effect();
}
- if param_modified(c, "panner", "balance") {
+ // A one-channel clip says where it is panned even when centred: without Audio Pan the
+ // importer pans the halves of a split stereo file to their sides (#371).
+ if param_modified(c, "panner", "balance") || c.source_channels.len() == 1 {
self.open_effect("Audio Pan", "audiopan", "audiopan", "audiopan", "audio", true);
self.float_param(c, "panner", "balance", "Pan", -1.0, 1.0, rate, |v| v / 100.0, "pan");
self.close_effect();
diff --git a/crates/interchange/tests/fcp7.rs b/crates/interchange/tests/fcp7.rs
index 6b2a9414..aad1d5f0 100644
--- a/crates/interchange/tests/fcp7.rs
+++ b/crates/interchange/tests/fcp7.rs
@@ -3,7 +3,7 @@
mod support;
-use filmcraft_interchange::{ExportOptions, Format, detect, export, import};
+use filmcraft_interchange::{ExportOptions, Format, Imported, detect, export, import};
use filmcraft_media::Generator;
use filmcraft_project::{ItemKind, Keyframe, Label, Marker, MarkerId, MarkerKind, MediaRef, Param, ParamValue, Project, TrackKind, TransitionAlign};
use filmcraft_time::{FrameRate, Tick};
@@ -600,3 +600,66 @@ fn audio_trackindex_1_is_channel_0_only_when_the_file_is_split() {
assert!(a1[1].source_channels.is_empty(), "{:?}", a1[1].source_channels);
assert_eq!(seq.tracks(TrackKind::Audio)[1].items[0].source_channels, vec![1u16]);
}
+
+/// A Premiere-style stereo file split onto two mono tracks (A1 = `trackindex` 1, A2 = 2).
+fn split_stereo_doc(pan_a1: &str) -> String {
+ format!(
+ r#"
+
+S48{RATE}
+
+"#
+ )
+}
+
+fn balances(imp: &Imported) -> Vec {
+ let seq = imp.project.sequence(only_seq(imp)).unwrap();
+ seq.tracks(TrackKind::Audio)
+ .iter()
+ .flat_map(|t| &t.items)
+ .map(|i| i.effect("panner").and_then(|e| e.param("balance")).and_then(|p| p.value.as_f64()).unwrap_or(0.0))
+ .collect()
+}
+
+/// #371: the halves of a split stereo file played centred, so both channels summed on both sides
+/// (+3 dB for a stereo effect, +6 dB for dual-mono dialogue). They now play on their own side.
+#[test]
+fn split_stereo_halves_are_panned_to_their_side() {
+ let (imp, _) = import(split_stereo_doc("").as_bytes(), Format::Fcp7Xml, None).unwrap();
+ assert_eq!(balances(&imp), vec![-100.0, 100.0]);
+}
+
+/// An Audio Pan filter on a split half still wins.
+#[test]
+fn split_stereo_half_keeps_its_audio_pan() {
+ let pan = r#"Audio Panaudiopanaudiopanaudiopanaudio
+ panPan-110"#;
+ let (imp, _) = import(split_stereo_doc(pan).as_bytes(), Format::Fcp7Xml, None).unwrap();
+ assert_eq!(balances(&imp), vec![0.0, 100.0]);
+}
+
+/// A centred one-channel clip (Breakout to Mono) round-trips centred: export writes its Audio Pan.
+#[test]
+fn centred_mono_breakout_round_trips_centred() {
+ let r = FrameRate::FPS_23_976;
+ let mut p = Project::new("P");
+ let a = media(&mut p, "/m/a.wav", false, true, r);
+ let s = sequence(&mut p, "t", r, false);
+ let left = clip(&mut p, s, TrackKind::Audio, 0, a, 0, 24, 0);
+ let right = clip(&mut p, s, TrackKind::Audio, 1, a, 0, 24, 0);
+ for (id, ch) in [(left, 0u16), (right, 1u16)] {
+ let (_, it) = p.sequence_mut(s).unwrap().find_item_mut(id).unwrap();
+ it.source_channels = vec![ch];
+ }
+ let (imp, xml, _) = roundtrip(&p, s, Format::Fcp7Xml, &ExportOptions::default());
+ assert_eq!(balances(&imp), vec![0.0, 0.0], "{xml}");
+}