@@ -56,11 +56,12 @@ getMusicalTime(realTime: number, referenceLevel?: number): MusicalTime | false
5656- ` false ` - If time is before start_time or after end_time
5757
5858** Reference Level Behavior:**
59- - ` referenceLevel=0 ` : Fractional position within cycle duration (containing unit for beats)
60- - ` referenceLevel=1 ` : Fractional position within beat duration (containing unit for subdivisions)
61- - ` referenceLevel=2 ` : Fractional position within subdivision duration (containing unit for sub-subdivisions)
62- - ` referenceLevel=n ` : Fractional position within level-(n-1) duration (containing unit for level-n)
63- - Default: Fractional position within finest subdivision (between pulses)
59+ - ` fractionalBeat ` : ALWAYS represents fractional position between pulses (finest level), regardless of referenceLevel
60+ - ` referenceLevel ` : Only affects the truncation of ` hierarchicalPosition ` :
61+ - ` referenceLevel=0 ` : hierarchicalPosition includes only beat level
62+ - ` referenceLevel=1 ` : hierarchicalPosition includes beat and subdivision levels
63+ - ` referenceLevel=n ` : hierarchicalPosition includes levels 0 through n
64+ - Default (None): hierarchicalPosition includes all levels
6465
6566** Boundaries:**
6667- ** Start** : ` realTime >= meter.startTime `
@@ -107,63 +108,51 @@ for each level in hierarchy:
107108 remainingTime = remainingTime % subdivisionDuration
108109```
109110
110- ### Step 4: Fractional Beat Calculation (Level -Based)
111+ ### Step 4: Fractional Beat Calculation (Always Pulse -Based)
111112
112- The fractional beat calculation depends on the specified reference level:
113-
114- #### Default Behavior (Pulse-Based)
115- When ` referenceLevel ` is not specified or equals ` hierarchy.length - 1 ` , calculate fraction between pulses:
113+ The fractional beat ALWAYS represents the position between pulses (finest level), regardless of reference level:
116114
117115```
118116currentPulseIndex = hierarchicalPositionToPulseIndex(positions, cycleNumber)
119- currentPulseTime = meter.allPulses[currentPulseIndex].realTime
120-
121- // Handle next pulse (accounting for cycle boundaries)
122- if currentPulseIndex + 1 < meter.allPulses.length:
123- nextPulseTime = meter.allPulses[currentPulseIndex + 1].realTime
124- else:
125- // Last pulse - use next cycle start
126- nextCycleStart = meter.startTime + (cycleNumber + 1) * meter.cycleDur
127- nextPulseTime = nextCycleStart
128117
129- pulseDuration = nextPulseTime - currentPulseTime
130- if pulseDuration <= 0 :
118+ // Bounds checking
119+ if currentPulseIndex < 0 or currentPulseIndex >= meter.allPulses.length :
131120 fractionalBeat = 0.0
132121else:
133- timeFromCurrentPulse = realTime - currentPulseTime
134- fractionalBeat = timeFromCurrentPulse / pulseDuration
122+ currentPulseTime = meter.allPulses[currentPulseIndex].realTime
123+
124+ // Handle next pulse (accounting for cycle boundaries)
125+ if currentPulseIndex + 1 < meter.allPulses.length:
126+ nextPulseTime = meter.allPulses[currentPulseIndex + 1].realTime
127+ else:
128+ // Last pulse - use next cycle start
129+ nextCycleStart = meter.startTime + (cycleNumber + 1) * meter.cycleDur
130+ nextPulseTime = nextCycleStart
131+
132+ pulseDuration = nextPulseTime - currentPulseTime
133+ if pulseDuration <= 0:
134+ fractionalBeat = 0.0
135+ else:
136+ timeFromCurrentPulse = realTime - currentPulseTime
137+ fractionalBeat = timeFromCurrentPulse / pulseDuration
135138
136139// Clamp to [0, 1] range
137140fractionalBeat = max(0.0, min(1.0, fractionalBeat))
138141```
139142
140- #### Reference Level Behavior
141- When ` referenceLevel ` is specified and < ` hierarchy.length - 1 ` :
142-
143- ```
144- // Truncate hierarchical position to reference level + 1
145- truncatedPosition = positions[0..referenceLevel]
146-
147- // Calculate start time of current reference-level unit
148- currentLevelStartTime = calculateLevelStartTime(truncatedPosition, cycleNumber, referenceLevel)
143+ ### Step 5: Handle Reference Level Truncation
149144
150- // Calculate duration of reference-level unit (accounting for actual pulse timing)
151- levelDuration = calculateLevelDuration(truncatedPosition, cycleNumber, referenceLevel)
145+ If a reference level is specified, truncate the hierarchical position (fractional_beat remains unchanged):
152146
153- if levelDuration <= 0:
154- fractionalBeat = 0.0
155- else:
156- timeFromLevelStart = realTime - currentLevelStartTime
157- fractionalBeat = timeFromLevelStart / levelDuration
158-
159- // Clamp to [0, 1] range
160- fractionalBeat = max(0.0, min(1.0, fractionalBeat))
161-
162- // Update hierarchical position to only include levels up to reference
163- positions = truncatedPosition
147+ ```
148+ if referenceLevel is not None and referenceLevel < len(hierarchy):
149+ // Truncate positions to reference level for final result
150+ positions = positions[0:referenceLevel+1]
151+
152+ // Note: fractionalBeat is NOT recalculated - it always remains pulse-based
164153```
165154
166- ### Step 5 : Result Construction
155+ ### Step 6 : Result Construction
167156```
168157return MusicalTime {
169158 cycleNumber: cycleNumber,
@@ -201,64 +190,7 @@ cycleOffset = cycleNumber * meter.getPulsesPerCycle()
201190return pulseIndex + cycleOffset
202191```
203192
204- ### calculateLevelStartTime()
205-
206- ** Purpose:** Calculate the start time of a hierarchical unit at a given reference level.
207-
208- ** Signature:**
209- ```
210- calculateLevelStartTime(positions: number[], cycleNumber: number, referenceLevel: number): number
211- ```
212-
213- ** Algorithm:**
214- ```
215- // Find the pulse index for the start of this reference-level unit
216- startPositions = positions.slice(0, referenceLevel + 1)
217- // Zero out all positions below the reference level
218- for i = referenceLevel + 1 to hierarchy.length - 1:
219- startPositions[i] = 0
220-
221- startPulseIndex = hierarchicalPositionToPulseIndex(startPositions, cycleNumber)
222- return meter.allPulses[startPulseIndex].realTime
223- ```
224-
225- ### calculateLevelDuration()
226-
227- ** Purpose:** Calculate the actual duration of a hierarchical unit based on pulse timing.
228-
229- ** Signature:**
230- ```
231- calculateLevelDuration(positions: number[], cycleNumber: number, referenceLevel: number): number
232- ```
233-
234- ** Algorithm:**
235- ```
236- // Get start time of current unit
237- startTime = calculateLevelStartTime(positions, cycleNumber, referenceLevel)
238-
239- // Calculate start time of next unit at same level
240- nextPositions = positions.slice()
241- nextPositions[referenceLevel]++
242-
243- // Handle overflow - if we've exceeded this level, move to next cycle or higher level
244- if nextPositions[referenceLevel] >= hierarchy[referenceLevel]:
245- if referenceLevel == 0:
246- // Next beat is in next cycle
247- nextCycleNumber = cycleNumber + 1
248- if nextCycleNumber >= meter.repetitions:
249- // Use meter end time
250- return meter.startTime + meter.repetitions * meter.cycleDur - startTime
251- nextPositions[0] = 0
252- return calculateLevelStartTime(nextPositions, nextCycleNumber, referenceLevel) - startTime
253- else:
254- // Carry over to higher level
255- nextPositions[referenceLevel] = 0
256- nextPositions[referenceLevel - 1]++
257- return calculateLevelDuration(nextPositions, cycleNumber, referenceLevel - 1)
258-
259- endTime = calculateLevelStartTime(nextPositions, cycleNumber, referenceLevel)
260- return endTime - startTime
261- ```
193+ ** Note:** The simplified implementation no longer requires the complex ` calculateLevelStartTime() ` and ` calculateLevelDuration() ` helper functions that were used in the previous reference-level-based fractional beat calculation.
262194
263195## Edge Cases & Error Handling
264196
@@ -299,10 +231,10 @@ Query: getMusicalTime(2.375, referenceLevel=0)
299231
300232Expected:
301233- cycleNumber: 0
302- - hierarchicalPosition: [2] (Beat 3)
303- - fractionalBeat: 0.594 (2.375s / 4.0s cycle duration = 59.4% through cycle )
304- - toString(): "C0:2+0.594 "
305- - Readable: "Cycle 1: Beat 3 + 0.594 through cycle "
234+ - hierarchicalPosition: [2] (Beat 3 only, due to referenceLevel=0 )
235+ - fractionalBeat: 0.0 (exactly on pulse, fractionalBeat always pulse-based )
236+ - toString(): "C0:2+0.000 "
237+ - Readable: "Cycle 1: Beat 3"
306238```
307239
308240### Test Case 3: Reference Level - Subdivision Level (referenceLevel=1)
@@ -313,9 +245,9 @@ Query: getMusicalTime(2.375, referenceLevel=1)
313245Expected:
314246- cycleNumber: 0
315247- hierarchicalPosition: [2, 1] (Beat 3, Subdivision 2)
316- - fractionalBeat: 0.375 (0.375s / 1.0s beat duration = 37.5% through beat 2 )
317- - toString(): "C0:2.1+0.375 "
318- - Readable: "Cycle 1: Beat 3, Subdivision 2 + 0.375 through beat "
248+ - fractionalBeat: 0.0 (exactly on pulse, fractionalBeat always pulse-based )
249+ - toString(): "C0:2.1+0.000 "
250+ - Readable: "Cycle 1: Beat 3, Subdivision 2"
319251```
320252
321253### Test Case 4: Complex Hierarchy with Reference Levels
@@ -326,7 +258,7 @@ Query: getMusicalTime(0.15625, referenceLevel=1)
326258Expected:
327259- cycleNumber: 0
328260- hierarchicalPosition: [1, 0] (Beat 2, Subdivision 1)
329- - fractionalBeat: 0.25 (0.25 through subdivision duration)
261+ - fractionalBeat: 0.25 (0.25 through finest-level pulse duration)
330262- toString(): "C0:1.0+0.250"
331263```
332264
0 commit comments