diff --git a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py index 45cd574..e4986e8 100644 --- a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py +++ b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py @@ -762,20 +762,37 @@ def aaf_sourceclip(self, otio_clip): "LinearInterp", "LinearInterp") self.aaf_file.dictionary.register_def(interp_def) - # PointList - length = int(otio_clip.duration().value) - c1 = self.aaf_file.create.ControlPoint() - c1["ControlPointSource"].value = 2 - c1["Time"].value = aaf2.rational.AAFRational(f"0/{length}") - c1["Value"].value = 0 - c2 = self.aaf_file.create.ControlPoint() - c2["ControlPointSource"].value = 2 - c2["Time"].value = aaf2.rational.AAFRational(f"{length - 1}/{length}") - c2["Value"].value = 0 + + # generate PointList for pan varying_value = self.aaf_file.create.VaryingValue() varying_value.parameterdef = param_def varying_value["Interpolation"].value = interp_def - varying_value["PointList"].extend([c1, c2]) + + length = int(otio_clip.duration().value) + + # default pan points are mid pan + default_points = [ + { + "ControlPointSource": 2, + "Time": f"0/{length}", + "Value": "1/2", + }, + { + "ControlPointSource": 2, + "Time": f"{length - 1}/{length}", + "Value": "1/2", + } + ] + cp_dict_list = otio_clip.metadata.get("AAF", {}).get("Pan", {}).get( + "ControlPoints", default_points) + + for cp_dict in cp_dict_list: + point = self.aaf_file.create.ControlPoint() + point["Time"].value = aaf2.rational.AAFRational(cp_dict["Time"]) + point["Value"].value = aaf2.rational.AAFRational(cp_dict["Value"]) + point["ControlPointSource"].value = cp_dict["ControlPointSource"] + varying_value["PointList"].append(point) + opgroup = self.timeline_mobslot.segment opgroup.parameters.append(varying_value) diff --git a/tests/test_aaf_adapter.py b/tests/test_aaf_adapter.py index 4d3bb3b..3192a5e 100644 --- a/tests/test_aaf_adapter.py +++ b/tests/test_aaf_adapter.py @@ -1937,6 +1937,76 @@ def test_aaf_writer_global_start_time(self): self._verify_aaf(tmp_aaf_path) + def test_aaf_writer_audio_pan(self): + """Test Clip with custom audio pan values""" + tl = otio.schema.Timeline() + + # Add an audio clip with pan metadata + clip = otio.schema.Clip( + name="Panned Audio Clip", + metadata={}, + source_range=otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime(0, 24), + duration=otio.opentime.RationalTime(100, 24), + ) + ) + clip.media_reference = otio.schema.MissingReference( + available_range=otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime(0, 24), + duration=otio.opentime.RationalTime(100, 24), + )) + + # Add pan metadata + clip.metadata["AAF"] = { + "Pan": { + "ControlPoints": [ + { + "ControlPointSource": 2, + "Time": "0", + "Value": "0", + }, + { + "ControlPointSource": 2, + "Time": "100", + "Value": "1", + } + ] + }, + "SourceID": "urn:smpte:umid:060a2b34.01010101.01010f00." + "13000000.060e2b34.7f7f2a80.5c9e6a3b.ace913a2" + } + + tl.tracks.append( + otio.schema.Track(children=[clip], kind=otio.schema.TrackKind.Audio) + ) + + _, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf') + otio.adapters.write_to_file(tl, tmp_aaf_path) + print(tmp_aaf_path) + + # verify pan values in AAF file + with aaf2.open(tmp_aaf_path) as aaf_file: + mob = next(aaf_file.content.compositionmobs()) + slot = mob.slots[0] + parameter = list(slot.segment.parameters)[0] + + # extract the pan values + param_dicts = [ + {k: v.value for k, v in dict(p).items()} + for p in parameter.pointlist + ] + + expected = [ + {'ControlPointSource': 2, + 'Time': aaf2.rational.AAFRational(0, 1), + 'Value': aaf2.rational.AAFRational(0, 1)}, + {'ControlPointSource': 2, + 'Time': aaf2.rational.AAFRational(100, 1), + 'Value': aaf2.rational.AAFRational(1, 1)} + ] + + self.assertEqual(param_dicts, expected) + def _verify_aaf(self, aaf_path): otio_timeline = otio.adapters.read_from_file(aaf_path, simplify=True) fd, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf')