Skip to content

Commit d12aadd

Browse files
jon-myersclaude
andcommitted
fix: correct fractional_beat calculation to always use finest level
The fractional_beat should always represent the position within the finest hierarchical level unit (between pulses), regardless of the reference_level parameter. The reference_level only affects hierarchical_position truncation. Fixed examples: - meter.get_musical_time(0.125) now returns C0:0.2+0.000 (was C0:0.2+0.500) - meter.get_musical_time(0.0625) now returns C0:0.1+0.000 (was C0:0.1+0.250) - meter.get_musical_time(0.03125) now returns C0:0.0+0.500 (was C0:0.0+0.125) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 154463f commit d12aadd

File tree

2 files changed

+41
-33
lines changed

2 files changed

+41
-33
lines changed

idtap/classes/meter.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,14 @@ def get_musical_time(self, real_time: float, reference_level: Optional[int] = No
575575
remaining_time = remaining_time % subdivision_duration
576576

577577
# Step 4: Fractional beat calculation
578-
if ref_level == len(self.hierarchy) - 1:
579-
# Default behavior: pulse-based calculation
580-
current_pulse_index = self._hierarchical_position_to_pulse_index(positions, cycle_number)
578+
# ALWAYS calculate fractional_beat as position within finest level unit (between pulses)
579+
# This is independent of reference_level, which only affects hierarchical_position truncation
580+
current_pulse_index = self._hierarchical_position_to_pulse_index(positions, cycle_number)
581+
582+
# Bounds checking
583+
if current_pulse_index < 0 or current_pulse_index >= len(self.all_pulses):
584+
fractional_beat = 0.0
585+
else:
581586
current_pulse_time = self.all_pulses[current_pulse_index].real_time
582587

583588
# Handle next pulse
@@ -594,33 +599,16 @@ def get_musical_time(self, real_time: float, reference_level: Optional[int] = No
594599
else:
595600
time_from_current_pulse = real_time - current_pulse_time
596601
fractional_beat = time_from_current_pulse / pulse_duration
597-
598-
# Clamp to [0, 1] range
599-
fractional_beat = max(0.0, min(1.0, fractional_beat))
600-
601-
else:
602-
# Reference level behavior - preserve full positions for fractional_beat calculation
603-
# but truncate for final result
604-
truncated_positions = positions[:ref_level + 1]
605-
606-
# Use full positions for accurate fractional_beat calculation
607-
# This prevents clustering when reference_level=0 (Issue #28)
608-
current_level_start_time = self._calculate_level_start_time(positions, cycle_number, ref_level)
609-
level_duration = self._calculate_level_duration(positions, cycle_number, ref_level)
610-
611-
if level_duration <= 0:
612-
fractional_beat = 0.0
613-
else:
614-
time_from_level_start = real_time - current_level_start_time
615-
fractional_beat = time_from_level_start / level_duration
616-
617-
# Clamp to [0, 1] range
618-
fractional_beat = max(0.0, min(1.0, fractional_beat))
619-
620-
# Update positions to only include levels up to reference for final result
621-
positions = truncated_positions
622602

623-
# Step 5: Result construction
603+
# Clamp to [0, 1] range
604+
fractional_beat = max(0.0, min(1.0, fractional_beat))
605+
606+
# Step 5: Handle reference level truncation (if specified)
607+
if ref_level is not None and ref_level < len(self.hierarchy) - 1:
608+
# Truncate positions to reference level for final result
609+
positions = positions[:ref_level + 1]
610+
611+
# Step 6: Result construction
624612
return MusicalTime(
625613
cycle_number=cycle_number,
626614
hierarchical_position=positions,

idtap/tests/musical_time_test.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ def test_reference_level_beat(self):
9393
"""Test Case 2 from spec: Reference level at beat level."""
9494
meter = Meter(hierarchy=[4, 4], tempo=240, start_time=0, repetitions=2)
9595

96-
result = meter.get_musical_time(1.375, reference_level=0) # Within bounds: 1.375 = beat 1, 37.5% through beat
96+
result = meter.get_musical_time(1.375, reference_level=0) # 1.375s = 2nd cycle, beat 1, subdivision 2 (exactly on pulse)
9797

9898
assert result is not False
9999
assert result.cycle_number == 1 # Second cycle
100-
assert result.hierarchical_position == [1] # Only beat level (beat 2)
101-
assert abs(result.fractional_beat - 0.5) < 0.1 # 50% through beat 2
102-
assert "C1:1+" in str(result)
100+
assert result.hierarchical_position == [1] # Only beat level (beat 2) due to reference_level=0
101+
assert abs(result.fractional_beat - 0.0) < 0.01 # Exactly on pulse (fractional_beat always pulse-based)
102+
assert str(result) == "C1:1+0.000"
103103

104104
def test_reference_level_subdivision(self):
105105
"""Test Case 3 from spec: Reference level at subdivision level."""
@@ -113,6 +113,26 @@ def test_reference_level_subdivision(self):
113113
assert abs(result.fractional_beat - 0.0) < 0.01 # Exactly on subdivision
114114
assert str(result) == "C0:1.2+0.000"
115115

116+
def test_johns_specific_examples(self):
117+
"""Test Jon's specific examples that revealed the fractional_beat calculation issue."""
118+
meter = Meter(hierarchy=[4, 4], tempo=240, start_time=0, repetitions=3)
119+
120+
# These examples should work correctly after the fix
121+
result = meter.get_musical_time(0.5)
122+
assert str(result) == "C0:2.0+0.000", f"Expected C0:2.0+0.000, got {result}"
123+
124+
result = meter.get_musical_time(0.25)
125+
assert str(result) == "C0:1.0+0.000", f"Expected C0:1.0+0.000, got {result}"
126+
127+
result = meter.get_musical_time(0.125)
128+
assert str(result) == "C0:0.2+0.000", f"Expected C0:0.2+0.000, got {result}"
129+
130+
result = meter.get_musical_time(0.0625)
131+
assert str(result) == "C0:0.1+0.000", f"Expected C0:0.1+0.000, got {result}"
132+
133+
result = meter.get_musical_time(0.03125)
134+
assert str(result) == "C0:0.0+0.500", f"Expected C0:0.0+0.500, got {result}"
135+
116136
def test_complex_hierarchy(self):
117137
"""Test Case 4 from spec: Complex hierarchy with reference levels."""
118138
meter = Meter(hierarchy=[3, 2, 4], tempo=480, start_time=0, repetitions=1) # Slower tempo

0 commit comments

Comments
 (0)