DatumHueEnterprise v0.0.24datumhue.com
Contents

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.

CallPlays
audio.synthone tone of a waveform, for a set time or until you release it
audio.sfxa short run of notes, each able to slide in pitch, wobble or arpeggiate
audio.musicpatterns of notes in a sequence you choose, one or more at a time, looping if you ask
audio.playa 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:

pad.lualua
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.

lua
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:

keys.lualua
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:

ControlWhen you assign it
Audio.volume, Audio.pan, Audio.at, audio.listener, AudioBus.volume, audio.master_volume, an effect's wet, dry and feedbackglides there over 50 ms, so it never clicks
Audio.pitch, Audio.duty, Music.temposteps at once, without a click
AudioFilter.cutoff, AudioFilter.resonance, AudioReverb.spaceglides there over 10 ms
AudioDelay.timeglides 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:

sweep.lualua
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:

drums.lualua
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:

follow.lualua
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:

level.lualua
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

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

datumhue.com

(c) 2026 Shuusei AB. Quote excerpts of this manual, with attribution to DatumHue, in your own documentation, tutorials, course materials, reviews, and other writing or video. Republishing it in whole or in substantial part needs written permission: support@datumhue.com.

Victor Mono is licensed under the SIL Open Font License 1.1
Copyright (c) 2024, Rune Bjørnerås (https://github.com/rubjo)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Try

    ↑↓ selectEnter openTab filter

    Settings

    Theme
    Text
    Print

    Keyboard shortcuts

    / or Ctrl KSearch
    ↑ ↓ EnterPick a result
    TabNext result filter
    j kNext or previous entry
    [ ]Previous or next page
    yCopy a link to the current entry
    ?These shortcuts
    EscClose

    On this page