Update Files

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

View File

@ -0,0 +1,36 @@
package aura.dsp;
import aura.threading.Fifo;
import aura.threading.Message;
import aura.types.AudioBuffer;
@:allow(aura.dsp.panner.Panner)
abstract class DSP {
public var bypass = false;
var inUse = false;
final messages: Fifo<Message> = new Fifo();
abstract function process(buffer: AudioBuffer): Void;
function synchronize() {
var message: Null<Message>;
while ((message = messages.tryPop()) != null) {
parseMessage(message);
}
}
function parseMessage(message: Message) {
switch (message.id) {
// TODO
case DSPMessageID.BypassEnable:
case DSPMessageID.BypassDisable:
default:
}
}
inline function sendMessage(message: Message) {
messages.add(message);
}
}

View File

@ -0,0 +1,61 @@
package aura.dsp;
import haxe.ds.Vector;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.CircularBuffer;
class DelayLine extends DSP {
public static inline var NUM_CHANNELS = 2;
public final maxDelaySamples: Int;
final delayBufs: Vector<CircularBuffer>;
public function new(maxDelaySamples: Int) {
this.maxDelaySamples = maxDelaySamples;
delayBufs = new Vector(NUM_CHANNELS);
for (i in 0...NUM_CHANNELS) {
delayBufs[i] = new CircularBuffer(maxDelaySamples);
}
}
public inline function setDelay(delaySamples: Int) {
for (i in 0...NUM_CHANNELS) {
delayBufs[i].setDelay(delaySamples);
}
}
public inline function setDelays(delaySamples: Array<Int>) {
for (i in 0...NUM_CHANNELS) {
delayBufs[i].setDelay(delaySamples[i]);
}
}
function process(buffer: AudioBuffer) {
for (c in 0...buffer.numChannels) {
final delayBuf = delayBufs[c];
if (delayBuf.delay == 0) continue;
final channelView = buffer.getChannelView(c);
for (i in 0...buffer.channelLength) {
delayBuf.set(channelView[i]);
channelView[i] = delayBuf.get();
delayBuf.increment();
}
}
}
override function parseMessage(message: Message) {
switch (message.id) {
case DSPMessageID.SetDelays:
setDelays(message.data);
default:
super.parseMessage(message);
}
}
}

View File

