Sound Card
A sound card plays the samples a program writes to it, as a real card’s DAC does. The players near its block hear them.
Facts
| | |
|—|—|
| Type | sound (the kernel names it dsp, then dsp1) |
| Address | An expansion slot path such as exp:0, or a hub’s |
| Formats | Rates 8000, 11025, 22050 or 44100 a second; unsigned 8-bit (u8), signed 16-bit little-endian (s16le) or IMA ADPCM (ima_adpcm); 1 or 2 channels |
| Buffer | One second of the current format |
| Interrupt | Raised when a refused write has room, or when what is queued has played out |
| Power | 6 W |
| Info | kind (sound) |
How it plays
A write takes what fits in the buffer and says how much (nothing is dropped silently). A program that is refused waits for the card’s interrupt, which comes when at least half the buffer is free. The card plays by game time: each tick its block sends what is due, still encoded, to the players within 40 blocks, and their games decode and play it. It decodes IMA ADPCM itself.
Mono comes from the card’s block. Stereo is two speakers half a block either side of the block, its left and right as seen facing its front, because the game does not place a stereo sound in the world. See Faces.
The card powers on at OSS’s defaults: 8000 a second, unsigned 8-bit, mono. Changing the format drops what is queued. A new format that is the same as the old one drops nothing.
In mcos
The card is OSS’s /dev/dsp (/dev/dsp1 for the next card), as on Linux.
- Write: what you write plays. A write waits while the buffer is full, as a pipe’s writer does, so a program never runs ahead of the sound. Each open starts at OSS’s defaults (8000 a second, unsigned 8-bit, mono), so a raw file plays as written; what another program left queued in that same format plays on first.
- Read: the file is write-only. Opening it to read fails with
EACCES(it plays; it does not record). - Poll: it is always ready for
OUT(a write waits by itself). It is ready forPRIwhile the card has news: its interrupt came since the file was last asked an ioctl (room after a refusedwrite, or the queue played out after asyncthat said not yet).
cat /clip.raw > /dev/dsp
aplay /clip.wav
aplay plays WAV files and sets the format for them. The file’s ioctls are OSS’s:
| ioctl | Argument | Does |
|---|---|---|
SNDCTL_DSP_SPEED |
a rate | sets the rate; answers what it is |
SNDCTL_DSP_SETFMT |
AFMT_U8, AFMT_S16_LE or AFMT_IMA_ADPCM |
sets the encoding; answers its name |
SNDCTL_DSP_CHANNELS |
1 or 2 | sets the channels; answers them |
SNDCTL_DSP_GETODELAY |
none | gives the bytes queued |
SNDCTL_DSP_RESET |
none | drops what is queued |
SNDCTL_DSP_SYNC |
none | waits until what is queued has played |
A value the card does not play fails with EINVAL (an unknown format name too). After OSS’s, the
file’s ioctls are the card’s own methods below, by name, with their arguments: an ADPCM block’s
size, which OSS never had, is
sys.ioctl(dsp, "setFormat", {block = 1024})
A method’s ioctl answers what the method does (true for one that answers nothing); its failure is
nil, the card’s message and EINVAL. setLabel sets the card’s label, as on every device file.
Methods
setFormat({rate, encoding, channels, block}) → nothing
Sets what it plays. Any field left out stays. encoding is "u8", "s16le" or "ima_adpcm".
An unsupported value fails with a message such as
unsupported rate: 9000 ([8000, 11025, 22050, 44100]), unsupported encoding: ... or
unsupported channels: 3 (1 or 2). block is for ADPCM only (WAV’s block align: at most 8192, a
multiple of 4 times the channels, and larger than 4 bytes a channel, else
bad ADPCM block size: ...); switching to ADPCM without one gives 512 bytes a channel. What was
queued goes, unless the new format is the same as the old.
getFormat() → {rate, encoding, channels, block}
The current format. block is 0 unless the encoding is ADPCM.
write(data) → count
Queues bytes to play and gives how many it took: fewer, or 0, when the buffer is full. Then its interrupt comes once half the buffer is free again. (Writing to the file does this for you, and waits.)
getDelay() → bytes
The bytes queued and not yet played (OSS’s SNDCTL_DSP_GETODELAY).
sync() → boolean
True once what is queued has played out. False: not yet, and the interrupt says when (OSS’s
SNDCTL_DSP_SYNC, which waits for it).
reset() → nothing
Drops what is queued, at once (OSS’s SNDCTL_DSP_RESET).
Events
None to pull. The interrupt line is the event: it is up while a write was refused and the buffer is
at least half empty, and while a sync waits and the queue has played out. Each write and
sync starts its wait afresh.
Examples
A tune, played through the file at OSS’s defaults (the program only makes samples and writes them):
-- Play a tune on the sound card: lua sound-melody.lua [notes]
--
-- The notes are names with an octave, a beat each (C4 D4 E4...), and "-" is a rest. Each note is a
-- square wave, made as samples at the card's defaults (OSS's: 8000 a second, unsigned 8-bit, mono),
-- so the program just writes them to /dev/dsp, as `cat file.raw > /dev/dsp` would. The card's
-- buffer holds a second; a write waits while it is full, so the program never runs ahead of the
-- music and needs no timer of its own.
local RATE = 8000 -- samples a second: the default every open of /dev/dsp starts at
local BEAT = 0.25 -- seconds a note lasts
local GAP = 0.02 -- seconds of silence at a note's end, so repeated notes are heard apart
local SILENCE = 128 -- an unsigned 8-bit sample's middle: no sound
local VOLUME = 60 -- how far the wave swings from the middle (at most 127)
local TUNE = "E4 D4 C4 D4 E4 E4 E4 - D4 D4 D4 - E4 G4 G4 -"
local SEMITONES = {C = -9, D = -7, E = -5, F = -4, G = -2, A = 0, B = 2}
-- A note's pitch in Hz, from its name ("A4" is 440; a "#" after the letter is a semitone up).
local function pitch(name)
local letter, sharp, octave = name:match("^(%u)(#?)(%d)$")
assert(letter and SEMITONES[letter], "not a note: " .. name)
local steps = SEMITONES[letter] + (sharp == "#" and 1 or 0) + (tonumber(octave) - 4) * 12
return 440 * 2 ^ (steps / 12)
end
-- A beat of samples: the note's square wave, then the gap (a rest is all silence).
local function samples(name)
local count = math.floor(BEAT * RATE)
local sounding = name == "-" and 0 or count - math.floor(GAP * RATE)
local period = name ~= "-" and RATE / pitch(name)
local high, low = string.char(SILENCE + VOLUME), string.char(SILENCE - VOLUME)
local out = {}
for i = 0, count - 1 do
if i < sounding then
out[#out + 1] = (i % period) < period / 2 and high or low
else
out[#out + 1] = string.char(SILENCE)
end
end
return table.concat(out)
end
local dsp = assert(io.open("/dev/dsp", "w"))
for name in (arg[1] or TUNE):gmatch("%S+") do
dsp:write(samples(name)) -- waits while the card's buffer is full
dsp:flush()
end
dsp:close() -- what is queued plays on after the program ends
A doorbell: a redstone card hears the button, and the program sets a 16-bit format with OSS’s ioctls
and waits with SNDCTL_DSP_SYNC for each ring to end:
-- A doorbell: when a button wired to the computer is pressed, play a two-tone chime ("ding-dong").
--
-- The button's signal comes in on a redstone card (its file, /dev/redstone0, a line a face:
-- "face level powered"). The chime is 16-bit, so the program sets the sound card with OSS's
-- ioctls (format, channels, rate), as any Linux program sets /dev/dsp, and makes the chime once.
-- After each ring, SNDCTL_DSP_SYNC waits until it has played out, so a second press while it
-- rings does not pile up rings. sys.poll waits for the redstone card's change (PRI): no busy wait.
local sys = require("sys")
local BUTTON = "north" -- the face the button's signal comes in on
local RATE = 11025 -- samples a second
local DING, DONG = 660, 520 -- the two tones' pitches, in Hz
local TONE = 0.4 -- seconds each tone lasts
local VOLUME = 12000 -- the loudest sample (16-bit samples go to 32767)
-- A tone fading out, as a struck chime does: a sine wave whose loudness falls away.
local function tone(hz)
local count = math.floor(TONE * RATE)
local out = {}
for i = 0, count - 1 do
local fade = 1 - i / count
out[#out + 1] = string.pack("<i2", math.floor(VOLUME * fade * fade * math.sin(2 * math.pi * hz * i / RATE)))
end
return table.concat(out)
end
local chime = tone(DING) .. tone(DONG)
local dsp = assert(sys.open("/dev/dsp", "w"))
assert(sys.ioctl(dsp, "SNDCTL_DSP_SETFMT", "AFMT_S16_LE"))
assert(sys.ioctl(dsp, "SNDCTL_DSP_CHANNELS", 1))
assert(sys.ioctl(dsp, "SNDCTL_DSP_SPEED", RATE))
local redstone = assert(io.open("/dev/redstone0"))
local watch = {[sys.fileno(redstone)] = {PRI = true}}
-- The button's level now: the file read again from its start.
local function pressed()
redstone:seek("set", 0)
return (tonumber(redstone:read("a"):match(BUTTON .. " (%d+)")) or 0) > 0
end
local was = pressed()
while true do
sys.poll(watch) -- block until the redstone card changes
local now = pressed()
if now and not was then
sys.write(dsp, chime) -- waits while the card's buffer is full
sys.ioctl(dsp, "SNDCTL_DSP_SYNC") -- and until it has all played
end
was = now
end
A stereo siren through the card’s own methods: write’s handshake with poll’s PRI, sync to
finish, and reset to stop at once when touch /tmp/siren.off asks:
-- An alarm siren: lua sound-siren.lua [seconds]
--
-- A tone sweeps up and down and swings between the card's two speakers, for SECONDS, or until
-- someone runs `touch /tmp/siren.off`. It drives the sound card through its own methods, which are
-- its /dev/dsp file's ioctls: setFormat (stereo), getFormat, write (which takes only what fits in
-- the buffer and says how much: the program waits for the card's news, poll's PRI, before it
-- writes the rest), sync (has it all played?), getDelay (what is still queued) and reset (drop it,
-- so the siren stops at once rather than playing on for its last second).
local sys = require("sys")
local SECONDS = tonumber(arg[1]) or 10 -- how long it sounds, unless stopped
local RATE = 8000 -- frames a second (each frame a left and a right sample)
local CHUNK = 0.25 -- seconds of sound made at a time
local SWEEP = 1 -- seconds for the tone to rise and fall back, and the sound to cross over
local LOW, HIGH = 500, 1000 -- the sweep's pitches, in Hz
local SILENCE = 128 -- an unsigned 8-bit sample's middle
local VOLUME = 90 -- how far a wave swings from it
local STOP = "/tmp/siren.off" -- the file that stops it
local dsp = assert(sys.open("/dev/dsp", "w"))
assert(sys.ioctl(dsp, "setFormat", {rate = RATE, encoding = "u8", channels = 2}))
local format = sys.ioctl(dsp, "getFormat")
print(("siren: %d Hz, %s, %d channels"):format(format.rate, format.encoding, format.channels))
-- Frames 'from' to 'from + count - 1' of the siren, as bytes: left, right, left, right...
local phase = 0
local function frames(from, count)
local out = {}
for i = from, from + count - 1 do
local t = (i / RATE) % SWEEP / SWEEP -- where in the sweep, 0 to 1
local rise = t < 0.5 and t * 2 or 2 - t * 2 -- up, then down
phase = phase + (LOW + (HIGH - LOW) * rise) / RATE
local wave = phase % 1 < 0.5 and VOLUME or -VOLUME
out[#out + 1] = string.char(SILENCE + math.floor(wave * (1 - rise)), SILENCE + math.floor(wave * rise))
end
return table.concat(out)
end
-- Queue all of 'data', waiting for room as the card plays: false if told to stop meanwhile.
local function play(data)
while #data > 0 do
local taken = assert(sys.ioctl(dsp, "write", data))
data = data:sub(taken + 1)
if #data > 0 then
sys.poll({[dsp] = {PRI = true}}) -- the card says when half its buffer is free
end
if sys.stat(STOP) then
return false
end
end
return true
end
local total, step = math.floor(SECONDS * RATE), math.floor(CHUNK * RATE)
local stopped = false
for from = 0, total - 1, step do
if not play(frames(from, math.min(step, total - from))) then
stopped = true
break
end
end
if stopped then
local queued = sys.ioctl(dsp, "getDelay")
sys.ioctl(dsp, "reset") -- silence, now
sys.unlink(STOP)
print(("siren: stopped (%d bytes not played)"):format(queued))
else
while not sys.ioctl(dsp, "sync") do -- false: not played out yet, and the card will say when
sys.poll({[dsp] = {PRI = true}})
end
print("siren: done")
end
sys.close(dsp)
Unsigned 8-bit samples centre on 128 (silence), so a square wave is a run of bytes above it and a
run below. A 16-bit sample is string.pack("<i2", value), from -32768 to 32767.