@realitycollective/iwsdk-environment 0.1.1-preview.2 → 0.1.1-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
8
8
 
9
9
  ### Added
10
10
 
11
+ - `@realitycollective/webxr-environment` - `AUDIO_ATTENUATION_DEFAULTS`, `resolveAudioSpatial` and `distanceGain`: the attenuation a positional voice has when its cue says nothing is stated in the core (Web Audio's panner defaults: inverse distance from 1 m, rolloff 1, 10 km, a full 360 degree cone with nothing outside it), and `AudioDirector` resolves every field before a request reaches a port, so `AudioVoiceRequest.spatial` is always complete for a positional voice and null only for one heard from the listener. The IWSDK and three.js ports write every field rather than inheriting their engine's. `AUDIO_LISTENER_RULE` states that the listener is the viewer's head.
12
+ - `@realitycollective/native-environment` - the audio slice's obligations now state the attenuation semantics, units and defaults and the listener rule; `NativeAudioTestHost` gains `voiceDistanceGain` and `listenerPose` readbacks and the kit setup a `headPose`, with two new cases: `audio/a positional voice attenuates by the core defaults, inverse from 1 m` and `audio/the listener is at the viewer's head`. The occlusion case now clears occlusion and passthrough afterwards, so a second run on the same host starts clean, and the kit setup takes `skySrc` for the texture sky it loads (default `kit://sky.hdr`).
11
13
  - `@realitycollective/native-environment` - `nativeEnvironmentHostConformanceCases()`, the host conformance kit a native app runs on its device against its real `environment` and `audio` slices, with the readbacks of the new `NativeEnvironmentTestHost` and `NativeAudioTestHost` types. It drives the real core directors over the host and checks what the host draws and sounds: every slot applied, passthrough suppressing sky and fog and restoring them unchanged, a transition's in-between values reaching the host, the remembered occlusion spec applied when passthrough starts, and the `overlap`, `restart`, `ignore`, `minIntervalMs` and stop policies. A host without `applyOcclusion` or without an `audio` slice fails the cases that need them, so those gaps show on the device.
12
14
  - `@realitycollective/native-environment` - binding cases for what a native host receives from the director: the five slots in the order sky, fog, ambient, key, ibl (only the slots that changed); `null` for sky and fog while passthrough is on and the spec back after; the remembered occlusion spec on passthrough, with the sky suppressed once the host reports occlusion `active`; a transition with no duration snapping; and each of the four easings' value halfway through.
13
15
  - `@realitycollective/webxr-environment` - **scene management**. `SceneManager` holds the rules and a new `ScenePort` builds and destroys content on each host. It covers a scene list (`register`, `list`), single and additive loads, `unload` of any scene in the stack with the rest keeping their order, preloading (`activate: false`, then `activate`) for an instant switch, an active scene (`getActiveScene`, `setActiveScene`) that receives new instances and whose `environment` drives the director with the configured transition, `instantiate` and `destroy` of named assets, `setNodeActive`, `makePersistent` (addressed afterwards under `PERSISTENT_SCENE_ID`), `bindNode` to tie something such as an interaction target to a node while its scene is shown, and `onSceneLoaded` / `onSceneUnloaded` / `onActiveSceneChanged`. Loads and unloads run in call order; a repeat load returns the same promise; an id not in the list rejects; a scene that repeats a node id fails to load. `dispose` leaves nothing on the host and restores the environment a scene first overrode. The owner placed it here on 2026-09-25, and `docs/BOUNDARY.md` records what moved in and what stays out.
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { AudioSource, AudioUtils, DistanceModel, PlaybackMode, Transform, } from "@iwsdk/core";
30
30
  import { Quaternion, Vector3 } from "three";
31
- import { AudioStartReaper } from "@realitycollective/webxr-environment";
31
+ import { AudioStartReaper, resolveAudioSpatial } from "@realitycollective/webxr-environment";
32
32
  export class IWSDKAudioPort {
33
33
  #world;
34
34
  #parent;
@@ -68,7 +68,10 @@ export class IWSDKAudioPort {
68
68
  view[3] = TEMP_ROTATION.w;
69
69
  }
70
70
  }
71
- const attenuation = request.spatial;
71
+ // Every field arrives resolved from the core defaults (the director's
72
+ // `resolveAudioSpatial`), Web Audio's own, so this port writes them all:
73
+ // the same cue attenuates the same on every platform.
74
+ const attenuation = positional ? (request.spatial ?? resolveAudioSpatial(null)) : null;
72
75
  entity.addComponent(AudioSource, {
73
76
  src: request.cue.src,
74
77
  volume: request.gain,
@@ -77,21 +80,15 @@ export class IWSDKAudioPort {
77
80
  autoplay: false,
78
81
  // The core already applied the cue's policy before we got here.
79
82
  playbackMode: PlaybackMode.Overlap,
80
- // Anything the cue did not describe is left to IWSDK's own defaults,
81
- // which is why these are spread in rather than defaulted here: this port
82
- // has no opinion about how far a sound carries, and inventing one would
83
- // make the same cue sound different on each engine.
84
- ...(attenuation?.refDistance === undefined ? {} : { refDistance: attenuation.refDistance }),
85
- ...(attenuation?.rolloffFactor === undefined
86
- ? {}
87
- : { rolloffFactor: attenuation.rolloffFactor }),
88
- ...(attenuation?.maxDistance === undefined ? {} : { maxDistance: attenuation.maxDistance }),
89
- ...(attenuation?.model === undefined ? {} : { distanceModel: distanceModel(attenuation.model) }),
90
- // Radians in the contract, degrees on the component - IWSDK passes these
91
- // straight to the same Web Audio panner three.js uses.
92
- ...(attenuation?.cone === undefined
83
+ ...(attenuation === null
93
84
  ? {}
94
85
  : {
86
+ refDistance: attenuation.refDistance,
87
+ rolloffFactor: attenuation.rolloffFactor,
88
+ maxDistance: attenuation.maxDistance,
89
+ distanceModel: distanceModel(attenuation.model),
90
+ // Radians in the contract, degrees on the component - IWSDK passes
91
+ // these straight to the same Web Audio panner three.js uses.
95
92
  coneInnerAngle: toDegrees(attenuation.cone.inner),
96
93
  coneOuterAngle: toDegrees(attenuation.cone.outer),
97
94
  coneOuterGain: attenuation.cone.outsideGain,
@@ -1 +1 @@
1
- {"version":3,"file":"audio-port.js","sourceRoot":"","sources":["../src/audio-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.\n *\n * ---------------------------------------------------------------------------\n * ONE ENTITY PER VOICE\n * ---------------------------------------------------------------------------\n * IWSDK's audio is a component on an entity, with its own pooling and its own\n * retrigger policy per source. Reusing one entity per CUE would hand those\n * decisions back to IWSDK and make `restart` and `ignore` mean something\n * different here than on three.js, which is the divergence this stack exists\n * to remove. So a voice is an entity: the director's policy wins, every voice\n * is individually stoppable, and IWSDK's own `playbackMode` is pinned to\n * `Overlap` so it never second-guesses the core.\n *\n * ---------------------------------------------------------------------------\n * KNOWING WHEN A SOUND FINISHED\n * ---------------------------------------------------------------------------\n * IWSDK reports whether a source IS playing, never that it has just stopped.\n * So this port polls in `update()` - which the director already calls - and\n * reports the end once a voice has been observed playing and then is not.\n * A voice that never starts at all (a missing file, a decode failure) would\n * otherwise be tracked forever, so it is reaped after `startTimeoutMs` - by\n * the shared `AudioStartReaper` every `AudioPort` in this family now composes\n * (see its file comment), not by bookkeeping local to this file. This port's\n * own contribution is calling `confirmStarted` the instant `AudioUtils.isPlaying`\n * first reports true, exactly the signal `AudioStartReaper` needs and exactly\n * what this file always polled for anyway - only the giving-up is shared now.\n */\nimport {\n AudioSource,\n AudioUtils,\n DistanceModel,\n PlaybackMode,\n Transform,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport { Quaternion, Vector3 } from \"three\";\nimport type { AudioPort, AudioVoiceRequest } from \"@realitycollective/webxr-environment\";\nimport { AudioStartReaper } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKAudioPortOptions {\n /** Parent for the voice entities. Defaults to the world's default parent. */\n readonly parent?: Entity;\n /**\n * How long to wait for a voice to start before giving up on it,\n * milliseconds. Default 10 000 - generous, because a cold asset fetch on a\n * headset over hotel wifi is slower than anyone's patience. See\n * `AudioStartReaper` / `DEFAULT_AUDIO_START_TIMEOUT_MS` in\n * `@realitycollective/webxr-environment`, which every platform now shares.\n */\n readonly startTimeoutMs?: number;\n}\n\ninterface Voice {\n readonly entity: Entity;\n readonly request: AudioVoiceRequest;\n started: boolean;\n}\n\nexport class IWSDKAudioPort implements AudioPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #reaper: AudioStartReaper;\n readonly #voices = new Map<number, Voice>();\n\n #disposed = false;\n\n constructor(world: World, options: IWSDKAudioPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#reaper = new AudioStartReaper(\n options.startTimeoutMs === undefined ? {} : { startTimeoutMs: options.startTimeoutMs },\n );\n }\n\n start(request: AudioVoiceRequest): void {\n if (this.#disposed) {\n request.ended();\n return;\n }\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n const positional = request.at !== null || (request.cue.positional ?? false);\n if (request.at !== null) {\n const view = entity.getVectorView(Transform, \"position\");\n view[0] = request.at[0];\n view[1] = request.at[1];\n view[2] = request.at[2];\n }\n // IWSDK's audio system hands the entity's transform to the same three.js\n // `PositionalAudio` the other adapter uses, so forward is +Z here too and\n // the entity is turned to point that axis the way the sound travels.\n const towards = request.facing ?? null;\n if (towards !== null) {\n const facing = TEMP_FACING.set(towards[0], towards[1], towards[2]);\n if (facing.lengthSq() > 0) {\n TEMP_ROTATION.setFromUnitVectors(AUDIO_FORWARD, facing.normalize());\n const view = entity.getVectorView(Transform, \"orientation\");\n view[0] = TEMP_ROTATION.x;\n view[1] = TEMP_ROTATION.y;\n view[2] = TEMP_ROTATION.z;\n view[3] = TEMP_ROTATION.w;\n }\n }\n const attenuation = request.spatial;\n entity.addComponent(AudioSource, {\n src: request.cue.src,\n volume: request.gain,\n loop: request.loop,\n positional,\n autoplay: false,\n // The core already applied the cue's policy before we got here.\n playbackMode: PlaybackMode.Overlap,\n // Anything the cue did not describe is left to IWSDK's own defaults,\n // which is why these are spread in rather than defaulted here: this port\n // has no opinion about how far a sound carries, and inventing one would\n // make the same cue sound different on each engine.\n ...(attenuation?.refDistance === undefined ? {} : { refDistance: attenuation.refDistance }),\n ...(attenuation?.rolloffFactor === undefined\n ? {}\n : { rolloffFactor: attenuation.rolloffFactor }),\n ...(attenuation?.maxDistance === undefined ? {} : { maxDistance: attenuation.maxDistance }),\n ...(attenuation?.model === undefined ? {} : { distanceModel: distanceModel(attenuation.model) }),\n // Radians in the contract, degrees on the component - IWSDK passes these\n // straight to the same Web Audio panner three.js uses.\n ...(attenuation?.cone === undefined\n ? {}\n : {\n coneInnerAngle: toDegrees(attenuation.cone.inner),\n coneOuterAngle: toDegrees(attenuation.cone.outer),\n coneOuterGain: attenuation.cone.outsideGain,\n }),\n });\n this.#voices.set(request.voiceId, { entity, request, started: false });\n this.#reaper.track(request.voiceId, request, () => this.#onStartTimeout(request.voiceId));\n AudioUtils.play(entity);\n }\n\n stop(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n this.#voices.delete(voiceId);\n this.#reaper.release(voiceId);\n this.#retire(voice);\n }\n\n setGain(voiceId: number, gain: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n AudioUtils.setVolume(voice.entity, gain);\n }\n\n update(deltaMs: number): void {\n if (this.#disposed) return;\n this.#reaper.update(deltaMs);\n if (this.#voices.size === 0) return;\n for (const [id, voice] of [...this.#voices]) {\n // A looping voice ends when someone stops it, never on its own.\n if (voice.request.loop) continue;\n const playing = AudioUtils.isPlaying(voice.entity);\n if (playing) {\n if (!voice.started) {\n voice.started = true;\n this.#reaper.confirmStarted(id);\n }\n continue;\n }\n // Never (yet) observed playing: still within its start window, or\n // already handed to `#onStartTimeout` by the reaper above - either way\n // there is nothing for THIS loop to do with it.\n if (!voice.started) continue;\n this.#voices.delete(id);\n this.#retire(voice);\n voice.request.ended();\n }\n }\n\n /** The shared reaper gave up on a voice that never started; see its file comment for the rule. */\n #onStartTimeout(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n console.warn(\n `[iwsdk-environment] cue \"${voice.request.cue.id}\" never started (${voice.request.cue.src})`,\n );\n this.#voices.delete(voiceId);\n this.#retire(voice);\n voice.request.ended();\n }\n\n dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n this.#reaper.clear();\n for (const [id, voice] of [...this.#voices]) {\n this.#voices.delete(id);\n this.#retire(voice);\n }\n }\n\n #retire(voice: Voice): void {\n AudioUtils.stop(voice.entity);\n voice.entity.destroy();\n }\n}\n\n/** Our distance model names onto IWSDK's enum, which holds the same three. */\nfunction distanceModel(model: \"linear\" | \"inverse\" | \"exponential\"): string {\n if (model === \"linear\") return DistanceModel.Linear;\n if (model === \"exponential\") return DistanceModel.Exponential;\n return DistanceModel.Inverse;\n}\n\n/** Web Audio points a source along +Z, and IWSDK's audio system follows it. */\nconst AUDIO_FORWARD = new Vector3(0, 0, 1);\nconst TEMP_FACING = new Vector3();\nconst TEMP_ROTATION = new Quaternion();\n\n/** Radians in the contract; degrees on the component. */\nfunction toDegrees(radians: number): number {\n return (radians * 180) / Math.PI;\n}\n"]}
1
+ {"version":3,"file":"audio-port.js","sourceRoot":"","sources":["../src/audio-port.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EACL,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,SAAS,GAGV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAqB7F,MAAM,OAAO,cAAc;IAChB,MAAM,CAAQ;IACd,OAAO,CAAqB;IAC5B,OAAO,CAAmB;IAC1B,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE5C,SAAS,GAAG,KAAK,CAAC;IAElB,YAAY,KAAY,EAAE,UAAiC,EAAE;QAC3D,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,OAAO,GAAG,IAAI,gBAAgB,CACjC,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CACvF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAA0B;QAC9B,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAC9C,SAAS,EACT,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,CAClE,CAAC;QACF,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,KAAK,CAAC,CAAC;QAC5E,IAAI,OAAO,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;YACzD,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1B,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QACvC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YACnE,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC1B,aAAa,CAAC,kBAAkB,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;gBACpE,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;gBAC5D,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,sEAAsE;QACtE,yEAAyE;QACzE,sDAAsD;QACtD,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvF,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE;YAC/B,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG;YACpB,MAAM,EAAE,OAAO,CAAC,IAAI;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,UAAU;YACV,QAAQ,EAAE,KAAK;YACf,gEAAgE;YAChE,YAAY,EAAE,YAAY,CAAC,OAAO;YAClC,GAAG,CAAC,WAAW,KAAK,IAAI;gBACtB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC;oBACE,WAAW,EAAE,WAAW,CAAC,WAAW;oBACpC,aAAa,EAAE,WAAW,CAAC,aAAa;oBACxC,WAAW,EAAE,WAAW,CAAC,WAAW;oBACpC,aAAa,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC;oBAC/C,mEAAmE;oBACnE,6DAA6D;oBAC7D,cAAc,EAAE,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;oBACjD,cAAc,EAAE,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;oBACjD,aAAa,EAAE,WAAW,CAAC,IAAI,CAAC,WAAW;iBAC5C,CAAC;SACP,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1F,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,CAAC,OAAe,EAAE,IAAY;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,OAAe;QACpB,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACpC,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,gEAAgE;YAChE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI;gBAAE,SAAS;YACjC,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACnD,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;oBACnB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;oBACrB,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;gBAClC,CAAC;gBACD,SAAS;YACX,CAAC;YACD,kEAAkE;YAClE,uEAAuE;YACvE,gDAAgD;YAChD,IAAI,CAAC,KAAK,CAAC,OAAO;gBAAE,SAAS;YAC7B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAED,kGAAkG;IAClG,eAAe,CAAC,OAAe;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,OAAO,CAAC,IAAI,CACV,4BAA4B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,oBAAoB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAC7F,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACpB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAY;QAClB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;CACF;AAED,8EAA8E;AAC9E,SAAS,aAAa,CAAC,KAA2C;IAChE,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,MAAM,CAAC;IACpD,IAAI,KAAK,KAAK,aAAa;QAAE,OAAO,aAAa,CAAC,WAAW,CAAC;IAC9D,OAAO,aAAa,CAAC,OAAO,CAAC;AAC/B,CAAC;AAED,+EAA+E;AAC/E,MAAM,aAAa,GAAG,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3C,MAAM,WAAW,GAAG,IAAI,OAAO,EAAE,CAAC;AAClC,MAAM,aAAa,GAAG,IAAI,UAAU,EAAE,CAAC;AAEvC,yDAAyD;AACzD,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AACnC,CAAC","sourcesContent":["/**\n * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.\n *\n * ---------------------------------------------------------------------------\n * ONE ENTITY PER VOICE\n * ---------------------------------------------------------------------------\n * IWSDK's audio is a component on an entity, with its own pooling and its own\n * retrigger policy per source. Reusing one entity per CUE would hand those\n * decisions back to IWSDK and make `restart` and `ignore` mean something\n * different here than on three.js, which is the divergence this stack exists\n * to remove. So a voice is an entity: the director's policy wins, every voice\n * is individually stoppable, and IWSDK's own `playbackMode` is pinned to\n * `Overlap` so it never second-guesses the core.\n *\n * ---------------------------------------------------------------------------\n * KNOWING WHEN A SOUND FINISHED\n * ---------------------------------------------------------------------------\n * IWSDK reports whether a source IS playing, never that it has just stopped.\n * So this port polls in `update()` - which the director already calls - and\n * reports the end once a voice has been observed playing and then is not.\n * A voice that never starts at all (a missing file, a decode failure) would\n * otherwise be tracked forever, so it is reaped after `startTimeoutMs` - by\n * the shared `AudioStartReaper` every `AudioPort` in this family now composes\n * (see its file comment), not by bookkeeping local to this file. This port's\n * own contribution is calling `confirmStarted` the instant `AudioUtils.isPlaying`\n * first reports true, exactly the signal `AudioStartReaper` needs and exactly\n * what this file always polled for anyway - only the giving-up is shared now.\n */\nimport {\n AudioSource,\n AudioUtils,\n DistanceModel,\n PlaybackMode,\n Transform,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport { Quaternion, Vector3 } from \"three\";\nimport type { AudioPort, AudioVoiceRequest } from \"@realitycollective/webxr-environment\";\nimport { AudioStartReaper, resolveAudioSpatial } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKAudioPortOptions {\n /** Parent for the voice entities. Defaults to the world's default parent. */\n readonly parent?: Entity;\n /**\n * How long to wait for a voice to start before giving up on it,\n * milliseconds. Default 10 000 - generous, because a cold asset fetch on a\n * headset over hotel wifi is slower than anyone's patience. See\n * `AudioStartReaper` / `DEFAULT_AUDIO_START_TIMEOUT_MS` in\n * `@realitycollective/webxr-environment`, which every platform now shares.\n */\n readonly startTimeoutMs?: number;\n}\n\ninterface Voice {\n readonly entity: Entity;\n readonly request: AudioVoiceRequest;\n started: boolean;\n}\n\nexport class IWSDKAudioPort implements AudioPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #reaper: AudioStartReaper;\n readonly #voices = new Map<number, Voice>();\n\n #disposed = false;\n\n constructor(world: World, options: IWSDKAudioPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#reaper = new AudioStartReaper(\n options.startTimeoutMs === undefined ? {} : { startTimeoutMs: options.startTimeoutMs },\n );\n }\n\n start(request: AudioVoiceRequest): void {\n if (this.#disposed) {\n request.ended();\n return;\n }\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n const positional = request.at !== null || (request.cue.positional ?? false);\n if (request.at !== null) {\n const view = entity.getVectorView(Transform, \"position\");\n view[0] = request.at[0];\n view[1] = request.at[1];\n view[2] = request.at[2];\n }\n // IWSDK's audio system hands the entity's transform to the same three.js\n // `PositionalAudio` the other adapter uses, so forward is +Z here too and\n // the entity is turned to point that axis the way the sound travels.\n const towards = request.facing ?? null;\n if (towards !== null) {\n const facing = TEMP_FACING.set(towards[0], towards[1], towards[2]);\n if (facing.lengthSq() > 0) {\n TEMP_ROTATION.setFromUnitVectors(AUDIO_FORWARD, facing.normalize());\n const view = entity.getVectorView(Transform, \"orientation\");\n view[0] = TEMP_ROTATION.x;\n view[1] = TEMP_ROTATION.y;\n view[2] = TEMP_ROTATION.z;\n view[3] = TEMP_ROTATION.w;\n }\n }\n // Every field arrives resolved from the core defaults (the director's\n // `resolveAudioSpatial`), Web Audio's own, so this port writes them all:\n // the same cue attenuates the same on every platform.\n const attenuation = positional ? (request.spatial ?? resolveAudioSpatial(null)) : null;\n entity.addComponent(AudioSource, {\n src: request.cue.src,\n volume: request.gain,\n loop: request.loop,\n positional,\n autoplay: false,\n // The core already applied the cue's policy before we got here.\n playbackMode: PlaybackMode.Overlap,\n ...(attenuation === null\n ? {}\n : {\n refDistance: attenuation.refDistance,\n rolloffFactor: attenuation.rolloffFactor,\n maxDistance: attenuation.maxDistance,\n distanceModel: distanceModel(attenuation.model),\n // Radians in the contract, degrees on the component - IWSDK passes\n // these straight to the same Web Audio panner three.js uses.\n coneInnerAngle: toDegrees(attenuation.cone.inner),\n coneOuterAngle: toDegrees(attenuation.cone.outer),\n coneOuterGain: attenuation.cone.outsideGain,\n }),\n });\n this.#voices.set(request.voiceId, { entity, request, started: false });\n this.#reaper.track(request.voiceId, request, () => this.#onStartTimeout(request.voiceId));\n AudioUtils.play(entity);\n }\n\n stop(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n this.#voices.delete(voiceId);\n this.#reaper.release(voiceId);\n this.#retire(voice);\n }\n\n setGain(voiceId: number, gain: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n AudioUtils.setVolume(voice.entity, gain);\n }\n\n update(deltaMs: number): void {\n if (this.#disposed) return;\n this.#reaper.update(deltaMs);\n if (this.#voices.size === 0) return;\n for (const [id, voice] of [...this.#voices]) {\n // A looping voice ends when someone stops it, never on its own.\n if (voice.request.loop) continue;\n const playing = AudioUtils.isPlaying(voice.entity);\n if (playing) {\n if (!voice.started) {\n voice.started = true;\n this.#reaper.confirmStarted(id);\n }\n continue;\n }\n // Never (yet) observed playing: still within its start window, or\n // already handed to `#onStartTimeout` by the reaper above - either way\n // there is nothing for THIS loop to do with it.\n if (!voice.started) continue;\n this.#voices.delete(id);\n this.#retire(voice);\n voice.request.ended();\n }\n }\n\n /** The shared reaper gave up on a voice that never started; see its file comment for the rule. */\n #onStartTimeout(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n console.warn(\n `[iwsdk-environment] cue \"${voice.request.cue.id}\" never started (${voice.request.cue.src})`,\n );\n this.#voices.delete(voiceId);\n this.#retire(voice);\n voice.request.ended();\n }\n\n dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n this.#reaper.clear();\n for (const [id, voice] of [...this.#voices]) {\n this.#voices.delete(id);\n this.#retire(voice);\n }\n }\n\n #retire(voice: Voice): void {\n AudioUtils.stop(voice.entity);\n voice.entity.destroy();\n }\n}\n\n/** Our distance model names onto IWSDK's enum, which holds the same three. */\nfunction distanceModel(model: \"linear\" | \"inverse\" | \"exponential\"): string {\n if (model === \"linear\") return DistanceModel.Linear;\n if (model === \"exponential\") return DistanceModel.Exponential;\n return DistanceModel.Inverse;\n}\n\n/** Web Audio points a source along +Z, and IWSDK's audio system follows it. */\nconst AUDIO_FORWARD = new Vector3(0, 0, 1);\nconst TEMP_FACING = new Vector3();\nconst TEMP_ROTATION = new Quaternion();\n\n/** Radians in the contract; degrees on the component. */\nfunction toDegrees(radians: number): number {\n return (radians * 180) / Math.PI;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/iwsdk-environment",
3
- "version": "0.1.1-preview.2",
3
+ "version": "0.1.1-preview.3",
4
4
  "description": "Meta IWSDK adapter for the Reality Collective WebXR Environment Extensions - drives IWSDK's own DomeGradient, light components, transform entities and AudioSource machinery from the engine-free @realitycollective/webxr-environment core, with a one-call registerEnvironment(world) setup and an ECS system that ticks the directors. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -35,7 +35,7 @@
35
35
  "typecheck": "tsc -p tsconfig.json --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@realitycollective/webxr-environment": "^0.1.1-preview.2"
38
+ "@realitycollective/webxr-environment": "^0.1.1-preview.3"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@iwsdk/core": ">=1.0.0 <2.0.0",