Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt

Use this file to discover all available pages before exploring further.

Keel’s audio system is built on miniaudio and exposed through a small set of module-level helpers. One-shot sound effects are decoded into memory on first play and mixed in a stereo int16 buffer on a background thread. Music tracks are streamed from disk frame by frame. All three volume channels—master, SFX, and music—are independently adjustable at runtime, and music transitions support linear fade-in and fade-out ramps.

Setup

Call setup_audio(app) once, before any play calls. It creates the AudioEngine, registers a PRE_UPDATE system that drains finished handles and advances music fades, and wires a shutdown hook so the device closes cleanly when the app exits.
import keel
from keel.audio import setup_audio, play_sound, play_music, stop_music, set_volume

app = keel.App(title="My Game", width=800, height=600)
audio = setup_audio(app)  # returns AudioSetup; idempotent
setup_audio returns an AudioSetup dataclass with a single field, engine, that holds the underlying AudioEngine. Most code uses the module-level helpers instead of touching the engine directly.
setup_audio is idempotent — calling it more than once on the same app returns the same AudioSetup without creating a second device or registering duplicate systems.

Sound Effects

play_sound decodes the file on the first call and caches it; subsequent plays reuse the cached PCM data with no disk I/O.
handle = play_sound(app, "assets/sounds/jump.wav")
handle = play_sound(app, "assets/sounds/coin.ogg", volume=0.6)
handle = play_sound(app, "assets/sounds/alarm.wav", volume=1.0, loop=True)

play_sound signature

def play_sound(
    app,
    path: str,
    volume: float = 1.0,
    loop: bool = False,
) -> SoundHandle: ...
ParameterTypeDefaultDescription
pathstrPath to the audio file (.wav, .ogg, .mp3, .flac)
volumefloat1.0Gain for this specific instance, clamped to [0.0, 1.0]
loopboolFalseLoop the sound indefinitely until stopped
Returns a SoundHandle — an immutable, hashable token you can use to stop the sound early.

Stopping a sound

handle = play_sound(app, "assets/sounds/engine.wav", loop=True)

# Later, when the engine should stop:
stop_sound(app, handle)
stop_sound is a no-op if the handle is unknown or the sound has already finished.

Checking playback state

Call is_playing directly on the engine if you need to poll whether a one-shot has finished:
from keel.audio import setup_audio

audio = setup_audio(app)
handle = play_sound(app, "assets/sounds/explosion.wav")

@app.system(keel.Phase.UPDATE)
def check_done(world, dt):
    if not audio.engine.is_playing(handle):
        print("explosion finished")

Music

play_music opens the file as a streaming source, replacing any track that is currently playing. The transition can be instant or ramped with fade_in.
play_music(app, "assets/music/theme.ogg")
play_music(app, "assets/music/theme.ogg", loop=True, fade_in=2.0)
play_music(app, "assets/music/battle.ogg", loop=True, fade_in=0.5)

play_music signature

def play_music(
    app,
    path: str,
    loop: bool = True,
    fade_in: float = 0.0,
) -> None: ...
ParameterTypeDefaultDescription
pathstrPath to the audio file
loopboolTrueLoop the track when it reaches the end
fade_infloat0.0Seconds to linearly ramp gain from 0.0 to the current music volume

Stopping music

# Instant stop.
stop_music(app)

# Fade out over 1.5 seconds, then stop.
stop_music(app, fade_out=1.5)

stop_music signature

def stop_music(app, fade_out: float = 0.0) -> None: ...
To cross-fade between two tracks, call stop_music(app, fade_out=N) and then immediately play_music(app, new_path, fade_in=N). The outgoing track ramps down while the new one ramps up in parallel.

Volume Control

Keel has three independent gain channels that multiply together on the way to the hardware:
  • Master — applies to everything (SFX + music)
  • SFX — applies only to one-shot sounds started with play_sound
  • Music — applies only to the streaming music track
set_volume(app, master=0.8)           # reduce overall volume to 80%
set_volume(app, sfx=0.5)              # halve SFX without touching music
set_volume(app, music=0.3)            # quiet music during cutscene dialogue
set_volume(app, master=1.0, sfx=0.7, music=0.6)  # set all three at once

set_volume signature

def set_volume(
    app,
    master: float | None = None,
    sfx: float | None = None,
    music: float | None = None,
) -> None: ...
Pass only the keyword arguments you want to change; None means “leave unchanged.” All values are clamped to [0.0, 1.0].

Reading current volumes

Access the properties directly on the AudioEngine:
audio = setup_audio(app)
print(audio.engine.master_volume)
print(audio.engine.sfx_volume)
print(audio.engine.music_volume)

AudioSource Component

keel.AudioSource is an optional ECS component that binds a sound to an entity’s lifetime. It is useful when a sound should live and die with the entity that owns it (a pickup that plays on collect, a turret emitter).
@keel.component
# Built-in — already decorated; shown here for reference.
class AudioSource:
    sound_id: int = 0    # index into your own sound path registry
    volume: float = 1.0
    loop: bool = False
    auto_play: bool = False
    playing: bool = False
AudioSource does not play audio automatically — Keel ships no built-in system that reads it. It is a data marker: your gameplay system queries for entities with AudioSource, decides when to call play_sound, and sets playing = True to track state. For simple one-shot effects, calling play_sound(app, path) directly from a system is simpler.

Supported Formats

The engine uses miniaudio’s decoder, which supports:
FormatExtensionNotes
OGG Vorbis.oggRecommended for music tracks
WAV.wavRecommended for short sound effects
MP3.mp3Supported via miniaudio’s built-in decoder
FLAC.flacSupported via miniaudio’s built-in decoder
All formats are decoded to stereo int16 at 44 100 Hz before mixing.

Complete Example

1

Install and import

pip install keelpy
import keel
from keel.audio import setup_audio, play_sound, play_music, stop_music, set_volume
from keel.renderer import setup_renderer_2d
2

Create the app and set up audio

app = keel.App(title="Audio Demo", width=800, height=600)
setup_renderer_2d(app)
audio = setup_audio(app)

# Set starting volume levels.
set_volume(app, master=1.0, sfx=0.8, music=0.5)
3

Start background music on launch

play_music(app, "assets/music/theme.ogg", loop=True, fade_in=2.0)
4

Play sound effects in a system

@app.system(keel.Phase.UPDATE)
def handle_input(world, dt):
    for ev in world.read_events(keel.KeyEvent):
        if ev.action == keel.PRESS:
            if ev.key == keel.KEY_SPACE:
                play_sound(app, "assets/sounds/jump.wav", volume=0.9)

            elif ev.key == keel.KEY_ESCAPE:
                # Fade out the music over one second, then quit.
                stop_music(app, fade_out=1.0)
                app.window.close()
5

Run

app.run()

API Quick Reference

Function / MethodDescription
setup_audio(app)Initialize the audio engine; returns AudioSetup. Idempotent.
play_sound(app, path, volume=1.0, loop=False)Play a one-shot or looping SFX; returns SoundHandle.
stop_sound(app, handle)Stop a specific sound by handle.
play_music(app, path, loop=True, fade_in=0.0)Stream a music track, replacing any current track.
stop_music(app, fade_out=0.0)Stop music, optionally ramping gain down first.
set_volume(app, master=None, sfx=None, music=None)Update any combination of the three gain channels.
audio.engine.is_playing(handle)True while the handle’s sound is still active.
audio.engine.is_music_playing()True while music is active, including during fade-out.

Build docs developers (and LLMs) love