Repository: playcanvas/engine
Pull request #4008 (2022-02-09 22:48:53)

Improve JSDoc comments for sound modules
* Refactor sound code to assume presence of Web Audio API and only fix up classes if it's not available (IE11).
* Improve quality of JSDoc comments to enable IntelliSense and correct type declarations.
* Upgrade calls to deprecated Web Audio API to the recommended API.

I confirm I have read the [contributing guidelines](https://github.com/playcanvas/engine/blob/master/.github/CONTRIBUTING.md) and signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform).



Patch:
diff --git a/src/audio/channel3d.js b/src/audio/channel3d.js
index 97aeb408a..9c9b39397 100644
--- a/src/audio/channel3d.js
+++ b/src/audio/channel3d.js
@@ -54,7 +54,9 @@ class Channel3d extends Channel {
 
     setPosition(position) {
         this.position.copy(position);
-        this.panner.setPosition(position.x, position.y, position.z);
+        this.panner.positionX.value = position.x;
+        this.panner.positionY.value = position.y;
+        this.panner.positionZ.value = position.z;
     }
 
     setVelocity(velocity) {
diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js
index 93033db14..bda9cfeea 100644
--- a/src/deprecated/deprecated.js
+++ b/src/deprecated/deprecated.js
@@ -37,7 +37,7 @@ import {
 import { drawQuadWithShader } from '../graphics/simple-post-effect.js';
 import { programlib } from '../graphics/program-lib/program-lib.js';
 import { shaderChunks } from '../graphics/program-lib/chunks/chunks.js';
-import { GraphicsDevice } from '../graphics/graphics-device.js';
+import { GraphicsDevice } from '../graphics/webgl/graphics-device.js';
 import { IndexBuffer } from '../graphics/index-buffer.js';
 import { createFullscreenQuad, drawFullscreenQuad, PostEffect } from '../graphics/post-effect.js';
 import { PostEffectQueue } from '../framework/components/camera/post-effect-queue.js';
diff --git a/src/framework/application.js b/src/framework/application.js
index d3088ff9f..fa2382f53 100644
--- a/src/framework/application.js
+++ b/src/framework/application.js
@@ -20,7 +20,7 @@ import {
     PRIMITIVE_TRIANGLES, PRIMITIVE_TRIFAN, PRIMITIVE_TRISTRIP
 } from '../graphics/constants.js';
 import { destroyPostEffectQuad } from '../graphics/simple-post-effect.js';
-import { GraphicsDevice } from '../graphics/graphics-device.js';
+import { GraphicsDevice } from '../graphics/webgl/graphics-device.js';
 
 import {
     LAYERID_DEPTH, LAYERID_IMMEDIATE, LAYERID_SKYBOX, LAYERID_UI, LAYERID_WORLD,
diff --git a/src/framework/stats.js b/src/framework/stats.js
index a6835f219..a9d72d81f 100644
--- a/src/framework/stats.js
+++ b/src/framework/stats.js
@@ -1,6 +1,6 @@
 import { getApplication } from './globals.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * Records performance-related statistics related to the application.
diff --git a/src/graphics/debug-graphics.js b/src/graphics/debug-graphics.js
index ee569aaf4..c3cd5cc86 100644
--- a/src/graphics/debug-graphics.js
+++ b/src/graphics/debug-graphics.js
@@ -1,4 +1,4 @@
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * Internal graphics debug system - gpu markers and similar. Note that the functions only execute in the
diff --git a/src/graphics/index-buffer.js b/src/graphics/index-buffer.js
index 41418ec1a..909a8bb1c 100644
--- a/src/graphics/index-buffer.js
+++ b/src/graphics/index-buffer.js
@@ -3,7 +3,7 @@ import {
     BUFFER_STATIC, INDEXFORMAT_UINT16, INDEXFORMAT_UINT32, typedArrayIndexFormatsByteSize
 } from './constants.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * An index buffer stores index values into a {@link VertexBuffer}. Indexed graphical primitives
diff --git a/src/graphics/post-effect.js b/src/graphics/post-effect.js
index a11d01162..c95a59a0c 100644
--- a/src/graphics/post-effect.js
+++ b/src/graphics/post-effect.js
@@ -4,7 +4,7 @@ import { VertexFormat } from './vertex-format.js';
 import { VertexIterator } from './vertex-iterator.js';
 
 /** @typedef {import('../math/vec4.js').Vec4} Vec4 */
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./render-target.js').RenderTarget} RenderTarget */
 /** @typedef {import('./shader.js').Shader} Shader */
 
diff --git a/src/graphics/program-lib/programs/standard.js b/src/graphics/program-lib/programs/standard.js
index 6b8bed6b4..177476232 100644
--- a/src/graphics/program-lib/programs/standard.js
+++ b/src/graphics/program-lib/programs/standard.js
@@ -25,7 +25,7 @@ import { LightsBuffer } from '../../../scene/lighting/lights-buffer.js';
 
 import { begin, end, fogCode, gammaCode, precisionCode, skinCode, tonemapCode, versionCode } from './common.js';
 
-/** @typedef {import('../../graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../../webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 const _matTex2D = [];
 
diff --git a/src/graphics/render-target.js b/src/graphics/render-target.js
index e5582b7c7..ad9ec66b4 100644
--- a/src/graphics/render-target.js
+++ b/src/graphics/render-target.js
@@ -1,7 +1,7 @@
 import { Debug } from '../core/debug.js';
 import { PIXELFORMAT_DEPTH, PIXELFORMAT_DEPTHSTENCIL } from './constants.js';
 
-import { GraphicsDevice } from './graphics-device.js';
+import { GraphicsDevice } from './webgl/graphics-device.js';
 
 /** @typedef {import('./texture.js').Texture} Texture */
 
diff --git a/src/graphics/reproject-texture.js b/src/graphics/reproject-texture.js
index 0b758892d..eb7844142 100644
--- a/src/graphics/reproject-texture.js
+++ b/src/graphics/reproject-texture.js
@@ -11,7 +11,7 @@ import { createShaderFromCode } from './program-lib/utils.js';
 import { drawQuadWithShader } from './simple-post-effect.js';
 import { shaderChunks } from './program-lib/chunks/chunks.js';
 import { RenderTarget } from './render-target.js';
-import { GraphicsDevice } from './graphics-device.js';
+import { GraphicsDevice } from './webgl/graphics-device.js';
 import { Texture } from './texture.js';
 import { DebugGraphics } from './debug-graphics.js';
 
diff --git a/src/graphics/shader-input.js b/src/graphics/shader-input.js
index 98393d396..60eb2c042 100644
--- a/src/graphics/shader-input.js
+++ b/src/graphics/shader-input.js
@@ -2,7 +2,7 @@ import { UNIFORMTYPE_FLOAT, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_VEC2, UNIFORMTYP
     UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY } from './constants.js';
 import { Version } from './version.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * Representation of a shader uniform.
diff --git a/src/graphics/shader.js b/src/graphics/shader.js
index bdd39d0e1..83c8462c2 100644
--- a/src/graphics/shader.js
+++ b/src/graphics/shader.js
@@ -1,4 +1,4 @@
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * A shader is a program that is responsible for rendering graphical primitives on a device's
diff --git a/src/graphics/simple-post-effect.js b/src/graphics/simple-post-effect.js
index 75ddf483d..a02650ab7 100644
--- a/src/graphics/simple-post-effect.js
+++ b/src/graphics/simple-post-effect.js
@@ -4,7 +4,7 @@ import { VertexFormat } from './vertex-format.js';
 import { DebugGraphics } from './debug-graphics.js';
 
 /** @typedef {import('../math/vec4.js').Vec4} Vec4 */
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./render-target.js').RenderTarget} RenderTarget */
 /** @typedef {import('./shader.js').Shader} Shader */
 /** @typedef {import('./texture.js').Texture} Texture */
diff --git a/src/graphics/texture.js b/src/graphics/texture.js
index 14506be27..5332d46a4 100644
--- a/src/graphics/texture.js
+++ b/src/graphics/texture.js
@@ -17,7 +17,7 @@ import {
     TEXTURETYPE_DEFAULT, TEXTURETYPE_RGBM, TEXTURETYPE_RGBE, TEXTURETYPE_SWIZZLEGGGR
 } from './constants.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 let _pixelSizeTable = null;
 let _blockSizeTable = null;
diff --git a/src/graphics/transform-feedback.js b/src/graphics/transform-feedback.js
index be40a3b0d..3d4366515 100644
--- a/src/graphics/transform-feedback.js
+++ b/src/graphics/transform-feedback.js
@@ -4,7 +4,7 @@ import { createShaderFromCode } from './program-lib/utils.js';
 import { VertexBuffer } from './vertex-buffer.js';
 import { DebugGraphics } from './debug-graphics.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./shader.js').Shader} Shader */
 
 /**
diff --git a/src/graphics/vertex-buffer.js b/src/graphics/vertex-buffer.js
index e1fd98e60..65e3163ea 100644
--- a/src/graphics/vertex-buffer.js
+++ b/src/graphics/vertex-buffer.js
@@ -1,7 +1,7 @@
 import { Debug } from '../core/debug.js';
 import { BUFFER_STATIC } from './constants.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./vertex-format.js').VertexFormat} VertexFormat */
 
 let id = 0;
diff --git a/src/graphics/vertex-format.js b/src/graphics/vertex-format.js
index 50b0a71e7..a6ac928de 100644
--- a/src/graphics/vertex-format.js
+++ b/src/graphics/vertex-format.js
@@ -8,7 +8,7 @@ import {
     SEMANTIC_COLOR, SEMANTIC_TANGENT, TYPE_FLOAT32, typedArrayTypesByteSize
 } from './constants.js';
 
-/** @typedef {import('./graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('./webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 /**
  * A vertex format is a descriptor that defines the layout of vertex data inside a
diff --git a/src/graphics/graphics-device.js b/src/graphics/webgl/graphics-device.js
similarity index 99%
rename from src/graphics/graphics-device.js
rename to src/graphics/webgl/graphics-device.js
index 60399de00..a4e89cb98 100644
--- a/src/graphics/graphics-device.js
+++ b/src/graphics/webgl/graphics-device.js
@@ -1,10 +1,10 @@
-import { setupVertexArrayObject } from '../polyfill/OESVertexArrayObject.js';
-import { EventHandler } from '../core/event-handler.js';
-import { now } from '../core/time.js';
-import { Debug } from '../core/debug.js';
-import { platform } from '../core/platform.js';
-import { WebglVertexBuffer } from './webgl/webgl-vertex-buffer.js';
-import { WebglIndexBuffer } from './webgl/webgl-index-buffer.js';
+import { setupVertexArrayObject } from '../../polyfill/OESVertexArrayObject.js';
+import { EventHandler } from '../../core/event-handler.js';
+import { now } from '../../core/time.js';
+import { Debug } from '../../core/debug.js';
+import { platform } from '../../core/platform.js';
+import { WebglVertexBuffer } from './webgl-vertex-buffer.js';
+import { WebglIndexBuffer } from './webgl-index-buffer.js';
 
 import {
     ADDRESS_CLAMP_TO_EDGE,
@@ -32,23 +32,23 @@ import {
     UNIFORMTYPE_TEXTURE2D, UNIFORMTYPE_TEXTURECUBE, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_TEXTURE2D_SHADOW,
     UNIFORMTYPE_TEXTURECUBE_SHADOW, UNIFORMTYPE_TEXTURE3D, UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY,
     semanticToLocation
-} from './constants.js';
-
-import { createShaderFromCode } from './program-lib/utils.js';
-import { drawQuadWithShader } from './simple-post-effect.js';
-import { programlib } from './program-lib/program-lib.js';
-import { shaderChunks } from './program-lib/chunks/chunks.js';
-import { RenderTarget } from './render-target.js';
-import { ProgramLibrary } from './program-library.js';
-import { ScopeSpace } from './scope-space.js';
-import { ShaderInput } from './shader-input.js';
-import { Texture } from './texture.js';
-import { VertexFormat } from './vertex-format.js';
-import { GrabPass } from './grab-pass.js';
-
-/** @typedef {import('./index-buffer.js').IndexBuffer} IndexBuffer */
-/** @typedef {import('./shader.js').Shader} Shader */
-/** @typedef {import('./vertex-buffer.js').VertexBuffer} VertexBuffer */
+} from '../constants.js';
+
+import { createShaderFromCode } from '../program-lib/utils.js';
+import { drawQuadWithShader } from '../simple-post-effect.js';
+import { programlib } from '../program-lib/program-lib.js';
+import { shaderChunks } from '../program-lib/chunks/chunks.js';
+import { RenderTarget } from '../render-target.js';
+import { ProgramLibrary } from '../program-library.js';
+import { ScopeSpace } from '../scope-space.js';
+import { ShaderInput } from '../shader-input.js';
+import { Texture } from '../texture.js';
+import { VertexFormat } from '../vertex-format.js';
+import { GrabPass } from '../grab-pass.js';
+
+/** @typedef {import('../index-buffer.js').IndexBuffer} IndexBuffer */
+/** @typedef {import('../shader.js').Shader} Shader */
+/** @typedef {import('../vertex-buffer.js').VertexBuffer} VertexBuffer */
 
 const EVENT_RESIZE = 'resizecanvas';
 
diff --git a/src/index.js b/src/index.js
index 749eae3e3..7b7b81a22 100644
--- a/src/index.js
+++ b/src/index.js
@@ -58,7 +58,7 @@ export { shFromCubemap } from './graphics/prefilter-cubemap.js';
 export { reprojectTexture } from './graphics/reproject-texture.js';
 export { programlib } from './graphics/program-lib/program-lib.js';
 export { shaderChunks } from './graphics/program-lib/chunks/chunks.js';
-export { GraphicsDevice } from './graphics/graphics-device.js';
+export { GraphicsDevice } from './graphics/webgl/graphics-device.js';
 export { EnvLighting } from './graphics/env-lighting.js';
 export { IndexBuffer } from './graphics/index-buffer.js';
 export { PostEffect, drawFullscreenQuad } from './graphics/post-effect.js';
diff --git a/src/resources/basis.js b/src/resources/basis.js
index 7ed310fbb..95deb1d2a 100644
--- a/src/resources/basis.js
+++ b/src/resources/basis.js
@@ -3,7 +3,7 @@ import { PIXELFORMAT_R5_G6_B5, PIXELFORMAT_R4_G4_B4_A4 } from '../graphics/const
 import { BasisWorker } from './basis-worker.js';
 import { http } from '../net/http.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 // get the list of the device's supported compression formats
 const getCompressionFormats = (device) => {
diff --git a/src/resources/container.js b/src/resources/container.js
index 08d8053e1..dd27a0466 100644
--- a/src/resources/container.js
+++ b/src/resources/container.js
@@ -3,7 +3,7 @@ import { GlbParser } from './parser/glb-parser.js';
 
 /** @typedef {import('../asset/asset-registry.js').AssetRegistry} AssetRegistry */
 /** @typedef {import('../framework/entity.js').Entity} Entity */
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./handler.js').ResourceHandler} ResourceHandler */
 
 /**
diff --git a/src/resources/cubemap.js b/src/resources/cubemap.js
index 7aea0599e..31d616126 100644
--- a/src/resources/cubemap.js
+++ b/src/resources/cubemap.js
@@ -7,7 +7,7 @@ import { Asset } from '../asset/asset.js';
 import { Texture } from '../graphics/texture.js';
 
 /** @typedef {import('../asset/asset-registry.js').AssetRegistry} AssetRegistry */
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./handler.js').ResourceHandler} ResourceHandler */
 /** @typedef {import('./loader.js').ResourceLoader} ResourceLoader */
 
diff --git a/src/resources/model.js b/src/resources/model.js
index ec43ca31b..977fa6cf7 100644
--- a/src/resources/model.js
+++ b/src/resources/model.js
@@ -8,7 +8,7 @@ import { JsonModelParser } from './parser/json-model.js';
 
 import { DefaultMaterial } from '../scene/materials/default-material.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./handler.js').ResourceHandler} ResourceHandler */
 
 /**
diff --git a/src/resources/sprite.js b/src/resources/sprite.js
index be02afbc4..d6a0df5f5 100644
--- a/src/resources/sprite.js
+++ b/src/resources/sprite.js
@@ -5,7 +5,7 @@ import { http } from '../net/http.js';
 import { Sprite } from '../scene/sprite.js';
 
 /** @typedef {import('../asset/asset-registry.js').AssetRegistry} AssetRegistry */
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./handler.js').ResourceHandler} ResourceHandler */
 
 // The scope of this function is the sprite asset
diff --git a/src/resources/texture.js b/src/resources/texture.js
index 1ebd08687..041240a45 100644
--- a/src/resources/texture.js
+++ b/src/resources/texture.js
@@ -17,7 +17,7 @@ import { HdrParser } from './parser/texture/hdr.js';
 
 /** @typedef {import('../asset/asset.js').Asset} Asset */
 /** @typedef {import('../asset/asset-registry.js').AssetRegistry} AssetRegistry */
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./handler.js').ResourceHandler} ResourceHandler */
 /** @typedef {import('./handler.js').resourceHandlerCallback} resourceHandlerCallback */
 /** @typedef {import('./loader.js').ResourceLoader} ResourceLoader */
diff --git a/src/scene/batching/batch-manager.js b/src/scene/batching/batch-manager.js
index bfef611f4..8865daf90 100644
--- a/src/scene/batching/batch-manager.js
+++ b/src/scene/batching/batch-manager.js
@@ -24,7 +24,7 @@ import { BatchGroup } from './batch-group.js';
 import { SkinBatchInstance } from './skin-batch-instance.js';
 
 /** @typedef {import('../../framework/entity.js').Entity} Entity */
-/** @typedef {import('../../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('../scene.js').Scene} Scene */
 
 function paramsIdentical(a, b) {
diff --git a/src/scene/composition/layer-composition.js b/src/scene/composition/layer-composition.js
index f979d289d..7fe198988 100644
--- a/src/scene/composition/layer-composition.js
+++ b/src/scene/composition/layer-composition.js
@@ -12,7 +12,7 @@ import { RenderAction } from './render-action.js';
 import { WorldClusters } from '../lighting/world-clusters.js';
 import { LightCompositionData } from './light-composition-data.js';
 
-/** @typedef {import('../../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('../layer.js').Layer} Layer */
 
 const tempSet = new Set();
diff --git a/src/scene/lightmapper/lightmapper.js b/src/scene/lightmapper/lightmapper.js
index b1f3023ca..eb8c16035 100644
--- a/src/scene/lightmapper/lightmapper.js
+++ b/src/scene/lightmapper/lightmapper.js
@@ -29,7 +29,7 @@ import { WorldClusters } from '../lighting/world-clusters.js';
 /** @typedef {import('../../asset/asset-registry.js').AssetRegistry} AssetRegistry */
 /** @typedef {import('../../framework/entity.js').Entity} Entity */
 /** @typedef {import('../renderer/forward-renderer.js').ForwardRenderer} ForwardRenderer */
-/** @typedef {import('../../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('../scene.js').Scene} Scene */
 
 import {
diff --git a/src/scene/mesh.js b/src/scene/mesh.js
index 54ea936f8..669ad316e 100644
--- a/src/scene/mesh.js
+++ b/src/scene/mesh.js
@@ -21,7 +21,7 @@ import { RENDERSTYLE_SOLID, RENDERSTYLE_WIREFRAME, RENDERSTYLE_POINTS } from './
 
 import { getApplication } from '../framework/globals.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./morph.js').Morph} Morph */
 /** @typedef {import('./skin.js').Skin} Skin */
 
diff --git a/src/scene/morph.js b/src/scene/morph.js
index 5b01fef72..328683f6d 100644
--- a/src/scene/morph.js
+++ b/src/scene/morph.js
@@ -9,7 +9,7 @@ import { getApplication } from '../framework/globals.js';
 
 import { BUFFER_STATIC, TYPE_FLOAT32, SEMANTIC_ATTR15, ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGB32F } from '../graphics/constants.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./morph-target.js').MorphTarget} MorphTarget */
 
 // value added to floats which are used as ints on the shader side to avoid values being rounded to one less occasionally
