// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. /* sound.core — raw ALSA PCM playback + a float32 sample buffer. * * Lilush links fully static against musl, so it cannot dlopen ALSA's plugin * layer (dmix, the "default" device, the config tree). That layer is therefore * unavailable no matter what we link, which is exactly why this module talks to * the kernel PCM interface directly: ioctl() on /dev/snd/pcmC*D*p, using the * structures and ioctl numbers from (shipped by the build * container's linux-headers). Zero third-party dependencies; static by * construction; the device fd plugs straight into LEV's wait_writable. * * Because dmix is gone the device is held exclusively, so all mixing happens * in-process. To keep that simple every Lua-facing sample buffer is * interleaved float32 in [-1, 1]; mixing accumulates in float. The hardware may * only accept s16, so the device converts float32 -> s16 internally on write * (the Lua layer never sees hardware formats). * * The hardware-parameter negotiation (the snd_mask / snd_interval dance) has no * helpers in the uapi header — alsa-lib keeps them private — so the small * param_* helpers below are reimplemented here, following tinyalsa's pcm.c. * * Frame offsets at the Lua boundary are 1-indexed to match the rest of the * codebase; they are converted to 0-indexed internally. */ #include #include #include #include #include #include #include #include #include #include #include #define RETURN_ERR(L) \ do { \ lua_pushnil(L); \ lua_pushstring(L, strerror(errno)); \ return 2; \ } while (0) #define DEVICE_MT "sound.device" #define BUFFER_MT "sound.buffer" /* A frame count cap that keeps frames*channels*4 well inside size_t. */ #define MAX_FRAMES (1 << 28) /* oscillator waveforms (sound.synth passes these as small integers) */ #define WAVE_SINE 0 #define WAVE_SQUARE 1 #define WAVE_SAW 2 #define WAVE_TRIANGLE 3 #define WAVE_NOISE 4 /* ── float32 sample buffer ─────────────────────────────────────────────── */ typedef struct { int frames; int channels; float *data; /* frames*channels interleaved f32; NULL only on alloc failure */ } snd_buffer_t; static snd_buffer_t *check_buffer(lua_State *L, int idx) { return (snd_buffer_t *)luaL_checkudata(L, idx, BUFFER_MT); } /* Allocate the userdata first so __gc owns every later failure path. Returns * the buffer with data NULL on OOM and leaves nil+err on the stack (nrets=2); * on success the buffer is on the stack (nrets=1). *nrets is set accordingly. */ static snd_buffer_t *buffer_push(lua_State *L, int frames, int channels, int *nrets) { if (frames < 0 || channels < 1 || channels > 8 || frames > MAX_FRAMES) { luaL_error(L, "invalid buffer geometry %d frames x %d channels", frames, channels); return NULL; /* unreachable */ } snd_buffer_t *b = (snd_buffer_t *)lua_newuserdata(L, sizeof(snd_buffer_t)); b->frames = frames; b->channels = channels; b->data = NULL; luaL_getmetatable(L, BUFFER_MT); lua_setmetatable(L, -2); size_t n = (size_t)frames * (size_t)channels; b->data = (float *)calloc(n ? n : 1, sizeof(float)); if (!b->data) { lua_pushnil(L); lua_pushstring(L, "out of memory allocating sample buffer"); *nrets = 2; return NULL; } *nrets = 1; return b; } /* sound.buffer(frames [, channels=2]) -> buffer | nil, err (zeroed = silence) */ static int l_buffer_new(lua_State *L) { int frames = (int)luaL_checkinteger(L, 1); int channels = (int)luaL_optinteger(L, 2, 2); int nrets; buffer_push(L, frames, channels, &nrets); return nrets; } static int l_buffer_gc(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); free(b->data); b->data = NULL; return 0; } static int l_buffer_frames(lua_State *L) { lua_pushinteger(L, check_buffer(L, 1)->frames); return 1; } static int l_buffer_channels(lua_State *L) { lua_pushinteger(L, check_buffer(L, 1)->channels); return 1; } /* b:clear() — back to silence. */ static int l_buffer_clear(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); memset(b->data, 0, (size_t)b->frames * b->channels * sizeof(float)); return 0; } /* b:tostring() -> raw interleaved f32 bytes (frames*channels*4). */ static int l_buffer_tostring(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); lua_pushlstring(L, (const char *)b->data, (size_t)b->frames * b->channels * sizeof(float)); return 1; } /* dst:mix(src, dst_off, src_off, n [, g1, g2, ...]) -> frames mixed * * Adds n frames of `src` (starting at 1-indexed src_off) into `dst` (starting * at 1-indexed dst_off), scaled by per-channel gains. With one gain it applies * to every channel (master volume); with none, unity. A mono source mixed into * a stereo destination is duplicated to both channels (so mono SFX play in a * stereo bus). Out-of-range regions are clipped to a no-op. This is the mixer's * inner loop. */ static int l_buffer_mix(lua_State *L) { snd_buffer_t *dst = check_buffer(L, 1); snd_buffer_t *src = check_buffer(L, 2); int dst_off = (int)luaL_optinteger(L, 3, 1) - 1; int src_off = (int)luaL_optinteger(L, 4, 1) - 1; int n = (int)luaL_optinteger(L, 5, src->frames); int ch = dst->channels; int mono = (src->channels == 1 && ch == 2); if (src->channels != ch && !mono) return luaL_error(L, "sound.buffer:mix channel mismatch (src %d, dst %d)", src->channels, ch); /* per-channel gains from the variadic tail (arg 6 onward) */ float gain[8]; int ngains = lua_gettop(L) - 5; for (int c = 0; c < ch; c++) gain[c] = 1.0f; if (ngains == 1) { float g = (float)luaL_checknumber(L, 6); for (int c = 0; c < ch; c++) gain[c] = g; } else if (ngains > 1) { for (int c = 0; c < ch && c < ngains; c++) gain[c] = (float)luaL_checknumber(L, 6 + c); } /* clip the region against both buffers */ if (dst_off < 0) { src_off -= dst_off; n += dst_off; dst_off = 0; } if (src_off < 0) { dst_off -= src_off; n += src_off; src_off = 0; } if (n > dst->frames - dst_off) n = dst->frames - dst_off; if (n > src->frames - src_off) n = src->frames - src_off; if (n <= 0) { lua_pushinteger(L, 0); return 1; } float *dp = dst->data + (size_t)dst_off * ch; if (mono) { const float *sp = src->data + src_off; for (int f = 0; f < n; f++) { float s = *sp++; dp[0] += s * gain[0]; dp[1] += s * gain[1]; dp += 2; } } else { const float *sp = src->data + (size_t)src_off * ch; for (int f = 0; f < n; f++) for (int c = 0; c < ch; c++) *dp++ += *sp++ * gain[c]; } lua_pushinteger(L, n); return 1; } /* b:osc(wave, freq, rate, phase, amp, dst_off, n) -> next_phase * * Additively writes an oscillator into [dst_off, dst_off+n) on every channel. * `phase` is in cycles [0,1); the returned phase lets callers chain segments * seamlessly. Used by sound.synth. */ static uint32_t osc_rng = 0x9e3779b9u; /* xorshift state for WAVE_NOISE */ static int l_buffer_osc(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); int wave = (int)luaL_checkinteger(L, 2); double freq = luaL_checknumber(L, 3); double rate = luaL_checknumber(L, 4); double phase = luaL_optnumber(L, 5, 0.0); float amp = (float)luaL_optnumber(L, 6, 1.0); int dst_off = (int)luaL_optinteger(L, 7, 1) - 1; int n = (int)luaL_optinteger(L, 8, b->frames - dst_off); if (rate <= 0.0) return luaL_error(L, "sound.buffer:osc rate must be positive"); if (dst_off < 0) { n += dst_off; dst_off = 0; } if (n > b->frames - dst_off) n = b->frames - dst_off; if (n <= 0) { lua_pushnumber(L, phase); return 1; } int ch = b->channels; double step = freq / rate; float *dp = b->data + (size_t)dst_off * ch; for (int f = 0; f < n; f++) { float v; switch (wave) { case WAVE_SQUARE: v = phase < 0.5 ? 1.0f : -1.0f; break; case WAVE_SAW: v = (float)(2.0 * phase - 1.0); break; case WAVE_TRIANGLE: v = phase < 0.5 ? (float)(4.0 * phase - 1.0) : (float)(3.0 - 4.0 * phase); break; case WAVE_NOISE: osc_rng ^= osc_rng << 13; osc_rng ^= osc_rng >> 17; osc_rng ^= osc_rng << 5; v = (float)((double)osc_rng / 2147483648.0 - 1.0); break; case WAVE_SINE: default: v = sinf((float)(2.0 * M_PI * phase)); break; } v *= amp; for (int c = 0; c < ch; c++) dp[c] += v; dp += ch; phase += step; if (phase >= 1.0) phase -= floor(phase); } lua_pushnumber(L, phase); return 1; } /* b:fade(g0, g1, dst_off, n) — multiply [dst_off, dst_off+n) by a linear gain * ramp from g0 to g1 across all channels (ADSR segments compose from these). */ static int l_buffer_fade(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); double g0 = luaL_checknumber(L, 2); double g1 = luaL_checknumber(L, 3); int dst_off = (int)luaL_optinteger(L, 4, 1) - 1; int n = (int)luaL_optinteger(L, 5, b->frames - dst_off); if (dst_off < 0) { n += dst_off; dst_off = 0; } if (n > b->frames - dst_off) n = b->frames - dst_off; if (n <= 0) return 0; int ch = b->channels; double dg = n > 1 ? (g1 - g0) / (double)(n - 1) : 0.0; float *dp = b->data + (size_t)dst_off * ch; for (int f = 0; f < n; f++) { float g = (float)(g0 + dg * f); for (int c = 0; c < ch; c++) *dp++ *= g; } return 0; } /* b:get(frame, channel) -> sample | nil (1-indexed; nil when out of range) */ static int l_buffer_get(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); int f = (int)luaL_checkinteger(L, 2) - 1; int c = (int)luaL_optinteger(L, 3, 1) - 1; if (f < 0 || f >= b->frames || c < 0 || c >= b->channels) { lua_pushnil(L); return 1; } lua_pushnumber(L, b->data[(size_t)f * b->channels + c]); return 1; } /* b:set(frame, channel, value) — 1-indexed; out-of-range is a silent no-op. */ static int l_buffer_set(lua_State *L) { snd_buffer_t *b = check_buffer(L, 1); int f = (int)luaL_checkinteger(L, 2) - 1; int c = (int)luaL_checkinteger(L, 3) - 1; float v = (float)luaL_checknumber(L, 4); if (f < 0 || f >= b->frames || c < 0 || c >= b->channels) return 0; b->data[(size_t)f * b->channels + c] = v; return 0; } /* ── raw ALSA hw-params helpers (no uapi helpers exist; cf. tinyalsa) ────── */ static int param_is_mask(int p) { return p >= SNDRV_PCM_HW_PARAM_FIRST_MASK && p <= SNDRV_PCM_HW_PARAM_LAST_MASK; } static int param_is_interval(int p) { return p >= SNDRV_PCM_HW_PARAM_FIRST_INTERVAL && p <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; } static struct snd_interval *param_to_interval(struct snd_pcm_hw_params *p, int n) { return &p->intervals[n - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL]; } static struct snd_mask *param_to_mask(struct snd_pcm_hw_params *p, int n) { return &p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK]; } static void param_set_mask(struct snd_pcm_hw_params *p, int n, unsigned int bit) { if (!param_is_mask(n) || bit >= SNDRV_MASK_MAX) return; struct snd_mask *m = param_to_mask(p, n); memset(m->bits, 0, sizeof(m->bits)); m->bits[bit >> 5] |= (1u << (bit & 31)); } static void param_set_int(struct snd_pcm_hw_params *p, int n, unsigned int val) { if (!param_is_interval(n)) return; struct snd_interval *i = param_to_interval(p, n); i->min = val; i->max = val; i->integer = 1; } static unsigned int param_get_int(struct snd_pcm_hw_params *p, int n) { if (param_is_interval(n)) { struct snd_interval *i = param_to_interval(p, n); if (i->integer) return i->max; } return 0; } static void param_init(struct snd_pcm_hw_params *p) { memset(p, 0, sizeof(*p)); for (int n = SNDRV_PCM_HW_PARAM_FIRST_MASK; n <= SNDRV_PCM_HW_PARAM_LAST_MASK; n++) { struct snd_mask *m = param_to_mask(p, n); memset(m->bits, 0xff, sizeof(m->bits)); } for (int n = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; n <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; n++) { struct snd_interval *i = param_to_interval(p, n); i->min = 0; i->max = ~0u; } p->rmask = ~0u; p->cmask = 0; p->info = ~0u; } /* ── PCM playback device ───────────────────────────────────────────────── */ typedef struct { int fd; int channels; int rate; int format; /* SNDRV_PCM_FORMAT_FLOAT_LE or SNDRV_PCM_FORMAT_S16_LE */ int frame_bytes; /* hardware bytes per interleaved frame */ int period; /* frames per period */ int buffer_size; /* frames in the ring buffer */ void *scratch; /* buffer_size frames in hardware format, for f32->hw convert */ } snd_device_t; static snd_device_t *check_device(lua_State *L, int idx) { return (snd_device_t *)luaL_checkudata(L, idx, DEVICE_MT); } static int opt_int(lua_State *L, int t, const char *key, int dflt) { if (!lua_istable(L, t)) return dflt; lua_getfield(L, t, key); int v = lua_isnil(L, -1) ? dflt : (int)luaL_checkinteger(L, -1); lua_pop(L, 1); return v; } /* Try one fixed format. Returns 0 on success (params written to *hp), -1 with * errno set on rejection. */ static int negotiate(int fd, struct snd_pcm_hw_params *hp, int format, int channels, int rate, int period, int periods) { param_init(hp); param_set_mask(hp, SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED); param_set_mask(hp, SNDRV_PCM_HW_PARAM_FORMAT, format); param_set_mask(hp, SNDRV_PCM_HW_PARAM_SUBFORMAT, SNDRV_PCM_SUBFORMAT_STD); param_set_int(hp, SNDRV_PCM_HW_PARAM_CHANNELS, channels); param_set_int(hp, SNDRV_PCM_HW_PARAM_RATE, rate); param_set_int(hp, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, period); param_set_int(hp, SNDRV_PCM_HW_PARAM_PERIODS, periods); if (ioctl(fd, SNDRV_PCM_IOCTL_HW_REFINE, hp) < 0) return -1; if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, hp) < 0) return -1; return 0; } /* sound.open([opts]) -> device | nil, err * * opts: card=0, device=0, rate=48000, channels=2, period=1024, periods=4, * format="auto"|"f32"|"s16". Opens /dev/snd/pcmCDp * non-blocking, negotiates hw/sw params, and PREPAREs the stream. Playback * auto-starts once the ring buffer fills (start_threshold = buffer_size). */ static int l_open(lua_State *L) { int card = opt_int(L, 1, "card", 0); int devno = opt_int(L, 1, "device", 0); int rate = opt_int(L, 1, "rate", 48000); int chans = opt_int(L, 1, "channels", 2); int period = opt_int(L, 1, "period", 1024); int periods = opt_int(L, 1, "periods", 4); const char *fmt = "auto"; if (lua_istable(L, 1)) { lua_getfield(L, 1, "format"); if (!lua_isnil(L, -1)) fmt = luaL_checkstring(L, -1); lua_pop(L, 1); } char path[64]; snprintf(path, sizeof(path), "/dev/snd/pcmC%dD%dp", card, devno); int fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC); if (fd < 0) RETURN_ERR(L); struct snd_pcm_hw_params hp; int chosen = -1; int want_f32 = (strcmp(fmt, "s16") != 0); /* "auto" and "f32" try float first */ if (want_f32 && negotiate(fd, &hp, SNDRV_PCM_FORMAT_FLOAT_LE, chans, rate, period, periods) == 0) { chosen = SNDRV_PCM_FORMAT_FLOAT_LE; } else if (strcmp(fmt, "f32") != 0 && negotiate(fd, &hp, SNDRV_PCM_FORMAT_S16_LE, chans, rate, period, periods) == 0) { chosen = SNDRV_PCM_FORMAT_S16_LE; } if (chosen < 0) { int e = errno; close(fd); errno = e; lua_pushnil(L); lua_pushfstring(L, "no usable PCM format on %s (%s)", path, strerror(errno)); return 2; } int act_period = (int)param_get_int(&hp, SNDRV_PCM_HW_PARAM_PERIOD_SIZE); int act_buffer = (int)param_get_int(&hp, SNDRV_PCM_HW_PARAM_BUFFER_SIZE); if (act_period <= 0) act_period = period; if (act_buffer <= 0) act_buffer = act_period * periods; /* software params: wake the poll once a full period of space exists, and * auto-start playback only when the whole ring buffer is queued. */ struct snd_pcm_sw_params sw; memset(&sw, 0, sizeof(sw)); sw.tstamp_mode = SNDRV_PCM_TSTAMP_NONE; sw.period_step = 1; sw.avail_min = act_period; sw.start_threshold = act_buffer; sw.stop_threshold = act_buffer; sw.silence_threshold = 0; sw.silence_size = 0; sw.boundary = act_buffer; /* boundary must be a power-of-two multiple of buffer_size (alsa-lib idiom) */ while (sw.boundary * 2 <= (0x7fffffffUL)) sw.boundary *= 2; if (ioctl(fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sw) < 0) { int e = errno; close(fd); errno = e; RETURN_ERR(L); } if (ioctl(fd, SNDRV_PCM_IOCTL_PREPARE) < 0) { int e = errno; close(fd); errno = e; RETURN_ERR(L); } int sample_bytes = (chosen == SNDRV_PCM_FORMAT_FLOAT_LE) ? 4 : 2; snd_device_t *d = (snd_device_t *)lua_newuserdata(L, sizeof(snd_device_t)); d->fd = fd; d->channels = chans; d->rate = rate; d->format = chosen; d->frame_bytes = sample_bytes * chans; d->period = act_period; d->buffer_size = act_buffer; d->scratch = NULL; luaL_getmetatable(L, DEVICE_MT); lua_setmetatable(L, -2); /* scratch only needed when we must convert f32 -> s16 on write */ if (chosen == SNDRV_PCM_FORMAT_S16_LE) { d->scratch = malloc((size_t)act_buffer * d->frame_bytes); if (!d->scratch) { /* __gc will close fd; report OOM */ lua_pushnil(L); lua_pushstring(L, "out of memory allocating device scratch"); return 2; } } return 1; } static int l_device_gc(lua_State *L) { snd_device_t *d = check_device(L, 1); if (d->fd >= 0) { close(d->fd); d->fd = -1; } free(d->scratch); d->scratch = NULL; return 0; } static int l_device_fd(lua_State *L) { lua_pushinteger(L, check_device(L, 1)->fd); return 1; } static int l_device_period(lua_State *L) { lua_pushinteger(L, check_device(L, 1)->period); return 1; } static int l_device_channels(lua_State *L) { lua_pushinteger(L, check_device(L, 1)->channels); return 1; } static int l_device_rate(lua_State *L) { lua_pushinteger(L, check_device(L, 1)->rate); return 1; } static int l_device_format(lua_State *L) { snd_device_t *d = check_device(L, 1); lua_pushstring(L, d->format == SNDRV_PCM_FORMAT_FLOAT_LE ? "f32" : "s16"); return 1; } /* dev:avail() -> free frames | nil, err */ static int l_device_avail(lua_State *L) { snd_device_t *d = check_device(L, 1); struct snd_pcm_status st; memset(&st, 0, sizeof(st)); if (ioctl(d->fd, SNDRV_PCM_IOCTL_STATUS, &st) < 0) RETURN_ERR(L); lua_pushinteger(L, (lua_Integer)st.avail); return 1; } /* dev:write(buf) -> frames written | nil, err * * Writes buf:frames() interleaved frames (f32). When the hardware is s16 the * float samples are clamped/converted into the device scratch first. Returns * nil,"EAGAIN" if the buffer is momentarily full (poll again) and nil,"EPIPE" * on underrun (call dev:recover()). */ static int l_device_write(lua_State *L) { snd_device_t *d = check_device(L, 1); snd_buffer_t *b = check_buffer(L, 2); if (b->channels != d->channels) return luaL_error(L, "sound device has %d channels, buffer has %d", d->channels, b->channels); int frames = b->frames; if (frames > d->buffer_size) frames = d->buffer_size; const void *src; if (d->format == SNDRV_PCM_FORMAT_FLOAT_LE) { src = b->data; } else { int16_t *out = (int16_t *)d->scratch; const float *in = b->data; size_t nsamp = (size_t)frames * d->channels; for (size_t i = 0; i < nsamp; i++) { float s = in[i]; if (s > 1.0f) s = 1.0f; else if (s < -1.0f) s = -1.0f; out[i] = (int16_t)lrintf(s * 32767.0f); } src = d->scratch; } struct snd_xferi xfer; xfer.result = 0; xfer.buf = (void *)src; xfer.frames = frames; if (ioctl(d->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &xfer) < 0) { lua_pushnil(L); if (errno == EAGAIN) lua_pushstring(L, "EAGAIN"); else if (errno == EPIPE) lua_pushstring(L, "EPIPE"); else lua_pushstring(L, strerror(errno)); return 2; } lua_pushinteger(L, (lua_Integer)xfer.result); return 1; } /* dev:recover() -> true | nil, err — re-prepare after an underrun (XRUN). */ static int l_device_recover(lua_State *L) { snd_device_t *d = check_device(L, 1); if (ioctl(d->fd, SNDRV_PCM_IOCTL_PREPARE) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } /* dev:start() -> true | nil, err — force playback to start (normally the * start_threshold auto-starts it once enough is queued). */ static int l_device_start(lua_State *L) { snd_device_t *d = check_device(L, 1); if (ioctl(d->fd, SNDRV_PCM_IOCTL_START) < 0) RETURN_ERR(L); lua_pushboolean(L, 1); return 1; } /* dev:drop() -> true | nil, err — stop immediately, discarding queued audio. */ static int l_device_drop(lua_State *L) { snd_device_t *d = check_device(L, 1); if (ioctl(d->fd, SNDRV_PCM_IOCTL_DROP) < 0) RETURN_ERR(L); /* a dropped stream must be re-prepared before reuse */ ioctl(d->fd, SNDRV_PCM_IOCTL_PREPARE); lua_pushboolean(L, 1); return 1; } static int l_device_close(lua_State *L) { snd_device_t *d = check_device(L, 1); if (d->fd >= 0) { close(d->fd); d->fd = -1; } free(d->scratch); d->scratch = NULL; return 0; } /* ── module-level conversion helpers ───────────────────────────────────── */ /* sound.from_pcm(data, channels, fmt) -> buffer | nil, err * * Decodes raw interleaved PCM bytes into a float32 buffer. fmt is one of * "u8", "s16", "s24", "s32", "f32" (little-endian). Used by the WAV loader. */ static int l_from_pcm(lua_State *L) { size_t len; const unsigned char *p = (const unsigned char *)luaL_checklstring(L, 1, &len); int channels = (int)luaL_checkinteger(L, 2); const char *fmt = luaL_checkstring(L, 3); if (channels < 1 || channels > 8) return luaL_error(L, "sound.from_pcm: bad channel count %d", channels); int bps; if (strcmp(fmt, "u8") == 0) bps = 1; else if (strcmp(fmt, "s16") == 0) bps = 2; else if (strcmp(fmt, "s24") == 0) bps = 3; else if (strcmp(fmt, "s32") == 0) bps = 4; else if (strcmp(fmt, "f32") == 0) bps = 4; else return luaL_error(L, "sound.from_pcm: unknown format '%s'", fmt); size_t frame_bytes = (size_t)bps * channels; int frames = frame_bytes ? (int)(len / frame_bytes) : 0; int nrets; snd_buffer_t *b = buffer_push(L, frames, channels, &nrets); if (!b) return nrets; size_t nsamp = (size_t)frames * channels; float *out = b->data; for (size_t i = 0; i < nsamp; i++) { const unsigned char *s = p + i * bps; float v; if (strcmp(fmt, "u8") == 0) { v = ((float)s[0] - 128.0f) / 128.0f; } else if (strcmp(fmt, "s16") == 0) { int16_t x = (int16_t)(s[0] | (s[1] << 8)); v = (float)x / 32768.0f; } else if (strcmp(fmt, "s24") == 0) { int32_t x = (int32_t)(s[0] | (s[1] << 8) | (s[2] << 16)); if (x & 0x800000) x |= ~0xffffff; /* sign-extend 24 -> 32 */ v = (float)x / 8388608.0f; } else if (strcmp(fmt, "s32") == 0) { int32_t x = (int32_t)((uint32_t)s[0] | ((uint32_t)s[1] << 8) | ((uint32_t)s[2] << 16) | ((uint32_t)s[3] << 24)); v = (float)((double)x / 2147483648.0); } else { /* f32 */ uint32_t u = (uint32_t)s[0] | ((uint32_t)s[1] << 8) | ((uint32_t)s[2] << 16) | ((uint32_t)s[3] << 24); memcpy(&v, &u, sizeof(v)); } out[i] = v; } return 1; } /* sound.resample(src, src_rate, dst_rate) -> buffer | nil, err * * Linear-interpolation resample so an asset at one sample rate plays at the * correct pitch on a device at another. */ static int l_resample(lua_State *L) { snd_buffer_t *src = check_buffer(L, 1); double src_rate = luaL_checknumber(L, 2); double dst_rate = luaL_checknumber(L, 3); if (src_rate <= 0 || dst_rate <= 0) return luaL_error(L, "sound.resample: rates must be positive"); int ch = src->channels; double ratio = dst_rate / src_rate; int dst_frames = (int)((double)src->frames * ratio + 0.5); if (dst_frames < 0 || dst_frames > MAX_FRAMES) return luaL_error(L, "sound.resample: result too large"); int nrets; snd_buffer_t *dst = buffer_push(L, dst_frames, ch, &nrets); if (!dst) return nrets; if (src->frames == 0) return 1; for (int i = 0; i < dst_frames; i++) { double pos = (double)i / ratio; int i0 = (int)pos; double fr = pos - i0; int i1 = i0 + 1; if (i1 >= src->frames) i1 = src->frames - 1; const float *a = src->data + (size_t)i0 * ch; const float *b = src->data + (size_t)i1 * ch; float *o = dst->data + (size_t)i * ch; for (int c = 0; c < ch; c++) o[c] = (float)(a[c] + (b[c] - a[c]) * fr); } return 1; } /* ── registration ──────────────────────────────────────────────────────── */ static const luaL_Reg buffer_methods[] = { {"frames", l_buffer_frames }, {"channels", l_buffer_channels}, {"clear", l_buffer_clear }, {"mix", l_buffer_mix }, {"osc", l_buffer_osc }, {"fade", l_buffer_fade }, {"get", l_buffer_get }, {"set", l_buffer_set }, {"tostring", l_buffer_tostring}, {NULL, NULL } }; static const luaL_Reg device_methods[] = { {"fd", l_device_fd }, {"period", l_device_period }, {"channels", l_device_channels}, {"rate", l_device_rate }, {"format", l_device_format }, {"avail", l_device_avail }, {"write", l_device_write }, {"recover", l_device_recover }, {"start", l_device_start }, {"drop", l_device_drop }, {"close", l_device_close }, {NULL, NULL } }; static const luaL_Reg module_funcs[] = { {"open", l_open }, {"buffer", l_buffer_new}, {"from_pcm", l_from_pcm }, {"resample", l_resample }, {NULL, NULL } }; int luaopen_sound_core(lua_State *L) { luaL_newmetatable(L, BUFFER_MT); lua_newtable(L); luaL_register(L, NULL, buffer_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_buffer_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); luaL_newmetatable(L, DEVICE_MT); lua_newtable(L); luaL_register(L, NULL, device_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_device_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); luaL_newlib(L, module_funcs); return 1; }