@ -0,0 +1,217 @@
package aura.dsp;
import haxe.ds.Vector;
import kha.FastFloat;
import kha.arrays.Float32Array;
import kha.arrays.Int32Array;
import aura.math.FFT;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.types.ComplexArray;
import aura.types.SwapBuffer;
import aura.utils.BufferUtils;
import aura.utils.MathUtils;
import aura.utils.Profiler;
/**
Calculates the 1D linear convolution of the input with another buffer called
`impulse`.
**/
class FFTConvolver extends DSP {
public static inline var NUM_CHANNELS = 2;
public static inline var FFT_SIZE = 1024;
public static inline var CHUNK_SIZE = Std.int(FFT_SIZE / 2);
/**
The amount of samples used to (temporally) interpolate
between consecutive impulse responses. Values larger than `CHUNK_SIZE`
are clamped to that length.
**Special values**:
- Any negative value: Automatically follows `CHUNK_SIZE`
- 0: Do not interpolate between consecutive impulse responses // TODO implement me
**/
// TODO: make thread-safe
public var temporalInterpolationLength = -1;
final impulseSwapBuffer: SwapBuffer;
/**
The part of the last output signal that was longer than the last frame
buffer and thus overlaps to the next frame. To prevent allocations
during runtime and to ensure that overlapPrev is not longer than one
FFT segment, the overlap vectors are preallocated to `CHUNK_SIZE - 1`.
Use `overlapLength` to get the true length.
**/
final overlapPrev: Vector<Vector<FastFloat>>;
/**
The (per-channel) overlap length of the convolution result for the
current impulse response.
**/
final overlapLength: Vector<Int>;
/**
The (per-channel) overlap length of the convolution result for the
impulse response from the previous processing block.
**/
final prevOverlapLength: Vector<Int>;
static var signalFFT: Null<RealValuedFFT>;
final impulseFFT: Null<RealValuedFFT>;
var currentImpulseAlternationIndex = 0;
final prevImpulseLengths: Int32Array = new Int32Array(NUM_CHANNELS);
public function new() {
assert(Error, isPowerOf2(FFT_SIZE), 'FFT_SIZE must be a power of 2, but it is $FFT_SIZE');
if (signalFFT == null) {
signalFFT = new RealValuedFFT(FFT_SIZE, 2, 2);
}
impulseFFT = new RealValuedFFT(FFT_SIZE, 1, NUM_CHANNELS * 2);
prevImpulseLengths = new Int32Array(NUM_CHANNELS);
for (i in 0...prevImpulseLengths.length) {
prevImpulseLengths[i] = 0;
}
impulseSwapBuffer = new SwapBuffer(CHUNK_SIZE * 2);
overlapPrev = new Vector(NUM_CHANNELS);
for (i in 0...NUM_CHANNELS) {
// Max. impulse size is CHUNK_SIZE
overlapPrev[i] = createEmptyVecF32(CHUNK_SIZE - 1);
}
overlapLength = createEmptyVecI(NUM_CHANNELS);
prevOverlapLength = createEmptyVecI(NUM_CHANNELS);
}
// TODO: move this into main thread and use swapbuffer for impulse freqs
// instead? Moving the impulse FFT computation into the main thread will
// also remove the fft computation while the swap buffer lock is active,
// reducing the lock time, but it occupies the main thread more...
function updateImpulseFromSwapBuffer(impulseLengths: Array<Int>) {
final impulseTimeDomain = impulseFFT.getInput(0);
impulseSwapBuffer.beginRead();
for (c in 0...impulseLengths.length) {
impulseSwapBuffer.read(impulseTimeDomain, 0, CHUNK_SIZE * c, CHUNK_SIZE);
inline calculateImpulseFFT(impulseLengths[c], c);
}
impulseSwapBuffer.endRead();
currentImpulseAlternationIndex = 1 - currentImpulseAlternationIndex;
}
inline function calculateImpulseFFT(impulseLength: Int, channelIndex: Int) {
impulseFFT.forwardFFT(0, NUM_CHANNELS * channelIndex + currentImpulseAlternationIndex);
overlapLength[channelIndex] = maxI(prevImpulseLengths[channelIndex], impulseLength - 1);
prevImpulseLengths[channelIndex] = impulseLength;
}
public function process(buffer: AudioBuffer) {
Profiler.event();
// TODO
assert(Critical, buffer.numChannels == NUM_CHANNELS);
for (c in 0...buffer.numChannels) {
if (overlapLength[c] < 0) return;
}
// Ensure correct boundaries
final isMultiple = (buffer.channelLength % CHUNK_SIZE) == 0 || (CHUNK_SIZE % buffer.channelLength) == 0;
assert(Debug, isMultiple, "channelLength must be a multiple of CHUNK_SIZE or vice versa");
var numSegments: Int; // Segments per channel frame
var segmentSize: Int;
if (CHUNK_SIZE < buffer.channelLength) {
numSegments = Std.int(buffer.channelLength / CHUNK_SIZE);
segmentSize = CHUNK_SIZE;
}
else {
// TODO: accumulate samples if buffer.channelLength < CHUNK_SIZE,
// then delay output
numSegments = 1;
segmentSize = buffer.channelLength;
}
final numInterpolationSteps = temporalInterpolationLength < 0 ? CHUNK_SIZE : minI(temporalInterpolationLength, CHUNK_SIZE);
final interpolationStepSize = 1 / numInterpolationSteps;
final signalTimeDomainCurrentImpulse = signalFFT.getInput(0);
final signalTimeDomainPrevImpulse = signalFFT.getInput(1);
final signalFreqDomainCurrentImpulse = signalFFT.getOutput(0);
final signalFreqDomainPrevImpulse = signalFFT.getOutput(1);
for (c in 0...buffer.numChannels) {
final channelView = buffer.getChannelView(c);
final impulseFreqDomainCurrent = impulseFFT.getOutput(NUM_CHANNELS * c + (1 - currentImpulseAlternationIndex));
final impulseFreqDomainPrev = impulseFFT.getOutput(NUM_CHANNELS * c + currentImpulseAlternationIndex);
for (s in 0...numSegments) {
final segmentOffset = s * segmentSize;
// Copy to FFT input buffer and apply padding
for (i in 0...segmentSize) {
signalTimeDomainCurrentImpulse[i] = channelView[segmentOffset + i];
}
for (i in segmentSize...FFT_SIZE) {
signalTimeDomainCurrentImpulse[i] = 0.0;
}
signalFFT.forwardFFT(0, 0);
// Copy signal frequency signal to multiply with
// both current and previous impulse frequency responses
signalFreqDomainPrevImpulse.copyFrom(signalFreqDomainCurrentImpulse);
// The actual convolution takes place here
// TODO: SIMD
for (i in 0...FFT_SIZE) {
signalFreqDomainCurrentImpulse[i] *= impulseFreqDomainCurrent[i];
signalFreqDomainPrevImpulse[i] *= impulseFreqDomainPrev[i];
}
// Transform back into time domain
signalFFT.inverseFFT(0, 0);
signalFFT.inverseFFT(1, 1);
// Interpolate (only for first segment) and copy to output
final actualNumInterpolationSteps = (s == 0) ? numInterpolationSteps : 0;
var t = 0.0;
for (i in 0...actualNumInterpolationSteps) {
channelView[segmentOffset + i] = lerpF32(signalTimeDomainPrevImpulse[i], signalTimeDomainCurrentImpulse[i], t);
t += interpolationStepSize;
}
for (i in actualNumInterpolationSteps...CHUNK_SIZE) {
channelView[segmentOffset + i] = signalTimeDomainCurrentImpulse[i];
}
// Apply overlapping from last segment
for (i in 0...prevOverlapLength[c]) {
channelView[segmentOffset + i] += overlapPrev[c][i];
}
// Write overlapping samples for next segment
for (i in 0...overlapLength[c]) {
overlapPrev[c][i] = signalTimeDomainCurrentImpulse[CHUNK_SIZE + i];
}
prevOverlapLength[c] = overlapLength[c];
}
}
}
override function parseMessage(message: Message) {
switch (message.id) {
case DSPMessageID.SwapBufferReady:
updateImpulseFromSwapBuffer(message.data);
default:
super.parseMessage(message);
}
}
}

View File