diff --git a/src/scene/picker.js b/src/scene/picker.js
index 2df21662e..c411d0a06 100644
--- a/src/scene/picker.js
+++ b/src/scene/picker.js
@@ -1,7 +1,7 @@
 import { Color } from '../math/color.js';
 
 import { ADDRESS_CLAMP_TO_EDGE, CLEARFLAG_DEPTH, FILTER_NEAREST, PIXELFORMAT_R8_G8_B8_A8 } from '../graphics/constants.js';
-import { GraphicsDevice } from '../graphics/graphics-device.js';
+import { GraphicsDevice } from '../graphics/webgl/graphics-device.js';
 import { RenderTarget } from '../graphics/render-target.js';
 import { Texture } from '../graphics/texture.js';
 
diff --git a/src/scene/procedural.js b/src/scene/procedural.js
index 13345558f..ba135f22b 100644
--- a/src/scene/procedural.js
+++ b/src/scene/procedural.js
@@ -9,7 +9,7 @@ import {
 
 import { Mesh } from './mesh.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 const primitiveUv1Padding = 4.0 / 64;
 const primitiveUv1PaddingScale = 1.0 - primitiveUv1Padding * 2;
diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js
index 3d6e4c98b..6a224724e 100644
--- a/src/scene/renderer/forward-renderer.js
+++ b/src/scene/renderer/forward-renderer.js
@@ -39,7 +39,7 @@ import { CookieRenderer } from './cookie-renderer.js';
 import { LightCamera } from './light-camera.js';
 import { WorldClustersDebug } from '../lighting/world-clusters-debug.js';
 
-/** @typedef {import('../../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 
 const viewInvMat = new Mat4();
 const viewMat = new Mat4();
diff --git a/src/scene/scene.js b/src/scene/scene.js
index 4afab39f6..69a4b7b0b 100644
--- a/src/scene/scene.js
+++ b/src/scene/scene.js
@@ -22,7 +22,7 @@ import { EnvLighting } from '../graphics/env-lighting.js';
 import { getApplication } from '../framework/globals.js';
 
 /** @typedef {import('../framework/entity.js').Entity} Entity */
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('../graphics/texture.js').Texture} Texture */
 /** @typedef {import('./composition/layer-composition.js').LayerComposition} LayerComposition */
 /** @typedef {import('./layer.js').Layer} Layer */
