Update Files

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

80
lib/aura/Tests/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,80 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Aura: Run unit tests",
"type": "process",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "dedicated",
},
"command": "node",
"args": [
"run.js"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"KHA_PATH": "${command:kha.findKha}",
"ELECTRON_BIN": "${command:kha.findKhaElectron}",
// "ELECTRON_NO_ATTACH_CONSOLE": "true",
}
},
"problemMatcher": [
"$haxe", // Default Haxe matcher
{
// Electron
"owner": "custom",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": [
// {
// "regexp": "^\\s*Error:\\s+(.*)\\s*$",
// "message": 1
// },
// {
// "regexp": "^\\s*Stack:\\s*$",
// },
// {
// "regexp": "^\\s*$",
// },
// {
// "regexp": "^\\s*Called from\\s+(.*)\\s+\\(file:\\/\\/\\/(.*)\\s+line\\s+(\\d+)\\s+column\\s+(\\d+)\\)\\s*$",
// "code": 1,
// "file": 2,
// "line": 3,
// "column": 4
// }
{
"regexp": "^\\s*Error:\\s+Uncaught\\s+(.*):(\\d+):\\s+(.*)\\s*$",
"file": 1,
"line": 2,
"message": 3
},
]
},
{
// Also catch exceptions thrown by test failures in nodejs
"owner": "haxe",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": [
{
"regexp": "^\\<ref\\s+\\*1\\>\\s+Error:\\s+(.*):(\\d+):(.*)$",
"file": 1,
"line": 2,
"message": 3,
},
]
}
],
},
]
}

View File

@ -0,0 +1,18 @@
electron.ipcMain.on('log-main', (event, type, text) => {
switch (type) {
case "log":
console.log(text);
break;
case "info":
console.info(text);
break;
case "warn":
console.warn(text);
break;
case "error":
console.error(text);
break;
default:
throw "Unreachable";
}
});

View File

@ -0,0 +1,20 @@
<!--
Custom index.html to prevent a wrong layout of test results due to the Kha canvas.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Aura Unit Tests</title>
</head>
<body style="margin: 0; padding: 0;">
<!--
We still need to include the canvas to prevent Kha errors, but it's
at size (0, 0).
-->
<canvas id="khanvas" width="0" height="0" tabindex="-1"></canvas>
<script src="kha.js"></script>
</body>
</html>

96
lib/aura/Tests/Main.hx Normal file
View File

@ -0,0 +1,96 @@
package;
import utest.Runner;
import utest.ui.Report;
#if instrument
import instrument.Instrumentation;
#end
class Main {
static function main() {
kha.System.start({title: "Aura Unit Tests", width: 1024, height: 768}, (window: kha.Window) -> {
replaceConsoleFunctions();
#if (AURA_ASSERT_LEVEL!="Debug")
trace("Warning: Running tests below highest assertion level, some tests might erroneously succeed");
#end
kha.Assets.loadEverything(() -> {
kha.audio2.Audio.samplesPerSecond = 44100;
aura.Aura.init();
var runner = new Runner();
runner.addCases(auratests, true);
// addCases() only allows one class per file (https://github.com/haxe-utest/utest/blob/f759c0aa257aa723b3dd607cf7cb53d16194d13f/src/utest/Runner.hx#L171),
// so we manually add classes here where this is not the case
runner.addCase(new auratests.dsp.TestSparseConvolver.TestSparseImpulseBuffer());
runner.onComplete.add((_) -> {
#if instrument
Instrumentation.endInstrumentation(Coverage);
#end
});
Report.create(runner);
// new utest.ui.text.PrintReport(runner);
runner.run();
});
});
}
/**
In Kha applications, `console.log()` calls called by `trace` are called
from within the renderer process which prevents them from showing up in
the console (instead they only show up in the devtools console).
A possible workaround is to run electron with `--enable-logging`,
but this will show the traces in a bunch of irrelevant and noisy debug
information and on Windows a bunch of terminal windows are opened if
electron is not directly called from the shell. So instead, we send
traces to the main thread and then log them there.
**See:**
- Log in main process/renderer process:
- https://stackoverflow.com/a/31759944/9985959
- Overriding console functions:
- https://stackoverflow.com/a/30197398/9985959
- Electron opening multiple empty terminals on Windows:
- https://github.com/electron/electron/issues/3846
- https://github.com/electron/electron/issues/4582
- https://github.com/electron-userland/spectron/issues/60#issuecomment-482070086
**/
static function replaceConsoleFunctions() {
#if kha_debug_html5
final oldConsole: Dynamic = js.Syntax.code("window.console");
function log(text: Dynamic) {
oldConsole.log(text);
js.Syntax.code("window.electron.logToMainProcess('log', {0})", text);
}
function info(text: Dynamic) {
oldConsole.info(text);
js.Syntax.code("window.electron.logToMainProcess('info', {0})", text);
}
function warn(text: Dynamic) {
oldConsole.warn(text);
js.Syntax.code("window.electron.logToMainProcess('warn', {0})", text);
}
function error(text: Dynamic) {
oldConsole.error(text);
js.Syntax.code("window.electron.logToMainProcess('error', {0})", text);
}
js.Syntax.code("window.console = {log: {0}, info: {1}, warn: {2}, error: {3}}", log, info, warn, error);
#end
}
}

View File

@ -0,0 +1,2 @@
-lib utest:1.13.2
-lib instrument:git:https://github.com/AlexHaxe/haxe-instrument.git#92a5691c7e77a696532e2e13ac1f717841f43015

48
lib/aura/Tests/Utils.hx Normal file
View File

@ -0,0 +1,48 @@
package;
import haxe.PosInfos;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Aura;
import aura.channels.UncompBufferChannel;
inline function createDummyHandle(): BaseChannelHandle {
final data = new kha.arrays.Float32Array(8);
final channel = new UncompBufferChannel(data, false);
return new BaseChannelHandle(channel);
}
inline function int32ToBytesString(i: Int): String {
var str = "";
for (j in 0...32) {
final mask = 1 << (31 - j);
str += (i & mask) == 0 ? "0" : "1";
}
return str;
}
inline function assertRaisesAssertion(func: Void->Void) {
#if (AURA_ASSERT_LEVEL!="NoAssertions")
Assert.raises(func, aura.utils.Assert.AuraAssertionException);
#else
Assert.pass();
#end
}
function assertEqualsFloat32Array(expected: Float32Array, have: Float32Array, ?pos: PosInfos) {
if (expected.length != have.length) {
Assert.fail('Expected Float32Array of length ${expected.length}, but got length ${have.length}', pos);
return;
}
for (i in 0...expected.length) {
if (!@:privateAccess Assert._floatEquals(expected[i], have[i])) {
Assert.fail('Expected value at index $i to be ${expected[i]}, but got ${have[i]} (only first difference reported)', pos);
return;
}
}
Assert.pass(null, pos);
}

View File

@ -0,0 +1,121 @@
package auratests;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Aura;
import aura.Types.Hertz;
import aura.types.AudioBuffer;
import aura.utils.BufferUtils;
import Utils;
class StaticValueGenerator extends aura.channels.generators.BaseGenerator {
public var counter = 0;
inline function new() {}
public static function create(): BaseChannelHandle {
return new BaseChannelHandle(new StaticValueGenerator());
}
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz) {
for (i in 0...requestedSamples.channelLength) {
for (c in 0...requestedSamples.numChannels) {
requestedSamples.getChannelView(c)[i] = (++counter) / 4096;
}
}
}
}
@:access(aura.Aura)
class TestAura extends utest.Test {
final staticInput = StaticValueGenerator.create();
function setup() {
staticInput.play();
@:privateAccess (cast staticInput.channel: StaticValueGenerator).counter = 0;
Aura.blockBufPos = 0;
}
function teardown() {
staticInput.setMixChannel(null);
}
function test_audioCallback_zeroIfNoInput() {
final compareArray = createEmptyF32Array(Aura.BLOCK_SIZE);
final buffer = new kha.audio2.Buffer(Aura.BLOCK_SIZE, 2, 44100);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(Aura.BLOCK_SIZE), buffer);
assertEqualsFloat32Array(compareArray, buffer.data);
}
function test_audioCallback_zeroIfNoSampleCache() {
final compareArray = createEmptyF32Array(Aura.BLOCK_SIZE);
staticInput.setMixChannel(Aura.masterChannel);
// Force sampleCache to be null
Aura.p_samplesBuffer.set(null);
kha.audio2.Audio.disableGcInteractions = true;
final buffer = new kha.audio2.Buffer(Aura.BLOCK_SIZE, 2, 44100);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(Aura.BLOCK_SIZE), buffer);
assertEqualsFloat32Array(compareArray, buffer.data);
}
function test_audioCallback_contiguouslyWritesBlocksToOutput() {
final numRequestedSamples = Aura.BLOCK_SIZE * 2 + 2;
final compareArray = new Float32Array(numRequestedSamples);
for (i in 0...compareArray.length) {
compareArray[i] = (i + 1) / 4096;
}
staticInput.setMixChannel(Aura.masterChannel);
final buffer = new kha.audio2.Buffer(numRequestedSamples, 2, 44100);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(numRequestedSamples), buffer);
assertEqualsFloat32Array(compareArray, buffer.data);
}
function test_audioCallback_splitLargeBlockOverMultipleCallbacks() {
final numRequestedSamples = Std.int(Aura.BLOCK_SIZE / 2) - 2;
final compareArray = new Float32Array(3 * numRequestedSamples);
for (i in 0...compareArray.length) {
compareArray[i] = (i + 1) / 4096;
}
staticInput.setMixChannel(Aura.masterChannel);
final buffer = new kha.audio2.Buffer(numRequestedSamples, 2, 44100);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(numRequestedSamples), buffer);
assertEqualsFloat32Array(compareArray.subarray(0, numRequestedSamples), buffer.data);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(numRequestedSamples), buffer);
assertEqualsFloat32Array(compareArray.subarray(numRequestedSamples, numRequestedSamples * 2), buffer.data);
fillBuffer(buffer.data, -1.0); // Poison buffer
Aura.audioCallback(new kha.internal.IntBox(numRequestedSamples), buffer);
assertEqualsFloat32Array(compareArray.subarray(numRequestedSamples * 2, numRequestedSamples * 3), buffer.data);
}
// TODO
// function test_audioCallback_synchronizesMasterChannel() {}
// function test_audioCallback_updatesTime() {}
// function test_audioCallback_numChannelsOtherThanNUM_OUTPUT_CHANNELS() {
// TODO this needs changes in the audio callback. Too dynamic? But Kha might request this...
// }
}

