Update Files

This commit is contained in:
2025-01-22 16:18:30 +01:00
parent ed4603cf95
commit a36294b518
16718 changed files with 2960346 additions and 0 deletions

View File

@ -0,0 +1,244 @@
package aura.channels;
import aura.channels.MixChannel.MixChannelHandle;
import aura.dsp.DSP;
import aura.dsp.panner.Panner;
import aura.threading.Fifo;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.Interpolator.LinearInterpolator;
import aura.utils.MathUtils;
/**
Main-thread handle to an audio channel in the audio thread.
**/
@:access(aura.channels.BaseChannel)
@:allow(aura.dsp.panner.Panner)
class BaseChannelHandle {
/**
Whether the playback of the handle's channel is currently paused.
**/
public var paused(get, never): Bool;
inline function get_paused(): Bool { return channel.paused; }
/**
Whether the playback of the handle's channel has finished.
On `MixerChannel`s this value is always `false`.
**/
public var finished(get, never): Bool;
inline function get_finished(): Bool { return channel.finished; }
public var panner(get, null): Null<Panner>;
inline function get_panner(): Null<Panner> { return channel.panner; }
/**
Link to the audio channel in the audio thread.
**/
final channel: BaseChannel;
var parentHandle: Null<MixChannelHandle> = null;
// Parameter cache for getter functions
var _volume: Float = 1.0;
var _pitch: Float = 1.0;
public inline function new(channel: BaseChannel) {
this.channel = channel;
}
/**
Starts the playback. If the sound wasn't played before or was stopped,
the playback starts from the beginning. If it is paused, playback starts
from the position where it was paused.
@param retrigger Controls the behaviour if the sound is already playing.
If true, restart playback from the beginning, else do nothing.
**/
public inline function play(retrigger = false) {
channel.sendMessage({ id: ChannelMessageID.Play, data: retrigger });
}
public inline function pause() {
channel.sendMessage({ id: ChannelMessageID.Pause, data: null });
}
public inline function stop() {
channel.sendMessage({ id: ChannelMessageID.Stop, data: null });
}
public inline function addInsert(insert: DSP): DSP {
return channel.addInsert(insert);
}
public inline function removeInsert(insert: DSP) {
channel.removeInsert(insert);
}
/**
Set the mix channel into which this channel routes its output.
Returns `true` if setting the mix channel was successful and `false` if
there would be a circular dependency or the amount of input channels of
the mix channel is already maxed out.
**/
public function setMixChannel(mixChannelHandle: MixChannelHandle): Bool {
if (mixChannelHandle == parentHandle) {
return true;
}
if (parentHandle != null) {
@:privateAccess parentHandle.removeInputChannel(this);
parentHandle = null;
}
if (mixChannelHandle == null) {
return true;
}
// Return false for circular references (including mixChannelHandle == this)
var curHandle = mixChannelHandle;
while (curHandle != null) {
if (curHandle == this) {
return false;
}
curHandle = curHandle.parentHandle;
}
final success = @:privateAccess mixChannelHandle.addInputChannel(this);
if (success) {
parentHandle = mixChannelHandle;
} else {
parentHandle = null;
}
return success;
}
public inline function setVolume(volume: Float) {
assert(Critical, volume >= 0, "Volume value must not be a negative number!");
channel.sendMessage({ id: ChannelMessageID.PVolume, data: maxF(0.0, volume) });
this._volume = volume;
}
public inline function getVolume(): Float {
return this._volume;
}
public inline function setPitch(pitch: Float) {
assert(Critical, pitch > 0, "Pitch value must be a positive number!");
channel.sendMessage({ id: ChannelMessageID.PPitch, data: maxF(0.0, pitch) });
this._pitch = pitch;
}
public inline function getPitch(): Float {
return this._pitch;
}
#if AURA_DEBUG
public function getDebugAttrs(): Map<String, String> {
return ["In use" => Std.string(@:privateAccess channel.isPlayable())];
}
#end
}
/**
Base class of all audio channels in the audio thread.
**/
@:allow(aura.Aura)
@:access(aura.dsp.DSP)
@:allow(aura.dsp.panner.Panner)
@:access(aura.dsp.panner.Panner)
abstract class BaseChannel {
final messages: Fifo<Message> = new Fifo();
final inserts: Array<DSP> = [];
var panner: Null<Panner> = null;
// Parameters
final pVolume = new LinearInterpolator(1.0);
final pDopplerRatio = new LinearInterpolator(1.0);
final pDstAttenuation = new LinearInterpolator(1.0);
var treeLevel(default, null): Int = 0;
var paused: Bool = false;
var finished: Bool = true;
abstract function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void;
abstract function play(retrigger: Bool): Void;
abstract function pause(): Void;
abstract function stop(): Void;
function isPlayable(): Bool {
return !paused && !finished;
}
function setTreeLevel(level: Int) {
this.treeLevel = level;
}
inline function processInserts(buffer: AudioBuffer) {
for (insert in inserts) {
if (insert.bypass) { continue; }
insert.process(buffer);
}
if (panner != null) {
panner.process(buffer);
}
}
inline function addInsert(insert: DSP): DSP {
assert(Critical, !insert.inUse, "DSP objects can only belong to one unique channel");
insert.inUse = true;
inserts.push(insert);
return insert;
}
inline function removeInsert(insert: DSP) {
var found = inserts.remove(insert);
if (found) {
insert.inUse = false;
}
}
function synchronize() {
var message: Null<Message>;
while ((message = messages.tryPop()) != null) {
parseMessage(message);
}
for (insert in inserts) {
insert.synchronize();
}
if (panner != null) {
panner.synchronize();
}
}
function parseMessage(message: Message) {
switch (message.id) {
case ChannelMessageID.Play: play(cast message.data);
case ChannelMessageID.Pause: pause();
case ChannelMessageID.Stop: stop();
case ChannelMessageID.PVolume: pVolume.targetValue = cast message.data;
case ChannelMessageID.PDopplerRatio: pDopplerRatio.targetValue = cast message.data;
case ChannelMessageID.PDstAttenuation: pDstAttenuation.targetValue = cast message.data;
default:
}
}
inline function sendMessage(message: Message) {
messages.add(message);
}
}
enum abstract AttenuationMode(Int) {
var Linear;
var Inverse;
var Exponential;
}

