organ: el gets a speaker, and fetches the voice from the engram
El could turn meaning into samples and could not make a sound. Every path from those samples to the air ran outside the language, through a 939-line Swift program that shelled out to afplay, so the voice was not a capability of El or of Neuron but a separate binary standing next to them. Two things land here. The speaker. el_audio_darwin.m is a CoreAudio AudioQueue realizer in its own translation unit, declared in el_runtime.h, deliberately not a patch to el_runtime.c — acquiring a device must not mean editing the middle of the language, the same rule the realizer registry follows for modalities. It takes samples straight out of memory, so nothing is written to disk and no process is spawned between the intent to speak and the sound. The async half (play/stop/playing/played_frames) exists because barge-in means stopping on the spot, and a blocking play cannot be interrupted. el_peripheral_null.c is the same entry points everywhere else, so El that speaks links anywhere and truthfully reports having no speaker. The voice. organ_voice_fetch asks the engram for a voice region by query and reads the geometry off the node that comes back. A voice is not a JSON file next to the code; it is a memory, and the organ retrieves it the way anything retrieves a memory. An absent region returns empty rather than a plausible default, because a caller must be able to tell 'this is how they sound' from 'I never heard them'. Underneath both: __str_set_char bounds-checked writes against strlen(), which is 0 for the zero-filled buffer __str_alloc hands back, so every write was rejected and every El-authored WAV in this repo was 55,244 bytes of silence that reported ok=true. Byte buffers now carry their capacity in a side table; text keeps the exact strlen behaviour it had. This is why nobody noticed El was mute. Measured: voice fetched from the engram reads f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531, matching the 30s LPC measurement; render is 20160 samples at 16 kHz; both the rendered utterance and an own-core tone played aloud through CoreAudio with no Swift and no afplay in the chain.
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
// organ.el — Neuron's I/O organ, in El.
|
||||
//
|
||||
// THE PRINCIPLE. El speaks. The engram stores geometry and does not speak.
|
||||
// Before this file the organ was a 939-line Swift program standing next to the
|
||||
// language (peripheral/src/periph.swift): Neuron's mouth and ears were a
|
||||
// separate binary, and "speak" meant "shell out to that binary, which shells
|
||||
// out to afplay." That is not a voice, it is a subprocess. The voice belongs to
|
||||
// the language and its runtime.
|
||||
//
|
||||
// THE SPLIT. Exactly two things here are not El, and they are the two things El
|
||||
// cannot express as arithmetic:
|
||||
//
|
||||
// the speaker — handing a buffer to the DAC and waiting for it to drain
|
||||
// the capture — asking the OS for samples off a mic or frames off a camera
|
||||
//
|
||||
// Those live in lang/runtime/el_audio_darwin.m and el_capture_darwin.m, as
|
||||
// their own translation units, declared in el_runtime.h. Everything ELSE that
|
||||
// the Swift did — WAV encode and decode, LPC autocorrelation, Levinson-Durbin,
|
||||
// formant extraction off the all-pole envelope, source-filter resynthesis, the
|
||||
// compact descriptors, the converse yield-or-hold decision — is arithmetic, and
|
||||
// arithmetic is El's. See organ_dsp.el for that half.
|
||||
//
|
||||
// WHERE THE VOICE COMES FROM. Not from this file, and not from a JSON manifest
|
||||
// on disk. A voice is GEOMETRY IN THE ENGRAM, and the organ goes and gets it by
|
||||
// asking the engram, the same way anything else asks the engram for anything:
|
||||
// a query against the graph, then read the numbers off the node that comes
|
||||
// back. organ_voice_fetch is that. The previous path, load_voice("...json"),
|
||||
// parsed a file — which quietly made the voice a build artifact instead of a
|
||||
// memory. If the region is not in the graph, the honest answer is an empty
|
||||
// result, not a default voice.
|
||||
//
|
||||
// WHAT THE ORGAN NEVER DOES. It never learns a word. Pronunciation, vocabulary
|
||||
// and phonemes are the language faculty's, already built as ingested geometry —
|
||||
// "the engram knows how to pronounce." The seam is synth_codes(codes, voice,
|
||||
// pmap): the codes and the phoneme map arrive from the language side as
|
||||
// geometry, and the organ's whole job is turning them into samples and getting
|
||||
// the samples out the speaker, plus the same trip in reverse for the senses.
|
||||
//
|
||||
// RAILS, all non-negotiable:
|
||||
// own-core — CoreAudio / AVFoundation / ImageIO, all shipped with the OS.
|
||||
// No cloud, no model, no heavy dependency. There is no network
|
||||
// call anywhere in the organ, by construction.
|
||||
// local-only — raw streams stay on the machine. What leaves a capture is a
|
||||
// DESCRIPTOR of a few dozen numbers, never the stream.
|
||||
// consent — two locks on the sensitive senses: a Neuron-level grant AND
|
||||
// the OS TCC permission. Camera and mic FAIL CLOSED without
|
||||
// both. The speaker is disclosed but not gated (see below).
|
||||
// disclosed — every device touch prints a [peripheral] line on stderr.
|
||||
// Nothing here is ever silent about being a device.
|
||||
|
||||
// ── Disclosure ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// stderr, not stdout: a program that announces "I am opening the microphone" on
|
||||
// stdout has corrupted its own output. And flushed immediately, so the line is
|
||||
// on the terminal BEFORE the device is touched — a disclosure that arrives
|
||||
// after the fact is a log, not a disclosure.
|
||||
|
||||
fn organ_disclose(msg: String) -> Bool {
|
||||
eprintln(" [peripheral] " + msg)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Consent, the Neuron-level lock ───────────────────────────────────────────
|
||||
//
|
||||
// The OS has its own lock (TCC) and it is not enough on its own: TCC grants the
|
||||
// TERMINAL access to the microphone, once, more or less forever. That says the
|
||||
// user trusts the app. It does not say the user consents to THIS program
|
||||
// listening THIS time. So Neuron keeps its own grant, revocable, on the same
|
||||
// footing — and both must be open for a sensitive sense to work.
|
||||
//
|
||||
// Stored next to the organ rather than in the engram deliberately: consent must
|
||||
// be inspectable and revocable without a running graph, and a permission that
|
||||
// can only be revoked by the system it governs is not a permission.
|
||||
|
||||
fn organ_consent_path() -> String {
|
||||
let home: String = env("PERIPH_HOME")
|
||||
if str_eq(home, "") {
|
||||
return "peripheral/.consent.json"
|
||||
}
|
||||
return home + "/.consent.json"
|
||||
}
|
||||
|
||||
fn organ_consent_granted(device: String) -> Bool {
|
||||
let raw: String = fs_read(organ_consent_path())
|
||||
if str_eq(raw, "") {
|
||||
return false
|
||||
}
|
||||
// A device is granted only on an explicit true. Anything unparseable,
|
||||
// missing or malformed reads as NOT granted — the failure direction for a
|
||||
// permission file is always closed.
|
||||
let key: String = "\"" + device + "\""
|
||||
let at: Int = str_index_of(raw, key)
|
||||
if at < 0 {
|
||||
return false
|
||||
}
|
||||
let tail: String = str_slice(raw, at, str_len(raw))
|
||||
let t: Int = str_index_of(tail, "true")
|
||||
let f: Int = str_index_of(tail, "false")
|
||||
if t < 0 {
|
||||
return false
|
||||
}
|
||||
if f < 0 {
|
||||
return true
|
||||
}
|
||||
// whichever token appears first after the key is this device's value
|
||||
if t < f {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_consent_write(camera: Bool, mic: Bool) -> Bool {
|
||||
let c: String = "false"
|
||||
if camera {
|
||||
c = "true"
|
||||
}
|
||||
let m: String = "false"
|
||||
if mic {
|
||||
m = "true"
|
||||
}
|
||||
return fs_write(organ_consent_path(), "{\"camera\": " + c + ", \"mic\": " + m + "}\n")
|
||||
}
|
||||
|
||||
fn organ_grant(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = true
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = true
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("granted '" + device + "' (Neuron-level) — raw stream stays local, never egresses.")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_revoke(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = false
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = false
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("revoked '" + device + "' (Neuron-level).")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_consent_status() -> String {
|
||||
let cam: String = "denied"
|
||||
if organ_consent_granted("camera") {
|
||||
cam = "granted"
|
||||
}
|
||||
let mic: String = "denied"
|
||||
if organ_consent_granted("mic") {
|
||||
mic = "granted"
|
||||
}
|
||||
return "camera=" + cam + " mic=" + mic
|
||||
}
|
||||
|
||||
// Both locks, in order, with a disclosure for each outcome. Returns false and
|
||||
// says exactly which lock is shut — a refusal that does not say why is
|
||||
// indistinguishable from a bug.
|
||||
fn organ_may_listen() -> Bool {
|
||||
if organ_consent_granted("mic") == false {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (Neuron-level). Run: organ grant mic")
|
||||
return false
|
||||
}
|
||||
if mic_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (OS/TCC), or no input device. Grant microphone access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'mic' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
fn organ_may_see() -> Bool {
|
||||
if organ_consent_granted("camera") == false {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (Neuron-level). Run: organ grant camera")
|
||||
return false
|
||||
}
|
||||
if camera_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (OS/TCC), or no capture device. Grant camera access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'camera' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── SPEAKER (efferent) ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Not consent-gated, and that is a deliberate asymmetry rather than an
|
||||
// oversight. The microphone and camera take information OFF the user without
|
||||
// them necessarily knowing; the speaker puts information INTO a room the user
|
||||
// is in, audibly, which is self-disclosing by its nature — you cannot secretly
|
||||
// speak aloud. So the speaker is DISCLOSED (every utterance announces itself on
|
||||
// stderr) but not gated. Gating it would mean Neuron needs permission to answer.
|
||||
|
||||
fn organ_speak_samples(samples: [Int], sr: Int) -> Bool {
|
||||
let n: Int = native_list_len(samples)
|
||||
if n <= 0 {
|
||||
organ_disclose("SPEAKER: nothing to say (0 samples) — not touching the device.")
|
||||
return false
|
||||
}
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build (" + speaker_name() + ") — cannot speak.")
|
||||
return false
|
||||
}
|
||||
let secs: Int = n * 1000 / sr
|
||||
organ_disclose("SPEAKER: playing " + int_to_str(n) + " samples (" + int_to_str(secs) + " ms @ " + int_to_str(sr) + " Hz) ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_pcm16(samples, sr)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_speak_wav(path: String) -> Bool {
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build — cannot speak.")
|
||||
return false
|
||||
}
|
||||
if fs_exists(path) == false {
|
||||
organ_disclose("SPEAKER: no such file: " + path)
|
||||
return false
|
||||
}
|
||||
organ_disclose("SPEAKER: playing '" + path + "' ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_wav(path)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
// ── The voice, fetched FROM THE ENGRAM ───────────────────────────────────────
|
||||
//
|
||||
// This is the part that matters most and is easiest to get subtly wrong. A
|
||||
// voice is not a constant in code and it is not a JSON file next to the code —
|
||||
// it is a region of the graph, put there by having heard someone, and the organ
|
||||
// retrieves it the way anything retrieves a memory: by asking.
|
||||
//
|
||||
// The node content is the geometry, in the engram's own flat key=value form:
|
||||
// voice will | f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531 ...
|
||||
// so the read is: query the graph, take the returned node, pull the numbers off
|
||||
// it. Nothing here opens a file.
|
||||
//
|
||||
// Returns [f0, f0_end, kf, f1, f2, f3], or an EMPTY list when the region is not
|
||||
// in the graph. Empty is the honest answer — a caller that gets no voice must
|
||||
// not be handed a plausible default and left unable to tell the difference
|
||||
// between "this is how they sound" and "I never heard them."
|
||||
|
||||
// Read an unsigned integer that follows `key` in `s`. Stops at the first
|
||||
// non-digit, returns 0 when the key is absent.
|
||||
fn organ_int_after(s: String, key: String) -> Int {
|
||||
let at: Int = str_index_of(s, key)
|
||||
if at < 0 {
|
||||
return 0
|
||||
}
|
||||
let i: Int = at + str_len(key)
|
||||
let n: Int = str_len(s)
|
||||
let v: Int = 0
|
||||
let seen: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c < 48 {
|
||||
i = n
|
||||
} else {
|
||||
if c > 57 {
|
||||
i = n
|
||||
} else {
|
||||
v = v * 10 + (c - 48)
|
||||
seen = seen + 1
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if seen == 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Ask the engram for a named voice region and read its geometry back.
|
||||
fn organ_voice_fetch(name: String) -> [Int] {
|
||||
let out: [Int] = native_list_empty()
|
||||
let marker: String = "voice " + name + " |"
|
||||
// The graph is asked by MEANING, not by id or by path.
|
||||
let hits: String = engram_search_json("voice " + name + " f0 formants", 12)
|
||||
let at: Int = str_index_of(hits, marker)
|
||||
if at < 0 {
|
||||
// Fall back to a scan of the resident graph before giving up: search is
|
||||
// geometric and a small graph may not rank the region first.
|
||||
let scan: String = engram_scan_nodes_json(500, 0)
|
||||
at = str_index_of(scan, marker)
|
||||
if at < 0 {
|
||||
organ_disclose("VOICE: no region for '" + name + "' in the engram — nothing to speak with.")
|
||||
return out
|
||||
}
|
||||
hits = scan
|
||||
}
|
||||
let win: String = str_slice(hits, at, at + 240)
|
||||
out = native_list_append(out, organ_int_after(win, "f0="))
|
||||
out = native_list_append(out, organ_int_after(win, "f0_end="))
|
||||
out = native_list_append(out, organ_int_after(win, "kf="))
|
||||
out = native_list_append(out, organ_int_after(win, "f1="))
|
||||
out = native_list_append(out, organ_int_after(win, "f2="))
|
||||
out = native_list_append(out, organ_int_after(win, "f3="))
|
||||
organ_disclose("VOICE: fetched '" + name + "' FROM THE ENGRAM — f0=" + int_to_str(native_list_get(out, 0)) + " f0_end=" + int_to_str(native_list_get(out, 1)) + " kf=" + int_to_str(native_list_get(out, 2)) + " f1=" + int_to_str(native_list_get(out, 3)) + " f2=" + int_to_str(native_list_get(out, 4)) + " f3=" + int_to_str(native_list_get(out, 5)))
|
||||
return out
|
||||
}
|
||||
|
||||
// Put a measured voice INTO the engram as geometry. This is the afferent end of
|
||||
// the same wire: a voiceprint (organ_dsp.el's LPC analysis) becomes a node, and
|
||||
// from then on the voice is a memory rather than a measurement someone happened
|
||||
// to write down. `prov` carries the honesty: COARSE means one formant triple, no
|
||||
// coarticulation, no prosody — an impression, explicitly not a clone.
|
||||
fn organ_voice_ingest(name: String, f0: Int, f0_end: Int, kf: Int, f1: Int, f2: Int, f3: Int, src: String, prov: String) -> String {
|
||||
let hub: String = engram_node("voice-signature-set " + name + " grounding=measured src=" + src, "VoiceSet", 90)
|
||||
let body: String = "voice " + name + " | f0=" + int_to_str(f0) + " f0_end=" + int_to_str(f0_end) + " kf=" + int_to_str(kf) + " f1=" + int_to_str(f1) + " f2=" + int_to_str(f2) + " f3=" + int_to_str(f3) + " grounding=measured src=" + src + " prov=" + prov
|
||||
let vid: String = engram_node(body, "Voice", 90)
|
||||
engram_connect(hub, vid, 90, "has-signature")
|
||||
organ_disclose("VOICE: ingested '" + name + "' into the engram as geometry (node " + vid + ").")
|
||||
return vid
|
||||
}
|
||||
|
||||
// Turn the fetched geometry into the voice slot-map the render consumes. Kept
|
||||
// separate from the fetch so the organ never invents a voice: if the fetch came
|
||||
// back empty this returns empty too, and the caller has to deal with it.
|
||||
fn organ_voice_profile(name: String, g: [Int]) -> [String] {
|
||||
let empty: [String] = native_list_empty()
|
||||
if native_list_len(g) < 6 {
|
||||
return empty
|
||||
}
|
||||
return voice_new(name, native_list_get(g, 0), native_list_get(g, 1), native_list_get(g, 2), 1000, 1000, 8)
|
||||
}
|
||||
|
||||
// ── Own-core tone ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The smallest possible proof that the organ owns its medium end to end: a sine
|
||||
// with a gentle attack and release, computed here, played by us, no file and no
|
||||
// library anywhere in the path.
|
||||
fn organ_tone(hz: Int, ms: Int, sr: Int) -> [Int] {
|
||||
let n: Int = sr * ms / 1000
|
||||
let out: [Int] = native_list_empty()
|
||||
let two_pi: Float = 6.283185307
|
||||
let srf: Float = int_to_float(sr)
|
||||
let hzf: Float = int_to_float(hz)
|
||||
let i: Int = 0
|
||||
// 20 ms of ramp at each end; a square-edged tone clicks, and a click is the
|
||||
// organ announcing that it does not understand envelopes.
|
||||
let ramp: Int = sr / 50
|
||||
if ramp < 1 {
|
||||
ramp = 1
|
||||
}
|
||||
while i < n {
|
||||
let t: Float = int_to_float(i) / srf
|
||||
let s: Float = math_sin(two_pi * hzf * t)
|
||||
let env: Int = 32767
|
||||
if i < ramp {
|
||||
env = 32767 * i / ramp
|
||||
}
|
||||
let tail: Int = n - i
|
||||
if tail < ramp {
|
||||
env = 32767 * tail / ramp
|
||||
}
|
||||
let v: Int = float_to_int(s * 9000.0) * env / 32767
|
||||
out = native_list_append(out, v)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user