@ -0,0 +1,86 @@
package aura.dsp;
import haxe.ds.Vector;
import kha.FastFloat;
import aura.Types;
import aura.types.AudioBuffer;
import aura.utils.BufferUtils;
import aura.utils.FrequencyUtils;
import aura.utils.MathUtils;
using aura.utils.StepIterator;
/**
A simple IIR (infinite impulse response) lowpass/bandpass/highpass filter
with a slope of 12 dB/octave.
**/
class Filter extends DSP {
/**
Whether the filter should be a low-/band- or highpass filter.
**/
public var filterMode: FilterMode;
final buf: Vector<Vector<FastFloat>>;
final cutoff: Vector<FastFloat>;
public function new(filterMode: FilterMode) {
this.filterMode = filterMode;
this.buf = new Vector(2); // Two channels
buf[0] = createEmptyVecF32(2); // Two buffers per channel
buf[1] = createEmptyVecF32(2);
this.cutoff = new Vector(2);
cutoff[0] = cutoff[1] = 1.0;
}
public function process(buffer: AudioBuffer) {
for (c in 0...buffer.numChannels) {
if (cutoff[c] == 1.0) { continue; }
final channelView = buffer.getChannelView(c);
for (i in 0...buffer.channelLength) {
// http://www.martin-finke.de/blog/articles/audio-plugins-013-filter/
buf[c][0] += cutoff[c] * (channelView[i] - buf[c][0]);
buf[c][1] += cutoff[c] * (buf[c][0] - buf[c][1]);
// TODO: Move the switch out of the loop, even if that means duplicate code?
channelView[i] = switch (filterMode) {
case LowPass: buf[c][1];
case HighPass: channelView[i] - buf[c][0];
case BandPass: buf[c][0] - buf[c][1];
}
}
}
}
/**
Set the cutoff frequency for this filter. `channels` state for which
channels to set the cutoff value.
**/
public inline function setCutoffFreq(cutoffFreq: Hertz, channels: Channels = All) {
final maxFreq = sampleRateToMaxFreq(Aura.sampleRate);
final c = frequencyToFactor(clampI(cutoffFreq, 0, maxFreq), maxFreq);
if (channels.matches(Channels.Left)) { cutoff[0] = c; }
if (channels.matches(Channels.Right)) { cutoff[1] = c; }
}
/**
Get the cutoff frequency of this filter. `channels` state from which
channels to get the cutoff value, if it's `Both`, the left channel's
cutoff frequency is returned.
**/
public inline function getCutoffFreq(channels: Channels = All): Hertz {
final c = channels.matches(Channels.Left) ? cutoff[0] : cutoff[1];
return factorToFrequency(c, sampleRateToMaxFreq(Aura.sampleRate));
}
}
enum abstract FilterMode(Int) {
var LowPass;
var BandPass;
var HighPass;
}

View File

@ -0,0 +1,99 @@
package aura.dsp;
import haxe.ds.Vector;
import kha.FastFloat;
import kha.arrays.Float32Array;
import aura.Types;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.CircularBuffer;
/**
A delay line that supports fractions of samples set as delay times.
The implementation follows the linear interpolation approach as presented
in https://ccrma.stanford.edu/~jos/pasp/Fractional_Delay_Filtering_Linear.html.
@see `aura.dsp.DelayLine`
**/
class FractionalDelayLine extends DSP {
/**
The maximum amount of channels this DSP effect supports.
**/
public final maxNumChannels: Int;
/**
The maximum amount of (whole) samples by which any channel of the input
can be delayed.
**/
public final maxDelayLength: Int;
final delayBufs: Vector<CircularBuffer>;
final delayLengthFracts: Float32Array;
public function new(maxNumChannels: Int, maxDelayLength: Int) {
this.maxNumChannels = maxNumChannels;
this.maxDelayLength = maxDelayLength;
delayLengthFracts = new Float32Array(maxNumChannels);
delayBufs = new Vector(maxNumChannels);
for (i in 0...maxNumChannels) {
delayLengthFracts[i] = 0.0;
delayBufs[i] = new CircularBuffer(maxDelayLength);
}
}
public inline function setDelayLength(channelMask: Channels, delayLength: FastFloat) {
assert(Error, delayLength >= 0);
assert(Error, delayLength < maxDelayLength);
sendMessage({id: DSPMessageID.SetDelays, data: [channelMask, delayLength]});
}
function process(buffer: AudioBuffer) {
for (c in 0...buffer.numChannels) {
if (delayBufs[c].delay == 0) continue;
final channelView = buffer.getChannelView(c);
for (i in 0...buffer.channelLength) {
delayBufs[c].set(channelView[i]);
var delayedSignalMm1 = delayBufs[c].get(); // M - 1
delayBufs[c].increment();
var delayedSignalM = delayBufs[c].get(); // M
channelView[i] = delayedSignalM + delayLengthFracts[c] * (delayedSignalMm1 - delayedSignalM);
}
}
}
override function parseMessage(message: Message) {
switch (message.id) {
case DSPMessageID.SetDelays:
final channelMask = message.dataAsArrayUnsafe()[0];
final delayLength = message.dataAsArrayUnsafe()[1];
at_setDelayLength(channelMask, delayLength);
default:
super.parseMessage(message);
}
}
inline function at_setDelayLength(channelMask: Channels, delayLength: FastFloat) {
final delayLengthFloor = Math.ffloor(delayLength); // TODO implement 32-bit ffloor
final delayLengthFract = delayLength - delayLengthFloor;
final delayLengthInt = Std.int(delayLengthFloor);
for (c in 0...maxNumChannels) {
if (!channelMask.matchesIndex(c)) {
continue;
}
delayLengthFracts[c] = delayLengthFract;
delayBufs[c].setDelay(delayLengthInt + 1);
}
}
}