View File

@ -0,0 +1,192 @@
package aura.channels;
#if (kha_html5 || kha_debug_html5)
import js.Browser;
import js.html.AudioElement;
import js.html.URL;
import kha.SystemImpl;
import kha.js.MobileWebAudioChannel;
import aura.threading.Message;
import aura.types.AudioBuffer;
/**
Channel dedicated for streaming playback on html5.
Because most browsers don't allow audio playback before the user has
interacted with the website or canvas at least once, we can't always play
audio without causing an exception. In order to not cause chaos with sounds
playing at wrong times, sounds are virtualized before they can actually be
played. This means that their playback position is tracked and as soon as
the user interacts with the web page, the audio starts playing at the
correct position as if the sound would be playing all the time since it was
started.
Note that on mobile browsers the `aura.channels.Html5MobileStreamChannel` is
used instead.
**/
class Html5StreamChannel extends BaseChannel {
static final virtualChannels: Array<Html5StreamChannel> = [];
final audioElement: AudioElement;
var virtualPosition: Float;
var lastUpdateTime: Float;
public function new(sound: kha.Sound, loop: Bool) {
audioElement = Browser.document.createAudioElement();
final mimeType = #if kha_debug_html5 "audio/ogg" #else "audio/mp4" #end;
final blob = new js.html.Blob([sound.compressedData.getData()], {type: mimeType});
// TODO: if removing channels, use revokeObjectUrl() ?
// see https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
audioElement.src = URL.createObjectURL(blob);
audioElement.loop = loop;
if (isVirtual()) {
virtualChannels.push(this);
}
}
inline function isVirtual(): Bool {
return !SystemImpl.mobileAudioPlaying;
}
@:allow(aura.Aura)
static function makeChannelsPhysical() {
for (channel in virtualChannels) {
channel.updateVirtualPosition();
channel.audioElement.currentTime = channel.virtualPosition;
if (!channel.finished && !channel.paused) {
channel.audioElement.play();
}
}
virtualChannels.resize(0);
}
inline function updateVirtualPosition() {
final now = kha.Scheduler.realTime();
if (finished) {
virtualPosition = 0;
}
else if (!paused) {
virtualPosition += now - lastUpdateTime;
while (virtualPosition > audioElement.duration) {
virtualPosition -= audioElement.duration;
}
}
lastUpdateTime = now;
}
public function play(retrigger: Bool) {
if (isVirtual()) {
updateVirtualPosition();
if (retrigger) {
virtualPosition = 0;
}
}
else {
audioElement.play();
if (retrigger) {
audioElement.currentTime = 0;
}
}
paused = false;
finished = false;
}
public function pause() {
if (isVirtual()) {
updateVirtualPosition();
}
else {
audioElement.pause();
}
paused = true;
}
public function stop() {
if (isVirtual()) {
updateVirtualPosition();
}
else {
audioElement.pause();
audioElement.currentTime = 0;
}
finished = true;
}
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: audioElement.volume = cast message.data;
case ChannelMessageID.PPitch:
case ChannelMessageID.PDopplerRatio:
case ChannelMessageID.PDstAttenuation:
default:
super.parseMessage(message);
}
}
}
/**
Wrapper around kha.js.MobileWebAudioChannel.
See https://github.com/Kode/Kha/issues/299 and
https://github.com/Kode/Kha/commit/12494b1112b64e4286b6a2fafc0f08462c1e7971
**/
class Html5MobileStreamChannel extends BaseChannel {
final khaChannel: kha.js.MobileWebAudioChannel;
public function new(sound: kha.Sound, loop: Bool) {
khaChannel = new kha.js.MobileWebAudioChannel(cast sound, loop);
}
public function play(retrigger: Bool) {
if (retrigger) {
khaChannel.position = 0;
}
khaChannel.play();
paused = false;
finished = false;
}
public function pause() {
khaChannel.pause();
paused = true;
}
public function stop() {
khaChannel.stop();
finished = true;
}
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: khaChannel.volume = cast message.data;
case ChannelMessageID.PPitch:
case ChannelMessageID.PDopplerRatio:
case ChannelMessageID.PDstAttenuation:
default:
super.parseMessage(message);
}
}
}
#end

