_midi_keys.py
python
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