View File

@ -0,0 +1,95 @@
package auratests;
import utest.Assert;
import aura.Aura;
import aura.Time;
import aura.Listener;
import aura.math.Vec3;
import Utils;
@:access(aura.Listener)
class TestListener extends utest.Test {
var listener: Listener;
function setup() {
listener = new Listener();
}
function teardown() {
Time.overrideTime = null;
}
function test_setLocation_multipleCallsOnFirstTimestep() {
Time.overrideTime = 0.0;
listener.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, listener.location.x);
Assert.floatEquals(0.6, listener.location.y);
Assert.floatEquals(0.7, listener.location.z);
Assert.floatEquals(0.0, listener.velocity.x);
Assert.floatEquals(0.0, listener.velocity.y);
Assert.floatEquals(0.0, listener.velocity.z);
Time.overrideTime = 0.0;
listener.setLocation(new Vec3(1.0, 2.0, 3.0));
Assert.floatEquals(1.0, listener.location.x);
Assert.floatEquals(2.0, listener.location.y);
Assert.floatEquals(3.0, listener.location.z);
Assert.floatEquals(0.0, listener.velocity.x);
Assert.floatEquals(0.0, listener.velocity.y);
Assert.floatEquals(0.0, listener.velocity.z);
}
function test_setLocation_firstCall_timeDeltaZero() {
Time.overrideTime = 0.0;
listener.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, listener.location.x);
Assert.floatEquals(0.6, listener.location.y);
Assert.floatEquals(0.7, listener.location.z);
Assert.floatEquals(0.0, listener.velocity.x);
Assert.floatEquals(0.0, listener.velocity.y);
Assert.floatEquals(0.0, listener.velocity.z);
}
function test_setLocation_firstCall_timeDeltaPositive() {
Time.overrideTime = 2.0;
listener.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, listener.location.x);
Assert.floatEquals(0.6, listener.location.y);
Assert.floatEquals(0.7, listener.location.z);
Assert.floatEquals(0.0, listener.velocity.x);
Assert.floatEquals(0.0, listener.velocity.y);
Assert.floatEquals(0.0, listener.velocity.z);
}
function test_setLocation_subsequentCalls_timeDeltaZero() {
// Regression test for https://github.com/MoritzBrueckner/aura/pull/8
Time.overrideTime = 1.0;
listener.setLocation(new Vec3(0.0, 0.0, 0.0));
Time.overrideTime = 3.0;
listener.setLocation(new Vec3(1.0, 2.0, 3.0));
Time.overrideTime = 3.0;
listener.setLocation(new Vec3(2.0, 4.0, 6.0));
Assert.floatEquals(2.0, listener.location.x);
Assert.floatEquals(4.0, listener.location.y);
Assert.floatEquals(6.0, listener.location.z);
// Compute velocity based on timestep 1.0
Assert.floatEquals(1.0, listener.velocity.x);
Assert.floatEquals(2.0, listener.velocity.y);
Assert.floatEquals(3.0, listener.velocity.z);
}
}

View File

@ -0,0 +1,95 @@
package auratests.channels;
import utest.Assert;
import aura.Aura;
import aura.channels.MixChannel;
import aura.channels.UncompBufferResamplingChannel;
class TestBaseChannelHandle extends utest.Test {
var handle: BaseChannelHandle;
var channel: UncompBufferResamplingChannel;
var data = new kha.arrays.Float32Array(8);
function setup() {
channel = new UncompBufferResamplingChannel(data, false, 44100);
handle = new BaseChannelHandle(channel);
}
function teardown() {}
function test_setMixChannelAddsInputIfNotYetExisting() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
Assert.equals(0, handle2.getNumInputs());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
}
function test_setMixChannelDoesntAddAlreadyExistingInput() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
}
function test_setMixChannelNullRemovesInputIfExisting() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
Assert.equals(0, handle2.getNumInputs());
Assert.isTrue(handle1.setMixChannel(null));
Assert.equals(0, handle2.getNumInputs());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
Assert.isTrue(handle1.setMixChannel(null));
Assert.equals(0, handle2.getNumInputs());
}
function test_setMixChannelSwitchingMixChannelCorrectlyChangesInputs() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
final handle3 = new MixChannelHandle(new MixChannel());
Assert.equals(0, handle2.getNumInputs());
Assert.equals(0, handle3.getNumInputs());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
Assert.equals(0, handle3.getNumInputs());
Assert.isTrue(handle1.setMixChannel(handle3));
Assert.equals(0, handle2.getNumInputs());
Assert.equals(1, handle3.getNumInputs());
}
function test_setMixChannelSelfReferenceReturnsFalseAndRemovesInput() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.equals(1, handle2.getNumInputs());
Assert.isFalse(handle1.setMixChannel(handle1));
Assert.equals(0, handle2.getNumInputs());
}
function test_setMixChannelCircularDependencyReturnsFalseAndRemovesInput() {
final handle1 = new MixChannelHandle(new MixChannel());
final handle2 = new MixChannelHandle(new MixChannel());
final handle3 = new MixChannelHandle(new MixChannel());
final handle4 = new MixChannelHandle(new MixChannel());
Assert.isTrue(handle3.setMixChannel(handle4));
Assert.isTrue(handle1.setMixChannel(handle2));
Assert.isTrue(handle2.setMixChannel(handle3));
Assert.isFalse(handle3.setMixChannel(handle1));
Assert.equals(0, handle4.getNumInputs());
}
}

View File

@ -0,0 +1,41 @@
package auratests.channels;
import utest.Assert;
import aura.channels.MixChannel;
@:access(aura.channels.MixChannel)
class TestMixChannel extends utest.Test {
var mixChannel: MixChannel;
var mixChannelHandle: MixChannelHandle;
function setupClass() {}
function setup() {
mixChannel = new MixChannel();
mixChannelHandle = new MixChannelHandle(mixChannel);
}
function teardown() {}
function test_startUnpausedAndUnfinished() {
// Regression test for https://github.com/MoritzBrueckner/aura/issues/7
final inputHandle = new MixChannelHandle(new MixChannel());
Assert.isFalse(mixChannel.paused);
Assert.isFalse(mixChannel.finished);
}
function test_isNotPlayable_ifNoInputChannelExists() {
Assert.isFalse(mixChannel.isPlayable());
}
function test_isPlayable_ifInputChannelExists() {
final inputHandle = new MixChannelHandle(new MixChannel());
inputHandle.setMixChannel(mixChannelHandle);
Assert.isTrue(mixChannel.isPlayable());
}
}

View File

@ -0,0 +1,218 @@
package auratests.channels;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Types.Balance;
import aura.channels.UncompBufferChannel;
import aura.dsp.sourcefx.SourceEffect;
import aura.types.AudioBuffer;
@:access(aura.channels.UncompBufferChannel)
class TestUncompBufferChannel extends utest.Test {
static inline var channelLength = 16;
var audioChannel: UncompBufferChannel;
var sourceFX1: SourceFXDummy;
var sourceFX2: SourceFXDummy;
final data = new Float32Array(2 * channelLength);
function setupClass() {}
function setup() {
audioChannel = new UncompBufferChannel(data, false);
sourceFX1 = new SourceFXDummy();
sourceFX2 = new SourceFXDummy();
audioChannel.addSourceEffect(sourceFX1);
audioChannel.addSourceEffect(sourceFX2);
}
function teardown() {}
function test_optionallyApplySourceEffects_isAppliedOnFirstPlay_ifNoEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(false);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
audioChannel.play(false);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isAppliedOnFirstPlay_ifAnyEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(true);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
audioChannel.play(false);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isNotAppliedOnSecondPlayAfterFinish_ifNoEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(false);
audioChannel.play(false);
audioChannel.stop();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(false);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isAppliedOnSecondPlayAfterFinish_ifAnyEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(true);
audioChannel.play(false);
audioChannel.stop();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(false);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isNotAppliedOnPlayAfterPause_ifNoEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(false);
audioChannel.play(false);
audioChannel.pause();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(false);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isNotAppliedOnPlayAfterPause_ifAnyEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(true);
audioChannel.play(false);
audioChannel.pause();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(false);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isNotAppliedOnRetrigger_ifNoEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(false);
audioChannel.play(false);
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(true);
Assert.isFalse(sourceFX1.wasProcessCalled);
Assert.isFalse(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isAppliedOnRetrigger_ifAnyEffectIsConfiguredToApplyOnReplay() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(true);
audioChannel.play(false);
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.play(true);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
}
function test_optionallyApplySourceEffects_isAppliedOnConsecutivePlays_ifEffectsHaveChanged() {
sourceFX1.applyOnReplay.store(false);
sourceFX2.applyOnReplay.store(false);
audioChannel.play(false);
audioChannel.stop();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
final tempSourceFX = new SourceFXDummy();
audioChannel.addSourceEffect(tempSourceFX);
audioChannel.play(false);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
audioChannel.stop();
sourceFX1.wasProcessCalled = false;
sourceFX2.wasProcessCalled = false;
audioChannel.removeSourceEffect(tempSourceFX);
audioChannel.play(false);
Assert.isTrue(sourceFX1.wasProcessCalled);
Assert.isTrue(sourceFX2.wasProcessCalled);
}
function test_nextSamples_onLoop_ApplySourceEffectsOnce() {
audioChannel.looping = true;
Assert.equals(0, sourceFX1.numProcessCalled);
Assert.equals(0, sourceFX2.numProcessCalled);
final outBuffer = new AudioBuffer(2, channelLength + 1);
audioChannel.nextSamples(outBuffer, 1000);
// Make sure process is only called once for _all_ channels
Assert.equals(1, sourceFX1.numProcessCalled);
Assert.equals(1, sourceFX2.numProcessCalled);
}
}
private class SourceFXDummy extends SourceEffect {
public var wasProcessCalled = false;
public var numProcessCalled = 0;
public function new() {}
function calculateRequiredChannelLength(srcChannelLength: Int): Int {
return srcChannelLength;
}
function process(srcBuffer: AudioBuffer, srcChannelLength: Int, dstBuffer: AudioBuffer): Int {
wasProcessCalled = true;
numProcessCalled++;
return srcChannelLength;
}
}

