Play sound
Play tones, sound effects, music and recorded samples, shape and filter them as they play, and mix them on buses with echo and reverb.
Play a sound
Every sound starts with one call. The call returns at once, however long
the sound lasts, and the handle it returns controls the sound while it
plays: an Audio, or a Music from audio.music.
| Call | Plays |
|---|---|
audio.synth | one tone of a waveform, for a set time or until you release it |
audio.sfx | a short run of notes, each able to slide in pitch, wobble or arpeggiate |
audio.music | patterns of notes in a sequence you choose, one or more at a time, looping if you ask |
audio.play | a recorded sample, decoded with Bytes:audio |
Sounds you start in one callback begin together, on the same sample. This program plays a quiet bass loop, gives you a button for each note of a scale and two jump sounds, and turns the music down with a slider:
local audio = datumhue.audio
local vec2 = datumhue.math.vec2
-- A quiet two-note bass line, looping on the "music" bus.
audio.music({
patterns = {
{
{ waveform = "sine", frequency = 110, duration = 0.4 },
{ waveform = "sine", frequency = 165, duration = 0.4 },
},
},
sequence = { 1 },
looping = true,
volume = 0.3,
bus = "music",
})
local pad = datumhue.ui.root:panel({
flex_direction = "column",
gap = 12,
padding = 16,
})
-- One button per note of a pentatonic scale.
local keys = pad:panel({ flex_direction = "row", gap = 8 })
for _, hz in ipairs({ 262, 294, 330, 392, 440 }) do
keys:button({
text = hz .. " Hz",
on_click = function()
audio.synth({
waveform = "triangle",
frequency = hz,
duration = 0.3,
})
end,
})
end
-- A rising jump sound, heard from the left or the right.
local function jump(x)
audio.sfx({
{
waveform = "square",
frequency = 300,
slide = 900,
duration = 0.15,
},
}, { at = vec2(x, 0) })
end
local sides = pad:panel({ flex_direction = "row", gap = 8 })
sides:button({
text = "Jump on the left",
on_click = function()
jump(-200)
end,
})
sides:button({
text = "Jump on the right",
on_click = function()
jump(200)
end,
})
-- One slider turns the whole music bus up or down.
pad:label({ text = "Music volume" })
pad:slider({
width = 240,
value = 1,
on_change = function(value)
audio.bus("music").volume = value
end,
})
In this example: datumhue.audio datumhue.math.vec2 datumhue.audio.music datumhue.ui.root UiElement:panel UiElement:button datumhue.audio.synth datumhue.audio.sfx UiElement:label UiElement:slider datumhue.audio.bus AudioBus.volume
An app plays at most 64 sounds at once. Past that, a new sound takes the
place of the app's oldest sound that does not loop, which fades out over
5 ms: its handle reads as gone, and its Audio:on_end runs. When all 64
loop, the call raises instead.
Play a recorded sample
Read the file, decode it with Bytes:audio, and play the result as
often as you like. Bytes:audio reads WAV and Ogg Vorbis, and decodes in
the background; AudioAsset:ready waits until it has, and audio.play
of a sample still decoding starts once it has decoded. A long sample, such as a
music track, plays while the rest of it decodes.
local hit = datumhue.package:dir():read("hit.ogg"):audio()
datumhue.audio.play(hit, { volume = 0.8, bus = "effects" })
Give audio.play an offset to start part-way into the sample, and
assign Audio.position to jump a playing sample to another time; it
fades for 5 ms on either side of the jump. AudioAsset.duration reads
the sample's length in seconds once it is ready, for a progress bar or
a timeline.
Shape notes with an envelope
Give a note or an audio.synth an envelope, and it fades in and out
instead of starting and stopping hard. It rises over attack to its
volume, falls over decay to the sustain level and holds there; when
it is let go, it fades over release. Each note starts from the level
the note before it left, so notes join without a click.
A note in audio.sfx or audio.music is let go after its duration,
and its release rings on until the next note starts. A note with a
volume of 0 is a rest, so the note before it rings on through its
whole release. A looping audio.synth is held until you call
Audio:release, as this keyboard does when a key comes up:
local audio = datumhue.audio
-- Five keys play a scale; each note sounds while its key is held.
local scale = { a = 262, s = 294, d = 330, f = 392, g = 440 }
local held = {}
datumhue.ui.root:label({ text = "Hold a, s, d, f or g" })
datumhue.input.on_key(function(key, pressed)
local hz = scale[key]
if not hz then
return
end
if pressed and not held[key] then
held[key] = audio.synth({
waveform = "sawtooth",
frequency = hz,
volume = 0.3,
looping = true,
envelope = {
attack = 0.02,
decay = 0.3,
sustain = 0.5,
release = 0.8,
},
})
elseif not pressed and held[key] then
-- The key is up: the note fades over its release.
held[key]:release()
held[key] = nil
end
end)
In this example: datumhue.audio datumhue.ui.root UiElement:label datumhue.input.on_key datumhue.audio.synth
Audio:release also takes a time in seconds. The sound starts no more
notes, fades out over that time and ends, whether it has an envelope or
not.
Change a sound as it plays
Every control on a handle moves while the sound plays, and reads back its new value at once. How a change sounds depends on the control:
| Control | When you assign it |
|---|---|
Audio.volume, Audio.pan, Audio.at, audio.listener, AudioBus.volume, audio.master_volume, an effect's wet, dry and feedback | glides there over 50 ms, so it never clicks |
Audio.pitch, Audio.duty, Music.tempo | steps at once, without a click |
AudioFilter.cutoff, AudioFilter.resonance, AudioReverb.space | glides there over 10 ms |
AudioDelay.time | glides there over 50 ms, bending the echoes like tape |
The methods ending in _to move a control over the seconds you give,
exactly as you hear it: Audio:volume_to, Audio:pitch_to,
Audio:pan_to, AudioFilter:cutoff_to and AudioBus:volume_to.
Audio:duty_to counts the sound's own seconds instead, which pitch
speeds up, as it does the notes. A pitch or cutoff move rises or falls
by the same number of octaves each second; the others move in a
straight line. Assigning the control during the move ends the move, and
the control goes on to the new value from where the move had reached.
While a sound is paused, its moves wait with it.
Give a sound a filter when you start it, or assign one to
Audio.filter later. A filter is low-pass unless you choose another
kind. Assigning a table changes only the keys it holds, and a cutoff
move goes on through a change of kind; nil removes the filter. This
program sweeps the cutoff up and down:
local tone = datumhue.audio.synth({
waveform = "sawtooth",
frequency = 110,
volume = 0.3,
looping = true,
filter = { cutoff = 200, resonance = 0.6 },
})
local filter = tone.filter
-- Open the filter over two seconds, then shut it over the next two.
local open = false
local function sweep()
open = not open
filter:cutoff_to(open and 4000 or 200, 2)
end
sweep()
datumhue.time.every(2, sweep)
In this example: datumhue.audio.synth Audio.filter AudioFilter:cutoff_to datumhue.time.every
Mix with buses
The volume you hear is three numbers multiplied together: your app's
audio.master_volume, the volume of the bus a sound plays through, and
the sound's own Audio.volume. Route a sound into a bus with
bus = "music", and AudioBus.volume turns everything on that bus up
or down at once, without touching each sound. AudioBus:volume_to fades
it, to duck the music under speech or fade it out at the end of a level.
A bus also passes its sounds through effects of its own: a filter
(AudioBus.filter), an echo (AudioBus.delay) and a reverb
(AudioBus.reverb), in that order, before its volume. Assign a table to
add an effect or change its keys, and nil to remove it. The echoes and
the reverb's tail ring on after the sounds that made them have ended.
AudioBus:remove fades a bus out over 20 ms with every sound on it, and
frees its name: audio.bus with that name later makes a new bus. An app
holds at most 32 buses, so remove the ones you make as you go, one for
each room or level.
One reverb on a bus is one room for every sound in it, where space on
audio.sfx gives each play a room of its own:
local audio = datumhue.audio
-- Everything on the "drums" bus shares one echo and one room.
local drums = audio.bus("drums")
drums.delay = { time = 0.25, feedback = 0.4, wet = 0.3 }
drums.reverb = { space = 0.5 }
local kick = { waveform = "sine", frequency = 90, slide = 40 }
local snare = { waveform = "noise", frequency = 3000, duration = 0.08 }
local beat = 0
datumhue.time.every(0.375, function()
beat = beat + 1
local hit = beat % 2 == 1 and kick or snare
audio.sfx({ hit }, { bus = "drums" })
end)
In this example: datumhue.audio datumhue.audio.bus AudioBus.delay AudioBus.reverb datumhue.time.every datumhue.audio.sfx
Place a sound in the world
Give audio.synth, audio.sfx or audio.play an at position, and
the sound plays panned toward its side and quieter the farther it is
from the ear. Both
are in any 2D units you like, such as your canvas's pixels. The ear is
audio.listener, at the origin until you move it; a sound given a
listener of its own is heard from there instead. A sound without at
plays centered, at full volume, from everywhere.
Assign Audio.at to move a sound, and audio.listener to move the
ear: the sound's pan and loudness follow, gliding so the move never
clicks. Assign them each frame, and a sound follows what makes it:
local vec2 = datumhue.math.vec2
-- A low hum that crosses in front of the ear, left to right, in four
-- seconds.
local hum = datumhue.audio.synth({
waveform = "sawtooth",
frequency = 80,
duration = 4,
volume = 0.2,
at = vec2(-400, 100),
})
local x = -400
datumhue.on_update(function(dt)
if hum.playing then
x = x + 200 * dt
hum.at = vec2(x, 100)
end
end)
In this example: datumhue.math.vec2 datumhue.audio.synth datumhue.on_update Audio.playing Audio.at
To pan a sound without a position, give it a pan from -1 (left) to 1
(right): audio.synth, audio.sfx, audio.play and audio.music all
take one. Audio.pan moves it while it plays and Audio:pan_to sweeps
it. A sound played with at pans by its position, so its pan only
reads. Each note, in audio.sfx and in music patterns alike, can add a
pan of its own, so a run of notes can move across the room.
Follow what plays
Audio.level reads how loud a sound was as it was heard, AudioBus.level
a bus, and audio.level everything your app plays: the RMS over the
last 1024 frames, about 21 ms, from 0 up. The peak beside each reads
the largest sample over the same span. Read them each frame for meters,
and for visuals that move with the sound:
local tone = datumhue.audio.synth({
waveform = "sawtooth",
frequency = 110,
volume = 0.3,
looping = true,
})
tone:volume_to(0, 4)
-- Read the level as the tone fades out.
datumhue.time.every(0.5, function()
print(string.format("level %.2f, peak %.2f", tone.level, tone.peak))
end)
In this example: datumhue.audio.synth Audio:volume_to datumhue.time.every Audio.level Audio.peak
Audio:on_end runs a function once, when a sound is heard ending: it
played out, was released, or made room for a newer sound. It does not
run after Audio:stop. It runs on the frame after the end is heard, so
a chain of sounds started from it leaves short gaps; give sounds that
must keep time a start_time, or play them as audio.music.
See also
- Make music, for tracks you edit as they play, sounds on the beat, and chip voices
- Read keys, mouse, touch and gamepads, for the keys that play notes
- Draw shapes and text
- Bodies, joints and collisions, whose collision handlers can play a sound on impact
Reference used on this page: datumhue.audio.synth datumhue.audio.sfx datumhue.audio.music datumhue.audio.play Bytes:audio datumhue.audio datumhue.math.vec2 datumhue.ui.root UiElement:panel UiElement:button UiElement:label UiElement:slider datumhue.audio.bus AudioBus.volume datumhue.package Package:dir Dir:read datumhue.input.on_key Audio.volume Audio.pan Audio.at datumhue.audio.listener datumhue.audio.master_volume Audio.pitch Audio.duty Music.tempo AudioFilter.cutoff AudioFilter.resonance AudioReverb.space AudioDelay.time Audio.filter AudioFilter:cutoff_to datumhue.time.every AudioBus.delay AudioBus.reverb datumhue.on_update Audio.playing Audio:volume_to Audio.level Audio.peak Audio:on_end AudioAsset:ready Audio.position AudioAsset.duration Audio:release Audio:pitch_to Audio:pan_to AudioBus:volume_to Audio:duty_to AudioBus.filter AudioBus:remove AudioBus.level datumhue.audio.level Audio:stop