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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

tests/assets/results/
uv.lock
32 changes: 28 additions & 4 deletions ariautils/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,38 @@ def _tempo_at_tick(tick: int) -> int:

return self

def apply_pedal_threshold(self, threshold: int = 64) -> "MidiDict":
"""Apply a threshold to raw sustain pedal values in place."""
def apply_pedal_threshold(
self,
threshold: int = 64,
buffer: int = 0,
transitions_only: bool = False,
) -> "MidiDict":
"""Apply threshold hysteresis to raw sustain pedal values in place."""

if threshold < 0 or threshold > 127:
raise ValueError("threshold must be between 0 and 127")
if buffer < 0 or threshold - buffer < 0 or threshold + buffer > 127:
raise ValueError("buffer must keep thresholds between 0 and 127")

for pedal_msg in self.pedal_msgs:
pedal_msg["data"] = 1 if pedal_msg["value"] >= threshold else 0
pedal_state: dict[int, Literal[0, 1]] = {}
pedal_msgs = []
for pedal_msg in sorted(self.pedal_msgs, key=lambda msg: msg["tick"]):
channel = pedal_msg["channel"]
prev_data: Literal[0, 1] = pedal_state.get(channel, 0)

if pedal_msg["value"] >= threshold + buffer:
data: Literal[0, 1] = 1
elif pedal_msg["value"] <= threshold - buffer:
data = 0
else:
data = prev_data

pedal_msg["data"] = data
pedal_state[channel] = data
if not transitions_only or data != prev_data:
pedal_msgs.append(pedal_msg)

self.pedal_msgs = pedal_msgs

return self

Expand Down
48 changes: 48 additions & 0 deletions tests/test_midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,54 @@ def test_apply_pedal_threshold(self) -> None:
)
self.assertEqual(midi_dict.note_msgs[0]["data"]["end"], 50)

def test_apply_pedal_threshold_buffer(self) -> None:
midi_dict = MidiDict.from_msg_dict(
{
"meta_msgs": [],
"tempo_msgs": [],
"pedal_msgs": [
{
"type": "pedal",
"data": 0,
"value": value,
"tick": tick,
"channel": 0,
}
for tick, value in enumerate(
[0, 63, 72, 70, 57, 56, 65, 73, 60, 55]
)
],
"instrument_msgs": [],
"note_msgs": [
{
"type": "note",
"data": {
"pitch": 60,
"start": 0,
"end": 50,
"velocity": 64,
},
"tick": 0,
"channel": 0,
}
],
"ticks_per_beat": 480,
"metadata": {},
}
)

result = midi_dict.apply_pedal_threshold(
threshold=64, buffer=8, transitions_only=True
)

self.assertIs(result, midi_dict)
self.assertEqual(
[msg["tick"] for msg in midi_dict.pedal_msgs], [2, 5, 7, 9]
)
self.assertEqual(
[msg["data"] for msg in midi_dict.pedal_msgs], [1, 0, 1, 0]
)

def test_resolve_pedal(self) -> None:
load_path = TEST_DATA_DIRECTORY.joinpath("arabesque.mid")
save_path = RESULTS_DATA_DIRECTORY.joinpath(
Expand Down
Loading