View File

@ -0,0 +1,126 @@
package auratests.channels;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Types.Balance;
import aura.channels.UncompBufferResamplingChannel;
import aura.dsp.sourcefx.SourceEffect;
import aura.types.AudioBuffer;
@:access(aura.channels.UncompBufferResamplingChannel)
class TestUncompBufferResamplingChannel extends utest.Test {
static inline var channelLength = 16;
var audioChannel: UncompBufferResamplingChannel;
final rampLeft = new Array<Float>();
final rampRight = new Array<Float>();
final data = new Float32Array(2 * channelLength); // interleaved stereo
function setupClass() {
rampLeft.resize(channelLength);
rampRight.resize(channelLength);
for (i in 0...channelLength) { // Fill data with a value ramp
final val = (i + 1) / channelLength;
data[i * 2 + 0] = rampLeft[i] = val;
data[i * 2 + 1] = rampRight[i] = -val;
}
}
function setup() {
audioChannel = new UncompBufferResamplingChannel(data, false, 1000);
}
function teardown() {}
function test_dataConversion() {
for (i in 0...channelLength) {
Assert.floatEquals(rampLeft[i], audioChannel.data.getChannelView(0)[i]);
Assert.floatEquals(rampRight[i], audioChannel.data.getChannelView(1)[i]);
}
}
function test_nextSamples() {
final outBuffer = new AudioBuffer(2, channelLength);
audioChannel.nextSamples(outBuffer, 1000);
for (i in 0...channelLength) {
Assert.floatEquals(rampLeft[i], outBuffer.getChannelView(0)[i]);
Assert.floatEquals(rampRight[i], outBuffer.getChannelView(1)[i]);
}
// Now the channel has processed all data and will reset position to 0
Assert.equals(0, audioChannel.playbackPosition);
Assert.floatEquals(0.0, audioChannel.floatPosition);
// No looping, but request more samples
final longOutBuffer = new AudioBuffer(2, channelLength + 4);
audioChannel.nextSamples(longOutBuffer, 1000);
for (i in 0...channelLength) {
Assert.floatEquals(rampLeft[i], longOutBuffer.getChannelView(0)[i]);
Assert.floatEquals(rampRight[i], longOutBuffer.getChannelView(1)[i]);
}
for (i in channelLength...channelLength + 4) {
Assert.floatEquals(0.0, longOutBuffer.getChannelView(0)[i]);
Assert.floatEquals(0.0, longOutBuffer.getChannelView(1)[i]);
}
// Now change the sample rate, second half should be zero
audioChannel.playbackPosition = 0;
audioChannel.floatPosition = 0.0;
audioChannel.nextSamples(outBuffer, 500);
for (i in Std.int(channelLength / 2)...channelLength) {
Assert.floatEquals(0.0, outBuffer.getChannelView(0)[i]);
Assert.floatEquals(0.0, outBuffer.getChannelView(1)[i]);
}
// Now with looping
audioChannel.playbackPosition = 0;
audioChannel.floatPosition = 0.0;
audioChannel.looping = true;
audioChannel.nextSamples(outBuffer, 500);
final halfChannelLength = Std.int(channelLength / 2);
for (i in 0...halfChannelLength) {
Assert.floatEquals(outBuffer.getChannelView(0)[i], outBuffer.getChannelView(0)[halfChannelLength + i], null, '$i');
Assert.floatEquals(outBuffer.getChannelView(1)[i], outBuffer.getChannelView(1)[halfChannelLength + i], null, '$i');
}
// TODO: check sample precise looping without gaps with unusual sample rates?
}
function test_nextSamples_onLoop_ApplySourceEffectsOnce() {
audioChannel.looping = true;
final sourceFX = new SourceFXDummy();
audioChannel.addSourceEffect(sourceFX);
Assert.equals(0, sourceFX.numProcessCalled);
final outBuffer = new AudioBuffer(2, channelLength + 1);
audioChannel.nextSamples(outBuffer, 1000);
// Make sure process is only called once for _all_ channels
Assert.equals(1, sourceFX.numProcessCalled);
}
}
private class SourceFXDummy extends SourceEffect {
public var numProcessCalled = 0;
public function new() {}
function calculateRequiredChannelLength(srcChannelLength: Int): Int {
return srcChannelLength;
}
function process(srcBuffer: AudioBuffer, srcChannelLength: Int, dstBuffer: AudioBuffer): Int {
numProcessCalled++;
return srcChannelLength;
}
}

View File

@ -0,0 +1,126 @@
package auratests.dsp;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Aura;
import aura.dsp.FFTConvolver;
import aura.types.AudioBuffer;
import aura.types.Complex;
import aura.utils.MathUtils;
import aura.utils.TestSignals;
@:access(aura.dsp.FFTConvolver)
class TestFFTConvolver extends utest.Test {
var audioBuffer: AudioBuffer;
var fftConvolver: FFTConvolver;
function setup() {
audioBuffer = new AudioBuffer(2, FFTConvolver.FFT_SIZE);
fftConvolver = new FFTConvolver();
}
function test_process_noFadeIfTemporalInterpLengthIsZero() {
fftConvolver.temporalInterpolationLength = 0;
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
}
setImpulseFreqsToConstant(new Complex(1.0, 0.0));
fftConvolver.process(audioBuffer);
discardOverlapForNextProcess();
for (i in 0...FFTConvolver.FFT_SIZE) {
Assert.floatEquals(Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), audioBuffer.getChannelView(1)[i]);
}
setImpulseFreqsToConstant(new Complex(0.0, 0.0));
fftConvolver.process(audioBuffer);
for (i in 0...FFTConvolver.FFT_SIZE) {
Assert.floatEquals(0, audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(0, audioBuffer.getChannelView(1)[i]);
}
}
function test_process_crossfadeIfTemporalInterpLengthIsLargerZero() {
fftConvolver.temporalInterpolationLength = 20;
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
}
setImpulseFreqsToConstant(new Complex(1.0, 0.0));
fftConvolver.process(audioBuffer);
discardOverlapForNextProcess();
for (i in 0...FFTConvolver.FFT_SIZE) {
final t = minF(i, fftConvolver.temporalInterpolationLength) / fftConvolver.temporalInterpolationLength;
Assert.floatEquals(lerp(0.0, Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), t), audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(lerp(0.0, Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), t), audioBuffer.getChannelView(1)[i]);
}
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 8 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.sin(i * 8 * Math.PI / audioBuffer.channelLength);
}
setImpulseFreqsToConstant(new Complex(0.0, 0.0));
fftConvolver.process(audioBuffer);
for (i in 0...FFTConvolver.FFT_SIZE) {
final t = minF(i, fftConvolver.temporalInterpolationLength) / fftConvolver.temporalInterpolationLength;
Assert.floatEquals(lerp(Math.sin(i * 8 * Math.PI / audioBuffer.channelLength), 0.0, t), audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(lerp(Math.sin(i * 8 * Math.PI / audioBuffer.channelLength), 0.0, t), audioBuffer.getChannelView(1)[i]);
}
}
function test_process_crossfadeEntireChunkSize() {
fftConvolver.temporalInterpolationLength = -1;
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.sin(i * 4 * Math.PI / audioBuffer.channelLength);
}
setImpulseFreqsToConstant(new Complex(1.0, 0.0));
fftConvolver.process(audioBuffer);
discardOverlapForNextProcess();
for (i in 0...FFTConvolver.FFT_SIZE) {
final t = minF(i, FFTConvolver.CHUNK_SIZE) / FFTConvolver.CHUNK_SIZE;
Assert.floatEquals(lerp(0.0, Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), t), audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(lerp(0.0, Math.sin(i * 4 * Math.PI / audioBuffer.channelLength), t), audioBuffer.getChannelView(1)[i]);
}
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 8 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.sin(i * 8 * Math.PI / audioBuffer.channelLength);
}
setImpulseFreqsToConstant(new Complex(0.0, 0.0));
fftConvolver.process(audioBuffer);
for (i in 0...FFTConvolver.FFT_SIZE) {
final t = minF(i, FFTConvolver.CHUNK_SIZE) / FFTConvolver.CHUNK_SIZE;
Assert.floatEquals(lerp(Math.sin(i * 8 * Math.PI / audioBuffer.channelLength), 0.0, t), audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(lerp(Math.sin(i * 8 * Math.PI / audioBuffer.channelLength), 0.0, t), audioBuffer.getChannelView(1)[i]);
}
}
function setImpulseFreqsToConstant(value: Complex) {
for (i in 0...FFTConvolver.FFT_SIZE) {
fftConvolver.impulseFFT.getOutput(0 + fftConvolver.currentImpulseAlternationIndex)[i] = value;
fftConvolver.impulseFFT.getOutput(2 + fftConvolver.currentImpulseAlternationIndex)[i] = value;
}
fftConvolver.currentImpulseAlternationIndex = 1 - fftConvolver.currentImpulseAlternationIndex;
fftConvolver.overlapLength[0] = FFTConvolver.CHUNK_SIZE;
fftConvolver.overlapLength[1] = FFTConvolver.CHUNK_SIZE;
fftConvolver.prevImpulseLengths[0] = FFTConvolver.CHUNK_SIZE;
fftConvolver.prevImpulseLengths[1] = FFTConvolver.CHUNK_SIZE;
}
function discardOverlapForNextProcess() {
for (c in 0...FFTConvolver.NUM_CHANNELS) {
for (i in 0...fftConvolver.overlapPrev[c].length) {
fftConvolver.overlapPrev[c][i] = 0.0;
}
}
}
}

