"""Shared MIDI note primitives — no cross-plugin imports. Extracted from ``midi_diff`` so that ``entity.py`` can import ``NoteKey``, ``_note_content_id``, and ``_note_summary`` without creating a cycle with ``midi_diff.py``. """ from typing import TypedDict from muse.core.types import blob_id _PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] class NoteKey(TypedDict): """Fully-specified MIDI note used as the LCS comparison unit. Two notes are considered identical in LCS iff all five fields match. A pitch change, velocity change, timing shift, or channel change counts as a delete of the old note and an insert of the new one. This is conservative but correct — it means the LCS finds true structural matches and surfaces real musical changes. """ pitch: int velocity: int start_tick: int duration_ticks: int channel: int def _pitch_name(midi_pitch: int) -> str: """Return a human-readable pitch string, e.g. ``"C4"``, ``"F#5"``.""" octave = midi_pitch // 12 - 1 name = _PITCH_NAMES[midi_pitch % 12] return f"{name}{octave}" def _note_content_id(note: NoteKey) -> str: """Return a ``sha256:``-prefixed ID for a note's five identity fields. This gives a stable ``content_id`` for use in ``InsertOp`` / ``DeleteOp`` without requiring the note to be stored as a separate blob in the object store. The ID uniquely identifies "this specific note event". """ payload = ( f"{note['pitch']}:{note['velocity']}:" f"{note['start_tick']}:{note['duration_ticks']}:{note['channel']}" ) return blob_id(payload.encode()) def _note_summary(note: NoteKey, ticks_per_beat: int) -> str: """Return a human-readable one-liner for a note, e.g. ``"C4 vel=80 @beat=1.00"``.""" beat = note["start_tick"] / max(ticks_per_beat, 1) dur = note["duration_ticks"] / max(ticks_per_beat, 1) return ( f"{_pitch_name(note['pitch'])} " f"vel={note['velocity']} " f"@beat={beat:.2f} " f"dur={dur:.2f}" )