@@ -906,3 +906,162 @@ def test_piece_serialization_reconnects_groups_and_fixes_slide():
906906 assert reconstructed .trajectories [0 ] is clone .phrases [0 ].trajectory_grid [0 ][0 ]
907907 assert reconstructed .trajectories [1 ] is clone .phrases [0 ].trajectory_grid [0 ][1 ]
908908 assert clone .phrases [0 ].trajectory_grid [0 ][0 ].articulations ['0.00' ].name == 'pluck'
909+
910+
911+ # ----------------------------------------------------------------------
912+ # Track Titles Tests (Issue #44)
913+ # ----------------------------------------------------------------------
914+
915+ def test_track_titles_default_initialization ():
916+ """Test that trackTitles defaults to empty strings matching instrumentation length."""
917+ raga = Raga ()
918+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
919+
920+ # Single instrument
921+ piece = Piece ({
922+ 'phrases' : [phrase ],
923+ 'instrumentation' : [Instrument .Sitar ],
924+ 'raga' : raga
925+ })
926+ assert piece .track_titles == ['' ]
927+
928+ # Multiple instruments
929+ piece_multi = Piece ({
930+ 'phraseGrid' : [[phrase ], [phrase ]],
931+ 'instrumentation' : [Instrument .Sitar , Instrument .Vocal_M ],
932+ 'raga' : raga
933+ })
934+ assert piece_multi .track_titles == ['' , '' ]
935+
936+
937+ def test_track_titles_explicit_values ():
938+ """Test trackTitles with explicit values provided."""
939+ raga = Raga ()
940+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
941+
942+ piece = Piece ({
943+ 'phraseGrid' : [[phrase ], [phrase ], [phrase ]],
944+ 'instrumentation' : [Instrument .Sarangi , Instrument .Sarangi , Instrument .Sarangi ],
945+ 'trackTitles' : ['Lead Melody' , 'Harmony' , 'Drone' ],
946+ 'raga' : raga
947+ })
948+ assert piece .track_titles == ['Lead Melody' , 'Harmony' , 'Drone' ]
949+
950+
951+ def test_track_titles_length_synchronization_shorter ():
952+ """Test that shorter trackTitles array is padded with empty strings."""
953+ raga = Raga ()
954+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
955+
956+ piece = Piece ({
957+ 'phraseGrid' : [[phrase ], [phrase ], [phrase ]],
958+ 'instrumentation' : [Instrument .Sarangi , Instrument .Sarangi , Instrument .Sarangi ],
959+ 'trackTitles' : ['Lead' ],
960+ 'raga' : raga
961+ })
962+ assert len (piece .track_titles ) == 3
963+ assert piece .track_titles == ['Lead' , '' , '' ]
964+
965+
966+ def test_track_titles_length_synchronization_longer ():
967+ """Test that longer trackTitles array is truncated to match instrumentation."""
968+ raga = Raga ()
969+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
970+
971+ piece = Piece ({
972+ 'phrases' : [phrase ],
973+ 'instrumentation' : [Instrument .Sitar ],
974+ 'trackTitles' : ['Main' , 'Extra' , 'Another' ],
975+ 'raga' : raga
976+ })
977+ assert len (piece .track_titles ) == 1
978+ assert piece .track_titles == ['Main' ]
979+
980+
981+ def test_track_titles_type_validation_not_list ():
982+ """Test that non-list trackTitles raises TypeError."""
983+ raga = Raga ()
984+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
985+
986+ with pytest .raises (TypeError , match = "Parameter 'trackTitles' must be a list" ):
987+ Piece ({
988+ 'phrases' : [phrase ],
989+ 'instrumentation' : [Instrument .Sitar ],
990+ 'trackTitles' : 'not a list' ,
991+ 'raga' : raga
992+ })
993+
994+
995+ def test_track_titles_type_validation_non_string_items ():
996+ """Test that trackTitles with non-string items raises TypeError."""
997+ raga = Raga ()
998+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
999+
1000+ with pytest .raises (TypeError , match = "All items in 'trackTitles' must be strings" ):
1001+ Piece ({
1002+ 'phrases' : [phrase ],
1003+ 'instrumentation' : [Instrument .Sitar ],
1004+ 'trackTitles' : [123 ],
1005+ 'raga' : raga
1006+ })
1007+
1008+
1009+ def test_track_titles_serialization_round_trip ():
1010+ """Test that trackTitles survives serialization and deserialization."""
1011+ raga = Raga ()
1012+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
1013+
1014+ piece = Piece ({
1015+ 'phraseGrid' : [[phrase ], [phrase ]],
1016+ 'instrumentation' : [Instrument .Sitar , Instrument .Sarangi ],
1017+ 'trackTitles' : ['Melody' , 'Harmony' ],
1018+ 'raga' : raga
1019+ })
1020+
1021+ # Serialize and deserialize
1022+ json_obj = piece .to_json ()
1023+ assert 'trackTitles' in json_obj
1024+ assert json_obj ['trackTitles' ] == ['Melody' , 'Harmony' ]
1025+
1026+ copy = Piece .from_json (json_obj )
1027+ assert copy .track_titles == ['Melody' , 'Harmony' ]
1028+
1029+ # Round trip again
1030+ assert copy .to_json ()['trackTitles' ] == piece .to_json ()['trackTitles' ]
1031+
1032+
1033+ def test_track_titles_empty_string_values ():
1034+ """Test that empty strings are valid trackTitles values."""
1035+ raga = Raga ()
1036+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
1037+
1038+ piece = Piece ({
1039+ 'phraseGrid' : [[phrase ], [phrase ]],
1040+ 'instrumentation' : [Instrument .Sitar , Instrument .Vocal_M ],
1041+ 'trackTitles' : ['' , '' ],
1042+ 'raga' : raga
1043+ })
1044+ assert piece .track_titles == ['' , '' ]
1045+
1046+
1047+ def test_track_titles_sarangi_trio_use_case ():
1048+ """Test the sarangi trio use case from the issue."""
1049+ raga = Raga ()
1050+ phrase = Phrase ({'trajectories' : [Trajectory ({'dur_tot' : 1 })], 'raga' : raga })
1051+
1052+ piece = Piece ({
1053+ 'phraseGrid' : [[phrase ], [phrase ], [phrase ]],
1054+ 'instrumentation' : [Instrument .Sarangi , Instrument .Sarangi , Instrument .Sarangi ],
1055+ 'trackTitles' : ['Lead Melody' , 'Harmony' , 'Drone' ],
1056+ 'raga' : raga
1057+ })
1058+
1059+ assert len (piece .track_titles ) == len (piece .instrumentation )
1060+ assert piece .track_titles [0 ] == 'Lead Melody'
1061+ assert piece .track_titles [1 ] == 'Harmony'
1062+ assert piece .track_titles [2 ] == 'Drone'
1063+
1064+ # Verify serialization preserves the titles
1065+ json_obj = piece .to_json ()
1066+ copy = Piece .from_json (json_obj )
1067+ assert copy .track_titles == piece .track_titles
0 commit comments