I think the reason it assumed it was a design decision was because of mobile?
More grok notes on this.
You’re correct — my previous analysis was based on the old OpenAL backend. As of Defold 1.12.0 (released January 2026), the macOS/iOS sound backend was completely replaced with a new AVAudio-based implementation (using AVFoundation / AVAudioEngine). This was done via PR #11072 to remove the deprecated OpenAL code. The old device_openal.cpp path is no longer used on Apple platforms; the new file is engine/sound/src/devices/device_avaudio.mm.
The first-play delay is still present (and feels exactly the same to users), because the core lazy-initialization design in the shared sound system was kept unchanged. The AVAudioEngine graph is created during device open, but the expensive activation (engine start, audio session activation, node attachment, hardware warm-up, etc.) only happens on the first sound.play().
Exact current root cause (as of dev branch / 1.12.x)
The sound system architecture is unchanged at the high level:
-
Engine init (dmSound::Initialize in sound.cpp):
- Calls
DeviceOpen → DeviceAVAudioOpen (in device_avaudio.mm).
- This creates the
AVAudioEngine, mixer nodes, player nodes, sets sample rate to 48 kHz, allocates buffers, etc.
- Does NOT start the engine or activate the audio session.
- Sets an internal flag (now
m_Started on the AVAudioDevice struct, previously m_IsDeviceStarted on the SoundSystem).
-
Sound update loop (UpdateInternal in sound.cpp — runs every frame or in the sound thread):
-
Scans for playing instances.
-
Lazy start check (still present, verbatim pattern from the OpenAL era):
// Simplified from current sound.cpp
if (!sound->m_IsDeviceStarted /* or equivalent device->m_Started */) {
sound->m_DeviceType->m_DeviceStart(sound->m_Device); // ← calls DeviceAVAudioStart
sound->m_IsDeviceStarted = true;
}
-
First sound.play() triggers DeviceAVAudioStart (in device_avaudio.mm):
- This is where the real delay hits:
[self.engine startAndReturnError:] (or equivalent engine.start()).
- AVAudioSession activation (
setActive:YES).
- Graph connection, hardware resource allocation, possible Core Audio / HAL warm-up on macOS.
- On macOS this one-time hit is often 0.5–1+ seconds (especially on newer macOS versions or certain hardware configs), exactly as you and other users observe.
- Subsequent plays are instant because the engine stays running.
The new backend was a drop-in replacement focused on deprecation and device-switching support (DeviceAVAudioReconfigureIfNeeded for headphones, etc.), but it inherited the exact same lazy-start philosophy that was in the OpenAL code. The PR notes and release notes do not mention changing the init timing.
This is not a buffer-size issue (the new backend still uses Defold’s software mixing + queued buffers) or decoding latency — it’s purely the one-time AVAudioEngine activation.
What the fix should be
Move device start to initialization time. Audio engines should be “hot” by the time the game reaches the first frame or menu, not on the first sound effect.
Recommended minimal change (cleanest)
In engine/sound/src/sound.cpp, in dmSound::Initialize:
// After successful DeviceOpen
Result res = sound->m_DeviceType->m_DeviceOpen(...);
if (res == RESULT_OK) {
// NEW: start immediately instead of lazy
sound->m_DeviceType->m_DeviceStart(sound->m_Device);
sound->m_IsDeviceStarted = true; // or device->m_Started = true;
}
Then update UpdateInternal to skip the lazy check entirely (or keep it only for platforms that still want lazy behavior).
Even better: make it configurable per-platform
Add a project setting (e.g., in game.project under [sound]):
start_audio_on_init = 1 ; default 1 on macOS/HTML5, 0 on mobile if power is a concern
Additional nice-to-have in the AVAudio backend
In device_avaudio.mm:
- Call
prepare() on nodes before start() (helps a tiny bit).
- Use
AVAudioSessionCategoryPlayback with setActive:YES as early as possible.
- Handle the
AVAudioEngineConfigurationChangeNotification more gracefully (already partially there via reconfigure).