Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions idtap/classes/meter.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,85 @@ def _pulse_dur(self) -> float:
def cycle_dur(self) -> float:
return self._pulse_dur * self._pulses_per_cycle

def _get_hierarchy_mult(self, layer: int) -> int:
"""Get the multiplier for a given hierarchy layer.

Handles both simple numbers and complex arrays like [3, 2] -> 5

Args:
layer: The hierarchy layer index

Returns:
The multiplier for that layer (sum if array, value if int)
"""
if layer >= len(self.hierarchy):
return 1
h = self.hierarchy[layer]
if isinstance(h, int):
return h
else:
return sum(h)

@property
def display_tempo(self) -> float:
"""Get the tempo as displayed in performance practice (at matra/beat level).

In Hindustani music, the internal tempo is stored at the finest pulse level,
but musicians typically think of tempo at layer 1 (the matra/beat level).

For a hierarchy like [[4,4,4,4], 4] (Tintal) with internal tempo 60 BPM:
- Layer 1 multiplier = 4
- Display tempo = 60 * 4 = 240 BPM

For complex hierarchies where layer 1 is an array like [3, 2],
sum the elements to get the multiplier (5).

Returns:
The tempo at the matra/beat level (layer 1)
"""
if len(self.hierarchy) < 2:
return self.tempo
return self.tempo * self._get_hierarchy_mult(1)

@display_tempo.setter
def display_tempo(self, new_tempo: float) -> None:
"""Set the tempo using the performance practice tempo (at matra/beat level).

Converts the matra-level tempo back to internal pulse tempo.

Args:
new_tempo: The desired tempo at the matra/beat level
"""
if len(self.hierarchy) < 2:
# For single-layer hierarchies, display tempo equals internal tempo
self.tempo = new_tempo
self._generate_pulse_structures()
else:
internal_tempo = new_tempo / self._get_hierarchy_mult(1)
self.tempo = internal_tempo
self._generate_pulse_structures()

def get_tempo_at_layer(self, layer: int) -> float:
"""Get the tempo at a specific hierarchical layer.

Args:
layer: The hierarchy layer (0 = coarsest/vibhag, higher = finer subdivisions)

Returns:
The tempo (BPM) at that layer

Raises:
ValueError: If layer is out of bounds
"""
if layer < 0 or layer >= len(self.hierarchy):
raise ValueError(f"Layer {layer} is out of bounds for hierarchy with {len(self.hierarchy)} layers")

# Start with base tempo and multiply by each layer's subdivision
result_tempo = self.tempo
for i in range(1, layer + 1):
result_tempo *= self._get_hierarchy_mult(i)
return result_tempo

def _generate_pulse_structures(self) -> None:
self.pulse_structures = [[]]
# single layer of pulses for simplified implementation
Expand Down
75 changes: 75 additions & 0 deletions idtap/tests/meter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,78 @@ def test_add_time_points():
m.add_time_points(new_times, 1)
for nt in new_times:
assert includes_with_tolerance(m.real_times, nt, 1e-8)


# display_tempo tests

def test_display_tempo_simple_hierarchy():
"""Test display_tempo with a simple 2-layer hierarchy."""
# Tintal-like: [[4,4,4,4], 4] with internal tempo 60 BPM
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
# Display tempo should be 60 * 4 = 240
assert m.display_tempo == 240


def test_display_tempo_single_layer():
"""Test display_tempo with single-layer hierarchy."""
m = Meter(hierarchy=[4], tempo=120)
# For single layer, display tempo equals internal tempo
assert m.display_tempo == 120


def test_display_tempo_three_layer():
"""Test display_tempo with a 3-layer hierarchy."""
# hierarchy [4, 4, 2] with internal tempo 60 BPM
# Layer 1 multiplier = 4
# Display tempo = 60 * 4 = 240
m = Meter(hierarchy=[4, 4, 2], tempo=60)
assert m.display_tempo == 240


def test_display_tempo_setter():
"""Test setting display_tempo."""
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
assert m.display_tempo == 240

# Set display tempo to 480
m.display_tempo = 480
# Internal tempo should now be 480 / 4 = 120
assert m.tempo == 120
assert m.display_tempo == 480


def test_display_tempo_setter_single_layer():
"""Test setting display_tempo with single-layer hierarchy."""
m = Meter(hierarchy=[4], tempo=120)
m.display_tempo = 180
assert m.tempo == 180
assert m.display_tempo == 180


def test_get_tempo_at_layer():
"""Test get_tempo_at_layer helper."""
m = Meter(hierarchy=[[4, 4, 4, 4], 4], tempo=60)
# Layer 0 tempo = internal tempo = 60
assert m.get_tempo_at_layer(0) == 60
# Layer 1 tempo = 60 * 4 = 240
assert m.get_tempo_at_layer(1) == 240


def test_get_tempo_at_layer_out_of_bounds():
"""Test get_tempo_at_layer with invalid layer."""
m = Meter(hierarchy=[4, 4], tempo=60)
with pytest.raises(ValueError):
m.get_tempo_at_layer(2)
with pytest.raises(ValueError):
m.get_tempo_at_layer(-1)


def test_get_hierarchy_mult():
"""Test _get_hierarchy_mult helper."""
m = Meter(hierarchy=[[4, 4, 4, 4], 4])
# Layer 0: [4,4,4,4] -> sum = 16
assert m._get_hierarchy_mult(0) == 16
# Layer 1: 4
assert m._get_hierarchy_mult(1) == 4
# Out of bounds returns 1
assert m._get_hierarchy_mult(5) == 1
Loading