View File

@ -0,0 +1,55 @@
package aura.dsp;
import aura.types.AudioBuffer;
import aura.utils.CircularBuffer;
import aura.utils.FrequencyUtils;
/**
The [Haas effect](https://en.wikipedia.org/wiki/Precedence_effect) is a
psychoacoustical effect that uses a delay of one stereo channel of ca.
3 - 50 milliseconds to create the perception of 3D sound.
Using a negative value for `delay` moves the sound to the left of the
listener by delaying the right channel. Using a positive value delays the
left channel and moves the sound to the right. If `delay` is `0`, this
effect does nothing.
**/
class HaasEffect extends DSP {
var delayChannelIdx: Int;
var diffSamples: Int;
var delayBuff: CircularBuffer;
public function new(delay: Millisecond) {
this.diffSamples = 0;
this.setDelay(delay);
}
public function process(buffer: AudioBuffer) {
if (diffSamples == 0) return;
for (c in 0...buffer.numChannels) {
if (c != delayChannelIdx) { continue; }
final channelView = buffer.getChannelView(c);
for (i in 0...buffer.channelLength) {
delayBuff.set(channelView[i]);
channelView[i] = delayBuff.get();
delayBuff.increment();
}
}
}
public function setDelay(delay: Millisecond) {
final prev = diffSamples;
this.diffSamples = msToSamples(Aura.sampleRate, delay);
if (prev != diffSamples) {
this.delayChannelIdx = (diffSamples > 0) ? 0 : 1;
this.delayBuff = new CircularBuffer((diffSamples < 0) ? -diffSamples : diffSamples);
}
}
public inline function getDelay(): Millisecond {
return samplesToMs(Aura.sampleRate, diffSamples);
}
}

View File

@ -0,0 +1,109 @@
package aura.dsp;
import haxe.ds.Vector;
import kha.FastFloat;
import kha.arrays.ByteArray;
import aura.types.AudioBuffer;
import aura.utils.CircularBuffer;
/**
Perform efficient convolution of sparse impulse responses (i.e., impulse
responses in which most samples have a value of 0).
**/
class SparseConvolver extends DSP {
static inline var NUM_CHANNELS = 2;
public final impulseBuffer: SparseImpulseBuffer;
final delayBufs: Vector<CircularBuffer>;
/**
Create a new `SparseConvolver` object.
@param maxNumImpulses The maximal amount of non-zero impulses that can be stored in `this.impulseBuffer`.
@param maxNumImpulseResponseSamples The highest possible position of any non-zero impulse stored in the `impulseBuffer`.
There is no bounds checking in place!
**/
public function new(maxNumImpulses: Int, maxNumImpulseResponseSamples: Int) {
assert(Error, maxNumImpulseResponseSamples > maxNumImpulses);
impulseBuffer = new SparseImpulseBuffer(maxNumImpulses);
delayBufs = new Vector(NUM_CHANNELS);
for (i in 0...NUM_CHANNELS) {
delayBufs[i] = new CircularBuffer(maxNumImpulseResponseSamples);
}
}
public inline function getMaxNumImpulses(): Int {
return impulseBuffer.length;
}
public inline function getMaxNumImpulseResponseSamples(): Int {
return delayBufs[0].length;
}
function process(buffer: AudioBuffer) {
assert(Error, buffer.numChannels == NUM_CHANNELS);
for (c in 0...buffer.numChannels) {
final channelView = buffer.getChannelView(c);
final delayBuf = delayBufs[c];
for (i in 0...buffer.channelLength) {
delayBuf.set(channelView[i]);
var convolutionSum: FastFloat = 0.0;
for (impulseIndex in 0...impulseBuffer.length) {
// Move read pointer to impulse position, probably not the
// most cache efficient operation but it looks pretty unavoidable
delayBuf.setDelay(impulseBuffer.getImpulsePos(impulseIndex));
convolutionSum += delayBuf.get() * impulseBuffer.getImpulseMagnitude(impulseIndex);
}
// TODO: impulse response must be longer than buffer.channelLength!
channelView[i] = convolutionSum;
delayBuf.increment();
}
}
}
}
/**
A cache efficient buffer to store `(position: Int, magnitude: FastFloat)`
pairs that represent impulses of varying magnitudes within a sparse impulse
response. The buffer is **NOT** guaranteed to be zero-initialized.
**/
abstract SparseImpulseBuffer(ByteArray) {
public var length(get, never): Int;
public inline function new(numImpulses: Int) {
this = ByteArray.make(numImpulses * 8);
}
public inline function get_length(): Int {
return this.byteLength >> 3;
}
public inline function getImpulsePos(index: Int): Int {
return this.getUint32(index * 8);
}
public inline function setImpulsePos(index: Int, position: Int) {
this.setUint32(index * 8, position);
}
public inline function getImpulseMagnitude(index: Int): FastFloat {
return this.getFloat32(index * 8 + 4);
}
public inline function setImpulseMagnitude(index: Int, magnitude: FastFloat) {
this.setFloat32(index * 8 + 4, magnitude);
}
}

