diff --git a/CHANGELOG.md b/CHANGELOG.md index 5968de9..5abccb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to EigenScript are documented here. ## [Unreleased] +### Fixed +- **gfx audio: `audio_play` / `audio_play_loop` accept `VAL_BUFFER` + samples (#578).** `audio_convert_samples` was list-only, so handing a + buffer — the type the language recommends for bulk samples and what a + DAW render produces — silently returned channel `0`; consumers had to + convert buffer→list per play (a 3-minute stereo render is ~16M + appends + VAL_NUM allocations) just for the C side to re-walk it into + int16. Buffers now convert directly off their C double array (same + clamping and 64 MB cap as lists); empty buffers are rejected like + empty lists. Gfx-suite checks cover play, play_loop, clamping, the + queued length, and the empty-buffer rejection. + ### Added - **`read_line` builtin (#558)** — blocking line read from stdin (`getline(3)`): returns the next line without its trailing newline diff --git a/src/ext_gfx.c b/src/ext_gfx.c index 14345c2..0ea5d7d 100644 --- a/src/ext_gfx.c +++ b/src/ext_gfx.c @@ -730,10 +730,29 @@ static void audio_mix_callback(void *ud, uint8_t *stream, int len) { } } -/* Convert a sample list (floats -1..1) to an owned int16 buffer. - * Returns NULL on bad shape or an over-64MB clip. */ +/* Convert samples (floats -1..1) to an owned int16 buffer. Accepts a + * VAL_LIST or a VAL_BUFFER (#578 — the buffer is the type the language + * recommends for bulk samples and what a DAW render produces; forcing a + * buffer→list conversion per play meant millions of appends + VAL_NUM + * allocations just to feed the mixer). Returns NULL on bad shape or an + * over-64MB clip. */ static int16_t* audio_convert_samples(Value *samples, int *out_n) { - if (!samples || samples->type != VAL_LIST) return NULL; + if (!samples) return NULL; + if (samples->type == VAL_BUFFER) { + int n = samples->data.buffer.count; + if (n <= 0 || (double)n * sizeof(int16_t) > 64.0 * 1024.0 * 1024.0) return NULL; + int16_t *buf = xmalloc_array(n, sizeof(int16_t)); + const double *src = samples->data.buffer.data; + for (int i = 0; i < n; i++) { + double s = src[i]; + if (s > 1.0) s = 1.0; + if (s < -1.0) s = -1.0; + buf[i] = (int16_t)(s * 32767); + } + *out_n = n; + return buf; + } + if (samples->type != VAL_LIST) return NULL; int n = samples->data.list.count; if (n <= 0 || (double)n * sizeof(int16_t) > 64.0 * 1024.0 * 1024.0) return NULL; int16_t *buf = xmalloc_array(n, sizeof(int16_t)); diff --git a/tests/test_audio.eigs b/tests/test_audio.eigs index bd7043a..e0cb5b2 100644 --- a/tests/test_audio.eigs +++ b/tests/test_audio.eigs @@ -117,6 +117,35 @@ if d != 0: assert_eq of [audio_stop of chinf, 0, "stop on an inactive channel returns 0"] audio_clear of null assert_eq of [audio_queue_size of null, 0, "clear empties every channel"] + + # ---- #578: VAL_BUFFER samples accepted (the bulk fast path) ---- + # A DAW render produces C-backed double buffers; audio_play used to + # silently return channel 0 for them, forcing a buffer->list convert + # per play. Buffer and list of the same samples must both play. + nb is 1000 + bclip is buffer of nb + bi is 0 + loop while bi < nb: + bclip[bi] is 0.1 + bi += 1 + chb is audio_play of bclip + assert_eq of [(chb > 0), 1, "audio_play accepts a buffer (#578)"] + assert_eq of [audio_queue_size of null, nb * 2, "buffer clip queued at full length"] + chbl is audio_play_loop of [bclip, 3] + assert_eq of [(chbl > 0), 1, "audio_play_loop accepts a buffer (#578)"] + assert_eq of [audio_stop of chbl, 1, "buffer loop channel stoppable"] + # Buffer values are clamped like list values (out-of-range -> +/-1). + hot is buffer of 100 + hi is 0 + loop while hi < 100: + hot[hi] is 9.5 + hi += 1 + chh is audio_play of hot + assert_eq of [(chh > 0), 1, "out-of-range buffer samples clamp, still play"] + # Empty buffer is still rejected (0), matching the empty-list rule. + assert_eq of [audio_play of (buffer of 0), 0, "empty buffer rejected"] + + audio_clear of null audio_close of null print of "mixer channel tests: ok" else: