@@ -120,6 +120,7 @@ def __init__(self, options: Optional[dict] = None) -> None:
120120 Instrument .Sitar : list (range (14 )),
121121 Instrument .Vocal_M : [0 , 1 , 2 , 3 , 4 , 5 , 6 , 12 , 13 ],
122122 Instrument .Vocal_F : [0 , 1 , 2 , 3 , 4 , 5 , 6 , 12 , 13 ],
123+ Instrument .Sarangi : list (range (14 )),
123124 }
124125
125126 first_inst = self .instrumentation [0 ]
@@ -169,29 +170,33 @@ def __init__(self, options: Optional[dict] = None) -> None:
169170 else :
170171 self .meters .append (Meter .from_json (m ))
171172
173+ # Parse section starts into a local variable, then apply to phrases
172174 ss_grid = opts .get ("sectionStartsGrid" )
173175 if ss_grid is None :
174176 ss = opts .get ("sectionStarts" , [0 ])
175177 ss_grid = [ss ]
176178 for _ in range (len (ss_grid ), len (self .instrumentation )):
177179 ss_grid .append ([0 ])
178- self . section_starts_grid : List [ List [ float ]] = [sorted (list (s )) for s in ss_grid ]
180+ ss_grid = [sorted (list (s )) for s in ss_grid ]
179181
180- # Migrate old sectionStartsGrid to phrase-level is_section_start properties
181- # This enables phrase-based section tracking while maintaining backward compatibility
182- if self .section_starts_grid and self .phrase_grid :
182+ # Apply section starts to phrase-level is_section_start flags
183+ if ss_grid and self .phrase_grid :
183184 for inst_idx , phrases in enumerate (self .phrase_grid ):
184- if inst_idx < len (self . section_starts_grid ):
185- starts = self . section_starts_grid [inst_idx ]
185+ if inst_idx < len (ss_grid ):
186+ starts = ss_grid [inst_idx ]
186187 for phrase_idx , phrase in enumerate (phrases ):
187- # Convert indices to integers for comparison
188- phrase .is_section_start = phrase_idx in [int (s ) for s in starts ]
188+ if phrase .is_section_start is None :
189+ phrase .is_section_start = phrase_idx in [int (s ) for s in starts ]
190+ # Ensure every phrase has a boolean is_section_start
191+ for phrase in phrases :
192+ if phrase .is_section_start is None :
193+ phrase .is_section_start = False
189194
190195 sc_grid = opts .get ("sectionCatGrid" )
191196 if sc_grid is None :
192197 section_cat = opts .get ("sectionCategorization" )
193198 sc_grid = []
194- for i , ss in enumerate (self . section_starts_grid ):
199+ for i , ss in enumerate (ss_grid ):
195200 if i == 0 :
196201 if section_cat is not None :
197202 for c in section_cat :
@@ -203,7 +208,7 @@ def __init__(self, options: Optional[dict] = None) -> None:
203208 row = [init_sec_categorization () for _ in ss ]
204209 sc_grid .append (row )
205210 self .section_cat_grid : List [List [SecCatType ]] = sc_grid
206- for i , ss in enumerate (self . section_starts_grid ):
211+ for i , ss in enumerate (ss_grid ):
207212 while len (self .section_cat_grid ) <= i :
208213 self .section_cat_grid .append ([init_sec_categorization () for _ in ss ])
209214 if len (self .section_cat_grid [i ]) < len (ss ):
@@ -219,7 +224,7 @@ def __init__(self, options: Optional[dict] = None) -> None:
219224 [ [f for f in fields if f != "" ] for fields in track ]
220225 for track in ad_hoc
221226 ]
222- while len (self .ad_hoc_section_cat_grid ) < len (self . section_starts_grid ):
227+ while len (self .ad_hoc_section_cat_grid ) < len (ss_grid ):
223228 self .ad_hoc_section_cat_grid .append ([[] for _ in self .ad_hoc_section_cat_grid [0 ]])
224229
225230 self .excerpt_range = opts .get ("excerptRange" )
@@ -464,12 +469,26 @@ def dur_array(self, arr: List[float]) -> None:
464469 self .dur_array_grid [0 ] = arr
465470
466471 @property
467- def section_starts (self ) -> List [float ]:
472+ def section_starts_grid (self ) -> List [List [int ]]:
473+ """Compute section starts from phrase-level is_section_start flags."""
474+ return [[idx for idx , p in enumerate (phrases ) if p .is_section_start ]
475+ for phrases in self .phrase_grid ]
476+
477+ @section_starts_grid .setter
478+ def section_starts_grid (self , value : List [List [int ]]) -> None :
479+ """Apply section starts to phrase-level is_section_start flags."""
480+ for inst_idx , starts in enumerate (value ):
481+ if inst_idx < len (self .phrase_grid ):
482+ for p_idx , phrase in enumerate (self .phrase_grid [inst_idx ]):
483+ phrase .is_section_start = p_idx in [int (s ) for s in starts ]
484+
485+ @property
486+ def section_starts (self ) -> List [int ]:
468487 return self .section_starts_grid [0 ]
469488
470489 @section_starts .setter
471- def section_starts (self , arr : List [float ]) -> None :
472- self .section_starts_grid [ 0 ] = arr
490+ def section_starts (self , arr : List [int ]) -> None :
491+ self .section_starts_grid = [ arr ] + self . section_starts_grid [ 1 :]
473492
474493 @property
475494 def section_categorization (self ) -> List [SecCatType ]:
@@ -863,10 +882,18 @@ def remove_meter(self, meter: Meter) -> None:
863882 self .meters .remove (meter )
864883
865884 # ------------------------------------------------------------------
866- def all_trajectories (self , inst : int = 0 ) -> List [Trajectory ]:
885+ def all_trajectories (self , inst : int = 0 , string_idx : int = 0 ) -> List [Trajectory ]:
886+ """Get all trajectories for a given instrument track and string index.
887+
888+ Args:
889+ inst: Instrument track index (default 0).
890+ string_idx: String index within the instrument (default 0).
891+ For Sitar/Sarangi, string 0 is main, string 1 is jor/second.
892+ """
867893 trajs : List [Trajectory ] = []
868894 for p in self .phrase_grid [inst ]:
869- trajs .extend (p .trajectories )
895+ if string_idx < len (p .trajectory_grid ):
896+ trajs .extend (p .trajectory_grid [string_idx ])
870897 return trajs
871898
872899 # ------------------------------------------------------------------
@@ -896,6 +923,45 @@ def track_from_phrase_uid(self, uid: str) -> int:
896923 return i
897924 raise ValueError ("Phrase not found" )
898925
926+ def string_from_traj (self , traj : Trajectory ) -> int :
927+ """Determine which string index contains a given trajectory.
928+
929+ Searches all phrases across all strings by unique_id.
930+ Returns the string index (0 or 1). Raises ValueError if not found.
931+ """
932+ for phrases in self .phrase_grid :
933+ for phrase in phrases :
934+ for string_idx , string_trajs in enumerate (phrase .trajectory_grid ):
935+ for t in string_trajs :
936+ if t .unique_id == traj .unique_id :
937+ return string_idx
938+ raise ValueError ("Trajectory not found in any string" )
939+
940+ def ensure_string_synchronization (self ) -> None :
941+ """For Sitar/Sarangi, ensure trajectory_grid[1] exists and is synchronized.
942+
943+ If string 1 is empty or contains only silent trajectories (id=12),
944+ fill it with a single silent trajectory matching the phrase duration.
945+ """
946+ polyphonic_instruments = {Instrument .Sitar , Instrument .Sarangi }
947+ for inst_idx , inst in enumerate (self .instrumentation ):
948+ if inst not in polyphonic_instruments :
949+ continue
950+ for phrase in self .phrase_grid [inst_idx ]:
951+ # Ensure trajectory_grid has at least 2 entries
952+ while len (phrase .trajectory_grid ) < 2 :
953+ phrase .trajectory_grid .append ([])
954+
955+ string_1 = phrase .trajectory_grid [1 ]
956+ is_empty_or_silent = (
957+ len (string_1 ) == 0 or
958+ all (t .id == 12 for t in string_1 )
959+ )
960+ if is_empty_or_silent :
961+ silent = Trajectory ({'id' : 12 , 'dur_tot' : phrase .dur_tot })
962+ phrase .trajectory_grid [1 ] = [silent ]
963+ phrase .reset ()
964+
899965 def traj_from_uid (self , uid : str , track : int = 0 ) -> Trajectory :
900966 for t in self .all_trajectories (track ):
901967 if t .unique_id == uid :
@@ -980,14 +1046,12 @@ def proportions_of_fixed_pitches(
9801046 return durations_of_fixed_pitches (trajs = self .all_trajectories (inst ), output_type = output_type , count_type = "proportional" )
9811047
9821048 # ------------------------------------------------------------------
983- def chikari_freqs (self , inst_idx : int ) -> List [float ]:
984- phrases = self .phrase_grid [inst_idx ]
985- for p in phrases :
986- if p .chikaris :
987- chikari = list (p .chikaris .values ())[0 ]
988- return [c .frequency for c in chikari .pitches [:2 ]]
989- f = self .raga .fundamental
990- return [f * 2 , f * 4 ]
1049+ def chikari_freqs (self , inst_idx : int = 0 ) -> List [float ]:
1050+ """Return 4 chikari frequencies derived from the raga.
1051+
1052+ Returns 0.0 for strings that are silent (None pitch).
1053+ """
1054+ return [p .frequency if p is not None else 0.0 for p in self .raga .chikari_pitches ]
9911055
9921056 # ------------------------------------------------------------------
9931057 def dur_starts (self , track : int = 0 ) -> List [float ]:
@@ -997,12 +1061,26 @@ def dur_starts(self, track: int = 0) -> List[float]:
9971061 raise Exception ("durTot is undefined" )
9981062 return get_starts ([d * self .dur_tot for d in self .dur_array_grid [track ]])
9991063
1000- def traj_start_times (self , inst : int = 0 ) -> List [float ]:
1001- trajs = self .all_trajectories (inst )
1002- times = [0.0 ]
1003- for t in trajs [:- 1 ]:
1004- times .append (times [- 1 ] + t .dur_tot )
1005- return times
1064+ def traj_start_times (self , inst : int = 0 , string_idx : int = 0 ) -> List [float ]:
1065+ """Get start times for all trajectories in a given string.
1066+
1067+ For string 0: cumulative duration (standard sequential timing).
1068+ For string > 0: phrase-boundary based (phrase.start_time + traj.start_time).
1069+ """
1070+ if string_idx == 0 :
1071+ trajs = self .all_trajectories (inst , 0 )
1072+ times = [0.0 ]
1073+ for t in trajs [:- 1 ]:
1074+ times .append (times [- 1 ] + t .dur_tot )
1075+ return times
1076+ else :
1077+ times : List [float ] = []
1078+ for p in self .phrase_grid [inst ]:
1079+ phrase_start = p .start_time or 0.0
1080+ if string_idx < len (p .trajectory_grid ):
1081+ for traj in p .trajectory_grid [string_idx ]:
1082+ times .append (phrase_start + (traj .start_time or 0.0 ))
1083+ return times
10061084
10071085 def all_pitches (self , repetition : bool = True , pitch_number : bool = False , track : int = 0 ) -> List [Any ]:
10081086 pitches : List [Any ] = []
@@ -1358,7 +1436,6 @@ def to_json(self) -> Dict[str, Any]:
13581436 "name" : self .name ,
13591437 "family_name" : self .family_name ,
13601438 "given_name" : self .given_name ,
1361- "sectionStartsGrid" : self .section_starts_grid ,
13621439 "sectionCatGrid" : self .section_cat_grid ,
13631440 "explicitPermissions" : self .explicit_permissions ,
13641441 "soloist" : self .soloist ,
0 commit comments