Even when I tap on the screen in iOS, the sound still doesn't play

I’ve added sound to my project, but it doesn’t seem to play — even when I tap on it. To resume audio on iOS, I’m using a specific function. But they doesnt seem to work as well.

  function unlockWorldAudio() {
      const audioSys = world.audio as any
      console.log('world audio called')

      // Some versions expose audioCtx or context
      const ctx = audioSys.ctx || audioSys.context || audioSys.audioCtx
      console.log('ctx ', ctx, ' >>>> ')
      if (ctx && ctx.state === 'suspended') {
        console.log('ctx', ctx.state)
        ctx.resume().then(() => {
          console.log('world.audio unlocked')
        })
      }
    }

    function resumeAudio(timeout = 1) {
      setTimeout(() => {
        console.log('resumeAudio')
        const context = new (window.AudioContext || (window as any).webkitAudioContext)()
        if (context && (context.state === 'suspended' || context.state === 'interrupted')) {
          context.resume().then(() => {
            console.log('resumeAudio - [audio-fix] AudioContext resumed')
          }).catch((e) => {
            console.warn('resumeAudio - [audio-fix] Resume failed:', e)
          })
        }
      }, timeout)
    }

Can you land your changes and share the project with the support workspace so I can take a look?

I’m unable to share the full project, but I’m including the relevant code snippet related to sound handling.

We’re using a PlaySound function that selects and plays audio clips based on an aid value. Each model in the experience has around 4–5 animations. Once an animation finishes, the GLTF_ANIMATION_FINISHED event listener triggers the next sound by calling PlaySound with the appropriate audio ID.

This setup manages sound transitions between animations. While background music plays correctly upon user interaction, some of the transition sounds fail to play consistently.

Question:
Is a separate user gesture or interaction required for each sound to play, or is there a way to handle this within the existing animation flow?

Code snippet:-

function PlaySound(aid = -1, canLoop = false) {
    let soundName = '00_intro.mp3'
    aid %= 5
    switch (aid) {
        case 0:
            soundName = '01_setup.mp3'
            break

        case 1:
            soundName = '02_story.mp3'
            break

        case 2:
            soundName = '03_story.mp3'
            break

        case 3:
            soundName = '04_story.mp3'
            break

        case 4:
            soundName = '05_story.mp3'
            break

    
        default:
            soundName = '00_intro.mp3'
            break
    }

    const soundPath = `assets/sounds/${soundName}`
    ecs.Audio.set(world, soundManager, {
        url: null,
        paused: false,
        loop: canLoop,
    })

    requestAnimationFrame(() => {
        console.log('playing sound', soundName, ' >>>> ', canLoop)
        ecs.Audio.set(world, gameSoundManager, {
            url: soundPath,
            paused: false,
            loop: canLoop,
        })
    })
}
world.events.addListener(fieldModel, ecs.events.GLTF_ANIMATION_FINISHED, () => {
    console.log('event anim ended = ', animId)
    animCounter++
    animId += 1
    PlaySound(animId)
})


I’m not sure your requestAnimationFrame function is ever getting called. I would use a State Machine in your Custom Component with a different state for each sound and then set the sound path onEnter for the given state. You should only need one interaction on the page for sounds to work.

Thanks for the suggestion. I actually implemented a state machine with a different state for each sound, setting the sound path in the onEnter of each state. However, the result is the same — some audio files play, while others still don’t.

code Snippet:-

import * as ecs from '@8thwall/ecs'
import {GlobalRef} from './GlobalReferences'

ecs.registerComponent({
  name: 'SoundManagerScript',
  schema: {
    audioCompNode: ecs.eid,
  },

  add: (world, component) => {
    GlobalRef.instance.soundManagerScript = component.eid

    // Unlock audio on first gesture (Safari/iOS requirement)
    window.addEventListener(
      'click',
      () => {
        console.log('🔓 Audio unlocked')
        world.audio.play()
      },
      {once: true}
    )
  },

  stateMachine: ({world, eid, schemaAttribute}) => {
    const {audioCompNode} = schemaAttribute.get(eid)

    // --- Utility ---
    function playSound(filename, loop = false) {

      const path = `assets/sounds/${filename}`
      console.log('🎶 Path:', path)

      // Reset audio node before playing
      ecs.Audio.set(world, audioCompNode, {
        url: path,
        paused: false,
        loop: false,
        volume: 1,
      })

    }

    // --- Define triggers ---
    const toDefault = ecs.defineTrigger()
    const toSetup = ecs.defineTrigger()
    const toStory1 = ecs.defineTrigger()
    const toStory2 = ecs.defineTrigger()
    const toStory3 = ecs.defineTrigger()
    const toStory4 = ecs.defineTrigger()
    const toHome = ecs.defineTrigger()
    const toNone = ecs.defineTrigger()

    // Put triggers in a map for wiring
    const triggerMap = {
      default: toDefault,
      setup: toSetup,
      story1: toStory1,
      story2: toStory2,
      story3: toStory3,
      story4: toStory4,
      home: toHome,
      none: toNone,
    }

    // --- Define states ---
    const defaultState = ecs.defineState('default').initial().onEnter(() => playSound('noSound.mp3'))
    const setup = ecs.defineState('setup').onEnter(() => playSound('01_setup.mp3'))
    const story1 = ecs.defineState('story1').onEnter(() => playSound('02_story.mp3'))
    const story2 = ecs.defineState('story2').onEnter(() => playSound('03_story.mp3'))
    const story3 = ecs.defineState('story3').onEnter(() => playSound('04_story.mp3'))
    const story4 = ecs.defineState('story4').onEnter(() => playSound('05_story.mp3'))
    const home = ecs.defineState('home').onEnter(() => playSound('homeMusic.mp3', true))
    const none = ecs.defineState('none').onEnter(() => playSound('noSound.mp3'))

    const allStates = [defaultState, setup, story1, story2, story3, story4, home, none]

    // --- Wire every trigger to every state (universal jumping) ---
    allStates.forEach((state) => {
      Object.entries(triggerMap).forEach(([name, trigger]) => {
        state.onTrigger(trigger, name)
      })
    })

    // --- Expose globally ---
    GlobalRef.instance.PlaySound = (stateName) => {
      const trigger = triggerMap[stateName]
      if (!trigger) {
        console.warn(`⚠️ Unknown sound state: ${stateName}`)
        return
      }

      console.log(`🎵 Switching to state: ${stateName}`)
      trigger.trigger()
    }

    return allStates
  },
})



//Function:-

  function PlaySoundBasedOnID(aid = -1, canLoop = false) {
    switch (aid) {
      case 0:  // Setup
        GlobalRef.instance.PlaySound('setup')
        break
      case 1:  // Story 1
        GlobalRef.instance.PlaySound('story1')
        break
      case 2:  // Story 2
        GlobalRef.instance.PlaySound('story2')
        break
      case 3:  // Story 3
        GlobalRef.instance.PlaySound('story3')
        break
      case 4:  // Story 4
        GlobalRef.instance.PlaySound('story4')
        break
      case 100:  // Home music (looping)
        GlobalRef.instance.PlaySound('home')
        break
      case 400:  // No sound
        GlobalRef.instance.PlaySound('none')
        break
      default:  // Default intro
        GlobalRef.instance.PlaySound('default')
        break
    }
  }

Is it random which file plays and which doesn’t or is it always the same file?

80% of the time it is the same file, and the rest is random

Nothing in the code stands out as a cause for this behavior. Since projects often involve many moving parts, I’m unable to fully debug the issue without access to the full project.