-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[==[ `sound` — audio for Lilush, aimed at game-engine use. A static-musl Lilush cannot dlopen ALSA's plugin layer (dmix, the "default" device), so this module talks to the kernel PCM interface directly and mixes in-process. It is a thin aggregator over: sound.core (C) — raw PCM playback device + float32 sample buffer + mixer ops sound.mixer (Lua)— software mixer: simultaneous voices, gain/pan, looping sound.wav (Lua)— WAV (RIFF) loader -> sample buffer sound.synth (Lua)— oscillator + ADSR tone generator sound.speech (Lua)— formant (Klatt-style) English text-to-speech Typical use, alongside a `term.gfx` frame loop, under one `lev.run`: local sound = require("sound") local dev = assert(sound.open({ rate = 48000, channels = 2 })) local mx = sound.mixer({ channels = dev:channels() }) local blip = assert(sound.synth.tone({ note = "A5", dur = 0.1, wave = "square" })) lev.run(function() sound.run(dev, mx) -- dedicated fd-driven writer task gfx.loop({ ... , input = function(k) if k == " " then mx:play(blip) end end }):run() end) ]==] local core = require("sound.core") local mixer = require("sound.mixer") local wav = require("sound.wav") local synth = require("sound.synth") local speech = require("sound.speech") ---! Run the audio writer task on the LEV event loop ---@ run(`dev`{.tbl}, `mx`{.tbl}, `opts`{.tbl .opt}) -> `task`{.tbl .or_fn} --[===[ Spawns (into the current `lev.run`) a coroutine that waits for the device to be writable and feeds it one period of mixed audio at a time — the recommended way to drive audio next to a frame loop. When no voices play it writes silence to keep the stream from underrunning. `opts`: `timeout` (poll/cancel-check interval in seconds, default 0.1); `cancel` (a `lev.cancel_token` — when fired the task exits); `name` (task name); `spawn` (set `false` to get the loop body back as a function and drive it yourself instead of auto-spawning). Requires LEV; with `spawn ~= false` it must be called inside `lev.run()`. ]===] local run = function(dev, mx, opts) opts = opts or {} local lev = require("lev") local levcore = require("lev.core") local out = assert(core.buffer(dev:period(), dev:channels())) local token = opts.cancel local timeout = opts.timeout or 0.1 local fd = dev:fd() local body = function() while true do if token and token.cancelled then return end local ok, err = levcore.wait_writable(fd, timeout) if token and token.cancelled then return end if err == "timeout" then -- just a poll wakeup; loop to re-check the cancel token elseif err and not ok then return -- fd error (e.g. device closed) else mx:fill(out) local w, werr = dev:write(out) if not w then if werr == "EPIPE" then dev:recover() elseif werr ~= "EAGAIN" then return -- unrecoverable write error end end end end end if opts.spawn == false then return body end return lev.spawn(body, { detached = true, name = opts.name or "sound_writer" }) end return { -- C core re-exports open = core.open, buffer = core.buffer, from_pcm = core.from_pcm, resample = core.resample, -- Lua layers mixer = mixer.new, wav = wav, synth = synth, speech = speech, -- LEV integration run = run, }