mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
feat(audio): implement sceAudioOutOutputs (#605)
* feat(audio): implement batched output submission * test(audio): cover batched output semantics * test(audio): cover multi-port output batches --------- Co-authored-by: diego <diego@DIGOTE-PC>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -11,8 +12,17 @@ namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOutExports
|
||||
{
|
||||
private const int AudioOutOutputParamSize = 16;
|
||||
private const int AudioOutMaximumOutputCount = 25;
|
||||
|
||||
internal const int AudioOutErrorInvalidPort = unchecked((int)0x80260003);
|
||||
internal const int AudioOutErrorInvalidPointer = unchecked((int)0x80260004);
|
||||
internal const int AudioOutErrorPortFull = unchecked((int)0x80260005);
|
||||
internal const int AudioOutErrorInvalidSize = unchecked((int)0x80260006);
|
||||
|
||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||
private static int _nextPortHandle;
|
||||
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||
|
||||
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
||||
// guest submits real samples or silence. Gated so it costs nothing when off.
|
||||
@@ -56,6 +66,7 @@ public static class AudioOutExports
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public IHostAudioStream? Backend { get; }
|
||||
public object SubmissionGate { get; } = new();
|
||||
public volatile float Volume = 1.0f;
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
@@ -83,7 +94,24 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Backend?.Dispose();
|
||||
public void Dispose()
|
||||
{
|
||||
lock (SubmissionGate)
|
||||
{
|
||||
Backend?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct OutputDescriptor(int Handle, ulong SourceAddress);
|
||||
|
||||
private struct ResolvedOutput
|
||||
{
|
||||
public int Handle;
|
||||
public ulong SourceAddress;
|
||||
public PortState Port;
|
||||
public byte[]? HostBuffer;
|
||||
public int HostBufferLength;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -115,9 +143,18 @@ public static class AudioOutExports
|
||||
string backendName;
|
||||
try
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||
backendName = audio.BackendName;
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
backend = streamFactory(frequency);
|
||||
backendName = "test";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||
backendName = audio.BackendName;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -192,6 +229,46 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w3PdaSTSwGE",
|
||||
ExportName = "sceAudioOutOutputs",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutOutputs(CpuContext ctx)
|
||||
{
|
||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||
var outputCount = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (outputCount == 0 || outputCount > AudioOutMaximumOutputCount)
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorPortFull);
|
||||
}
|
||||
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||
}
|
||||
|
||||
var count = checked((int)outputCount);
|
||||
Span<byte> parameterBytes =
|
||||
stackalloc byte[AudioOutMaximumOutputCount * AudioOutOutputParamSize];
|
||||
parameterBytes = parameterBytes[..checked(count * AudioOutOutputParamSize)];
|
||||
if (!ctx.Memory.TryRead(parameterAddress, parameterBytes))
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||
}
|
||||
|
||||
Span<OutputDescriptor> descriptors = stackalloc OutputDescriptor[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var entry = parameterBytes.Slice(i * AudioOutOutputParamSize, AudioOutOutputParamSize);
|
||||
descriptors[i] = new OutputDescriptor(
|
||||
BinaryPrimitives.ReadInt32LittleEndian(entry),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(entry[8..]));
|
||||
}
|
||||
|
||||
return ctx.SetReturn(SubmitOutputs(ctx, descriptors));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QOQtbeDqsT4",
|
||||
ExportName = "sceAudioOutOutput",
|
||||
@@ -225,16 +302,7 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (_traceOutput)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _outputCount);
|
||||
if (n <= 8 || n % 200 == 0)
|
||||
{
|
||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
||||
}
|
||||
}
|
||||
TraceOutput(handle, port, source);
|
||||
|
||||
if (port.Backend is null)
|
||||
{
|
||||
@@ -272,6 +340,184 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SubmitOutputs(CpuContext ctx, ReadOnlySpan<OutputDescriptor> descriptors)
|
||||
{
|
||||
var resolvedArray = ArrayPool<ResolvedOutput>.Shared.Rent(descriptors.Length);
|
||||
var resolved = resolvedArray.AsSpan(0, descriptors.Length);
|
||||
resolved.Clear();
|
||||
|
||||
Span<int> lockOrder = stackalloc int[descriptors.Length];
|
||||
var acquiredLocks = 0;
|
||||
try
|
||||
{
|
||||
uint bufferLength = 0;
|
||||
for (var i = 0; i < descriptors.Length; i++)
|
||||
{
|
||||
var descriptor = descriptors[i];
|
||||
for (var previous = 0; previous < i; previous++)
|
||||
{
|
||||
if (resolved[previous].Handle == descriptor.Handle)
|
||||
{
|
||||
return AudioOutErrorInvalidPort;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Ports.TryGetValue(descriptor.Handle, out var port))
|
||||
{
|
||||
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
bufferLength = port.BufferLength;
|
||||
}
|
||||
else if (port.BufferLength != bufferLength)
|
||||
{
|
||||
return AudioOutErrorInvalidSize;
|
||||
}
|
||||
|
||||
resolved[i].Handle = descriptor.Handle;
|
||||
resolved[i].SourceAddress = descriptor.SourceAddress;
|
||||
resolved[i].Port = port;
|
||||
lockOrder[i] = i;
|
||||
}
|
||||
|
||||
// Every batch takes port locks in handle order. Two guest threads can
|
||||
// submit overlapping batches in a different descriptor order without
|
||||
// deadlocking each other.
|
||||
for (var i = 1; i < lockOrder.Length; i++)
|
||||
{
|
||||
var index = lockOrder[i];
|
||||
var position = i;
|
||||
while (position > 0 &&
|
||||
resolved[lockOrder[position - 1]].Handle > resolved[index].Handle)
|
||||
{
|
||||
lockOrder[position] = lockOrder[position - 1];
|
||||
position--;
|
||||
}
|
||||
|
||||
lockOrder[position] = index;
|
||||
}
|
||||
|
||||
for (; acquiredLocks < lockOrder.Length; acquiredLocks++)
|
||||
{
|
||||
Monitor.Enter(resolved[lockOrder[acquiredLocks]].Port.SubmissionGate);
|
||||
}
|
||||
|
||||
// AudioOutClose removes the handle before waiting for SubmissionGate.
|
||||
// Recheck after acquiring all gates so a close racing this batch cannot
|
||||
// turn a validated submission into a write to a disposed backend.
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
if (!Ports.TryGetValue(resolved[i].Handle, out var current) ||
|
||||
!ReferenceEquals(current, resolved[i].Port))
|
||||
{
|
||||
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||
}
|
||||
}
|
||||
|
||||
// Stage every guest buffer before the first host submission. A bad
|
||||
// pointer in a later descriptor therefore cannot partially enqueue the
|
||||
// earlier ports.
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
ref var output = ref resolved[i];
|
||||
if (output.SourceAddress == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceBuffer = ArrayPool<byte>.Shared.Rent(output.Port.BufferByteLength);
|
||||
try
|
||||
{
|
||||
var source = sourceBuffer.AsSpan(0, output.Port.BufferByteLength);
|
||||
if (!ctx.Memory.TryRead(output.SourceAddress, source))
|
||||
{
|
||||
return AudioOutErrorInvalidPointer;
|
||||
}
|
||||
|
||||
TraceOutput(output.Handle, output.Port, source);
|
||||
|
||||
output.HostBufferLength = checked(
|
||||
(int)output.Port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||
output.HostBuffer = ArrayPool<byte>.Shared.Rent(output.HostBufferLength);
|
||||
AudioPcmConversion.ConvertToStereoPcm16(
|
||||
source,
|
||||
output.HostBuffer.AsSpan(0, output.HostBufferLength),
|
||||
checked((int)output.Port.BufferLength),
|
||||
output.Port.Channels,
|
||||
output.Port.BytesPerSample,
|
||||
output.Port.IsFloat,
|
||||
output.Port.Volume);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(sourceBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
PortState? pacingPort = null;
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
ref var output = ref resolved[i];
|
||||
if (output.HostBuffer is null ||
|
||||
output.Port.Backend is null ||
|
||||
!output.Port.Backend.Submit(
|
||||
output.HostBuffer.AsSpan(0, output.HostBufferLength)))
|
||||
{
|
||||
if (pacingPort is null ||
|
||||
HasLongerBufferDuration(output.Port, pacingPort))
|
||||
{
|
||||
pacingPort = output.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A batch is one guest scheduling point. When one or more ports have
|
||||
// no usable backend, pace once using the longest affected buffer rather
|
||||
// than sleeping once per port.
|
||||
pacingPort?.PaceSilence();
|
||||
return checked((int)resolved[0].Port.BufferLength);
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (var i = acquiredLocks - 1; i >= 0; i--)
|
||||
{
|
||||
Monitor.Exit(resolved[lockOrder[i]].Port.SubmissionGate);
|
||||
}
|
||||
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
if (resolved[i].HostBuffer is { } hostBuffer)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(hostBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPool<ResolvedOutput>.Shared.Return(resolvedArray, clearArray: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasLongerBufferDuration(PortState candidate, PortState current) =>
|
||||
(ulong)candidate.BufferLength * current.Frequency >
|
||||
(ulong)current.BufferLength * candidate.Frequency;
|
||||
|
||||
private static void TraceOutput(int handle, PortState port, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!_traceOutput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var n = Interlocked.Increment(ref _outputCount);
|
||||
if (n <= 8 || n % 200 == 0)
|
||||
{
|
||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b+uAV89IlxE",
|
||||
ExportName = "sceAudioOutSetVolume",
|
||||
@@ -362,6 +608,25 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
foreach (var handle in Ports.Keys)
|
||||
{
|
||||
if (Ports.TryRemove(handle, out var port))
|
||||
{
|
||||
port.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_nextPortHandle = 0;
|
||||
_outputCount = 0;
|
||||
Volatile.Write(ref _shutdown, false);
|
||||
Volatile.Write(ref _streamFactoryForTests, null);
|
||||
}
|
||||
|
||||
private static bool _shutdown;
|
||||
|
||||
private static bool TryGetFormat(
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
[CollectionDefinition("AudioOutState", DisableParallelization = true)]
|
||||
public sealed class AudioOutStateCollection
|
||||
{
|
||||
public const string Name = "AudioOutState";
|
||||
}
|
||||
|
||||
[Collection(AudioOutStateCollection.Name)]
|
||||
public sealed class AudioOutExportsTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const int MemorySize = 0x4000;
|
||||
private const ulong ParameterAddress = MemoryBase + 0x100;
|
||||
private const ulong FirstSourceAddress = MemoryBase + 0x1000;
|
||||
private const ulong SecondSourceAddress = MemoryBase + 0x2000;
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(MemoryBase, MemorySize);
|
||||
private readonly CpuContext _ctx;
|
||||
private readonly List<RecordingAudioStream> _streams = [];
|
||||
|
||||
public AudioOutExportsTests()
|
||||
{
|
||||
AudioOutExports.ResetForTests();
|
||||
AudioOutExports.SetStreamFactoryForTests(_ =>
|
||||
{
|
||||
var stream = new RecordingAudioStream();
|
||||
_streams.Add(stream);
|
||||
return stream;
|
||||
});
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_StagesAndSubmitsSingleStereoPort()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
Span<byte> source = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source, -32768);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[2..], 32767);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[4..], -1234);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[6..], 5678);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, source));
|
||||
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 1);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(2UL, _ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(source.ToArray(), Assert.Single(_streams[0].Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_SubmitsEveryPortInTheBatch()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 2);
|
||||
byte[] firstSource = [1, 0, 2, 0, 3, 0, 4, 0];
|
||||
byte[] secondSource = [5, 0, 6, 0, 7, 0, 8, 0];
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, firstSource));
|
||||
Assert.True(_memory.TryWrite(SecondSourceAddress, secondSource));
|
||||
|
||||
// Reverse handle order to exercise canonical lock ordering without
|
||||
// changing which guest buffer belongs to each port.
|
||||
WriteDescriptor(0, secondHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, firstHandle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(secondSource, Assert.Single(_streams[0].Submissions));
|
||||
Assert.Equal(firstSource, Assert.Single(_streams[1].Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_AcceptsNullBufferAsSynchronizationOnly()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
WriteDescriptor(0, handle, sourceAddress: 0);
|
||||
|
||||
var result = Submit(outputCount: 1);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Empty(_streams[0].Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_FaultInLaterBufferDoesNotPartiallySubmit()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 2);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, secondHandle, MemoryBase + MemorySize);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPointer, result);
|
||||
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsDuplicateHandlesBeforeSubmission()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||
Assert.True(_memory.TryWrite(SecondSourceAddress, new byte[8]));
|
||||
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||
WriteDescriptor(1, handle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPort, result);
|
||||
Assert.Empty(_streams[0].Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsPortsWithDifferentBufferLengths()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 4);
|
||||
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, secondHandle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidSize, result);
|
||||
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsUnknownHandle()
|
||||
{
|
||||
WriteDescriptor(0, 99, FirstSourceAddress);
|
||||
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPort,
|
||||
Submit(outputCount: 1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u)]
|
||||
[InlineData(26u)]
|
||||
public void Outputs_RejectsInvalidOutputCount(uint outputCount)
|
||||
{
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorPortFull,
|
||||
Submit(outputCount));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsNullOrUnreadableParameterArray()
|
||||
{
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||
Submit(outputCount: 1, parameterAddress: 0));
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||
Submit(outputCount: 1, parameterAddress: MemoryBase + MemorySize));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputsExportRegistersForBothGenerations()
|
||||
{
|
||||
foreach (var generation in new[] { Generation.Gen4, Generation.Gen5 })
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation));
|
||||
|
||||
Assert.True(manager.TryGetExport("w3PdaSTSwGE", out var export));
|
||||
Assert.Equal("sceAudioOutOutputs", export.Name);
|
||||
Assert.Equal("libSceAudioOut", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => AudioOutExports.ResetForTests();
|
||||
|
||||
private int OpenPort(uint bufferLength)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 1;
|
||||
_ctx[CpuRegister.Rsi] = 0;
|
||||
_ctx[CpuRegister.Rdx] = 0;
|
||||
_ctx[CpuRegister.Rcx] = bufferLength;
|
||||
_ctx[CpuRegister.R8] = 48000;
|
||||
_ctx[CpuRegister.R9] = 1;
|
||||
return AudioOutExports.AudioOutOpen(_ctx);
|
||||
}
|
||||
|
||||
private int Submit(uint outputCount, ulong parameterAddress = ParameterAddress)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = parameterAddress;
|
||||
_ctx[CpuRegister.Rsi] = outputCount;
|
||||
return AudioOutExports.AudioOutOutputs(_ctx);
|
||||
}
|
||||
|
||||
private void WriteDescriptor(int index, int handle, ulong sourceAddress)
|
||||
{
|
||||
Span<byte> descriptor = stackalloc byte[16];
|
||||
descriptor.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(descriptor, handle);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(descriptor[8..], sourceAddress);
|
||||
Assert.True(_memory.TryWrite(
|
||||
ParameterAddress + unchecked((ulong)(index * descriptor.Length)),
|
||||
descriptor));
|
||||
}
|
||||
|
||||
private sealed class RecordingAudioStream : IHostAudioStream
|
||||
{
|
||||
public List<byte[]> Submissions { get; } = [];
|
||||
|
||||
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||
{
|
||||
Submissions.Add(stereoPcm16.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user