View File

@ -0,0 +1,120 @@
package aura.dsp.panner;
import kha.FastFloat;
import kha.arrays.Float32Array;
import aura.Types.Channels;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.types.HRTF;
import aura.utils.MathUtils;
import aura.utils.Pointer;
class HRTFPanner extends Panner {
public var hrtf: HRTF;
final hrtfConvolver: FFTConvolver;
final hrtfDelayLine: FractionalDelayLine;
final hrirPtrDelay: Pointer<FastFloat>;
final hrirPtrImpulseLength: Pointer<Int>;
final hrir: Float32Array;
final hrirOpp: Float32Array;
public function new(handle: BaseChannelHandle, hrtf: HRTF) {
super(handle);
this.hrtf = hrtf;
hrtfConvolver = new FFTConvolver();
hrtfDelayLine = new FractionalDelayLine(2, Math.ceil(hrtf.maxDelayLength));
hrtfConvolver.bypass = true;
hrtfDelayLine.bypass = true;
hrirPtrDelay = new Pointer<FastFloat>();
hrirPtrImpulseLength = new Pointer<Int>();
hrir = new Float32Array(FFTConvolver.CHUNK_SIZE);
hrirOpp = new Float32Array(FFTConvolver.CHUNK_SIZE);
}
override public function update3D() {
final listener = Aura.listener;
final dirToChannel = this.location.sub(listener.location);
if (dirToChannel.length == 0) {
hrtfConvolver.bypass = true;
hrtfDelayLine.bypass = true;
handle.channel.sendMessage({ id: ChannelMessageID.PDstAttenuation, data: 1.0 });
return;
}
final look = listener.look;
final up = listener.right.cross(look).normalized();
// Project the channel position (relative to the listener) to the plane
// described by the listener's look and right vectors
final projectedChannelPos = projectPointOntoPlane(dirToChannel, up).normalized();
final elevationCos = up.dot(dirToChannel.normalized());
// 180: top, 0: bottom
final elevation = 180 - (Math.acos(elevationCos) * (180 / Math.PI));
var angle = getFullAngleDegrees(look, projectedChannelPos, up);
angle = angle != 0 ? 360 - angle : 0; // Make clockwise
hrtf.getInterpolatedHRIR(elevation, angle, hrir, hrirPtrImpulseLength, hrirPtrDelay);
final hrirLength = hrirPtrImpulseLength.getSure();
final hrirDelay = hrirPtrDelay.getSure();
if (hrtf.numChannels == 1) {
hrtf.getInterpolatedHRIR(elevation, 360 - angle, hrirOpp, hrirPtrImpulseLength, hrirPtrDelay);
final hrirOppLength = hrirPtrImpulseLength.getSure();
final hrirOppDelay = hrirPtrDelay.getSure();
final swapBuf = @:privateAccess hrtfConvolver.impulseSwapBuffer;
swapBuf.beginWrite();
// Left channel
swapBuf.write(hrir, 0, 0, hrirLength);
swapBuf.writeZero(hrirLength, FFTConvolver.CHUNK_SIZE);
// Right channel
swapBuf.write(hrirOpp, 0, FFTConvolver.CHUNK_SIZE, hrirOppLength);
swapBuf.writeZero(FFTConvolver.CHUNK_SIZE + hrirOppLength, swapBuf.length);
swapBuf.endWrite();
hrtfConvolver.bypass = false;
hrtfDelayLine.bypass = false;
hrtfConvolver.sendMessage({id: DSPMessageID.SwapBufferReady, data: [hrirLength, hrirOppLength]});
hrtfDelayLine.setDelayLength(Channels.Left, hrirDelay);
hrtfDelayLine.setDelayLength(Channels.Right, hrirOppDelay);
}
else {
for (c in 0...hrtf.numChannels) {
// final delaySamples = Math.round(hrir.delays[0]);
// TODO: handle interleaved coeffs of stereo HRTFs
// Deinterleave when reading the file?
}
}
super.update3D();
}
override public function reset3D() {
hrtfConvolver.bypass = true;
hrtfDelayLine.bypass = true;
super.reset3D();
}
function process(buffer: AudioBuffer) {
if (!hrtfConvolver.bypass) {
hrtfConvolver.synchronize();
hrtfConvolver.process(buffer);
hrtfDelayLine.synchronize();
hrtfDelayLine.process(buffer);
}
}
}

View File