View File

@ -0,0 +1,67 @@
package auratests.dsp;
import utest.Assert;
import aura.Aura;
import aura.dsp.FractionalDelayLine;
import aura.types.AudioBuffer;
import aura.utils.TestSignals;
@:access(aura.dsp.FractionalDelayLine)
class TestFractionalDelayLine extends utest.Test {
var audioBuffer: AudioBuffer;
var delayLine: FractionalDelayLine;
function setup() {
audioBuffer = new AudioBuffer(2, 8);
delayLine = new FractionalDelayLine(2, 8);
}
function test_zeroDelayTime_noDelay() {
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(0));
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(1));
delayLine.at_setDelayLength(Left, 0.0);
delayLine.at_setDelayLength(Right, 0.0);
delayLine.process(audioBuffer);
Assert.floatEquals(1.0, audioBuffer.getChannelView(0)[0]);
Assert.floatEquals(0.0, audioBuffer.getChannelView(0)[1]);
Assert.floatEquals(1.0, audioBuffer.getChannelView(1)[0]);
Assert.floatEquals(0.0, audioBuffer.getChannelView(1)[1]);
}
function test_integralDelayTime_independentChannels() {
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(0));
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(1));
delayLine.at_setDelayLength(Left, 1.0);
delayLine.at_setDelayLength(Right, 3.0);
delayLine.process(audioBuffer);
Assert.floatEquals(0.0, audioBuffer.getChannelView(0)[0]);
Assert.floatEquals(1.0, audioBuffer.getChannelView(0)[1]);
Assert.floatEquals(0.0, audioBuffer.getChannelView(1)[0]);
Assert.floatEquals(1.0, audioBuffer.getChannelView(1)[3]);
}
function test_floatDelayTime_independentChannels() {
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(0));
TestSignals.fillUnitImpulse(audioBuffer.getChannelView(1));
delayLine.at_setDelayLength(Left, 0.8);
delayLine.at_setDelayLength(Right, 3.4);
delayLine.process(audioBuffer);
Assert.floatEquals(0.2, audioBuffer.getChannelView(0)[0]);
Assert.floatEquals(0.8, audioBuffer.getChannelView(0)[1]);
Assert.floatEquals(0.6, audioBuffer.getChannelView(1)[3]);
Assert.floatEquals(0.4, audioBuffer.getChannelView(1)[4]);
}
}

View File

