159 lines
6.3 KiB
C#
159 lines
6.3 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Audio;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Audio;
|
|
|
|
public sealed class AudioOut2MixTests
|
|
{
|
|
private const ulong MemoryBase = 0x1_0000_0000;
|
|
private const ushort ObjectMain = 0x100;
|
|
private const float HalfPower = 0.70710677f;
|
|
|
|
[Theory]
|
|
[InlineData((ushort)0, true)]
|
|
[InlineData((ushort)1, true)]
|
|
[InlineData((ushort)2, true)]
|
|
[InlineData(ObjectMain, true)]
|
|
[InlineData((ushort)3, false)]
|
|
[InlineData((ushort)4, false)]
|
|
[InlineData((ushort)5, false)]
|
|
[InlineData((ushort)6, false)]
|
|
public void OnlyMainBgmAndVoicePortsReachTheSpeakers(ushort portType, bool expected)
|
|
{
|
|
Assert.Equal(expected, AudioOut2Exports.RoutesToSpeakers(portType));
|
|
Assert.Equal(expected, AudioOut2Exports.TryGetStereoGains(portType, 2, 1f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(64u, HalfPower, HalfPower)]
|
|
[InlineData(65u, HalfPower, -HalfPower)]
|
|
[InlineData(0u, 1f, 1f)]
|
|
[InlineData(2u, HalfPower, -HalfPower)]
|
|
public void FirstOrderAmbisonicsDecodeToStereoCardioids(uint ambisonics, float left, float right)
|
|
{
|
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0.5f, ambisonics, 0, out var leftGain, out var rightGain));
|
|
Assert.Equal(left * 0.5f, leftGain, 5);
|
|
Assert.Equal(right * 0.5f, rightGain, 5);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(66u)]
|
|
[InlineData(67u)]
|
|
[InlineData(99u)]
|
|
[InlineData(1u)]
|
|
[InlineData(3u)]
|
|
[InlineData(15u)]
|
|
public void HigherAmbisonicComponentsDoNotFeedStereo(uint ambisonics)
|
|
{
|
|
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, ambisonics, 0, out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void PlainObjectsPlayCentredOrPassThroughToOneSide()
|
|
{
|
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, 0, out var left, out var right));
|
|
Assert.Equal(HalfPower, left, 5);
|
|
Assert.Equal(HalfPower, right, 5);
|
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughLeft, out left, out right));
|
|
Assert.Equal((1f, 0f), (left, right));
|
|
Assert.True(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsNone, AudioOut2Exports.PassthroughRight, out left, out right));
|
|
Assert.Equal((0f, 1f), (left, right));
|
|
Assert.False(AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 0f, AudioOut2Exports.AmbisonicsNone, 0, out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void FifthOrderSceneDecodesASourceToItsSide()
|
|
{
|
|
const int frames = 4;
|
|
var leftSource = DecodeScene(frames, azimuthSine: 1f);
|
|
var centreSource = DecodeScene(frames, azimuthSine: 0f);
|
|
|
|
Assert.Equal(2f * HalfPower, leftSource[0], 4);
|
|
Assert.Equal(0f, leftSource[1], 4);
|
|
Assert.Equal(HalfPower, centreSource[0], 4);
|
|
Assert.Equal(HalfPower, centreSource[1], 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetAttributesStoresGainAmbisonicsAndPassthrough()
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
var param = new byte[0x40];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(param, ObjectMain);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param.AsSpan(4), 0x100);
|
|
Assert.True(memory.TryWrite(MemoryBase, param));
|
|
ctx[CpuRegister.Rdi] = 0;
|
|
ctx[CpuRegister.Rsi] = MemoryBase;
|
|
ctx[CpuRegister.Rdx] = MemoryBase + 0x80;
|
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortCreate(ctx));
|
|
var handle = BinaryPrimitives.ReadUInt64LittleEndian(Read(memory, MemoryBase + 0x80, 8));
|
|
|
|
var values = new byte[12];
|
|
BinaryPrimitives.WriteSingleLittleEndian(values, 0.25f);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(4), 65);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(values.AsSpan(8), AudioOut2Exports.PassthroughLeft);
|
|
Assert.True(memory.TryWrite(MemoryBase + 0x100, values));
|
|
var attributes = new byte[0x18 * 3];
|
|
WriteAttribute(attributes, 0, 1, MemoryBase + 0x100);
|
|
WriteAttribute(attributes, 1, 8, MemoryBase + 0x104);
|
|
WriteAttribute(attributes, 2, 5, MemoryBase + 0x108);
|
|
Assert.True(memory.TryWrite(MemoryBase + 0x200, attributes));
|
|
ctx[CpuRegister.Rdi] = handle;
|
|
ctx[CpuRegister.Rsi] = MemoryBase + 0x200;
|
|
ctx[CpuRegister.Rdx] = 3;
|
|
|
|
Assert.Equal(0, AudioOut2Exports.AudioOut2PortSetAttributes(ctx));
|
|
|
|
Assert.Equal((0.25f, 65u, AudioOut2Exports.PassthroughLeft), AudioOut2Exports.PortMixAttributes(handle));
|
|
ctx[CpuRegister.Rdi] = handle;
|
|
AudioOut2Exports.AudioOut2PortDestroy(ctx);
|
|
}
|
|
|
|
private static float[] DecodeScene(int frames, float azimuthSine)
|
|
{
|
|
var mix = new float[frames * 2];
|
|
var source = new byte[frames * sizeof(float)];
|
|
for (var acn = 0u; acn < 36; acn++)
|
|
{
|
|
var component = acn switch
|
|
{
|
|
0 => 1f,
|
|
1 => azimuthSine,
|
|
_ => 0.6f,
|
|
};
|
|
for (var frame = 0; frame < frames; frame++)
|
|
{
|
|
BinaryPrimitives.WriteSingleLittleEndian(source.AsSpan(frame * sizeof(float)), component);
|
|
}
|
|
|
|
if (AudioOut2Exports.TryGetStereoGains(ObjectMain, 1, 1f, AudioOut2Exports.AmbisonicsAcnBase + acn, 0, out var left, out var right))
|
|
{
|
|
AudioOut2Exports.MixPortIntoStereo(source, mix, frames, 1, sizeof(float), true, left, right);
|
|
}
|
|
}
|
|
|
|
return mix;
|
|
}
|
|
|
|
private static void WriteAttribute(byte[] attributes, int index, uint id, ulong value)
|
|
{
|
|
var entry = attributes.AsSpan(index * 0x18, 0x18);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(entry, id);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x08..], value);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(entry[0x10..], sizeof(uint));
|
|
}
|
|
|
|
private static byte[] Read(FakeCpuMemory memory, ulong address, int length)
|
|
{
|
|
var bytes = new byte[length];
|
|
Assert.True(memory.TryRead(address, bytes));
|
|
return bytes;
|
|
}
|
|
}
|