forked from LeenkxTeam/LNXSDK
Update Files
This commit is contained in:
244
lib/aura/Sources/aura/channels/BaseChannel.hx
Normal file
244
lib/aura/Sources/aura/channels/BaseChannel.hx
Normal 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;
|
||||
}
|
||||
192
lib/aura/Sources/aura/channels/Html5StreamChannel.hx
Normal file
192
lib/aura/Sources/aura/channels/Html5StreamChannel.hx
Normal 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
|
||||
299
lib/aura/Sources/aura/channels/MixChannel.hx
Normal file
299
lib/aura/Sources/aura/channels/MixChannel.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
lib/aura/Sources/aura/channels/StreamChannel.hx
Normal file
64
lib/aura/Sources/aura/channels/StreamChannel.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
264
lib/aura/Sources/aura/channels/UncompBufferChannel.hx
Normal file
264
lib/aura/Sources/aura/channels/UncompBufferChannel.hx
Normal 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;
|
||||
}
|
||||
145
lib/aura/Sources/aura/channels/UncompBufferResamplingChannel.hx
Normal file
145
lib/aura/Sources/aura/channels/UncompBufferResamplingChannel.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/aura/Sources/aura/channels/generators/BaseGenerator.hx
Normal file
16
lib/aura/Sources/aura/channels/generators/BaseGenerator.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
42
lib/aura/Sources/aura/channels/generators/BrownNoise.hx
Normal file
42
lib/aura/Sources/aura/channels/generators/BrownNoise.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
67
lib/aura/Sources/aura/channels/generators/PinkNoise.hx
Normal file
67
lib/aura/Sources/aura/channels/generators/PinkNoise.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
27
lib/aura/Sources/aura/channels/generators/WhiteNoise.hx
Normal file
27
lib/aura/Sources/aura/channels/generators/WhiteNoise.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user