diff --git a/Backends/HTML5/kha/audio2/Audio.hx b/Backends/HTML5/kha/audio2/Audio.hx index 3ef780fb9..c93c5556d 100644 --- a/Backends/HTML5/kha/audio2/Audio.hx +++ b/Backends/HTML5/kha/audio2/Audio.hx @@ -1,21 +1,36 @@ package kha.audio2; +import js.Browser.document; +import js.Browser.window; import js.Browser; import js.Syntax; +import js.html.MessagePort; import js.html.URL; import js.html.audio.AudioContext; +import js.html.audio.AudioDestinationNode; import js.html.audio.AudioProcessingEvent; import js.html.audio.ScriptProcessorNode; +import js.lib.Promise; import kha.Sound; +import kha.audio2.AudioWorkletProcessorJs; import kha.internal.IntBox; import kha.js.AEAudioChannel; +@:native("AudioWorkletNode") +private extern class AudioWorkletNode { + var port: MessagePort; + function new(context: AudioContext, name: String, options: {outputChannelCount: Array}); + + function connect(arg: AudioDestinationNode): Void; +} + class Audio { public static var disableGcInteractions = false; static var intBox: IntBox = new IntBox(0); static var buffer: Buffer; @:noCompletion public static var _context: AudioContext; static var processingNode: ScriptProcessorNode; + static var workletNode: AudioWorkletNode; static function initContext(): Void { try { @@ -37,13 +52,26 @@ class Audio { return false; Audio.samplesPerSecond = Math.round(_context.sampleRate); - var bufferSize = 1024 * 2; + final bufferSize = 1024 * 2; buffer = new Buffer(bufferSize * 4, 2, Std.int(_context.sampleRate)); + final useWorklet = window.isSecureContext && (_context : Dynamic).audioWorklet != null; + // final useWorklet = false; + + if (useWorklet) { + initAudioWorklet(bufferSize); + } + else { + initOnAudioProcess(bufferSize); + } + return true; + } + + static function initOnAudioProcess(bufferSize: Int): Void { processingNode = _context.createScriptProcessor(bufferSize, 0, 2); - processingNode.onaudioprocess = function(e: AudioProcessingEvent) { - var output1 = e.outputBuffer.getChannelData(0); - var output2 = e.outputBuffer.getChannelData(1); + processingNode.onaudioprocess = (e: AudioProcessingEvent) -> { + final output1 = e.outputBuffer.getChannelData(0); + final output2 = e.outputBuffer.getChannelData(1); if (audioCallback != null) { intBox.value = e.outputBuffer.length * 2; audioCallback(intBox, buffer); @@ -65,7 +93,48 @@ class Audio { } } processingNode.connect(_context.destination); - return true; + } + + static function initAudioWorklet(bufferSize: Int): Void { + final workletName = "kha-audio-processor"; + // Create a blob for the worklet processor code + final pjs = AudioWorkletProcessorJs.getProcessorJs(workletName); + final blob = new js.html.Blob([pjs], {type: "application/javascript"}); + final url = URL.createObjectURL(blob); + final promise: Promise = (_context : Dynamic).audioWorklet.addModule(url); + promise.then(_ -> { + workletNode = new AudioWorkletNode(_context, workletName, {outputChannelCount: [2]}); + workletNode.connect(_context.destination); + + final bufferLength = bufferSize * 2; + final audioArray = new js.lib.Float32Array(bufferLength); + + // Send buffer data to worklet on each frame + final sendBuffer = () -> { + // throttled web pages will make strange sounds, mute instead + if (document.visibilityState != VISIBLE) { + workletNode.port.postMessage([]); + return; + } + if (audioCallback == null) + return; + intBox.value = bufferLength; + audioCallback(intBox, buffer); + for (i in 0...bufferLength) { + audioArray[i] = buffer.data.get(buffer.readLocation); + buffer.readLocation++; + if (buffer.readLocation >= buffer.size) { + buffer.readLocation = 0; + } + } + workletNode.port.postMessage(audioArray); + } + // worklet requests more data + workletNode.port.onmessage = (_) -> { + sendBuffer(); + } + sendBuffer(); + }); } public static var samplesPerSecond: Int; @@ -84,7 +153,7 @@ class Audio { public static function stream(sound: Sound, loop: Bool = false): kha.audio1.AudioChannel { // var source = _context.createMediaStreamSource(cast sound.compressedData.getData()); // source.connect(_context.destination); - var element = Browser.document.createAudioElement(); + var element = document.createAudioElement(); #if kha_debug_html5 var blob = new js.html.Blob([sound.compressedData.getData()], {type: "audio/ogg"}); #else diff --git a/Backends/HTML5/kha/audio2/AudioWorkletProcessorJs.hx b/Backends/HTML5/kha/audio2/AudioWorkletProcessorJs.hx new file mode 100644 index 000000000..780b84b48 --- /dev/null +++ b/Backends/HTML5/kha/audio2/AudioWorkletProcessorJs.hx @@ -0,0 +1,41 @@ +package kha.audio2; + +@:noDoc +class AudioWorkletProcessorJs { + public static function getProcessorJs(name: String): String { + return ' + class KhaAudioProcessor extends AudioWorkletProcessor { + needMoreData = 0 + constructor() { + super(); + this.buffer = []; + this.nextBuffer = []; + this.port.onmessage = (event) => { + this.nextBuffer = event.data; + }; + } + process(inputs, outputs, parameters) { + const output = outputs[0]; + if (this.buffer.length < output[0].length * 2) { + this.buffer = this.nextBuffer; + this.port.postMessage(this.needMoreData); + } + if (this.buffer.length >= output[0].length * 2) { + for (let i = 0; i < output[0].length; i++) { + output[0][i] = this.buffer[i * 2]; + output[1][i] = this.buffer[i * 2 + 1]; + } + this.buffer = this.buffer.slice(output[0].length * 2); + } else { + for (let i = 0; i < output[0].length; i++) { + output[0][i] = 0; + output[1][i] = 0; + } + } + return true; + } + } + registerProcessor("$name", KhaAudioProcessor); + '; + } +}