"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI). The first NON-TEXT surface, and the proof of the general shape. "Music is language and it is math" (Will): symbolic music is tractable and geometry-native, so it is the natural efferent twin to try first after text. CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical surface. That is the whole thesis of the multimodal projector: the same geometry-carrying IR drives text AND music; a text projector reads the words, a music projector reads the meaning-geometry. The mapping is deterministic and faithful to the geometry's structure: relation lemma -> scale degree (same relation -> same pitch class; meaning has a consistent sonic form) polarity -> mode (aff = major third above; neg = minor third / lowered — SACRED polarity is audible, a negated edge sounds negated) confidence -> note duration (stronger grounding rings longer) importance -> velocity (more important source = louder) section -> phrase + register shift (structure becomes musical form) Own-the-core: a Standard MIDI File is a header chunk + a track chunk of delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI library. Format 0, one track. """ from __future__ import annotations import io import os import struct import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from document_ir import DocumentIR, Provenance # noqa: E402 from projectors.base import TwoStageProjector, register # noqa: E402 _TICKS = 480 # ticks per quarter note _C_MAJOR = [0, 2, 4, 5, 7, 9, 11] # semitone offsets of a diatonic scale def _vlq(n: int) -> bytes: """MIDI variable-length quantity encoding of a delta time.""" if n == 0: return b"\x00" out = bytearray() out.append(n & 0x7F) n >>= 7 while n: out.insert(0, (n & 0x7F) | 0x80) n >>= 7 return bytes(out) def _degree_for(relation: str) -> int: """Stable scale degree for a relation lemma (same relation -> same pitch).""" if not relation: return 0 return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR) def _note_for(p: Provenance, base: int) -> tuple[int, int, int]: """(pitch, velocity, duration_ticks) for one geometry edge.""" root = base + _C_MAJOR[_degree_for(p.relation)] # polarity -> mode: affirmed edges take the bright major third, negated edges # take the darker minor third. The negation is AUDIBLE and never dropped. third = 4 if p.polarity == "aff" else 3 pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0))) velocity = int(56 + 60 * min(1.0, max(0.0, p.importance))) velocity = max(40, min(120, velocity)) # confidence -> duration: quarter .. dotted-half dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence)))) return pitch, velocity, dur # a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale + # tempo. Swapping this profile re-voices the SAME geometry — surface as parameter. _DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000, "registers": [60, 55, 64, 50, 67, 48], "program": 0} class MidiProjector(TwoStageProjector): """geometry -> symbolic music, in the shared two-stage shape. ``plan(frame)`` -> a music_spec: an ordered list of note dicts derived deterministically from the frame's provenance geometry (the peer's ``plan(frame, profile) -> spec``). ``realize(spec)`` -> Standard MIDI File bytes (the peer's ``realize(spec, profile) -> surface``; here the surface is symbolic MIDI, the minimal audio proof — a richer additive-synth audio projector conforms identically). """ surface = "midi" media_type = "audio/midi" ext = "mid" modality = "audio" def __init__(self, profile: dict | None = None): self.profile = profile or _DEFAULT_PROFILE # -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- # def plan(self, doc: DocumentIR) -> list[dict]: registers = self.profile["registers"] spec: list[dict] = [] for si, sec in enumerate(doc.sections): base = registers[si % len(registers)] provs = [p for p in sec.all_provenance() if p.kind in ("fact", "interpretation")] for i, p in enumerate(provs): pitch, vel, dur = _note_for(p, base) spec.append({"pitch": pitch, "velocity": vel, "dur": dur, "rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0, "relation": p.relation, "polarity": p.polarity}) return spec # -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ # def realize(self, spec: list[dict]) -> bytes: ev = bytearray() ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:] ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F]) for note in spec: ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]]) ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0]) ev += _vlq(0) + b"\xFF\x2F\x00" track = bytes(ev) buf = io.BytesIO() buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS)) buf.write(b"MTrk" + struct.pack(">I", len(track)) + track) return buf.getvalue() register(MidiProjector())