@ -0,0 +1,102 @@
package auratests.dsp;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.Aura;
import aura.dsp.SparseConvolver;
import aura.types.AudioBuffer;
import aura.utils.TestSignals;
@:access(aura.dsp.SparseConvolver)
class TestSparseConvolver extends utest.Test {
var audioBuffer: AudioBuffer;
var sparseConvolver: SparseConvolver;
function setup() {
audioBuffer = new AudioBuffer(2, 512);
sparseConvolver = new SparseConvolver(1, 4);
}
function test_simpleDelay() {
for (i in 0...audioBuffer.channelLength) {
audioBuffer.getChannelView(0)[i] = Math.sin(i * 2 * Math.PI / audioBuffer.channelLength);
audioBuffer.getChannelView(1)[i] = Math.cos(i * 2 * Math.PI / audioBuffer.channelLength);
}
final impulse = sparseConvolver.impulseBuffer;
impulse.setImpulsePos(0, 3);
impulse.setImpulseMagnitude(0, 1.0);
sparseConvolver.process(audioBuffer);
final wanted = new AudioBuffer(2, audioBuffer.channelLength);
for (i in 0...wanted.channelLength) {
wanted.getChannelView(0)[i] = Math.sin((i - 3) * 2 * Math.PI / wanted.channelLength);
wanted.getChannelView(1)[i] = Math.cos((i - 3) * 2 * Math.PI / wanted.channelLength);
}
for (i in 0...3) {
Assert.floatEquals(0, audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(0, audioBuffer.getChannelView(1)[i]);
}
for (i in 3...wanted.channelLength) {
Assert.floatEquals(wanted.getChannelView(0)[i], audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(wanted.getChannelView(1)[i], audioBuffer.getChannelView(1)[i]);
}
// Overlap
audioBuffer.clear();
sparseConvolver.process(audioBuffer);
for (i in 0...3) {
Assert.floatEquals(wanted.getChannelView(0)[i], audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(wanted.getChannelView(1)[i], audioBuffer.getChannelView(1)[i]);
}
for (i in 3...wanted.channelLength) {
Assert.floatEquals(0, audioBuffer.getChannelView(0)[i]);
Assert.floatEquals(0, audioBuffer.getChannelView(1)[i]);
}
}
}
@:access(aura.dsp.SparseConvolver.SparseImpulseBuffer)
class TestSparseImpulseBuffer extends utest.Test {
var buffer: SparseImpulseBuffer;
function setup() {
buffer = new SparseImpulseBuffer(4);
}
function test_length() {
Assert.equals(1, new SparseImpulseBuffer(1).length);
Assert.equals(2, new SparseImpulseBuffer(2).length);
Assert.equals(3, new SparseImpulseBuffer(3).length);
Assert.equals(1024, new SparseImpulseBuffer(1024).length);
}
function test_impulsePos_notOverwrittenByOtherImpulses() {
buffer.setImpulsePos(0, 3);
buffer.setImpulsePos(1, 9);
buffer.setImpulsePos(2, 17);
buffer.setImpulsePos(3, 42);
Assert.equals(3, buffer.getImpulsePos(0));
Assert.equals(9, buffer.getImpulsePos(1));
Assert.equals(17, buffer.getImpulsePos(2));
Assert.equals(42, buffer.getImpulsePos(3));
}
function test_impulseMagnitude_notOverwrittenByOtherImpulses() {
buffer.setImpulseMagnitude(0, 0.3);
buffer.setImpulseMagnitude(1, 0.9);
buffer.setImpulseMagnitude(2, 0.17);
buffer.setImpulseMagnitude(3, 0.42);
Assert.floatEquals(0.3, buffer.getImpulseMagnitude(0));
Assert.floatEquals(0.9, buffer.getImpulseMagnitude(1));
Assert.floatEquals(0.17, buffer.getImpulseMagnitude(2));
Assert.floatEquals(0.42, buffer.getImpulseMagnitude(3));
}
}

View File

@ -0,0 +1,283 @@
package auratests.dsp.panner;
import utest.Assert;
import aura.Aura;
import aura.Time;
import aura.dsp.panner.Panner;
import aura.math.Vec3;
import aura.types.AudioBuffer;
import Utils;
private class NonAbstractPanner extends Panner {
public function process(buffer: AudioBuffer) {}
}
@:access(aura.channels.BaseChannel)
@:access(aura.channels.BaseChannelHandle)
@:access(aura.dsp.panner.Panner)
class TestPanner extends utest.Test {
var handle: BaseChannelHandle;
var panner: Panner;
function setup() {
handle = Utils.createDummyHandle();
panner = new NonAbstractPanner(handle);
}
function teardown() {
Time.overrideTime = null;
}
function test_setLocation_multipleCallsOnFirstTimestep() {
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, panner.location.x);
Assert.floatEquals(0.6, panner.location.y);
Assert.floatEquals(0.7, panner.location.z);
Assert.floatEquals(0.0, panner.velocity.x);
Assert.floatEquals(0.0, panner.velocity.y);
Assert.floatEquals(0.0, panner.velocity.z);
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(1.0, 2.0, 3.0));
Assert.floatEquals(1.0, panner.location.x);
Assert.floatEquals(2.0, panner.location.y);
Assert.floatEquals(3.0, panner.location.z);
Assert.floatEquals(0.0, panner.velocity.x);
Assert.floatEquals(0.0, panner.velocity.y);
Assert.floatEquals(0.0, panner.velocity.z);
}
function test_setLocation_firstCall_timeDeltaZero() {
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, panner.location.x);
Assert.floatEquals(0.6, panner.location.y);
Assert.floatEquals(0.7, panner.location.z);
Assert.floatEquals(0.0, panner.velocity.x);
Assert.floatEquals(0.0, panner.velocity.y);
Assert.floatEquals(0.0, panner.velocity.z);
}
function test_setLocation_firstCall_timeDeltaPositive() {
Time.overrideTime = 2.0;
panner.setLocation(new Vec3(0.5, 0.6, 0.7));
Assert.floatEquals(0.5, panner.location.x);
Assert.floatEquals(0.6, panner.location.y);
Assert.floatEquals(0.7, panner.location.z);
Assert.floatEquals(0.0, panner.velocity.x);
Assert.floatEquals(0.0, panner.velocity.y);
Assert.floatEquals(0.0, panner.velocity.z);
}
function test_setLocation_subsequentCalls_timeDeltaZero() {
// Regression test for https://github.com/MoritzBrueckner/aura/pull/8
Time.overrideTime = 1.0;
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
Time.overrideTime = 3.0;
panner.setLocation(new Vec3(1.0, 2.0, 3.0));
Time.overrideTime = 3.0;
panner.setLocation(new Vec3(2.0, 4.0, 6.0));
Assert.floatEquals(2.0, panner.location.x);
Assert.floatEquals(4.0, panner.location.y);
Assert.floatEquals(6.0, panner.location.z);
// Compute velocity based on timestep 1.0
Assert.floatEquals(1.0, panner.velocity.x);
Assert.floatEquals(2.0, panner.velocity.y);
Assert.floatEquals(3.0, panner.velocity.z);
}
function test_update3D_noDopplerJumpIfLocationWasUninitialized() {
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(20.0, 0.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(1.0, handle.channel.pDopplerRatio.targetValue);
}
function test_noDopplerEffect_ifNoMovement() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(5.0, 4.0, 3.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(5.0, 4.0, 3.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(1.0, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_physicallyCorrectValues_pannerMovesAway() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(2.0, 0.0, 0.0));
panner.update3D();
Assert.floatEquals(4.0, @:privateAccess panner.velocity.length);
Assert.floatEquals(0.0, @:privateAccess aura.Aura.listener.velocity.length);
handle.channel.synchronize();
// Values calculated at
// https://www.omnicalculator.com/physics/doppler-effect?c=EUR&v=f0:5000!Hz,v:343.4!ms,vs:2!ms,vr:0!ms
// Assuming that their implementation is correct
Assert.floatEquals(4942.43 / 5000, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_physicallyCorrectValues_listenerMovesAway() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(2.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
Assert.floatEquals(4.0, @:privateAccess aura.Aura.listener.velocity.length);
Assert.floatEquals(0.0, @:privateAccess panner.velocity.length);
handle.channel.synchronize();
Assert.floatEquals(4941.76 / 5000, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_physicallyCorrectValues_pannerMovesCloser() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(4.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(2.0, 0.0, 0.0));
panner.update3D();
Assert.floatEquals(4.0, @:privateAccess panner.velocity.length);
Assert.floatEquals(0.0, @:privateAccess aura.Aura.listener.velocity.length);
handle.channel.synchronize();
Assert.floatEquals(5058.93 / 5000, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_physicallyCorrectValues_listenerMovesCloser() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(4.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(2.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
Assert.floatEquals(4.0, @:privateAccess aura.Aura.listener.velocity.length);
Assert.floatEquals(0.0, @:privateAccess panner.velocity.length);
handle.channel.synchronize();
Assert.floatEquals(5058.24 / 5000, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_noDopplerEffectIfNoRadialVelocity() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(2.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(1, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_noDopplerEffectIfNoRadialVelocity2() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(5.0, 0.0, 0.0));
panner.setLocation(new Vec3(5.0, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(5.0, 0.0, 0.0));
panner.setLocation(new Vec3(5.0, 0.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(1, handle.channel.pDopplerRatio.targetValue);
}
function test_calculateDoppler_noDopplerEffectIfNoRadialVelocity3() {
Time.overrideTime = 0.0;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(2.0, 2.0, 0.0));
panner.update3D();
Time.overrideTime = 0.5;
aura.Aura.listener.setLocation(new Vec3(0.0, 0.0, 0.0));
panner.setLocation(new Vec3(0.0, 2.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(1, handle.channel.pDopplerRatio.targetValue);
}
function test_dopplerEffect_isZeroIfPannerMovesCloserAtSpeedOfSound() {
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(Panner.SPEED_OF_SOUND + 1, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 1.0;
panner.setLocation(new Vec3(1, 0.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(0, handle.channel.pDopplerRatio.targetValue);
}
function test_dopplerEffect_pannerMovesCloserAboveSpeedOfSound() {
Time.overrideTime = 0.0;
panner.setLocation(new Vec3(Panner.SPEED_OF_SOUND + 5, 0.0, 0.0));
panner.update3D();
Time.overrideTime = 1.0;
panner.setLocation(new Vec3(1, 0.0, 0.0));
panner.update3D();
handle.channel.synchronize();
Assert.floatEquals(-85.85, handle.channel.pDopplerRatio.targetValue);
}
}

View File

@ -0,0 +1,66 @@
package auratests.format;
import haxe.Int64;
import haxe.io.Bytes;
import haxe.io.BytesInput;
import utest.Assert;
using aura.format.InputExtension;
class TestInputExtension extends utest.Test {
var bytes: Bytes;
var inp: BytesInput;
// 10000000 01000000 00100000 00010000 - 00001000 00000100 00000010 00000001
var inputValue = Int64.make(
1 << 31 | 1 << 22 | 1 << 13 | 1 << 4,
1 << 27 | 1 << 18 | 1 << 9 | 1
);
// 00000001 00000010 00000100 00001000 - 00010000 00100000 01000000 10000000
var inputValueInverted = Int64.make(
1 << 24 | 1 << 17 | 1 << 10 | 1 << 3,
1 << 28 | 1 << 21 | 1 << 14 | 1 << 7
);
function setup() {
bytes = Bytes.alloc(8);
inp = new BytesInput(bytes);
}
function test_readInt64_littleEndian_correctRead() {
bytes.setInt64(0, inputValue); // setInt64 is little-endian
inp.bigEndian = false;
assertI64Equals(inputValue, inp.readInt64());
}
function test_readInt64_bigEndian_correctRead() {
bytes.setInt64(0, inputValue);
inp.bigEndian = true;
assertI64Equals(inputValueInverted, inp.readInt64());
}
function test_readUint32_isUnsigned() {
bytes.setInt32(0, 1 << 31);
inp.bigEndian = false;
assertI64Equals(Int64.make(0, -2147483648/* -2^31, sign bit doesn't mean anything in low part */) , inp.readUInt32());
}
function test_readUint32_littleEndian_correctRead() {
bytes.setInt32(0, inputValue.high);
inp.bigEndian = false;
assertI64Equals(Int64.make(0, inputValue.high), inp.readUInt32());
}
function test_readUint32_bigEndian_correctRead() {
bytes.setInt32(0, inputValue.high);
inp.bigEndian = true;
assertI64Equals(Int64.make(0, inputValueInverted.low), inp.readUInt32());
}
function assertI64Equals(want: Int64, have: Int64, ?pos: haxe.PosInfos) {
final errorMessage = 'Expected (high: ${want.high}, low: ${want.low}), have (high: ${have.high}, low: ${have.low}).';
Assert.isTrue(want.low == have.low && want.high == have.high, errorMessage, pos);
}
}

View File

@ -0,0 +1,92 @@
package auratests.math;
import utest.Assert;
import aura.math.FFT;
import Utils;
@:depends(auratests.types.TestComplexArray)
class TestFFT extends utest.Test {
function test_bitReverseUint32() {
// Haxe has some issue with signed/unsigned ints here, so we instead
// compare the individual strings as bits. This also makes the output in
// case of assertion failures much nicer to look at.
Assert.equals(Utils.int32ToBytesString(0xFF000000), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x000000FF, 32)));
Assert.equals(Utils.int32ToBytesString(0x00FF0000), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x0000FF00, 32)));
Assert.equals(Utils.int32ToBytesString(0x0000FF00), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x00FF0000, 32)));
Assert.equals(Utils.int32ToBytesString(0x000000FF), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0xFF000000, 32)));
Assert.equals(Utils.int32ToBytesString(0xC0000000), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x00000003, 32)));
Assert.equals(Utils.int32ToBytesString(0x20000000), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x00000004, 32)));
Assert.equals(Utils.int32ToBytesString(0x00FF0000), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x000000FF, 24)));
Assert.equals(Utils.int32ToBytesString(0x0000FF00), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x0000FF00, 24)));
Assert.equals(Utils.int32ToBytesString(0x000000FF), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x00FF0000, 24)));
Assert.equals(Utils.int32ToBytesString(0x0000FF00), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x000000FF, 16)));
Assert.equals(Utils.int32ToBytesString(0x00000003), Utils.int32ToBytesString(@:privateAccess aura.math.FFT.bitReverseUint32(0x00000018, 5)));
}
function test_RealValuedFFT() {
final realFFT = new RealValuedFFT(64, 2, 1);
final inputBuffer = realFFT.getInput(0);
for (i in 0...realFFT.size) {
inputBuffer[i] = Math.sin(i / realFFT.size * 2 * Math.PI * 8);
}
realFFT.forwardFFT(0, 0);
var maxIdx = 0;
var maxVal = realFFT.getOutput(0)[0].real;
for (i in 1...realFFT.size) {
final val = realFFT.getOutput(0)[i].real;
if (val > maxVal) {
maxVal = val;
maxIdx = i;
}
}
Assert.equals(8, maxIdx);
realFFT.inverseFFT(1, 0);
// Assert that ifft(fft(array)) == array
for (i in 0...realFFT.size) {
Assert.floatEquals(realFFT.getInput(0)[i], realFFT.getInput(1)[i]);
}
}
function test_ComplexValuedFFT() {
final cplxFFT = new ComplexValuedFFT(64, 2, 1);
final inputBuffer = cplxFFT.getInput(0);
for (i in 0...cplxFFT.size) {
inputBuffer[i].real = Math.sin(i / cplxFFT.size * 2 * Math.PI * 8);
inputBuffer[i].imag = 0.0;
}
cplxFFT.forwardFFT(0, 0);
// var maxIdx = 0;
// var maxVal = cplxFFT.getOutput(0)[0].real;
// for (i in 1...cplxFFT.size) {
// final val = cplxFFT.getOutput(0)[i].real;
// if (val > maxVal) {
// maxVal = val;
// maxIdx = i;
// }
// }
// Assert.equals(8, maxIdx);
cplxFFT.inverseFFT(1, 0);
// Assert that ifft(fft(array)) == array
for (i in 0...cplxFFT.size) {
Assert.floatEquals(cplxFFT.getInput(0)[i].real, cplxFFT.getInput(1)[i].real);
Assert.floatEquals(cplxFFT.getInput(0)[i].imag, cplxFFT.getInput(1)[i].imag);
}
}
}

