diff --git a/src/lime/_internal/backend/native/NativeAudioSource.hx b/src/lime/_internal/backend/native/NativeAudioSource.hx index 4b6b527f00..ab076a0c5a 100644 --- a/src/lime/_internal/backend/native/NativeAudioSource.hx +++ b/src/lime/_internal/backend/native/NativeAudioSource.hx @@ -17,9 +17,6 @@ import lime.utils.UInt8Array; @:access(lime.media.AudioBuffer) class NativeAudioSource { - private static var hasDirectChannelsExt:Null; - private static var hasALSoftLatencyExt:Null; - private var completed:Bool; private var dataLength:Int; private var format:Int; @@ -62,16 +59,6 @@ class NativeAudioSource public function init():Void { - if (hasALSoftLatencyExt == null) - { - hasALSoftLatencyExt = AL.isExtensionPresent("AL_SOFT_source_latency"); - } - - if (hasDirectChannelsExt == null) - { - hasDirectChannelsExt = AL.isExtensionPresent("AL_SOFT_direct_channels") && AL.isExtensionPresent("AL_SOFT_direct_channels_remix"); - } - format = 0; switch (parent.buffer.dataFormat) @@ -110,7 +97,7 @@ class NativeAudioSource AL.sourcei(handle, AL.BUFFER, parent.buffer.__srcBuffer); - if (hasDirectChannelsExt) + if (AudioManager.__directChannelsExtSupported) { AL.sourcei(handle, AL.DIRECT_CHANNELS_SOFT, AL.REMIX_UNMATCHED_SOFT); } @@ -345,7 +332,7 @@ class NativeAudioSource public function getLatency():Float { - if (hasALSoftLatencyExt) + if (AudioManager.__latencyExtSupported) { var offsets = AL.getSourcedvSOFT(handle, AL.SEC_OFFSET_LATENCY_SOFT, 2); diff --git a/src/lime/media/AudioManager.hx b/src/lime/media/AudioManager.hx index ca84d6c105..7f223d514d 100644 --- a/src/lime/media/AudioManager.hx +++ b/src/lime/media/AudioManager.hx @@ -1,216 +1,648 @@ package lime.media; -import lime.system.CFFIPointer; import haxe.MainLoop; +import lime.app.Event; +import lime.system.CFFIPointer; +#if lime_openal +import lime.media.openal.AL; +import lime.media.openal.ALC; +import lime.media.openal.ALContext; +import lime.media.openal.ALDevice; +import sys.thread.Mutex; #if (windows || mac || linux || android || ios) import haxe.io.Path; import lime.system.System; import sys.FileSystem; import sys.io.File; #end -import haxe.Timer; -import lime._internal.backend.native.NativeCFFI; -import lime.media.openal.AL; -import lime.media.openal.ALC; -import lime.media.openal.ALContext; -import lime.media.openal.ALDevice; -import lime.app.Application; -#if (js && html5) -import js.Browser; +#elseif lime_howlerjs +import lime.media.howlerjs.Howler; #end #if !lime_debug @:fileXml('tags="haxe,release"') @:noDebug #end -@:allow(lime._internal.backend.native.NativeApplication) -@:access(lime._internal.backend.native.NativeCFFI) @:access(lime.media.openal.ALDevice) class AudioManager { + /** + The version of OpenAL Configuration. + **/ @:noCompletion - private static var AUDIO_CONFIG_VERSION:String = "1.1"; + private static final AUDIO_CONFIG_VERSION:String = "1.1"; - @:noCompletion - private static var resumeOnFocus:Bool = false; + /** + A read-only variable if the AudioManager is active or suspended. + **/ + public static var active(default, null):Bool; - @:noCompletion - private static var active:Bool = true; + /** + Should it automatically switch to the default playback device whenever it changes. + + Only works on Native target. + **/ + public static var automaticDefaultPlaybackDevice:Bool = true; + /** + The current used context to use for the audio manager. + **/ public static var context:AudioContext; - public static function init(context:AudioContext = null) + /** + The gain (volume) of the audio manager. A value of `1.0` represents the default volume. + Property is in a linear scale. + **/ + public static var gain(get, set):Float; + + /** + Mutes the audio manager playback. + **/ + public static var muted(get, set):Bool; + + /** + Dispatched when the default for the playback device is changed. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onDefaultPlaybackDeviceChanged = new EventVoid>(); + + /** + Dispatched whenever a playback device is added. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onPlaybackDeviceAdded = new EventVoid>(); + + /** + Dispatched whenever a playback device is removed. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onPlaybackDeviceRemoved = new EventVoid>(); + + /** + Dispatched when the default for the capture device is changed. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onDefaultCaptureDeviceChanged = new EventVoid>(); + + /** + Dispatched whenever a capture device is added. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onCaptureDeviceAdded = new EventVoid>(); + + /** + Dispatched whenever a capture device is removed. + 'Device Name' -> Void. + + Only works on Native target. + **/ + public static var onCaptureDeviceRemoved = new EventVoid>(); + + @:noCompletion private static var __gain:Float = 1; + @:noCompletion private static var __muted:Bool = false; + #if lime_openal + @:noCompletion private static var __pendingDeviceEventMutex:Mutex = new Mutex(); + @:noCompletion private static var __pendingDeviceEventCheck:Bool; + @:noCompletion private static var __pendingPlaybackDevicesAddition:Array; + @:noCompletion private static var __pendingPlaybackDevicesRemoval:Array; + @:noCompletion private static var __pendingCaptureDevicesAddition:Array; + @:noCompletion private static var __pendingCaptureDevicesRemoval:Array; + @:noCompletion private static var __pendingDefaultPlaybackDevice:Bool; + @:noCompletion private static var __pendingDefaultCaptureDevice:Bool; + + @:noCompletion private static var __captureExtSupported:Bool; + @:noCompletion private static var __directChannelsExtSupported:Bool; + @:noCompletion private static var __disconnectExtSupported:Bool; + @:noCompletion private static var __enumerateAllSupported:Bool; + @:noCompletion private static var __latencyExtSupported:Bool; + @:noCompletion private static var __resumeOnFocus:Bool; + @:noCompletion private static var __reopenDeviceSupported:Bool; + @:noCompletion private static var __systemEventsSupported:Bool; + #end + + /** + Initializes an `AudioManager` to playback to and capture from audio devices. + Automatically dispatched when Application is constructed. + + @param context Optional; An Audio Context to initalize the `AudioManager` with. + **/ + public static function init(?context:AudioContext) { - if (AudioManager.context == null) + if (AudioManager.context != null) return; + + if (context == null) { - if (context == null) - { - AudioManager.context = new AudioContext(); + context = new AudioContext(); + } + + AudioManager.context = context; + + #if lime_openal + if (context.type == OPENAL) + { + #if (windows || mac || linux || android || ios) + __setupConfig(); + #end + + refresh(); + + #if !mobile + if (__reopenDeviceSupported) AL.disable(AL.STOP_SOURCES_ON_DISCONNECT_SOFT); + if (__systemEventsSupported) { + ALC.eventControlSOFT([ + ALC.EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT, + ALC.EVENT_TYPE_DEVICE_ADDED_SOFT, + ALC.EVENT_TYPE_DEVICE_REMOVED_SOFT], + true); + ALC.eventCallbackSOFT(__deviceEventCallback); + } + #end + } + #end + } + + /** + Gets the default capture audio device name from the host operating system. - context = AudioManager.context; + Only works on Native target. - #if !lime_doc_gen - if (context.type == OPENAL) + @return The default capture audio device name. + **/ + public static function getCaptureDefaultDeviceName():String + { + #if (lime_openal && !lime_doc_gen) + if (context != null && context.type == OPENAL && __captureExtSupported) + { + return __formatDeviceName(ALC.getString(null, ALC.CAPTURE_DEFAULT_DEVICE_SPECIFIER)); + } + #end + return ''; + } + + /** + Gets all of the available capture audio device names from the host operating system. + Note: "Default Device" is only exclusive in SDL3, this does not appear in every other OpenAL Soft backends. + + Only works on Native target. + + @return An array containing available capture audio device names. + **/ + public static function getCaptureDeviceNames():Array + { + #if (lime_openal && !lime_doc_gen) + if (context == null || context.type != OPENAL || !__captureExtSupported) return []; + + final arr = ALC.getStringList(null, ALC.CAPTURE_DEVICE_SPECIFIER); + for (i in 0...arr.length) arr[i] = __formatDeviceName(arr[i]); + + return arr; + #else + return []; + #end + } + + /** + Gets the current used playback audio device name. + + Only works on Native target. + + @return Current playback audio device name. + **/ + public static function getCurrentPlaybackDeviceName():String + { + #if (lime_openal && !lime_doc_gen) + if (context != null && context.type == OPENAL) + { + var currentContext = ALC.getCurrentContext(); + if (currentContext != null) + { + var device = ALC.getContextsDevice(currentContext); + if (device != null) { - #if (windows || mac || linux || android || ios) - setupConfig(); - #end - - var alc = context.openal; - var device = alc.openDevice(); - if (device != null) - { - var ctx = alc.createContext(device); - alc.makeContextCurrent(ctx); - alc.processContext(ctx); - - if (alc.isExtensionPresent('ALC_SOFT_system_events', device) && alc.isExtensionPresent('ALC_SOFT_reopen_device', device)) - { - if (alc.isExtensionPresent('AL_SOFT_hold_on_disconnect')) - alc.disable(AL.STOP_SOURCES_ON_DISCONNECT_SOFT); - - alc.eventControlSOFT([ALC.EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT, ALC.EVENT_TYPE_DEVICE_ADDED_SOFT, ALC.EVENT_TYPE_DEVICE_REMOVED_SOFT], true); - - alc.eventCallbackSOFT(deviceEventCallback); - } - } + if (__enumerateAllSupported) return __formatDeviceName(ALC.getString(device, ALC.ALL_DEVICES_SPECIFIER)); + else return __formatDeviceName(ALC.getString(device, ALC.DEVICE_SPECIFIER)); } - #end } + } + #end + return ''; + } - AudioManager.context = context; + /** + Gets the default playback audio device name from the host operating system. + + Only works on Native target. + BUG: In SDL3 backend (All platforms except IOS), it will return "Default Device" instead of + the actual default device. + + @return The default playback audio device name. + **/ + public static function getPlaybackDefaultDeviceName():String + { + #if (lime_openal && !lime_doc_gen) + if (context != null && context.type == OPENAL) + { + var deviceName:String; + if (__enumerateAllSupported) deviceName = __formatDeviceName(ALC.getString(null, ALC.DEFAULT_ALL_DEVICES_SPECIFIER)); + else deviceName = __formatDeviceName(ALC.getString(null, ALC.DEFAULT_DEVICE_SPECIFIER)); + + return deviceName; } + #end + return ''; } - public static function resume():Void + /** + Gets all of the available playback audio device names from the host operating system. + + Only works on Native target. + + @return An array containing available playback audio device names. + **/ + public static function getPlaybackDeviceNames():Array { - if (active) - return; + #if (lime_openal && !lime_doc_gen) + if (context == null || context.type != OPENAL) return []; + else if (!__enumerateAllSupported) return [__formatDeviceName(ALC.getString(null, ALC.DEVICE_SPECIFIER))]; - #if !lime_doc_gen - if (context != null && context.type == OPENAL) + final arr = ALC.getStringList(null, ALC.ALL_DEVICES_SPECIFIER); + for (i in 0...arr.length) arr[i] = __formatDeviceName(arr[i]); + + // SDL3 Backend Exclusive, apparently not a bug but it acts as a virtual device to automatically + // switch to a default device without having to code it on your own, except that it's + // been already coded so this isn't needed anymore. + arr.remove("Default Device"); + + return arr; + #else + return []; + #end + } + + /** + Refresh the context with optionally to different device. + + Only works on Native target. + + @param deviceName Optional; The device name to use to for playbacking the audios. + **/ + public static function refresh(?deviceName:String):Bool + { + #if (lime_openal && !lime_doc_gen) + if (context == null || context.type != OPENAL) return false; + + if (deviceName == null) deviceName = getPlaybackDefaultDeviceName(); + + var currentContext = ALC.getCurrentContext(); + var device = currentContext != null ? ALC.getContextsDevice(currentContext) : null; + + #if !mobile + if (device != null && __reopenDeviceSupported && ALC.reopenDeviceSOFT(device, deviceName, null)) { - var alc = context.openal; - var currentContext = alc.getCurrentContext(); + __refresh(); + return true; + } + #end + if (currentContext != null) + { + ALC.destroyContext(currentContext); + currentContext = null; + } + + if (device != null) + { + ALC.closeDevice(device); + device = null; + } + + if ((device = ALC.openDevice()) == null || (currentContext = ALC.createContext(device)) == null + || !ALC.makeContextCurrent(currentContext)) + { + return false; + } + + ALC.processContext(currentContext); + __refresh(); + return true; + #else + return false; + #end + } + + /** + Resumes the current `AudioManager` context. + **/ + public static function resume():Void + { + #if lime_openal + if (context != null && context.type == OPENAL) + { + var currentContext = ALC.getCurrentContext(); if (currentContext != null) { - var device = alc.getContextsDevice(currentContext); - alc.resumeDevice(device); - alc.processContext(currentContext); + var device = ALC.getContextsDevice(currentContext); + if (device != null) ALC.resumeDevice(device); + + ALC.processContext(currentContext); } } + #elseif (js && html5) + #if lime_howlerjs + if (untyped Howler.ctx) + { + Howler.ctx.resume(); + } + #end + if (context != null && context.type == WEB) + { + context.web.resume(); + } #end - - active = true; } + /** + Shutdowns the current `AudioManager` context. + **/ public static function shutdown():Void { - #if !lime_doc_gen + #if lime_openal if (context != null && context.type == OPENAL) { - var alc = context.openal; - var currentContext = alc.getCurrentContext(); - var device = alc.getContextsDevice(currentContext); - + var currentContext = ALC.getCurrentContext(); if (currentContext != null) { - alc.makeContextCurrent(null); - alc.destroyContext(currentContext); + ALC.makeContextCurrent(null); + ALC.destroyContext(currentContext); - if (device != null) - { - alc.closeDevice(device); - } + var device = ALC.getContextsDevice(currentContext); + if (device != null) ALC.closeDevice(device); } } + #elseif (js && html5) + #if lime_howlerjs + if (untyped Howler.ctx) + { + Howler.ctx.unload(); + } + #end + if (context != null && context.type == WEB) + { + context.web.close(); + } #end context = null; } + /** + Pauses the current `AudioManager` context. + **/ public static function suspend():Void { - if (!active) - return; - - #if !lime_doc_gen + #if lime_openal if (context != null && context.type == OPENAL) { - var alc = context.openal; - var currentContext = alc.getCurrentContext(); - var device = alc.getContextsDevice(currentContext); - + var currentContext = ALC.getCurrentContext(); if (currentContext != null) { - alc.suspendContext(currentContext); + ALC.suspendContext(currentContext); - if (device != null) - { - alc.pauseDevice(device); - } + var device = ALC.getContextsDevice(currentContext); + if (device != null) ALC.pauseDevice(device); } } + #elseif (js && html5) + #if lime_howlerjs + if (untyped Howler.ctx) + { + Howler.ctx.suspend(); + } + #end + if (context != null && context.type == WEB) + { + context.web.suspend(); + } #end - - active = false; } + @:allow(lime._internal.backend.native.NativeApplication) @:noCompletion private static function onActivate():Void { - if (resumeOnFocus) + if (__resumeOnFocus) { - resumeOnFocus = false; - - AudioManager.resume(); + context.web.suspend(); } } + @:allow(lime._internal.backend.native.NativeApplication) @:noCompletion private static function onDeactivate():Void { - resumeOnFocus = AudioManager.active; - + __resumeOnFocus = AudioManager.active; AudioManager.suspend(); } - @:noCompletion - private static function deviceEventCallback(eventType:Int, deviceType:Int, handle:CFFIPointer, message:#if hl hl.Bytes #else String #end):Void + @:noCompletion private static inline function get_muted():Bool { + return __muted; + } + + @:noCompletion private static inline function set_muted(value:Bool):Bool + { + __muted = value; + if (context == null) return __muted; + + #if !lime_doc_gen + #if lime_openal + if (context.type == OPENAL) AL.listenerf(AL.GAIN, value ? 0 : __gain); + #elseif (js && html5 && lime_howlerjs) + Howler.mute(value); + #end + #end + return value; + } + + @:noCompletion private static inline function get_gain():Float + { + return __gain; + } + + @:noCompletion private static inline function set_gain(value:Float):Float + { + __gain = value; + if (context == null) return __gain; + #if !lime_doc_gen - if (eventType == ALC.EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT && deviceType == ALC.PLAYBACK_DEVICE_SOFT) + #if lime_openal + if (context.type == OPENAL) AL.listenerf(AL.GAIN, __muted ? 0 : value); + #elseif (js && html5 && lime_howlerjs) + Howler.volume(value); + #end + #end + return value; + } + + #if (lime_openal && !lime_doc_gen) + @:noCompletion private static function __refresh():Void + { + var currentContext = ALC.getCurrentContext(); + if (currentContext == null) return; + + var device = ALC.getContextsDevice(currentContext); + if (device == null) return; + + __captureExtSupported = ALC.isExtensionPresent(null, 'ALC_EXT_CAPTURE'); + __disconnectExtSupported = ALC.isExtensionPresent(null, 'ALC_EXT_disconnect'); + __enumerateAllSupported = ALC.isExtensionPresent(null, 'ALC_ENUMERATE_ALL_EXT'); + __reopenDeviceSupported = ALC.isExtensionPresent(null, 'ALC_SOFT_reopen_device'); + __systemEventsSupported = ALC.isExtensionPresent(null, 'ALC_SOFT_system_events'); + + __latencyExtSupported = AL.isExtensionPresent('AL_SOFT_source_latency'); + __directChannelsExtSupported = AL.isExtensionPresent('AL_SOFT_direct_channels') && AL.isExtensionPresent('AL_SOFT_direct_channels_remix'); + + gain = __gain; + AL.distanceModel(AL.NONE); + } + + @:noCompletion private static function __formatDeviceName(deviceName:String) + { + #if lime_openal + if (StringTools.startsWith(deviceName, 'OpenAL Soft on ')) return deviceName.substr(15); + else if (StringTools.startsWith(deviceName, 'OpenAL on ')) return deviceName.substr(10); + #end + else if (StringTools.startsWith(deviceName, 'Generic Software on ')) return deviceName.substr(20); + else return deviceName; + } + + @:noCompletion + private static function __deviceEventRun():Void + { + __pendingDeviceEventMutex.acquire(); + + if (__pendingDefaultPlaybackDevice) { - var device = new ALDevice(handle); + __pendingDefaultPlaybackDevice = false; + if (automaticDefaultPlaybackDevice) refresh(); + onDefaultPlaybackDeviceChanged.dispatch(getPlaybackDefaultDeviceName()); + } - MainLoop.runInMainThread(function():Void - { - var alc = context.openal; + if (__pendingDefaultCaptureDevice) + { + __pendingDefaultCaptureDevice = false; + onDefaultCaptureDeviceChanged.dispatch(getCaptureDefaultDeviceName()); + } - if (device == null) - { - var currentContext = alc.getCurrentContext(); + if (__pendingPlaybackDevicesAddition != null) + { + for (deviceName in __pendingPlaybackDevicesAddition) onPlaybackDeviceAdded.dispatch(deviceName); + __pendingPlaybackDevicesAddition = null; + } - var device = alc.getContextsDevice(currentContext); + if (__pendingPlaybackDevicesRemoval != null) + { + for (deviceName in __pendingPlaybackDevicesRemoval) onPlaybackDeviceRemoved.dispatch(deviceName); + __pendingPlaybackDevicesRemoval = null; + } - if (device != null) - alc.reopenDeviceSOFT(device, null, null); - } - else - { - alc.reopenDeviceSOFT(device, null, null); - } + if (__pendingCaptureDevicesAddition != null) + { + for (deviceName in __pendingCaptureDevicesAddition) onCaptureDeviceAdded.dispatch(deviceName); + __pendingCaptureDevicesAddition = null; + } - }); + if (__pendingCaptureDevicesRemoval != null) + { + for (deviceName in __pendingCaptureDevicesRemoval) onCaptureDeviceRemoved.dispatch(deviceName); + __pendingCaptureDevicesRemoval = null; } - #end + + __pendingDeviceEventCheck = false; + __pendingDeviceEventMutex.release(); } @:noCompletion - private static function setupConfig():Void + private static function __deviceEventCallback(eventType:Int, deviceType:Int, handle:CFFIPointer, + #if hl _message:hl.Bytes #else message:String #end) + { + #if hl var message:String = CFFI.stringValue(_message); #end + var device:ALDevice = handle != null ? new ALDevice(handle) : null; + var deviceName = __getDeviceNameFromMessage(message); + + var currentContext = ALC.getCurrentContext(); + var currentDevice = currentContext != null ? ALC.getContextsDevice(currentContext) : null; + + __pendingDeviceEventMutex.acquire(); + + if (deviceType == ALC.PLAYBACK_DEVICE_SOFT) + { + switch (eventType) + { + case ALC.EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT: + __pendingDefaultPlaybackDevice = true; + case ALC.EVENT_TYPE_DEVICE_ADDED_SOFT: + var formattedDeviceName = __formatDeviceName(deviceName); + if (__pendingPlaybackDevicesRemoval != null) __pendingPlaybackDevicesRemoval.remove(formattedDeviceName); + if (__pendingPlaybackDevicesAddition == null) __pendingPlaybackDevicesAddition = []; + __pendingPlaybackDevicesAddition.push(formattedDeviceName); + case ALC.EVENT_TYPE_DEVICE_REMOVED_SOFT: + var formattedDeviceName = __formatDeviceName(deviceName); + if (__pendingPlaybackDevicesAddition != null) __pendingPlaybackDevicesAddition.remove(formattedDeviceName); + if (__pendingPlaybackDevicesRemoval == null) __pendingPlaybackDevicesRemoval = []; + __pendingPlaybackDevicesRemoval.push(formattedDeviceName); + } + } + else if (deviceType == ALC.CAPTURE_DEVICE_SOFT) + { + switch (eventType) + { + case ALC.EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT: + __pendingDefaultCaptureDevice = true; + case ALC.EVENT_TYPE_DEVICE_ADDED_SOFT: + var formattedDeviceName = __formatDeviceName(deviceName); + if (__pendingCaptureDevicesRemoval != null) __pendingCaptureDevicesRemoval.remove(formattedDeviceName); + if (__pendingCaptureDevicesAddition == null) __pendingCaptureDevicesAddition = []; + __pendingCaptureDevicesAddition.push(formattedDeviceName); + case ALC.EVENT_TYPE_DEVICE_REMOVED_SOFT: + var formattedDeviceName = __formatDeviceName(deviceName); + if (__pendingCaptureDevicesAddition != null) __pendingCaptureDevicesAddition.remove(formattedDeviceName); + if (__pendingCaptureDevicesRemoval == null) __pendingCaptureDevicesRemoval = []; + __pendingCaptureDevicesRemoval.push(formattedDeviceName); + } + } + + if (!__pendingDeviceEventCheck) + { + __pendingDeviceEventCheck = true; + MainLoop.runInMainThread(__deviceEventRun); + } + + __pendingDeviceEventMutex.release(); + } + + @:noCompletion + private static function __getDeviceNameFromMessage(message:String):Null + { + if (StringTools.startsWith(message, 'Device removed: ')) return message.substr(16); + else if (StringTools.startsWith(message, 'Device added: ')) return message.substr(14); + else return null; + } + + #if (windows || mac || linux || android || ios) + @:noCompletion + private static function __setupConfig():Void { - #if (lime_openal && (windows || mac || linux || android || ios)) final alConfig:Array = []; alConfig.push('[general]'); @@ -246,6 +678,7 @@ class AudioManager Sys.putEnv('ALSOFT_CONF', path); } catch (e:Dynamic) {} - #end } + #end + #end } diff --git a/src/lime/media/WebAudioContext.hx b/src/lime/media/WebAudioContext.hx index c1f934b442..9021433080 100644 --- a/src/lime/media/WebAudioContext.hx +++ b/src/lime/media/WebAudioContext.hx @@ -18,6 +18,16 @@ class WebAudioContext return null; } + public function suspend():Dynamic /*Promise*/ + { + return null; + } + + public function close():Dynamic /*Promise*/ + { + return null; + } + public function createAnalyser():Dynamic /*AnalyserNode*/ { return null;