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:
Neuron
2026-08-16 16:27:30 -05:00
parent 95a05109d1
commit 5503e1d9a4
6 changed files with 1884 additions and 3 deletions
+78
View File
@@ -80,6 +80,84 @@ void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
/* stderr counterpart of println (defined in el_seed.c). El could write to
* stdout and nowhere else, which is right for a program's RESULT and wrong for
* everything about how that result was produced. Disclosure especially has to
* leave on a stream the caller can separate from the answer: a program that
* announces "I am about to open the microphone" on stdout has corrupted its own
* output. Flushed on every call, so a disclosure reaches the terminal BEFORE
* the device it describes is touched rather than whenever the buffer drains. */
void eprintln(el_val_t s);
/* ── Peripheral: the speaker, the microphone, the camera ─────────────────────
*
* El's I/O organ. Implemented per platform in its OWN translation unit —
* el_audio_darwin.m / el_capture_darwin.m on Darwin, el_peripheral_null.c
* everywhere else — so El code that speaks or listens links on every platform
* and merely reports having no device where there isn't one. Declared here and
* deliberately NOT implemented in el_runtime.c: acquiring a device must not
* mean editing the middle of the language, the same rule the realizer registry
* follows for modalities.
*
* These are the ONLY parts of the organ that are not El. Everything above the
* sample buffer — WAV encode/decode, LPC autocorrelation, Levinson-Durbin,
* formant extraction, source-filter resynthesis, the compact descriptors, the
* converse decision loop — is arithmetic, and arithmetic belongs in El. What
* remains here is what El cannot express: handing a buffer to the DAC and
* waiting for it to drain, and asking the OS for frames off a capture device.
*
* Local by construction: none of these entry points has a network path. Samples
* and pixels go to and from local hardware and nowhere else. Consent is
* enforced ABOVE this layer in El (peripheral/src/organ.el) for the Neuron-level
* grant, and BELOW it by the OS for TCC; capture fails closed on either. */
/* Speaker (efferent). speaker_play_pcm16 BLOCKS until the audio has actually
* been played rather than merely queued, so a caller can sequence utterances
* without guessing durations and without clipping each tail. */
el_val_t speaker_available(void); /* 1 if a real speaker backs this build */
el_val_t speaker_name(void); /* backend id, e.g. "coreaudio-audioqueue" */
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate); /* [Int] 16-bit mono; 1 ok */
el_val_t speaker_play_wav(el_val_t path); /* 16-bit mono RIFF/WAVE; 1 ok */
/* Asynchronous playback — required by converse, which must keep listening while
* it speaks and must be able to stop ON THE SPOT mid-buffer. A blocking play
* cannot be interrupted, and "finish the current buffer" is not barge-in.
* speaker_stop() halts output immediately; speaker_playing() reports whether
* the hardware is still going; speaker_played_frames() is how far it actually
* got, which is what makes an interrupted utterance resumable at the sample. */
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate);
el_val_t speaker_playing(void);
el_val_t speaker_stop(void);
el_val_t speaker_played_frames(void);
/* Microphone (afferent). Fails CLOSED: returns 0 unless the OS has granted
* capture access. mic_capture_pcm16 blocks for `seconds` and returns an [Int]
* of 16-bit mono samples at `sample_rate` — the raw stream is handed to El and
* never written anywhere by this layer. mic_available() reports device +
* permission state without prompting. */
el_val_t mic_available(void); /* 1 device present AND OS-authorized */
el_val_t mic_request_access(void); /* prompt once; 1 if granted */
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate); /* [Int], empty on refusal */
/* Live monitoring for full-duplex converse. mic_monitor_start enables the OS
* voice-processing unit (acoustic echo cancellation) so the microphone does not
* hear the speaker — without AEC, Neuron barges in on its own voice and
* turn-taking is unusable in a real room. mic_monitor_rms returns the current
* short-window RMS as a Float in 0..1. */
el_val_t mic_monitor_start(void); /* 1 ok; 2 = started but AEC unavailable */
el_val_t mic_monitor_rms(void); /* Float */
el_val_t mic_monitor_stop(void);
/* Camera (afferent). Fails CLOSED like the microphone. camera_capture_rgb
* returns a Map with width/height and the frame as an [Int] of packed RGB
* bytes, so the descriptor arithmetic can happen in El rather than here.
* camera_capture_jpeg writes an encoded frame via ImageIO, which is a codec and
* not something El should re-implement. */
el_val_t camera_available(void);
el_val_t camera_request_access(void);
el_val_t camera_capture_rgb(void); /* Map{width,height,pixels:[Int]} or 0 */
el_val_t camera_capture_jpeg(el_val_t path); /* 1 ok */
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t a, el_val_t b);