From db2482dbe298a3e9349775d2768fda8f26cc3bee Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 23 Jun 2026 05:58:41 +0000 Subject: [PATCH 1/2] Update leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx --- leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx b/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx index 5c66cfdd..17da2ff4 100644 --- a/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx +++ b/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx @@ -20,6 +20,7 @@ class LeenkxSendMessageNode extends LogicNode { } override function run(from:Int) { + #if js var connection = inputs[1].get(); if (connection == null) return; var api: String = inputs[2].get(); @@ -348,6 +349,7 @@ class LeenkxSendMessageNode extends LogicNode { return; } } + #end } } From 78ea055aea130b14988f1e0319f92c7a03352e41 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Tue, 23 Jun 2026 14:54:15 -0700 Subject: [PATCH 2/2] Update Aura --- Kha/Backends/Krom/kha/LoaderImpl.hx | 6 +- Kha/Backends/Krom/kha/krom/Sound.hx | 26 ++- Kha/Sources/kha/Sound.hx | 4 +- lib/aura/Backends/hl/aura/aurahl.c | 2 +- lib/aura/Backends/hl/aura/math/FFT.h | 28 +-- lib/aura/Sources/aura/Aura.hx | 49 +++-- lib/aura/Sources/aura/channels/BaseChannel.hx | 16 ++ .../aura/channels/Html5StreamChannel.hx | 195 +++++++++++++----- lib/aura/Sources/aura/channels/MixChannel.hx | 29 +++ .../Sources/aura/channels/StreamChannel.hx | 6 +- lib/aura/Sources/aura/dsp/panner/Panner.hx | 4 +- .../Sources/aura/dsp/panner/StereoPanner.hx | 3 + .../Sources/aura/format/BytesExtension.hx | 17 ++ .../Sources/aura/format/InputExtension.hx | 23 +++ .../aura/format/audio/OggVorbisReader.hx | 86 ++++++++ lib/aura/Sources/aura/math/Vec3.hx | 2 +- lib/aura/Sources/aura/threading/Message.hx | 2 +- lib/aura/khafile.js | 7 +- 18 files changed, 407 insertions(+), 98 deletions(-) create mode 100644 lib/aura/Sources/aura/format/BytesExtension.hx create mode 100644 lib/aura/Sources/aura/format/audio/OggVorbisReader.hx diff --git a/Kha/Backends/Krom/kha/LoaderImpl.hx b/Kha/Backends/Krom/kha/LoaderImpl.hx index 18e60ebf..7a6acdc2 100644 --- a/Kha/Backends/Krom/kha/LoaderImpl.hx +++ b/Kha/Backends/Krom/kha/LoaderImpl.hx @@ -30,15 +30,15 @@ class LoaderImpl { } public static function loadSoundFromDescription(desc: Dynamic, done: kha.Sound->Void, failed: AssetError->Void) { - var sound = Krom.loadSound(desc.files[0]); - if (sound == null) { + var sound = new kha.krom.Sound(desc.files[0]); + if (sound.uncompressedData == null) { failed({ url: desc.files.join(","), error: "Could not load sound(s)", }); } else { - done(new kha.krom.Sound(Bytes.ofData(sound))); + done(sound); } } diff --git a/Kha/Backends/Krom/kha/krom/Sound.hx b/Kha/Backends/Krom/kha/krom/Sound.hx index b874c986..70c02091 100644 --- a/Kha/Backends/Krom/kha/krom/Sound.hx +++ b/Kha/Backends/Krom/kha/krom/Sound.hx @@ -2,24 +2,28 @@ package kha.krom; import haxe.io.Bytes; +using StringTools; + class Sound extends kha.Sound { - public function new(bytes: Bytes) { + public function new(filename: String) { super(); - var count = Std.int(bytes.length / 4); - uncompressedData = new kha.arrays.Float32Array(count); - for (i in 0...count) { - uncompressedData[i] = bytes.getFloat(i * 4); - } + var sound = Krom.loadSound(filename); + if (sound != null) { + var bytes = Bytes.ofData(sound.buffer); + var count = Std.int(bytes.length / 4); + uncompressedData = new kha.arrays.Float32Array(count); + for (i in 0...count) { + uncompressedData[i] = bytes.getFloat(i * 4); + } - compressedData = null; + this.sampleRate = sound.sampleRate; + this.channels = sound.channels; + this.length = sound.length; + } } override public function uncompress(done: Void->Void): Void { done(); } - - override public function unload(): Void { - super.unload(); - } } diff --git a/Kha/Sources/kha/Sound.hx b/Kha/Sources/kha/Sound.hx index 2f249ae2..126b7dcd 100644 --- a/Kha/Sources/kha/Sound.hx +++ b/Kha/Sources/kha/Sound.hx @@ -68,7 +68,7 @@ class Sound implements Resource { var soundBytes = output.getBytes(); var count = Std.int(soundBytes.length / 4); if (header.channel == 1) { - length = count / kha.audio2.Audio.samplesPerSecond; // header.sampleRate; + length = count / header.sampleRate; uncompressedData = new kha.arrays.Float32Array(count * 2); for (i in 0...count) { uncompressedData[i * 2 + 0] = soundBytes.getFloat(i * 4); @@ -76,7 +76,7 @@ class Sound implements Resource { } } else { - length = count / 2 / kha.audio2.Audio.samplesPerSecond; // header.sampleRate; + length = count / 2 / header.sampleRate; uncompressedData = new kha.arrays.Float32Array(count); for (i in 0...count) { uncompressedData[i] = soundBytes.getFloat(i * 4); diff --git a/lib/aura/Backends/hl/aura/aurahl.c b/lib/aura/Backends/hl/aura/aurahl.c index 9a85849e..b3dad93d 100644 --- a/lib/aura/Backends/hl/aura/aurahl.c +++ b/lib/aura/Backends/hl/aura/aurahl.c @@ -1,3 +1,3 @@ // Keep this file so that the headers are included in the compilation -#include "hl/aura/math/FFT.h" +#include "hl/aura/math/fft.h" #include "hl/aura/types/complex_array.h" diff --git a/lib/aura/Backends/hl/aura/math/FFT.h b/lib/aura/Backends/hl/aura/math/FFT.h index 6be88b25..7bcff38f 100644 --- a/lib/aura/Backends/hl/aura/math/FFT.h +++ b/lib/aura/Backends/hl/aura/math/FFT.h @@ -2,27 +2,27 @@ #include -//#include +#include #include "hl/aura/aurahl.h" #include "common_c/math/fft.h" #include "common_c/types/complex_t.h" -//HL_PRIM void AURA_HL_FUNC(ditfft2)(aura__types___ComplexArray__HL_ComplexArrayImpl time_array, int t, aura__types___ComplexArray__HL_ComplexArrayImpl freq_array, int f, int n, int step, bool inverse) { -// const aura_complex_t *times = (aura_complex_t*) time_array->self; -// aura_complex_t *freqs = (aura_complex_t*) freq_array->self; +HL_PRIM void AURA_HL_FUNC(ditfft2)(aura__types___ComplexArray__HL_ComplexArrayImpl time_array, int t, aura__types___ComplexArray__HL_ComplexArrayImpl freq_array, int f, int n, int step, bool inverse) { + const aura_complex_t *times = (aura_complex_t*) time_array->self; + aura_complex_t *freqs = (aura_complex_t*) freq_array->self; -/// aura_ditfft2(times, t, freqs, f, n, step, inverse); -//} + aura_ditfft2(times, t, freqs, f, n, step, inverse); +} -//HL_PRIM void AURA_HL_FUNC(ditfft2_iterative)(aura__types___ComplexArray__HL_ComplexArrayImpl time_array, aura__types___ComplexArray__HL_ComplexArrayImpl freq_array, int n, bool inverse, aura__types___ComplexArray__HL_ComplexArrayImpl exp_rotation_step_table) { -// const aura_complex_t *times = (aura_complex_t*) time_array->self; -// aura_complex_t *freqs = (aura_complex_t*) freq_array->self; +HL_PRIM void AURA_HL_FUNC(ditfft2_iterative)(aura__types___ComplexArray__HL_ComplexArrayImpl time_array, aura__types___ComplexArray__HL_ComplexArrayImpl freq_array, int n, bool inverse, aura__types___ComplexArray__HL_ComplexArrayImpl exp_rotation_step_table) { + const aura_complex_t *times = (aura_complex_t*) time_array->self; + aura_complex_t *freqs = (aura_complex_t*) freq_array->self; -// const aura_complex_t *exp_lut = (aura_complex_t*) exp_rotation_step_table->self; + const aura_complex_t *exp_lut = (aura_complex_t*) exp_rotation_step_table->self; -// aura_ditfft2_iterative(times, freqs, n, inverse, exp_lut); -//} + aura_ditfft2_iterative(times, freqs, n, inverse, exp_lut); +} -//DEFINE_PRIM(_VOID, ditfft2, _BYTES _I32 _BYTES _I32 _I32 _I32 _BOOL) -//DEFINE_PRIM(_VOID, ditfft2_iterative, _BYTES _BYTES _I32 _BOOL _BYTES) +DEFINE_PRIM(_VOID, ditfft2, _BYTES _I32 _BYTES _I32 _I32 _I32 _BOOL) +DEFINE_PRIM(_VOID, ditfft2_iterative, _BYTES _BYTES _I32 _BOOL _BYTES) diff --git a/lib/aura/Sources/aura/Aura.hx b/lib/aura/Sources/aura/Aura.hx index 2ebc1bcf..d6e44c6f 100644 --- a/lib/aura/Sources/aura/Aura.hx +++ b/lib/aura/Sources/aura/Aura.hx @@ -53,6 +53,10 @@ class Aura { static final hrtfs = new Map(); + #if (kha_html5 || kha_debug_html5) + public static var audioContext: js.html.audio.AudioContext; + #end + public static function init(?options: AuraOptions) { sampleRate = kha.audio2.Audio.samplesPerSecond; assert(Critical, sampleRate != 0, "sampleRate must not be 0!"); @@ -63,12 +67,16 @@ class Aura { listener = new Listener(); BufferCache.init(); - - // Sample buffer to prevent allocation - final initialBufferSize = 4096; - if (!BufferCache.getBuffer(TFloat32Array, p_samplesBuffer, 1, initialBufferSize)) { - trace('CRITICAL: Failed to allocate initial sample buffer during Aura.init()!'); + + #if (kha_html5 || kha_debug_html5) + if (kha.SystemImpl.mobile) { + audioContext = kha.js.MobileWebAudio._context; } + else { + audioContext = new js.html.audio.AudioContext(); + } + #end + // Create a few preconfigured mix channels masterChannel = createMixChannel("master"); createMixChannel("music").setMixChannel(masterChannel); @@ -134,16 +142,31 @@ class Aura { } #end - count++; + function onChannelCountInitialized() { + count++; + if (onProgress != null) { + onProgress(count, length, soundName); + } - if (onProgress != null) { - onProgress(count, length, soundName); + if (count == length) { + done(); + } } - if (count == length) { - done(); - return; - } + #if (kha_html5 || kha_debug_html5) + if (kha.SystemImpl.mobile) { + // Mobile web audio channels are always decoded and + // the channel count is set by Kha afterwards + onChannelCountInitialized(); + } + else { + // HACK: Kha does not set sound.channel for compressed + // sounds on non-mobile html5 targets, so do it manually + aura.channels.Html5StreamChannel.initializeChannelCount(sound, onChannelCountInitialized); + } + #else + onChannelCountInitialized(); + #end }, (error: kha.AssetError) -> { onLoadingError(error, failed, soundName); }); } @@ -320,7 +343,7 @@ class Aura { } #if (kha_html5 || kha_debug_html5) - final newChannel = kha.SystemImpl.mobile ? new Html5MobileStreamChannel(sound, loop) : new Html5StreamChannel(sound, loop); + final newChannel = kha.SystemImpl.mobile ? new Html5MobileStreamChannel(sound, loop, cast(mixChannelHandle.channel, MixChannel)) : new Html5StreamChannel(sound, loop, cast(mixChannelHandle.channel, MixChannel)); #else final khaChannel: Null = kha.audio2.Audio1.stream(sound, loop); if (khaChannel == null) { diff --git a/lib/aura/Sources/aura/channels/BaseChannel.hx b/lib/aura/Sources/aura/channels/BaseChannel.hx index b94144d4..08e25e1c 100644 --- a/lib/aura/Sources/aura/channels/BaseChannel.hx +++ b/lib/aura/Sources/aura/channels/BaseChannel.hx @@ -8,6 +8,9 @@ import aura.threading.Message; import aura.types.AudioBuffer; import aura.utils.Interpolator.LinearInterpolator; import aura.utils.MathUtils; +#if (kha_html5 || kha_debug_html5) +import js.html.audio.GainNode; +#end /** Main-thread handle to an audio channel in the audio thread. @@ -90,6 +93,7 @@ class BaseChannelHandle { } if (mixChannelHandle == null) { + channel.cleanUp(); return true; } @@ -105,6 +109,12 @@ class BaseChannelHandle { final success = @:privateAccess mixChannelHandle.addInputChannel(this); if (success) { parentHandle = mixChannelHandle; + #if (kha_html5 || kha_debug_html5) + if (channel is MixChannel) { + channel.gain.disconnect(); + channel.gain.connect(@:privateAccess parentHandle.getMixChannel().gain); + } + #end } else { parentHandle = null; } @@ -164,6 +174,10 @@ abstract class BaseChannel { var paused: Bool = false; var finished: Bool = true; + #if (kha_html5 || kha_debug_html5) + public var gain: GainNode; + #end + abstract function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void; abstract function play(retrigger: Bool): Void; @@ -218,6 +232,8 @@ abstract class BaseChannel { } } + function cleanUp() {} + function parseMessage(message: Message) { switch (message.id) { case ChannelMessageID.Play: play(cast message.data); diff --git a/lib/aura/Sources/aura/channels/Html5StreamChannel.hx b/lib/aura/Sources/aura/channels/Html5StreamChannel.hx index ffa27467..241c9528 100644 --- a/lib/aura/Sources/aura/channels/Html5StreamChannel.hx +++ b/lib/aura/Sources/aura/channels/Html5StreamChannel.hx @@ -10,14 +10,21 @@ import js.html.audio.ChannelMergerNode; import js.html.audio.GainNode; import js.html.audio.MediaElementAudioSourceNode; import js.html.URL; +import js.lib.ArrayBuffer; import kha.SystemImpl; import kha.js.MobileWebAudio; import kha.js.MobileWebAudioChannel; +import aura.Aura; +import aura.format.audio.OggVorbisReader; import aura.threading.Message; import aura.types.AudioBuffer; +using StringTools; + +using aura.format.BytesExtension; + /** Channel dedicated for streaming playback on html5. @@ -36,16 +43,16 @@ import aura.types.AudioBuffer; class Html5StreamChannel extends BaseChannel { static final virtualChannels: Array = []; - final audioContext: AudioContext; - final audioElement: AudioElement; - final source: MediaElementAudioSourceNode; + var audioContext: AudioContext; + var audioElement: AudioElement; + var source: MediaElementAudioSourceNode; - final gain: GainNode; - final leftGain: GainNode; - final rightGain: GainNode; - final attenuationGain: GainNode; - final splitter: ChannelSplitterNode; - final merger: ChannelMergerNode; + var masterGain: GainNode; + var leftGain: GainNode; + var rightGain: GainNode; + var attenuationGain: GainNode; + var splitter: ChannelSplitterNode; + var merger: ChannelMergerNode; var virtualPosition: Float; var lastUpdateTime: Float; @@ -53,13 +60,13 @@ class Html5StreamChannel extends BaseChannel { var dopplerRatio: Float = 1.0; var pitch: Float = 1.0; - public function new(sound: kha.Sound, loop: Bool) { - audioContext = new AudioContext(); + public function new(sound: kha.Sound, loop: Bool, parentChannel: MixChannel) { + audioContext = Aura.audioContext; audioElement = Browser.document.createAudioElement(); source = audioContext.createMediaElementSource(audioElement); - final mimeType = #if kha_debug_html5 "audio/ogg" #else "audio/mp4" #end; - final soundData: js.lib.ArrayBuffer = sound.compressedData.getData(); + final mimeType = sound.compressedData.isByteMagic(0, "OggS") ? "audio/ogg" : "audio/mp4"; + final soundData: ArrayBuffer = sound.compressedData.getData(); final blob = new js.html.Blob([soundData], {type: mimeType}); // TODO: if removing channels, use revokeObjectUrl() ? @@ -67,36 +74,37 @@ class Html5StreamChannel extends BaseChannel { audioElement.src = URL.createObjectURL(blob); audioElement.loop = loop; untyped audioElement.preservesPitch = false; + audioElement.addEventListener("ended", () -> { + stop(); + }); splitter = audioContext.createChannelSplitter(2); leftGain = audioContext.createGain(); rightGain = audioContext.createGain(); attenuationGain = audioContext.createGain(); merger = audioContext.createChannelMerger(2); - gain = audioContext.createGain(); + masterGain = audioContext.createGain(); source.connect(splitter); - // The sound data needs to be decoded because `sounds.channels` returns `0`. - audioContext.decodeAudioData(soundData, function (buffer) { - // TODO: add more cases for Quad and 5.1 ? - https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#audio_channels - switch (buffer.numberOfChannels) { - case 1: - splitter.connect(leftGain, 0); - splitter.connect(rightGain, 0); - case 2: - splitter.connect(leftGain, 0); - splitter.connect(rightGain, 1); - default: - } - }); + // TODO: add more cases for Quad and 5.1 ? - https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#audio_channels + switch (sound.channels) { + case 1: + splitter.connect(leftGain, 0); + splitter.connect(rightGain, 0); + case 2: + splitter.connect(leftGain, 0); + splitter.connect(rightGain, 1); + default: + throw 'Unsupported channel count: ${sound.channels}'; + } leftGain.connect(merger, 0, 0); rightGain.connect(merger, 0, 1); merger.connect(attenuationGain); - attenuationGain.connect(gain); + attenuationGain.connect(masterGain); - gain.connect(audioContext.destination); + masterGain.connect(parentChannel.gain); if (isVirtual()) { virtualChannels.push(this); @@ -177,20 +185,45 @@ class Html5StreamChannel extends BaseChannel { finished = true; } + /** + Clean up Web Audio nodes. Called automatically when `BaseChannelHandle.setMixChannel(null)` is used. + **/ + override function cleanUp() { + source.disconnect(); + splitter.disconnect(); + leftGain.disconnect(); + rightGain.disconnect(); + merger.disconnect(); + attenuationGain.disconnect(); + masterGain.disconnect(); + audioElement.pause(); + audioElement.src = ""; + URL.revokeObjectURL(audioElement.src); + + source = null; + splitter = null; + leftGain = null; + rightGain = null; + merger = null; + attenuationGain = null; + masterGain = null; + audioElement = null; + } + function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {} override function parseMessage(message: Message) { switch (message.id) { // Because we're using a HTML implementation here, we cannot use the // LinearInterpolator parameters - case ChannelMessageID.PVolume: attenuationGain.gain.value = cast message.data; + case ChannelMessageID.PVolume: masterGain.gain.value = cast message.data; case ChannelMessageID.PPitch: pitch = cast message.data; updatePlaybackRate(); case ChannelMessageID.PDopplerRatio: dopplerRatio = cast message.data; updatePlaybackRate(); - case ChannelMessageID.PDstAttenuation: gain.gain.value = cast message.data; + case ChannelMessageID.PDstAttenuation: attenuationGain.gain.value = cast message.data; case ChannelMessageID.PVolumeLeft: leftGain.gain.value = cast message.data; case ChannelMessageID.PVolumeRight: rightGain.gain.value = cast message.data; @@ -217,24 +250,23 @@ class Html5StreamChannel extends BaseChannel { https://github.com/Kode/Kha/commit/12494b1112b64e4286b6a2fafc0f08462c1e7971 **/ class Html5MobileStreamChannel extends BaseChannel { - final audioContext: AudioContext; - final khaChannel: kha.js.MobileWebAudioChannel; + var audioContext: AudioContext; + var khaChannel: kha.js.MobileWebAudioChannel; + var parentChannel: MixChannel; - final leftGain: GainNode; - final rightGain: GainNode; - final attenuationGain: GainNode; - final splitter: ChannelSplitterNode; - final merger: ChannelMergerNode; + var leftGain: GainNode; + var rightGain: GainNode; + var attenuationGain: GainNode; + var splitter: ChannelSplitterNode; + var merger: ChannelMergerNode; var dopplerRatio: Float = 1.0; var pitch: Float = 1.0; - public function new(sound: kha.Sound, loop: Bool) { - audioContext = MobileWebAudio._context; + public function new(sound: kha.Sound, loop: Bool, pc: MixChannel) { + audioContext = Aura.audioContext; khaChannel = new kha.js.MobileWebAudioChannel(cast sound, loop); - - @:privateAccess khaChannel.gain.disconnect(audioContext.destination); - @:privateAccess khaChannel.source.disconnect(@:privateAccess khaChannel.gain); + parentChannel = pc; splitter = audioContext.createChannelSplitter(2); leftGain = audioContext.createGain(); @@ -242,8 +274,6 @@ class Html5MobileStreamChannel extends BaseChannel { merger = audioContext.createChannelMerger(2); attenuationGain = audioContext.createGain(); - @:privateAccess khaChannel.source.connect(splitter); - // TODO: add more cases for Quad and 5.1 ? - https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#audio_channels switch (sound.channels) { case 1: @@ -253,6 +283,7 @@ class Html5MobileStreamChannel extends BaseChannel { splitter.connect(leftGain, 0); splitter.connect(rightGain, 1); default: + throw 'Unsupported channel count: ${sound.channels}'; } leftGain.connect(merger, 0, 0); @@ -260,14 +291,19 @@ class Html5MobileStreamChannel extends BaseChannel { merger.connect(attenuationGain); attenuationGain.connect(@:privateAccess khaChannel.gain); - @:privateAccess khaChannel.gain.connect(audioContext.destination); + reconnectKhaChannelNodes(); } public function play(retrigger: Bool) { if (retrigger) { khaChannel.position = 0; } + + @:privateAccess khaChannel.source.onended = null; khaChannel.play(); + // `MobileWebAudioChannel` recreates a 'source' when `khaChannel.play()` is called + // Reconnect 'source' and 'gain' to the proper nodes + reconnectKhaChannelNodes(); paused = false; finished = false; @@ -283,6 +319,30 @@ class Html5MobileStreamChannel extends BaseChannel { finished = true; } + /** + Clean up Web Audio nodes. Called automatically when `BaseChannelHandle.setMixChannel(null)` is used. + **/ + override function cleanUp() { + @:privateAccess khaChannel.source.onended = null; + @:privateAccess khaChannel.source.disconnect(); + splitter.disconnect(); + leftGain.disconnect(); + rightGain.disconnect(); + merger.disconnect(); + attenuationGain.disconnect(); + @:privateAccess khaChannel.gain.disconnect(); + khaChannel.stop(); + + @:privateAccess khaChannel.gain = null; + @:privateAccess khaChannel.source = null; + splitter = null; + leftGain = null; + rightGain = null; + merger = null; + attenuationGain = null; + khaChannel = null; + } + function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {} override function parseMessage(message: Message) { @@ -311,6 +371,49 @@ class Html5MobileStreamChannel extends BaseChannel { } catch (e) {} } + + function reconnectKhaChannelNodes() { + @:privateAccess khaChannel.gain.disconnect(); + @:privateAccess khaChannel.source.disconnect(); + @:privateAccess khaChannel.source.connect(splitter); + @:privateAccess khaChannel.source.onended = stop; + @:privateAccess khaChannel.gain.connect(parentChannel.gain); + } +} + +function initializeChannelCount(sound: kha.Sound, done: Void->Void) { + /* + Peek into the file to detect the file format, Kha sadly does not expose + this information. + + Using `Reflect.field(kha.Assets.sounds, soundName + "Description").files` + (i.e. the data in files.json generated by Khamake) would introduce a + dependency on Kha internals: A `kha.Sound` can be backed by multiple + exported files and Kha's `LoaderImpl` decides on the order in which + those files are tried to be loaded. + */ + final isOgg = sound.compressedData.isByteMagic(0, "OggS"); + if (isOgg) { + final oggReader = new OggVorbisReader(sound.compressedData); + sound.channels = oggReader.getNumChannels(); + done(); + } + else { + /* + In case of other formats, try to let the JS runtime decode the + entire sound data. + + HACK: decodeAudioData() detaches the array buffer but requires a + non-detached buffer, so a clone is made to ensure that the array + buffer of sound.compressedData is never detached and can still be + used by other code. + */ + final soundDataClone: ArrayBuffer = sound.compressedData.getData().slice(0); + kha.audio2.Audio._context.decodeAudioData(soundDataClone, function (buffer) { + sound.channels = buffer.numberOfChannels; + done(); + }); + } } #end diff --git a/lib/aura/Sources/aura/channels/MixChannel.hx b/lib/aura/Sources/aura/channels/MixChannel.hx index da6350e2..b099f4b8 100644 --- a/lib/aura/Sources/aura/channels/MixChannel.hx +++ b/lib/aura/Sources/aura/channels/MixChannel.hx @@ -6,6 +6,11 @@ import haxe.ds.Vector; import sys.thread.Mutex; #end +#if (kha_html5 || kha_debug_html5) +import aura.Aura; +import js.html.audio.AudioContext; +#end + import aura.channels.BaseChannel.BaseChannelHandle; import aura.threading.BufferCache; import aura.threading.Message; @@ -89,7 +94,17 @@ class MixChannel extends BaseChannel { **/ var inputChannelsCopy: Vector; + #if (kha_html5 || kha_debug_html5) + var audioContext: AudioContext; + #end + public function new() { + #if (kha_html5 || kha_debug_html5) + audioContext = Aura.audioContext; + gain = audioContext.createGain(); + gain.connect(audioContext.destination); + #end + inputChannels = new Vector(channelSize); // Make sure super.isPlayable() is true until we find better semantics @@ -296,4 +311,18 @@ class MixChannel extends BaseChannel { } } } + + #if (kha_html5 || kha_debug_html5) + //TODO: add the rest of the messages for effects or create a separate `Html5MixChannel` class? + override function parseMessage(message: Message) { + switch (message.id) { + // Because we're using a HTML implementation here, we cannot use the + // LinearInterpolator parameters + case ChannelMessageID.PVolume: gain.gain.value = cast message.data; + + default: + super.parseMessage(message); + } + } + #end } diff --git a/lib/aura/Sources/aura/channels/StreamChannel.hx b/lib/aura/Sources/aura/channels/StreamChannel.hx index 5064d6e1..5ef724bc 100644 --- a/lib/aura/Sources/aura/channels/StreamChannel.hx +++ b/lib/aura/Sources/aura/channels/StreamChannel.hx @@ -44,8 +44,12 @@ class StreamChannel extends BaseChannel { } final khaBuffer = p_khaBuffer.get(); - khaChannel.nextSamples(khaBuffer, requestedSamples.channelLength, sampleRate); + khaChannel.nextSamples(khaBuffer, requestedSamples.numChannels * requestedSamples.channelLength, sampleRate); requestedSamples.deinterleaveFromFloat32Array(khaBuffer, requestedSamples.numChannels); + + if (khaChannel.finished) { + finished = true; + } } override function parseMessage(message: Message) { diff --git a/lib/aura/Sources/aura/dsp/panner/Panner.hx b/lib/aura/Sources/aura/dsp/panner/Panner.hx index 52e5e117..63e0893c 100644 --- a/lib/aura/Sources/aura/dsp/panner/Panner.hx +++ b/lib/aura/Sources/aura/dsp/panner/Panner.hx @@ -43,7 +43,7 @@ abstract class Panner extends DSP { public function new(handle: BaseChannelHandle) { this.inUse = true; // Don't allow using panners with addInsert() this.handle = handle; - this.handle.channel.panner = this; + handle.channel.panner = this; } public inline function setHandle(handle: BaseChannelHandle) { @@ -52,7 +52,7 @@ abstract class Panner extends DSP { } reset3D(); this.handle = handle; - this.handle.channel.panner = this; + handle.channel.panner = this; } /** diff --git a/lib/aura/Sources/aura/dsp/panner/StereoPanner.hx b/lib/aura/Sources/aura/dsp/panner/StereoPanner.hx index 7e7248c3..05373f0a 100644 --- a/lib/aura/Sources/aura/dsp/panner/StereoPanner.hx +++ b/lib/aura/Sources/aura/dsp/panner/StereoPanner.hx @@ -61,10 +61,13 @@ class StereoPanner extends Panner { public inline function setBalance(balance: Balance) { this._balance = balance; + final volumeLeft = Math.sqrt(~balance); final volumeRight = Math.sqrt(balance); + sendMessage({ id: StereoPannerMessageID.PVolumeLeft, data: volumeLeft }); sendMessage({ id: StereoPannerMessageID.PVolumeRight, data: volumeRight }); + #if (kha_html5 || kha_debug_html5) handle.channel.sendMessage({ id: ChannelMessageID.PVolumeLeft, data: volumeLeft }); handle.channel.sendMessage({ id: ChannelMessageID.PVolumeRight, data: volumeRight }); diff --git a/lib/aura/Sources/aura/format/BytesExtension.hx b/lib/aura/Sources/aura/format/BytesExtension.hx new file mode 100644 index 00000000..4f1740b8 --- /dev/null +++ b/lib/aura/Sources/aura/format/BytesExtension.hx @@ -0,0 +1,17 @@ +package aura.format; + +import haxe.io.Bytes; + +using StringTools; + +/** + Variant of `aura.format.InputExtension.isByteMagic()` for `haxe.io.Bytes`. +**/ +inline function isByteMagic(bytes: Bytes, position: Int, magicASCII: String): Bool { + var match = true; + for (i in 0...magicASCII.length) { + match = match && bytes.get(position + i) == magicASCII.fastCodeAt(i); + } + + return match; +} diff --git a/lib/aura/Sources/aura/format/InputExtension.hx b/lib/aura/Sources/aura/format/InputExtension.hx index 7d8d536a..7dd72317 100644 --- a/lib/aura/Sources/aura/format/InputExtension.hx +++ b/lib/aura/Sources/aura/format/InputExtension.hx @@ -3,6 +3,8 @@ package aura.format; import haxe.Int64; import haxe.io.Input; +using StringTools; + inline function readInt64(inp: Input): Int64 { final first = inp.readInt32(); final second = inp.readInt32(); @@ -19,3 +21,24 @@ inline function readUInt32(inp: Input): Int64 { return out; } + +/** + Platform- and encoding-independent way of matching the input with an ASCII + magic string. This function does not consider the input endianess, it is + assumed that the order of characters in `magicASCII` matches the byte order + in the input stream. + + - `inp.readString(len, haxe.io.Encoding.UTF8)` does not work if the input + streams contains data that can be interpreted as multi-byte characters. + + - `inp.readString(len, haxe.io.Encoding.RawNative)` does not yield + platform-indepent results. +**/ +inline function isByteMagic(inp: Input, magicASCII: String): Bool { + var match = true; + for (i in 0...magicASCII.length) { + match = match && inp.readByte() == magicASCII.fastCodeAt(i); + } + + return match; +} diff --git a/lib/aura/Sources/aura/format/audio/OggVorbisReader.hx b/lib/aura/Sources/aura/format/audio/OggVorbisReader.hx new file mode 100644 index 00000000..992fa638 --- /dev/null +++ b/lib/aura/Sources/aura/format/audio/OggVorbisReader.hx @@ -0,0 +1,86 @@ +/** + Ogg layout: + https://en.wikipedia.org/wiki/Ogg#Page_structure + + Vorbis layout: + https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2 + https://wiki.xiph.org/OggVorbis +**/ + +package aura.format.audio; + +import haxe.io.Bytes; +import haxe.io.BytesInput; + +using aura.format.InputExtension; + +class OggVorbisReader { + + static inline final OGG_PAGE_HEADER_TYPE_BEG_OF_STREAM = 2; + static inline final VORBIS_PACKET_TYPE_IDENTIFICATION = 1; + + final inp: BytesInput; + + final firstSegmentPosition = 0; + final segmentTable: Bytes; + + public inline function new(bytes: Bytes) { + this.inp = new BytesInput(bytes); + inp.bigEndian = false; + + if (!inp.isByteMagic("OggS")) { + throw "Cannot read .ogg file, file does not start with 'OggS' magic"; + } + + inp.position += 1; // Skip version + + final oggHeaderType = inp.readByte(); + if (oggHeaderType != OGG_PAGE_HEADER_TYPE_BEG_OF_STREAM) { + throw "Cannot read .ogg file, first header type was expected to be 'Beginning Of Stream'"; + } + + inp.position += 8; // Skip granule position + inp.position += 4; // Skip bitstream serial number + + final pageSequenceNumber = inp.readUInt32(); + if (pageSequenceNumber != 0) { + throw "Cannot read .ogg file, first page sequence number was expected to be 0"; + } + + inp.position += 4; // Skip checksum (for now) + + final numPageSegments = inp.readByte(); + if (numPageSegments == 0) { + throw "Cannot read .ogg file, first page has no segments"; + } + + segmentTable = Bytes.alloc(numPageSegments); + inp.readFullBytes(segmentTable, 0, numPageSegments); + + firstSegmentPosition = inp.position; + + final packetType = inp.readByte(); + if (packetType != VORBIS_PACKET_TYPE_IDENTIFICATION) { + throw "Cannot read .ogg file, Vorbis identification header expected"; + } + + if (!inp.isByteMagic("vorbis")) { + throw "Cannot read .ogg file, only Ogg Vorbis files are supported"; + } + + // See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2, version must be 0 + final version = inp.readUInt32(); + if (version != 0) { + throw "Cannot read .ogg file, Vorbis version field expected to be 0"; + } + } + + public function getNumChannels(): Int { + inp.position = firstSegmentPosition + 1 + 6 + 4; // Skip packet type + vorbis identifier + version + + final numChannels = inp.readByte(); + assert(Critical, numChannels > 0); + + return numChannels; + } +} diff --git a/lib/aura/Sources/aura/math/Vec3.hx b/lib/aura/Sources/aura/math/Vec3.hx index 44eaffd4..8bf8fc1a 100644 --- a/lib/aura/Sources/aura/math/Vec3.hx +++ b/lib/aura/Sources/aura/math/Vec3.hx @@ -30,7 +30,7 @@ abstract Vec3(FastVector3) from FastVector3 to FastVector3 { return new FastVector4(this.x, this.y, this.z); } - #if (AURA_WITH_IRON || leenkx) + #if (AURA_WITH_IRON || armory) @:from public static inline function fromIronVec3(v: iron.math.Vec3): Vec3{ return new FastVector3(v.x, v.y, v.z); diff --git a/lib/aura/Sources/aura/threading/Message.hx b/lib/aura/Sources/aura/threading/Message.hx index 416b9e36..6577f070 100644 --- a/lib/aura/Sources/aura/threading/Message.hx +++ b/lib/aura/Sources/aura/threading/Message.hx @@ -25,7 +25,7 @@ class ChannelMessageID extends MessageID { final PPitch; final PDopplerRatio; final PDstAttenuation; - + #if (kha_html5 || kha_debug_html5) final PVolumeLeft; final PVolumeRight; diff --git a/lib/aura/khafile.js b/lib/aura/khafile.js index 994861a7..d91f3dcb 100644 --- a/lib/aura/khafile.js +++ b/lib/aura/khafile.js @@ -7,12 +7,13 @@ const targetsHL = ["windows-hl", "linux-hl", "macos-hl", "osx-hl", "android-hl", const targetsCPP = ["windows", "linux", "macos", "osx"]; const targetsHTML5 = ["html5", "debug-html5"]; -function addBackends(project) { - project.localLibraryPath = "Backends"; +async function addBackends(project) { + //project.localLibraryPath = "Backends"; const isHL = targetsHL.indexOf(Project.platform) >= 0; if (isHL) { + await project.addProject("Backends/hl"); project.addDefine("AURA_BACKEND_HL"); console.log("[Aura] Using HL/C backend"); } @@ -31,7 +32,7 @@ async function main() { project.addSources('Sources'); if (process.argv.indexOf("--aura-no-backend") == -1) { - addBackends(project); + await addBackends(project); } else { project.addDefine("AURA_NO_BACKEND");