View File

@ -0,0 +1,34 @@
package auratests.threading;
import aura.threading.Fifo;
import utest.Assert;
class TestFifo extends utest.Test {
var fifo: Fifo<Int>;
function setup() {
fifo = new Fifo();
}
function test_popFromEmptyFifoReturnsNull() {
Assert.isNull(fifo.tryPop());
}
function test_fifoIsEmptyAfterPoppingLastItem() {
fifo.add(0);
fifo.add(1);
fifo.tryPop();
fifo.tryPop();
Assert.isNull(fifo.tryPop());
}
function test_ItemsArePoppedInOrderTheyAreAdded() {
fifo.add(0);
fifo.add(1);
Assert.equals(0, fifo.tryPop());
Assert.equals(1, fifo.tryPop());
}
}

View File

@ -0,0 +1,323 @@
package auratests.types;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.types.AudioBuffer;
import aura.utils.BufferUtils;
import Utils;
class TestAudioBuffer extends utest.Test {
var buffer: AudioBuffer;
function setup() {
buffer = new AudioBuffer(2, 8);
}
function teardown() {}
function test_numChannels() {
Assert.equals(2, buffer.numChannels);
assertRaisesAssertion(() -> {
new AudioBuffer(0, 8);
});
assertRaisesAssertion(() -> {
new AudioBuffer(-1, 8);
});
}
function test_channelLength() {
Assert.equals(8, buffer.channelLength);
assertRaisesAssertion(() -> {
new AudioBuffer(2, 0);
});
assertRaisesAssertion(() -> {
new AudioBuffer(2, -1);
});
}
function test_AudioBufferChannelView_GetSetWithSameIndexAccessSameValue() {
final view0 = buffer.getChannelView(0);
final view1 = buffer.getChannelView(1);
view0[1] = 8.0;
view1[6] = 4.0;
Assert.floatEquals(8.0, view0[1]);
Assert.floatEquals(4.0, view1[6]);
}
function test_channelViewsDoNotOverlap() {
final view0 = buffer.getChannelView(0);
final view1 = buffer.getChannelView(1);
// Fill views one after each other
for (i in 0...buffer.numChannels) {
view0[i] = 0.0;
}
for (i in 0...buffer.numChannels) {
view1[i] = 1.0;
}
for (i in 0...buffer.numChannels) {
Assert.floatEquals(0.0, view0[i]);
}
}
function test_interleaveToFloat32Array_assertionIfCopyingNegativeAmountOfSamples() {
final array = new Float32Array(4);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, 0, 0, -1);
});
}
function test_interleaveToFloat32Array_assertionIfArrayIsTooSmall() {
final array = new Float32Array(4);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, 0, 0, buffer.channelLength);
});
}
function test_interleaveToFloat32Array_assertionIfSourceOffsetIsNegative() {
final array = new Float32Array(64);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, -1, 0, buffer.channelLength);
});
}
function test_interleaveToFloat32Array_assertionIfTargetOffsetIsNegative() {
final array = new Float32Array(16);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, 0, -1, buffer.channelLength);
});
}
function test_interleaveToFloat32Array_assertionIfSourceOffsetIsTooLarge() {
final array = new Float32Array(64);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, 500, 0, buffer.channelLength);
});
}
function test_interleaveToFloat32Array_assertionIfTargetOffsetIsTooLarge() {
final array = new Float32Array(16);
assertRaisesAssertion(() -> {
buffer.interleaveToFloat32Array(array, 0, 500, buffer.channelLength);
});
}
function test_clear() {
fillBuffer(buffer.getChannelView(0), 1.0);
fillBuffer(buffer.getChannelView(1), -1.0);
buffer.clear();
final zeroArray = new Float32Array(buffer.numChannels * buffer.channelLength);
clearBuffer(zeroArray);
assertEqualsFloat32Array(zeroArray, buffer.rawData);
}
function test_interleaveToFloat32Array_numSamplesToCopy() {
final view0 = buffer.getChannelView(0);
final view1 = buffer.getChannelView(1);
// Fill views one after each other
for (i in 0...buffer.channelLength) {
view0[i] = i;
}
for (i in 0...buffer.channelLength) {
view1[i] = 8 + i;
}
final targetArray = new Float32Array(32);
clearBuffer(targetArray);
final compareArray = new Float32Array(32);
clearBuffer(compareArray);
compareArray[0] = 0;
compareArray[1] = 8;
compareArray[2] = 1;
compareArray[3] = 9;
compareArray[4] = 2;
compareArray[5] = 10;
compareArray[6] = 3;
compareArray[7] = 11;
compareArray[8] = 4;
compareArray[9] = 12;
buffer.interleaveToFloat32Array(targetArray, 0, 0, 5);
assertEqualsFloat32Array(compareArray, targetArray);
}
function test_interleaveToFloat32Array_sourceOffset() {
final view0 = buffer.getChannelView(0);
final view1 = buffer.getChannelView(1);
// Fill views one after each other
for (i in 0...buffer.channelLength) {
view0[i] = i;
}
for (i in 0...buffer.channelLength) {
view1[i] = 8 + i;
}
final targetArray = new Float32Array(32);
clearBuffer(targetArray);
final compareArray = new Float32Array(32);
clearBuffer(compareArray);
compareArray[0] = 2;
compareArray[1] = 10;
compareArray[2] = 3;
compareArray[3] = 11;
compareArray[4] = 4;
compareArray[5] = 12;
compareArray[6] = 5;
compareArray[7] = 13;
compareArray[8] = 6;
compareArray[9] = 14;
compareArray[10] = 7;
compareArray[11] = 15;
buffer.interleaveToFloat32Array(targetArray, 2, 0, 6);
assertEqualsFloat32Array(compareArray, targetArray);
}
function test_interleaveToFloat32Array_targetOffset() {
final view0 = buffer.getChannelView(0);
final view1 = buffer.getChannelView(1);
// Fill views one after each other
for (i in 0...buffer.channelLength) {
view0[i] = i;
}
for (i in 0...buffer.channelLength) {
view1[i] = 8 + i;
}
final targetArray = new Float32Array(32);
clearBuffer(targetArray);
final compareArray = new Float32Array(32);
clearBuffer(compareArray);
compareArray[10] = 0;
compareArray[11] = 8;
compareArray[12] = 1;
compareArray[13] = 9;
compareArray[14] = 2;
compareArray[15] = 10;
compareArray[16] = 3;
compareArray[17] = 11;
compareArray[18] = 4;
compareArray[19] = 12;
compareArray[20] = 5;
compareArray[21] = 13;
compareArray[22] = 6;
compareArray[23] = 14;
compareArray[24] = 7;
compareArray[25] = 15;
buffer.interleaveToFloat32Array(targetArray, 0, 10, 8);
assertEqualsFloat32Array(compareArray, targetArray);
}
function test_test_deinterleaveFromFloat32Array_assertionIfSourceArrayTooSmall() {
final array = new Float32Array(15);
assertRaisesAssertion(() -> {
buffer.deinterleaveFromFloat32Array(array, 2);
});
}
function test_test_deinterleaveFromFloat32Array_assertionIfNumSourceChannelsNegative() {
final array = new Float32Array(16);
assertRaisesAssertion(() -> {
buffer.deinterleaveFromFloat32Array(array, -1);
});
}
function test_test_deinterleaveFromFloat32Array_assertionIfMoreSourceChannelsThanBufferChannels() {
final array = new Float32Array(32);
assertRaisesAssertion(() -> {
buffer.deinterleaveFromFloat32Array(array, 3);
});
}
function test_deinterleaveFromFloat32Array_oneChannelOnly() {
buffer.clear();
final sourceArray = new Float32Array(8);
for (i in 0...sourceArray.length) {
sourceArray[i] = 2 + i;
}
final compareArrayLeft = new Float32Array(8);
compareArrayLeft[0] = 2;
compareArrayLeft[1] = 3;
compareArrayLeft[2] = 4;
compareArrayLeft[3] = 5;
compareArrayLeft[4] = 6;
compareArrayLeft[5] = 7;
compareArrayLeft[6] = 8;
compareArrayLeft[7] = 9;
final compareArrayRight = new Float32Array(8);
clearBuffer(compareArrayRight);
buffer.deinterleaveFromFloat32Array(sourceArray, 1);
assertEqualsFloat32Array(compareArrayLeft, buffer.getChannelView(0));
assertEqualsFloat32Array(compareArrayRight, buffer.getChannelView(1));
}
function test_deinterleaveFromFloat32Array_allChannels() {
buffer.clear();
final sourceArray = new Float32Array(16);
for (i in 0...8) {
sourceArray[2 * i] = 2 + i;
sourceArray[2 * i + 1] = -1 - i;
}
final compareArrayLeft = new Float32Array(8);
compareArrayLeft[0] = 2;
compareArrayLeft[1] = 3;
compareArrayLeft[2] = 4;
compareArrayLeft[3] = 5;
compareArrayLeft[4] = 6;
compareArrayLeft[5] = 7;
compareArrayLeft[6] = 8;
compareArrayLeft[7] = 9;
final compareArrayRight = new Float32Array(8);
compareArrayRight[0] = -1;
compareArrayRight[1] = -2;
compareArrayRight[2] = -3;
compareArrayRight[3] = -4;
compareArrayRight[4] = -5;
compareArrayRight[5] = -6;
compareArrayRight[6] = -7;
compareArrayRight[7] = -8;
buffer.deinterleaveFromFloat32Array(sourceArray, 2);
assertEqualsFloat32Array(compareArrayLeft, buffer.getChannelView(0));
assertEqualsFloat32Array(compareArrayRight, buffer.getChannelView(1));
}
}