@ -0,0 +1,166 @@
package aura.dsp.panner;
import kha.FastFloat;
import kha.math.FastVector3;
import aura.channels.BaseChannel.BaseChannelHandle;
import aura.math.Vec3;
import aura.threading.Message;
import aura.utils.MathUtils;
abstract class Panner extends DSP {
static inline var REFERENCE_DST = 1.0;
static inline var SPEED_OF_SOUND = 343.4; // Air, m/s
/**
The strength of the doppler effect.
This value is multiplied to the calculated doppler effect, thus:
- A value of `0.0` results in no doppler effect.
- A value between `0.0` and `1.0` attenuates the effect (smaller values: more attenuation).
- A value of `1.0` does not attenuate or amplify the doppler effect.
- A value larger than `1.0` amplifies the doppler effect (larger values: more amplification).
**/
public var dopplerStrength = 1.0;
public var attenuationMode = AttenuationMode.Inverse;
public var attenuationFactor = 1.0;
public var maxDistance = 10.0;
// public var minDistance = 1;
var handle: BaseChannelHandle;
/**
The location of this audio source in world space.
**/
var location: Vec3 = new Vec3(0, 0, 0);
/**
The velocity of this audio source in world space.
**/
var velocity: Vec3 = new Vec3(0, 0, 0);
public function new(handle: BaseChannelHandle) {
this.inUse = true; // Don't allow using panners with addInsert()
this.handle = handle;
this.handle.channel.panner = this;
}
public inline function setHandle(handle: BaseChannelHandle) {
if (this.handle != null) {
this.handle.channel.panner = null;
}
reset3D();
this.handle = handle;
this.handle.channel.panner = this;
}
/**
Update the channel's audible 3D parameters after changing the channel's
or the listener's position or rotation.
**/
public function update3D() {
final displacementToSource = location.sub(Aura.listener.location);
calculateAttenuation(displacementToSource);
calculateDoppler(displacementToSource);
};
/**
Reset all the audible 3D sound parameters (balance, doppler effect etc.)
which are calculated by `update3D()`. This function does *not* reset the
location value of the sound, so if you call `update3D()` again, you will
hear the sound at the same position as before you called `reset3D()`.
**/
public function reset3D() {
handle.channel.sendMessage({ id: ChannelMessageID.PDopplerRatio, data: 1.0 });
handle.channel.sendMessage({ id: ChannelMessageID.PDstAttenuation, data: 1.0 });
};
/**
Set the location of this panner in world space.
Calling this function also sets the panner's velocity if the call
to this function is not the first call for this panner. This behavior
avoids audible "jumps" in the doppler effect for initial placement
of objects if they are far away from the origin.
**/
public function setLocation(location: Vec3) {
final time = Time.getTime();
final timeDeltaLastCall = time - _setLocation_lastCallTime;
// If the last time setLocation() was called was at an earlier time step
if (timeDeltaLastCall > 0) {
_setLocation_lastLocation.setFrom(this.location);
_setLocation_lastVelocityUpdateTime = _setLocation_lastCallTime;
}
final timeDeltaVelocityUpdate = time - _setLocation_lastVelocityUpdateTime;
this.location.setFrom(location);
if (!_setLocation_initializedLocation) {
// Prevent jumps in the doppler effect caused by initial distance
// too far away from the origin
_setLocation_initializedLocation = true;
}
else if (timeDeltaVelocityUpdate > 0) {
velocity.setFrom(location.sub(_setLocation_lastLocation).mult(1 / timeDeltaVelocityUpdate));
}
_setLocation_lastCallTime = time;
}
var _setLocation_initializedLocation = false;
var _setLocation_lastLocation: Vec3 = new Vec3(0, 0, 0);
var _setLocation_lastCallTime: Float = 0.0;
var _setLocation_lastVelocityUpdateTime: Float = 0.0;
function calculateAttenuation(dirToChannel: FastVector3) {
final dst = maxF(REFERENCE_DST, dirToChannel.length);
final dstAttenuation = switch (attenuationMode) {
case Linear:
maxF(0.0, 1 - attenuationFactor * (dst - REFERENCE_DST) / (maxDistance - REFERENCE_DST));
case Inverse:
REFERENCE_DST / (REFERENCE_DST + attenuationFactor * (dst - REFERENCE_DST));
case Exponential:
Math.pow(dst / REFERENCE_DST, -attenuationFactor);
}
handle.channel.sendMessage({ id: ChannelMessageID.PDstAttenuation, data: dstAttenuation });
}
function calculateDoppler(displacementToSource: FastVector3) {
final listener = Aura.listener;
var dopplerRatio: FastFloat = 1.0;
if (dopplerStrength != 0.0 && (listener.velocity.length != 0 || this.velocity.length != 0)) {
final dist = displacementToSource.length;
if (dist == 0) {
// We don't have any radial velocity here...
handle.channel.sendMessage({ id: ChannelMessageID.PDopplerRatio, data: 1.0 });
return;
}
// Calculate radial velocity
final vr = listener.velocity.dot(displacementToSource) / dist;
final vs = this.velocity.dot(displacementToSource) / dist;
// Sound source comes closer exactly at speed of sound,
// make silent and prevent division by zero below
if (vs == -SPEED_OF_SOUND) {
handle.channel.sendMessage({ id: ChannelMessageID.PDopplerRatio, data: 0.0 });
return;
}
dopplerRatio = (SPEED_OF_SOUND + vr) / (SPEED_OF_SOUND + vs);
dopplerRatio = Math.pow(dopplerRatio, dopplerStrength);
}
handle.channel.sendMessage({ id: ChannelMessageID.PDopplerRatio, data: dopplerRatio });
}
}
enum abstract AttenuationMode(Int) {
var Linear;
var Inverse;
var Exponential;
}

View File

