Skip to content

Commit 92a6912

Browse files
jon-myersclaude
andcommitted
feat: add display_tempo property to Meter class
Adds display_tempo property to convert between internal pulse tempo and performance practice tempo (at matra/beat level). In Hindustani music, musicians think of tempo at layer 1, but the internal representation stores tempo at the finest pulse level. New methods: - display_tempo (property getter/setter): Get/set tempo at matra level - get_tempo_at_layer(layer): Get tempo at any specific hierarchy layer - _get_hierarchy_mult(layer): Helper to get multiplier for a layer Closes #55 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 41a4abd commit 92a6912

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed

idtap/classes/meter.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,85 @@ def _pulse_dur(self) -> float:
346346
def cycle_dur(self) -> float:
347347
return self._pulse_dur * self._pulses_per_cycle
348348

349+
def _get_hierarchy_mult(self, layer: int) -> int:
350+
"""Get the multiplier for a given hierarchy layer.
351+
352+
Handles both simple numbers and complex arrays like [3, 2] -> 5
353+
354+
Args:
355+
layer: The hierarchy layer index
356+
357+
Returns:
358+
The multiplier for that layer (sum if array, value if int)
359+
"""
360+
if layer >= len(self.hierarchy):
361+
return 1
362+
h = self.hierarchy[layer]
363+
if isinstance(h, int):
364+
return h
365+
else:
366+
return sum(h)
367+
368+
@property
369+
def display_tempo(self) -> float:
370+
"""Get the tempo as displayed in performance practice (at matra/beat level).
371+
372+
In Hindustani music, the internal tempo is stored at the finest pulse level,
373+
but musicians typically think of tempo at layer 1 (the matra/beat level).
374+
375+
For a hierarchy like [[4,4,4,4], 4] (Tintal) with internal tempo 60 BPM:
376+
- Layer 1 multiplier = 4
377+
- Display tempo = 60 * 4 = 240 BPM
378+
379+
For complex hierarchies where layer 1 is an array like [3, 2],
380+
sum the elements to get the multiplier (5).
381+
382+
Returns:
383+
The tempo at the matra/beat level (layer 1)
384+
"""
385+
if len(self.hierarchy) < 2:
386+
return self.tempo
387+
return self.tempo * self._get_hierarchy_mult(1)
388+
389+
@display_tempo.setter
390+
def display_tempo(self, new_tempo: float) -> None:
391+
"""Set the tempo using the performance practice tempo (at matra/beat level).
392+
393+
Converts the matra-level tempo back to internal pulse tempo.
394+
395+
Args:
396+
new_tempo: The desired tempo at the matra/beat level
397+
"""
398+
if len(self.hierarchy) < 2:
399+
# For single-layer hierarchies, display tempo equals internal tempo
400+
self.tempo = new_tempo
401+
self._generate_pulse_structures()
402+
else:
403+
internal_tempo = new_tempo / self._get_hierarchy_mult(1)
404+
self.tempo = internal_tempo
405+
self._generate_pulse_structures()
406+
407+
def get_tempo_at_layer(self, layer: int) -> float:
408+
"""Get the tempo at a specific hierarchical layer.
409+
410+
Args:
411+
layer: The hierarchy layer (0 = coarsest/vibhag, higher = finer subdivisions)
412+
413+
Returns:
414+
The tempo (BPM) at that layer
415+
416+
Raises:
417+
ValueError: If layer is out of bounds
418+
"""
419+
if layer < 0 or layer >= len(self.hierarchy):
420+
raise ValueError(f"Layer {layer} is out of bounds for hierarchy with {len(self.hierarchy)} layers")
421+
422+
# Start with base tempo and multiply by each layer's subdivision
423+
result_tempo = self.tempo
424+
for i in range(1, layer + 1):
425+
result_tempo *= self._get_hierarchy_mult(i)
426+
return result_tempo
427+
349428
def _generate_pulse_structures(self) -> None:
350429
self.pulse_structures = [[]]
351430
# single layer of pulses for simplified implementation

idtap/tests/meter_test.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,78 @@ def test_add_time_points():
157157
m.add_time_points(new_times, 1)
158158
for nt in new_times:
159159
assert includes_with_tolerance(m.real_times, nt, 1e-8)
160+
161+
162+
# display_tempo tests
163+
164+
def test_display_tempo_simple_hierarchy():
165+
"""Test display_tempo with a simple 2-layer hierarchy."""
166+
# Tintal-like: [[4,4,4,4], 4] with internal tempo 60 BPM
167+
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
168+
# Display tempo should be 60 * 4 = 240
169+
assert m.display_tempo == 240
170+
171+
172+
def test_display_tempo_single_layer():
173+
"""Test display_tempo with single-layer hierarchy."""
174+
m = Meter(hierarchy=[4], tempo=120)
175+
# For single layer, display tempo equals internal tempo
176+
assert m.display_tempo == 120
177+
178+
179+
def test_display_tempo_three_layer():
180+
"""Test display_tempo with a 3-layer hierarchy."""
181+
# hierarchy [4, 4, 2] with internal tempo 60 BPM
182+
# Layer 1 multiplier = 4
183+
# Display tempo = 60 * 4 = 240
184+
m = Meter(hierarchy=[4, 4, 2], tempo=60)
185+
assert m.display_tempo == 240
186+
187+
188+
def test_display_tempo_setter():
189+
"""Test setting display_tempo."""
190+
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
191+
assert m.display_tempo == 240
192+
193+
# Set display tempo to 480
194+
m.display_tempo = 480
195+
# Internal tempo should now be 480 / 4 = 120
196+
assert m.tempo == 120
197+
assert m.display_tempo == 480
198+
199+
200+
def test_display_tempo_setter_single_layer():
201+
"""Test setting display_tempo with single-layer hierarchy."""
202+
m = Meter(hierarchy=[4], tempo=120)
203+
m.display_tempo = 180
204+
assert m.tempo == 180
205+
assert m.display_tempo == 180
206+
207+
208+
def test_get_tempo_at_layer():
209+
"""Test get_tempo_at_layer helper."""
210+
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
211+
# Layer 0 tempo = internal tempo = 60
212+
assert m.get_tempo_at_layer(0) == 60
213+
# Layer 1 tempo = 60 * 4 = 240
214+
assert m.get_tempo_at_layer(1) == 240
215+
216+
217+
def test_get_tempo_at_layer_out_of_bounds():
218+
"""Test get_tempo_at_layer with invalid layer."""
219+
m = Meter(hierarchy=[4, 4], tempo=60)
220+
with pytest.raises(ValueError):
221+
m.get_tempo_at_layer(2)
222+
with pytest.raises(ValueError):
223+
m.get_tempo_at_layer(-1)
224+
225+
226+
def test_get_hierarchy_mult():
227+
"""Test _get_hierarchy_mult helper."""
228+
m = Meter(hierarchy=[[4, 4, 4, 4], 4])
229+
# Layer 0: [4,4,4,4] -> sum = 16
230+
assert m._get_hierarchy_mult(0) == 16
231+
# Layer 1: 4
232+
assert m._get_hierarchy_mult(1) == 4
233+
# Out of bounds returns 1
234+
assert m._get_hierarchy_mult(5) == 1

0 commit comments

Comments
 (0)