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:
+130
-3
@@ -154,9 +154,18 @@ static void seed_request_start(void) {
|
||||
* file still links on its own. */
|
||||
__attribute__((weak)) void el_str_cache_flush(void);
|
||||
|
||||
/* Byte-buffer capacity registry (defined below, next to the string
|
||||
* primitives). The arena frees the pointers it tracked, so any capacity
|
||||
* entry for those addresses must go with them — otherwise a later malloc
|
||||
* reusing the address would inherit a stale width. */
|
||||
static void seed_cap_drop(const char* p);
|
||||
|
||||
static void seed_request_end(void) {
|
||||
_seed_arena_on = 0;
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]);
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) {
|
||||
seed_cap_drop(_seed_arena.ptrs[i]);
|
||||
free(_seed_arena.ptrs[i]);
|
||||
}
|
||||
_seed_arena.count = 0;
|
||||
if (el_str_cache_flush) el_str_cache_flush(); /* freed pointers may be reused */
|
||||
}
|
||||
@@ -188,6 +197,114 @@ static char* seed_strbuf(size_t n) {
|
||||
|
||||
static el_val_t seed_wrap_str(char* s) { return EL_STR(s); }
|
||||
|
||||
/* ── Byte-buffer capacity registry ────────────────────────────────────────────
|
||||
* A String produced by __str_alloc is a fixed-size BYTE BUFFER, not text. Its
|
||||
* length is the capacity it was asked for; strlen() is meaningless on it,
|
||||
* because the buffer is zero-filled and binary content (PCM audio, RIFF
|
||||
* headers, image rasters) contains NUL bytes by nature.
|
||||
*
|
||||
* Before this registry existed, __str_set_char bounds-checked the write index
|
||||
* against strlen(p). For a freshly __str_alloc'd buffer strlen(p) == 0, so the
|
||||
* check `idx >= len` rejected EVERY index and the function was a total no-op:
|
||||
* every El program that built bytes this way wrote a file of pure zeros and
|
||||
* still saw a success return. That is why El's own-core WAV writer emitted
|
||||
* 55,244 silent bytes with a correct-looking header length and no header.
|
||||
*
|
||||
* The fix cannot be "trust the index", because that removes the bound. It also
|
||||
* cannot be a length header stored behind the pointer, because __str_set_char
|
||||
* accepts any String — including a string literal in .rodata, where reading the
|
||||
* bytes preceding the pointer is undefined and may fault. So capacity is kept
|
||||
* in a side table keyed by the pointer itself: allocation registers, the arena
|
||||
* sweep unregisters, and anything not registered keeps the exact strlen
|
||||
* behaviour it had before. Text semantics are unchanged; byte buffers gain the
|
||||
* bound they always should have had. */
|
||||
|
||||
typedef struct {
|
||||
char* ptr; /* NULL = empty slot, (char*)1 = tombstone */
|
||||
size_t cap;
|
||||
} SeedCapEntry;
|
||||
|
||||
#define SEED_CAP_TOMB ((char*)1)
|
||||
|
||||
static _Thread_local SeedCapEntry* _seed_cap = NULL;
|
||||
static _Thread_local size_t _seed_cap_mask = 0; /* table size - 1 */
|
||||
static _Thread_local size_t _seed_cap_used = 0; /* live + tombstoned */
|
||||
|
||||
static size_t seed_cap_hash(const char* p) {
|
||||
uintptr_t h = (uintptr_t)p >> 4; /* malloc alignment: low bits are dead */
|
||||
h *= (uintptr_t)0x9E3779B97F4A7C15ull;
|
||||
return (size_t)(h >> 32);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap);
|
||||
|
||||
static void seed_cap_grow(void) {
|
||||
size_t old_size = _seed_cap_mask ? _seed_cap_mask + 1 : 0;
|
||||
SeedCapEntry* old = _seed_cap;
|
||||
size_t new_size = old_size ? old_size * 2 : 256;
|
||||
SeedCapEntry* fresh = calloc(new_size, sizeof(SeedCapEntry));
|
||||
if (!fresh) return; /* out of memory: keep old table */
|
||||
_seed_cap = fresh;
|
||||
_seed_cap_mask = new_size - 1;
|
||||
_seed_cap_used = 0;
|
||||
for (size_t i = 0; i < old_size; i++) {
|
||||
if (old[i].ptr && old[i].ptr != SEED_CAP_TOMB) seed_cap_put(old[i].ptr, old[i].cap);
|
||||
}
|
||||
free(old);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap) {
|
||||
if (!p) return;
|
||||
if (!_seed_cap || (_seed_cap_used + 1) * 4 >= (_seed_cap_mask + 1) * 3) {
|
||||
seed_cap_grow();
|
||||
if (!_seed_cap) return;
|
||||
}
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
size_t first_free = (size_t)-1;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (e == p) { _seed_cap[i].cap = cap; return; } /* address reused */
|
||||
if (e == SEED_CAP_TOMB && first_free == (size_t)-1) first_free = i;
|
||||
if (!e) {
|
||||
if (first_free != (size_t)-1) i = first_free; else _seed_cap_used++;
|
||||
_seed_cap[i].ptr = p;
|
||||
_seed_cap[i].cap = cap;
|
||||
return;
|
||||
}
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Capacity of a registered byte buffer, or -1 when the pointer is not one. */
|
||||
static int64_t seed_cap_get(const char* p) {
|
||||
if (!p || !_seed_cap) return -1;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return -1;
|
||||
if (e == (char*)p) return (int64_t)_seed_cap[i].cap;
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
static void seed_cap_drop(const char* p) {
|
||||
if (!p || !_seed_cap) return;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return;
|
||||
if (e == (char*)p) { _seed_cap[i].ptr = SEED_CAP_TOMB; return; }
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Effective addressable length of a String: its buffer capacity when it is a
|
||||
* byte buffer, otherwise strlen. */
|
||||
static int64_t seed_addressable_len(const char* p) {
|
||||
int64_t cap = seed_cap_get(p);
|
||||
return cap >= 0 ? cap : (int64_t)strlen(p);
|
||||
}
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s) {
|
||||
@@ -199,7 +316,7 @@ el_val_t __str_len(el_val_t s) {
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i) {
|
||||
const char* p = EL_CSTR(s);
|
||||
if (!p) return 0;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return 0;
|
||||
return (el_val_t)(unsigned char)p[idx];
|
||||
@@ -210,13 +327,14 @@ el_val_t __str_alloc(el_val_t n) {
|
||||
if (sz < 0) sz = 0;
|
||||
char* buf = seed_strbuf((size_t)sz);
|
||||
memset(buf, 0, (size_t)sz + 1);
|
||||
seed_cap_put(buf, (size_t)sz); /* this is a byte buffer of width sz */
|
||||
return seed_wrap_str(buf);
|
||||
}
|
||||
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
|
||||
char* p = (char*)(uintptr_t)s;
|
||||
if (!p) return s;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return s;
|
||||
p[idx] = (char)(unsigned char)(int64_t)c;
|
||||
@@ -406,6 +524,15 @@ el_val_t __fs_mkdir(el_val_t path) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* stderr counterpart of println. Flushed immediately: a disclosure line is only
|
||||
* worth anything if it lands before the thing it discloses happens. */
|
||||
void eprintln(el_val_t s) {
|
||||
const char* p = EL_CSTR(s);
|
||||
fputs(p ? p : "", stderr);
|
||||
fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) {
|
||||
const char* p = EL_CSTR(path);
|
||||
const char* b = EL_CSTR(bytes);
|
||||
|
||||
Reference in New Issue
Block a user