@ -0,0 +1,127 @@
package aura.dsp.panner;
import aura.threading.Message;
import aura.types.AudioBuffer;
import aura.utils.Interpolator.LinearInterpolator;
import aura.utils.MathUtils;
using aura.utils.StepIterator;
class StereoPanner extends Panner {
final pVolumeLeft = new LinearInterpolator(1.0);
final pVolumeRight = new LinearInterpolator(1.0);
var _balance = Balance.CENTER;
override public function update3D() {
final listener = Aura.listener;
final dirToChannel = this.location.sub(listener.location);
if (dirToChannel.length == 0) {
setBalance(Balance.CENTER);
handle.channel.sendMessage({ id: ChannelMessageID.PDstAttenuation, data: 1.0 });
return;
}
final look = listener.look;
final up = listener.right.cross(look).normalized();
// Project the channel position (relative to the listener) to the plane
// described by the listener's look and right vectors
final projectedChannelPos = projectPointOntoPlane(dirToChannel, up).normalized();
// Angle cosine
var angle = listener.look.dot(projectedChannelPos);
// The calculated angle cosine looks like this on the unit circle:
// / 1 \
// 0 x 0 , where x is the listener and top is on the front
// \ -1 /
// Make the center 0.5, use absolute angle to prevent phase flipping.
// We loose front/back information here, but that's ok
angle = Math.abs(angle * 0.5);
// The angle cosine doesn't contain side information, so if the sound is
// to the right of the listener, we must invert the angle
if (listener.right.dot(projectedChannelPos) > 0) {
angle = 1 - angle;
}
setBalance(angle);
super.update3D();
}
override public function reset3D() {
setBalance(Balance.CENTER);
super.reset3D();
}
public inline function setBalance(balance: Balance) {
this._balance = balance;
sendMessage({ id: StereoPannerMessageID.PVolumeLeft, data: Math.sqrt(~balance) });
sendMessage({ id: StereoPannerMessageID.PVolumeRight, data: Math.sqrt(balance) });
}
public inline function getBalance(): Balance {
return this._balance;
}
function process(buffer: AudioBuffer) {
assert(Critical, buffer.numChannels == 2, "A StereoPanner can only be applied to stereo channels");
final channelViewL = buffer.getChannelView(0);
final channelViewR = buffer.getChannelView(1);
final stepSizeL = pVolumeLeft.getLerpStepSize(buffer.channelLength);
final stepSizeR = pVolumeRight.getLerpStepSize(buffer.channelLength);
#if AURA_SIMD
final stepSizesL = pVolumeLeft.getLerpStepSizes32x4(buffer.channelLength);
final stepSizesR = pVolumeRight.getLerpStepSizes32x4(buffer.channelLength);
final lenRemainder = mod4(buffer.channelLength);
final startRemainder = buffer.channelLength - lenRemainder - 1;
for (i in (0...buffer.channelLength).step(4)) {
pVolumeLeft.applySIMD32x4(channelViewL, i, stepSizesL);
pVolumeRight.applySIMD32x4(channelViewR, i, stepSizesR);
}
for (i in startRemainder...lenRemainder) {
channelViewL[i] *= pVolumeLeft.currentValue;
channelViewR[i] *= pVolumeRight.currentValue;
pVolumeLeft.currentValue += stepSizeL;
pVolumeRight.currentValue += stepSizeR;
}
#else
for (i in 0...buffer.channelLength) {
channelViewL[i] *= pVolumeLeft.currentValue;
channelViewR[i] *= pVolumeRight.currentValue;
pVolumeLeft.currentValue += stepSizeL;
pVolumeRight.currentValue += stepSizeR;
}
#end
pVolumeLeft.updateLast();
pVolumeRight.updateLast();
}
override function parseMessage(message: Message) {
switch (message.id) {
case StereoPannerMessageID.PVolumeLeft: pVolumeLeft.targetValue = cast message.data;
case StereoPannerMessageID.PVolumeRight: pVolumeRight.targetValue = cast message.data;
default:
super.parseMessage(message);
}
}
}
class StereoPannerMessageID extends DSPMessageID {
final PVolumeLeft;
final PVolumeRight;
}

View File

@ -0,0 +1,60 @@
package aura.dsp.sourcefx;
import aura.types.AudioBuffer;
import aura.Types;
/**
A special type of audio effect that—unlike insert effects—is not applied
continuously during audio playback but instead to the audio source buffer of
an `aura.channels.UncompBufferChannel` object.
This allows `SourceEffect`s to bake effects or provide sound variations
(for example by selecting random sounds from a pool of sounds, or by creating
sound variations on the fly with `aura.dsp.sourcefx.VelvetNoiseVariator`).
**/
abstract class SourceEffect {
/**
If `false` (default), `SourceEffect.process()` is only called
before the linked audio channel is played for the very first time with
its current combination of source effects. Adding or removing source
effects to a channel results in a recalculation of all source effects
on that channel.
If `true`, _additionally_ call `SourceEffect.process()` before each
consecutive replay of the audio source, including:
- Repetitions if the audio source is looping
- Calls to `audioChannel.play()` if the audio channel was stopped or
`play()` is called with `retrigger` set to `true`.
**/
public var applyOnReplay(default, null): AtomicBool = new AtomicBool(false);
/**
`SourceEffect`s are allowed to change the length of the source
audio passed as `srcBuffer` to `SourceEffect.process()`.
This function is used to calculate the amount of memory that needs to be
allocated to efficiently process all audio source effects of a channel.
It must return the least required channel length of the effect's
destination buffer with respect to the given source channel length.
**/
abstract function calculateRequiredChannelLength(srcChannelLength: Int): Int;
/**
Apply the effect to the audio data stored in the given source buffer and
write the result into the destination buffer.
- `srcBuffer` and `dstBuffer` may or may not point to the same object.
- The channels of `srcBuffer` might be longer than the valid audio
contained, use `srcChannelLength` to get the amount of valid samples
in each channel of the source buffer.
- `dstBuffer` is guaranteed to contain channels _at least_ the length
of `calculateRequiredChannelLength(srcChannelLength)`, it is expected
that the source effect fills `dstBuffer` exactly to that length.
This function must return the required destination channel length as
calculated by `calculateRequiredChannelLength(srcChannelLength)`.
**/
abstract function process(srcBuffer: AudioBuffer, srcChannelLength: Int, dstBuffer: AudioBuffer): Int;
}