View File

@ -0,0 +1,299 @@
package aura.channels;
import haxe.ds.Vector;
#if cpp
import sys.thread.Mutex;
#end
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.threading.BufferCache;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.Profiler;
/**
Main-thread handle to a `MixChannel` in the audio thread.
**/
class MixChannelHandle extends BaseChannelHandle {
#if AURA_DEBUG
public var name: String = "";
public var inputHandles: Array<BaseChannelHandle> = new Array();
#end
public inline function getNumInputs(): Int {
return getMixChannel().getNumInputs();
}
/**
Adds an input channel. Returns `true` if adding the channel was
successful, `false` if the amount of input channels is already maxed
out.
**/
inline function addInputChannel(channelHandle: BaseChannelHandle): Bool {
assert(Error, channelHandle != null, "channelHandle must not be null");
final foundChannel = getMixChannel().addInputChannel(channelHandle.channel);
#if AURA_DEBUG
if (foundChannel) inputHandles.push(channelHandle);
#end
return foundChannel;
}
/**
Removes an input channel from this `MixChannel`.
**/
inline function removeInputChannel(channelHandle: BaseChannelHandle) {
#if AURA_DEBUG
inputHandles.remove(channelHandle);
#end
getMixChannel().removeInputChannel(channelHandle.channel);
}
inline function getMixChannel(): MixChannel {
return cast this.channel;
}
#if AURA_DEBUG
public override function getDebugAttrs(): Map<String, String> {
return super.getDebugAttrs().mergeIntoThis([
"Name" => name,
"Num inserts" => Std.string(@:privateAccess channel.inserts.length),
]);
}
#end
}
/**
A channel that mixes together the output of multiple input channels.
**/
@:access(aura.dsp.DSP)
class MixChannel extends BaseChannel {
#if cpp
static var mutex: Mutex = new Mutex();
#end
/**
The amount of inputs a MixChannel can hold. Set this value via
`Aura.init(channelSize)`.
**/
static var channelSize: Int;
var inputChannels: Vector<BaseChannel>;
var numUsedInputs: Int = 0;
/**
Temporary copy of inputChannels for thread safety.
**/
var inputChannelsCopy: Vector<BaseChannel>;
public function new() {
inputChannels = new Vector<BaseChannel>(channelSize);
// Make sure super.isPlayable() is true until we find better semantics
// for MixChannel.play()/pause()/stop()
this.finished = false;
}
/**
Adds an input channel. Returns `true` if adding the channel was
successful, `false` if the amount of input channels is already maxed
out.
**/
public function addInputChannel(channel: BaseChannel): Bool {
var foundChannel = false;
#if cpp
mutex.acquire();
#end
for (i in 0...MixChannel.channelSize) {
if (inputChannels[i] == null) { // || inputChannels[i].finished) {
inputChannels[i] = channel;
numUsedInputs++;
channel.setTreeLevel(this.treeLevel + 1);
foundChannel = true;
break;
}
}
updateChannelsCopy();
#if cpp
mutex.release();
#end
return foundChannel;
}
public function removeInputChannel(channel: BaseChannel) {
#if cpp
mutex.acquire();
#end
for (i in 0...MixChannel.channelSize) {
if (inputChannels[i] == channel) {
inputChannels[i] = null;
numUsedInputs--;
break;
}
}
updateChannelsCopy();
#if cpp
mutex.release();
#end
}
public inline function getNumInputs() {
return numUsedInputs;
}
/**
Copy the references to the inputs channels for thread safety. This
function does not acquire any additional mutexes.
@see `MixChannel.inputChannelsCopy`
**/
inline function updateChannelsCopy() {
inputChannelsCopy = inputChannels.copy();
// TODO: Streaming
// for (i in 0...channelCount) {
// internalStreamChannels[i] = streamChannels[i];
// }
}
override function isPlayable(): Bool {
// TODO: be more intelligent here and actually check inputs?
return super.isPlayable() && numUsedInputs != 0;
}
override function setTreeLevel(level: Int) {
this.treeLevel = level;
for (inputChannel in inputChannels) {
if (inputChannel != null) {
inputChannel.setTreeLevel(level + 1);
}
}
}
override function synchronize() {
for (inputChannel in inputChannels) {
if (inputChannel != null) {
inputChannel.synchronize();
}
}
super.synchronize();
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void {
Profiler.event();
if (numUsedInputs == 0) {
requestedSamples.clear();
return;
}
final inputBuffer = BufferCache.getTreeBuffer(treeLevel, requestedSamples.numChannels, requestedSamples.channelLength);
if (inputBuffer == null) {
requestedSamples.clear();
return;
}
var first = true;
var foundPlayableInput = false;
for (channel in inputChannelsCopy) {
if (channel == null || !channel.isPlayable()) {
continue;
}
foundPlayableInput = true;
channel.nextSamples(inputBuffer, sampleRate);
if (first) {
// To prevent feedback loops, the input buffer has to be cleared
// before all inputs are added to it. To not waste calculations,
// we do not clear the buffer here but instead just override
// the previous sample cache.
for (i in 0...requestedSamples.rawData.length) {
requestedSamples.rawData[i] = inputBuffer.rawData[i];
}
first = false;
}
else {
for (i in 0...requestedSamples.rawData.length) {
requestedSamples.rawData[i] += inputBuffer.rawData[i];
}
}
}
// for (channel in internalStreamChannels) {
// if (channel == null || !channel.isPlayable())
// continue;
// foundPlayableInput = true;
// channel.nextSamples(inputBuffer, samples, buffer.samplesPerSecond);
// for (i in 0...samples) {
// sampleCacheAccumulated[i] += inputBuffer[i] * channel.volume;
// }
// }
if (!foundPlayableInput) {
// Didn't read from input channels, clear possible garbage values
requestedSamples.clear();
return;
}
// Apply volume of this channel
final stepVol = pVolume.getLerpStepSize(requestedSamples.channelLength);
for (c in 0...requestedSamples.numChannels) {
final channelView = requestedSamples.getChannelView(c);
for (i in 0...requestedSamples.channelLength) {
channelView[i] *= pVolume.currentValue;
pVolume.currentValue += stepVol;
}
pVolume.currentValue = pVolume.lastValue;
}
pVolume.updateLast();
processInserts(requestedSamples);
}
/**
Calls `play()` for all input channels.
**/
public function play(retrigger: Bool): Void {
for (inputChannel in inputChannels) {
if (inputChannel != null) {
inputChannel.play(retrigger);
}
}
}
/**
Calls `pause()` for all input channels.
**/
public function pause(): Void {
for (inputChannel in inputChannels) {
if (inputChannel != null) {
inputChannel.pause();
}
}
}
/**
Calls `stop()` for all input channels.
**/
public function stop(): Void {
for (inputChannel in inputChannels) {
if (inputChannel != null) {
inputChannel.stop();
}
}
}
}

View File

@ -0,0 +1,64 @@
package aura.channels;
import aura.utils.Pointer;
import kha.arrays.Float32Array;
import aura.threading.BufferCache;
import aura.threading.Message;
import aura.types.AudioBuffer;
/**
Wrapper around `kha.audio2.StreamChannel` (for now).
**/
class StreamChannel extends BaseChannel {
final khaChannel: kha.audio2.StreamChannel;
final p_khaBuffer = new Pointer<Float32Array>(null);
public function new(khaChannel: kha.audio2.StreamChannel) {
this.khaChannel = khaChannel;
}
public function play(retrigger: Bool) {
paused = false;
finished = false;
khaChannel.play();
if (retrigger) {
khaChannel.position = 0;
}
}
public function pause() {
paused = true;
khaChannel.pause();
}
public function stop() {
finished = true;
khaChannel.stop();
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {
if (!BufferCache.getBuffer(TFloat32Array, p_khaBuffer, 1, requestedSamples.numChannels * requestedSamples.channelLength)) {
requestedSamples.clear();
return;
}
final khaBuffer = p_khaBuffer.get();
khaChannel.nextSamples(khaBuffer, requestedSamples.channelLength, sampleRate);
requestedSamples.deinterleaveFromFloat32Array(khaBuffer, requestedSamples.numChannels);
}
override function parseMessage(message: Message) {
switch (message.id) {
// Because we're using a Kha implementation here, we cannot use the
// LinearInterpolator parameters
case ChannelMessageID.PVolume: khaChannel.volume = cast message.data;
case ChannelMessageID.PPitch:
case ChannelMessageID.PDopplerRatio:
case ChannelMessageID.PDstAttenuation:
default:
super.parseMessage(message);
}
}
}

View File

@ -0,0 +1,264 @@
package aura.channels;
import kha.arrays.Float32Array;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.dsp.sourcefx.SourceEffect;
import aura.utils.MathUtils;
import aura.threading.Message;
import aura.types.AudioBuffer;
// TODO make handle thread-safe!
@:access(aura.channels.UncompBufferChannel)
class UncompBufferChannelHandle extends BaseChannelHandle {
final _sourceEffects: Array<SourceEffect> = []; // main-thread twin of channel.sourceEffects. TODO investigate better solution
var _playbackDataLength = -1;
inline function getUncompBufferChannel(): UncompBufferChannel {
return cast this.channel;
}
/**
Return the sound's length in seconds.
**/
public inline function getLength(): Float {
return getUncompBufferChannel().data.channelLength / Aura.sampleRate;
}
/**
Return the channel's current playback position in seconds.
**/
public inline function getPlaybackPosition(): Float {
return getUncompBufferChannel().playbackPosition / Aura.sampleRate;
}
/**
Set the channel's current playback position in seconds.
**/
public inline function setPlaybackPosition(value: Float) {
final pos = Math.round(value * Aura.sampleRate);
getUncompBufferChannel().playbackPosition = clampI(pos, 0, getUncompBufferChannel().data.channelLength);
}
public function addSourceEffect(sourceEffect: SourceEffect) {
_sourceEffects.push(sourceEffect);
final playbackData = updatePlaybackBuffer();
getUncompBufferChannel().sendMessage({ id: UncompBufferChannelMessageID.AddSourceEffect, data: [sourceEffect, playbackData] });
}
public function removeSourceEffect(sourceEffect: SourceEffect) {
if (_sourceEffects.remove(sourceEffect)) {
final playbackData = updatePlaybackBuffer();
getUncompBufferChannel().sendMessage({ id: UncompBufferChannelMessageID.RemoveSourceEffect, data: [sourceEffect, playbackData] });
}
}
@:access(aura.dsp.sourcefx.SourceEffect)
function updatePlaybackBuffer(): Null<AudioBuffer> {
final data = getUncompBufferChannel().data;
var playbackData: Null<AudioBuffer> = null;
if (_sourceEffects.length == 0) {
playbackData = data;
}
else {
var requiredChannelLength = data.channelLength;
var prevChannelLength = data.channelLength;
for (sourceEffect in _sourceEffects) {
prevChannelLength = sourceEffect.calculateRequiredChannelLength(prevChannelLength);
requiredChannelLength = maxI(requiredChannelLength, prevChannelLength);
}
if (_playbackDataLength != requiredChannelLength) {
playbackData = new AudioBuffer(data.numChannels, requiredChannelLength);
_playbackDataLength = requiredChannelLength;
}
}
// if null -> no buffer to change in channel
return playbackData;
}
}
@:allow(aura.channels.UncompBufferChannelHandle)
class UncompBufferChannel extends BaseChannel {
public static inline var NUM_CHANNELS = 2;
final sourceEffects: Array<SourceEffect> = [];
var appliedSourceEffects = false;
/** The current playback position in samples. **/
var playbackPosition: Int = 0;
var looping: Bool = false;
/**
The original audio source data for this channel.
**/
final data: AudioBuffer;
/**
The audio data used for playback. This might be different than `this.data`
if this channel has `AudioSourceEffect`s assigned to it.
**/
var playbackData: AudioBuffer;
public function new(data: Float32Array, looping: Bool) {
this.data = this.playbackData = new AudioBuffer(2, Std.int(data.length / 2));
this.data.deinterleaveFromFloat32Array(data, 2);
this.looping = looping;
}
override function parseMessage(message: Message) {
switch (message.id) {
case UncompBufferChannelMessageID.AddSourceEffect:
final sourceEffect: SourceEffect = message.dataAsArrayUnsafe()[0];
final _playbackData = message.dataAsArrayUnsafe()[1];
if (_playbackData != null) {
playbackData = _playbackData;
}
addSourceEffect(sourceEffect);
case UncompBufferChannelMessageID.RemoveSourceEffect:
final sourceEffect: SourceEffect = message.dataAsArrayUnsafe()[0];
final _playbackData = message.dataAsArrayUnsafe()[1];
if (_playbackData != null) {
playbackData = _playbackData;
}
removeSourceEffect(sourceEffect);
default: super.parseMessage(message);
}
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void {
assert(Critical, requestedSamples.numChannels == playbackData.numChannels);
final stepDopplerRatio = pDopplerRatio.getLerpStepSize(requestedSamples.channelLength);
final stepDstAttenuation = pDstAttenuation.getLerpStepSize(requestedSamples.channelLength);
final stepVol = pVolume.getLerpStepSize(requestedSamples.channelLength);
var samplesWritten = 0;
// As long as there are more samples requested
while (samplesWritten < requestedSamples.channelLength) {
// Check how many samples we can actually write
final samplesToWrite = minI(playbackData.channelLength - playbackPosition, requestedSamples.channelLength - samplesWritten);
for (c in 0...requestedSamples.numChannels) {
final outChannelView = requestedSamples.getChannelView(c);
final dataChannelView = playbackData.getChannelView(c);
// Reset interpolators for channel
pDopplerRatio.currentValue = pDopplerRatio.lastValue;
pDstAttenuation.currentValue = pDstAttenuation.lastValue;
pVolume.currentValue = pVolume.lastValue;
for (i in 0...samplesToWrite) {
final value = dataChannelView[playbackPosition + i] * pVolume.currentValue * pDstAttenuation.currentValue;
outChannelView[samplesWritten + i] = value;
// TODO: SIMD
pDopplerRatio.currentValue += stepDopplerRatio;
pDstAttenuation.currentValue += stepDstAttenuation;
pVolume.currentValue += stepVol;
}
}
samplesWritten += samplesToWrite;
playbackPosition += samplesToWrite;
if (playbackPosition >= playbackData.channelLength) {
playbackPosition = 0;
if (looping) {
optionallyApplySourceEffects();
}
else {
finished = true;
break;
}
}
}
// Fill further requested samples with zeroes
for (c in 0...requestedSamples.numChannels) {
final channelView = requestedSamples.getChannelView(c);
for (i in samplesWritten...requestedSamples.channelLength) {
channelView[i] = 0;
}
}
pDopplerRatio.updateLast();
pDstAttenuation.updateLast();
pVolume.updateLast();
processInserts(requestedSamples);
}
function play(retrigger: Bool): Void {
if (finished || retrigger || !appliedSourceEffects) {
optionallyApplySourceEffects();
}
paused = false;
finished = false;
if (retrigger) {
playbackPosition = 0;
}
}
function pause(): Void {
paused = true;
}
function stop(): Void {
playbackPosition = 0;
finished = true;
}
inline function addSourceEffect(audioSourceEffect: SourceEffect) {
sourceEffects.push(audioSourceEffect);
appliedSourceEffects = false;
}
inline function removeSourceEffect(audioSourceEffect: SourceEffect) {
sourceEffects.remove(audioSourceEffect);
appliedSourceEffects = false;
}
/**
Apply all source effects to `playbackData`, if there are any.
**/
@:access(aura.dsp.sourcefx.SourceEffect)
function optionallyApplySourceEffects() {
var currentSrcBuffer = data;
var previousLength = data.channelLength;
var needsReprocessing = !appliedSourceEffects;
if (!needsReprocessing) {
for (sourceEffect in sourceEffects) {
if (sourceEffect.applyOnReplay.load()) {
needsReprocessing = true;
break;
}
}
}
if (needsReprocessing) {
for (sourceEffect in sourceEffects) {
previousLength = sourceEffect.process(currentSrcBuffer, previousLength, playbackData);
currentSrcBuffer = playbackData;
}
}
appliedSourceEffects = true;
}
}
private class UncompBufferChannelMessageID extends ChannelMessageID {
final AddSourceEffect;
final RemoveSourceEffect;
}

View File

@ -0,0 +1,145 @@
// =============================================================================
// Roughly based on
// https://github.com/Kode/Kha/blob/master/Sources/kha/audio2/ResamplingAudioChannel.hx
// =============================================================================
package aura.channels;
import kha.arrays.Float32Array;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.MathUtils;
import aura.utils.Interpolator.LinearInterpolator;
import aura.utils.Profiler;
import aura.utils.Resampler;
class UncompBufferResamplingChannel extends UncompBufferChannel {
public var sampleRate: Hertz;
public var floatPosition: Float = 0.0;
final pPitch = new LinearInterpolator(1.0);
public function new(data: Float32Array, looping: Bool, sampleRate: Hertz) {
super(data, looping);
this.sampleRate = sampleRate;
};
override function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void {
Profiler.event();
assert(Critical, requestedSamples.numChannels == playbackData.numChannels);
final stepDopplerRatio = pDopplerRatio.getLerpStepSize(requestedSamples.channelLength);
final stepDstAttenuation = pDstAttenuation.getLerpStepSize(requestedSamples.channelLength);
final stepPitch = pPitch.getLerpStepSize(requestedSamples.channelLength);
final stepVol = pVolume.getLerpStepSize(requestedSamples.channelLength);
final resampleLength = Resampler.getResampleLength(playbackData.channelLength, this.sampleRate, sampleRate);
var samplesWritten = 0;
var reachedEndOfData = false;
// As long as there are more samples requested and there is data left
while (samplesWritten < requestedSamples.channelLength && !reachedEndOfData) {
final initialFloatPosition = floatPosition;
// Check how many samples we can actually write
final samplesToWrite = minI(resampleLength - playbackPosition, requestedSamples.channelLength - samplesWritten);
for (c in 0...requestedSamples.numChannels) {
final outChannelView = requestedSamples.getChannelView(c);
// Reset interpolators for channel
pDopplerRatio.currentValue = pDopplerRatio.lastValue;
pDstAttenuation.currentValue = pDstAttenuation.lastValue;
pPitch.currentValue = pPitch.lastValue;
pVolume.currentValue = pVolume.lastValue;
floatPosition = initialFloatPosition;
for (i in 0...samplesToWrite) {
var sampledVal: Float = Resampler.sampleAtTargetPositionLerp(playbackData.getChannelView(c), floatPosition, this.sampleRate, sampleRate);
if (pDopplerRatio.currentValue <= 0) {
// In this case, the audio is inaudible at the time of emission at its source,
// although technically the sound would eventually arrive at the listener in reverse.
// We don't simulate the latter, but still make the sound silent for some added realism
outChannelView[samplesWritten + i] = 0.0;
floatPosition += pPitch.currentValue;
}
else {
outChannelView[samplesWritten + i] = sampledVal * pVolume.currentValue * pDstAttenuation.currentValue;
floatPosition += pPitch.currentValue * pDopplerRatio.currentValue;
}
pDopplerRatio.currentValue += stepDopplerRatio;
pDstAttenuation.currentValue += stepDstAttenuation;
pPitch.currentValue += stepPitch;
pVolume.currentValue += stepVol;
if (floatPosition >= resampleLength) {
if (looping) {
while (floatPosition >= resampleLength) {
playbackPosition -= resampleLength;
floatPosition -= resampleLength; // Keep fraction
}
if (c == 0) {
optionallyApplySourceEffects();
}
}
else {
stop();
reachedEndOfData = true;
break;
}
}
else {
playbackPosition = Std.int(floatPosition);
}
}
}
samplesWritten += samplesToWrite;
}
// We're out of data, but more samples are requested
for (c in 0...requestedSamples.numChannels) {
final channelView = requestedSamples.getChannelView(c);
for (i in samplesWritten...requestedSamples.channelLength) {
channelView[i] = 0;
}
}
pDopplerRatio.updateLast();
pDstAttenuation.updateLast();
pPitch.updateLast();
pVolume.updateLast();
processInserts(requestedSamples);
}
override public function play(retrigger: Bool) {
super.play(retrigger);
if (retrigger) {
floatPosition = 0.0;
}
}
override public function stop() {
super.stop();
floatPosition = 0.0;
}
override public function pause() {
super.pause();
floatPosition = playbackPosition;
}
override function parseMessage(message: Message) {
switch (message.id) {
case ChannelMessageID.PPitch: pPitch.targetValue = cast message.data;
default:
super.parseMessage(message);
}
}
}

View File

@ -0,0 +1,16 @@
package aura.channels.generators;
abstract class BaseGenerator extends BaseChannel {
public function play(retrigger: Bool): Void {
paused = false;
finished = false;
}
public function pause(): Void {
paused = true;
}
public function stop(): Void {
finished = true;
}
}

View File

@ -0,0 +1,42 @@
package aura.channels.generators;
import haxe.ds.Vector;
import kha.FastFloat;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.types.AudioBuffer;
import aura.utils.BufferUtils;
/**
Signal noise produced by Brownian motion.
**/
class BrownNoise extends BaseGenerator {
final last: Vector<FastFloat>;
inline function new() {
last = createEmptyVecF32(2);
}
/**
Creates a new BrownNoise channel and returns a handle to it.
**/
public static function create(): BaseChannelHandle {
return new BaseChannelHandle(new BrownNoise());
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {
for (c in 0...requestedSamples.numChannels) {
final channelView = requestedSamples.getChannelView(c);
for (i in 0...requestedSamples.channelLength) {
final white = Math.random() * 2 - 1;
channelView[i] = (last[c] + (0.02 * white)) / 1.02;
last[c] = channelView[i];
channelView[i] * 3.5;
}
}
processInserts(requestedSamples);
}
}

View File

@ -0,0 +1,67 @@
package aura.channels.generators;
import haxe.ds.Vector;
import kha.FastFloat;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.types.AudioBuffer;
import aura.utils.BufferUtils;
/**
Signal with a frequency spectrum such that the power spectral density
(energy or power per Hz) is inversely proportional to the frequency of the
signal. Each octave (halving/doubling in frequency) carries an equal amount
of noise power.
**/
class PinkNoise extends BaseGenerator {
final b0: Vector<FastFloat>;
final b1: Vector<FastFloat>;
final b2: Vector<FastFloat>;
final b3: Vector<FastFloat>;
final b4: Vector<FastFloat>;
final b5: Vector<FastFloat>;
final b6: Vector<FastFloat>;
inline function new() {
b0 = createEmptyVecF32(2);
b1 = createEmptyVecF32(2);
b2 = createEmptyVecF32(2);
b3 = createEmptyVecF32(2);
b4 = createEmptyVecF32(2);
b5 = createEmptyVecF32(2);
b6 = createEmptyVecF32(2);
}
/**
Creates a new PinkNoise channel and returns a handle to it.
**/
public static function create(): BaseChannelHandle {
return new BaseChannelHandle(new PinkNoise());
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {
for (c in 0...requestedSamples.numChannels) {
final channelView = requestedSamples.getChannelView(c);
for (i in 0...requestedSamples.channelLength) {
final white = Math.random() * 2 - 1;
// Paul Kellet's refined method from
// https://www.firstpr.com.au/dsp/pink-noise/
b0[c] = 0.99886 * b0[c] + white * 0.0555179;
b1[c] = 0.99332 * b1[c] + white * 0.0750759;
b2[c] = 0.96900 * b2[c] + white * 0.1538520;
b3[c] = 0.86650 * b3[c] + white * 0.3104856;
b4[c] = 0.55000 * b4[c] + white * 0.5329522;
b5[c] = -0.7616 * b5[c] - white * 0.0168980;
channelView[i] = b0[c] + b1[c] + b2[c] + b3[c] + b4[c] + b5[c] + b6[c] + white * 0.5362;
channelView[i] *= 0.11;
b6[c] = white * 0.115926;
}
}
processInserts(requestedSamples);
}
}

View File

@ -0,0 +1,27 @@
package aura.channels.generators;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.types.AudioBuffer;
/**
Random signal with a constant power spectral density.
**/
class WhiteNoise extends BaseGenerator {
inline function new() {}
/**
Creates a new WhiteNoise channel and returns a handle to it.
**/
public static function create(): BaseChannelHandle {
return new BaseChannelHandle(new WhiteNoise());
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {
for (i in 0...requestedSamples.rawData.length) {
requestedSamples.rawData[i] = Math.random() * 2 - 1;
}
processInserts(requestedSamples);
}
}