Update Files
This commit is contained in:
36
lib/aura/Sources/aura/dsp/DSP.hx
Normal file
36
lib/aura/Sources/aura/dsp/DSP.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
61
lib/aura/Sources/aura/dsp/DelayLine.hx
Normal file
61
lib/aura/Sources/aura/dsp/DelayLine.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
217
lib/aura/Sources/aura/dsp/FFTConvolver.hx
Normal file
217
lib/aura/Sources/aura/dsp/FFTConvolver.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
lib/aura/Sources/aura/dsp/Filter.hx
Normal file
86
lib/aura/Sources/aura/dsp/Filter.hx
Normal 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;
|
||||
}
|
||||
99
lib/aura/Sources/aura/dsp/FractionalDelayLine.hx
Normal file
99
lib/aura/Sources/aura/dsp/FractionalDelayLine.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
lib/aura/Sources/aura/dsp/HaasEffect.hx
Normal file
55
lib/aura/Sources/aura/dsp/HaasEffect.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
109
lib/aura/Sources/aura/dsp/SparseConvolver.hx
Normal file
109
lib/aura/Sources/aura/dsp/SparseConvolver.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
120
lib/aura/Sources/aura/dsp/panner/HRTFPanner.hx
Normal file
120
lib/aura/Sources/aura/dsp/panner/HRTFPanner.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
166
lib/aura/Sources/aura/dsp/panner/Panner.hx
Normal file
166
lib/aura/Sources/aura/dsp/panner/Panner.hx
Normal 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;
|
||||
}
|
||||
127
lib/aura/Sources/aura/dsp/panner/StereoPanner.hx
Normal file
127
lib/aura/Sources/aura/dsp/panner/StereoPanner.hx
Normal 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;
|
||||
}
|
||||
60
lib/aura/Sources/aura/dsp/sourcefx/SourceEffect.hx
Normal file
60
lib/aura/Sources/aura/dsp/sourcefx/SourceEffect.hx
Normal 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;
|
||||
}
|
||||
119
lib/aura/Sources/aura/dsp/sourcefx/VelvetNoiseVariator.hx
Normal file
119
lib/aura/Sources/aura/dsp/sourcefx/VelvetNoiseVariator.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user