diff --git a/src/scene/skin.js b/src/scene/skin.js
index ec68a84e6..8272e055c 100644
--- a/src/scene/skin.js
+++ b/src/scene/skin.js
@@ -1,4 +1,4 @@
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('../math/mat4.js').Mat4} Mat4 */
 
 /**
diff --git a/src/scene/sprite.js b/src/scene/sprite.js
index cbbc5ca3b..df96e7470 100644
--- a/src/scene/sprite.js
+++ b/src/scene/sprite.js
@@ -5,7 +5,7 @@ import { Vec2 } from '../math/vec2.js';
 import { SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from './constants.js';
 import { createMesh } from './procedural.js';
 
-/** @typedef {import('../graphics/graphics-device.js').GraphicsDevice} GraphicsDevice */
+/** @typedef {import('../graphics/webgl/graphics-device.js').GraphicsDevice} GraphicsDevice */
 /** @typedef {import('./mesh.js').Mesh} Mesh */
 /** @typedef {import('./texture-atlas.js').TextureAtlas} TextureAtlas */
 
diff --git a/src/sound/instance.js b/src/sound/instance.js
index 0979ae51a..6d3b0fdc2 100644
--- a/src/sound/instance.js
+++ b/src/sound/instance.js
@@ -11,8 +11,14 @@ const STATE_PLAYING = 0;
 const STATE_PAUSED = 1;
 const STATE_STOPPED = 2;
 
-// Return time % duration but always return a number
-// instead of NaN when duration is 0
+/**
+ * Return time % duration but always return a number instead of NaN when duration is 0.
+ *
+ * @param {number} time - The time.
+ * @param {number} duration - The duration.
+ * @returns {number} The time % duration.
+ * @ignore
+ */
 function capTime(time, duration) {
     return (time % duration) || 0;
 }
