-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- Manual hardware smoke test for the sound module. NOT part of run_tests.bash: -- it needs a real PCM device (/dev/snd) and exclusive access (no dmix under -- static musl), so it must run on a host with sound hardware and membership in -- the `audio` group: -- -- lilush tests/sound/manual/smoke.lua -- -- It plays a 440 Hz sine, then a chord of three synth voices at once (proving -- the in-process mixer), driven by the LEV writer task. local lev = require("lev") local sound = require("sound") -- Card/device default to 0/0 but vary per machine (e.g. card 0 is often -- HDMI-only and the analog output is card 1). Pass them as args, or set -- LILUSH_SND_CARD / LILUSH_SND_DEVICE. `cat /proc/asound/pcm` lists the -- playback ("p") devices — pick a `NN-MM ... playback` line: card NN, device MM. local card = tonumber(arg[1]) or tonumber(os.getenv("LILUSH_SND_CARD")) or 0 local devno = tonumber(arg[2]) or tonumber(os.getenv("LILUSH_SND_DEVICE")) or 0 local dev, err = sound.open({ card = card, device = devno, rate = 48000, channels = 2 }) if not dev then io.stderr:write(string.format("could not open audio device hw:%d,%d: %s\n", card, devno, tostring(err))) io.stderr:write("(check `cat /proc/asound/pcm`, then pass: lilush smoke.lua )\n") os.exit(1) end print( string.format( "opened device: %d Hz, %d ch, format %s, period %d frames", dev:rate(), dev:channels(), dev:format(), dev:period() ) ) local mx = sound.mixer({ channels = dev:channels() }) local rate = dev:rate() local a4 = assert(sound.synth.tone({ note = "A4", dur = 1.0, rate = rate, amp = 0.5, release = 0.1 })) local c5 = assert(sound.synth.tone({ note = "C5", dur = 1.2, rate = rate, amp = 0.3, wave = "triangle" })) local e5 = assert(sound.synth.tone({ note = "E5", dur = 1.2, rate = rate, amp = 0.3, wave = "triangle" })) lev.run(function() -- The writer task loops forever; a cancel token lets us stop it so -- lev.run can drain and return once playback is done. local token = lev.cancel_token() sound.run(dev, mx, { cancel = token }) print("playing A4 sine (1s)...") io.stdout:flush() mx:play(a4) lev.sleep(1.2) print("playing an A-major chord — three voices mixed at once (1.2s)...") io.stdout:flush() mx:play(a4, { pan = -0.4 }) mx:play(c5, { pan = 0.0 }) mx:play(e5, { pan = 0.4 }) lev.sleep(1.6) token:cancel() lev.sleep(0.2) -- let the writer notice the token and exit print("done.") end)