View File

@ -0,0 +1,50 @@
package auratests.types;
import utest.Assert;
import aura.types.Complex;
import aura.types.ComplexArray;
class TestComplexArray extends utest.Test {
var array: ComplexArray;
function setup() {
array = new ComplexArray(4);
}
function teardown() {}
function test_length() {
Assert.equals(4, array.length);
}
@:depends(test_length)
function test_isZeroInitialized() {
for (i in 0...array.length) {
var tmp = array[i];
Assert.equals(0.0, tmp.real);
Assert.equals(0.0, tmp.imag);
}
}
function test_getSetBasicFunctionality() {
array[0] = new Complex(3.14, 1.1);
Assert.floatEquals(3.14, array[0].real);
Assert.floatEquals(1.1, array[0].imag);
}
function test_getSetCorrectStride() {
array[0] = new Complex(3.14, 1.1);
array[1] = new Complex(9.9, 1.23);
Assert.floatEquals(9.9, array[1].real);
Assert.floatEquals(1.23, array[1].imag);
Assert.floatEquals(3.14, array[0].real); // Ensure no overrides happen
Assert.floatEquals(1.1, array[0].imag);
}
function test_getSetArrayCopiesValues() {
var tmp = new Complex(3.14, 1.1);
array[0] = tmp;
Assert.isTrue(tmp != array[0]);
}
}

View File

@ -0,0 +1,42 @@
package auratests.utils;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.utils.BufferUtils;
import aura.utils.CircularBuffer;
import Utils;
@:access(aura.utils.CircularBuffer)
class TestCircularBuffer extends utest.Test {
var buffer: CircularBuffer;
function setup() {
buffer = new CircularBuffer(8);
}
function teardown() {}
function test_new_assertThatSizeIsPositiveNumber() {
assertRaisesAssertion(() -> {
new CircularBuffer(0);
});
assertRaisesAssertion(() -> {
new CircularBuffer(-1);
});
}
function test_new_dataInitializedToZero() {
// Please note that this test always succeeds on JS and has additional
// false negatives on other targets, there the test still succeeds if data
// is not actively initialized but the values are still 0
// TODO If Aura has it's own array types at some point in time, implement
// active poisoning of values if unit tests are run
final compareArray = createEmptyF32Array(buffer.length);
assertEqualsFloat32Array(compareArray, buffer.data);
}
}

View File

@ -0,0 +1,110 @@
package auratests.utils;
import kha.simd.Float32x4;
import utest.Assert;
import aura.types.AudioBuffer.AudioBufferChannelView;
import aura.utils.Interpolator.LinearInterpolator;
class TestLinearInterpolator extends utest.Test {
static inline var NUM_SAMPLES = 8;
function test_isInitializedToTargetValue() {
final interp = new LinearInterpolator(0.0);
Assert.floatEquals(0.0, interp.currentValue);
Assert.floatEquals(0.0, interp.lastValue);
Assert.floatEquals(0.0, interp.targetValue);
}
function test_stepSizeIsCorrectForPositiveSteps() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = 4.0;
final stepSize = interp.getLerpStepSize(NUM_SAMPLES);
Assert.floatEquals(0.5, stepSize);
}
function test_stepSizeIsCorrectForNegativeSteps() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = -4.0;
final stepSize = interp.getLerpStepSize(NUM_SAMPLES);
Assert.floatEquals(-0.5, stepSize);
}
function test_stepsReachTargetValue() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = 4.0;
final stepSize = interp.getLerpStepSize(NUM_SAMPLES);
for (_ in 0...NUM_SAMPLES) {
interp.currentValue += stepSize;
}
Assert.floatEquals(interp.targetValue, interp.currentValue);
}
function test_updateLastUpdatesLastAndCurrentValue() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = 4.0;
interp.updateLast();
Assert.floatEquals(interp.targetValue, interp.lastValue);
Assert.floatEquals(interp.targetValue, interp.currentValue);
}
function test_getLerpStepSizes32x4IsCorrectForPositiveSteps() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = 4.0;
final stepSizes = interp.getLerpStepSizes32x4(NUM_SAMPLES);
Assert.floatEquals(0.5, Float32x4.getFast(stepSizes, 0));
Assert.floatEquals(1.0, Float32x4.getFast(stepSizes, 1));
Assert.floatEquals(1.5, Float32x4.getFast(stepSizes, 2));
Assert.floatEquals(2.0, Float32x4.getFast(stepSizes, 3));
}
function test_getLerpStepSizes32x4IsCorrectForNegativeSteps() {
final interp = new LinearInterpolator(0.0);
interp.targetValue = -4.0;
final stepSizes = interp.getLerpStepSizes32x4(NUM_SAMPLES);
Assert.floatEquals(-0.5, Float32x4.getFast(stepSizes, 0));
Assert.floatEquals(-1.0, Float32x4.getFast(stepSizes, 1));
Assert.floatEquals(-1.5, Float32x4.getFast(stepSizes, 2));
Assert.floatEquals(-2.0, Float32x4.getFast(stepSizes, 3));
}
function test_applySIMD32x4() {
final samples = new AudioBufferChannelView(NUM_SAMPLES);
for (i in 0...NUM_SAMPLES) {
samples[i] = 1.0;
}
final interp = new LinearInterpolator(0.0);
interp.targetValue = 4.0;
final stepSizes = interp.getLerpStepSizes32x4(NUM_SAMPLES);
interp.applySIMD32x4(samples, 0, stepSizes);
Assert.floatEquals(0.5, samples[0]);
Assert.floatEquals(1.0, samples[1]);
Assert.floatEquals(1.5, samples[2]);
Assert.floatEquals(2.0, samples[3]);
Assert.floatEquals(2.0, interp.currentValue);
interp.applySIMD32x4(samples, 4, stepSizes);
Assert.floatEquals(2.5, samples[4]);
Assert.floatEquals(3.0, samples[5]);
Assert.floatEquals(3.5, samples[6]);
Assert.floatEquals(4.0, samples[7]);
Assert.floatEquals(4.0, interp.currentValue);
}
}

View File

@ -0,0 +1,26 @@
package auratests.utils;
import utest.Assert;
import aura.utils.MathUtils;
class TestMathUtils extends utest.Test {
function test_maxMin() {
Assert.equals(18, minI(42, 18));
Assert.equals(18, minI(18, 42));
Assert.equals(-99, minI(-99, 42));
Assert.equals(42, maxI(42, 18));
Assert.equals(42, maxI(18, 42));
Assert.equals(42, maxI(-99, 42));
Assert.equals(3.14, minF(3.14, 11.11));
Assert.equals(3.14, minF(11.11, 3.14));
Assert.equals(-28.1, minF(-28.1, 3.07));
Assert.equals(11.11, maxF(3.14, 11.11));
Assert.equals(11.11, maxF(11.11, 3.14));
Assert.equals(3.07, maxF(-28.1, 3.07));
}
}

View File

