organ: the rest of the peripheral moves into El
El SDK CI - dev / build-and-test (pull_request) Failing after 4m18s
El SDK CI - dev / build-and-test (pull_request) Failing after 4m18s
The speaker and the voice-fetch landed in the previous commit. This is the remainder of the 939-line Swift program, ported, and the line it draws is between DEVICE and ARITHMETIC rather than between languages. Two things stay realizers, because they are the two things El cannot express as arithmetic: handing a buffer to the DAC and waiting for it to drain (el_audio_darwin.m), and asking the OS for samples off a mic or frames off a camera (el_capture_darwin.m). Both are their own translation units declared in el_runtime.h, never patches to el_runtime.c. Everything else is El. WAV decode, LPC autocorrelation, Levinson-Durbin at order 16, formant extraction off the all-pole envelope, source-filter resynthesis, and the three descriptors are organ_dsp.el. Consent, disclosure and the scene descriptor are organ.el. Barge-in, yield-or-hold, backchannel and resume are organ_converse.el. The organ never learns a word. Codes and phoneme geometry arrive from the language side; the organ turns them into samples and gets the samples out the speaker, and runs the same trip in reverse for the senses. No lexicon, no grapheme-to-phoneme, by design. Barge-in needed pause/resume and a real DAC position rather than a tick counter, because "finish the buffer" is not barge-in and a queue holding three buffers is a third of a second wrong about where it is. An injected barge also had to fire once rather than stay true, which is otherwise a livelock the moment a backchannel resumes. Measured against the Swift on out/mic_room.wav: seconds, rms, peak, zcr, centroid and F0 agree to every printed digit; formants F1-F5 and bandwidths B1-B5 are identical. imitate cannot match bit-for-bit because the Swift excites unvoiced frames with Double.random — two Swift runs correlate 0.957 with each other and El correlates 0.958 with Swift, so the port is as close to the original as the original is to itself. Verified end to end: consent fails closed on both locks, real mic capture (16000 frames), real camera frame (1920x1080 -> 15 numbers), voiceprint, imitate, hear-imitate, a voice learned by ear and fetched back out of the engram, and all five converse paths with real audio. The binary contains zero afplay/Swift strings and spawns no child process while speaking.
This commit is contained in:
@@ -314,6 +314,25 @@ el_val_t speaker_played_frames(void) {
|
||||
return (el_val_t)g_aq_state->pos;
|
||||
}
|
||||
|
||||
/* Pause where we are, keeping the queue and its position intact.
|
||||
*
|
||||
* This is the difference between barge-in and "finish the buffer". The moment
|
||||
* the microphone hears speech, output must stop AT THAT SAMPLE — a listener
|
||||
* experiences even 200ms of continued talking as being talked over. Pause
|
||||
* rather than stop because the interruption might turn out to be a backchannel
|
||||
* ("mm-hm"), and the right response to a backchannel is to carry on as though
|
||||
* nothing happened, which requires the queue to still be exactly where it was.
|
||||
* A stop-and-restart would re-attack the buffer and be audible as a stutter. */
|
||||
el_val_t speaker_pause(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueuePause(g_aq) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_resume(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueueStart(g_aq, NULL) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_stop(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
/* immediate: do NOT let the queue finish what it is holding */
|
||||
@@ -322,6 +341,131 @@ el_val_t speaker_stop(void) {
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Decode a 16-bit RIFF/WAVE into a freshly malloc'd mono int16 buffer.
|
||||
* Returns frames, or 0 on any failure; *out is set only on success. Shared by
|
||||
* the blocking and async WAV paths. */
|
||||
static int64_t el_wav_load(const char* path, int16_t** out, int32_t* out_sr) {
|
||||
if (!path || !out) return 0;
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
||||
long size = ftell(f);
|
||||
if (size <= 44) { fclose(f); return 0; }
|
||||
rewind(f);
|
||||
unsigned char* d = (unsigned char*)malloc((size_t)size);
|
||||
if (!d) { fclose(f); return 0; }
|
||||
size_t got = fread(d, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(d); return 0; }
|
||||
if (memcmp(d, "RIFF", 4) != 0 || memcmp(d + 8, "WAVE", 4) != 0) { free(d); return 0; }
|
||||
|
||||
int32_t sr = 0, channels = 0, bits = 0;
|
||||
long dataOff = -1, dataLen = 0, o = 12;
|
||||
/* Chunk-walk rather than assuming fmt-then-data at fixed offsets: recorders
|
||||
* routinely interleave JUNK/FLLR padding, and a fixed-offset parser reads
|
||||
* padding as audio. */
|
||||
while (o + 8 <= size) {
|
||||
long sz = (long)d[o+4] | ((long)d[o+5] << 8) | ((long)d[o+6] << 16) | ((long)d[o+7] << 24);
|
||||
if (sz < 0) break;
|
||||
if (memcmp(d + o, "fmt ", 4) == 0 && o + 24 <= size) {
|
||||
channels = (int32_t)(d[o+10] | (d[o+11] << 8));
|
||||
sr = (int32_t)((long)d[o+12] | ((long)d[o+13] << 8) | ((long)d[o+14] << 16) | ((long)d[o+15] << 24));
|
||||
bits = (int32_t)(d[o+22] | (d[o+23] << 8));
|
||||
} else if (memcmp(d + o, "data", 4) == 0) {
|
||||
dataOff = o + 8;
|
||||
dataLen = sz;
|
||||
if (dataOff + dataLen > size) dataLen = size - dataOff;
|
||||
}
|
||||
o += 8 + sz + (sz & 1);
|
||||
}
|
||||
if (dataOff < 0 || sr <= 0 || bits != 16 || channels < 1 || dataLen <= 0) { free(d); return 0; }
|
||||
|
||||
long frames = dataLen / (2 * channels);
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)frames * sizeof(int16_t));
|
||||
if (!pcm) { free(d); return 0; }
|
||||
for (long i = 0; i < frames; i++) {
|
||||
long b = dataOff + i * 2 * channels;
|
||||
pcm[i] = (int16_t)((unsigned)d[b] | ((unsigned)d[b+1] << 8));
|
||||
}
|
||||
free(d);
|
||||
*out = pcm;
|
||||
if (out_sr) *out_sr = sr;
|
||||
return (int64_t)frames;
|
||||
}
|
||||
|
||||
/* Async WAV playback. converse speaks PRE-RENDERED segments and must keep
|
||||
* listening while it does, so it needs the file on the queue without blocking
|
||||
* and needs to be able to stop it mid-buffer. Going through the file rather
|
||||
* than an El [Int] also avoids marshalling a million-element list per segment
|
||||
* for audio the caller never intends to look at. */
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p) return (el_val_t)0;
|
||||
|
||||
el_audio_teardown();
|
||||
|
||||
int32_t sr = 0;
|
||||
int16_t* pcm = NULL;
|
||||
int64_t frames = el_wav_load(p, &pcm, &sr);
|
||||
if (frames <= 0 || !pcm) { free(pcm); return (el_val_t)0; }
|
||||
|
||||
g_aq_pcm = pcm;
|
||||
g_aq_sr = sr;
|
||||
g_aq_state = (ElAqState*)calloc(1, sizeof(ElAqState));
|
||||
if (!g_aq_state) { el_audio_teardown(); return (el_val_t)0; }
|
||||
g_aq_state->pcm = g_aq_pcm;
|
||||
g_aq_state->frames = frames;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sr;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, g_aq_state, NULL, NULL, 0, &g_aq) != noErr || !g_aq) {
|
||||
el_audio_teardown();
|
||||
return (el_val_t)0;
|
||||
}
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
int64_t remain = g_aq_state->frames - g_aq_state->pos;
|
||||
if (remain <= 0) break;
|
||||
AudioQueueBufferRef b = NULL;
|
||||
if (AudioQueueAllocateBuffer(g_aq, EL_AQ_FRAMES * sizeof(int16_t), &b) != noErr) break;
|
||||
int64_t k = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(b->mAudioData, g_aq_state->pcm + g_aq_state->pos, (size_t)k * sizeof(int16_t));
|
||||
b->mAudioDataByteSize = (UInt32)(k * (int64_t)sizeof(int16_t));
|
||||
g_aq_state->pos += k;
|
||||
if (AudioQueueEnqueueBuffer(g_aq, b, 0, NULL) != noErr) break;
|
||||
g_aq_state->inflight++;
|
||||
}
|
||||
if (g_aq_state->inflight == 0) { el_audio_teardown(); return (el_val_t)0; }
|
||||
if (AudioQueueStart(g_aq, NULL) != noErr) { el_audio_teardown(); return (el_val_t)0; }
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Total frames and sample rate of a WAV, without playing it — wav-info, and the
|
||||
* duration converse needs to compute progress through a segment. */
|
||||
el_val_t wav_frames(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)n;
|
||||
}
|
||||
|
||||
el_val_t wav_rate(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(n > 0 ? sr : 0);
|
||||
}
|
||||
|
||||
/* Play a 16-bit mono RIFF/WAVE file. Present because the render already knows
|
||||
* how to write a WAV and a caller may reasonably want to hear one back without
|
||||
* re-rendering it; the parse is deliberately minimal and chunk-walking, so the
|
||||
|
||||
@@ -42,10 +42,23 @@ el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate) {
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_pause(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_resume(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_playing(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_stop(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_played_frames(void) { return (el_val_t)0; }
|
||||
|
||||
/* WAV geometry is pure parsing and would work fine here, but reporting a
|
||||
* duration for audio this build cannot play would invite a caller to sequence
|
||||
* around a silence. Refuse consistently with the rest of the file. */
|
||||
el_val_t wav_frames(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
el_val_t wav_rate(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
|
||||
/* ── Microphone ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t mic_available(void) { return (el_val_t)0; }
|
||||
|
||||
@@ -126,10 +126,18 @@ el_val_t speaker_play_wav(el_val_t path); /* 16-bit mono RIFF/WAVE; 1 o
|
||||
* 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_play_wav_async(el_val_t path);
|
||||
el_val_t speaker_pause(void); /* stop AT THIS SAMPLE, keep position */
|
||||
el_val_t speaker_resume(void); /* carry on from exactly there */
|
||||
el_val_t speaker_playing(void);
|
||||
el_val_t speaker_stop(void);
|
||||
el_val_t speaker_played_frames(void);
|
||||
|
||||
/* WAV geometry without playing — wav-info, and the segment duration converse
|
||||
* needs to turn elapsed time into progress. */
|
||||
el_val_t wav_frames(el_val_t path);
|
||||
el_val_t wav_rate(el_val_t path);
|
||||
|
||||
/* 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
|
||||
|
||||
Reference in New Issue
Block a user