diff --git a/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/AudioFocusHelper.java b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/AudioFocusHelper.java
new file mode 100644
index 000000000..3ed2a6973
--- /dev/null
+++ b/Apps/Playground/Android/BabylonNative/src/main/java/com/babylonjs/embedding/AudioFocusHelper.java
@@ -0,0 +1,67 @@
+package com.babylonjs.embedding;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+
+/**
+ * Requests Android audio focus before WebAudio playback starts.
+ */
+public final class AudioFocusHelper {
+ private static AudioManager audioManager;
+ private static AudioFocusRequest audioFocusRequest;
+ private static final AudioManager.OnAudioFocusChangeListener focusListener = focusChange -> {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ nativeNotifyAudioInterruption(true);
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ nativeNotifyAudioInterruption(false);
+ break;
+ default:
+ break;
+ }
+ };
+
+ static {
+ System.loadLibrary("BabylonNativeEmbedding");
+ }
+
+ private AudioFocusHelper() {}
+
+ public static void initialize(Context context) {
+ audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ public static void requestPlaybackFocus() {
+ if (audioManager == null) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (audioFocusRequest == null) {
+ AudioAttributes attributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_GAME)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build();
+ audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .setAudioAttributes(attributes)
+ .setOnAudioFocusChangeListener(focusListener)
+ .build();
+ }
+
+ audioManager.requestAudioFocus(audioFocusRequest);
+ } else {
+ audioManager.requestAudioFocus(
+ focusListener,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ }
+ }
+
+ private static native void nativeNotifyAudioInterruption(boolean began);
+}
diff --git a/Apps/Playground/Android/app/build.gradle b/Apps/Playground/Android/app/build.gradle
index ac75899a0..b2ca6715c 100644
--- a/Apps/Playground/Android/app/build.gradle
+++ b/Apps/Playground/Android/app/build.gradle
@@ -82,6 +82,11 @@ tasks.named('preBuild').configure { dependsOn ':BabylonNative:assembleRelease' }
tasks.register('copyFiles') {
doLast {
+ copy {
+ from '../../../node_modules/babylonjs-addons'
+ include "babylonjs.addons.js"
+ into 'src/main/assets/Scripts'
+ }
copy {
from '../../../node_modules/babylonjs'
include "babylon.max.js"
diff --git a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java
index ca725d6ca..0a68ee1e3 100644
--- a/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java
+++ b/Apps/Playground/Android/app/src/main/java/com/android/babylonnative/playground/PlaygroundActivity.java
@@ -38,7 +38,8 @@ protected void onCreate(Bundle icicle) {
// the first View attach completes engine init on the JS thread, in
// submission order.
loadBootstrapScripts(mRuntimeHandle);
- BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/experience.js");
+ BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/audio.js");
+ BabylonNative.runtimeLoadScript(mRuntimeHandle, "app:///Scripts/playground_runner.js");
mView = new BabylonView(getApplication(), mRuntimeHandle);
setContentView(mView);
diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt
index e24dd7be0..ee610c9cf 100644
--- a/Apps/Playground/CMakeLists.txt
+++ b/Apps/Playground/CMakeLists.txt
@@ -15,6 +15,7 @@ set(DEPENDENCIES
set(SCRIPTS
"Scripts/experience.js"
+ "Scripts/audio.js"
"Scripts/playground_runner.js"
"Scripts/validation_native.js"
"Scripts/config.json")
diff --git a/Apps/Playground/Scripts/audio.js b/Apps/Playground/Scripts/audio.js
new file mode 100644
index 000000000..8a9d68617
--- /dev/null
+++ b/Apps/Playground/Scripts/audio.js
@@ -0,0 +1,143 @@
+///
+
+(function () {
+ if (typeof document === "undefined") {
+ globalThis.document = {
+ addEventListener: function () { },
+ removeEventListener: function () { },
+ createElement: function (tag) {
+ if (tag === "audio") {
+ return new Audio();
+ }
+ return {};
+ },
+ createTextNode: function (text) {
+ return { nodeValue: text };
+ }
+ };
+ }
+
+ function createBeepWavArrayBuffer(sampleRate, durationSeconds, frequency, amplitude) {
+ const frameCount = Math.max(1, Math.floor(sampleRate * durationSeconds));
+ const bytesPerSample = 2;
+ const channelCount = 1;
+ const dataSize = frameCount * channelCount * bytesPerSample;
+ const buffer = new ArrayBuffer(44 + dataSize);
+ const view = new DataView(buffer);
+
+ const writeString = function (offset, value) {
+ for (let i = 0; i < value.length; i++) {
+ view.setUint8(offset + i, value.charCodeAt(i));
+ }
+ };
+
+ writeString(0, "RIFF");
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(8, "WAVE");
+ writeString(12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, channelCount, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
+ view.setUint16(32, channelCount * bytesPerSample, true);
+ view.setUint16(34, 8 * bytesPerSample, true);
+ writeString(36, "data");
+ view.setUint32(40, dataSize, true);
+
+ for (let frame = 0; frame < frameCount; frame++) {
+ const sample = Math.sin((2 * Math.PI * frequency * frame) / sampleRate) * amplitude;
+ const intSample = Math.max(-32768, Math.min(32767, Math.round(sample * 32767)));
+ view.setInt16(44 + frame * bytesPerSample, intSample, true);
+ }
+
+ return buffer;
+ }
+
+ let clickAudioContext = null;
+ let clickAudioBuffer = null;
+
+ async function playClickBeep() {
+ if (typeof AudioContext === "undefined") {
+ BABYLON.Tools.Warn("AudioContext is not available");
+ return;
+ }
+
+ if (!clickAudioContext) {
+ clickAudioContext = new AudioContext();
+ }
+
+ if (clickAudioContext.state === "suspended") {
+ await clickAudioContext.resume();
+ }
+
+ if (!clickAudioBuffer) {
+ const wavBytes = createBeepWavArrayBuffer(44100, 0.25, 880, 0.4);
+ clickAudioBuffer = await clickAudioContext.decodeAudioData(wavBytes.slice(0));
+ }
+
+ const source = clickAudioContext.createBufferSource();
+ source.buffer = clickAudioBuffer;
+ source.connect(clickAudioContext.destination);
+ source.start(clickAudioContext.currentTime);
+ BABYLON.Tools.Log("Click beep");
+ }
+
+ function createScene() {
+ const engine = new BABYLON.NativeEngine({ adaptToDeviceRatio: true });
+ const scene = new BABYLON.Scene(engine);
+ scene.createDefaultCamera(true, true, true);
+
+ const beepBuffer = createBeepWavArrayBuffer(44100, 0.25, 880, 0.4);
+ let spatialSource = null;
+ let spatialStarted = false;
+
+ scene.onPointerObservable.add(function (pointerInfo) {
+ if (pointerInfo.type !== BABYLON.PointerEventTypes.POINTERDOWN) {
+ return;
+ }
+
+ playClickBeep().catch(function (error) {
+ BABYLON.Tools.Warn("Click beep failed: " + error);
+ });
+
+ if (!spatialStarted && typeof BABYLON.Sound === "function") {
+ spatialStarted = true;
+ const legacySound = new BABYLON.Sound("beep", beepBuffer, scene, function () {
+ BABYLON.Tools.Log("Spatial beep started after click");
+ }, {
+ autoplay: true,
+ loop: true,
+ spatialSound: true,
+ maxDistance: 20
+ });
+ spatialSource = legacySound;
+ } else if (spatialSource && typeof spatialSource.play === "function") {
+ spatialSource.play();
+ }
+ });
+
+ let angle = 0;
+ scene.onBeforeRenderObservable.add(function () {
+ if (!spatialSource) {
+ return;
+ }
+
+ angle += 0.02;
+ const x = Math.cos(angle) * 3;
+ const z = Math.sin(angle) * 3;
+
+ if (typeof spatialSource.setPosition === "function") {
+ spatialSource.setPosition(new BABYLON.Vector3(x, 0, z));
+ } else if (spatialSource._soundSource) {
+ spatialSource._soundSource.position = new BABYLON.Vector3(x, 0, z);
+ }
+ });
+
+ BABYLON.Tools.Log("Click anywhere to play a beep (and start spatial audio)");
+
+ return scene;
+ }
+
+ globalThis.createScene = createScene;
+})();
diff --git a/Apps/Playground/iOS/AppDelegate.swift b/Apps/Playground/iOS/AppDelegate.swift
index 3c1e7165e..867e2e48b 100644
--- a/Apps/Playground/iOS/AppDelegate.swift
+++ b/Apps/Playground/iOS/AppDelegate.swift
@@ -24,7 +24,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// first BNView attach completes engine initialization on the JS
// thread, in submission order.
PlaygroundBootstrap.loadScripts(runtime)
- runtime.loadScript("app:///Scripts/experience.js")
+ runtime.loadScript("app:///Scripts/audio.js")
+ runtime.loadScript("app:///Scripts/playground_runner.js")
self.runtime = runtime
return true
diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt
index 4ffe04381..17c7e3e22 100644
--- a/Apps/UnitTests/CMakeLists.txt
+++ b/Apps/UnitTests/CMakeLists.txt
@@ -78,6 +78,7 @@ target_link_libraries(UnitTests
PRIVATE NativeEncoding
PRIVATE ScriptLoader
PRIVATE ShaderCache
+ PRIVATE WebAudio
PRIVATE Window
PRIVATE XMLHttpRequest
PRIVATE gtest_main
diff --git a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js
index 53a4d4c17..8623f5919 100644
--- a/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js
+++ b/Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js
@@ -24017,6 +24017,46 @@ exports.isNumeric = function (input) {
return !isNaN(parseFloat(input));
};
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js"
+/*!*************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js ***!
+ \*************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _arrayLikeToArray)
+/* harmony export */ });
+function _arrayLikeToArray(r, a) {
+ (null == a || a > r.length) && (a = r.length);
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
+ return n;
+}
+
+
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/arrayWithoutHoles.js"
+/*!**************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/arrayWithoutHoles.js ***!
+ \**************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _arrayWithoutHoles)
+/* harmony export */ });
+/* harmony import */ var _arrayLikeToArray_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./arrayLikeToArray.js */ "../../node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js");
+
+function _arrayWithoutHoles(r) {
+ if (Array.isArray(r)) return (0,_arrayLikeToArray_js__WEBPACK_IMPORTED_MODULE_0__["default"])(r);
+}
+
+
/***/ },
/***/ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js"
@@ -24057,6 +24097,92 @@ function _asyncToGenerator(n) {
}
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/iterableToArray.js"
+/*!************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/iterableToArray.js ***!
+ \************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _iterableToArray)
+/* harmony export */ });
+function _iterableToArray(r) {
+ if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
+}
+
+
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/nonIterableSpread.js"
+/*!**************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/nonIterableSpread.js ***!
+ \**************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _nonIterableSpread)
+/* harmony export */ });
+function _nonIterableSpread() {
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
+}
+
+
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/toConsumableArray.js"
+/*!**************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/toConsumableArray.js ***!
+ \**************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _toConsumableArray)
+/* harmony export */ });
+/* harmony import */ var _arrayWithoutHoles_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./arrayWithoutHoles.js */ "../../node_modules/@babel/runtime/helpers/esm/arrayWithoutHoles.js");
+/* harmony import */ var _iterableToArray_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./iterableToArray.js */ "../../node_modules/@babel/runtime/helpers/esm/iterableToArray.js");
+/* harmony import */ var _unsupportedIterableToArray_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./unsupportedIterableToArray.js */ "../../node_modules/@babel/runtime/helpers/esm/unsupportedIterableToArray.js");
+/* harmony import */ var _nonIterableSpread_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./nonIterableSpread.js */ "../../node_modules/@babel/runtime/helpers/esm/nonIterableSpread.js");
+
+
+
+
+function _toConsumableArray(r) {
+ return (0,_arrayWithoutHoles_js__WEBPACK_IMPORTED_MODULE_0__["default"])(r) || (0,_iterableToArray_js__WEBPACK_IMPORTED_MODULE_1__["default"])(r) || (0,_unsupportedIterableToArray_js__WEBPACK_IMPORTED_MODULE_2__["default"])(r) || (0,_nonIterableSpread_js__WEBPACK_IMPORTED_MODULE_3__["default"])();
+}
+
+
+/***/ },
+
+/***/ "../../node_modules/@babel/runtime/helpers/esm/unsupportedIterableToArray.js"
+/*!***********************************************************************************!*\
+ !*** ../../node_modules/@babel/runtime/helpers/esm/unsupportedIterableToArray.js ***!
+ \***********************************************************************************/
+(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (/* binding */ _unsupportedIterableToArray)
+/* harmony export */ });
+/* harmony import */ var _arrayLikeToArray_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./arrayLikeToArray.js */ "../../node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js");
+
+function _unsupportedIterableToArray(r, a) {
+ if (r) {
+ if ("string" == typeof r) return (0,_arrayLikeToArray_js__WEBPACK_IMPORTED_MODULE_0__["default"])(r, a);
+ var t = {}.toString.call(r).slice(8, -1);
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? (0,_arrayLikeToArray_js__WEBPACK_IMPORTED_MODULE_0__["default"])(r, a) : void 0;
+ }
+}
+
+
/***/ },
/***/ "../../node_modules/chai/index.js"
@@ -28239,14 +28365,15 @@ var __webpack_exports__ = {};
!*** ./src/tests.javaScript.all.ts ***!
\*************************************/
__webpack_require__.r(__webpack_exports__);
-/* harmony import */ var _babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/asyncToGenerator */ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js");
-/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime/regenerator */ "../../node_modules/@babel/runtime/regenerator/index.js");
-/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__);
-/* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! mocha */ "../../node_modules/mocha/browser-entry.js");
-/* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(mocha__WEBPACK_IMPORTED_MODULE_2__);
-/* harmony import */ var chai__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! chai */ "../../node_modules/chai/index.js");
-/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @babylonjs/materials */ "@babylonjs/core");
-/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_4__);
+/* harmony import */ var _babel_runtime_helpers_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/toConsumableArray */ "../../node_modules/@babel/runtime/helpers/esm/toConsumableArray.js");
+/* harmony import */ var _babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime/helpers/asyncToGenerator */ "../../node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js");
+/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime/regenerator */ "../../node_modules/@babel/runtime/regenerator/index.js");
+/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2__);
+/* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! mocha */ "../../node_modules/mocha/browser-entry.js");
+/* harmony import */ var mocha__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(mocha__WEBPACK_IMPORTED_MODULE_3__);
+/* harmony import */ var chai__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! chai */ "../../node_modules/chai/index.js");
+/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @babylonjs/materials */ "@babylonjs/core");
+/* harmony import */ var _babylonjs_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_babylonjs_core__WEBPACK_IMPORTED_MODULE_5__);
@@ -28255,9 +28382,9 @@ __webpack_require__.r(__webpack_exports__);
-mocha__WEBPACK_IMPORTED_MODULE_2__.setup("bdd");
+mocha__WEBPACK_IMPORTED_MODULE_3__.setup("bdd");
// @ts-ignore
-mocha__WEBPACK_IMPORTED_MODULE_2__.reporter("spec");
+mocha__WEBPACK_IMPORTED_MODULE_3__.reporter("spec");
@@ -28267,26 +28394,26 @@ describe("RequestFile", function () {
this.timeout(0);
it("should throw when requesting a URL with no protocol", function () {
function requestFile() {
- (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.RequestFile)("noprotocol.gltf", function () {});
+ (0,_babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.RequestFile)("noprotocol.gltf", function () {});
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(requestFile).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(requestFile).to.throw();
});
});
describe("ColorParsing", function () {
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("")).to.equal(0);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("transparent")).to.equal(0);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("#123")).to.equal(0xff332211);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("#1234")).to.equal(0x44332211);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("#123456")).to.equal(0xff563412);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("#12345678")).to.equal(0x78563412);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("snow")).to.equal(0xfffafaff);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("rgb(16,32,48)")).to.equal(0xff302010);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("rgba(16,32,48,64)")).to.equal(0x40302010);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(_native.Canvas.parseColor("rgb(16, 32 , 48 )")).to.equal(
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("")).to.equal(0);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("transparent")).to.equal(0);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("#123")).to.equal(0xff332211);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("#1234")).to.equal(0x44332211);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("#123456")).to.equal(0xff563412);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("#12345678")).to.equal(0x78563412);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("snow")).to.equal(0xfffafaff);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("rgb(16,32,48)")).to.equal(0xff302010);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("rgba(16,32,48,64)")).to.equal(0x40302010);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(_native.Canvas.parseColor("rgb(16, 32 , 48 )")).to.equal(
0xff302010
);
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(
_native.Canvas.parseColor("rgba( 16, 32 , 48 , 64 )")
).to.equal(0x40302010);
@@ -28294,75 +28421,75 @@ describe("ColorParsing", function () {
function incorrectColor() {
_native.Canvas.parseColor("unknownColor");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("#");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("#12345");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgb(11)");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgb(11,22,33");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgb(11,22,33,");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgba(11, 22, 33, )");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgba(11, 22, 33, 44, 55, 66 )");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgb");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
it("should throw", function () {
function incorrectColor() {
_native.Canvas.parseColor("rgba");
}
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(incorrectColor).to.throw();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(incorrectColor).to.throw();
});
});
function createSceneAndWait(callback, done) {
- var engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.NativeEngine();
- var scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.Scene(engine);
+ var engine = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.NativeEngine();
+ var scene = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Scene(engine);
scene.createDefaultCamera();
callback(engine, scene);
scene.executeWhenReady(function () {
@@ -28375,7 +28502,7 @@ describe("Materials", function () {
it("Empty ShaderMaterial should compile", function (done) {
function createEmptyShaderMat() {
createSceneAndWait(function (engine, scene) {
- var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.MeshBuilder.CreateSphere(
+ var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.MeshBuilder.CreateSphere(
"sphere",
{ diameter: 2, segments: 32 },
scene
@@ -28384,7 +28511,7 @@ describe("Materials", function () {
vertexSource: "void main() {}",
fragmentSource: "void main() {}"
};
- var mat = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.ShaderMaterial("shader", scene, shaders, {});
+ var mat = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.ShaderMaterial("shader", scene, shaders, {});
sphere.material = mat;
}, done);
}
@@ -28392,12 +28519,12 @@ describe("Materials", function () {
});
it("GradientMaterial should compile", function (done) {
createSceneAndWait(function (engine, scene) {
- var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.MeshBuilder.CreateSphere(
+ var sphere = _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.MeshBuilder.CreateSphere(
"sphere",
{ diameter: 2, segments: 32 },
scene
);
- var gradientMaterial = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.GradientMaterial("grad", scene);
+ var gradientMaterial = new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.GradientMaterial("grad", scene);
sphere.material = gradientMaterial;
}, done);
});
@@ -28408,21 +28535,21 @@ describe("PostProcesses", function () {
it("PassPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.PassPostProcess("Scene copy", 1.0, camera);
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.PassPostProcess("Scene copy", 1.0, camera);
}, done);
});
it("BlackAndWhitePostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.BlackAndWhitePostProcess("bandw", 1.0, camera);
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.BlackAndWhitePostProcess("bandw", 1.0, camera);
}, done);
});
it("BlurPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.BlurPostProcess(
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.BlurPostProcess(
"Horizontal blur",
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.Vector2(1.0, 0),
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Vector2(1.0, 0),
32,
0.25,
camera
@@ -28432,9 +28559,9 @@ describe("PostProcesses", function () {
it("ConvolutionPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.ConvolutionPostProcess(
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.ConvolutionPostProcess(
"Sepia",
- _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.ConvolutionPostProcess.EmbossKernel,
+ _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.ConvolutionPostProcess.EmbossKernel,
1.0,
camera
);
@@ -28443,28 +28570,28 @@ describe("PostProcesses", function () {
it("HighlightsPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.HighlightsPostProcess("highlights", 1.0, camera);
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.HighlightsPostProcess("highlights", 1.0, camera);
}, done);
});
it("TonemapPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.TonemapPostProcess("tonemap", _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.TonemappingOperator.Hable, 1.0, camera);
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.TonemapPostProcess("tonemap", _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.TonemappingOperator.Hable, 1.0, camera);
}, done);
});
it("ImageProcessingPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.ImageProcessingPostProcess("processing", 1.0, camera);
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.ImageProcessingPostProcess("processing", 1.0, camera);
}, done);
});
it("RefractionPostProcess", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.RefractionPostProcess(
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.RefractionPostProcess(
"Refraction",
"https://playground.babylonjs.com/textures/grass.jpg",
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.Color3(1.0, 1.0, 1.0),
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.Color3(1.0, 1.0, 1.0),
0.5,
0.5,
1.0,
@@ -28475,7 +28602,7 @@ describe("PostProcesses", function () {
it("DefaultPipeline", function (done) {
createSceneAndWait(function (engine, scene) {
var camera = scene._activeCamera;
- new _babylonjs_core__WEBPACK_IMPORTED_MODULE_4__.DefaultRenderingPipeline(
+ new _babylonjs_core__WEBPACK_IMPORTED_MODULE_5__.DefaultRenderingPipeline(
"defaultPipeline", // The name of the pipeline
true, // Do you want the pipeline to use HDR texture?
scene, // The scene instance
@@ -28529,25 +28656,25 @@ describe("PostProcesses", function () {
describe("NativeEncoding", function () {
this.timeout(0);function
- expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee3(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0:
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(blob).to.be.instanceOf(Blob);_context3.next = 1;return (
+ expectValidPNG(_x) {return _expectValidPNG.apply(this, arguments);}function _expectValidPNG() {_expectValidPNG = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee3(blob) {var arrayBuffer, pngSignature;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context3) {while (1) switch (_context3.prev = _context3.next) {case 0:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(blob).to.be.instanceOf(Blob);_context3.next = 1;return (
blob.arrayBuffer());case 1:arrayBuffer = _context3.sent;
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(arrayBuffer.byteLength).to.be.greaterThan(0);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(arrayBuffer.byteLength).to.be.greaterThan(0);
pngSignature = new Uint8Array(arrayBuffer.slice(0, 4));
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[0]).to.equal(137); // PNG signature bytes
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[1]).to.equal(80); // 'P'
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[2]).to.equal(78); // 'N'
- (0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(pngSignature[3]).to.equal(71); // 'G'
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(pngSignature[0]).to.equal(137); // PNG signature bytes
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(pngSignature[1]).to.equal(80); // 'P'
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(pngSignature[2]).to.equal(78); // 'N'
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(pngSignature[3]).to.equal(71); // 'G'
case 2:case "end":return _context3.stop();}}, _callee3);}));return _expectValidPNG.apply(this, arguments);}
- it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0:
+ it("should encode a PNG", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee() {var pixelData, result;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context) {while (1) switch (_context.prev = _context.next) {case 0:
pixelData = new Uint8Array(4).fill(255);_context.next = 1;return (
_native.EncodeImageAsync(pixelData, 1, 1, "image/png", false));case 1:result = _context.sent;_context.next = 2;return (
expectValidPNG(result));case 2:case "end":return _context.stop();}}, _callee);}))
);
- it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().mark(function _callee2() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0:
+ it("should handle multiple concurrent encoding tasks", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee2() {var pixelDatas, i, results;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context2) {while (1) switch (_context2.prev = _context2.next) {case 0:
pixelDatas = [];
for (i = 0; i < 10; i++) {
pixelDatas.push(new Uint8Array(4).fill(255));
@@ -28559,6 +28686,327 @@ describe("NativeEncoding", function () {
);
});
+describe("WebAudio", function () {
+ this.timeout(0);
+
+ function createSilentWavArrayBuffer(sampleRate, frameCount) {
+ var bytesPerSample = 2;
+ var channelCount = 1;
+ var dataSize = frameCount * channelCount * bytesPerSample;
+ var buffer = new ArrayBuffer(44 + dataSize);
+ var view = new DataView(buffer);
+ var writeString = function writeString(offset, value) {
+ for (var i = 0; i < value.length; i++) {
+ view.setUint8(offset + i, value.charCodeAt(i));
+ }
+ };
+
+ writeString(0, "RIFF");
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(8, "WAVE");
+ writeString(12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, channelCount, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
+ view.setUint16(32, channelCount * bytesPerSample, true);
+ view.setUint16(34, 8 * bytesPerSample, true);
+ writeString(36, "data");
+ view.setUint32(40, dataSize, true);
+ return buffer;
+ }
+
+ function createSineWavArrayBuffer(sampleRate, frameCount, frequency) {var amplitude = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0.5;
+ var bytesPerSample = 2;
+ var channelCount = 1;
+ var dataSize = frameCount * channelCount * bytesPerSample;
+ var buffer = new ArrayBuffer(44 + dataSize);
+ var view = new DataView(buffer);
+ var writeString = function writeString(offset, value) {
+ for (var i = 0; i < value.length; i++) {
+ view.setUint8(offset + i, value.charCodeAt(i));
+ }
+ };
+
+ writeString(0, "RIFF");
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(8, "WAVE");
+ writeString(12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, channelCount, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
+ view.setUint16(32, channelCount * bytesPerSample, true);
+ view.setUint16(34, 8 * bytesPerSample, true);
+ writeString(36, "data");
+ view.setUint32(40, dataSize, true);
+
+ for (var frame = 0; frame < frameCount; frame++) {
+ var sample = Math.sin(2 * Math.PI * frequency * frame / sampleRate) * amplitude;
+ var intSample = Math.max(-32768, Math.min(32767, Math.round(sample * 32767)));
+ view.setInt16(44 + frame * bytesPerSample, intSample, true);
+ }
+
+ return buffer;
+ }function
+
+ waitForTime(_x2, _x3) {return _waitForTime.apply(this, arguments);}function _waitForTime() {_waitForTime = (0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee0(context, targetTime) {var timeoutMs,start,_args0 = arguments;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context0) {while (1) switch (_context0.prev = _context0.next) {case 0:timeoutMs = _args0.length > 2 && _args0[2] !== undefined ? _args0[2] : 5000;
+ start = Date.now();case 1:if (!(
+ context.currentTime < targetTime)) {_context0.next = 4;break;}if (!(
+ Date.now() - start > timeoutMs)) {_context0.next = 2;break;}throw (
+ new Error("Timed out waiting for currentTime >= ".concat(targetTime)));case 2:_context0.next = 3;return (
+
+ new Promise(function (resolve) {return setTimeout(resolve, 10);}));case 3:_context0.next = 1;break;case 4:case "end":return _context0.stop();}}, _callee0);}));return _waitForTime.apply(this, arguments);}
+
+
+
+ it("should expose AudioContext and play a decoded buffer through the node graph", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee4() {var context, sampleRate, frameCount, audioBuffer, source, gain, panner, ended;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context4) {while (1) switch (_context4.prev = _context4.next) {case 0:
+ // JavaScriptCore reports some host constructors as typeof "object".
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(AudioContext).to.exist;
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(webkitAudioContext).to.exist;
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(AudioContext).to.equal(webkitAudioContext);
+
+ context = new AudioContext();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(context.state).to.equal("suspended");_context4.next = 1;return (
+ context.resume());case 1:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(context.state).to.equal("running");
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(context.sampleRate).to.be.greaterThan(0);
+
+ sampleRate = 44100;
+ frameCount = sampleRate; // 1 second
+ _context4.next = 2;return context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount));case 2:audioBuffer = _context4.sent;
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audioBuffer.sampleRate).to.equal(sampleRate);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audioBuffer.length).to.equal(frameCount);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audioBuffer.duration).to.be.closeTo(1, 0.01);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audioBuffer.numberOfChannels).to.equal(1);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audioBuffer.getChannelData(0).length).to.equal(frameCount);
+
+ source = context.createBufferSource();
+ gain = context.createGain();
+ panner = context.createPanner();
+
+ source.buffer = audioBuffer;
+ gain.gain.value = 0.5;
+ panner.setPosition(1, 0, 0);
+ context.listener.setPosition(0, 0, 0);
+
+ source.connect(gain);
+ gain.connect(panner);
+ panner.connect(context.destination);
+
+ ended = false;
+ source.addEventListener("ended", function () {
+ ended = true;
+ });
+
+ source.start();_context4.next = 3;return (
+ context.resume());case 3:_context4.next = 4;return (
+
+
+ new Promise(function (resolve) {return setTimeout(resolve, 50);}));case 4:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ended).to.equal(false);
+
+ source.stop();_context4.next = 5;return (
+ new Promise(function (resolve) {return setTimeout(resolve, 50);}));case 5:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ended).to.equal(true);_context4.next = 6;return (
+
+ context.close());case 6:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(context.state).to.equal("closed");case 7:case "end":return _context4.stop();}}, _callee4);}))
+ );
+
+ it("should automate AudioParam values over time", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee5() {var context, gain, scheduleStart, sampleRate, frameCount, audioBuffer, source, curve, curveStart;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context5) {while (1) switch (_context5.prev = _context5.next) {case 0:
+ context = new AudioContext();
+ gain = context.createGain();
+ gain.connect(context.destination);
+
+ scheduleStart = context.currentTime;
+ gain.gain.cancelScheduledValues(scheduleStart);
+ gain.gain.setValueAtTime(0, scheduleStart);
+ gain.gain.linearRampToValueAtTime(1, scheduleStart + 1);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(gain.gain.value).to.be.closeTo(0, 0.01);
+
+ sampleRate = context.sampleRate;
+ frameCount = Math.ceil(sampleRate * 1.5);_context5.next = 1;return (
+ context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount)));case 1:audioBuffer = _context5.sent;
+ source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(gain);
+ source.start(scheduleStart);_context5.next = 2;return (
+ context.resume());case 2:_context5.next = 3;return (
+
+ waitForTime(context, scheduleStart + 0.5));case 3:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(gain.gain.value).to.be.closeTo(0.5, 0.15);
+
+ curve = new Float32Array([0, 0.25, 0.75, 1]);
+ curveStart = context.currentTime;
+ gain.gain.cancelScheduledValues(curveStart);
+ gain.gain.setValueCurveAtTime(curve, curveStart, 0.2);_context5.next = 4;return (
+ waitForTime(context, curveStart + 0.1));case 4:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(gain.gain.value).to.be.greaterThan(0.1);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(gain.gain.value).to.be.lessThan(0.9);
+
+ source.stop();_context5.next = 5;return (
+ context.close());case 5:case "end":return _context5.stop();}}, _callee5);}))
+ );
+
+ it("should return analyser frequency and time-domain data for non-silent audio", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee6() {var context, sampleRate, frequency, frameCount, audioBuffer, source, analyser, startTime, freqBytes, freqFloats, timeBytes, timeFloats, maxFreqByte, maxFreqFloat, flatTimeDomain, i, flatByteTimeDomain, _i;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context6) {while (1) switch (_context6.prev = _context6.next) {case 0:
+ context = new AudioContext();
+ sampleRate = context.sampleRate;
+ frequency = 440;
+ frameCount = sampleRate;_context6.next = 1;return (
+ context.decodeAudioData(createSineWavArrayBuffer(sampleRate, frameCount, frequency)));case 1:audioBuffer = _context6.sent;
+
+ source = context.createBufferSource();
+ analyser = context.createAnalyser();
+ analyser.fftSize = 2048;
+ analyser.smoothingTimeConstant = 0;
+
+ source.buffer = audioBuffer;
+ source.connect(analyser);
+ analyser.connect(context.destination);
+
+ startTime = context.currentTime;
+ source.start(startTime);_context6.next = 2;return (
+ context.resume());case 2:_context6.next = 3;return (
+
+ waitForTime(context, startTime + 0.25));case 3:
+
+ freqBytes = new Uint8Array(analyser.frequencyBinCount);
+ freqFloats = new Float32Array(analyser.frequencyBinCount);
+ timeBytes = new Uint8Array(analyser.fftSize);
+ timeFloats = new Float32Array(analyser.fftSize);
+
+ analyser.getByteFrequencyData(freqBytes);
+ analyser.getFloatFrequencyData(freqFloats);
+ analyser.getByteTimeDomainData(timeBytes);
+ analyser.getFloatTimeDomainData(timeFloats);
+
+ maxFreqByte = Math.max.apply(Math, (0,_babel_runtime_helpers_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__["default"])(freqBytes));
+ maxFreqFloat = Math.max.apply(Math, (0,_babel_runtime_helpers_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__["default"])(freqFloats));
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(maxFreqByte).to.be.greaterThan(0);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(maxFreqFloat).to.be.greaterThan(analyser.minDecibels);
+
+ flatTimeDomain = true;
+ i = 0;case 4:if (!(i < timeFloats.length)) {_context6.next = 6;break;}if (!(
+ Math.abs(timeFloats[i]) > 0.001)) {_context6.next = 5;break;}
+ flatTimeDomain = false;return _context6.abrupt("continue", 6);case 5:i++;_context6.next = 4;break;case 6:
+
+
+
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(flatTimeDomain).to.equal(false);
+
+ flatByteTimeDomain = true;
+ _i = 0;case 7:if (!(_i < timeBytes.length)) {_context6.next = 9;break;}if (!(
+ timeBytes[_i] !== 128)) {_context6.next = 8;break;}
+ flatByteTimeDomain = false;return _context6.abrupt("continue", 9);case 8:_i++;_context6.next = 7;break;case 9:
+
+
+
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(flatByteTimeDomain).to.equal(false);
+
+ source.stop();_context6.next = 10;return (
+ context.close());case 10:case "end":return _context6.stop();}}, _callee6);}))
+ );
+
+ it("should support Web Audio constructors", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee7() {var context, gainNode, pannerNode, stereoPannerNode, analyserNode, buffer, sourceNode;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context7) {while (1) switch (_context7.prev = _context7.next) {case 0:
+ context = new AudioContext();
+
+ gainNode = new GainNode(context);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(gainNode.gain.value).to.equal(1);
+
+ pannerNode = new PannerNode(context);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(pannerNode.positionX.value).to.equal(0);
+
+ stereoPannerNode = new StereoPannerNode(context);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(stereoPannerNode.pan.value).to.equal(0);
+
+ analyserNode = new AnalyserNode(context);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(analyserNode.fftSize).to.equal(2048);
+
+ buffer = new AudioBuffer({ length: 128, sampleRate: 44100, numberOfChannels: 2 });
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(buffer.length).to.equal(128);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(buffer.sampleRate).to.equal(44100);
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(buffer.numberOfChannels).to.equal(2);
+
+ sourceNode = new AudioBufferSourceNode(context, { buffer: buffer });
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(sourceNode.buffer).to.equal(buffer);_context7.next = 1;return (
+
+ context.close());case 1:case "end":return _context7.stop();}}, _callee7);}))
+ );
+
+ it("should fire ended naturally when a short buffer finishes", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee8() {var context, sampleRate, frameCount, audioBuffer, source, ended, startTime, pollStart;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context8) {while (1) switch (_context8.prev = _context8.next) {case 0:
+ context = new AudioContext();
+ sampleRate = context.sampleRate;
+ frameCount = Math.ceil(sampleRate * 0.2);_context8.next = 1;return (
+ context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount)));case 1:audioBuffer = _context8.sent;
+
+ source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(context.destination);
+
+ ended = false;
+ source.addEventListener("ended", function () {
+ ended = true;
+ });
+
+ startTime = context.currentTime;
+ source.start(startTime);_context8.next = 2;return (
+ context.resume());case 2:_context8.next = 3;return (
+ waitForTime(context, startTime + audioBuffer.duration + 0.2));case 3:
+
+ // Poll briefly in case the ended callback is still marshaling to the JS thread.
+ pollStart = Date.now();case 4:if (!(
+ !ended && Date.now() - pollStart < 1000)) {_context8.next = 6;break;}_context8.next = 5;return (
+ new Promise(function (resolve) {return setTimeout(resolve, 10);}));case 5:_context8.next = 4;break;case 6:
+
+
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ended).to.equal(true);_context8.next = 7;return (
+
+ context.close());case 7:case "end":return _context8.stop();}}, _callee8);}))
+ );
+
+ it("should start buffer sources at a scheduled time", /*#__PURE__*/(0,_babel_runtime_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_1__["default"])(/*#__PURE__*/_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee9() {var context, sampleRate, frameCount, audioBuffer, source, ended, startTime, pollStart;return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function (_context9) {while (1) switch (_context9.prev = _context9.next) {case 0:
+ context = new AudioContext();_context9.next = 1;return (
+ context.resume());case 1:
+
+ sampleRate = context.sampleRate;
+ frameCount = Math.ceil(sampleRate * 0.1);_context9.next = 2;return (
+ context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount)));case 2:audioBuffer = _context9.sent;
+
+ source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(context.destination);
+
+ ended = false;
+ source.addEventListener("ended", function () {
+ ended = true;
+ });
+
+ startTime = context.currentTime + 0.2;
+ source.start(startTime);_context9.next = 3;return (
+ waitForTime(context, startTime + 0.05));case 3:
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ended).to.equal(false);_context9.next = 4;return (
+
+ waitForTime(context, startTime + audioBuffer.duration + 0.2));case 4:
+ pollStart = Date.now();case 5:if (!(
+ !ended && Date.now() - pollStart < 1000)) {_context9.next = 7;break;}_context9.next = 6;return (
+ new Promise(function (resolve) {return setTimeout(resolve, 10);}));case 6:_context9.next = 5;break;case 7:
+
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ended).to.equal(true);_context9.next = 8;return (
+
+ context.close());case 8:case "end":return _context9.stop();}}, _callee9);}))
+ );
+
+ it("should report supported mime types from Audio.canPlayType", function () {
+ var audio = new Audio();
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audio.canPlayType("audio/wav")).to.equal("probably");
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audio.canPlayType("audio/mpeg")).to.equal("probably");
+ (0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(audio.canPlayType("audio/unsupported-format")).to.equal("");
+ });
+});
+
mocha.run(function (failures) {
// Test program will wait for code to be set before exiting
if (failures > 0) {
diff --git a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts
index e7ebaacbe..3c0c52293 100644
--- a/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts
+++ b/Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts
@@ -328,6 +328,327 @@ describe("NativeEncoding", function () {
});
});
+describe("WebAudio", function () {
+ this.timeout(0);
+
+ function createSilentWavArrayBuffer(sampleRate: number, frameCount: number): ArrayBuffer {
+ const bytesPerSample = 2;
+ const channelCount = 1;
+ const dataSize = frameCount * channelCount * bytesPerSample;
+ const buffer = new ArrayBuffer(44 + dataSize);
+ const view = new DataView(buffer);
+ const writeString = (offset: number, value: string) => {
+ for (let i = 0; i < value.length; i++) {
+ view.setUint8(offset + i, value.charCodeAt(i));
+ }
+ };
+
+ writeString(0, "RIFF");
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(8, "WAVE");
+ writeString(12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, channelCount, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
+ view.setUint16(32, channelCount * bytesPerSample, true);
+ view.setUint16(34, 8 * bytesPerSample, true);
+ writeString(36, "data");
+ view.setUint32(40, dataSize, true);
+ return buffer;
+ }
+
+ function createSineWavArrayBuffer(sampleRate: number, frameCount: number, frequency: number, amplitude: number = 0.5): ArrayBuffer {
+ const bytesPerSample = 2;
+ const channelCount = 1;
+ const dataSize = frameCount * channelCount * bytesPerSample;
+ const buffer = new ArrayBuffer(44 + dataSize);
+ const view = new DataView(buffer);
+ const writeString = (offset: number, value: string) => {
+ for (let i = 0; i < value.length; i++) {
+ view.setUint8(offset + i, value.charCodeAt(i));
+ }
+ };
+
+ writeString(0, "RIFF");
+ view.setUint32(4, 36 + dataSize, true);
+ writeString(8, "WAVE");
+ writeString(12, "fmt ");
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, channelCount, true);
+ view.setUint32(24, sampleRate, true);
+ view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
+ view.setUint16(32, channelCount * bytesPerSample, true);
+ view.setUint16(34, 8 * bytesPerSample, true);
+ writeString(36, "data");
+ view.setUint32(40, dataSize, true);
+
+ for (let frame = 0; frame < frameCount; frame++) {
+ const sample = Math.sin((2 * Math.PI * frequency * frame) / sampleRate) * amplitude;
+ const intSample = Math.max(-32768, Math.min(32767, Math.round(sample * 32767)));
+ view.setInt16(44 + frame * bytesPerSample, intSample, true);
+ }
+
+ return buffer;
+ }
+
+ async function waitForTime(context: AudioContext, targetTime: number, timeoutMs: number = 5000): Promise {
+ const start = Date.now();
+ while (context.currentTime < targetTime) {
+ if (Date.now() - start > timeoutMs) {
+ throw new Error(`Timed out waiting for currentTime >= ${targetTime}`);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+ }
+
+ it("should expose AudioContext and play a decoded buffer through the node graph", async function () {
+ // JavaScriptCore reports some host constructors as typeof "object".
+ expect(AudioContext).to.exist;
+ expect(webkitAudioContext).to.exist;
+ expect(AudioContext).to.equal(webkitAudioContext);
+
+ const context = new AudioContext();
+ expect(context.state).to.equal("suspended");
+ await context.resume();
+ expect(context.state).to.equal("running");
+ expect(context.sampleRate).to.be.greaterThan(0);
+
+ const sampleRate = 44100;
+ const frameCount = sampleRate; // 1 second
+ const audioBuffer = await context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount));
+ expect(audioBuffer.sampleRate).to.equal(sampleRate);
+ expect(audioBuffer.length).to.equal(frameCount);
+ expect(audioBuffer.duration).to.be.closeTo(1, 0.01);
+ expect(audioBuffer.numberOfChannels).to.equal(1);
+ expect(audioBuffer.getChannelData(0).length).to.equal(frameCount);
+
+ const source = context.createBufferSource();
+ const gain = context.createGain();
+ const panner = context.createPanner();
+
+ source.buffer = audioBuffer;
+ gain.gain.value = 0.5;
+ panner.setPosition(1, 0, 0);
+ context.listener.setPosition(0, 0, 0);
+
+ source.connect(gain);
+ gain.connect(panner);
+ panner.connect(context.destination);
+
+ let ended = false;
+ source.addEventListener("ended", () => {
+ ended = true;
+ });
+
+ source.start();
+ await context.resume();
+
+ // Allow the audio thread to mix at least one callback.
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(ended).to.equal(false);
+
+ source.stop();
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(ended).to.equal(true);
+
+ await context.close();
+ expect(context.state).to.equal("closed");
+ });
+
+ it("should automate AudioParam values over time", async function () {
+ const context = new AudioContext();
+ const gain = context.createGain();
+ gain.connect(context.destination);
+
+ const scheduleStart = context.currentTime;
+ gain.gain.cancelScheduledValues(scheduleStart);
+ gain.gain.setValueAtTime(0, scheduleStart);
+ gain.gain.linearRampToValueAtTime(1, scheduleStart + 1);
+ expect(gain.gain.value).to.be.closeTo(0, 0.01);
+
+ const sampleRate = context.sampleRate;
+ const frameCount = Math.ceil(sampleRate * 1.5);
+ const audioBuffer = await context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount));
+ const source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(gain);
+ source.start(scheduleStart);
+ await context.resume();
+
+ await waitForTime(context, scheduleStart + 0.5);
+ expect(gain.gain.value).to.be.closeTo(0.5, 0.15);
+
+ const curve = new Float32Array([0, 0.25, 0.75, 1]);
+ const curveStart = context.currentTime;
+ gain.gain.cancelScheduledValues(curveStart);
+ gain.gain.setValueCurveAtTime(curve, curveStart, 0.2);
+ await waitForTime(context, curveStart + 0.1);
+ expect(gain.gain.value).to.be.greaterThan(0.1);
+ expect(gain.gain.value).to.be.lessThan(0.9);
+
+ source.stop();
+ await context.close();
+ });
+
+ it("should return analyser frequency and time-domain data for non-silent audio", async function () {
+ const context = new AudioContext();
+ const sampleRate = context.sampleRate;
+ const frequency = 440;
+ const frameCount = sampleRate;
+ const audioBuffer = await context.decodeAudioData(createSineWavArrayBuffer(sampleRate, frameCount, frequency));
+
+ const source = context.createBufferSource();
+ const analyser = context.createAnalyser();
+ analyser.fftSize = 2048;
+ analyser.smoothingTimeConstant = 0;
+
+ source.buffer = audioBuffer;
+ source.connect(analyser);
+ analyser.connect(context.destination);
+
+ const startTime = context.currentTime;
+ source.start(startTime);
+ await context.resume();
+
+ await waitForTime(context, startTime + 0.25);
+
+ const freqBytes = new Uint8Array(analyser.frequencyBinCount);
+ const freqFloats = new Float32Array(analyser.frequencyBinCount);
+ const timeBytes = new Uint8Array(analyser.fftSize);
+ const timeFloats = new Float32Array(analyser.fftSize);
+
+ analyser.getByteFrequencyData(freqBytes);
+ analyser.getFloatFrequencyData(freqFloats);
+ analyser.getByteTimeDomainData(timeBytes);
+ analyser.getFloatTimeDomainData(timeFloats);
+
+ const maxFreqByte = Math.max(...freqBytes);
+ const maxFreqFloat = Math.max(...freqFloats);
+ expect(maxFreqByte).to.be.greaterThan(0);
+ expect(maxFreqFloat).to.be.greaterThan(analyser.minDecibels);
+
+ let flatTimeDomain = true;
+ for (let i = 0; i < timeFloats.length; i++) {
+ if (Math.abs(timeFloats[i]) > 0.001) {
+ flatTimeDomain = false;
+ break;
+ }
+ }
+ expect(flatTimeDomain).to.equal(false);
+
+ let flatByteTimeDomain = true;
+ for (let i = 0; i < timeBytes.length; i++) {
+ if (timeBytes[i] !== 128) {
+ flatByteTimeDomain = false;
+ break;
+ }
+ }
+ expect(flatByteTimeDomain).to.equal(false);
+
+ source.stop();
+ await context.close();
+ });
+
+ it("should support Web Audio constructors", async function () {
+ const context = new AudioContext();
+
+ const gainNode = new GainNode(context);
+ expect(gainNode.gain.value).to.equal(1);
+
+ const pannerNode = new PannerNode(context);
+ expect(pannerNode.positionX.value).to.equal(0);
+
+ const stereoPannerNode = new StereoPannerNode(context);
+ expect(stereoPannerNode.pan.value).to.equal(0);
+
+ const analyserNode = new AnalyserNode(context);
+ expect(analyserNode.fftSize).to.equal(2048);
+
+ const buffer = new AudioBuffer({ length: 128, sampleRate: 44100, numberOfChannels: 2 });
+ expect(buffer.length).to.equal(128);
+ expect(buffer.sampleRate).to.equal(44100);
+ expect(buffer.numberOfChannels).to.equal(2);
+
+ const sourceNode = new AudioBufferSourceNode(context, { buffer });
+ expect(sourceNode.buffer).to.equal(buffer);
+
+ await context.close();
+ });
+
+ it("should fire ended naturally when a short buffer finishes", async function () {
+ const context = new AudioContext();
+ const sampleRate = context.sampleRate;
+ const frameCount = Math.ceil(sampleRate * 0.2);
+ const audioBuffer = await context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount));
+
+ const source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(context.destination);
+
+ let ended = false;
+ source.addEventListener("ended", () => {
+ ended = true;
+ });
+
+ const startTime = context.currentTime;
+ source.start(startTime);
+ await context.resume();
+ await waitForTime(context, startTime + audioBuffer.duration + 0.2);
+
+ // Poll briefly in case the ended callback is still marshaling to the JS thread.
+ const pollStart = Date.now();
+ while (!ended && Date.now() - pollStart < 1000) {
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+
+ expect(ended).to.equal(true);
+
+ await context.close();
+ });
+
+ it("should start buffer sources at a scheduled time", async function () {
+ const context = new AudioContext();
+ await context.resume();
+
+ const sampleRate = context.sampleRate;
+ const frameCount = Math.ceil(sampleRate * 0.1);
+ const audioBuffer = await context.decodeAudioData(createSilentWavArrayBuffer(sampleRate, frameCount));
+
+ const source = context.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(context.destination);
+
+ let ended = false;
+ source.addEventListener("ended", () => {
+ ended = true;
+ });
+
+ const startTime = context.currentTime + 0.2;
+ source.start(startTime);
+ await waitForTime(context, startTime + 0.05);
+ expect(ended).to.equal(false);
+
+ await waitForTime(context, startTime + audioBuffer.duration + 0.2);
+ const pollStart = Date.now();
+ while (!ended && Date.now() - pollStart < 1000) {
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+ expect(ended).to.equal(true);
+
+ await context.close();
+ });
+
+ it("should report supported mime types from Audio.canPlayType", function () {
+ const audio = new Audio();
+ expect(audio.canPlayType("audio/wav")).to.equal("probably");
+ expect(audio.canPlayType("audio/mpeg")).to.equal("probably");
+ expect(audio.canPlayType("audio/unsupported-format")).to.equal("");
+ });
+});
+
mocha.run((failures) => {
// Test program will wait for code to be set before exiting
if (failures > 0) {
diff --git a/Apps/UnitTests/Source/Tests.JavaScript.cpp b/Apps/UnitTests/Source/Tests.JavaScript.cpp
index 817d3f06d..9f0dcdb05 100644
--- a/Apps/UnitTests/Source/Tests.JavaScript.cpp
+++ b/Apps/UnitTests/Source/Tests.JavaScript.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -48,6 +49,7 @@ TEST(JavaScript, All)
device.StartRenderingCurrentFrame();
std::optional nativeCanvas;
+ std::optional nativeWebAudio;
Babylon::AppRuntime::Options options{};
@@ -66,7 +68,7 @@ TEST(JavaScript, All)
std::promise exitCodePromise;
- runtime.Dispatch([&exitCodePromise, &device, &nativeCanvas](Napi::Env env) {
+ runtime.Dispatch([&exitCodePromise, &device, &nativeCanvas, &nativeWebAudio](Napi::Env env) {
device.AddToJavaScript(env);
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
@@ -76,6 +78,7 @@ TEST(JavaScript, All)
Babylon::Polyfills::Window::Initialize(env);
Babylon::Polyfills::Blob::Initialize(env);
nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env));
+ nativeWebAudio.emplace(Babylon::Polyfills::WebAudio::Initialize(env));
Babylon::Plugins::NativeEngine::Initialize(env);
Babylon::Plugins::NativeEncoding::Initialize(env);
@@ -108,7 +111,13 @@ TEST(JavaScript, All)
auto exitCode = exitCodeFuture.get();
EXPECT_EQ(exitCode, 0);
+ if (nativeWebAudio)
+ {
+ nativeWebAudio->ShutdownPlayback();
+ }
+
// Runtime destructor joins the JS thread; must happen before Finish.
+ nativeWebAudio.reset();
nativeCanvas.reset();
device.FinishRenderingCurrentFrame();
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1f5c3894d..4164a3dbe 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -59,6 +59,10 @@ FetchContent_Declare(metal-cpp
GIT_REPOSITORY https://github.com/bkaradzic/metal-cpp.git
GIT_TAG metal-cpp_26
EXCLUDE_FROM_ALL)
+FetchContent_Declare(miniaudio
+ GIT_REPOSITORY https://github.com/mackron/miniaudio.git
+ GIT_TAG 0.11.22
+ EXCLUDE_FROM_ALL)
FetchContent_Declare(SPIRV-Cross
GIT_REPOSITORY https://github.com/BabylonJS/SPIRV-Cross.git
GIT_TAG a512817ddbcd879a3929aef7d1d762871bdf8635
@@ -142,6 +146,7 @@ option(BABYLON_NATIVE_POLYFILL_SCHEDULING "Include Babylon Native Polyfill Sched
option(BABYLON_NATIVE_POLYFILL_URL "Include Babylon Native Polyfill URL." ON)
option(BABYLON_NATIVE_POLYFILL_WEBSOCKET "Include Babylon Native Polyfill WebSocket." ON)
option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON)
+option(BABYLON_NATIVE_POLYFILL_WEBAUDIO "Include Babylon Native Polyfill WebAudio." ON)
# Embedding
option(BABYLON_NATIVE_EMBEDDING "Build the cross-platform Babylon::Embedding facade (Runtime + View)." ON)
diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt
index 58a82367f..31453979c 100644
--- a/Dependencies/CMakeLists.txt
+++ b/Dependencies/CMakeLists.txt
@@ -27,6 +27,15 @@ FetchContent_MakeAvailable_With_Message(base-n)
add_library(base-n INTERFACE)
target_include_directories(base-n INTERFACE "${base-n_SOURCE_DIR}/include")
+# --------------------------------------------------
+# miniaudio
+# --------------------------------------------------
+if(BABYLON_NATIVE_POLYFILL_WEBAUDIO)
+ # miniaudio's own CMakeLists defines the `miniaudio` target (header-only
+ # include path plus optional extras). Consume it as-is.
+ FetchContent_MakeAvailable_With_Message(miniaudio)
+endif()
+
# --------------------------------------------------
# bgfx.cmake
# --------------------------------------------------
diff --git a/Documentation/Polyfills.md b/Documentation/Polyfills.md
index 2c687dc19..fdd90baee 100644
--- a/Documentation/Polyfills.md
+++ b/Documentation/Polyfills.md
@@ -6,5 +6,6 @@ The polyfills are still in an early stage and are subject to change.
At the moment, we are using the following polyfills:
* [Canvas](../Polyfills/Canvas/readme.md)
* [Console](../Polyfills/Console/readme.md)
+* [WebAudio](../Polyfills/WebAudio/Readme.md)
* [Window](../Polyfills/Window/readme.md)
* [XMLHttpRequest](../Polyfills/XMLHttpRequest/readme.md)
\ No newline at end of file
diff --git a/Embedding/Android/CMakeLists.txt b/Embedding/Android/CMakeLists.txt
index fa820b076..6eda98124 100644
--- a/Embedding/Android/CMakeLists.txt
+++ b/Embedding/Android/CMakeLists.txt
@@ -18,7 +18,9 @@ set(ANDROID_EMBEDDING_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Include/Platf
set(SOURCES
"${ANDROID_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Android/RuntimeHandle.h"
- "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/BabylonNativeEmbedding.cpp")
+ "${ANDROID_EMBEDDING_INCLUDE_ROOT}/Babylon/Embedding/Android/AudioPlatform.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/BabylonNativeEmbedding.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/AudioPlatformAndroid.cpp")
add_library(BabylonNativeEmbedding SHARED ${SOURCES})
@@ -28,6 +30,7 @@ target_include_directories(BabylonNativeEmbedding PUBLIC "${ANDROID_EMBEDDING_IN
target_link_libraries(BabylonNativeEmbedding
PRIVATE Embedding
+ PRIVATE WebAudio
PRIVATE AndroidExtensions
PRIVATE android # ANativeWindow_fromSurface
PRIVATE log
diff --git a/Embedding/Android/src/main/cpp/AudioPlatformAndroid.cpp b/Embedding/Android/src/main/cpp/AudioPlatformAndroid.cpp
new file mode 100644
index 000000000..2daf85da9
--- /dev/null
+++ b/Embedding/Android/src/main/cpp/AudioPlatformAndroid.cpp
@@ -0,0 +1,73 @@
+#include
+#include
+
+#include
+
+namespace
+{
+ JavaVM* g_javaVM{nullptr};
+
+ void RequestAndroidPlaybackFocus()
+ {
+ if (!g_javaVM)
+ {
+ return;
+ }
+
+ JNIEnv* env{nullptr};
+ bool detach = false;
+ const jint envStatus = g_javaVM->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6);
+ if (envStatus == JNI_EDETACHED)
+ {
+ if (g_javaVM->AttachCurrentThread(&env, nullptr) != JNI_OK)
+ {
+ return;
+ }
+ detach = true;
+ }
+ else if (envStatus != JNI_OK)
+ {
+ return;
+ }
+
+ jclass helperClass = env->FindClass("com/babylonjs/embedding/AudioFocusHelper");
+ if (helperClass != nullptr)
+ {
+ jmethodID requestMethod = env->GetStaticMethodID(helperClass, "requestPlaybackFocus", "()V");
+ if (requestMethod != nullptr)
+ {
+ env->CallStaticVoidMethod(helperClass, requestMethod);
+ }
+ }
+
+ if (detach)
+ {
+ g_javaVM->DetachCurrentThread();
+ }
+ }
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_babylonjs_embedding_AudioFocusHelper_nativeNotifyAudioInterruption(JNIEnv*, jclass, jboolean began)
+{
+ Babylon::Polyfills::Internal::NotifyAudioInterruption(began == JNI_TRUE);
+}
+
+void Babylon::Embedding::Android::RegisterWebAudioPlatform(JNIEnv* env, jobject context, JavaVM* javaVM)
+{
+ g_javaVM = javaVM;
+
+ jclass helperClass = env->FindClass("com/babylonjs/embedding/AudioFocusHelper");
+ if (helperClass != nullptr)
+ {
+ jmethodID initializeMethod = env->GetStaticMethodID(helperClass, "initialize", "(Landroid/content/Context;)V");
+ if (initializeMethod != nullptr)
+ {
+ env->CallStaticVoidMethod(helperClass, initializeMethod, context);
+ }
+ }
+
+ Babylon::Polyfills::RegisterAudioPreparePlaybackHandler([]() {
+ RequestAndroidPlaybackFocus();
+ });
+}
diff --git a/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp
index 4e506e025..f059668c5 100644
--- a/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp
+++ b/Embedding/Android/src/main/cpp/BabylonNativeEmbedding.cpp
@@ -17,6 +17,9 @@
#include
#include
#include
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+#include
+#endif
#include
@@ -285,6 +288,9 @@ Java_com_babylonjs_embedding_BabylonNative_setContext(
return;
}
android::global::Initialize(javaVM, context);
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ Babylon::Embedding::Android::RegisterWebAudioPlatform(env, context, javaVM);
+#endif
}
JNIEXPORT void JNICALL
diff --git a/Embedding/Apple/CMakeLists.txt b/Embedding/Apple/CMakeLists.txt
index 6bea2bf8e..7f390a0f6 100644
--- a/Embedding/Apple/CMakeLists.txt
+++ b/Embedding/Apple/CMakeLists.txt
@@ -23,7 +23,8 @@ set(SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm"
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntimeInternal.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm"
- "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm")
+ "${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm"
+ "${CMAKE_CURRENT_SOURCE_DIR}/Source/AudioSession.mm")
add_library(BabylonNativeEmbedding STATIC ${SOURCES})
@@ -35,15 +36,18 @@ target_include_directories(BabylonNativeEmbedding
target_link_libraries(BabylonNativeEmbedding
PUBLIC Embedding
+ PRIVATE WebAudio
PRIVATE "-framework Foundation"
PRIVATE "-framework QuartzCore"
- PRIVATE "-framework MetalKit")
+ PRIVATE "-framework MetalKit"
+ PRIVATE "-framework AVFoundation")
# Enable ARC for the Obj-C++ files.
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNRuntime.mm"
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNView.mm"
"${CMAKE_CURRENT_SOURCE_DIR}/Source/BNViewDelegate.mm"
+ "${CMAKE_CURRENT_SOURCE_DIR}/Source/AudioSession.mm"
PROPERTIES COMPILE_FLAGS "-fobjc-arc")
set_property(TARGET BabylonNativeEmbedding PROPERTY FOLDER Embedding)
diff --git a/Embedding/Apple/Source/AudioSession.mm b/Embedding/Apple/Source/AudioSession.mm
new file mode 100644
index 000000000..ee952b553
--- /dev/null
+++ b/Embedding/Apple/Source/AudioSession.mm
@@ -0,0 +1,50 @@
+#include
+
+#import
+
+namespace
+{
+ void ConfigureAudioSession()
+ {
+ AVAudioSession* session = [AVAudioSession sharedInstance];
+ NSError* error = nil;
+ [session setCategory:AVAudioSessionCategoryPlayback error:&error];
+ [session setActive:YES error:&error];
+ }
+
+ void HandleInterruption(NSNotification* notification)
+ {
+ const auto type = static_cast(
+ [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]);
+
+ if (type == AVAudioSessionInterruptionTypeBegan)
+ {
+ Babylon::Polyfills::Internal::NotifyAudioInterruption(true);
+ }
+ else if (type == AVAudioSessionInterruptionTypeEnded)
+ {
+ ConfigureAudioSession();
+ Babylon::Polyfills::Internal::NotifyAudioInterruption(false);
+ }
+ }
+
+ struct AudioSessionRegistrar
+ {
+ AudioSessionRegistrar()
+ {
+ Babylon::Polyfills::RegisterAudioPreparePlaybackHandler([]() {
+ ConfigureAudioSession();
+ });
+
+ [[NSNotificationCenter defaultCenter]
+ addObserverForName:AVAudioSessionInterruptionNotification
+ object:[AVAudioSession sharedInstance]
+ queue:nil
+ usingBlock:^(NSNotification* notification) {
+ HandleInterruption(notification);
+ }];
+ }
+ };
+
+ AudioSessionRegistrar g_audioSessionRegistrar{};
+}
diff --git a/Embedding/CMakeLists.txt b/Embedding/CMakeLists.txt
index 20715f0bb..180051354 100644
--- a/Embedding/CMakeLists.txt
+++ b/Embedding/CMakeLists.txt
@@ -80,6 +80,11 @@ if(BABYLON_NATIVE_POLYFILL_WEBSOCKET)
target_link_libraries(Embedding PRIVATE WebSocket)
endif()
+if(BABYLON_NATIVE_POLYFILL_WEBAUDIO)
+ target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_POLYFILL_WEBAUDIO=1)
+ target_link_libraries(Embedding PRIVATE WebAudio)
+endif()
+
# ----- Conditionally-included plugins -----
if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE)
diff --git a/Embedding/Include/Platform/Android/Babylon/Embedding/Android/AudioPlatform.h b/Embedding/Include/Platform/Android/Babylon/Embedding/Android/AudioPlatform.h
new file mode 100644
index 000000000..0250a2405
--- /dev/null
+++ b/Embedding/Include/Platform/Android/Babylon/Embedding/Android/AudioPlatform.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include
+
+namespace Babylon::Embedding::Android
+{
+ void RegisterWebAudioPlatform(JNIEnv* env, jobject context, JavaVM* javaVM);
+}
diff --git a/Embedding/Source/Runtime.cpp b/Embedding/Source/Runtime.cpp
index 852528288..c8ab2cdde 100644
--- a/Embedding/Source/Runtime.cpp
+++ b/Embedding/Source/Runtime.cpp
@@ -56,6 +56,10 @@
#include
#endif
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+#include
+#endif
+
#include
#include
#include
@@ -145,6 +149,14 @@ namespace Babylon::Embedding
m_canvas.reset();
#endif
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ if (m_webAudio)
+ {
+ m_webAudio->ShutdownPlayback();
+ }
+ m_webAudio.reset();
+#endif
+
#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT
m_input = nullptr;
#endif
@@ -260,6 +272,10 @@ namespace Babylon::Embedding
implPtr->m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env));
#endif
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ implPtr->m_webAudio.emplace(Babylon::Polyfills::WebAudio::Initialize(env));
+#endif
+
// 3. Plugins.
#if BABYLON_NATIVE_PLUGIN_NATIVETRACING
Babylon::Plugins::NativeTracing::Initialize(env);
@@ -411,6 +427,12 @@ namespace Babylon::Embedding
m_impl->SaveShaderCache();
#endif
m_impl->m_appRuntime->Suspend();
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ if (m_impl->m_webAudio)
+ {
+ m_impl->m_webAudio->SuspendPlayback();
+ }
+#endif
}
}
@@ -425,6 +447,12 @@ namespace Babylon::Embedding
if (m_impl->m_suspendCount.fetch_sub(1, std::memory_order_relaxed) == 1)
{
m_impl->m_appRuntime->Resume();
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ if (m_impl->m_webAudio)
+ {
+ m_impl->m_webAudio->ResumePlayback();
+ }
+#endif
// Re-open the frame on the attached View. On a view that was
// attached but never sized, this also drives the deferred
// first-Resize init via InitializeIfReady.
diff --git a/Embedding/Source/RuntimeImpl.h b/Embedding/Source/RuntimeImpl.h
index 6f2260b3b..fddae238b 100644
--- a/Embedding/Source/RuntimeImpl.h
+++ b/Embedding/Source/RuntimeImpl.h
@@ -11,6 +11,10 @@
#include
#endif
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+#include
+#endif
+
#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT
#include
#endif
@@ -54,6 +58,10 @@ namespace Babylon::Embedding
std::optional m_canvas;
#endif
+#if BABYLON_NATIVE_POLYFILL_WEBAUDIO
+ std::optional m_webAudio;
+#endif
+
#if BABYLON_NATIVE_PLUGIN_NATIVEINPUT
// Owned by the JS world (returned by NativeInput::CreateForJavaScript).
// We just keep a pointer for forwarding View::OnPointer* calls.
diff --git a/Polyfills/CMakeLists.txt b/Polyfills/CMakeLists.txt
index 16c1b767f..53e8178f1 100644
--- a/Polyfills/CMakeLists.txt
+++ b/Polyfills/CMakeLists.txt
@@ -5,3 +5,7 @@ endif()
if(BABYLON_NATIVE_POLYFILL_CANVAS)
add_subdirectory(Canvas)
endif()
+
+if(BABYLON_NATIVE_POLYFILL_WEBAUDIO)
+ add_subdirectory(WebAudio)
+endif()
diff --git a/Polyfills/WebAudio/CMakeLists.txt b/Polyfills/WebAudio/CMakeLists.txt
new file mode 100644
index 000000000..20dfedcc6
--- /dev/null
+++ b/Polyfills/WebAudio/CMakeLists.txt
@@ -0,0 +1,49 @@
+set(SOURCES
+ "Include/Babylon/Polyfills/WebAudio.h"
+ "Source/WebAudio.cpp"
+ "Source/AudioEngine.cpp"
+ "Source/AudioEngine.h"
+ "Source/AudioNode.cpp"
+ "Source/AudioNode.h"
+ "Source/AudioParamState.cpp"
+ "Source/AudioParam.cpp"
+ "Source/AudioParam.h"
+ "Source/AudioBuffer.cpp"
+ "Source/AudioBuffer.h"
+ "Source/AudioBufferSourceNode.cpp"
+ "Source/AudioBufferSourceNode.h"
+ "Source/GainNode.cpp"
+ "Source/GainNode.h"
+ "Source/PannerNode.cpp"
+ "Source/PannerNode.h"
+ "Source/StereoPannerNode.cpp"
+ "Source/StereoPannerNode.h"
+ "Source/AnalyserNode.cpp"
+ "Source/AnalyserNode.h"
+ "Source/AudioListener.cpp"
+ "Source/AudioListener.h"
+ "Source/AudioDestinationNode.cpp"
+ "Source/AudioDestinationNode.h"
+ "Source/AudioContext.cpp"
+ "Source/AudioContext.h"
+ "Source/HTMLAudioElement.cpp"
+ "Source/HTMLAudioElement.h"
+ "Source/AudioPlatform.cpp"
+ "Source/miniaudio_impl.cpp")
+
+add_library(WebAudio ${SOURCES})
+
+warnings_as_errors(WebAudio)
+
+target_include_directories(WebAudio
+ PUBLIC "Include"
+ PRIVATE "Source")
+
+target_link_libraries(WebAudio
+ PUBLIC napi
+ PRIVATE JsRuntimeInternal
+ PRIVATE miniaudio
+ PRIVATE arcana)
+
+set_property(TARGET WebAudio PROPERTY FOLDER Polyfills)
+source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
diff --git a/Polyfills/WebAudio/Include/Babylon/Polyfills/AudioPlatform.h b/Polyfills/WebAudio/Include/Babylon/Polyfills/AudioPlatform.h
new file mode 100644
index 000000000..06e5ddacf
--- /dev/null
+++ b/Polyfills/WebAudio/Include/Babylon/Polyfills/AudioPlatform.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace Babylon::Polyfills
+{
+ using AudioPreparePlaybackHandler = std::function;
+ using AudioInterruptionHandler = std::function;
+
+ void BABYLON_API RegisterAudioPreparePlaybackHandler(AudioPreparePlaybackHandler handler);
+ void BABYLON_API RegisterAudioInterruptionHandler(AudioInterruptionHandler handler);
+
+ namespace Internal
+ {
+ class AudioEngine;
+
+ void PreparePlayback();
+ void ReleasePlayback();
+ void NotifyAudioInterruption(bool began);
+
+ void RegisterAudioEngine(const std::shared_ptr& engine);
+ void UnregisterAudioEngine(AudioEngine* engine);
+ }
+}
diff --git a/Polyfills/WebAudio/Include/Babylon/Polyfills/WebAudio.h b/Polyfills/WebAudio/Include/Babylon/Polyfills/WebAudio.h
new file mode 100644
index 000000000..6e11a1b0b
--- /dev/null
+++ b/Polyfills/WebAudio/Include/Babylon/Polyfills/WebAudio.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+namespace Babylon::Polyfills
+{
+ class WebAudio final
+ {
+ public:
+ class Impl;
+
+ WebAudio(const WebAudio& other) = default;
+ WebAudio& operator=(const WebAudio& other) = default;
+
+ WebAudio(WebAudio&&) noexcept = default;
+ WebAudio& operator=(WebAudio&&) noexcept = default;
+
+ ~WebAudio();
+
+ void SuspendPlayback() const;
+ void ResumePlayback() const;
+ void ShutdownPlayback() const;
+
+ // This instance must live as long as the JS Runtime.
+ // If JSRuntime is attached/detached (BabylonReactNative),
+ // then this instance must live forever.
+ [[nodiscard]] static WebAudio BABYLON_API Initialize(Napi::Env env);
+
+ private:
+ WebAudio(std::shared_ptr impl);
+ std::shared_ptr m_impl{};
+ };
+}
diff --git a/Polyfills/WebAudio/Readme.md b/Polyfills/WebAudio/Readme.md
new file mode 100644
index 000000000..04fdd0763
--- /dev/null
+++ b/Polyfills/WebAudio/Readme.md
@@ -0,0 +1,64 @@
+# WebAudio
+
+Implements the subset of the Web Audio API used by Babylon.js, backed by [miniaudio](https://github.com/mackron/miniaudio).
+
+## Platform support
+
+| Platform | Status |
+|----------|--------|
+| Windows / macOS / Linux (desktop) | Validated via unit tests (null backend) and Playground `audio.js` (real device). Suitable for buffer-based Babylon audio. |
+| iOS / Android | Built into Embedding with `AVAudioSession` (iOS) and Android audio-focus hooks. Playground loads `audio.js` on both platforms for on-device validation. |
+| CI / headless | Use `BABYLON_NATIVE_WEBAUDIO_BACKEND=null` so graph logic runs without a playback device. |
+
+Enable with `BABYLON_NATIVE_POLYFILL_WEBAUDIO=ON` (default). The polyfill is initialized from `Embedding` when linked.
+
+## Lifecycle (browser-aligned)
+
+- `AudioContext` starts in the `"suspended"` state; call `resume()` after a user gesture on mobile.
+- The native playback device is opened lazily on the first successful `resume()`.
+- `Embedding::Runtime::Suspend/Resume` pauses and resumes the shared audio device when the host app backgrounds.
+- iOS activates `AVAudioSession` and handles interruptions; Android requests audio focus before playback.
+- A minimal `document` stub (`createElement("audio")`, no-op event listeners) is installed automatically when `globalThis.document` is undefined.
+
+## Supported surface
+
+- `AudioContext` / `webkitAudioContext`
+- `AudioBuffer`, `AudioBufferSourceNode`, `GainNode`, `PannerNode`, `StereoPannerNode`
+- `AudioListener`, `AudioParam` (automation timeline: `setValueAtTime`, ramps, `setValueCurveAtTime`, `cancelScheduledValues`)
+- `AnalyserNode` (time-domain snapshot + Hann-windowed FFT frequency data with smoothing)
+- `AudioDestinationNode`
+- `Audio` (`canPlayType` for format detection)
+- `AudioBufferSourceNode.start(when)` scheduled start times
+
+Decoding supports the formats provided by miniaudio (wav, mp3, flac, ogg, and others depending on build).
+
+Set `BABYLON_NATIVE_WEBAUDIO_BACKEND=null` to force a software clock for headless CI (no hardware device). When no playback device is available at runtime, the polyfill falls back to miniaudio's null backend automatically so construction never crashes.
+
+## Babylon.js coverage
+
+Works for the paths Babylon actually uses on native:
+
+- Legacy `BABYLON.Sound` with `ArrayBuffer` / `decodeAudioData`
+- v9 `CreateAudioEngineAsync` + `CreateSoundAsync` (when present in the Babylon build)
+- 3D spatial audio (`PannerNode`, `AudioListener`, distance attenuation)
+- Volume / pitch ramps via `AudioParam` automation
+- Audio analysers (`getByteFrequencyData`, `getFloatFrequencyData`, time-domain getters)
+
+## Not supported (yet)
+
+- `OfflineAudioContext`
+- `MediaElementAudioSourceNode` / `MediaStream` sources
+- URL streaming via `HTMLAudioElement` (`play` / `pause` are stubs; use `decodeAudioData` instead)
+- Full Web Audio spec (`setTargetAtTime`, HRTF panning, `AudioWorklet`, etc.)
+
+## App integration
+
+Embedding hosts that link `BabylonNativeEmbedding` receive platform audio-session setup automatically. No per-app `document` stub is required.
+
+For custom hosts that initialize the polyfill directly, `WebAudio::Initialize` still installs the built-in `document` stub when needed.
+
+On mobile, ensure the first audible playback follows a user gesture and call `AudioContext.resume()` from that gesture if autoplay is blocked. See `Apps/Playground/Scripts/audio.js` for a working example (pointer tap resumes audio).
+
+## Playground demo
+
+Playground loads `app:///Scripts/audio.js` on desktop, iOS, and Android. The script generates an in-memory beep WAV and plays it through legacy `BABYLON.Sound` and/or `CreateSoundAsync` while orbiting a spatialized source around the camera.
diff --git a/Polyfills/WebAudio/Source/AnalyserNode.cpp b/Polyfills/WebAudio/Source/AnalyserNode.cpp
new file mode 100644
index 000000000..0812a0d35
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AnalyserNode.cpp
@@ -0,0 +1,244 @@
+#include "AnalyserNode.h"
+#include "AudioContext.h"
+#include "AudioEngine.h"
+
+#include
+
+#include
+#include
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ namespace
+ {
+ void LockAnd(const std::shared_ptr& context, auto&& fn)
+ {
+ if (context && context->engine)
+ {
+ std::lock_guard lock{context->engine->GraphMutex()};
+ fn();
+ }
+ else
+ {
+ fn();
+ }
+ }
+ }
+
+ void NativeAnalyserNode::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceMethod("connect", &NativeAnalyserNode::Connect),
+ InstanceMethod("disconnect", &NativeAnalyserNode::Disconnect),
+ InstanceMethod("getByteFrequencyData", &NativeAnalyserNode::GetByteFrequencyData),
+ InstanceMethod("getFloatFrequencyData", &NativeAnalyserNode::GetFloatFrequencyData),
+ InstanceMethod("getByteTimeDomainData", &NativeAnalyserNode::GetByteTimeDomainData),
+ InstanceMethod("getFloatTimeDomainData", &NativeAnalyserNode::GetFloatTimeDomainData),
+ InstanceAccessor("fftSize", &NativeAnalyserNode::GetFftSize, &NativeAnalyserNode::SetFftSize),
+ InstanceAccessor("frequencyBinCount", &NativeAnalyserNode::GetFrequencyBinCount, nullptr),
+ InstanceAccessor("minDecibels", &NativeAnalyserNode::GetMinDecibels, &NativeAnalyserNode::SetMinDecibels),
+ InstanceAccessor("maxDecibels", &NativeAnalyserNode::GetMaxDecibels, &NativeAnalyserNode::SetMaxDecibels),
+ InstanceAccessor("smoothingTimeConstant", &NativeAnalyserNode::GetSmoothingTimeConstant, &NativeAnalyserNode::SetSmoothingTimeConstant),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set(JS_CONSTRUCTOR_NAME, func);
+ }
+
+ Napi::Object NativeAnalyserNode::CreateInstance(Napi::Env env, const std::shared_ptr& context)
+ {
+ auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CONSTRUCTOR_NAME).As();
+ auto object = func.New({});
+ NativeAnalyserNode::Unwrap(object)->InitializeState(context);
+ return object;
+ }
+
+ NativeAnalyserNode::NativeAnalyserNode(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ {
+ if (info.Length() > 0 && info[0].IsObject())
+ {
+ auto* context = NativeAudioContext::Unwrap(info[0].As());
+ if (context)
+ {
+ info.This().As().Set("context", info[0]);
+ InitializeState(context->State());
+ }
+ }
+ }
+
+ void NativeAnalyserNode::InitializeState(const std::shared_ptr& context)
+ {
+ m_state = std::make_shared();
+ m_state->context = context;
+ BindNodeState(Value(), m_state);
+ SetAudioNodeProperties(Value(), 1, 1);
+ }
+
+ Napi::Value NativeAnalyserNode::Connect(const Napi::CallbackInfo& info)
+ {
+ auto destination = UnwrapNodeState(info[0]);
+ if (m_state && destination)
+ {
+ LockAnd(m_state->context.lock(), [&]() { m_state->Connect(destination); });
+ }
+ return info[0];
+ }
+
+ Napi::Value NativeAnalyserNode::Disconnect(const Napi::CallbackInfo& info)
+ {
+ if (!m_state)
+ {
+ return info.Env().Undefined();
+ }
+
+ if (info.Length() > 0)
+ {
+ auto destination = UnwrapNodeState(info[0]);
+ LockAnd(m_state->context.lock(), [&]() { m_state->Disconnect(destination); });
+ }
+ else
+ {
+ LockAnd(m_state->context.lock(), [&]() { m_state->Disconnect(); });
+ }
+ return info.Env().Undefined();
+ }
+
+ void NativeAnalyserNode::GetByteFrequencyData(const Napi::CallbackInfo& info)
+ {
+ if (!m_state || info.Length() == 0 || !info[0].IsTypedArray())
+ {
+ return;
+ }
+
+ auto array = info[0].As();
+ std::vector magnitudesDb;
+ LockAnd(m_state->context.lock(), [&]() { m_state->ComputeFrequencyData(magnitudesDb); });
+
+ const size_t count = std::min(array.ElementLength(), magnitudesDb.size());
+ const float dbRange = m_state->maxDecibels - m_state->minDecibels;
+ for (size_t index = 0; index < count; ++index)
+ {
+ const float normalized = dbRange > 0.f ? (magnitudesDb[index] - m_state->minDecibels) / dbRange : 0.f;
+ array[index] = static_cast(std::round(std::max(0.f, std::min(1.f, normalized)) * 255.f));
+ }
+ }
+
+ void NativeAnalyserNode::GetFloatFrequencyData(const Napi::CallbackInfo& info)
+ {
+ if (!m_state || info.Length() == 0 || !info[0].IsTypedArray())
+ {
+ return;
+ }
+
+ auto array = info[0].As();
+ std::vector magnitudesDb;
+ LockAnd(m_state->context.lock(), [&]() { m_state->ComputeFrequencyData(magnitudesDb); });
+
+ const size_t count = std::min(array.ElementLength(), magnitudesDb.size());
+ for (size_t index = 0; index < count; ++index)
+ {
+ array[index] = magnitudesDb[index];
+ }
+ }
+
+ void NativeAnalyserNode::GetByteTimeDomainData(const Napi::CallbackInfo& info)
+ {
+ if (!m_state || info.Length() == 0 || !info[0].IsTypedArray())
+ {
+ return;
+ }
+
+ auto array = info[0].As();
+ std::vector snapshot;
+ LockAnd(m_state->context.lock(), [&]() { m_state->CopyTimeDomainSnapshot(snapshot); });
+
+ const size_t count = std::min(array.ElementLength(), snapshot.size());
+ for (size_t index = 0; index < count; ++index)
+ {
+ const float clamped = std::max(-1.f, std::min(1.f, snapshot[index]));
+ array[index] = static_cast(std::round((clamped + 1.f) * 128.f));
+ }
+ }
+
+ void NativeAnalyserNode::GetFloatTimeDomainData(const Napi::CallbackInfo& info)
+ {
+ if (!m_state || info.Length() == 0 || !info[0].IsTypedArray())
+ {
+ return;
+ }
+
+ auto array = info[0].As();
+ std::vector snapshot;
+ LockAnd(m_state->context.lock(), [&]() { m_state->CopyTimeDomainSnapshot(snapshot); });
+
+ const size_t count = std::min(array.ElementLength(), snapshot.size());
+ for (size_t index = 0; index < count; ++index)
+ {
+ array[index] = snapshot[index];
+ }
+ }
+
+ Napi::Value NativeAnalyserNode::GetFftSize(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->fftSize : 2048);
+ }
+
+ void NativeAnalyserNode::SetFftSize(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ LockAnd(m_state->context.lock(), [&]() { m_state->SetFftSize(value.ToNumber().Uint32Value()); });
+ }
+ }
+
+ Napi::Value NativeAnalyserNode::GetFrequencyBinCount(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->FrequencyBinCount() : 1024);
+ }
+
+ Napi::Value NativeAnalyserNode::GetMinDecibels(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->minDecibels : -100.f);
+ }
+
+ void NativeAnalyserNode::SetMinDecibels(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->minDecibels = value.ToNumber().FloatValue();
+ }
+ }
+
+ Napi::Value NativeAnalyserNode::GetMaxDecibels(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->maxDecibels : -30.f);
+ }
+
+ void NativeAnalyserNode::SetMaxDecibels(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->maxDecibels = value.ToNumber().FloatValue();
+ }
+ }
+
+ Napi::Value NativeAnalyserNode::GetSmoothingTimeConstant(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->smoothingTimeConstant : 0.8f);
+ }
+
+ void NativeAnalyserNode::SetSmoothingTimeConstant(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->smoothingTimeConstant = value.ToNumber().FloatValue();
+ }
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AnalyserNode.h b/Polyfills/WebAudio/Source/AnalyserNode.h
new file mode 100644
index 000000000..1ac82c416
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AnalyserNode.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "AudioNode.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAnalyserNode final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+ static Napi::Object CreateInstance(Napi::Env env, const std::shared_ptr& context);
+
+ explicit NativeAnalyserNode(const Napi::CallbackInfo& info);
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AnalyserNode";
+
+ void InitializeState(const std::shared_ptr& context);
+
+ Napi::Value Connect(const Napi::CallbackInfo& info);
+ Napi::Value Disconnect(const Napi::CallbackInfo& info);
+ void GetByteFrequencyData(const Napi::CallbackInfo& info);
+ void GetFloatFrequencyData(const Napi::CallbackInfo& info);
+ void GetByteTimeDomainData(const Napi::CallbackInfo& info);
+ void GetFloatTimeDomainData(const Napi::CallbackInfo& info);
+
+ Napi::Value GetFftSize(const Napi::CallbackInfo& info);
+ void SetFftSize(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetFrequencyBinCount(const Napi::CallbackInfo& info);
+ Napi::Value GetMinDecibels(const Napi::CallbackInfo& info);
+ void SetMinDecibels(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetMaxDecibels(const Napi::CallbackInfo& info);
+ void SetMaxDecibels(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetSmoothingTimeConstant(const Napi::CallbackInfo& info);
+ void SetSmoothingTimeConstant(const Napi::CallbackInfo& info, const Napi::Value& value);
+
+ std::shared_ptr m_state{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioBuffer.cpp b/Polyfills/WebAudio/Source/AudioBuffer.cpp
new file mode 100644
index 000000000..462b120f9
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioBuffer.cpp
@@ -0,0 +1,128 @@
+#include "AudioBuffer.h"
+
+#include
+
+#include
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ void NativeAudioBuffer::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceAccessor("sampleRate", &NativeAudioBuffer::GetSampleRate, nullptr),
+ InstanceAccessor("length", &NativeAudioBuffer::GetLength, nullptr),
+ InstanceAccessor("duration", &NativeAudioBuffer::GetDuration, nullptr),
+ InstanceAccessor("numberOfChannels", &NativeAudioBuffer::GetNumberOfChannels, nullptr),
+ InstanceMethod("getChannelData", &NativeAudioBuffer::GetChannelData),
+ InstanceMethod("copyFromChannel", &NativeAudioBuffer::CopyFromChannel),
+ InstanceMethod("copyToChannel", &NativeAudioBuffer::CopyToChannel),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set(JS_CONSTRUCTOR_NAME, func);
+ }
+
+ Napi::Object NativeAudioBuffer::CreateInstance(Napi::Env env, const std::shared_ptr& data)
+ {
+ auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CONSTRUCTOR_NAME).As();
+ auto object = func.New({});
+ auto* buffer = NativeAudioBuffer::Unwrap(object);
+ buffer->m_data = data;
+ return object;
+ }
+
+ NativeAudioBuffer::NativeAudioBuffer(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ , m_data{std::make_shared()}
+ {
+ if (info.Length() > 0 && info[0].IsObject())
+ {
+ auto options = info[0].As();
+ m_data->length = options.Get("length").ToNumber().Uint32Value();
+ m_data->numberOfChannels = options.Has("numberOfChannels") ? options.Get("numberOfChannels").ToNumber().Uint32Value() : 1;
+ m_data->sampleRate = options.Has("sampleRate") ? options.Get("sampleRate").ToNumber().Uint32Value() : 48000;
+ m_data->channels.assign(m_data->numberOfChannels, std::vector(m_data->length, 0.f));
+ }
+ }
+
+ std::shared_ptr NativeAudioBuffer::Data() const
+ {
+ return m_data;
+ }
+
+ Napi::Value NativeAudioBuffer::GetSampleRate(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_data ? m_data->sampleRate : 0);
+ }
+
+ Napi::Value NativeAudioBuffer::GetLength(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_data ? m_data->length : 0);
+ }
+
+ Napi::Value NativeAudioBuffer::GetDuration(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_data ? m_data->Duration() : 0.0);
+ }
+
+ Napi::Value NativeAudioBuffer::GetNumberOfChannels(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_data ? m_data->numberOfChannels : 0);
+ }
+
+ Napi::Value NativeAudioBuffer::GetChannelData(const Napi::CallbackInfo& info)
+ {
+ const uint32_t channel = info[0].As().Uint32Value();
+ if (!m_data || channel >= m_data->numberOfChannels)
+ {
+ throw Napi::Error::New(info.Env(), "IndexSizeError: channel index out of range");
+ }
+
+ auto& channelData = m_data->channels[channel];
+ return Napi::Float32Array::New(info.Env(), channelData.size(), Napi::ArrayBuffer::New(info.Env(), channelData.data(), channelData.size() * sizeof(float), [data = m_data](Napi::Env, void*) {}), 0);
+ }
+
+ void NativeAudioBuffer::CopyFromChannel(const Napi::CallbackInfo& info)
+ {
+ auto destination = info[0].As();
+ const uint32_t channel = info[1].As().Uint32Value();
+ const uint32_t startInChannel = info.Length() > 2 ? info[2].As().Uint32Value() : 0;
+
+ if (!m_data || channel >= m_data->numberOfChannels)
+ {
+ throw Napi::Error::New(info.Env(), "IndexSizeError: channel index out of range");
+ }
+
+ const auto& source = m_data->channels[channel];
+ const size_t count = std::min(static_cast(destination.ElementLength()), source.size() > startInChannel ? source.size() - startInChannel : 0);
+ if (count > 0)
+ {
+ std::memcpy(destination.Data(), source.data() + startInChannel, count * sizeof(float));
+ }
+ }
+
+ void NativeAudioBuffer::CopyToChannel(const Napi::CallbackInfo& info)
+ {
+ auto source = info[0].As();
+ const uint32_t channel = info[1].As().Uint32Value();
+ const uint32_t startInChannel = info.Length() > 2 ? info[2].As().Uint32Value() : 0;
+
+ if (!m_data || channel >= m_data->numberOfChannels)
+ {
+ throw Napi::Error::New(info.Env(), "IndexSizeError: channel index out of range");
+ }
+
+ auto& destination = m_data->channels[channel];
+ const size_t count = std::min(static_cast(source.ElementLength()), destination.size() > startInChannel ? destination.size() - startInChannel : 0);
+ if (count > 0)
+ {
+ std::memcpy(destination.data() + startInChannel, source.Data(), count * sizeof(float));
+ }
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioBuffer.h b/Polyfills/WebAudio/Source/AudioBuffer.h
new file mode 100644
index 000000000..4d015907c
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioBuffer.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "AudioNode.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAudioBuffer final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+ static Napi::Object CreateInstance(Napi::Env env, const std::shared_ptr& data);
+
+ explicit NativeAudioBuffer(const Napi::CallbackInfo& info);
+
+ std::shared_ptr Data() const;
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AudioBuffer";
+
+ Napi::Value GetSampleRate(const Napi::CallbackInfo& info);
+ Napi::Value GetLength(const Napi::CallbackInfo& info);
+ Napi::Value GetDuration(const Napi::CallbackInfo& info);
+ Napi::Value GetNumberOfChannels(const Napi::CallbackInfo& info);
+ Napi::Value GetChannelData(const Napi::CallbackInfo& info);
+ void CopyFromChannel(const Napi::CallbackInfo& info);
+ void CopyToChannel(const Napi::CallbackInfo& info);
+
+ std::shared_ptr m_data{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioBufferSourceNode.cpp b/Polyfills/WebAudio/Source/AudioBufferSourceNode.cpp
new file mode 100644
index 000000000..3c839ffeb
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioBufferSourceNode.cpp
@@ -0,0 +1,345 @@
+#include "AudioBufferSourceNode.h"
+#include "AudioBuffer.h"
+#include "AudioContext.h"
+#include "AudioEngine.h"
+#include "AudioParam.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ namespace
+ {
+ void LockAnd(const std::shared_ptr& context, auto&& fn)
+ {
+ if (context && context->engine)
+ {
+ std::lock_guard lock{context->engine->GraphMutex()};
+ fn();
+ }
+ else
+ {
+ fn();
+ }
+ }
+ }
+
+ void NativeAudioBufferSourceNode::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceMethod("connect", &NativeAudioBufferSourceNode::Connect),
+ InstanceMethod("disconnect", &NativeAudioBufferSourceNode::Disconnect),
+ InstanceMethod("start", &NativeAudioBufferSourceNode::Start),
+ InstanceMethod("stop", &NativeAudioBufferSourceNode::Stop),
+ InstanceMethod("addEventListener", &NativeAudioBufferSourceNode::AddEventListener),
+ InstanceMethod("removeEventListener", &NativeAudioBufferSourceNode::RemoveEventListener),
+ InstanceAccessor("buffer", &NativeAudioBufferSourceNode::GetBuffer, &NativeAudioBufferSourceNode::SetBuffer),
+ InstanceAccessor("loop", &NativeAudioBufferSourceNode::GetLoop, &NativeAudioBufferSourceNode::SetLoop),
+ InstanceAccessor("loopStart", &NativeAudioBufferSourceNode::GetLoopStart, &NativeAudioBufferSourceNode::SetLoopStart),
+ InstanceAccessor("loopEnd", &NativeAudioBufferSourceNode::GetLoopEnd, &NativeAudioBufferSourceNode::SetLoopEnd),
+ InstanceAccessor("playbackRate", &NativeAudioBufferSourceNode::GetPlaybackRate, nullptr),
+ InstanceAccessor("detune", &NativeAudioBufferSourceNode::GetDetune, nullptr),
+ InstanceAccessor("onended", &NativeAudioBufferSourceNode::GetOnEnded, &NativeAudioBufferSourceNode::SetOnEnded),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set(JS_CONSTRUCTOR_NAME, func);
+ }
+
+ Napi::Object NativeAudioBufferSourceNode::CreateInstance(Napi::Env env, const std::shared_ptr& context)
+ {
+ auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CONSTRUCTOR_NAME).As();
+ auto object = func.New({});
+ NativeAudioBufferSourceNode::Unwrap(object)->InitializeState(context, env.Undefined());
+ return object;
+ }
+
+ struct NativeAudioBufferSourceNode::EndedDispatcher : std::enable_shared_from_this
+ {
+ explicit EndedDispatcher(Napi::Object jsThis)
+ : jsThis{Napi::Persistent(jsThis)}
+ {
+ }
+
+ void NotifyEnded(const std::shared_ptr& engine)
+ {
+ if (!engine)
+ {
+ return;
+ }
+
+ engine->ScheduleOnJsThread([self = shared_from_this()]() {
+ if (self->jsThis.IsEmpty())
+ {
+ return;
+ }
+
+ auto* node = NativeAudioBufferSourceNode::Unwrap(self->jsThis.Value());
+ if (node)
+ {
+ node->FireEnded();
+ }
+ });
+ }
+
+ Napi::ObjectReference jsThis{};
+ };
+
+ NativeAudioBufferSourceNode::NativeAudioBufferSourceNode(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ {
+ m_endedDispatcher = std::make_shared(info.This().As());
+
+ if (info.Length() > 0 && info[0].IsObject())
+ {
+ auto* context = NativeAudioContext::Unwrap(info[0].As());
+ if (context)
+ {
+ info.This().As().Set("context", info[0]);
+ InitializeState(context->State(), info.Length() > 1 ? info[1] : info.Env().Undefined());
+ }
+ }
+ }
+
+ NativeAudioBufferSourceNode::~NativeAudioBufferSourceNode()
+ {
+ if (m_state)
+ {
+ auto context = m_state->context.lock();
+ if (context && context->engine)
+ {
+ context->engine->UnregisterSource(m_state.get());
+ }
+ m_state->onEnded = nullptr;
+ }
+
+ if (m_endedDispatcher)
+ {
+ m_endedDispatcher->jsThis.Reset();
+ }
+ }
+
+ void NativeAudioBufferSourceNode::InitializeState(const std::shared_ptr& context, const Napi::Value& options)
+ {
+ m_state = std::make_shared();
+ m_state->context = context;
+ BindNodeState(Value(), m_state);
+ SetAudioNodeProperties(Value(), 0, 1);
+
+ auto getCurrentTime = MakeCurrentTimeGetter(context);
+ m_playbackRate = Napi::Persistent(NativeAudioParam::CreateInstance(Env(), std::shared_ptr(m_state, &m_state->playbackRate), 1.f, getCurrentTime));
+ m_detune = Napi::Persistent(NativeAudioParam::CreateInstance(Env(), std::shared_ptr(m_state, &m_state->detune), 0.f, getCurrentTime));
+
+ if (options.IsObject())
+ {
+ auto optionsObject = options.As();
+ if (optionsObject.Has("buffer") && optionsObject.Get("buffer").IsObject())
+ {
+ auto bufferValue = optionsObject.Get("buffer");
+ auto* buffer = NativeAudioBuffer::Unwrap(bufferValue.As());
+ m_buffer = Napi::Persistent(bufferValue.As());
+ m_state->buffer = buffer ? buffer->Data() : nullptr;
+ }
+ if (optionsObject.Has("loop"))
+ {
+ m_state->loop = optionsObject.Get("loop").ToBoolean();
+ }
+ if (optionsObject.Has("loopStart"))
+ {
+ m_state->loopStart = optionsObject.Get("loopStart").ToNumber().DoubleValue();
+ }
+ if (optionsObject.Has("loopEnd"))
+ {
+ m_state->loopEnd = optionsObject.Get("loopEnd").ToNumber().DoubleValue();
+ }
+ }
+
+ if (context && context->engine)
+ {
+ context->engine->RegisterSource(m_state);
+ }
+
+ if (!m_endedDispatcher)
+ {
+ m_endedDispatcher = std::make_shared(Value());
+ }
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::Connect(const Napi::CallbackInfo& info)
+ {
+ auto destination = UnwrapNodeState(info[0]);
+ if (m_state && destination)
+ {
+ LockAnd(m_state->context.lock(), [&]() { m_state->Connect(destination); });
+ }
+ return info[0];
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::Disconnect(const Napi::CallbackInfo& info)
+ {
+ if (!m_state)
+ {
+ return info.Env().Undefined();
+ }
+
+ if (info.Length() > 0)
+ {
+ auto destination = UnwrapNodeState(info[0]);
+ LockAnd(m_state->context.lock(), [&]() { m_state->Disconnect(destination); });
+ }
+ else
+ {
+ LockAnd(m_state->context.lock(), [&]() { m_state->Disconnect(); });
+ }
+ return info.Env().Undefined();
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetBuffer(const Napi::CallbackInfo& info)
+ {
+ return m_buffer.IsEmpty() ? info.Env().Null() : m_buffer.Value();
+ }
+
+ void NativeAudioBufferSourceNode::SetBuffer(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (!m_state)
+ {
+ return;
+ }
+
+ if (value.IsNull() || value.IsUndefined())
+ {
+ m_buffer.Reset();
+ m_state->buffer.reset();
+ return;
+ }
+
+ auto* buffer = NativeAudioBuffer::Unwrap(value.As());
+ m_buffer = Napi::Persistent(value.As());
+ m_state->buffer = buffer ? buffer->Data() : nullptr;
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetLoop(const Napi::CallbackInfo& info)
+ {
+ return Napi::Boolean::New(info.Env(), m_state && m_state->loop);
+ }
+
+ void NativeAudioBufferSourceNode::SetLoop(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->loop = value.ToBoolean();
+ }
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetLoopStart(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->loopStart : 0.0);
+ }
+
+ void NativeAudioBufferSourceNode::SetLoopStart(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->loopStart = value.ToNumber().DoubleValue();
+ }
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetLoopEnd(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), m_state ? m_state->loopEnd : 0.0);
+ }
+
+ void NativeAudioBufferSourceNode::SetLoopEnd(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (m_state)
+ {
+ m_state->loopEnd = value.ToNumber().DoubleValue();
+ }
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetPlaybackRate(const Napi::CallbackInfo&)
+ {
+ return m_playbackRate.Value();
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetDetune(const Napi::CallbackInfo&)
+ {
+ return m_detune.Value();
+ }
+
+ Napi::Value NativeAudioBufferSourceNode::GetOnEnded(const Napi::CallbackInfo& info)
+ {
+ return m_onended.IsEmpty() ? info.Env().Null() : m_onended.Value();
+ }
+
+ void NativeAudioBufferSourceNode::SetOnEnded(const Napi::CallbackInfo&, const Napi::Value& value)
+ {
+ if (value.IsFunction())
+ {
+ m_onended = Napi::Persistent(value.As());
+ }
+ else
+ {
+ m_onended.Reset();
+ }
+ }
+
+ void NativeAudioBufferSourceNode::FireEnded()
+ {
+ m_eventTarget.DispatchEvent(Env(), "ended");
+ if (!m_onended.IsEmpty())
+ {
+ m_onended.Call({});
+ }
+ }
+
+ void NativeAudioBufferSourceNode::Start(const Napi::CallbackInfo& info)
+ {
+ if (!m_state)
+ {
+ return;
+ }
+
+ const double when = info.Length() > 0 ? info[0].ToNumber().DoubleValue() : 0.0;
+ const double offset = info.Length() > 1 ? info[1].ToNumber().DoubleValue() : 0.0;
+ const double duration = info.Length() > 2 ? info[2].ToNumber().DoubleValue() : -1.0;
+
+ auto context = m_state->context.lock();
+ auto engine = context ? context->engine : nullptr;
+ auto dispatcher = m_endedDispatcher;
+
+ m_state->onEnded = [engine, dispatcher]() {
+ if (dispatcher)
+ {
+ dispatcher->NotifyEnded(engine);
+ }
+ };
+
+ LockAnd(context, [&]() { m_state->Start(when, offset, duration); });
+ }
+
+ void NativeAudioBufferSourceNode::Stop(const Napi::CallbackInfo&)
+ {
+ if (!m_state)
+ {
+ return;
+ }
+
+ LockAnd(m_state->context.lock(), [&]() { m_state->Stop(); });
+ }
+
+ void NativeAudioBufferSourceNode::AddEventListener(const Napi::CallbackInfo& info)
+ {
+ m_eventTarget.AddEventListener(info);
+ }
+
+ void NativeAudioBufferSourceNode::RemoveEventListener(const Napi::CallbackInfo& info)
+ {
+ m_eventTarget.RemoveEventListener(info);
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioBufferSourceNode.h b/Polyfills/WebAudio/Source/AudioBufferSourceNode.h
new file mode 100644
index 000000000..324d86e94
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioBufferSourceNode.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "AudioNode.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAudioBufferSourceNode final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+ static Napi::Object CreateInstance(Napi::Env env, const std::shared_ptr& context);
+
+ explicit NativeAudioBufferSourceNode(const Napi::CallbackInfo& info);
+ ~NativeAudioBufferSourceNode() override;
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AudioBufferSourceNode";
+
+ void InitializeState(const std::shared_ptr& context, const Napi::Value& options);
+
+ Napi::Value Connect(const Napi::CallbackInfo& info);
+ Napi::Value Disconnect(const Napi::CallbackInfo& info);
+ Napi::Value GetBuffer(const Napi::CallbackInfo& info);
+ void SetBuffer(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetLoop(const Napi::CallbackInfo& info);
+ void SetLoop(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetLoopStart(const Napi::CallbackInfo& info);
+ void SetLoopStart(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetLoopEnd(const Napi::CallbackInfo& info);
+ void SetLoopEnd(const Napi::CallbackInfo& info, const Napi::Value& value);
+ Napi::Value GetPlaybackRate(const Napi::CallbackInfo& info);
+ Napi::Value GetDetune(const Napi::CallbackInfo& info);
+ Napi::Value GetOnEnded(const Napi::CallbackInfo& info);
+ void SetOnEnded(const Napi::CallbackInfo& info, const Napi::Value& value);
+ void Start(const Napi::CallbackInfo& info);
+ void Stop(const Napi::CallbackInfo& info);
+ void AddEventListener(const Napi::CallbackInfo& info);
+ void RemoveEventListener(const Napi::CallbackInfo& info);
+
+ void FireEnded();
+
+ struct EndedDispatcher;
+
+ std::shared_ptr m_state{};
+ std::shared_ptr m_endedDispatcher{};
+ Napi::ObjectReference m_buffer{};
+ Napi::ObjectReference m_playbackRate{};
+ Napi::ObjectReference m_detune{};
+ Napi::FunctionReference m_onended{};
+ EventTargetSupport m_eventTarget{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioContext.cpp b/Polyfills/WebAudio/Source/AudioContext.cpp
new file mode 100644
index 000000000..42abed72a
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioContext.cpp
@@ -0,0 +1,309 @@
+#include "AudioContext.h"
+#include "AnalyserNode.h"
+#include "AudioBuffer.h"
+#include "AudioBufferSourceNode.h"
+#include "AudioDestinationNode.h"
+#include "AudioEngine.h"
+#include "AudioListener.h"
+#include "GainNode.h"
+#include "PannerNode.h"
+#include "StereoPannerNode.h"
+
+#include
+#include
+
+#include
+#include
+
+#include
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ void NativeAudioContext::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceAccessor("destination", &NativeAudioContext::GetDestination, nullptr),
+ InstanceAccessor("listener", &NativeAudioContext::GetListener, nullptr),
+ InstanceAccessor("state", &NativeAudioContext::GetState, nullptr),
+ InstanceAccessor("sampleRate", &NativeAudioContext::GetSampleRate, nullptr),
+ InstanceAccessor("currentTime", &NativeAudioContext::GetCurrentTime, nullptr),
+ InstanceMethod("resume", &NativeAudioContext::Resume),
+ InstanceMethod("suspend", &NativeAudioContext::Suspend),
+ InstanceMethod("close", &NativeAudioContext::Close),
+ InstanceMethod("decodeAudioData", &NativeAudioContext::DecodeAudioData),
+ InstanceMethod("createGain", &NativeAudioContext::CreateGain),
+ InstanceMethod("createBufferSource", &NativeAudioContext::CreateBufferSource),
+ InstanceMethod("createPanner", &NativeAudioContext::CreatePanner),
+ InstanceMethod("createStereoPanner", &NativeAudioContext::CreateStereoPanner),
+ InstanceMethod("createAnalyser", &NativeAudioContext::CreateAnalyser),
+ InstanceMethod("createBuffer", &NativeAudioContext::CreateBuffer),
+ InstanceMethod("addEventListener", &NativeAudioContext::AddEventListener),
+ InstanceMethod("removeEventListener", &NativeAudioContext::RemoveEventListener),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set("webkitAudioContext", func);
+ }
+
+ NativeAudioContext::NativeAudioContext(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ {
+ InitializeContext();
+ }
+
+ NativeAudioContext::~NativeAudioContext()
+ {
+ if (m_state && m_state->engine)
+ {
+ m_state->engine->UnregisterContext(m_state.get());
+ }
+ }
+
+ void NativeAudioContext::InitializeContext()
+ {
+ m_state = std::make_shared();
+ m_state->engine = GetAudioEngineFromJavaScript(Env());
+ m_state->destination = std::make_shared();
+ m_state->destination->context = m_state;
+ m_state->running.store(false);
+ m_state->state = "suspended";
+
+ if (m_state->engine)
+ {
+ m_state->engine->RegisterContext(m_state);
+ }
+
+ m_destination = Napi::Persistent(NativeAudioDestinationNode::CreateInstance(Env(), std::static_pointer_cast(m_state->destination)));
+ m_listener = Napi::Persistent(NativeAudioListener::CreateInstance(Env(), m_state->listener, MakeCurrentTimeGetter(m_state)));
+ }
+
+ std::shared_ptr NativeAudioContext::State() const
+ {
+ return m_state;
+ }
+
+ Napi::Value NativeAudioContext::GetDestination(const Napi::CallbackInfo&)
+ {
+ return m_destination.Value();
+ }
+
+ Napi::Value NativeAudioContext::GetListener(const Napi::CallbackInfo&)
+ {
+ return m_listener.Value();
+ }
+
+ Napi::Value NativeAudioContext::GetState(const Napi::CallbackInfo& info)
+ {
+ return Napi::String::New(info.Env(), m_state ? m_state->state : "closed");
+ }
+
+ Napi::Value NativeAudioContext::GetSampleRate(const Napi::CallbackInfo& info)
+ {
+ const uint32_t sampleRate = (m_state && m_state->engine) ? m_state->engine->SampleRate() : 48000;
+ return Napi::Number::New(info.Env(), sampleRate);
+ }
+
+ Napi::Value NativeAudioContext::GetCurrentTime(const Napi::CallbackInfo& info)
+ {
+ const double currentTime = (m_state && m_state->engine) ? m_state->engine->CurrentTime() : 0.0;
+ return Napi::Number::New(info.Env(), currentTime);
+ }
+
+ void NativeAudioContext::SetState(const std::string& state)
+ {
+ if (!m_state || m_state->state == state)
+ {
+ return;
+ }
+
+ m_state->state = state;
+ m_state->running.store(state == "running");
+ if (m_state->engine)
+ {
+ m_state->engine->OnContextRunningChanged();
+ }
+ m_eventTarget.DispatchEvent(Env(), "statechange");
+ }
+
+ Napi::Value NativeAudioContext::Resume(const Napi::CallbackInfo& info)
+ {
+ auto deferred = Napi::Promise::Deferred::New(info.Env());
+ if (m_state && m_state->state != "closed")
+ {
+ if (m_state->engine && m_state->engine->EnsureDeviceRunning())
+ {
+ SetState("running");
+ }
+ else
+ {
+ SetState("suspended");
+ }
+ }
+ deferred.Resolve(info.Env().Undefined());
+ return deferred.Promise();
+ }
+
+ Napi::Value NativeAudioContext::Suspend(const Napi::CallbackInfo& info)
+ {
+ auto deferred = Napi::Promise::Deferred::New(info.Env());
+ if (m_state && m_state->state != "closed")
+ {
+ SetState("suspended");
+ }
+ deferred.Resolve(info.Env().Undefined());
+ return deferred.Promise();
+ }
+
+ Napi::Value NativeAudioContext::Close(const Napi::CallbackInfo& info)
+ {
+ auto deferred = Napi::Promise::Deferred::New(info.Env());
+ if (m_state)
+ {
+ SetState("closed");
+ if (m_state->engine)
+ {
+ m_state->engine->UnregisterContext(m_state.get());
+ }
+ }
+ deferred.Resolve(info.Env().Undefined());
+ return deferred.Promise();
+ }
+
+ Napi::Value NativeAudioContext::DecodeAudioData(const Napi::CallbackInfo& info)
+ {
+ auto env = info.Env();
+ auto deferred = Napi::Promise::Deferred::New(env);
+
+ if (info.Length() < 1 || !info[0].IsArrayBuffer())
+ {
+ deferred.Reject(Napi::Error::New(env, "decodeAudioData requires an ArrayBuffer").Value());
+ return deferred.Promise();
+ }
+
+ auto arrayBuffer = info[0].As();
+ std::vector bytes(static_cast(arrayBuffer.Data()), static_cast(arrayBuffer.Data()) + arrayBuffer.ByteLength());
+
+ auto runtimeScheduler = std::make_shared(JsRuntime::GetFromJavaScript(env));
+
+ arcana::make_task(arcana::threadpool_scheduler, arcana::cancellation::none(), [bytes = std::move(bytes)]() {
+ auto data = std::make_shared();
+
+ ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);
+ ma_decoder decoder;
+ if (ma_decoder_init_memory(bytes.data(), bytes.size(), &config, &decoder) != MA_SUCCESS)
+ {
+ throw std::runtime_error("Unable to decode audio data");
+ }
+
+ ma_uint64 frameCount = 0;
+ if (ma_decoder_get_length_in_pcm_frames(&decoder, &frameCount) != MA_SUCCESS)
+ {
+ ma_decoder_uninit(&decoder);
+ throw std::runtime_error("Unable to read decoded audio length");
+ }
+
+ const uint32_t channels = decoder.outputChannels;
+ const uint32_t sampleRate = decoder.outputSampleRate;
+ std::vector interleaved(static_cast(frameCount) * channels);
+ ma_uint64 framesRead = 0;
+ if (ma_decoder_read_pcm_frames(&decoder, interleaved.data(), frameCount, &framesRead) != MA_SUCCESS)
+ {
+ ma_decoder_uninit(&decoder);
+ throw std::runtime_error("Unable to read decoded audio frames");
+ }
+ ma_decoder_uninit(&decoder);
+
+ data->sampleRate = sampleRate;
+ data->numberOfChannels = channels;
+ data->length = static_cast(framesRead);
+ data->channels.assign(channels, std::vector(data->length));
+
+ for (uint32_t frame = 0; frame < data->length; ++frame)
+ {
+ for (uint32_t channel = 0; channel < channels; ++channel)
+ {
+ data->channels[channel][frame] = interleaved[frame * channels + channel];
+ }
+ }
+
+ return data;
+ }).then(*runtimeScheduler, arcana::cancellation::none(), [env, deferred, runtimeScheduler](const arcana::expected, std::exception_ptr>& result) {
+ if (result.has_error())
+ {
+ deferred.Reject(Napi::Error::New(env, result.error()).Value());
+ return;
+ }
+
+ deferred.Resolve(NativeAudioBuffer::CreateInstance(env, result.value()));
+ });
+
+ return deferred.Promise();
+ }
+
+ Napi::Value NativeAudioContext::CreateGain(const Napi::CallbackInfo& info)
+ {
+ auto object = NativeGainNode::CreateInstance(info.Env(), m_state);
+ object.Set("context", info.This());
+ return object;
+ }
+
+ Napi::Value NativeAudioContext::CreateBufferSource(const Napi::CallbackInfo& info)
+ {
+ auto object = NativeAudioBufferSourceNode::CreateInstance(info.Env(), m_state);
+ object.Set("context", info.This());
+ return object;
+ }
+
+ Napi::Value NativeAudioContext::CreatePanner(const Napi::CallbackInfo& info)
+ {
+ auto object = NativePannerNode::CreateInstance(info.Env(), m_state);
+ object.Set("context", info.This());
+ return object;
+ }
+
+ Napi::Value NativeAudioContext::CreateStereoPanner(const Napi::CallbackInfo& info)
+ {
+ auto object = NativeStereoPannerNode::CreateInstance(info.Env(), m_state);
+ object.Set("context", info.This());
+ return object;
+ }
+
+ Napi::Value NativeAudioContext::CreateAnalyser(const Napi::CallbackInfo& info)
+ {
+ auto object = NativeAnalyserNode::CreateInstance(info.Env(), m_state);
+ object.Set("context", info.This());
+ return object;
+ }
+
+ Napi::Value NativeAudioContext::CreateBuffer(const Napi::CallbackInfo& info)
+ {
+ const uint32_t numberOfChannels = info[0].As().Uint32Value();
+ const uint32_t length = info[1].As().Uint32Value();
+ const uint32_t sampleRate = info[2].As().Uint32Value();
+
+ auto data = std::make_shared();
+ data->numberOfChannels = numberOfChannels;
+ data->length = length;
+ data->sampleRate = sampleRate;
+ data->channels.assign(numberOfChannels, std::vector(length, 0.f));
+ return NativeAudioBuffer::CreateInstance(info.Env(), data);
+ }
+
+ void NativeAudioContext::AddEventListener(const Napi::CallbackInfo& info)
+ {
+ m_eventTarget.AddEventListener(info);
+ }
+
+ void NativeAudioContext::RemoveEventListener(const Napi::CallbackInfo& info)
+ {
+ m_eventTarget.RemoveEventListener(info);
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioContext.h b/Polyfills/WebAudio/Source/AudioContext.h
new file mode 100644
index 000000000..484ef67c7
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioContext.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "AudioNode.h"
+#include "AudioEngine.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAudioContext final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+
+ explicit NativeAudioContext(const Napi::CallbackInfo& info);
+ ~NativeAudioContext() override;
+
+ std::shared_ptr State() const;
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AudioContext";
+
+ void InitializeContext();
+
+ Napi::Value GetDestination(const Napi::CallbackInfo& info);
+ Napi::Value GetListener(const Napi::CallbackInfo& info);
+ Napi::Value GetState(const Napi::CallbackInfo& info);
+ Napi::Value GetSampleRate(const Napi::CallbackInfo& info);
+ Napi::Value GetCurrentTime(const Napi::CallbackInfo& info);
+ Napi::Value Resume(const Napi::CallbackInfo& info);
+ Napi::Value Suspend(const Napi::CallbackInfo& info);
+ Napi::Value Close(const Napi::CallbackInfo& info);
+ Napi::Value DecodeAudioData(const Napi::CallbackInfo& info);
+ Napi::Value CreateGain(const Napi::CallbackInfo& info);
+ Napi::Value CreateBufferSource(const Napi::CallbackInfo& info);
+ Napi::Value CreatePanner(const Napi::CallbackInfo& info);
+ Napi::Value CreateStereoPanner(const Napi::CallbackInfo& info);
+ Napi::Value CreateAnalyser(const Napi::CallbackInfo& info);
+ Napi::Value CreateBuffer(const Napi::CallbackInfo& info);
+ void AddEventListener(const Napi::CallbackInfo& info);
+ void RemoveEventListener(const Napi::CallbackInfo& info);
+
+ void SetState(const std::string& state);
+
+ std::shared_ptr m_state{};
+ Napi::ObjectReference m_destination{};
+ Napi::ObjectReference m_listener{};
+ EventTargetSupport m_eventTarget{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioDestinationNode.cpp b/Polyfills/WebAudio/Source/AudioDestinationNode.cpp
new file mode 100644
index 000000000..9a508977f
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioDestinationNode.cpp
@@ -0,0 +1,61 @@
+#include "AudioDestinationNode.h"
+#include "AudioEngine.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ void NativeAudioDestinationNode::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceMethod("connect", &NativeAudioDestinationNode::Connect),
+ InstanceMethod("disconnect", &NativeAudioDestinationNode::Disconnect),
+ InstanceAccessor("maxChannelCount", &NativeAudioDestinationNode::GetMaxChannelCount, nullptr),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ env.Global().Set(JS_CONSTRUCTOR_NAME, func);
+ }
+
+ Napi::Object NativeAudioDestinationNode::CreateInstance(Napi::Env env, const std::shared_ptr& state)
+ {
+ auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CONSTRUCTOR_NAME).As();
+ auto object = func.New({});
+ auto* self = NativeAudioDestinationNode::Unwrap(object);
+ self->m_state = state;
+ BindNodeState(object, state);
+ SetAudioNodeProperties(object, 1, 0);
+ object.Set("channelCount", 2);
+ return object;
+ }
+
+ NativeAudioDestinationNode::NativeAudioDestinationNode(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ {
+ }
+
+ std::shared_ptr NativeAudioDestinationNode::State() const
+ {
+ return m_state;
+ }
+
+ Napi::Value NativeAudioDestinationNode::Connect(const Napi::CallbackInfo& info)
+ {
+ return info[0];
+ }
+
+ Napi::Value NativeAudioDestinationNode::Disconnect(const Napi::CallbackInfo& info)
+ {
+ return info.Env().Undefined();
+ }
+
+ Napi::Value NativeAudioDestinationNode::GetMaxChannelCount(const Napi::CallbackInfo& info)
+ {
+ return Napi::Number::New(info.Env(), 2);
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioDestinationNode.h b/Polyfills/WebAudio/Source/AudioDestinationNode.h
new file mode 100644
index 000000000..a3d2119e3
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioDestinationNode.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "AudioNode.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAudioDestinationNode final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+ static Napi::Object CreateInstance(Napi::Env env, const std::shared_ptr& state);
+
+ explicit NativeAudioDestinationNode(const Napi::CallbackInfo& info);
+
+ std::shared_ptr State() const;
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AudioDestinationNode";
+
+ Napi::Value Connect(const Napi::CallbackInfo& info);
+ Napi::Value Disconnect(const Napi::CallbackInfo& info);
+ Napi::Value GetMaxChannelCount(const Napi::CallbackInfo& info);
+
+ std::shared_ptr m_state{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioEngine.cpp b/Polyfills/WebAudio/Source/AudioEngine.cpp
new file mode 100644
index 000000000..4598e98b9
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioEngine.cpp
@@ -0,0 +1,454 @@
+#include "AudioEngine.h"
+#include "AudioNode.h"
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ namespace
+ {
+ constexpr uint32_t OUTPUT_CHANNEL_COUNT = 2;
+ constexpr uint32_t NULL_BACKEND_FRAME_COUNT = 256;
+ constexpr const char* NULL_BACKEND_ENV = "BABYLON_NATIVE_WEBAUDIO_BACKEND";
+
+ bool UseNullBackend()
+ {
+ const char* value = std::getenv(NULL_BACKEND_ENV);
+ return value != nullptr && std::strcmp(value, "null") == 0;
+ }
+ }
+
+ std::shared_ptr AudioEngine::Create(JsRuntime& runtime)
+ {
+ auto engine = std::shared_ptr(new AudioEngine(runtime));
+ Babylon::Polyfills::Internal::RegisterAudioEngine(engine);
+ return engine;
+ }
+
+ AudioEngine::AudioEngine(JsRuntime& runtime)
+ : m_runtimeScheduler{runtime}
+ {
+ }
+
+ AudioEngine::~AudioEngine()
+ {
+ PrepareForShutdown();
+ }
+
+ void AudioEngine::PrepareForShutdown()
+ {
+ if (m_shuttingDown.exchange(true))
+ {
+ return;
+ }
+
+ UnregisterAudioEngine(this);
+ SetClockSuspended(true);
+ SuspendDevice();
+ {
+ std::lock_guard lock{m_graphMutex};
+ m_sources.clear();
+ }
+ ShutdownDevice();
+ }
+
+ bool AudioEngine::InitializeDevice()
+ {
+ if (m_shuttingDown.load(std::memory_order_relaxed))
+ {
+ return false;
+ }
+
+ std::lock_guard lock{m_deviceMutex};
+ if (m_deviceInitialized.load(std::memory_order_relaxed))
+ {
+ return true;
+ }
+
+ const bool forceNull = UseNullBackend();
+ if (forceNull)
+ {
+ m_usingNullBackend.store(true, std::memory_order_relaxed);
+ m_sampleRate = 48000;
+ m_deviceInitialized.store(true, std::memory_order_relaxed);
+ return true;
+ }
+
+ m_device = std::make_unique();
+
+ ma_device_config config = ma_device_config_init(ma_device_type_playback);
+ config.playback.format = ma_format_f32;
+ config.playback.channels = OUTPUT_CHANNEL_COUNT;
+ config.dataCallback = DataCallback;
+ config.pUserData = this;
+
+ ma_result result = MA_ERROR;
+ {
+ Babylon::Polyfills::Internal::PreparePlayback();
+ result = ma_device_init(nullptr, &config, m_device.get());
+ if (result != MA_SUCCESS)
+ {
+ ma_backend backends[] = {ma_backend_null};
+ result = ma_device_init_ex(backends, 1, nullptr, &config, m_device.get());
+ m_usingNullBackend.store(true, std::memory_order_relaxed);
+ }
+ else
+ {
+ m_usingNullBackend.store(false, std::memory_order_relaxed);
+ }
+ }
+
+ if (result != MA_SUCCESS)
+ {
+ m_device.reset();
+ m_deviceInitialized.store(false, std::memory_order_relaxed);
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ return false;
+ }
+
+ m_sampleRate = m_device->sampleRate;
+ m_deviceInitialized.store(true, std::memory_order_relaxed);
+ return true;
+ }
+
+ void AudioEngine::ShutdownDevice()
+ {
+ StopNullClock();
+
+ std::lock_guard lock{m_deviceMutex};
+ if (!m_device)
+ {
+ m_deviceInitialized.store(false, std::memory_order_relaxed);
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ return;
+ }
+
+ if (m_deviceStarted.load(std::memory_order_relaxed))
+ {
+ ma_device_stop(m_device.get());
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ }
+
+ ma_device_uninit(m_device.get());
+ m_device.reset();
+ m_deviceInitialized.store(false, std::memory_order_relaxed);
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ }
+
+ bool AudioEngine::EnsureDeviceRunning()
+ {
+ if (m_shuttingDown.load(std::memory_order_relaxed))
+ {
+ return false;
+ }
+
+ if (!InitializeDevice())
+ {
+ return false;
+ }
+
+ std::lock_guard lock{m_deviceMutex};
+ if (!m_deviceInitialized.load(std::memory_order_relaxed))
+ {
+ return false;
+ }
+
+ if (!m_usingNullBackend.load(std::memory_order_relaxed) && !m_device)
+ {
+ return false;
+ }
+
+ if (!m_deviceStarted.load(std::memory_order_relaxed))
+ {
+ if (m_usingNullBackend.load(std::memory_order_relaxed))
+ {
+ StartNullClock();
+ }
+ else if (ma_device_start(m_device.get()) != MA_SUCCESS)
+ {
+ return false;
+ }
+
+ m_deviceStarted.store(true, std::memory_order_relaxed);
+ }
+
+ return true;
+ }
+
+ void AudioEngine::SuspendDevice()
+ {
+ if (m_usingNullBackend.load(std::memory_order_relaxed))
+ {
+ StopNullClock();
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ return;
+ }
+
+ std::lock_guard lock{m_deviceMutex};
+ if (!m_device || !m_deviceStarted.load(std::memory_order_relaxed))
+ {
+ return;
+ }
+
+ ma_device_stop(m_device.get());
+ m_deviceStarted.store(false, std::memory_order_relaxed);
+ }
+
+ void AudioEngine::ResumeDevice()
+ {
+ if (m_usingNullBackend.load(std::memory_order_relaxed))
+ {
+ if (!m_deviceStarted.load(std::memory_order_relaxed))
+ {
+ StartNullClock();
+ m_deviceStarted.store(true, std::memory_order_relaxed);
+ }
+ return;
+ }
+
+ std::lock_guard lock{m_deviceMutex};
+ if (!m_device || m_deviceStarted.load(std::memory_order_relaxed))
+ {
+ return;
+ }
+
+ if (ma_device_start(m_device.get()) == MA_SUCCESS)
+ {
+ m_deviceStarted.store(true, std::memory_order_relaxed);
+ }
+ }
+
+ void AudioEngine::TryResumeAfterInterruption()
+ {
+ if (HasRunningContext())
+ {
+ EnsureDeviceRunning();
+ ResumeDevice();
+ SetClockSuspended(false);
+ }
+ }
+
+ void AudioEngine::SetClockSuspended(bool suspended)
+ {
+ m_clockSuspended.store(suspended, std::memory_order_relaxed);
+ }
+
+ bool AudioEngine::HasRunningContext()
+ {
+ std::lock_guard lock{m_graphMutex};
+ for (const auto& weakContext : m_contexts)
+ {
+ auto context = weakContext.lock();
+ if (context && context->running.load(std::memory_order_relaxed))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void AudioEngine::OnContextRunningChanged()
+ {
+ const bool anyRunning = HasRunningContext();
+ SetClockSuspended(!anyRunning);
+ if (anyRunning)
+ {
+ EnsureDeviceRunning();
+ ResumeDevice();
+ }
+ else
+ {
+ SuspendDevice();
+ }
+ }
+
+ uint32_t AudioEngine::SampleRate() const
+ {
+ return m_sampleRate;
+ }
+
+ double AudioEngine::CurrentTime() const
+ {
+ return static_cast(m_framesProcessed.load(std::memory_order_relaxed)) / static_cast(m_sampleRate);
+ }
+
+ bool AudioEngine::IsRunning() const
+ {
+ return m_deviceStarted.load(std::memory_order_relaxed);
+ }
+
+ bool AudioEngine::UsesNullBackend() const
+ {
+ return m_usingNullBackend.load(std::memory_order_relaxed);
+ }
+
+ bool AudioEngine::DeviceInitialized() const
+ {
+ return m_deviceInitialized.load(std::memory_order_relaxed);
+ }
+
+ void AudioEngine::RegisterContext(const std::shared_ptr& context)
+ {
+ {
+ std::lock_guard lock{m_graphMutex};
+ m_contexts.push_back(context);
+ }
+ OnContextRunningChanged();
+ }
+
+ void AudioEngine::UnregisterContext(AudioContextState* context)
+ {
+ {
+ std::lock_guard lock{m_graphMutex};
+ m_contexts.erase(
+ std::remove_if(m_contexts.begin(), m_contexts.end(), [context](const std::weak_ptr& weak) {
+ auto shared = weak.lock();
+ return !shared || shared.get() == context;
+ }),
+ m_contexts.end());
+ }
+ OnContextRunningChanged();
+ }
+
+ void AudioEngine::RegisterSource(const std::shared_ptr& source)
+ {
+ std::lock_guard lock{m_graphMutex};
+ m_sources.push_back(source);
+ }
+
+ void AudioEngine::UnregisterSource(BufferSourceState* source)
+ {
+ std::lock_guard lock{m_graphMutex};
+ m_sources.erase(
+ std::remove_if(m_sources.begin(), m_sources.end(), [source](const std::weak_ptr& weak) {
+ auto shared = weak.lock();
+ return !shared || shared.get() == source;
+ }),
+ m_sources.end());
+ }
+
+ void AudioEngine::ScheduleOnJsThread(std::function callback)
+ {
+ m_runtimeScheduler([callback = std::move(callback)]() {
+ callback();
+ });
+ }
+
+ std::mutex& AudioEngine::GraphMutex()
+ {
+ return m_graphMutex;
+ }
+
+ std::function MakeCurrentTimeGetter(const std::shared_ptr& context)
+ {
+ return [weakContext = std::weak_ptr(context)]() {
+ auto sharedContext = weakContext.lock();
+ if (!sharedContext || !sharedContext->engine)
+ {
+ return 0.0;
+ }
+
+ return sharedContext->engine->CurrentTime();
+ };
+ }
+
+ void AudioEngine::DataCallback(ma_device* device, void* output, const void* /*input*/, uint32_t frameCount)
+ {
+ auto* engine = static_cast(device->pUserData);
+ engine->Mix(static_cast(output), frameCount);
+ }
+
+ void AudioEngine::StartNullClock()
+ {
+ if (m_nullClockRunning.load(std::memory_order_relaxed) || m_shuttingDown.load(std::memory_order_relaxed))
+ {
+ return;
+ }
+
+ m_nullClockRunning.store(true, std::memory_order_relaxed);
+ m_nullClockThread = std::thread([this]() {
+ std::vector buffer(static_cast(NULL_BACKEND_FRAME_COUNT) * OUTPUT_CHANNEL_COUNT, 0.f);
+ while (m_nullClockRunning.load(std::memory_order_relaxed) && !m_shuttingDown.load(std::memory_order_relaxed))
+ {
+ if (!m_clockSuspended.load(std::memory_order_relaxed))
+ {
+ Mix(buffer.data(), NULL_BACKEND_FRAME_COUNT);
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(5));
+ }
+ });
+ }
+
+ void AudioEngine::StopNullClock()
+ {
+ if (!m_nullClockRunning.exchange(false, std::memory_order_relaxed))
+ {
+ return;
+ }
+
+ if (m_nullClockThread.joinable())
+ {
+ m_nullClockThread.detach();
+ }
+ }
+
+ void AudioEngine::Mix(float* outputInterleaved, uint32_t frameCount)
+ {
+ std::memset(outputInterleaved, 0, sizeof(float) * frameCount * OUTPUT_CHANNEL_COUNT);
+
+ if (m_shuttingDown.load(std::memory_order_relaxed) || m_clockSuspended.load(std::memory_order_relaxed))
+ {
+ return;
+ }
+
+ std::vector mixL(frameCount, 0.f);
+ std::vector mixR(frameCount, 0.f);
+
+ {
+ std::lock_guard lock{m_graphMutex};
+
+ const double blockStartTime = static_cast(m_framesProcessed.load(std::memory_order_relaxed)) / static_cast(m_sampleRate);
+
+ m_sources.erase(
+ std::remove_if(m_sources.begin(), m_sources.end(), [](const std::weak_ptr& weak) {
+ return weak.expired();
+ }),
+ m_sources.end());
+
+ for (const auto& weakSource : m_sources)
+ {
+ auto source = weakSource.lock();
+ if (!source || !source->started.load())
+ {
+ continue;
+ }
+
+ auto context = source->context.lock();
+ if (!context || !context->running.load())
+ {
+ continue;
+ }
+
+ std::fill(mixL.begin(), mixL.end(), 0.f);
+ std::fill(mixR.begin(), mixR.end(), 0.f);
+ source->Mix(mixL.data(), mixR.data(), frameCount, m_sampleRate, context->listener, blockStartTime);
+
+ for (uint32_t frame = 0; frame < frameCount; ++frame)
+ {
+ outputInterleaved[frame * OUTPUT_CHANNEL_COUNT] += mixL[frame];
+ outputInterleaved[frame * OUTPUT_CHANNEL_COUNT + 1] += mixR[frame];
+ }
+ }
+ }
+
+ m_framesProcessed.fetch_add(frameCount, std::memory_order_relaxed);
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioEngine.h b/Polyfills/WebAudio/Source/AudioEngine.h
new file mode 100644
index 000000000..cba12ff02
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioEngine.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct ma_device;
+
+namespace Babylon::Polyfills::Internal
+{
+ struct AudioNodeState;
+ struct AudioContextState;
+ struct BufferSourceState;
+
+ class AudioEngine final : public std::enable_shared_from_this
+ {
+ public:
+ static std::shared_ptr Create(JsRuntime& runtime);
+ ~AudioEngine();
+
+ AudioEngine(const AudioEngine&) = delete;
+ AudioEngine& operator=(const AudioEngine&) = delete;
+
+ uint32_t SampleRate() const;
+ double CurrentTime() const;
+ bool IsRunning() const;
+ bool UsesNullBackend() const;
+ bool DeviceInitialized() const;
+
+ bool EnsureDeviceRunning();
+ void SuspendDevice();
+ void ResumeDevice();
+ void TryResumeAfterInterruption();
+ void SetClockSuspended(bool suspended);
+ void PrepareForShutdown();
+ void OnContextRunningChanged();
+ bool HasRunningContext();
+
+ void RegisterContext(const std::shared_ptr& context);
+ void UnregisterContext(AudioContextState* context);
+
+ void RegisterSource(const std::shared_ptr& source);
+ void UnregisterSource(BufferSourceState* source);
+
+ void ScheduleOnJsThread(std::function callback);
+
+ std::mutex& GraphMutex();
+
+ private:
+ explicit AudioEngine(JsRuntime& runtime);
+
+ bool InitializeDevice();
+ void ShutdownDevice();
+ void StartNullClock();
+ void StopNullClock();
+
+ static void DataCallback(ma_device* device, void* output, const void* input, uint32_t frameCount);
+ void Mix(float* outputInterleaved, uint32_t frameCount);
+
+ JsRuntimeScheduler m_runtimeScheduler;
+ std::unique_ptr m_device{};
+ mutable std::mutex m_deviceMutex{};
+ uint32_t m_sampleRate{48000};
+ std::atomic m_framesProcessed{0};
+ std::atomic m_deviceStarted{false};
+ std::atomic m_deviceInitialized{false};
+ std::atomic m_usingNullBackend{false};
+ std::atomic m_clockSuspended{true};
+ std::atomic m_shuttingDown{false};
+ std::atomic m_nullClockRunning{false};
+ std::thread m_nullClockThread{};
+
+ std::mutex m_graphMutex{};
+ std::vector> m_contexts{};
+ std::vector> m_sources{};
+ };
+
+ std::function MakeCurrentTimeGetter(const std::shared_ptr& context);
+
+ std::shared_ptr GetAudioEngineFromJavaScript(Napi::Env env);
+}
diff --git a/Polyfills/WebAudio/Source/AudioListener.cpp b/Polyfills/WebAudio/Source/AudioListener.cpp
new file mode 100644
index 000000000..53921af0f
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioListener.cpp
@@ -0,0 +1,104 @@
+#include "AudioListener.h"
+#include "AudioEngine.h"
+#include "AudioParam.h"
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ void NativeAudioListener::Initialize(Napi::Env env)
+ {
+ Napi::HandleScope scope{env};
+
+ Napi::Function func = DefineClass(
+ env,
+ JS_CONSTRUCTOR_NAME,
+ {
+ InstanceAccessor("positionX", &NativeAudioListener::GetPositionX, nullptr),
+ InstanceAccessor("positionY", &NativeAudioListener::GetPositionY, nullptr),
+ InstanceAccessor("positionZ", &NativeAudioListener::GetPositionZ, nullptr),
+ InstanceAccessor("forwardX", &NativeAudioListener::GetForwardX, nullptr),
+ InstanceAccessor("forwardY", &NativeAudioListener::GetForwardY, nullptr),
+ InstanceAccessor("forwardZ", &NativeAudioListener::GetForwardZ, nullptr),
+ InstanceAccessor("upX", &NativeAudioListener::GetUpX, nullptr),
+ InstanceAccessor("upY", &NativeAudioListener::GetUpY, nullptr),
+ InstanceAccessor("upZ", &NativeAudioListener::GetUpZ, nullptr),
+ InstanceMethod("setPosition", &NativeAudioListener::SetPosition),
+ InstanceMethod("setOrientation", &NativeAudioListener::SetOrientation),
+ });
+
+ JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
+ }
+
+ Napi::Object NativeAudioListener::CreateInstance(Napi::Env env, AudioListenerState& listener, const std::function& getCurrentTime)
+ {
+ auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CONSTRUCTOR_NAME).As();
+ auto object = func.New({});
+ auto* self = NativeAudioListener::Unwrap(object);
+ self->m_listener = &listener;
+ self->m_getCurrentTime = getCurrentTime;
+
+ auto bindParam = [&](AudioParamState& state, float defaultValue, Napi::ObjectReference& ref) {
+ auto statePtr = std::shared_ptr(std::shared_ptr{}, &state);
+ auto param = NativeAudioParam::CreateInstance(env, statePtr, defaultValue, getCurrentTime);
+ ref = Napi::Persistent(param);
+ state.SetDefault(defaultValue);
+ };
+
+ bindParam(listener.positionX, 0.f, self->m_positionX);
+ bindParam(listener.positionY, 0.f, self->m_positionY);
+ bindParam(listener.positionZ, 0.f, self->m_positionZ);
+ bindParam(listener.forwardX, 0.f, self->m_forwardX);
+ bindParam(listener.forwardY, 0.f, self->m_forwardY);
+ bindParam(listener.forwardZ, -1.f, self->m_forwardZ);
+ bindParam(listener.upX, 0.f, self->m_upX);
+ bindParam(listener.upY, 1.f, self->m_upY);
+ bindParam(listener.upZ, 0.f, self->m_upZ);
+
+ return object;
+ }
+
+ NativeAudioListener::NativeAudioListener(const Napi::CallbackInfo& info)
+ : Napi::ObjectWrap{info}
+ {
+ }
+
+ Napi::Value NativeAudioListener::GetPositionX(const Napi::CallbackInfo&) { return m_positionX.Value(); }
+ Napi::Value NativeAudioListener::GetPositionY(const Napi::CallbackInfo&) { return m_positionY.Value(); }
+ Napi::Value NativeAudioListener::GetPositionZ(const Napi::CallbackInfo&) { return m_positionZ.Value(); }
+ Napi::Value NativeAudioListener::GetForwardX(const Napi::CallbackInfo&) { return m_forwardX.Value(); }
+ Napi::Value NativeAudioListener::GetForwardY(const Napi::CallbackInfo&) { return m_forwardY.Value(); }
+ Napi::Value NativeAudioListener::GetForwardZ(const Napi::CallbackInfo&) { return m_forwardZ.Value(); }
+ Napi::Value NativeAudioListener::GetUpX(const Napi::CallbackInfo&) { return m_upX.Value(); }
+ Napi::Value NativeAudioListener::GetUpY(const Napi::CallbackInfo&) { return m_upY.Value(); }
+ Napi::Value NativeAudioListener::GetUpZ(const Napi::CallbackInfo&) { return m_upZ.Value(); }
+
+ void NativeAudioListener::SetPosition(const Napi::CallbackInfo& info)
+ {
+ if (!m_listener || info.Length() < 3)
+ {
+ return;
+ }
+
+ const double currentTime = m_getCurrentTime ? m_getCurrentTime() : 0.0;
+ m_listener->positionX.SetImmediate(info[0].As().FloatValue(), currentTime);
+ m_listener->positionY.SetImmediate(info[1].As().FloatValue(), currentTime);
+ m_listener->positionZ.SetImmediate(info[2].As().FloatValue(), currentTime);
+ }
+
+ void NativeAudioListener::SetOrientation(const Napi::CallbackInfo& info)
+ {
+ if (!m_listener || info.Length() < 6)
+ {
+ return;
+ }
+
+ const double currentTime = m_getCurrentTime ? m_getCurrentTime() : 0.0;
+ m_listener->forwardX.SetImmediate(info[0].As().FloatValue(), currentTime);
+ m_listener->forwardY.SetImmediate(info[1].As().FloatValue(), currentTime);
+ m_listener->forwardZ.SetImmediate(info[2].As().FloatValue(), currentTime);
+ m_listener->upX.SetImmediate(info[3].As().FloatValue(), currentTime);
+ m_listener->upY.SetImmediate(info[4].As().FloatValue(), currentTime);
+ m_listener->upZ.SetImmediate(info[5].As().FloatValue(), currentTime);
+ }
+}
diff --git a/Polyfills/WebAudio/Source/AudioListener.h b/Polyfills/WebAudio/Source/AudioListener.h
new file mode 100644
index 000000000..7cdeba46c
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioListener.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "AudioNode.h"
+
+#include
+
+#include
+
+namespace Babylon::Polyfills::Internal
+{
+ class NativeAudioListener final : public Napi::ObjectWrap
+ {
+ public:
+ static void Initialize(Napi::Env env);
+ static Napi::Object CreateInstance(Napi::Env env, AudioListenerState& listener, const std::function& getCurrentTime);
+
+ explicit NativeAudioListener(const Napi::CallbackInfo& info);
+
+ private:
+ static constexpr auto JS_CONSTRUCTOR_NAME = "AudioListener";
+
+ Napi::Value GetPositionX(const Napi::CallbackInfo& info);
+ Napi::Value GetPositionY(const Napi::CallbackInfo& info);
+ Napi::Value GetPositionZ(const Napi::CallbackInfo& info);
+ Napi::Value GetForwardX(const Napi::CallbackInfo& info);
+ Napi::Value GetForwardY(const Napi::CallbackInfo& info);
+ Napi::Value GetForwardZ(const Napi::CallbackInfo& info);
+ Napi::Value GetUpX(const Napi::CallbackInfo& info);
+ Napi::Value GetUpY(const Napi::CallbackInfo& info);
+ Napi::Value GetUpZ(const Napi::CallbackInfo& info);
+ void SetPosition(const Napi::CallbackInfo& info);
+ void SetOrientation(const Napi::CallbackInfo& info);
+
+ AudioListenerState* m_listener{nullptr};
+ std::function m_getCurrentTime{};
+ Napi::ObjectReference m_positionX{};
+ Napi::ObjectReference m_positionY{};
+ Napi::ObjectReference m_positionZ{};
+ Napi::ObjectReference m_forwardX{};
+ Napi::ObjectReference m_forwardY{};
+ Napi::ObjectReference m_forwardZ{};
+ Napi::ObjectReference m_upX{};
+ Napi::ObjectReference m_upY{};
+ Napi::ObjectReference m_upZ{};
+ };
+}
diff --git a/Polyfills/WebAudio/Source/AudioNode.cpp b/Polyfills/WebAudio/Source/AudioNode.cpp
new file mode 100644
index 000000000..3bbf67d6b
--- /dev/null
+++ b/Polyfills/WebAudio/Source/AudioNode.cpp
@@ -0,0 +1,600 @@
+#include "AudioNode.h"
+
+#include