Sound effects

day-part-sound plays short clips bundled with the app (a tap, a chime, a card sliding onto a pile) through each platform’s low-latency engine. It is a headless part in parts/, like day-part-haptics, and the two are meant to be used together: a game pairs each haptic phrase with a clip, and the player turns either off in its settings.

Authoring

use day_part_sound::Play;

// Decode ahead, so the first play starts at once.
day_part_sound::preload(&[res::assets::sounds::tap_wav, res::assets::sounds::win_wav]);

day_part_sound::play(&res::assets::sounds::tap_wav);
day_part_sound::play_with(&res::assets::sounds::win_wav, Play { volume: 0.8, pan: -0.5, priority: 1 });
FunctionWhat it does
play(&clip)play at full volume, centered
play_with(&clip, Play { volume, pan, priority })play at a volume from 0 to 1, a pan from −1 (left) to 1 (right), and a priority
preload(&[clip, …])load clips now, so their first play is immediate
unload_all()release every loaded clip, for a screen that is closing
set_volume(v) / volume()the master volume every play is scaled by
set_enabled(on) / is_enabled()a switch for all sound, for an app-wide setting
is_supported()whether this platform has a sound engine Day can reach
recent()the clips most recently played, oldest first, for tests and walkthroughs

Every call is fire-and-forget. None returns an error or panics, and none blocks on audio: a missing or unreadable clip is logged once and stays silent, and so does a platform without an engine. Call them from the UI thread, where the rest of an app’s code runs.

A clip is named by its generated res::assets::… constant (resources.md), or by path with AssetName::from_static("sounds/tap.wav") where a const needs one. Day-Games keeps its cues as statics that pair a clip with a haptic phrase; its gamekit/src/chrome.rs is a worked example.

Clips

Clips are ordinary data assets under the app’s resource/assets/, bundled on every platform like any other file there. The format is WAV. The canonical clip is 16-bit PCM, mono, 44.1 kHz, which every engine plays exactly as it is. Other PCM layouts (stereo; 8-, 24- or 32-bit integer; 32-bit float; another sample rate) also play: where an engine takes raw samples (Apple, Windows) the part converts them to the canonical form as it loads, and elsewhere the platform’s decoder reads them. Compressed formats are not supported on every engine, so the part does not accept them.

Keep clips short. Android’s and HarmonyOS’s SoundPool refuse a clip that decodes past about 1 MB, which is about 11 seconds of the canonical format, and every clip stays in memory while loaded. ffmpeg converts anything to the canonical form:

ffmpeg -i in.ogg -ac 1 -ar 44100 -c:a pcm_s16le -map_metadata -1 out.wav

Mixing

  • Voices. At most VOICES (8) clips sound at once. When all are busy, the oldest voice with the lowest priority stops for the new clip; a clip whose priority is below everything playing is skipped instead.
  • Repeats. A clip asked for again within MIN_REPEAT_MS (40 ms) of its last start is skipped, so a burst of the same sound (a streak of bricks, a deal of cards) stays crisp rather than piling up into one loud smear.
  • Volume. A play’s volume is multiplied by the master volume; a result of 0 plays nothing.
  • Pan. Apple platforms, Android, HarmonyOS and the web place a clip across the stereo field. Windows and Linux play every clip centered.
  • Other audio. Sound effects mix with music and podcasts from other apps rather than interrupting them. On iOS they follow the Silent switch.

Per-platform realization

PlatformEngineNotes
iOSAVAudioEngine with a pool of AVAudioPlayerNodesthe .ambient audio session: mixes with other audio, silenced by the Silent switch
macOSAVAudioEngine with a pool of AVAudioPlayerNodesthe engine starts on the first load
AndroidSoundPool with USAGE_GAME audio attributesloads in the background from the APK’s assets
HarmonyOSSoundPool with STREAM_USAGE_GAMEloads in the background from the app’s raw files
webWeb Audiofetches and decodes each clip; the audio context starts on the page’s first tap or key press, as browsers require
WindowsXAudio2a mastering voice in the game-effects category and 8 source voices
Linux (GTK, Qt)libcanberra, loaded at run timethe desktop’s event-sound service; each clip is cached under $XDG_CACHE_HOME/day-part-sound/ for it to read

Where loading happens in the background (Android, HarmonyOS, the web), a clip played before it is ready is skipped, because a late sound effect is worse than none; preload when a screen opens avoids that. On Linux, is_supported() is false when libcanberra is not installed, and the part has no build-time dependency on it.

The part has no cargo features: the engine is chosen by #[cfg(target_os)] and, for Android, HarmonyOS and the web, by the daybridge arms in src/lib.rs (bridge.md). On Apple platforms it links AVFAudio through [package.metadata.day.ios] and [package.metadata.day.macos] (extending.md). No platform asks the user for a permission to play sound.

Checking it

Run with DAY_LOG=debug and every play logs a play sounds/… line under the day_part_sound target, whether or not the machine has speakers. In a test or a dayscript, recent() gives the same record: Day-Showcase’s walkthrough taps a sound button and asserts the path it shows.

What it does not do

The part plays clips, not music. A music track needs streaming, looping and seeking, and would exceed SoundPool’s limit; that is a job for a streaming player rather than a sound-effects pool.