@ -0,0 +1,82 @@
package auratests.utils;
import utest.Assert;
import kha.arrays.Float32Array;
import aura.utils.Resampler;
import Utils;
class TestResampler extends utest.Test {
final sourceData = new Float32Array(4);
final sourceSampleRate = 100;
function setupClass() {
sourceData[0] = 0.0;
sourceData[1] = 1.0;
sourceData[2] = 2.0;
sourceData[3] = 3.0;
}
function test_getResampleLength() {
Assert.equals(Std.int(sourceData.length / 2), Resampler.getResampleLength(sourceData.length, sourceSampleRate, Std.int(sourceSampleRate / 2)));
Assert.equals(sourceData.length * 2, Resampler.getResampleLength(sourceData.length, sourceSampleRate, sourceSampleRate * 2));
}
function test_sourceSamplePosToTargetPos() {
Assert.floatEquals(1.0, Resampler.sourceSamplePosToTargetPos(2.0, 100, 50));
Assert.floatEquals(4.0, Resampler.sourceSamplePosToTargetPos(2.0, 100, 200));
}
function test_targetSamplePosToSourcePos() {
Assert.floatEquals(4.0, Resampler.targetSamplePosToSourcePos(2.0, 100, 50));
Assert.floatEquals(1.0, Resampler.targetSamplePosToSourcePos(2.0, 100, 200));
}
function test_sampleAtTargetPositionLerp_SamplesExactValuesAtDiscretePositions() {
Assert.floatEquals(0.0, Resampler.sampleAtTargetPositionLerp(sourceData, 0.0, sourceSampleRate, 100));
Assert.floatEquals(1.0, Resampler.sampleAtTargetPositionLerp(sourceData, 1.0, sourceSampleRate, 100));
}
function test_sampleAtTargetPositionLerp_InterpolatesLinearlyBetweenDiscreteSamples() {
Assert.floatEquals(0.5, Resampler.sampleAtTargetPositionLerp(sourceData, 0.5, sourceSampleRate, 100));
Assert.floatEquals(0.3, Resampler.sampleAtTargetPositionLerp(sourceData, 0.3, sourceSampleRate, 100));
}
function test_sampleAtTargetPositionLerp_AssertsSamplePositionNotNegative() {
assertRaisesAssertion(() -> {
Resampler.sampleAtTargetPositionLerp(sourceData, -1.0, sourceSampleRate, 100);
});
}
function test_sampleAtTargetPositionLerp_ClampsValuesOutOfUpperDataBounds() {
Assert.floatEquals(3.0, Resampler.sampleAtTargetPositionLerp(sourceData, 3.5, sourceSampleRate, 100));
Assert.floatEquals(3.0, Resampler.sampleAtTargetPositionLerp(sourceData, 4.0, sourceSampleRate, 100));
}
function test_sampleAtTargetPositionLerp_DifferentSampleRatesUsed() {
Assert.floatEquals(0.0, Resampler.sampleAtTargetPositionLerp(sourceData, 0.0, sourceSampleRate, Std.int(sourceSampleRate / 2)));
Assert.floatEquals(1.0, Resampler.sampleAtTargetPositionLerp(sourceData, 0.5, sourceSampleRate, Std.int(sourceSampleRate / 2)));
Assert.floatEquals(2.0, Resampler.sampleAtTargetPositionLerp(sourceData, 1.0, sourceSampleRate, Std.int(sourceSampleRate / 2)));
Assert.floatEquals(0.0, Resampler.sampleAtTargetPositionLerp(sourceData, 0.0, sourceSampleRate, sourceSampleRate * 2));
Assert.floatEquals(0.25, Resampler.sampleAtTargetPositionLerp(sourceData, 0.5, sourceSampleRate, sourceSampleRate * 2));
Assert.floatEquals(0.5, Resampler.sampleAtTargetPositionLerp(sourceData, 1.0, sourceSampleRate, sourceSampleRate * 2));
}
function test_resampleFloat32Array() {
final targetData = new Float32Array(8);
Resampler.resampleFloat32Array(sourceData, 100, targetData, 200);
Assert.floatEquals(0.0, targetData[0]);
Assert.floatEquals(0.5, targetData[1]);
Assert.floatEquals(1.0, targetData[2]);
Assert.floatEquals(1.5, targetData[3]);
Assert.floatEquals(2.0, targetData[4]);
Assert.floatEquals(2.5, targetData[5]);
Assert.floatEquals(3.0, targetData[6]);
Assert.floatEquals(3.0, targetData[7]); // Don't extrapolate data
}
}

View File

@ -0,0 +1,7 @@
const path = require("path");
const utils = require(path.join(__dirname, "utils.js"));
(async () => {
await utils.install_deps();
})();

71
lib/aura/Tests/khafile.js Normal file
View File

@ -0,0 +1,71 @@
const fs = require("fs");
const path = require("path");
const utils = require(path.join(__dirname, "utils.js"));
const useInstrument = true;
async function run() {
if (!fs.existsSync(".haxelib/")) {
await utils.install_deps();
}
const project = new Project('Aura Tests');
project.addSources(".");
await project.addProject("../");
project.addLibrary("utest");
project.addDefine("UTEST_PRINT_TESTS");
// Easier to match problems with the problem matcher below, enable if
// running headless in command line (this will prevent displaying the html output)
// project.addDefine("UTEST_FAILURE_THROW");
if (useInstrument) {
project.addLibrary("instrument");
project.addDefine("instrument_quiet");
project.addDefine("coverage-console-package-summary-reporter");
// project.addDefine("coverage-console-summary-reporter");
project.addParameter("--macro instrument.Instrumentation.coverage(['aura'], null, ['auratests'])");
// From https://github.com/HaxeFoundation/hxnodejs/blob/master/extraParams.hxml
// to fix sys access error on nodejs even if it should work
// Reference:
// https://github.com/AlexHaxe/haxe-instrument/issues/8
// https://github.com/HaxeFoundation/hxnodejs/issues/59
// https://community.haxe.org/t/using-sys-in-nodejs-target/3702
// project.addParameter("--macro allowPackage('sys')");
// project.addParameter("--macro define('nodejs')");
// project.addParameter("--macro _internal.SuppressDeprecated.run()");
}
if (project.targetOptions.html5.expose === undefined) {
project.targetOptions.html5.expose = "";
}
project.targetOptions.html5.expose += "logToMainProcess: (type, text) => electron.ipcRenderer.send('log-main', type, text),";
project.addParameter("--no-opt");
project.addParameter("--no-inline");
project.addParameter('-dce full');
// project.addParameter("--macro nullSafety('aura', Strict)");
// project.addParameter("--macro nullSafety('aura', StrictThreaded)");
project.addDefine("AURA_UNIT_TESTS");
project.addDefine("AURA_ASSERT_LEVEL=Debug");
project.addCDefine("KINC_NO_WAYLAND"); // Causes errors in the CI
callbacks.postBuild = () => {
fs.copyFileSync("Data/index.html", "build/debug-html5/index.html");
const electronJSAppend = fs.readFileSync("Data/electron-append.js", "utf8");
fs.appendFileSync("build/debug-html5/electron.js", "\n\n" + electronJSAppend);
};
resolve(project);
}
await run();

57
lib/aura/Tests/readme.md Normal file
View File

@ -0,0 +1,57 @@
# Aura Tests
The `/Tests` directory contains a test suite to test some of Aura's functionality.
Because Kha/Kinc currently cannot run in headless mode (see the relevant
[issue](https://github.com/Kode/Kinc/issues/564)) in targets other than node.js
and the node.js target breaks quite often due to Kha updates, the tests currently
run semi-automatically (i.e. they are invoked by the user) on Kha's `debug-html5`
target in Electron. As a consequence, it is currently not possible to run
the tests in a CI pipeline.
## Setup
Running the tests requires node.js which you probably already have installed
since it is required for running Khamake.
If you are using Leenkx, you can instead use the node.js executables included
in the SDK at `<sdk-path>/nodejs`.
## Running the Tests
### Using VSCode/VSCodium
1. Add the `/Tests` directory as a folder to your VSCode workspace
using `File > Add Folder to Workspace`. VSCode unfortunately doesn't search
for task.json files in subdirectories.
2. Press `F1` or `Ctrl + Shift + P` and select `Tasks: Run Test Task`.
> **Note**<br>
> The task automatically picks up the Kha version (and its corresponding
> Electron version) as configured for the Kha extension for VSCode.
### From the Command Line
1. Point the environment variable `KHA_PATH` to the root path of the Kha repository.
2. Point the environment variable `ELECTRON_BIN` to an Electron executable.
3. Run the following on a command line opened in this `/Tests` directory:
```batch
node run.js
```
## Updating Dependencies
The first time the test project is built, all necessary dependencies are
automatically installed. If you want to update them to the newest version,
simply run the following on a command line opened in this `/Tests` directory:
```batch
node install_deps.js
```
## Defines
While the tests are run, the define `AURA_UNIT_TESTS` is set and the assertion
level is set to `AURA_ASSERT_LEVEL=Debug`.

30
lib/aura/Tests/run.js Normal file
View File

@ -0,0 +1,30 @@
const path = require("path");
const utils = require(path.join(__dirname, "utils.js"));
async function run() {
const khaPath = utils.getEnvVarSafe("KHA_PATH");
const electron_bin = utils.getEnvVarSafe("ELECTRON_BIN");
khamake_args = [
path.join(khaPath, "make"),
"debug-html5",
"--debug"
]
electron_args = [
"--no-sandbox",
"--force-device-scale-factor=1",
// "--enable-logging",
// "--trace-warnings",
"--force_low_power_gpu",
"build/debug-html5/electron.js"
]
await utils.spawnCommand("node", khamake_args, true);
await utils.spawnCommand(electron_bin, electron_args, true);
}
(async () => {
await run();
})();

58
lib/aura/Tests/utils.js Normal file
View File

@ -0,0 +1,58 @@
const child_process = require("child_process");
async function spawnCommand(command, args, exitOnErr) {
exitOnErr = exitOnErr === undefined ? true : exitOnErr;
const proc = child_process.spawn(command, args);
proc.stdout.on("data", function (data) {
console.log(data.toString().trim());
});
proc.stderr.on("data", function (data) {
console.error(data.toString().trim());
});
return new Promise(function(resolve, reject) {
proc.on("close", function (code) {
if (code == 0) {
console.log(`Child process exited with code ${code}`);
resolve();
} else {
console.error(`Child process failed with code ${code}`);
if (exitOnErr) {
process.exit(code);
}
reject();
}
});
});
}
function getEnvVarSafe(varname) {
const value = process.env[varname];
if (value === undefined) {
exitWithError(`Environment variable '${varname}' not set!`)
}
return value;
}
function exitWithError(message, exitCode) {
exitCode = exitCode === undefined ? 1 : exitCode;
console.error("[Error] " + message);
process.exit(exitCode);
}
async function install_deps() {
console.log("Downloading haxelib dependencies...");
await spawnCommand("haxelib", ["newrepo"]);
await spawnCommand("haxelib", ["install", "TestDeps.hxml", "--always"]);
}
module.exports = {
spawnCommand: spawnCommand,
getEnvVarSafe: getEnvVarSafe,
exitWithError: exitWithError,
install_deps: install_deps,
}