Add native audio/image efferent surfaces + projector proof-of-shape
audio-surface.el / image-surface.el: own-core additive-synthesis WAV and
raster-PNG renderers (integer-only DSP, since EL has no floats), rendered
from learned engram signatures via a pluggable surface-profile
abstraction (surface-profile.el). audio-demo.el / image-demo.el are
drivers. NOTE: demo files hardcode absolute paths to this worktree's own
directory — will need a path fixup before landing.
elp/projector/ is a Python package the author's own README marks as
"STAGING/PROOF-OF-SHAPE — not the deliverable", superseded by the native
.el surface-profile work above; kept as a validated architecture proof.
Generated output (elp/faculty/{out,sig}, elp/projector/out,
__pycache__) intentionally excluded.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user