View File

@ -0,0 +1,119 @@
package aura.dsp.sourcefx;
import kha.FastFloat;
import aura.dsp.SparseConvolver;
import aura.types.AudioBuffer;
import aura.utils.MathUtils;
import aura.utils.FrequencyUtils;
/**
Generate infinite variations on short percussive samples on the fly,
following the technique from the paper linked below.
The parameters of this effect need careful tweaking. Some examples can be
found in _Table 1_ in the paper linked below.
**Paper**:
Fagerström, Jon & Schlecht, Sebastian & Välimäki, Vesa. (2021).
One-to-Many Conversion for Percussive Samples. doi.org/10.23919/DAFx51585.2021.9768256.
**/
class VelvetNoiseVariator extends SourceEffect {
public final noiseLengthMs: FastFloat;
public final strength: FastFloat;
public final decayRate: FastFloat;
final highpassFilter: Filter;
final sparseConvolver: SparseConvolver;
var averageImpulseSpacing: Float;
/**
Create a new `VelvetNoiseVariator`.
@param noiseLengthMs The length of the velvet noise used for convolution, in milliseconds.
@param numImpulses The amount of impulses in the velvet noise.
@param decayRate The strength of the exponential decay of the velvet noise impulses.
@param lowShelfCutoff The cutoff frequency for the integrated high-pass filter.
@param strength The strength/influence of this effect. Think of this as a dry/wet control.
**/
public function new(noiseLengthMs: FastFloat, numImpulses: Int, decayRate: FastFloat, lowShelfCutoff: Hertz, strength: FastFloat) {
this.noiseLengthMs = noiseLengthMs;
final noiseLengthSamples = msToSamples(Aura.sampleRate, noiseLengthMs);
this.sparseConvolver = new SparseConvolver(numImpulses, noiseLengthSamples);
this.averageImpulseSpacing = maxF(1.0, noiseLengthSamples / numImpulses);
this.highpassFilter = new Filter(HighPass);
highpassFilter.setCutoffFreq(lowShelfCutoff, All);
this.applyOnReplay.store(true);
this.decayRate = decayRate;
this.strength = strength;
}
public static function fillVelvetNoiseSparse(impulseBuffer: SparseImpulseBuffer, averageImpulseSpacing: Float, decayRate: FastFloat) {
var nextGridPosPrecise = 0.0;
var nextGridPosRounded = 0;
var nextImpulsePos = 0;
// Attenuate consecutive pulses
final expFactor = Math.pow(E_INV, decayRate); // e^(-decayRate) == 1/e^decayRate == (1/e)^decayRate
var exponentialDecayFactor = 1.0;
for (i in 0...impulseBuffer.length) {
final currentGridPosRounded = nextGridPosRounded;
nextGridPosPrecise += averageImpulseSpacing;
nextGridPosRounded = Math.round(nextGridPosPrecise);
nextImpulsePos = currentGridPosRounded + Std.random(nextGridPosRounded - currentGridPosRounded);
impulseBuffer.setImpulsePos(i, nextImpulsePos);
impulseBuffer.setImpulseMagnitude(i, (Math.random() < 0.5 ? -1.0 : 1.0) * exponentialDecayFactor);
exponentialDecayFactor *= expFactor; // e^(-decayRate*i) == e^(-decayRate)^i
}
}
function calculateRequiredChannelLength(srcChannelLength: Int): Int {
return srcChannelLength + sparseConvolver.getMaxNumImpulseResponseSamples() - 1;
}
@:access(aura.dsp.SparseConvolver)
function process(srcBuffer: AudioBuffer, srcChannelLength: Int, dstBuffer: AudioBuffer): Int {
final requiredLength = calculateRequiredChannelLength(srcChannelLength);
// Copy and pad data
for (c in 0...srcBuffer.numChannels) {
final srcChannelView = srcBuffer.getChannelView(c);
final dstChannelView = dstBuffer.getChannelView(c);
for (i in 0...srcChannelLength) {
dstChannelView[i] = srcChannelView[i];
}
// Pad with zeroes to convolve without overlapping
for (i in srcChannelLength...requiredLength) {
dstChannelView[i] = 0.0;
}
}
fillVelvetNoiseSparse(sparseConvolver.impulseBuffer, averageImpulseSpacing, decayRate);
highpassFilter.process(dstBuffer);
sparseConvolver.process(dstBuffer);
for (c in 0...srcBuffer.numChannels) {
final srcChannelView = srcBuffer.getChannelView(c);
final dstChannelView = dstBuffer.getChannelView(c);
for (i in 0...srcChannelLength) {
dstChannelView[i] = dstChannelView[i] * strength + srcChannelView[i];
}
for (i in srcChannelLength...requiredLength) {
dstChannelView[i] = dstChannelView[i] * strength;
}
}
return requiredLength;
}
}