@@ -20,18 +26,18 @@ function capTime(time, duration) {
 /**
  * A SoundInstance plays a {@link Sound}.
  *
- * @property {number} volume The volume modifier to play the sound with. In range 0-1.
- * @property {number} pitch The pitch modifier to play the sound with. Must be larger than 0.01.
- * @property {number} currentTime Gets or sets the current time of the sound that is playing. If
- * the value provided is bigger than the duration of the instance it will wrap from the beginning.
- * @property {AudioBufferSourceNode} source Gets the source that plays the sound resource. If the
- * Web Audio API is not supported the type of source is
- * [Audio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio). Source is only
- * available after calling play.
- * @property {Sound} sound The sound resource that the instance will play.
  * @augments EventHandler
  */
 class SoundInstance extends EventHandler {
+    /**
+     * Gets the source that plays the sound resource. If the Web Audio API is not supported the
+     * type of source is [Audio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio).
+     * Source is only available after calling play.
+     *
+     * @type {AudioBufferSourceNode}
+     */
+    source = null;
+
     /**
      * Create a new SoundInstance instance.
      *
@@ -44,7 +50,7 @@ class SoundInstance extends EventHandler {
      * end or not.
      * @param {number} [options.startTime=0] - The time from which the playback will start in
      * seconds. Default is 0 to start at the beginning.
-     * @param {number} [options.duration=null] - The total time after the startTime in seconds when
+     * @param {number} [options.duration=0] - The total time after the startTime in seconds when
      * playback will stop or restart if loop is true.
      * @param {Function} [options.onPlay=null] - Function called when the instance starts playing.
      * @param {Function} [options.onPause=null] - Function called when the instance is paused.
@@ -55,76 +61,232 @@ class SoundInstance extends EventHandler {
     constructor(manager, sound, options) {
         super();
 
+        /**
+         * @type {SoundManager}
+         * @private
+         */
         this._manager = manager;
 
+        /**
+         * @type {number}
+         * @private
+         */
         this._volume = options.volume !== undefined ? math.clamp(Number(options.volume) || 0, 0, 1) : 1;
+
+        /**
+         * @type {number}
+         * @private
+         */
         this._pitch = options.pitch !== undefined ? Math.max(0.01, Number(options.pitch) || 0) : 1;
+
+        /**
+         * @type {boolean}
+         * @private
+         */
         this._loop = !!(options.loop !== undefined ? options.loop : false);
+
+        /**
+         * @type {Sound}
+         * @private
+         */
         this._sound = sound;
 
-        // start at 'stopped'
+        /**
+         * Start at 'stopped'.
+         *
+         * @type {number}
+         * @private
+         */
         this._state = STATE_STOPPED;
-        // true if the manager was suspended
+
+        /**
+         * True if the manager was suspended.
+         *
+         * @type {boolean}
+         * @private
+         */
         this._suspended = false;
-        // true if we want to suspend the event handled to the 'onended' event
+
+        /**
+         * True if we want to suspend the event handled to the 'onended' event.
+         *
+         * @type {boolean}
+         * @private
+         */
         this._suspendEndEvent = false;
-        // true if we want to suspend firing instance events
+
+        /**
+         * True if we want to suspend firing instance events.
+         *
+         * @type {boolean}
+         * @private
+         */
         this._suspendInstanceEvents = false;
-        // if true then the instance will start playing its source
-        // when its created
+
+        /**
+         * If true then the instance will start playing its source when its created.
+         *
+         * @type {boolean}
+         * @private
+         */
         this._playWhenLoaded = true;
 
+        /**
+         * @type {number}
+         * @private
+         */
         this._startTime = Math.max(0, Number(options.startTime) || 0);
+
+        /**
+         * @type {number}
+         * @private
+         */
         this._duration = Math.max(0, Number(options.duration) || 0);
-        this._startOffset = null;
 
-        // source is initialized when play() is called
-        this.source = null;
+        /**
+         * @type {number|null}
+         * @private
+         */
+        this._startOffset = null;
 
         // external event handlers
+        /** @private */
         this._onPlayCallback = options.onPlay;
+        /** @private */
         this._onPauseCallback = options.onPause;
+        /** @private */
         this._onResumeCallback = options.onResume;
+        /** @private */
         this._onStopCallback = options.onStop;
+        /** @private */
         this._onEndCallback = options.onEnd;
 
         if (hasAudioContext()) {
+            /**
+             * @type {number}
+             * @private
+             */
             this._startedAt = 0;
 
-            // manually keep track of the playback position
-            // because the Web Audio API does not provide a way to do this
-            // accurately if the playbackRate is not 1
+            /**
+             * Manually keep track of the playback position because the Web Audio API does not
+             * provide a way to do this accurately if the playbackRate is not 1.
+             *
+             * @type {number}
+             * @private
+             */
             this._currentTime = 0;
+
+            /**
+             * @type {number}
+             * @private
+             */
             this._currentOffset = 0;
 
-            // The input node is the one that is connected to the source.
+            /**
+             * The input node is the one that is connected to the source.
+             *
+             * @type {AudioNode|null}
+             * @private
+             */
             this._inputNode = null;
-            // the connected node is the one that is connected to the destination (speakers). Any
-            // external nodes will be connected to this node.
+
+            /**
+             * The connected node is the one that is connected to the destination (speakers). Any
+             * external nodes will be connected to this node.
+             *
+             * @type {AudioNode|null}
+             * @private
+             */
             this._connectorNode = null;
 
-            // The first external node set by a user
+            /**
+             * The first external node set by a user.
+             *
+             * @type {AudioNode|null}
+             * @private
+             */
             this._firstNode = null;
-            // The last external node set by a user
+
+            /**
+             * The last external node set by a user.
+             *
+             * @type {AudioNode|null}
+             * @private
+             */
             this._lastNode = null;
 
             this._initializeNodes();
 
-            // bind internal event handlers to 'this'
+            /** @private */
             this._endedHandler = this._onEnded.bind(this);
-
         } else {
-
+            /** @private */
             this._isReady = false;
 
+            /** @private */
             this._loadedMetadataHandler = this._onLoadedMetadata.bind(this);
+            /** @private */
             this._timeUpdateHandler = this._onTimeUpdate.bind(this);
+            /** @private */
             this._endedHandler = this._onEnded.bind(this);
 
             this._createSource();
         }
     }
 
+    /**
+     * Gets or sets the current time of the sound that is playing. If the value provided is bigger
+     * than the duration of the instance it will wrap from the beginning.
+     *
+     * @type {number}
+     */
+    set currentTime(value) {
+        if (value < 0) return;
+
+        if (this._state === STATE_PLAYING) {
+            const suspend = this._suspendInstanceEvents;
+            this._suspendInstanceEvents = true;
+
+            // stop first which will set _startOffset to null
+            this.stop();
+
+            // set _startOffset and play
+            this._startOffset = value;
+            this.play();
+            this._suspendInstanceEvents = suspend;
+        } else {
+            // set _startOffset which will be used when the instance will start playing
+            this._startOffset = value;
+            // set _currentTime
+            this._currentTime = value;
+        }
+    }
+
+    get currentTime() {
+        // if the user has set the currentTime and we have not used it yet
+        // then just return that
+        if (this._startOffset !== null) {
+            return this._startOffset;
+        }
+
+        // if the sound is paused return the currentTime calculated when
+        // pause() was called
+        if (this._state === STATE_PAUSED) {
+            return this._currentTime;
+        }
+
+        // if the sound is stopped or we don't have a source
+        // return 0
+        if (this._state === STATE_STOPPED || !this.source) {
+            return 0;
+        }
+
+        // recalculate current time
+        this._updateCurrentTime();
+        return this._currentTime;
+    }
+
     /**
      * The duration of the sound that the instance will play starting from startTime.
      *
@@ -203,6 +365,47 @@ class SoundInstance extends EventHandler {
         return this._loop;
     }
 
+    /**
+     * The pitch modifier to play the sound with. Must be larger than 0.01.
+     *
+     * @type {number}
+     */
+    set pitch(pitch) {
+        // set offset to current time so that
+        // we calculate the rest of the time with the new pitch
+        // from now on
+        this._currentOffset = this.currentTime;
+        this._startedAt = this._manager.context.currentTime;
+
+        this._pitch = Math.max(Number(pitch) || 0, 0.01);
+        if (this.source) {
+            this.source.playbackRate.value = this._pitch;
+        }
+    }
+
+    get pitch() {
+        return this._pitch;
+    }
+
+    /**
+     * The sound resource that the instance will play.
+     *
+     * @type {Sound}
+     */
+    set sound(value) {
+        this._sound = value;
+
+        if (this._state !== STATE_STOPPED) {
+            this.stop();
+        } else {
+            this._createSource();
+        }
+    }
+
+    get sound() {
+        return this._sound;
+    }
+
     /**
      * The start time from which the sound will start playing.
      *
@@ -223,6 +426,24 @@ class SoundInstance extends EventHandler {
         return this._startTime;
     }
 
+    /**
+     * The volume modifier to play the sound with. In range 0-1.
+     *
+     * @type {number}
+     */
+    set volume(volume) {
+        volume = math.clamp(volume, 0, 1);
+        this._volume = volume;
+        if (this.gain) {
+            this.gain.gain.value = volume * this._manager.volume;
+        }
+    }
+
+    get volume() {
+        return this._volume;
+    }
+
+    /** @private */
     _onPlay() {
         this.fire('play');
 
@@ -230,6 +451,7 @@ class SoundInstance extends EventHandler {
             this._onPlayCallback(this);
     }
 
+    /** @private */
     _onPause() {
         this.fire('pause');
 
@@ -237,6 +459,7 @@ class SoundInstance extends EventHandler {
             this._onPauseCallback(this);
     }
 
+    /** @private */
     _onResume() {
         this.fire('resume');
 
@@ -244,6 +467,7 @@ class SoundInstance extends EventHandler {
             this._onResumeCallback(this);
     }
 
+    /** @private */
     _onStop() {
         this.fire('stop');
 
@@ -251,6 +475,7 @@ class SoundInstance extends EventHandler {
             this._onStopCallback(this);
     }
 
+    /** @private */
     _onEnded() {
         // the callback is not fired synchronously
         // so only reset _suspendEndEvent to false when the
@@ -300,465 +525,356 @@ class SoundInstance extends EventHandler {
             this.resume();
         }
     }
-}
 
-if (hasAudioContext()) {
-    Object.assign(SoundInstance.prototype, {
-        /**
-         * @function
-         * @private
-         * @name SoundInstance#_initializeNodes
-         * @description Creates internal audio nodes and connects them.
-         */
-        _initializeNodes: function () {
-            // create gain node for volume control
-            this.gain = this._manager.context.createGain();
-            this._inputNode = this.gain;
-            // the gain node is also the connector node for 2D sound instances
-            this._connectorNode = this.gain;
-            this._connectorNode.connect(this._manager.context.destination);
-        },
-
-        /**
-         * @function
-         * @name SoundInstance#play
-         * @description Begins playback of sound. If the sound is not loaded this will return false.
-         * If the sound is already playing this will restart the sound.
-         * @returns {boolean} True if the sound was started.
-         */
-        play: function () {
-            if (this._state !== STATE_STOPPED) {
-                this.stop();
-            }
-
-            if (!this.source) {
-                this._createSource();
-            }
-
-            // calculate start offset
-            let offset = capTime(this._startOffset, this.duration);
-            offset = capTime(this._startTime + offset, this._sound.duration);
-            // reset start offset now that we started the sound
-            this._startOffset = null;
-
-            // start source with specified offset and duration
-            if (this._duration) {
-                this.source.start(0, offset, this._duration);
-            } else {
-                this.source.start(0, offset);
-            }
-
-            // reset times
-            this._startedAt = this._manager.context.currentTime;
-            this._currentTime = 0;
-            this._currentOffset = offset;
-
-            // set state to playing
-            this._state = STATE_PLAYING;
-            // no need for this anymore
-            this._playWhenLoaded = false;
-
-            // Initialize volume and loop - note moved to be after start() because of Chrome bug
-            this.volume = this._volume;
-            this.loop = this._loop;
-            this.pitch = this._pitch;
+    /**
+     * Creates internal audio nodes and connects them.
+     *
+     * @private
+     */
+    _initializeNodes() {
+        // create gain node for volume control
+        this.gain = this._manager.context.createGain();
+        this._inputNode = this.gain;
+        // the gain node is also the connector node for 2D sound instances
+        this._connectorNode = this.gain;
+        this._connectorNode.connect(this._manager.context.destination);
+    }
 
-            // handle suspend events / volumechange events
-            this._manager.on('volumechange', this._onManagerVolumeChange, this);
-            this._manager.on('suspend', this._onManagerSuspend, this);
-            this._manager.on('resume', this._onManagerResume, this);
-            this._manager.on('destroy', this._onManagerDestroy, this);
+    /**
+     * Begins playback of sound. If the sound is not loaded this will return false. If the sound is
+     * already playing this will restart the sound.
+     *
+     * @returns {boolean} True if the sound was started.
+     */
+    play() {
+        if (this._state !== STATE_STOPPED) {
+            this.stop();
+        }
 
-            // suspend immediately if manager is suspended
-            if (this._manager.suspended) {
-                this._onManagerSuspend();
-            }
+        if (!this.source) {
+            this._createSource();
+        }
 
-            if (!this._suspendInstanceEvents)
-                this._onPlay();
+        // calculate start offset
+        let offset = capTime(this._startOffset, this.duration);
+        offset = capTime(this._startTime + offset, this._sound.duration);
+        // reset start offset now that we started the sound
+        this._startOffset = null;
 
-            return true;
-        },
+        // start source with specified offset and duration
+        if (this._duration) {
+            this.source.start(0, offset, this._duration);
+        } else {
+            this.source.start(0, offset);
+        }
 
-        /**
-         * @function
-         * @name SoundInstance#pause
-         * @description Pauses playback of sound. Call resume() to resume playback from the same position.
-         * @returns {boolean} Returns true if the sound was paused.
-         */
-        pause: function () {
-            // no need for this anymore
-            this._playWhenLoaded = false;
+        // reset times
+        this._startedAt = this._manager.context.currentTime;
+        this._currentTime = 0;
+        this._currentOffset = offset;
 
-            if (this._state !== STATE_PLAYING || !this.source)
-                return false;
+        // set state to playing
+        this._state = STATE_PLAYING;
+        // no need for this anymore
+        this._playWhenLoaded = false;
 
-            // store current time
-            this._updateCurrentTime();
+        // Initialize volume and loop - note moved to be after start() because of Chrome bug
+        this.volume = this._volume;
+        this.loop = this._loop;
+        this.pitch = this._pitch;
+
+        // handle suspend events / volumechange events
+        this._manager.on('volumechange', this._onManagerVolumeChange, this);
+        this._manager.on('suspend', this._onManagerSuspend, this);
+        this._manager.on('resume', this._onManagerResume, this);
+        this._manager.on('destroy', this._onManagerDestroy, this);
+
+        // suspend immediately if manager is suspended
+        if (this._manager.suspended) {
+            this._onManagerSuspend();
+        }
 
-            // set state to paused
-            this._state = STATE_PAUSED;
+        if (!this._suspendInstanceEvents)
+            this._onPlay();
 
-            // Stop the source and re-create it because we cannot reuse the same source.
-            // Suspend the end event as we are manually stopping the source
-            this._suspendEndEvent = true;
-            this.source.stop(0);
-            this.source = null;
+        return true;
+    }
 
-            // reset user-set start offset
-            this._startOffset = null;
+    /**
+     * Pauses playback of sound. Call resume() to resume playback from the same position.
+     *
+     * @returns {boolean} Returns true if the sound was paused.
+     */
+    pause() {
+        // no need for this anymore
+        this._playWhenLoaded = false;
 
-            if (!this._suspendInstanceEvents)
-                this._onPause();
+        if (this._state !== STATE_PLAYING || !this.source)
+            return false;
 
-            return true;
-        },
+        // store current time
+        this._updateCurrentTime();
 
-        /**
-         * @function
-         * @name SoundInstance#resume
-         * @description Resumes playback of the sound. Playback resumes at the point that the audio was paused.
-         * @returns {boolean} Returns true if the sound was resumed.
-         */
-        resume: function () {
-            if (this._state !== STATE_PAUSED) {
-                return false;
-            }
+        // set state to paused
+        this._state = STATE_PAUSED;
 
-            if (!this.source) {
-                this._createSource();
-            }
+        // Stop the source and re-create it because we cannot reuse the same source.
+        // Suspend the end event as we are manually stopping the source
+        this._suspendEndEvent = true;
+        this.source.stop(0);
+        this.source = null;
 
-            // start at point where sound was paused
-            let offset = this.currentTime;
+        // reset user-set start offset
+        this._startOffset = null;
 
-            // if the user set the 'currentTime' property while the sound
-            // was paused then use that as the offset instead
-            if (this._startOffset !== null) {
-                offset = capTime(this._startOffset, this.duration);
-                offset = capTime(this._startTime + offset, this._sound.duration);
+        if (!this._suspendInstanceEvents)
+            this._onPause();
 
-                // reset offset
-                this._startOffset = null;
-            }
+        return true;
+    }
 
-            // start source
-            if (this._duration) {
-                this.source.start(0, offset, this._duration);
-            } else {
-                this.source.start(0, offset);
-            }
+    /**
+     * Resumes playback of the sound. Playback resumes at the point that the audio was paused.
+     *
+     * @returns {boolean} Returns true if the sound was resumed.
+     */
+    resume() {
+        if (this._state !== STATE_PAUSED) {
+            return false;
+        }
 
-            // set state back to playing
-            this._state = STATE_PLAYING;
+        if (!this.source) {
+            this._createSource();
+        }
 
-            this._startedAt = this._manager.context.currentTime;
-            this._currentOffset = offset;
+        // start at point where sound was paused
+        let offset = this.currentTime;
 
-            // Initialize parameters
-            this.volume = this._volume;
-            this.loop = this._loop;
-            this.pitch = this._pitch;
-            this._playWhenLoaded = false;
+        // if the user set the 'currentTime' property while the sound
+        // was paused then use that as the offset instead
+        if (this._startOffset !== null) {
+            offset = capTime(this._startOffset, this.duration);
+            offset = capTime(this._startTime + offset, this._sound.duration);
 
-            if (!this._suspendInstanceEvents)
-                this._onResume();
+            // reset offset
+            this._startOffset = null;
+        }
 
-            return true;
-        },
+        // start source
+        if (this._duration) {
+            this.source.start(0, offset, this._duration);
+        } else {
+            this.source.start(0, offset);
+        }
 
-        /**
-         * @function
-         * @name SoundInstance#stop
-         * @description Stops playback of sound. Calling play() again will restart playback from the beginning of the sound.
-         * @returns {boolean} Returns true if the sound was stopped.
-         */
-        stop: function () {
-            this._playWhenLoaded = false;
+        // set state back to playing
+        this._state = STATE_PLAYING;
 
-            if (this._state === STATE_STOPPED || !this.source)
-                return false;
+        this._startedAt = this._manager.context.currentTime;
+        this._currentOffset = offset;
 
-            // unsubscribe from manager events
-            this._manager.off('volumechange', this._onManagerVolumeChange, this);
-            this._manager.off('suspend', this._onManagerSuspend, this);
-            this._manager.off('resume', this._onManagerResume, this);
-            this._manager.off('destroy', this._onManagerDestroy, this);
+        // Initialize parameters
+        this.volume = this._volume;
+        this.loop = this._loop;
+        this.pitch = this._pitch;
+        this._playWhenLoaded = false;
 
-            // reset stored times
-            this._startedAt = 0;
-            this._currentTime = 0;
-            this._currentOffset = 0;
+        if (!this._suspendInstanceEvents)
+            this._onResume();
 
-            this._startOffset = null;
+        return true;
+    }
 
-            this._suspendEndEvent = true;
-            if (this._state === STATE_PLAYING) {
-                this.source.stop(0);
-            }
-            this.source = null;
+    /**
+     * Stops playback of sound. Calling play() again will restart playback from the beginning of
+     * the sound.
+     *
+     * @returns {boolean} Returns true if the sound was stopped.
+     */
+    stop() {
+        this._playWhenLoaded = false;
 
-            // set the state to stopped
-            this._state = STATE_STOPPED;
+        if (this._state === STATE_STOPPED || !this.source)
+            return false;
 
-            if (!this._suspendInstanceEvents)
-                this._onStop();
+        // unsubscribe from manager events
+        this._manager.off('volumechange', this._onManagerVolumeChange, this);
+        this._manager.off('suspend', this._onManagerSuspend, this);
+        this._manager.off('resume', this._onManagerResume, this);
+        this._manager.off('destroy', this._onManagerDestroy, this);
 
-            return true;
-        },
+        // reset stored times
+        this._startedAt = 0;
+        this._currentTime = 0;
+        this._currentOffset = 0;
 
-        /**
-         * @function
-         * @name SoundInstance#setExternalNodes
-         * @description Connects external Web Audio API nodes. You need to pass
-         * the first node of the node graph that you created externally and the last node of that graph. The first
-         * node will be connected to the audio source and the last node will be connected to the destination of the
-         * AudioContext (e.g. speakers). Requires Web Audio API support.
-         * @param {AudioNode} firstNode - The first node that will be connected to the audio source of sound instances.
-         * @param {AudioNode} [lastNode] - The last node that will be connected to the destination of the AudioContext.
-         * If unspecified then the firstNode will be connected to the destination instead.
-         * @example
-         * var context = app.systems.sound.context;
-         * var analyzer = context.createAnalyzer();
-         * var distortion = context.createWaveShaper();
-         * var filter = context.createBiquadFilter();
-         * analyzer.connect(distortion);
-         * distortion.connect(filter);
-         * instance.setExternalNodes(analyzer, filter);
-         */
-        setExternalNodes: function (firstNode, lastNode) {
-            if (!firstNode) {
-                console.error('The firstNode must be a valid Audio Node');
-                return;
-            }
+        this._startOffset = null;
 
-            if (!lastNode) {
-                lastNode = firstNode;
-            }
+        this._suspendEndEvent = true;
+        if (this._state === STATE_PLAYING) {
+            this.source.stop(0);
+        }
+        this.source = null;
 
-            // connections are:
-            // source -> inputNode -> connectorNode -> [firstNode -> ... -> lastNode] -> speakers
+        // set the state to stopped
+        this._state = STATE_STOPPED;
 
-            const speakers = this._manager.context.destination;
+        if (!this._suspendInstanceEvents)
+            this._onStop();
 
-            if (this._firstNode !== firstNode) {
-                if (this._firstNode) {
-                    // if firstNode already exists means the connector node
-                    // is connected to it so disconnect it
-                    this._connectorNode.disconnect(this._firstNode);
-                } else {
-                    // if firstNode does not exist means that its connected
-                    // to the speakers so disconnect it
-                    this._connectorNode.disconnect(speakers);
-                }
+        return true;
+    }
 
-                // set first node and connect with connector node
-                this._firstNode = firstNode;
-                this._connectorNode.connect(firstNode);
-            }
+    /**
+     * Connects external Web Audio API nodes. You need to pass the first node of the node graph
+     * that you created externally and the last node of that graph. The first node will be
+     * connected to the audio source and the last node will be connected to the destination of the
+     * AudioContext (e.g. speakers). Requires Web Audio API support.
+     *
+     * @param {AudioNode} firstNode - The first node that will be connected to the audio source of sound instances.
+     * @param {AudioNode} [lastNode] - The last node that will be connected to the destination of the AudioContext.
+     * If unspecified then the firstNode will be connected to the destination instead.
+     * @example
+     * var context = app.systems.sound.context;
+     * var analyzer = context.createAnalyzer();
+     * var distortion = context.createWaveShaper();
+     * var filter = context.createBiquadFilter();
+     * analyzer.connect(distortion);
+     * distortion.connect(filter);
+     * instance.setExternalNodes(analyzer, filter);
+     */
+    setExternalNodes(firstNode, lastNode) {
+        if (!firstNode) {
+            console.error('The firstNode must be a valid Audio Node');
+            return;
+        }
 
-            if (this._lastNode !== lastNode) {
-                if (this._lastNode) {
-                    // if last node exists means it's connected to the speakers so disconnect it
-                    this._lastNode.disconnect(speakers);
-                }
+        if (!lastNode) {
+            lastNode = firstNode;
+        }
 
-                // set last node and connect with speakers
-                this._lastNode = lastNode;
-                this._lastNode.connect(speakers);
-            }
-        },
+        // connections are:
+        // source -> inputNode -> connectorNode -> [firstNode -> ... -> lastNode] -> speakers
 
-        /**
-         * @function
-         * @name SoundInstance#clearExternalNodes
-         * @description Clears any external nodes set by {@link SoundInstance#setExternalNodes}.
-         */
-        clearExternalNodes: function () {
-            const speakers = this._manager.context.destination;
+        const speakers = this._manager.context.destination;
 
-            // break existing connections
+        if (this._firstNode !== firstNode) {
             if (this._firstNode) {
+                // if firstNode already exists means the connector node
+                // is connected to it so disconnect it
                 this._connectorNode.disconnect(this._firstNode);
-                this._firstNode = null;
+            } else {
+                // if firstNode does not exist means that its connected
+                // to the speakers so disconnect it
+                this._connectorNode.disconnect(speakers);
             }
 
+            // set first node and connect with connector node
+            this._firstNode = firstNode;
+            this._connectorNode.connect(firstNode);
+        }
+
+        if (this._lastNode !== lastNode) {
             if (this._lastNode) {
+                // if last node exists means it's connected to the speakers so disconnect it
                 this._lastNode.disconnect(speakers);
-                this._lastNode = null;
             }
 
-            // reset connect to speakers
-            this._connectorNode.connect(speakers);
-        },
-
-
-        /**
-         * @function
-         * @name SoundInstance#getExternalNodes
-         * @description Gets any external nodes set by {@link SoundInstance#setExternalNodes}.
-         * @returns {AudioNode[]} Returns an array that contains the two nodes set by {@link SoundInstance#setExternalNodes}.
-         */
-        getExternalNodes: function () {
-            return [this._firstNode, this._lastNode];
-        },
-
-        /**
-         * @private
-         * @function
-         * @description Creates the source for the instance.
-         * @returns {AudioBufferSourceNode|null} Returns the created source or null if the sound
-         * instance has no {@link Sound} associated with it.
-         */
-        _createSource: function () {
-            if (!this._sound) {
-                return null;
-            }
-
-            const context = this._manager.context;
-
-            if (this._sound.buffer) {
-                this.source = context.createBufferSource();
-                this.source.buffer = this._sound.buffer;
-
-                // Connect up the nodes
-                this.source.connect(this._inputNode);
-
-                // set events
-                this.source.onended = this._endedHandler;
-
-                // set loopStart and loopEnd so that the source starts and ends at the correct user-set times
-                this.source.loopStart = capTime(this._startTime, this.source.buffer.duration);
-                if (this._duration) {
-                    this.source.loopEnd = Math.max(this.source.loopStart, capTime(this._startTime + this._duration, this.source.buffer.duration));
-                }
-            }
-
-            return this.source;
-        },
-
-        /**
-         * @private
-         * @function
-         * @name SoundInstance#_updateCurrentTime
-         * @description Sets the current time taking into account the time the instance started playing, the current pitch and the current time offset.
-         */
-        _updateCurrentTime: function () {
-            this._currentTime = capTime((this._manager.context.currentTime - this._startedAt) * this._pitch + this._currentOffset, this.duration);
-        },
-
-        /**
-         * @private
-         * @function
-         * @name SoundInstance#_onManagerDestroy
-         * @description Handle the manager's 'destroy' event.
-         */
-        _onManagerDestroy: function () {
-            if (this.source && this._state === STATE_PLAYING) {
-                this.source.stop(0);
-                this.source = null;
-            }
+            // set last node and connect with speakers
+            this._lastNode = lastNode;
+            this._lastNode.connect(speakers);
         }
-    });
+    }
 
-    Object.defineProperty(SoundInstance.prototype, 'volume', {
-        get: function () {
-            return this._volume;
-        },
+    /**
+     * Clears any external nodes set by {@link SoundInstance#setExternalNodes}.
+     */
+    clearExternalNodes() {
+        const speakers = this._manager.context.destination;
 
-        set: function (volume) {
-            volume = math.clamp(volume, 0, 1);
-            this._volume = volume;
-            if (this.gain) {
-                this.gain.gain.value = volume * this._manager.volume;
-            }
+        // break existing connections
+        if (this._firstNode) {
+            this._connectorNode.disconnect(this._firstNode);
+            this._firstNode = null;
         }
-    });
 
-    Object.defineProperty(SoundInstance.prototype, 'pitch', {
-        get: function () {
-            return this._pitch;
-        },
+        if (this._lastNode) {
+            this._lastNode.disconnect(speakers);
+            this._lastNode = null;
+        }
 
-        set: function (pitch) {
-            // set offset to current time so that
-            // we calculate the rest of the time with the new pitch
-            // from now on
-            this._currentOffset = this.currentTime;
-            this._startedAt = this._manager.context.currentTime;
+        // reset connect to speakers
+        this._connectorNode.connect(speakers);
+    }
 
-            this._pitch = Math.max(Number(pitch) || 0, 0.01);
-            if (this.source) {
-                this.source.playbackRate.value = this._pitch;
-            }
+    /**
+     * Gets any external nodes set by {@link SoundInstance#setExternalNodes}.
+     *
+     * @returns {AudioNode[]} Returns an array that contains the two nodes set by
+     * {@link SoundInstance#setExternalNodes}.
+     */
+    getExternalNodes() {
+        return [this._firstNode, this._lastNode];
+    }
 
+    /**
+     * Creates the source for the instance.
+     *
+     * @returns {AudioBufferSourceNode|null} Returns the created source or null if the sound
+     * instance has no {@link Sound} associated with it.
+     * @private
+     */
+    _createSource() {
+        if (!this._sound) {
+            return null;
         }
-    });
 
-    Object.defineProperty(SoundInstance.prototype, 'sound', {
-        get: function () {
-            return this._sound;
-        },
+        const context = this._manager.context;
 
-        set: function (value) {
-            this._sound = value;
-
-            if (this._state !== STATE_STOPPED) {
-                this.stop();
-            } else {
-                this._createSource();
-            }
-        }
-    });
+        if (this._sound.buffer) {
+            this.source = context.createBufferSource();
+            this.source.buffer = this._sound.buffer;
 
-    Object.defineProperty(SoundInstance.prototype, 'currentTime', {
-        get: function () {
-            // if the user has set the currentTime and we have not used it yet
-            // then just return that
-            if (this._startOffset !== null) {
-                return this._startOffset;
-            }
+            // Connect up the nodes
+            this.source.connect(this._inputNode);
 
-            // if the sound is paused return the currentTime calculated when
-            // pause() was called
-            if (this._state === STATE_PAUSED) {
-                return this._currentTime;
-            }
+            // set events
+            this.source.onended = this._endedHandler;
 
-            // if the sound is stopped or we don't have a source
-            // return 0
-            if (this._state === STATE_STOPPED || !this.source) {
-                return 0;
+            // set loopStart and loopEnd so that the source starts and ends at the correct user-set times
+            this.source.loopStart = capTime(this._startTime, this.source.buffer.duration);
+            if (this._duration) {
+                this.source.loopEnd = Math.max(this.source.loopStart, capTime(this._startTime + this._duration, this.source.buffer.duration));
             }
+        }
 
-            // recalculate current time
-            this._updateCurrentTime();
-            return this._currentTime;
-        },
-        set: function (value) {
-            if (value < 0) return;
-
-            if (this._state === STATE_PLAYING) {
-                const suspend = this._suspendInstanceEvents;
-                this._suspendInstanceEvents = true;
+        return this.source;
+    }
 
-                // stop first which will set _startOffset to null
-                this.stop();
+    /**
+     * Sets the current time taking into account the time the instance started playing, the current
+     * pitch and the current time offset.
+     *
+     * @private
+     */
+    _updateCurrentTime() {
+        this._currentTime = capTime((this._manager.context.currentTime - this._startedAt) * this._pitch + this._currentOffset, this.duration);
+    }
 
-                // set _startOffset and play
-                this._startOffset = value;
-                this.play();
-                this._suspendInstanceEvents = suspend;
-            } else {
-                // set _startOffset which will be used when the instance will start playing
-                this._startOffset = value;
-                // set _currentTime
-                this._currentTime = value;
-            }
+    /**
+     * Handle the manager's 'destroy' event.
+     *
+     * @private
+     */
+    _onManagerDestroy() {
+        if (this.source && this._state === STATE_PLAYING) {
+            this.source.stop(0);
+            this.source = null;
         }
-    });
+    }
+}
 
-} else {
+if (!hasAudioContext()) {
     Object.assign(SoundInstance.prototype, {
         play: function () {
             if (this._state !== STATE_STOPPED) {
@@ -913,12 +1029,6 @@ if (hasAudioContext()) {
             }
         },
 
-        /**
-         * @private
-         * @function
-         * @name SoundInstance#_onManagerDestroy
-         * @description Handle the manager's 'destroy' event.
-         */
         _onManagerDestroy: function () {
             if (this.source) {
                 this.source.pause();
@@ -977,6 +1087,7 @@ if (hasAudioContext()) {
 
             return this.source.currentTime - this._startTime;
         },
+
         set: function (value) {
             if (value < 0) return;
 
diff --git a/src/sound/instance3d.js b/src/sound/instance3d.js
index 9f3e5c65b..ac94a4e23 100644
--- a/src/sound/instance3d.js
+++ b/src/sound/instance3d.js
@@ -15,25 +15,21 @@ const MAX_DISTANCE = 10000;
 /**
  * A SoundInstance3d plays a {@link Sound} in 3D.
  *
- * @property {Vec3} position The position of the sound in 3D space.
- * @property {Vec3} velocity The velocity of the sound.
- * @property {string} distanceModel Determines which algorithm to use to reduce the volume of the
- * audio as it moves away from the listener. Can be:
- *
- * - {@link DISTANCE_LINEAR}
- * - {@link DISTANCE_INVERSE}
- * - {@link DISTANCE_EXPONENTIAL}
- *
- * Default is {@link DISTANCE_LINEAR}.
- * @property {number} refDistance The reference distance for reducing volume as the sound source
- * moves further from the listener.
- * @property {number} maxDistance The maximum distance from the listener at which audio falloff
- * stops. Note the volume of the audio is not 0 after this distance, but just doesn't fall off
- * anymore.
- * @property {number} rollOffFactor The factor used in the falloff equation.
  * @augments SoundInstance
  */
 class SoundInstance3d extends SoundInstance {
+    /**
+     * @type {Vec3}
+     * @private
+     */
+    _position = new Vec3();
+
+    /**
+     * @type {Vec3}
+     * @private
+     */
+    _velocity = new Vec3();
+
     /**
      * Create a new SoundInstance3d instance.
      *
@@ -65,16 +61,12 @@ class SoundInstance3d extends SoundInstance {
      * doesn't fall off anymore.
      * @param {number} [options.rollOffFactor=1] - The factor used in the falloff equation.
      */
-    constructor(manager, sound, options) {
+    constructor(manager, sound, options = {}) {
         super(manager, sound, options);
 
-        options = options || {};
-
-        this._position = new Vec3();
         if (options.position)
             this.position = options.position;
 
-        this._velocity = new Vec3();
         if (options.velocity)
             this.velocity = options.velocity;
 
@@ -83,77 +75,114 @@ class SoundInstance3d extends SoundInstance {
         this.rollOffFactor = options.rollOffFactor !== undefined ? Number(options.rollOffFactor) : 1;
         this.distanceModel = options.distanceModel !== undefined ? options.distanceModel : DISTANCE_LINEAR;
     }
-}
 
-if (hasAudioContext()) {
-    Object.assign(SoundInstance3d.prototype, {
-        _initializeNodes: function () {
-            this.gain = this._manager.context.createGain();
-            this.panner = this._manager.context.createPanner();
-            this.panner.connect(this.gain);
-            this._inputNode = this.panner;
-            this._connectorNode = this.gain;
-            this._connectorNode.connect(this._manager.context.destination);
-        }
-    });
+    /**
+     * Allocate Web Audio resources for this instance.
+     *
+     * @private
+     */
+    _initializeNodes() {
+        this.gain = this._manager.context.createGain();
+        this.panner = this._manager.context.createPanner();
+        this.panner.connect(this.gain);
+        this._inputNode = this.panner;
+        this._connectorNode = this.gain;
+        this._connectorNode.connect(this._manager.context.destination);
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'position', {
-        get: function () {
-            return this._position;
-        },
-        set: function (position) {
-            this._position.copy(position);
-            this.panner.setPosition(position.x, position.y, position.z);
-        }
-    });
+    /**
+     * The position of the sound in 3D space.
+     *
+     * @type {Vec3}
+     */
+    set position(value) {
+        this._position.copy(value);
+        this.panner.positionX.value = value.x;
+        this.panner.positionY.value = value.y;
+        this.panner.positionZ.value = value.z;
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'velocity', {
-        get: function () {
-            return this._velocity;
-        },
-        set: function (velocity) {
-            this._velocity.copy(velocity);
-            this.panner.setVelocity(velocity.x, velocity.y, velocity.z);
-        }
-    });
+    get position() {
+        return this._position;
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'maxDistance', {
-        get: function () {
-            return this.panner.maxDistance;
-        },
-        set: function (value) {
-            this.panner.maxDistance = value;
-        }
-    });
+    /**
+     * The velocity of the sound.
+     *
+     * @type {Vec3}
+     */
+    set velocity(velocity) {
+        this._velocity.copy(velocity);
+        this.panner.setVelocity(velocity.x, velocity.y, velocity.z);
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'refDistance', {
-        get: function () {
-            return this.panner.refDistance;
-        },
-        set: function (value) {
-            this.panner.refDistance = value;
-        }
-    });
+    get velocity() {
+        return this._velocity;
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'rollOffFactor', {
-        get: function () {
-            return this.panner.rolloffFactor;
-        },
-        set: function (value) {
-            this.panner.rolloffFactor = value;
-        }
-    });
+    /**
+     * The maximum distance from the listener at which audio falloff stops. Note the volume of the
+     * audio is not 0 after this distance, but just doesn't fall off anymore.
+     *
+     * @type {number}
+     */
+    set maxDistance(value) {
+        this.panner.maxDistance = value;
+    }
 
-    Object.defineProperty(SoundInstance3d.prototype, 'distanceModel', {
-        get: function () {
-            return this.panner.distanceModel;
-        },
-        set: function (value) {
-            this.panner.distanceModel = value;
-        }
-    });
+    get maxDistance() {
+        return this.panner.maxDistance;
+    }
+
+    /**
+     * The reference distance for reducing volume as the sound source moves further from the
+     * listener.
+     *
+     * @type {number}
+     */
+    set refDistance(value) {
+        this.panner.refDistance = value;
+    }
+
+    get refDistance() {
+        return this.panner.refDistance;
+    }
+
+    /**
+     * The factor used in the falloff equation.
+     *
+     * @type {number}
+     */
+    set rollOffFactor(value) {
+        this.panner.rolloffFactor = value;
+    }
+
+    get rollOffFactor() {
+        return this.panner.rolloffFactor;
+    }
+
+    /**
+     * Determines which algorithm to use to reduce the volume of the audio as it moves away from
+     * the listener. Can be:
+     *
+     * - {@link DISTANCE_LINEAR}
+     * - {@link DISTANCE_INVERSE}
+     * - {@link DISTANCE_EXPONENTIAL}
+     *
+     * Default is {@link DISTANCE_LINEAR}.
+     *
+     * @type {string}
+     */
+    set distanceModel(value) {
+        this.panner.distanceModel = value;
+    }
+
+    get distanceModel() {
+        return this.panner.distanceModel;
+    }
+}
 
-} else {
+if (!hasAudioContext()) {
     // temp vector storage
     let offset = new Vec3();
 
diff --git a/src/sound/listener.js b/src/sound/listener.js
index 19ef4bcdd..d7b8f0efe 100644
--- a/src/sound/listener.js
+++ b/src/sound/listener.js
@@ -15,50 +15,111 @@ class Listener {
      * @param {SoundManager} manager - The sound manager.
      */
     constructor(manager) {
+        /**
+         * @type {SoundManager}
+         * @private
+         */
         this._manager = manager;
 
+        /**
+         * @type {Vec3}
+         * @private
+         */
         this.position = new Vec3();
+        /**
+         * @type {Vec3}
+         * @private
+         */
         this.velocity = new Vec3();
+        /**
+         * @type {Mat4}
+         * @private
+         */
         this.orientation = new Mat4();
     }
 
+    /**
+     * Get the position of the listener.
+     *
+     * @returns {Vec3} The position of the listener.
+     */
     getPosition() {
         return this.position;
     }
 
+    /**
+     * Set the position of the listener.
+     *
+     * @param {Vec3} position - The new position of the listener.
+     */
     setPosition(position) {
         this.position.copy(position);
         const listener = this.listener;
         if (listener) {
-            listener.setPosition(position.x, position.y, position.z);
+            listener.positionX.value = position.x;
+            listener.positionY.value = position.y;
+            listener.positionZ.value = position.z;
         }
     }
 
+    /**
+     * Get the velocity of the listener.
+     *
+     * @returns {Vec3} The velocity of the listener.
+     */
     getVelocity() {
         return this.velocity;
     }
 
+    /**
+     * Set the velocity of the listener.
+     *
+     * @param {Vec3} velocity - The new velocity of the listener.
+     */
     setVelocity(velocity) {
         this.velocity.copy(velocity);
         const listener = this.listener;
         if (listener) {
-            listener.setPosition(velocity.x, velocity.y, velocity.z);
+            listener.positionX.value = velocity.x;
+            listener.positionY.value = velocity.y;
+            listener.positionZ.value = velocity.z;
         }
     }
 
+    /**
+     * Set the orientation matrix of the listener.
+     *
+     * @param {Mat4} orientation - The new orientation matrix of the listener.
+     */
     setOrientation(orientation) {
         this.orientation.copy(orientation);
         const listener = this.listener;
         if (listener) {
-            listener.setOrientation(-orientation.data[8], -orientation.data[9], -orientation.data[10],
-                                    orientation.data[4], orientation.data[5], orientation.data[6]);
+            const m = orientation.data;
+            listener.forwardX.value = -m[8];
+            listener.forwardY.value = -m[9];
+            listener.forwardZ.value = -m[10];
+
+            listener.upX.value = m[4];
+            listener.upY.value = m[5];
+            listener.upZ.value = m[6];
         }
     }
 
+    /**
+     * Get the orientation matrix of the listener.
+     *
+     * @returns {Mat4} The orientation matrix of the listener.
+     */
     getOrientation() {
         return this.orientation;
     }
 
+    /**
+     * Get the listener.
+     *
+     * @type {AudioListener|null}
+     */
     get listener() {
         const context = this._manager.context;
         return context ? context.listener : null;
diff --git a/src/sound/manager.js b/src/sound/manager.js
index 02042778c..604d04ede 100644
--- a/src/sound/manager.js
+++ b/src/sound/manager.js
@@ -26,7 +26,15 @@ class SoundManager extends EventHandler {
     constructor(options) {
         super();
 
+        /**
+         * @type {AudioContext}
+         * @private
+         */
         this._context = null;
+        /**
+         * @type {boolean}
+         * @private
+         */
         this._forceWebAudioApi = options.forceWebAudioApi;
 
         this._resumeContext = null;
@@ -93,6 +101,12 @@ class SoundManager extends EventHandler {
         return this._volume;
     }
 
+    /**
+     * Get the Web Audio API context.
+     *
+     * @type {AudioContext}
+     * @ignore
+     */
     get context() {
         // lazy create the AudioContext if possible
         if (!this._context) {
diff --git a/src/sound/sound.js b/src/sound/sound.js
index 247d136a6..542fff809 100644
--- a/src/sound/sound.js
+++ b/src/sound/sound.js
@@ -1,13 +1,21 @@
 /**
  * Represents the resource of an audio asset.
- *
- * @property {AudioBuffer} buffer If the Web Audio API is supported this contains the audio data.
- * @property {HTMLAudioElement} audio If the Web Audio API is not supported this contains the
- * audio data.
- * @property {number} duration Returns the duration of the sound. If the sound is not loaded it
- * returns 0.
  */
 class Sound {
+    /**
+     * If the Web Audio API is not supported this contains the audio data.
+     *
+     * @type {HTMLAudioElement}
+     */
+    audio;
+
+     /**
+      * If the Web Audio API is supported this contains the audio data.
+      *
+      * @type {AudioBuffer}
+      */
+    buffer;
+
     /**
      * Create a new Sound instance.
      *
@@ -22,6 +30,11 @@ class Sound {
         }
     }
 
+    /**
+     * Gets the duration of the sound. If the sound is not loaded it returns 0.
+     *
+     * @type {number}
+     */
     get duration() {
         let duration = 0;
         if (this.buffer) {


Test patch:
diff --git a/test/framework/application.test.mjs b/test/framework/application.test.mjs
index 871426537..b0b65f64f 100644
--- a/test/framework/application.test.mjs
+++ b/test/framework/application.test.mjs
@@ -4,7 +4,7 @@ import { AssetRegistry } from '../../src/asset/asset-registry.js';
 import { BatchManager } from '../../src/scene/batching/batch-manager.js';
 import { ComponentSystemRegistry } from '../../src/framework/components/registry.js';
 import { Entity } from '../../src/framework/entity.js';
-import { GraphicsDevice } from '../../src/graphics/graphics-device.js';
+import { GraphicsDevice } from '../../src/graphics/webgl/graphics-device.js';
 import { I18n } from '../../src/i18n/i18n.js';
 import { Lightmapper } from '../../src/scene/lightmapper/lightmapper.js';
 import { ResourceLoader } from '../../src/resources/loader.js';
diff --git a/test/graphics/graphics-device.test.mjs b/test/graphics/graphics-device.test.mjs
index 4f82c75dd..b0328625b 100644
--- a/test/graphics/graphics-device.test.mjs
+++ b/test/graphics/graphics-device.test.mjs
@@ -1,4 +1,4 @@
-import { GraphicsDevice } from '../../src/graphics/graphics-device.js';
+import { GraphicsDevice } from '../../src/graphics/webgl/graphics-device.js';
 
 import { HTMLCanvasElement } from '@playcanvas/canvas-mock';