Big delay playing sounds the first time (desktop)

I’m getting a strange delay, almost of a second when playing a sound for the first time. It’s like the sounds are not loaded initially. This on desktop on both editor run or a full no-debug build.

I’m using the latest beta and I’m on a Macbook pro with Sequoia. Anyone else hitting this? Is this due to sound format?
I haven’t done anything with sound in a very long time.

Attached is a minimal test project. If you press the LMB you’ll see the screen turn red but the sound doesn’t play immediately the first time. Subsequent plays are fine.

SoundTest.zip (150.6 KB)

I’m getting access denied on your upload can you reupload?

This is on us! Investigating!

1 Like

It was misconfiguration of the forum. Now it works correctly.

2 Likes

Tested your sample on Defold Engine 1.12.2 (e43be33) and indeed the first sound does play delayed after the first red flash on my mac. If this is a problem for you you could play a silent sound on startup. Maybe there could be a future game.project config to force it to be ready at start without this or just force it if audio playback is included in the app manifest.

I asked grok (the grok.com version) to research the repo (very useful tool for this kind of thing) here is its findings.


The delay on the first sound.play() (but not subsequent ones) on macOS is caused by lazy initialization of the OpenAL audio device/context in Defold’s sound engine. This is a deliberate design choice in the core sound system that only activates the actual audio hardware/output on the first active sound instance. Apple’s OpenAL implementation (which Defold uses on macOS via the __MACH__ path) introduces noticeable one-time latency when the context is activated, due to its underlying Core Audio integration. Once activated, the device stays “warm” for the rest of the app run, so later plays are instant.

Exact location and flow in the open-source code

Defold’s sound engine lives in engine/sound/ (repo: GitHub - defold/defold: Defold is a completely free to use game engine for development of desktop, mobile and web games. · GitHub ). The relevant files are:

  • engine/sound/src/sound.cpp — core sound system, mixing, threading, and lazy device start.
  • engine/sound/src/devices/device_openal.cpp — the OpenAL backend used on macOS (and iOS/Android in some configs).

1. Engine startup (sound system init — no delay here)

In sound.cpp::Initialize() (called once at engine boot via dmSound::Init):

  • Platform init + OpenDevice() → calls DeviceOpenALOpen.

  • In device_openal.cpp::DeviceOpenALOpen:

    al_device = alcOpenDevice(0);  // creates device
    al_context = alcCreateContext(...);  // tries 48 kHz then 44.1 kHz
    alcMakeContextCurrent(al_context);   // temporary
    // ... generate buffers + 1 source
    alcMakeContextCurrent(NULL);         // immediately deactivates!
    
  • Creates output buffers, optional sound thread (SoundThread), etc.

  • m_IsDeviceStarted = false (this flag lives in the SoundSystem struct).

Device is opened but not started/activated yet. The context is explicitly deactivated.

2. Sound thread / update loop (runs continuously)

  • If threading is enabled (default on desktop/macOS): SoundThread loops and calls UpdateInternal every ~8 ms (dmTime::Sleep(8000)).
  • Update() just returns the latest atomic status (or calls UpdateInternal directly if no thread).

3. First sound.play() → the actual delay trigger

sound.play() (Lua API) ultimately calls dmSound::Play(HSoundInstance) in sound.cpp:

Result Play(HSoundInstance sound_instance) {
    sound_instance->m_Playing = 1;  // just sets a flag
    return RESULT_OK;
}

The real work happens in the next UpdateInternal() (in the sound thread or main update):

  • Detects active playing instances.
  • Lazy device start (this is the root cause):
if (sound->m_IsDeviceStarted == false) {
    sound->m_DeviceType->m_DeviceStart(sound->m_Device);  // calls DeviceOpenALStart
    sound->m_IsDeviceStarted = true;
}

In device_openal.cpp::DeviceOpenALStart:

void DeviceOpenALStart(dmSound::HDevice device) {
    OpenALDevice* openal = (OpenALDevice*) device;
    if (!alcMakeContextCurrent(openal->m_Context)) {
        dmLogError(...);
    }
}

This is the first time the OpenAL context becomes active after engine start. Then:

  • UpdateInternal begins mixing (MixInstancesMaster).
  • Buffers get filled and queued via DeviceOpenALQueuealBufferData + alSourceQueueBuffers + alSourcePlay (if not already playing).

Why the delay only on macOS / first play?

  • Apple’s OpenAL (deprecated since macOS 10.15) sits on Core Audio. Activating the context / starting audio I/O the very first time incurs OS-level overhead (hardware resource allocation, audio session activation, possible driver warm-up, etc.). This is well-known to cause 0.5–1+ second delays in some apps on macOS (especially recent versions like Sequoia).
  • Subsequent sound.play() calls skip the if (!m_IsDeviceStarted) block entirely — the context is already current and the source is running.
  • The 6-buffer / 768-frame OpenAL queue (mentioned in Defold forums) adds some inherent latency, but that’s constant; the extra first-play hit is the lazy activation.

No other lazy loading (sound data decoding happens at component load time or on-demand but not tied to this delay; buffers are pre-allocated).

Why this design?

Defold mixes audio in software (CPU) and only uses the native backend (OpenAL here) to queue pre-mixed buffers. Keeping the device inactive until needed saves resources and avoids background audio on mobile/desktop when nothing is playing. On macOS this trade-off surfaces as the first-play delay because of Apple’s audio stack.

please open an issue for this in our issue tracker, I’ll take alook

We don’t use OpenAL on iOS/macOS anymore. Since 1.12.0 it is AVAudio Engine

3 Likes

I don’t think so, no. A second delay is unacceptable imho, for simply waiting for the first sound.
Initialization should happen during the initialization phases.

2 Likes

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:

  1. Engine init (dmSound::Initialize in sound.cpp):

    • Calls DeviceOpenDeviceAVAudioOpen (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).
  2. 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;
      }
      
  3. 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).
3 Likes

Posting this here for reference.

It appears that the delay is there if using a different output device than the speakers on the Macbook.
Airpro pro or even the speakers from the Apple Studio Display have the delay. The internal speakers do not.

At this point I’m keen to consider that this is an Apple issue. I’m probably going to test some other engines to see how they behave and will try to trick it into activating via a fake sound played as the game starts.