import { PositionalAudio } from "three"; import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js'; import { isDevEnvironment } from "../engine/debug/index.js"; import { Application, ApplicationEvents } from "../engine/engine_application.js"; import { findObjectOfType } from "../engine/engine_components.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { DeviceUtilities, getParam } from "../engine/engine_utils.js"; import { AudioListener } from "./AudioListener.js"; import { Behaviour, GameObject } from "./Component.js"; const debug = getParam("debugaudio"); /** * Defines how audio volume attenuates over distance from the listener. */ export enum AudioRolloffMode { /** * Logarithmic rolloff provides a natural, real-world attenuation where volume decreases * exponentially with distance. */ Logarithmic = 0, /** * Linear rolloff provides a straightforward volume reduction that decreases at a constant * rate with distance. */ Linear = 1, /** * Custom rolloff allows for defining specialized distance-based attenuation curves. * Note: Custom rolloff is not fully implemented in this version. */ Custom = 2, } /** * Plays audio clips in the scene with support for spatial (3D) positioning. * * **Browser autoplay policies:** * Web browsers require user interaction before playing audio. Use * `AudioSource.userInteractionRegistered` to check if playback is allowed, * or `registerWaitForAllowAudio()` to queue playback until interaction occurs. * * **Spatial audio:** * Set `spatialBlend` to 1 for full 3D positioning, or 0 for 2D (non-spatial). * Requires an {@link AudioListener} in the scene (typically on the camera). * * **Visibility handling:** * Audio automatically pauses when the tab is hidden unless `playInBackground = true`. * On mobile, audio always pauses in background regardless of this setting. * * @example Play audio on button click * ```ts * onClick() { * const audio = this.getComponent(AudioSource); * audio.play(); * } * ``` * * @example Wait for user interaction * ```ts * AudioSource.registerWaitForAllowAudio(() => { * this.getComponent(AudioSource)?.play(); * }); * ``` * * @summary Plays audio clips from files or media streams * @category Multimedia * @group Components * @see {@link AudioListener} for the audio receiver component * @see {@link AudioRolloffMode} for distance attenuation options * @see {@link Voip} for voice communication * @see {@link PlayableDirector} for timeline-based audio * @link https://engine.needle.tools/samples/?overlay=samples&tag=audio * @link https://spatial-audio-zubckswmztj.needle.run/ */ export class AudioSource extends Behaviour { /** * Checks if the user has interacted with the page to allow audio playback. * Audio playback often requires a user gesture first due to browser autoplay policies. * This is the same as calling {@link Application.userInteractionRegistered}. * * @returns Whether user interaction has been registered to allow audio playback */ public static get userInteractionRegistered(): boolean { return Application.userInteractionRegistered; } /** * Registers a callback that will be executed once the user has interacted with the page, * allowing audio playback to begin. * This is the same as calling {@link Application.registerWaitForInteraction}. * * @param cb - The callback function to execute when user interaction is registered */ public static registerWaitForAllowAudio(cb: Function) { Application.registerWaitForInteraction(cb); } /** * The audio clip to play. Can be a URL string pointing to an audio file or a {@link MediaStream} object. */ @serializable(URL) clip: string | MediaStream = ""; /** * When true, the audio will automatically start playing when the component is enabled. * When false, you must call play() manually to start audio playback. * @default false */ @serializable() playOnAwake: boolean = false; /** * When true, the audio clip will be loaded during initialization rather than when play() is called. * This can reduce playback delay but increases initial loading time. * @default true */ @serializable() preload: boolean = true; /** * When true, audio will continue playing when the browser tab loses focus. * When false, audio will pause when the tab is minimized or not active. * @default true */ @serializable() playInBackground: boolean = true; /** * Indicates whether the audio is currently playing. * * @returns True if the audio is playing, false otherwise */ get isPlaying(): boolean { // For the media-element path (string clips and MediaStreams) three.js does NOT maintain // sound.isPlaying (hasPlaybackControl is false once a media/stream source is set), so the //