gabriel / muse public
_midi_keys.py python
58 lines 2.0 KB
Raw
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 19 days ago
1 """Shared MIDI note primitives — no cross-plugin imports.
2
3 Extracted from ``midi_diff`` so that ``entity.py`` can import ``NoteKey``,
4 ``_note_content_id``, and ``_note_summary`` without creating a cycle with
5 ``midi_diff.py``.
6 """
7
8 from typing import TypedDict
9
10 from muse.core.types import blob_id
11
12 _PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
13
14 class NoteKey(TypedDict):
15 """Fully-specified MIDI note used as the LCS comparison unit.
16
17 Two notes are considered identical in LCS iff all five fields match.
18 A pitch change, velocity change, timing shift, or channel change
19 counts as a delete of the old note and an insert of the new one.
20 This is conservative but correct — it means the LCS finds true
21 structural matches and surfaces real musical changes.
22 """
23
24 pitch: int
25 velocity: int
26 start_tick: int
27 duration_ticks: int
28 channel: int
29
30 def _pitch_name(midi_pitch: int) -> str:
31 """Return a human-readable pitch string, e.g. ``"C4"``, ``"F#5"``."""
32 octave = midi_pitch // 12 - 1
33 name = _PITCH_NAMES[midi_pitch % 12]
34 return f"{name}{octave}"
35
36 def _note_content_id(note: NoteKey) -> str:
37 """Return a ``sha256:``-prefixed ID for a note's five identity fields.
38
39 This gives a stable ``content_id`` for use in ``InsertOp`` / ``DeleteOp``
40 without requiring the note to be stored as a separate blob in the object
41 store. The ID uniquely identifies "this specific note event".
42 """
43 payload = (
44 f"{note['pitch']}:{note['velocity']}:"
45 f"{note['start_tick']}:{note['duration_ticks']}:{note['channel']}"
46 )
47 return blob_id(payload.encode())
48
49 def _note_summary(note: NoteKey, ticks_per_beat: int) -> str:
50 """Return a human-readable one-liner for a note, e.g. ``"C4 vel=80 @beat=1.00"``."""
51 beat = note["start_tick"] / max(ticks_per_beat, 1)
52 dur = note["duration_ticks"] / max(ticks_per_beat, 1)
53 return (
54 f"{_pitch_name(note['pitch'])} "
55 f"vel={note['velocity']} "
56 f"@beat={beat:.2f} "
57 f"dur={dur:.2f}"
58 )
File History 1 commit
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 19 days ago