mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 23:19:44 +08:00
[audio] added sdl audio backend and in-tree atrac9 decoder
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much guest audio the host device has actually played, in seconds.
|
||||||
|
///
|
||||||
|
/// This is the only clock in the emulator that advances at the rate the player
|
||||||
|
/// hears. Wall clock runs ahead of it whenever the guest cannot feed the device
|
||||||
|
/// (the stream underruns and the missing time is never played), so anything
|
||||||
|
/// that has to stay in step with the guest's audio — host-decoded video being
|
||||||
|
/// the case that matters — has to follow this rather than <see cref="Stopwatch"/>.
|
||||||
|
///
|
||||||
|
/// Reported per stream and kept as the furthest-along value: the guest's ports
|
||||||
|
/// all carry one mix, and the leading port is the one whose position the
|
||||||
|
/// listener perceives.
|
||||||
|
/// </summary>
|
||||||
|
public static class GuestAudioClock
|
||||||
|
{
|
||||||
|
private static long _playedMicroseconds;
|
||||||
|
private static long _lastAdvanceTimestamp;
|
||||||
|
|
||||||
|
/// <summary>Seconds of guest audio the device has played. Monotonic.</summary>
|
||||||
|
public static double PlayedSeconds =>
|
||||||
|
Interlocked.Read(ref _playedMicroseconds) / 1_000_000.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True while a stream has reported progress recently. False means no guest
|
||||||
|
/// audio is playing, and callers must fall back to wall clock rather than
|
||||||
|
/// stalling on a clock that will never advance.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsRunning
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var last = Interlocked.Read(ref _lastAdvanceTimestamp);
|
||||||
|
return last != 0 &&
|
||||||
|
Stopwatch.GetElapsedTime(last) < TimeSpan.FromMilliseconds(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Report(double playedSeconds)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(playedSeconds) || playedSeconds < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var microseconds = (long)(playedSeconds * 1_000_000.0);
|
||||||
|
var current = Interlocked.Read(ref _playedMicroseconds);
|
||||||
|
while (microseconds > current)
|
||||||
|
{
|
||||||
|
var seen = Interlocked.CompareExchange(
|
||||||
|
ref _playedMicroseconds,
|
||||||
|
microseconds,
|
||||||
|
current);
|
||||||
|
if (seen == current)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _lastAdvanceTimestamp, Stopwatch.GetTimestamp());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = seen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,4 +15,17 @@ public interface IHostAudioStream : IDisposable
|
|||||||
/// audio, in which case the caller paces the guest itself.
|
/// audio, in which case the caller paces the guest itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool Submit(ReadOnlySpan<byte> stereoPcm16);
|
bool Submit(ReadOnlySpan<byte> stereoPcm16);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audio already handed to the device and not yet played, in milliseconds —
|
||||||
|
/// the cushion protecting playback from a late submission. Zero means the
|
||||||
|
/// device has run dry and is emitting silence.
|
||||||
|
///
|
||||||
|
/// Callers that pace the guest against an emulated hardware queue need this:
|
||||||
|
/// pacing purely on wall clock releases exactly one buffer per buffer-period
|
||||||
|
/// and so keeps the cushion at zero, which turns any scheduling jitter into
|
||||||
|
/// an audible dropout. Returns -1 when the backend cannot report a depth, in
|
||||||
|
/// which case callers must fall back to their own pacing.
|
||||||
|
/// </summary>
|
||||||
|
int QueuedMilliseconds => -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional host-audio extension for backends that can accept the guest's
|
||||||
|
/// interleaved PCM layout directly and perform device conversion themselves.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostPcmAudioOutput : IHostAudioOutput
|
||||||
|
{
|
||||||
|
IHostAudioStream OpenPcmStream(uint sampleRate, int channels, HostPcmFormat format);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HostPcmFormat
|
||||||
|
{
|
||||||
|
Signed16,
|
||||||
|
Float32,
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SDL;
|
||||||
|
using static SDL.SDL3;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE.Host.Sdl;
|
||||||
|
|
||||||
|
internal sealed unsafe class SdlHostAudio : IHostPcmAudioOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cap for streams this class paces itself (AudioOut). Blocking the guest
|
||||||
|
/// here is that path's only pacing, so the device settles at this depth —
|
||||||
|
/// it is the playback latency, and the floor under it is how much jitter the
|
||||||
|
/// stream can absorb before it runs dry.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly int TargetQueuedMilliseconds =
|
||||||
|
int.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_LATENCY_MS"),
|
||||||
|
out var latencyMs) && latencyMs > 0
|
||||||
|
? latencyMs
|
||||||
|
: 60;
|
||||||
|
|
||||||
|
private const int MaximumWaitMilliseconds = 250;
|
||||||
|
private static readonly object InitGate = new();
|
||||||
|
private static bool _initialized;
|
||||||
|
|
||||||
|
public string BackendName => "sdl3";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stereo PCM16 stream with a caller-chosen backpressure cap. Callers that
|
||||||
|
/// pace the guest themselves pass a deeper cap so this class's backpressure
|
||||||
|
/// does not fight their pacing.
|
||||||
|
/// </summary>
|
||||||
|
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate, int maxQueuedPcmBytes = 32 * 1024)
|
||||||
|
=> OpenStream(
|
||||||
|
sampleRate,
|
||||||
|
channels: 2,
|
||||||
|
HostPcmFormat.Signed16,
|
||||||
|
maxQueuedPcmBytes > 0 ? maxQueuedPcmBytes : 32 * 1024);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guest-format stream for AudioOut, which has no queue model of its own:
|
||||||
|
/// blocking here is that path's only pacing, so the device settles at
|
||||||
|
/// TargetQueuedMilliseconds and that depth is the playback latency.
|
||||||
|
/// </summary>
|
||||||
|
public IHostAudioStream OpenPcmStream(uint sampleRate, int channels, HostPcmFormat format)
|
||||||
|
{
|
||||||
|
var bytesPerSample = format == HostPcmFormat.Float32 ? sizeof(float) : sizeof(short);
|
||||||
|
var cap = checked((int)((long)sampleRate * channels * bytesPerSample *
|
||||||
|
TargetQueuedMilliseconds / 1_000));
|
||||||
|
return OpenStream(sampleRate, channels, format, cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHostAudioStream OpenStream(
|
||||||
|
uint sampleRate,
|
||||||
|
int channels,
|
||||||
|
HostPcmFormat format,
|
||||||
|
int maximumQueuedBytes)
|
||||||
|
{
|
||||||
|
if (sampleRate is < 8_000 or > 384_000 || channels is < 1 or > 8)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
sampleRate is < 8_000 or > 384_000 ? nameof(sampleRate) : nameof(channels));
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureInitialized();
|
||||||
|
return new AudioStream(sampleRate, channels, format, maximumQueuedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
lock (InitGate)
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((SDL_WasInit(SDL_InitFlags.SDL_INIT_AUDIO) & SDL_InitFlags.SDL_INIT_AUDIO) == 0 &&
|
||||||
|
!SDL_InitSubSystem(SDL_InitFlags.SDL_INIT_AUDIO))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SDL audio initialization failed: {GetError()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetError()
|
||||||
|
{
|
||||||
|
var error = Unsafe_SDL_GetError();
|
||||||
|
return error is null ? "unknown SDL error" : Marshal.PtrToStringUTF8((nint)error) ?? "unknown SDL error";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly bool _traceQueue = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_QUEUE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static int _nextStreamId;
|
||||||
|
|
||||||
|
private sealed class AudioStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly int _maximumQueuedBytes;
|
||||||
|
private readonly int _bytesPerFrame;
|
||||||
|
private readonly uint _sampleRate;
|
||||||
|
private readonly int _streamId = Interlocked.Increment(ref _nextStreamId);
|
||||||
|
private SDL_AudioStream* _stream;
|
||||||
|
private bool _disposed;
|
||||||
|
private long _totalSubmittedBytes;
|
||||||
|
|
||||||
|
// Queue diagnostics for the current report window.
|
||||||
|
private long _windowStart = Stopwatch.GetTimestamp();
|
||||||
|
private long _submissions;
|
||||||
|
private long _submittedBytes;
|
||||||
|
private long _blockedTicks;
|
||||||
|
private long _drops;
|
||||||
|
private long _emptyObservations;
|
||||||
|
private int _minQueuedBytes = int.MaxValue;
|
||||||
|
private int _maxQueuedBytes;
|
||||||
|
private long _queuedByteSum;
|
||||||
|
|
||||||
|
public AudioStream(
|
||||||
|
uint sampleRate,
|
||||||
|
int channels,
|
||||||
|
HostPcmFormat format,
|
||||||
|
int maximumQueuedBytes)
|
||||||
|
{
|
||||||
|
var bytesPerSample = format == HostPcmFormat.Float32 ? sizeof(float) : sizeof(short);
|
||||||
|
_bytesPerFrame = channels * bytesPerSample;
|
||||||
|
_sampleRate = sampleRate;
|
||||||
|
var spec = new SDL_AudioSpec
|
||||||
|
{
|
||||||
|
format = format == HostPcmFormat.Float32
|
||||||
|
? SDL_AudioFormat.SDL_AUDIO_F32LE
|
||||||
|
: SDL_AudioFormat.SDL_AUDIO_S16LE,
|
||||||
|
channels = checked((byte)channels),
|
||||||
|
freq = checked((int)sampleRate),
|
||||||
|
};
|
||||||
|
|
||||||
|
_stream = SDL_OpenAudioDeviceStream(
|
||||||
|
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||||
|
&spec,
|
||||||
|
null,
|
||||||
|
IntPtr.Zero);
|
||||||
|
if (_stream is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SDL audio stream creation failed: {GetError()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SDL_ResumeAudioStreamDevice(_stream))
|
||||||
|
{
|
||||||
|
SDL_DestroyAudioStream(_stream);
|
||||||
|
_stream = null;
|
||||||
|
throw new InvalidOperationException($"SDL audio stream start failed: {GetError()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_maximumQueuedBytes = maximumQueuedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int QueuedMilliseconds
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _stream is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytesPerSecond = (double)_bytesPerFrame * _sampleRate;
|
||||||
|
return bytesPerSecond <= 0
|
||||||
|
? -1
|
||||||
|
: (int)(SDL_GetAudioStreamQueued(_stream) / bytesPerSecond * 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> pcm)
|
||||||
|
{
|
||||||
|
if (pcm.IsEmpty)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed || _stream is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockStart = Stopwatch.GetTimestamp();
|
||||||
|
var deadline = blockStart +
|
||||||
|
(Stopwatch.Frequency * MaximumWaitMilliseconds / 1_000);
|
||||||
|
int queued;
|
||||||
|
var overrun = false;
|
||||||
|
while ((queued = SDL_GetAudioStreamQueued(_stream)) > _maximumQueuedBytes)
|
||||||
|
{
|
||||||
|
if (Stopwatch.GetTimestamp() >= deadline)
|
||||||
|
{
|
||||||
|
// Enqueue anyway rather than discarding the buffer. A gap in
|
||||||
|
// the stream is an audible click; the extra latency of one
|
||||||
|
// over-deep submission is not, and the queue recovers as soon
|
||||||
|
// as the device drains back under the cap.
|
||||||
|
overrun = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordSubmission(queued, blockStart, dropped: overrun, bytes: pcm.Length);
|
||||||
|
bool submitted;
|
||||||
|
fixed (byte* data = pcm)
|
||||||
|
{
|
||||||
|
submitted = SDL_PutAudioStreamData(_stream, (nint)data, pcm.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitted)
|
||||||
|
{
|
||||||
|
// Everything handed over minus what the device still holds is
|
||||||
|
// what the player has actually heard.
|
||||||
|
_totalSubmittedBytes += pcm.Length;
|
||||||
|
var bytesPerSecond = (double)_bytesPerFrame * _sampleRate;
|
||||||
|
if (bytesPerSecond > 0)
|
||||||
|
{
|
||||||
|
GuestAudioClock.Report(
|
||||||
|
Math.Max(0, _totalSubmittedBytes - queued - pcm.Length) / bytesPerSecond);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return submitted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Samples the queue depth at the moment the guest was allowed to write.
|
||||||
|
/// That depth is the playback latency the guest's audio is subject to, so
|
||||||
|
/// it is the number to look at when the sound is late; an observed depth
|
||||||
|
/// of zero is a genuine underrun, which is what a crackle sounds like.
|
||||||
|
/// Caller holds <see cref="_gate"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void RecordSubmission(int queuedBytes, long blockStart, bool dropped, int bytes)
|
||||||
|
{
|
||||||
|
if (!_traceQueue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
_submissions++;
|
||||||
|
_submittedBytes += bytes;
|
||||||
|
_blockedTicks += now - blockStart;
|
||||||
|
_queuedByteSum += queuedBytes;
|
||||||
|
_minQueuedBytes = Math.Min(_minQueuedBytes, queuedBytes);
|
||||||
|
_maxQueuedBytes = Math.Max(_maxQueuedBytes, queuedBytes);
|
||||||
|
if (dropped)
|
||||||
|
{
|
||||||
|
_drops++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queuedBytes == 0)
|
||||||
|
{
|
||||||
|
_emptyObservations++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedTicks = now - _windowStart;
|
||||||
|
if (elapsedTicks < Stopwatch.Frequency)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_windowStart = now;
|
||||||
|
var seconds = elapsedTicks / (double)Stopwatch.Frequency;
|
||||||
|
var bytesPerSecond = (double)_bytesPerFrame * _sampleRate;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[PERF][AUDIO] stream#{_streamId} {seconds:F1}s " +
|
||||||
|
$"queued_ms min={ToMilliseconds(_minQueuedBytes, bytesPerSecond):F0} " +
|
||||||
|
$"avg={ToMilliseconds((int)(_queuedByteSum / Math.Max(1, _submissions)), bytesPerSecond):F0} " +
|
||||||
|
$"max={ToMilliseconds(_maxQueuedBytes, bytesPerSecond):F0} " +
|
||||||
|
$"cap={ToMilliseconds(_maximumQueuedBytes, bytesPerSecond):F0} " +
|
||||||
|
$"submits/s={_submissions / seconds:F0} " +
|
||||||
|
$"fill={_submittedBytes / seconds / bytesPerSecond * 100.0:F0}% " +
|
||||||
|
$"blocked={_blockedTicks * 100.0 / elapsedTicks:F0}% " +
|
||||||
|
$"empty={_emptyObservations} drops={_drops}");
|
||||||
|
|
||||||
|
_submissions = 0;
|
||||||
|
_submittedBytes = 0;
|
||||||
|
_blockedTicks = 0;
|
||||||
|
_drops = 0;
|
||||||
|
_emptyObservations = 0;
|
||||||
|
_minQueuedBytes = int.MaxValue;
|
||||||
|
_maxQueuedBytes = 0;
|
||||||
|
_queuedByteSum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ToMilliseconds(int bytes, double bytesPerSecond) =>
|
||||||
|
bytesPerSecond <= 0 ? 0 : bytes / bytesPerSecond * 1000.0;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_stream is not null)
|
||||||
|
{
|
||||||
|
SDL_ClearAudioStream(_stream);
|
||||||
|
SDL_DestroyAudioStream(_stream);
|
||||||
|
_stream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using LibAtrac9.Utilities;
|
using LibAtrac9.Utilities;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using LibAtrac9.Utilities;
|
using LibAtrac9.Utilities;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using LibAtrac9.Utilities;
|
using LibAtrac9.Utilities;
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using LibAtrac9.Utilities;
|
using LibAtrac9.Utilities;
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2018 VGMToolbox Project
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AssemblyName>SharpEmu.LibAtrac9</AssemblyName>
|
||||||
|
<RootNamespace>LibAtrac9</RootNamespace>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9
|
namespace LibAtrac9
|
||||||
{
|
{
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using static LibAtrac9.HuffmanCodebooks;
|
using static LibAtrac9.HuffmanCodebooks;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
namespace LibAtrac9.Utilities
|
namespace LibAtrac9.Utilities
|
||||||
{
|
{
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -2,13 +2,17 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Acm;
|
namespace SharpEmu.Libs.Acm;
|
||||||
|
|
||||||
public static class AcmExports
|
public static class AcmExports
|
||||||
{
|
{
|
||||||
|
private const int AcmBatchErrorBytes = 32;
|
||||||
|
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
|
||||||
private static int _nextContextHandle;
|
private static int _nextContextHandle;
|
||||||
|
private static int _nextBatchHandle;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ZIXln2K3XMk",
|
Nid = "ZIXln2K3XMk",
|
||||||
@@ -23,12 +27,17 @@ public static class AcmExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextContextHandle));
|
||||||
Span<byte> handleBytes = stackalloc byte[sizeof(ulong)];
|
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(handleBytes, handle);
|
BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, handle);
|
||||||
return ctx.Memory.TryWrite(outContextAddress, handleBytes)
|
if (!ctx.Memory.TryWrite(outContextAddress, handleBytes))
|
||||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
{
|
||||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contexts[handle] = 0;
|
||||||
|
Trace($"context_create context={handle}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -38,7 +47,196 @@ public static class AcmExports
|
|||||||
LibraryName = "libSceAcm")]
|
LibraryName = "libSceAcm")]
|
||||||
public static int AcmContextDestroy(CpuContext ctx)
|
public static int AcmContextDestroy(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_ = ctx;
|
var context = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
Contexts.TryRemove(context, out _);
|
||||||
|
Trace($"context_destroy context={context}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tW9W+CAG4FE",
|
||||||
|
ExportName = "sceAcmBatchStartBuffer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmBatchStartBuffer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var context = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var commandsAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var commandsSize = ctx[CpuRegister.Rdx];
|
||||||
|
var errorAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var batchAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (!Contexts.ContainsKey(context) ||
|
||||||
|
batchAddress == 0 ||
|
||||||
|
(commandsSize != 0 && commandsAddress == 0))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompleteBatchStart(ctx, context, 1, errorAddress, batchAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8fe55ktlNVo",
|
||||||
|
ExportName = "sceAcmBatchStartBuffers",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmBatchStartBuffers(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var context = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var infoCount = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var infoArrayAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var errorAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var batchAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (!Contexts.ContainsKey(context) ||
|
||||||
|
batchAddress == 0 ||
|
||||||
|
(infoCount != 0 && infoArrayAddress == 0))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompleteBatchStart(ctx, context, infoCount, errorAddress, batchAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RLN3gRlXJLE",
|
||||||
|
ExportName = "sceAcmBatchWait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmBatchWait(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var context = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
return ctx.SetReturn(
|
||||||
|
Contexts.ContainsKey(context)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "r7z5YQFZo+U",
|
||||||
|
ExportName = "sceAcmBatchJobNotification",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmBatchJobNotification(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 32);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "u70oWo92SYQ",
|
||||||
|
ExportName = "sceAcm_ConvReverb_SharedInput",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmConvReverbSharedInput(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 1024);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9nLbWmRDpa8",
|
||||||
|
ExportName = "sceAcm_ConvReverb_SharedIr",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmConvReverbSharedIr(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 1024);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KovqaFbmtsM",
|
||||||
|
ExportName = "sceAcm_FFT",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmFft(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 256);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DR-ZCmvVR9Q",
|
||||||
|
ExportName = "sceAcm_IFFT",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmIfft(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 256);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LA4RCNKnFjg",
|
||||||
|
ExportName = "sceAcm_Panner",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAcm")]
|
||||||
|
public static int AcmPanner(CpuContext ctx) =>
|
||||||
|
AdvanceBatchInfo(ctx, 512);
|
||||||
|
|
||||||
|
internal static void ResetForTests()
|
||||||
|
{
|
||||||
|
Contexts.Clear();
|
||||||
|
Interlocked.Exchange(ref _nextContextHandle, 0);
|
||||||
|
Interlocked.Exchange(ref _nextBatchHandle, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompleteBatchStart(
|
||||||
|
CpuContext ctx,
|
||||||
|
uint context,
|
||||||
|
uint infoCount,
|
||||||
|
ulong errorAddress,
|
||||||
|
ulong batchAddress)
|
||||||
|
{
|
||||||
|
if (errorAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> error = stackalloc byte[AcmBatchErrorBytes];
|
||||||
|
error.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(errorAddress, error))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var batch = unchecked((uint)Interlocked.Increment(ref _nextBatchHandle));
|
||||||
|
Span<byte> batchBytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(batchBytes, batch);
|
||||||
|
if (!ctx.Memory.TryWrite(batchAddress, batchBytes))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace($"batch_start context={context} count={infoCount} batch={batch}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AdvanceBatchInfo(CpuContext ctx, ulong byteCount)
|
||||||
|
{
|
||||||
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (infoAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[24];
|
||||||
|
if (!ctx.Memory.TryRead(infoAddress, info))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(info);
|
||||||
|
var offset = BinaryPrimitives.ReadUInt64LittleEndian(info[8..]);
|
||||||
|
var size = BinaryPrimitives.ReadUInt64LittleEndian(info[16..]);
|
||||||
|
if (buffer != 0 && size != 0)
|
||||||
|
{
|
||||||
|
var nextOffset = offset > ulong.MaxValue - byteCount
|
||||||
|
? ulong.MaxValue
|
||||||
|
: offset + byteCount;
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[8..], Math.Min(size, nextOffset));
|
||||||
|
if (!ctx.Memory.TryWrite(infoAddress, info))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_ACM"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] acm.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,409 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using LibAtrac9;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
internal enum Atrac9PcmEncoding
|
||||||
|
{
|
||||||
|
Signed16,
|
||||||
|
Signed32,
|
||||||
|
Float,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct Atrac9DecodeResult(
|
||||||
|
int Status,
|
||||||
|
int InputConsumed,
|
||||||
|
int OutputWritten,
|
||||||
|
ulong TotalDecodedSamples,
|
||||||
|
uint Frames);
|
||||||
|
|
||||||
|
internal sealed class Atrac9DecodeState
|
||||||
|
{
|
||||||
|
internal const int ResultNotInitialized = 0x00000001;
|
||||||
|
internal const int ResultInvalidData = 0x00000002;
|
||||||
|
internal const int ResultInvalidParameter = 0x00000004;
|
||||||
|
internal const int ResultPartialInput = 0x00000008;
|
||||||
|
internal const int ResultNotEnoughRoom = 0x00000010;
|
||||||
|
internal const int ResultCodecError = 0x40000000;
|
||||||
|
|
||||||
|
private const int MaxContainerHeaderBytes = 8 * 1024;
|
||||||
|
|
||||||
|
private enum ContainerScan
|
||||||
|
{
|
||||||
|
NotContainer,
|
||||||
|
NeedMoreData,
|
||||||
|
Found,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private Atrac9Decoder? _decoder;
|
||||||
|
private byte[]? _configData;
|
||||||
|
private byte[]? _compressed;
|
||||||
|
private short[][]? _planarPcm;
|
||||||
|
private byte[]? _containerHeader;
|
||||||
|
private int _containerHeaderLength;
|
||||||
|
private int _compressedLength;
|
||||||
|
private ulong _totalDecodedSamples;
|
||||||
|
|
||||||
|
public Atrac9Config? Config
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _decoder?.Config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInitialize(ReadOnlySpan<byte> configData)
|
||||||
|
{
|
||||||
|
if (configData.Length < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var normalizedConfig = configData[..4].ToArray();
|
||||||
|
var decoder = new Atrac9Decoder();
|
||||||
|
decoder.Initialize(normalizedConfig);
|
||||||
|
var config = decoder.Config;
|
||||||
|
|
||||||
|
_decoder = decoder;
|
||||||
|
_configData = normalizedConfig;
|
||||||
|
_compressed = new byte[config.SuperframeBytes];
|
||||||
|
_planarPcm = CreatePcmBuffer(config.ChannelCount, config.SuperframeSamples);
|
||||||
|
_compressedLength = 0;
|
||||||
|
_totalDecodedSamples = 0;
|
||||||
|
_containerHeaderLength = 0;
|
||||||
|
Trace(
|
||||||
|
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
|
||||||
|
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
|
||||||
|
$"superframe_samples={config.SuperframeSamples} superframe_bytes={config.SuperframeBytes} " +
|
||||||
|
$"frames_per_superframe={config.FramesPerSuperframe}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is ArgumentException or InvalidDataException or InvalidOperationException)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_configData is null)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoder = new Atrac9Decoder();
|
||||||
|
decoder.Initialize((byte[])_configData.Clone());
|
||||||
|
_decoder = decoder;
|
||||||
|
_compressedLength = 0;
|
||||||
|
_totalDecodedSamples = 0;
|
||||||
|
_containerHeaderLength = 0;
|
||||||
|
if (_compressed is not null)
|
||||||
|
{
|
||||||
|
Array.Clear(_compressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Atrac9DecodeResult Decode(
|
||||||
|
ReadOnlySpan<byte> input,
|
||||||
|
Span<byte> output,
|
||||||
|
Atrac9PcmEncoding encoding,
|
||||||
|
int requestedChannels,
|
||||||
|
bool multipleFrames)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_decoder is null || _compressed is null || _planarPcm is null)
|
||||||
|
{
|
||||||
|
return new Atrac9DecodeResult(
|
||||||
|
ResultNotInitialized,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
_totalDecodedSamples,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = _decoder.Config;
|
||||||
|
var channels = requestedChannels > 0 ? requestedChannels : config.ChannelCount;
|
||||||
|
if (channels is < 1 or > 16)
|
||||||
|
{
|
||||||
|
return new Atrac9DecodeResult(
|
||||||
|
ResultInvalidParameter,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
_totalDecodedSamples,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytesPerSample = GetBytesPerSample(encoding);
|
||||||
|
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
|
||||||
|
var consumed = 0;
|
||||||
|
var written = 0;
|
||||||
|
uint frames = 0;
|
||||||
|
var status = 0;
|
||||||
|
|
||||||
|
while (_compressedLength == config.SuperframeBytes ||
|
||||||
|
consumed < input.Length)
|
||||||
|
{
|
||||||
|
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
|
||||||
|
// container rather than a pointer into its `data` chunk, so the
|
||||||
|
// stream has to be advanced past the header before the first
|
||||||
|
// superframe — otherwise every job fails with invalid data and
|
||||||
|
// the title drops the voice. This is checked at every superframe
|
||||||
|
// boundary, not just after initialize, because a looping voice
|
||||||
|
// rewinds to the file header without reinitialising.
|
||||||
|
if (_compressedLength == 0 && (_containerHeaderLength != 0 || consumed < input.Length))
|
||||||
|
{
|
||||||
|
var scan = ScanContainerHeader(input[consumed..], out var headerBytes);
|
||||||
|
if (scan == ContainerScan.NeedMoreData)
|
||||||
|
{
|
||||||
|
consumed = input.Length;
|
||||||
|
status |= ResultPartialInput;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scan == ContainerScan.Found)
|
||||||
|
{
|
||||||
|
_containerHeader = null;
|
||||||
|
_containerHeaderLength = 0;
|
||||||
|
consumed += headerBytes;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_compressedLength < config.SuperframeBytes)
|
||||||
|
{
|
||||||
|
var copied = Math.Min(config.SuperframeBytes - _compressedLength, input.Length - consumed);
|
||||||
|
input.Slice(consumed, copied).CopyTo(_compressed.AsSpan(_compressedLength));
|
||||||
|
_compressedLength += copied;
|
||||||
|
consumed += copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_compressedLength < config.SuperframeBytes)
|
||||||
|
{
|
||||||
|
status |= ResultPartialInput;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.Length - written < outputBytesPerSuperframe)
|
||||||
|
{
|
||||||
|
status |= ResultNotEnoughRoom;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_decoder.Decode(_compressed, _planarPcm);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is ArgumentException or InvalidDataException or InvalidOperationException or IndexOutOfRangeException)
|
||||||
|
{
|
||||||
|
Trace(
|
||||||
|
$"decode_failed superframe_bytes={config.SuperframeBytes} " +
|
||||||
|
$"config={Convert.ToHexString(_configData ?? [])} " +
|
||||||
|
$"head={Convert.ToHexString(_compressed.AsSpan(0, Math.Min(16, _compressed.Length)))} " +
|
||||||
|
$"error={exception.GetType().Name}: {exception.Message}");
|
||||||
|
_compressedLength = 0;
|
||||||
|
return new Atrac9DecodeResult(
|
||||||
|
status | ResultInvalidData | ResultCodecError,
|
||||||
|
consumed,
|
||||||
|
written,
|
||||||
|
_totalDecodedSamples,
|
||||||
|
frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteInterleaved(
|
||||||
|
_planarPcm,
|
||||||
|
output.Slice(written, outputBytesPerSuperframe),
|
||||||
|
config.SuperframeSamples,
|
||||||
|
channels,
|
||||||
|
encoding);
|
||||||
|
|
||||||
|
written += outputBytesPerSuperframe;
|
||||||
|
_compressedLength = 0;
|
||||||
|
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
|
||||||
|
frames += unchecked((uint)config.FramesPerSuperframe);
|
||||||
|
|
||||||
|
if (!multipleFrames)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Atrac9DecodeResult(
|
||||||
|
status,
|
||||||
|
consumed,
|
||||||
|
written,
|
||||||
|
_totalDecodedSamples,
|
||||||
|
frames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContainerScan ScanContainerHeader(ReadOnlySpan<byte> input, out int inputHeaderBytes)
|
||||||
|
{
|
||||||
|
inputHeaderBytes = 0;
|
||||||
|
|
||||||
|
if (_containerHeaderLength == 0 &&
|
||||||
|
(input.Length < 4 || !input[..4].SequenceEqual("RIFF"u8)))
|
||||||
|
{
|
||||||
|
return ContainerScan.NotContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_containerHeader ??= new byte[MaxContainerHeaderBytes];
|
||||||
|
var previousLength = _containerHeaderLength;
|
||||||
|
var copied = Math.Min(input.Length, MaxContainerHeaderBytes - previousLength);
|
||||||
|
input[..copied].CopyTo(_containerHeader.AsSpan(previousLength));
|
||||||
|
var totalLength = previousLength + copied;
|
||||||
|
|
||||||
|
if (!TryFindRiffDataOffset(_containerHeader.AsSpan(0, totalLength), out var dataOffset))
|
||||||
|
{
|
||||||
|
if (totalLength >= MaxContainerHeaderBytes)
|
||||||
|
{
|
||||||
|
// Not a shape we understand — fall back to treating the stream
|
||||||
|
// as raw superframes rather than swallowing it forever. The
|
||||||
|
// scratch is dropped without advancing the caller's cursor, so
|
||||||
|
// the bytes are still decoded normally.
|
||||||
|
Trace($"container_scan_gave_up bytes={totalLength}");
|
||||||
|
_containerHeader = null;
|
||||||
|
_containerHeaderLength = 0;
|
||||||
|
return ContainerScan.NotContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_containerHeaderLength = totalLength;
|
||||||
|
return ContainerScan.NeedMoreData;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputHeaderBytes = dataOffset - previousLength;
|
||||||
|
Trace($"container_header_skipped bytes={dataOffset} from_this_input={inputHeaderBytes}");
|
||||||
|
return ContainerScan.Found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindRiffDataOffset(ReadOnlySpan<byte> header, out int dataOffset)
|
||||||
|
{
|
||||||
|
dataOffset = 0;
|
||||||
|
if (header.Length < 12 ||
|
||||||
|
!header[..4].SequenceEqual("RIFF"u8) ||
|
||||||
|
!header.Slice(8, 4).SequenceEqual("WAVE"u8))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 12;
|
||||||
|
while (offset + 8 <= header.Length)
|
||||||
|
{
|
||||||
|
var chunkId = header.Slice(offset, 4);
|
||||||
|
var chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(offset + 4, 4));
|
||||||
|
offset += 8;
|
||||||
|
if (chunkId.SequenceEqual("data"u8))
|
||||||
|
{
|
||||||
|
dataOffset = offset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIFF chunks are word aligned.
|
||||||
|
var advance = chunkSize + (chunkSize & 1);
|
||||||
|
if (advance > (ulong)(int.MaxValue - offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += (int)advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short[][] CreatePcmBuffer(int channels, int samples)
|
||||||
|
{
|
||||||
|
var result = new short[channels][];
|
||||||
|
for (var channel = 0; channel < channels; channel++)
|
||||||
|
{
|
||||||
|
result[channel] = new short[samples];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetBytesPerSample(Atrac9PcmEncoding encoding) =>
|
||||||
|
encoding switch
|
||||||
|
{
|
||||||
|
Atrac9PcmEncoding.Signed16 => sizeof(short),
|
||||||
|
Atrac9PcmEncoding.Signed32 => sizeof(int),
|
||||||
|
Atrac9PcmEncoding.Float => sizeof(float),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(encoding)),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void WriteInterleaved(
|
||||||
|
short[][] source,
|
||||||
|
Span<byte> destination,
|
||||||
|
int samples,
|
||||||
|
int channels,
|
||||||
|
Atrac9PcmEncoding encoding)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
for (var sample = 0; sample < samples; sample++)
|
||||||
|
{
|
||||||
|
for (var channel = 0; channel < channels; channel++)
|
||||||
|
{
|
||||||
|
var sourceChannel = Math.Min(channel, source.Length - 1);
|
||||||
|
var value = source[sourceChannel][sample];
|
||||||
|
switch (encoding)
|
||||||
|
{
|
||||||
|
case Atrac9PcmEncoding.Signed16:
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[offset..], value);
|
||||||
|
offset += sizeof(short);
|
||||||
|
break;
|
||||||
|
case Atrac9PcmEncoding.Signed32:
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(destination[offset..], value << 16);
|
||||||
|
offset += sizeof(int);
|
||||||
|
break;
|
||||||
|
case Atrac9PcmEncoding.Float:
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(
|
||||||
|
destination[offset..],
|
||||||
|
BitConverter.SingleToInt32Bits(value / 32768.0f));
|
||||||
|
offset += sizeof(float);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(encoding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trace(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] ajm.at9.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clear()
|
||||||
|
{
|
||||||
|
_decoder = null;
|
||||||
|
_configData = null;
|
||||||
|
_compressed = null;
|
||||||
|
_planarPcm = null;
|
||||||
|
_containerHeader = null;
|
||||||
|
_containerHeaderLength = 0;
|
||||||
|
_compressedLength = 0;
|
||||||
|
_totalDecodedSamples = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ public static class AudioOutExports
|
|||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat,
|
bool isFloat,
|
||||||
|
bool preservesGuestFormat,
|
||||||
IHostAudioStream? backend)
|
IHostAudioStream? backend)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
@@ -54,6 +55,7 @@ public static class AudioOutExports
|
|||||||
Channels = channels;
|
Channels = channels;
|
||||||
BytesPerSample = bytesPerSample;
|
BytesPerSample = bytesPerSample;
|
||||||
IsFloat = isFloat;
|
IsFloat = isFloat;
|
||||||
|
PreservesGuestFormat = preservesGuestFormat;
|
||||||
Backend = backend;
|
Backend = backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ public static class AudioOutExports
|
|||||||
public int Channels { get; }
|
public int Channels { get; }
|
||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
|
public bool PreservesGuestFormat { get; }
|
||||||
public IHostAudioStream? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
public object SubmissionGate { get; } = new();
|
public object SubmissionGate { get; } = new();
|
||||||
public volatile float Volume = 1.0f;
|
public volatile float Volume = 1.0f;
|
||||||
@@ -140,6 +143,7 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
IHostAudioStream? backend = null;
|
IHostAudioStream? backend = null;
|
||||||
|
var preservesGuestFormat = false;
|
||||||
string backendName;
|
string backendName;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -152,7 +156,18 @@ public static class AudioOutExports
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var audio = HostPlatform.Current.Audio;
|
var audio = HostPlatform.Current.Audio;
|
||||||
|
if (audio is IHostPcmAudioOutput pcmAudio)
|
||||||
|
{
|
||||||
|
backend = pcmAudio.OpenPcmStream(
|
||||||
|
frequency,
|
||||||
|
channels,
|
||||||
|
isFloat ? HostPcmFormat.Float32 : HostPcmFormat.Signed16);
|
||||||
|
preservesGuestFormat = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||||
|
}
|
||||||
backendName = audio.BackendName;
|
backendName = audio.BackendName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,6 +188,7 @@ public static class AudioOutExports
|
|||||||
channels,
|
channels,
|
||||||
bytesPerSample,
|
bytesPerSample,
|
||||||
isFloat,
|
isFloat,
|
||||||
|
preservesGuestFormat,
|
||||||
backend);
|
backend);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||||
@@ -321,18 +337,13 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
var outputLength = port.PreservesGuestFormat
|
||||||
|
? port.BufferByteLength
|
||||||
|
: checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AudioPcmConversion.ConvertToStereoPcm16(
|
ConvertForHost(port, source, output.AsSpan(0, outputLength));
|
||||||
source,
|
|
||||||
output.AsSpan(0, outputLength),
|
|
||||||
checked((int)port.BufferLength),
|
|
||||||
port.Channels,
|
|
||||||
port.BytesPerSample,
|
|
||||||
port.IsFloat,
|
|
||||||
port.Volume);
|
|
||||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
port.PaceSilence();
|
||||||
@@ -449,17 +460,11 @@ public static class AudioOutExports
|
|||||||
|
|
||||||
TraceOutput(output.Handle, output.Port, source);
|
TraceOutput(output.Handle, output.Port, source);
|
||||||
|
|
||||||
output.HostBufferLength = checked(
|
output.HostBufferLength = output.Port.PreservesGuestFormat
|
||||||
(int)output.Port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
? output.Port.BufferByteLength
|
||||||
|
: checked((int)output.Port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||||
output.HostBuffer = ArrayPool<byte>.Shared.Rent(output.HostBufferLength);
|
output.HostBuffer = ArrayPool<byte>.Shared.Rent(output.HostBufferLength);
|
||||||
AudioPcmConversion.ConvertToStereoPcm16(
|
ConvertForHost(output.Port, source, output.HostBuffer.AsSpan(0, output.HostBufferLength));
|
||||||
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -513,6 +518,24 @@ public static class AudioOutExports
|
|||||||
(ulong)candidate.BufferLength * current.Frequency >
|
(ulong)candidate.BufferLength * current.Frequency >
|
||||||
(ulong)current.BufferLength * candidate.Frequency;
|
(ulong)current.BufferLength * candidate.Frequency;
|
||||||
|
|
||||||
|
private static void ConvertForHost(PortState port, ReadOnlySpan<byte> source, Span<byte> destination)
|
||||||
|
{
|
||||||
|
if (port.PreservesGuestFormat)
|
||||||
|
{
|
||||||
|
AudioPcmConversion.CopyWithVolume(source, destination, port.IsFloat, port.Volume);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioPcmConversion.ConvertToStereoPcm16(
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
checked((int)port.BufferLength),
|
||||||
|
port.Channels,
|
||||||
|
port.BytesPerSample,
|
||||||
|
port.IsFloat,
|
||||||
|
port.Volume);
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceOutput(int handle, PortState port, ReadOnlySpan<byte> source)
|
private static void TraceOutput(int handle, PortState port, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
if (!_traceOutput)
|
if (!_traceOutput)
|
||||||
|
|||||||
@@ -43,6 +43,46 @@ internal static class AudioPcmConversion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies interleaved PCM without changing its channel layout. SDL can convert
|
||||||
|
/// this directly to the physical device, which preserves surround mixes that
|
||||||
|
/// would otherwise be truncated to the first two guest channels.
|
||||||
|
/// </summary>
|
||||||
|
public static void CopyWithVolume(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
Span<byte> destination,
|
||||||
|
bool isFloat,
|
||||||
|
float volume)
|
||||||
|
{
|
||||||
|
var clampedVolume = Math.Clamp(volume, 0.0f, 1.0f);
|
||||||
|
if (clampedVolume >= 1.0f)
|
||||||
|
{
|
||||||
|
source.CopyTo(destination);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFloat)
|
||||||
|
{
|
||||||
|
for (var offset = 0; offset < source.Length; offset += sizeof(float))
|
||||||
|
{
|
||||||
|
var sample = BinaryPrimitives.ReadSingleLittleEndian(source.Slice(offset, sizeof(float)));
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(
|
||||||
|
destination.Slice(offset, sizeof(float)),
|
||||||
|
sample * clampedVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var offset = 0; offset < source.Length; offset += sizeof(short))
|
||||||
|
{
|
||||||
|
var sample = BinaryPrimitives.ReadInt16LittleEndian(source.Slice(offset, sizeof(short)));
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(
|
||||||
|
destination.Slice(offset, sizeof(short)),
|
||||||
|
ApplyVolume(sample, clampedVolume));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static short ReadSample(
|
private static short ReadSample(
|
||||||
ReadOnlySpan<byte> frame,
|
ReadOnlySpan<byte> frame,
|
||||||
int channel,
|
int channel,
|
||||||
|
|||||||
@@ -2,28 +2,84 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Codec;
|
namespace SharpEmu.Libs.Codec;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
|
/// libSceVideodec / libSceAudiodec compatibility exports.
|
||||||
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
|
|
||||||
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
|
|
||||||
/// and report "no output produced" so guests advance instead of failing on
|
|
||||||
/// unresolved imports.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CodecExports
|
public static class CodecExports
|
||||||
{
|
{
|
||||||
private const int Ok = 0;
|
private const int Ok = 0;
|
||||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||||
|
private const int AudiodecErrorInvalidType = unchecked((int)0x807F0001);
|
||||||
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
|
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
|
||||||
|
private const int AudiodecErrorInvalidParamSize = unchecked((int)0x807F0004);
|
||||||
|
private const int AudiodecErrorInvalidBsiInfoSize = unchecked((int)0x807F0005);
|
||||||
|
private const int AudiodecErrorInvalidAuInfoSize = unchecked((int)0x807F0006);
|
||||||
|
private const int AudiodecErrorInvalidPcmItemSize = unchecked((int)0x807F0007);
|
||||||
|
private const int AudiodecErrorInvalidCtrlPointer = unchecked((int)0x807F0008);
|
||||||
|
private const int AudiodecErrorInvalidParamPointer = unchecked((int)0x807F0009);
|
||||||
|
private const int AudiodecErrorInvalidBsiInfoPointer = unchecked((int)0x807F000A);
|
||||||
|
private const int AudiodecErrorInvalidAuInfoPointer = unchecked((int)0x807F000B);
|
||||||
|
private const int AudiodecErrorInvalidPcmItemPointer = unchecked((int)0x807F000C);
|
||||||
|
private const int AudiodecErrorInvalidAuPointer = unchecked((int)0x807F000D);
|
||||||
|
private const int AudiodecErrorInvalidPcmPointer = unchecked((int)0x807F000E);
|
||||||
|
private const int AudiodecErrorInvalidHandle = unchecked((int)0x807F000F);
|
||||||
|
private const int AudiodecErrorInvalidWordLength = unchecked((int)0x807F0010);
|
||||||
|
private const int AudiodecErrorInvalidAuSize = unchecked((int)0x807F0011);
|
||||||
|
private const int AudiodecErrorInvalidPcmSize = unchecked((int)0x807F0012);
|
||||||
|
private const uint AudiodecTypeAt9 = 1;
|
||||||
|
private const uint AudiodecTypeMp3 = 2;
|
||||||
|
private const uint AudiodecTypeAac = 3;
|
||||||
|
private const int MaxAudioDecoders = 64;
|
||||||
|
private const int MaxDecodeBufferBytes = 64 * 1024 * 1024;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
|
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
|
private static readonly ConcurrentDictionary<int, AudioDecoderState> AudioDecoders = new();
|
||||||
|
private static readonly ConcurrentDictionary<uint, int> AudioCodecInitCounts = new();
|
||||||
|
private static readonly object AudioDecoderGate = new();
|
||||||
private static long _nextHandle = 1;
|
private static long _nextHandle = 1;
|
||||||
|
private static int _nextAudioHandle;
|
||||||
|
|
||||||
|
private sealed class AudioDecoderState
|
||||||
|
{
|
||||||
|
public required uint CodecType { get; init; }
|
||||||
|
|
||||||
|
public required int WordSize { get; init; }
|
||||||
|
|
||||||
|
public required int Channels { get; init; }
|
||||||
|
|
||||||
|
public required int SampleRate { get; init; }
|
||||||
|
|
||||||
|
public required int FrameBytes { get; init; }
|
||||||
|
|
||||||
|
public required int FramesPerSuperframe { get; init; }
|
||||||
|
|
||||||
|
public required int FrameSamples { get; init; }
|
||||||
|
|
||||||
|
public Atrac9DecodeState? Atrac9 { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct AudioControl(
|
||||||
|
ulong ParamAddress,
|
||||||
|
ulong BsiInfoAddress,
|
||||||
|
ulong AuInfoAddress,
|
||||||
|
ulong PcmItemAddress,
|
||||||
|
ulong AuAddress,
|
||||||
|
uint AuSize,
|
||||||
|
ulong PcmAddress,
|
||||||
|
uint PcmSize,
|
||||||
|
int WordSize,
|
||||||
|
byte[]? Atrac9Config,
|
||||||
|
uint AacMaxChannels,
|
||||||
|
uint AacSampleRateIndex);
|
||||||
|
|
||||||
// ---- Video decoder ----
|
// ---- Video decoder ----
|
||||||
|
|
||||||
@@ -65,34 +121,572 @@ public static class CodecExports
|
|||||||
|
|
||||||
// ---- Audio decoder ----
|
// ---- Audio decoder ----
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "VjhsmxpcezI", ExportName = "sceAudiodecInitLibrary",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecInitLibrary(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var codecType = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!IsValidAudioCodecType(codecType))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidType);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioCodecInitCounts.AddOrUpdate(codecType, 1, static (_, count) => count + 1);
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "h5jSB2QIDV0", ExportName = "sceAudiodecTermLibrary",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecTermLibrary(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var codecType = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!IsValidAudioCodecType(codecType))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidType);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioCodecInitCounts.AddOrUpdate(codecType, 0, static (_, count) => Math.Max(0, count - 1));
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
|
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
|
||||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
public static int AudiodecCreateDecoder(CpuContext ctx)
|
public static int AudiodecCreateDecoder(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
|
var controlAddress = ctx[CpuRegister.Rdi];
|
||||||
AudioDecoders[handle] = 1;
|
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
// sceAudiodec returns the handle directly (>= 0) or a negative error.
|
if (!IsValidAudioCodecType(codecType))
|
||||||
ctx[CpuRegister.Rax] = handle;
|
{
|
||||||
return unchecked((int)handle);
|
return SetReturn(ctx, AudiodecErrorInvalidType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var validation = TryReadAudioControl(ctx, controlAddress, codecType, decode: false, out var control);
|
||||||
|
if (validation != Ok)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AudioCodecInitCounts.TryGetValue(codecType, out var initCount) || initCount == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoder = CreateAudioDecoder(codecType, control);
|
||||||
|
if (decoder is null)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
int handle;
|
||||||
|
lock (AudioDecoderGate)
|
||||||
|
{
|
||||||
|
if (AudioDecoders.Count >= MaxAudioDecoders)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_nextAudioHandle = _nextAudioHandle % MaxAudioDecoders + 1;
|
||||||
|
handle = _nextAudioHandle;
|
||||||
|
}
|
||||||
|
while (AudioDecoders.ContainsKey(handle));
|
||||||
|
|
||||||
|
AudioDecoders[handle] = decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteAudioDecoderInfo(ctx, control.BsiInfoAddress, decoder, Ok);
|
||||||
|
return SetReturn(ctx, handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
|
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
|
||||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
public static int AudiodecDecode(CpuContext ctx)
|
public static int AudiodecDecode(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// No decoder present: report success with zero output samples so the
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
// caller treats the frame as silent rather than erroring.
|
if (!AudioDecoders.TryGetValue(handle, out var decoder))
|
||||||
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var validation = TryReadAudioControl(
|
||||||
|
ctx,
|
||||||
|
ctx[CpuRegister.Rsi],
|
||||||
|
decoder.CodecType,
|
||||||
|
decode: true,
|
||||||
|
out var control);
|
||||||
|
if (validation != Ok)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control.AuSize > MaxDecodeBufferBytes)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidAuSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control.PcmSize > MaxDecodeBufferBytes)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidPcmSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoder.CodecType == AudiodecTypeAt9 && decoder.Atrac9 is not null)
|
||||||
|
{
|
||||||
|
DecodeAtrac9(ctx, decoder, control);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DecodeAudioSilence(ctx, decoder, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
|
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
|
||||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
public static int AudiodecDeleteDecoder(CpuContext ctx)
|
public static int AudiodecDeleteDecoder(CpuContext ctx)
|
||||||
{
|
{
|
||||||
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return SetReturn(
|
||||||
|
ctx,
|
||||||
|
AudioDecoders.TryRemove(handle, out _)
|
||||||
|
? Ok
|
||||||
|
: AudiodecErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(Nid = "6Vf9WTLDoss", ExportName = "sceAudiodecClearContext",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
|
||||||
|
public static int AudiodecClearContext(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!AudioDecoders.TryGetValue(handle, out var decoder))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, AudiodecErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder.Atrac9?.Reset();
|
||||||
return SetReturn(ctx, Ok);
|
return SetReturn(ctx, Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsValidAudioCodecType(uint codecType) =>
|
||||||
|
codecType is AudiodecTypeAt9 or AudiodecTypeMp3 or AudiodecTypeAac;
|
||||||
|
|
||||||
|
private static int TryReadAudioControl(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong controlAddress,
|
||||||
|
uint codecType,
|
||||||
|
bool decode,
|
||||||
|
out AudioControl control)
|
||||||
|
{
|
||||||
|
control = default;
|
||||||
|
if (controlAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidCtrlPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> controlData = stackalloc byte[32];
|
||||||
|
if (!ctx.Memory.TryRead(controlAddress, controlData))
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidCtrlPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paramAddress = BinaryPrimitives.ReadUInt64LittleEndian(controlData);
|
||||||
|
var bsiInfoAddress = BinaryPrimitives.ReadUInt64LittleEndian(controlData[8..]);
|
||||||
|
var auInfoAddress = BinaryPrimitives.ReadUInt64LittleEndian(controlData[16..]);
|
||||||
|
var pcmItemAddress = BinaryPrimitives.ReadUInt64LittleEndian(controlData[24..]);
|
||||||
|
if (paramAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidParamPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bsiInfoAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidBsiInfoPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auInfoAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidAuInfoPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pcmItemAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidPcmItemPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> auInfo = stackalloc byte[24];
|
||||||
|
if (!ctx.Memory.TryRead(auInfoAddress, auInfo))
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidAuInfoPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BinaryPrimitives.ReadUInt32LittleEndian(auInfo) != auInfo.Length)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidAuInfoSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> pcmItem = stackalloc byte[24];
|
||||||
|
if (!ctx.Memory.TryRead(pcmItemAddress, pcmItem))
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidPcmItemPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BinaryPrimitives.ReadUInt32LittleEndian(pcmItem) != pcmItem.Length)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidPcmItemSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
var auAddress = BinaryPrimitives.ReadUInt64LittleEndian(auInfo[8..]);
|
||||||
|
var auSize = BinaryPrimitives.ReadUInt32LittleEndian(auInfo[16..]);
|
||||||
|
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcmItem[8..]);
|
||||||
|
var pcmSize = BinaryPrimitives.ReadUInt32LittleEndian(pcmItem[16..]);
|
||||||
|
if (decode && auAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidAuPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decode && pcmAddress == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidPcmPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameterResult = TryReadAudioParameters(
|
||||||
|
ctx,
|
||||||
|
codecType,
|
||||||
|
paramAddress,
|
||||||
|
bsiInfoAddress,
|
||||||
|
out var wordSize,
|
||||||
|
out var atrac9Config,
|
||||||
|
out var aacMaxChannels,
|
||||||
|
out var aacSampleRateIndex);
|
||||||
|
if (parameterResult != Ok)
|
||||||
|
{
|
||||||
|
return parameterResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decode && auSize == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidAuSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decode && pcmSize == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidPcmSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
control = new AudioControl(
|
||||||
|
paramAddress,
|
||||||
|
bsiInfoAddress,
|
||||||
|
auInfoAddress,
|
||||||
|
pcmItemAddress,
|
||||||
|
auAddress,
|
||||||
|
auSize,
|
||||||
|
pcmAddress,
|
||||||
|
pcmSize,
|
||||||
|
wordSize,
|
||||||
|
atrac9Config,
|
||||||
|
aacMaxChannels,
|
||||||
|
aacSampleRateIndex);
|
||||||
|
return Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TryReadAudioParameters(
|
||||||
|
CpuContext ctx,
|
||||||
|
uint codecType,
|
||||||
|
ulong paramAddress,
|
||||||
|
ulong bsiInfoAddress,
|
||||||
|
out int wordSize,
|
||||||
|
out byte[]? atrac9Config,
|
||||||
|
out uint aacMaxChannels,
|
||||||
|
out uint aacSampleRateIndex)
|
||||||
|
{
|
||||||
|
wordSize = 0;
|
||||||
|
atrac9Config = null;
|
||||||
|
aacMaxChannels = 0;
|
||||||
|
aacSampleRateIndex = 0;
|
||||||
|
|
||||||
|
var paramSize = codecType switch
|
||||||
|
{
|
||||||
|
AudiodecTypeAt9 => 12,
|
||||||
|
AudiodecTypeMp3 => 8,
|
||||||
|
AudiodecTypeAac => 24,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
var bsiSize = codecType switch
|
||||||
|
{
|
||||||
|
AudiodecTypeAt9 => 36,
|
||||||
|
AudiodecTypeMp3 => 24,
|
||||||
|
AudiodecTypeAac => 20,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
if (paramSize == 0 || bsiSize == 0)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidType;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> param = stackalloc byte[paramSize];
|
||||||
|
if (!ctx.Memory.TryRead(paramAddress, param))
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidParamPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suppliedParamSize = BinaryPrimitives.ReadUInt32LittleEndian(param);
|
||||||
|
if (codecType == AudiodecTypeAac
|
||||||
|
? suppliedParamSize < paramSize
|
||||||
|
: suppliedParamSize != paramSize)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidParamSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bsi = stackalloc byte[bsiSize];
|
||||||
|
if (!ctx.Memory.TryRead(bsiInfoAddress, bsi))
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidBsiInfoPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BinaryPrimitives.ReadUInt32LittleEndian(bsi) != bsiSize)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidBsiInfoSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
wordSize = BinaryPrimitives.ReadInt32LittleEndian(param[4..]);
|
||||||
|
if (wordSize is < 0 or > 2)
|
||||||
|
{
|
||||||
|
return AudiodecErrorInvalidWordLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codecType == AudiodecTypeAt9)
|
||||||
|
{
|
||||||
|
atrac9Config = param[8..12].ToArray();
|
||||||
|
}
|
||||||
|
else if (codecType == AudiodecTypeAac)
|
||||||
|
{
|
||||||
|
aacSampleRateIndex = BinaryPrimitives.ReadUInt32LittleEndian(param[12..]);
|
||||||
|
aacMaxChannels = BinaryPrimitives.ReadUInt32LittleEndian(param[16..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioDecoderState? CreateAudioDecoder(uint codecType, AudioControl control)
|
||||||
|
{
|
||||||
|
if (codecType == AudiodecTypeAt9)
|
||||||
|
{
|
||||||
|
var atrac9 = new Atrac9DecodeState();
|
||||||
|
if (control.Atrac9Config is null || !atrac9.TryInitialize(control.Atrac9Config))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = atrac9.Config!;
|
||||||
|
return new AudioDecoderState
|
||||||
|
{
|
||||||
|
CodecType = codecType,
|
||||||
|
WordSize = control.WordSize,
|
||||||
|
Channels = config.ChannelCount,
|
||||||
|
SampleRate = config.SampleRate,
|
||||||
|
FrameBytes = config.SuperframeBytes,
|
||||||
|
FramesPerSuperframe = config.FramesPerSuperframe,
|
||||||
|
FrameSamples = config.FrameSamples,
|
||||||
|
Atrac9 = atrac9,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codecType == AudiodecTypeMp3)
|
||||||
|
{
|
||||||
|
return new AudioDecoderState
|
||||||
|
{
|
||||||
|
CodecType = codecType,
|
||||||
|
WordSize = control.WordSize,
|
||||||
|
Channels = 2,
|
||||||
|
SampleRate = 48_000,
|
||||||
|
FrameBytes = 1_441,
|
||||||
|
FramesPerSuperframe = 1,
|
||||||
|
FrameSamples = 1_152,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var channels = control.AacMaxChannels == 0
|
||||||
|
? 2
|
||||||
|
: unchecked((int)Math.Min(control.AacMaxChannels, 8));
|
||||||
|
return new AudioDecoderState
|
||||||
|
{
|
||||||
|
CodecType = codecType,
|
||||||
|
WordSize = control.WordSize,
|
||||||
|
Channels = channels,
|
||||||
|
SampleRate = GetAacSampleRate(control.AacSampleRateIndex),
|
||||||
|
FrameBytes = 4_608,
|
||||||
|
FramesPerSuperframe = 1,
|
||||||
|
FrameSamples = 2_048,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DecodeAtrac9(CpuContext ctx, AudioDecoderState decoder, AudioControl control)
|
||||||
|
{
|
||||||
|
var input = ArrayPool<byte>.Shared.Rent(checked((int)control.AuSize));
|
||||||
|
var output = ArrayPool<byte>.Shared.Rent(checked((int)control.PcmSize));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(control.AuAddress, input.AsSpan(0, checked((int)control.AuSize))))
|
||||||
|
{
|
||||||
|
WriteAudioDecoderInfo(ctx, control.BsiInfoAddress, decoder, AudiodecErrorInvalidAuPointer);
|
||||||
|
WriteAudioBufferSizes(ctx, control, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = decoder.Atrac9!.Decode(
|
||||||
|
input.AsSpan(0, checked((int)control.AuSize)),
|
||||||
|
output.AsSpan(0, checked((int)control.PcmSize)),
|
||||||
|
GetAtrac9Encoding(decoder.WordSize),
|
||||||
|
decoder.Channels,
|
||||||
|
multipleFrames: false);
|
||||||
|
|
||||||
|
var outputWritten = result.OutputWritten;
|
||||||
|
if (outputWritten != 0 &&
|
||||||
|
!ctx.Memory.TryWrite(control.PcmAddress, output.AsSpan(0, outputWritten)))
|
||||||
|
{
|
||||||
|
outputWritten = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteAudioBufferSizes(
|
||||||
|
ctx,
|
||||||
|
control,
|
||||||
|
unchecked((uint)result.InputConsumed),
|
||||||
|
unchecked((uint)outputWritten));
|
||||||
|
WriteAudioDecoderInfo(ctx, control.BsiInfoAddress, decoder, result.Status);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(input);
|
||||||
|
ArrayPool<byte>.Shared.Return(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DecodeAudioSilence(CpuContext ctx, AudioDecoderState decoder, AudioControl control)
|
||||||
|
{
|
||||||
|
var wantedPcm = checked(
|
||||||
|
decoder.FrameSamples *
|
||||||
|
decoder.Channels *
|
||||||
|
GetAudioBytesPerSample(decoder.WordSize));
|
||||||
|
var outputSize = Math.Min(control.PcmSize, unchecked((uint)wantedPcm));
|
||||||
|
ClearGuestMemory(ctx, control.PcmAddress, outputSize);
|
||||||
|
WriteAudioBufferSizes(
|
||||||
|
ctx,
|
||||||
|
control,
|
||||||
|
Math.Min(control.AuSize, unchecked((uint)decoder.FrameBytes)),
|
||||||
|
outputSize);
|
||||||
|
WriteAudioDecoderInfo(ctx, control.BsiInfoAddress, decoder, Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteAudioBufferSizes(
|
||||||
|
CpuContext ctx,
|
||||||
|
AudioControl control,
|
||||||
|
uint inputConsumed,
|
||||||
|
uint outputWritten)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(value, inputConsumed);
|
||||||
|
_ = ctx.Memory.TryWrite(control.AuInfoAddress + 16, value);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(value, outputWritten);
|
||||||
|
_ = ctx.Memory.TryWrite(control.PcmItemAddress + 16, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteAudioDecoderInfo(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong infoAddress,
|
||||||
|
AudioDecoderState decoder,
|
||||||
|
int result)
|
||||||
|
{
|
||||||
|
if (decoder.CodecType == AudiodecTypeAt9)
|
||||||
|
{
|
||||||
|
Span<byte> info = stackalloc byte[36];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info, unchecked((uint)info.Length));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[4..], unchecked((uint)decoder.Channels));
|
||||||
|
var superframeSamples = checked(decoder.FrameSamples * decoder.FramesPerSuperframe);
|
||||||
|
var bitrate = superframeSamples == 0
|
||||||
|
? 0u
|
||||||
|
: unchecked((uint)((ulong)decoder.FrameBytes * 8 * (uint)decoder.SampleRate /
|
||||||
|
(uint)superframeSamples));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[8..], bitrate);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[12..], unchecked((uint)decoder.SampleRate));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[16..], unchecked((uint)decoder.FrameBytes));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[20..], unchecked((uint)decoder.FramesPerSuperframe));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
|
info[24..],
|
||||||
|
unchecked((uint)(decoder.FrameBytes / Math.Max(decoder.FramesPerSuperframe, 1))));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[28..], unchecked((uint)decoder.FrameSamples));
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(info[32..], result);
|
||||||
|
_ = ctx.Memory.TryWrite(infoAddress, info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoder.CodecType == AudiodecTypeMp3)
|
||||||
|
{
|
||||||
|
Span<byte> info = stackalloc byte[24];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info, unchecked((uint)info.Length));
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(info[20..], result);
|
||||||
|
_ = ctx.Memory.TryWrite(infoAddress, info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> aacInfo = stackalloc byte[20];
|
||||||
|
aacInfo.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(aacInfo, unchecked((uint)aacInfo.Length));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(aacInfo[4..], unchecked((uint)decoder.SampleRate));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(aacInfo[8..], unchecked((uint)decoder.Channels));
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(aacInfo[16..], result);
|
||||||
|
_ = ctx.Memory.TryWrite(infoAddress, aacInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Atrac9PcmEncoding GetAtrac9Encoding(int wordSize) =>
|
||||||
|
wordSize switch
|
||||||
|
{
|
||||||
|
0 => Atrac9PcmEncoding.Signed32,
|
||||||
|
1 => Atrac9PcmEncoding.Signed16,
|
||||||
|
2 => Atrac9PcmEncoding.Float,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(wordSize)),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static int GetAudioBytesPerSample(int wordSize) =>
|
||||||
|
wordSize == 1 ? sizeof(short) : sizeof(int);
|
||||||
|
|
||||||
|
private static int GetAacSampleRate(uint index)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<int> rates =
|
||||||
|
[
|
||||||
|
96_000, 88_200, 64_000, 48_000, 44_100, 32_000,
|
||||||
|
24_000, 22_050, 16_000, 12_000, 11_025, 8_000,
|
||||||
|
];
|
||||||
|
return index < rates.Length ? rates[unchecked((int)index)] : 48_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearGuestMemory(CpuContext ctx, ulong address, uint byteCount)
|
||||||
|
{
|
||||||
|
Span<byte> zero = stackalloc byte[256];
|
||||||
|
var cursor = address;
|
||||||
|
var remaining = byteCount;
|
||||||
|
while (remaining != 0)
|
||||||
|
{
|
||||||
|
var length = unchecked((int)Math.Min(remaining, (uint)zero.Length));
|
||||||
|
if (!ctx.Memory.TryWrite(cursor, zero[..length]))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor += unchecked((uint)length);
|
||||||
|
remaining -= unchecked((uint)length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ResetAudioDecodersForTests()
|
||||||
|
{
|
||||||
|
AudioDecoders.Clear();
|
||||||
|
AudioCodecInitCounts.Clear();
|
||||||
|
Interlocked.Exchange(ref _nextAudioHandle, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
|
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
|
||||||
ctx.TryWriteUInt64(address, handle);
|
ctx.TryWriteUInt64(address, handle);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public static class Ngs2Exports
|
|||||||
// The grain length defaults to 256 frames (matching the 8192-byte AudioOut
|
// The grain length defaults to 256 frames (matching the 8192-byte AudioOut
|
||||||
// buffers games copy it into) until the title overrides it.
|
// buffers games copy it into) until the title overrides it.
|
||||||
private const int DefaultGrainSamples = 256;
|
private const int DefaultGrainSamples = 256;
|
||||||
private const double OutputSampleRate = 48000.0;
|
private const int DefaultSampleRate = 48000;
|
||||||
|
|
||||||
private sealed class SystemState
|
private sealed class SystemState
|
||||||
{
|
{
|
||||||
@@ -38,6 +38,7 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
public uint Uid { get; }
|
public uint Uid { get; }
|
||||||
public int GrainSamples { get; set; } = DefaultGrainSamples;
|
public int GrainSamples { get; set; } = DefaultGrainSamples;
|
||||||
|
public int SampleRate { get; set; } = DefaultSampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record RackState(ulong SystemHandle, uint RackId);
|
private sealed record RackState(ulong SystemHandle, uint RackId);
|
||||||
@@ -496,6 +497,7 @@ public static class Ngs2Exports
|
|||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Span<byte> renderBufferInfo = stackalloc byte[RenderBufferInfoSize];
|
||||||
for (uint i = 0; i < bufferInfoCount; i++)
|
for (uint i = 0; i < bufferInfoCount; i++)
|
||||||
{
|
{
|
||||||
var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize);
|
var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize);
|
||||||
@@ -527,10 +529,9 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
if (ShouldTrace() && Interlocked.Increment(ref _renderInfoDumps) <= 4)
|
if (ShouldTrace() && Interlocked.Increment(ref _renderInfoDumps) <= 4)
|
||||||
{
|
{
|
||||||
Span<byte> rbi = stackalloc byte[RenderBufferInfoSize];
|
ctx.Memory.TryRead(entryAddress, renderBufferInfo);
|
||||||
ctx.Memory.TryRead(entryAddress, rbi);
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] ngs2.renderbufinfo addr=0x{bufferAddress:X} size={bufferSize} ch={channels} raw={Convert.ToHexString(rbi)}");
|
$"[LOADER][TRACE] ngs2.renderbufinfo addr=0x{bufferAddress:X} size={bufferSize} ch={channels} raw={Convert.ToHexString(renderBufferInfo)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,6 +553,7 @@ public static class Ngs2Exports
|
|||||||
CpuContext ctx, ulong systemHandle, ulong bufferAddress, ulong bufferSize, int channels)
|
CpuContext ctx, ulong systemHandle, ulong bufferAddress, ulong bufferSize, int channels)
|
||||||
{
|
{
|
||||||
int grain;
|
int grain;
|
||||||
|
int sampleRate;
|
||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
if (!Systems.TryGetValue(systemHandle, out var system))
|
if (!Systems.TryGetValue(systemHandle, out var system))
|
||||||
@@ -560,6 +562,7 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
|
|
||||||
grain = system.GrainSamples;
|
grain = system.GrainSamples;
|
||||||
|
sampleRate = system.SampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
var capacityFrames = (int)Math.Min((ulong)grain, bufferSize / (ulong)(channels * sizeof(float)));
|
var capacityFrames = (int)Math.Min((ulong)grain, bufferSize / (ulong)(channels * sizeof(float)));
|
||||||
@@ -590,7 +593,7 @@ public static class Ngs2Exports
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MixOneVoice(accum, capacityFrames, channels, voice);
|
MixOneVoice(accum, capacityFrames, channels, sampleRate, voice);
|
||||||
mixedAnything = true;
|
mixedAnything = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,15 +609,20 @@ public static class Ngs2Exports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resample one voice from its source rate to 48 kHz (nearest-sample) and add
|
// Resample one voice to the system rate and add
|
||||||
// it to the front stereo pair. Advances the voice cursor and handles loop /
|
// it to the front stereo pair. Advances the voice cursor and handles loop /
|
||||||
// one-shot end. Must be called under StateGate.
|
// one-shot end. Must be called under StateGate.
|
||||||
private static void MixOneVoice(float[] accum, int frames, int channels, VoiceState voice)
|
private static void MixOneVoice(
|
||||||
|
float[] accum,
|
||||||
|
int frames,
|
||||||
|
int channels,
|
||||||
|
int outputSampleRate,
|
||||||
|
VoiceState voice)
|
||||||
{
|
{
|
||||||
var pcm = voice.Pcm!;
|
var pcm = voice.Pcm!;
|
||||||
var loopEnd = voice.LoopEnd > 0 && voice.LoopEnd <= pcm.Length ? voice.LoopEnd : pcm.Length;
|
var loopEnd = voice.LoopEnd > 0 && voice.LoopEnd <= pcm.Length ? voice.LoopEnd : pcm.Length;
|
||||||
var loopStart = voice.LoopStart;
|
var loopStart = voice.LoopStart;
|
||||||
var step = voice.SourceRate / OutputSampleRate;
|
var step = voice.SourceRate / (double)outputSampleRate;
|
||||||
var gain = voice.Gain / 32768f;
|
var gain = voice.Gain / 32768f;
|
||||||
var pos = voice.Position;
|
var pos = voice.Position;
|
||||||
for (var f = 0; f < frames; f++)
|
for (var f = 0; f < frames; f++)
|
||||||
@@ -640,7 +648,14 @@ public static class Ngs2Exports
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sample = pcm[idx] * gain;
|
var next = idx + 1;
|
||||||
|
if (next >= loopEnd)
|
||||||
|
{
|
||||||
|
next = loopStart >= 0 && loopStart < loopEnd ? loopStart : idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fraction = pos - idx;
|
||||||
|
var sample = (float)((pcm[idx] + ((pcm[next] - pcm[idx]) * fraction)) * gain);
|
||||||
var baseIndex = f * channels;
|
var baseIndex = f * channels;
|
||||||
accum[baseIndex] += sample;
|
accum[baseIndex] += sample;
|
||||||
if (channels > 1)
|
if (channels > 1)
|
||||||
@@ -736,7 +751,27 @@ public static class Ngs2Exports
|
|||||||
ExportName = "sceNgs2SystemSetSampleRate",
|
ExportName = "sceNgs2SystemSetSampleRate",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2SystemSetSampleRate(CpuContext ctx) => ValidateSystem(ctx);
|
public static int Ngs2SystemSetSampleRate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var systemHandle = ctx[CpuRegister.Rdi];
|
||||||
|
var sampleRate = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
lock (StateGate)
|
||||||
|
{
|
||||||
|
if (!Systems.TryGetValue(systemHandle, out var system))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleRate is < 8000 or > 192000)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
system.SampleRate = sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "gThZqM5PYlQ",
|
Nid = "gThZqM5PYlQ",
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Acm;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
[CollectionDefinition("AcmState", DisableParallelization = true)]
|
||||||
|
public sealed class AcmStateCollection
|
||||||
|
{
|
||||||
|
public const string Name = "AcmState";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection(AcmStateCollection.Name)]
|
||||||
|
public sealed class AcmExportsTests : IDisposable
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_1000_0000;
|
||||||
|
private const ulong ContextAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong InfoArrayAddress = MemoryBase + 0x200;
|
||||||
|
private const ulong ErrorAddress = MemoryBase + 0x300;
|
||||||
|
private const ulong BatchAddress = MemoryBase + 0x400;
|
||||||
|
|
||||||
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
|
||||||
|
public AcmExportsTests()
|
||||||
|
{
|
||||||
|
AcmExports.ResetForTests();
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextAndBatchLifecycle_UsesFourByteHandlesAndClearsErrors()
|
||||||
|
{
|
||||||
|
Span<byte> contextSentinel = stackalloc byte[8];
|
||||||
|
contextSentinel.Fill(0xCC);
|
||||||
|
Assert.True(_memory.TryWrite(ContextAddress, contextSentinel));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ContextAddress;
|
||||||
|
Assert.Equal(0, AcmExports.AcmContextCreate(_ctx));
|
||||||
|
var context = ReadUInt32(ContextAddress);
|
||||||
|
Assert.Equal(1u, context);
|
||||||
|
Assert.Equal(0xCCCCCCCCu, ReadUInt32(ContextAddress + 4));
|
||||||
|
|
||||||
|
Span<byte> errorSentinel = stackalloc byte[32];
|
||||||
|
errorSentinel.Fill(0xCC);
|
||||||
|
Assert.True(_memory.TryWrite(ErrorAddress, errorSentinel));
|
||||||
|
WriteUInt64(InfoArrayAddress, MemoryBase + 0x500);
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
_ctx[CpuRegister.Rdx] = InfoArrayAddress;
|
||||||
|
_ctx[CpuRegister.Rcx] = ErrorAddress;
|
||||||
|
_ctx[CpuRegister.R8] = BatchAddress;
|
||||||
|
Assert.Equal(0, AcmExports.AcmBatchStartBuffers(_ctx));
|
||||||
|
Assert.Equal(1u, ReadUInt32(BatchAddress));
|
||||||
|
Assert.All(ReadBytes(ErrorAddress, 32), value => Assert.Equal(0, value));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = context;
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
_ctx[CpuRegister.Rdx] = 0;
|
||||||
|
Assert.Equal(0, AcmExports.AcmBatchWait(_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchStartBuffers_RejectsUnknownContextAndMissingOutputs()
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = 99;
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
_ctx[CpuRegister.Rdx] = InfoArrayAddress;
|
||||||
|
_ctx[CpuRegister.R8] = BatchAddress;
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||||
|
AcmExports.AcmBatchStartBuffers(_ctx));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = ContextAddress;
|
||||||
|
Assert.Equal(0, AcmExports.AcmContextCreate(_ctx));
|
||||||
|
_ctx[CpuRegister.Rdi] = ReadUInt32(ContextAddress);
|
||||||
|
_ctx[CpuRegister.Rsi] = 1;
|
||||||
|
_ctx[CpuRegister.Rdx] = 0;
|
||||||
|
_ctx[CpuRegister.R8] = BatchAddress;
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||||
|
AcmExports.AcmBatchStartBuffers(_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcmBatchExports_RegisterForBothGenerations()
|
||||||
|
{
|
||||||
|
foreach (var generation in new[] { Generation.Gen4, Generation.Gen5 })
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation));
|
||||||
|
|
||||||
|
Assert.True(manager.TryGetExport("8fe55ktlNVo", out var start));
|
||||||
|
Assert.Equal("sceAcmBatchStartBuffers", start.Name);
|
||||||
|
Assert.True(manager.TryGetExport("RLN3gRlXJLE", out var wait));
|
||||||
|
Assert.Equal("sceAcmBatchWait", wait.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
AcmExports.ResetForTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint ReadUInt32(ulong address)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||||
|
Assert.True(_memory.TryRead(address, value));
|
||||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadBytes(ulong address, int length)
|
||||||
|
{
|
||||||
|
var value = new byte[length];
|
||||||
|
Assert.True(_memory.TryRead(address, value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteUInt64(ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||||
|
Assert.True(_memory.TryWrite(address, bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
private const ulong MemoryBase = 0x1_0000_0000;
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
private const ulong ContextAddress = MemoryBase + 0x100;
|
private const ulong ContextAddress = MemoryBase + 0x100;
|
||||||
private const ulong InstanceAddress = MemoryBase + 0x200;
|
private const ulong InstanceAddress = MemoryBase + 0x200;
|
||||||
|
private const ulong BatchInfoAddress = MemoryBase + 0x300;
|
||||||
|
private const ulong StatisticsAddress = MemoryBase + 0x400;
|
||||||
|
private const ulong BatchBufferAddress = MemoryBase + 0x500;
|
||||||
|
|
||||||
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1000);
|
||||||
private readonly CpuContext _ctx;
|
private readonly CpuContext _ctx;
|
||||||
@@ -80,6 +83,171 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
Assert.Equal(InvalidContext, RegisterCodec(contextId + 1, 1));
|
Assert.Equal(InvalidContext, RegisterCodec(contextId + 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MemoryRegistration_TracksValidContextAndToleratesRepeatedUnregister()
|
||||||
|
{
|
||||||
|
var contextId = Initialize();
|
||||||
|
const ulong address = 0x4_D4E0_0000;
|
||||||
|
|
||||||
|
Assert.Equal(0, RegisterMemory(contextId, address, 4));
|
||||||
|
Assert.Equal(0, UnregisterMemory(contextId, address));
|
||||||
|
Assert.Equal(0, UnregisterMemory(contextId, address));
|
||||||
|
Assert.Equal(InvalidContext, RegisterMemory(contextId + 1, address, 4));
|
||||||
|
Assert.Equal(InvalidContext, UnregisterMemory(contextId + 1, address));
|
||||||
|
Assert.Equal(InvalidParameter, RegisterMemory(contextId, 0, 4));
|
||||||
|
Assert.Equal(InvalidParameter, RegisterMemory(contextId, address, 0));
|
||||||
|
Assert.Equal(InvalidParameter, UnregisterMemory(contextId, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchInitializeAndStatistics_WriteExpectedAbiStructures()
|
||||||
|
{
|
||||||
|
Span<byte> sentinel = stackalloc byte[48];
|
||||||
|
sentinel.Fill(0xCC);
|
||||||
|
Assert.True(_memory.TryWrite(StatisticsAddress, sentinel));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = BatchBufferAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 0x200;
|
||||||
|
_ctx[CpuRegister.Rdx] = BatchInfoAddress;
|
||||||
|
Assert.Equal(0, AjmExports.AjmBatchInitialize(_ctx));
|
||||||
|
|
||||||
|
Assert.Equal(BatchBufferAddress, ReadUInt64(BatchInfoAddress));
|
||||||
|
Assert.Equal(0ul, ReadUInt64(BatchInfoAddress + 8));
|
||||||
|
Assert.Equal(0x200ul, ReadUInt64(BatchInfoAddress + 16));
|
||||||
|
Assert.Equal(0ul, ReadUInt64(BatchInfoAddress + 24));
|
||||||
|
Assert.Equal(0ul, ReadUInt64(BatchInfoAddress + 32));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = BatchInfoAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = StatisticsAddress;
|
||||||
|
_ctx.SetXmmRegister(0, BitConverter.SingleToUInt32Bits(0.25f), 0);
|
||||||
|
Assert.Equal(0, AjmExports.AjmBatchJobGetStatistics(_ctx));
|
||||||
|
|
||||||
|
Assert.Equal(88ul, ReadUInt64(BatchInfoAddress + 8));
|
||||||
|
Assert.Equal(BatchBufferAddress, ReadUInt64(BatchInfoAddress + 24));
|
||||||
|
Assert.Equal(0ul, ReadUInt64(BatchInfoAddress + 32));
|
||||||
|
Assert.All(ReadBytes(StatisticsAddress, 48), value => Assert.Equal(0, value));
|
||||||
|
Assert.All(ReadBytes(BatchBufferAddress, 88), value => Assert.Equal(0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Demon's Souls creates ATRAC9 voices with low flag words 1, 2, 4 and 8 —
|
||||||
|
/// the channel counts of the streams they carry. Rejecting any of them left
|
||||||
|
/// the title holding SCE_AJM_INSTANCE_INVALID for that voice, so its 4- and
|
||||||
|
/// 8-channel movie stems played silence.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0x1_0000_0001ul)]
|
||||||
|
[InlineData(0x1_0000_0002ul)]
|
||||||
|
[InlineData(0x1_0000_0004ul)]
|
||||||
|
[InlineData(0x1_0000_0008ul)]
|
||||||
|
public void InstanceCreate_AcceptsEveryChannelCountFlagWord(ulong flags)
|
||||||
|
{
|
||||||
|
var contextId = Initialize();
|
||||||
|
Assert.Equal(0, RegisterCodec(contextId, 1));
|
||||||
|
|
||||||
|
Assert.Equal(0, CreateInstance(contextId, 1, flags, InstanceAddress));
|
||||||
|
Assert.Equal(0x4001u, ReadUInt32(InstanceAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// sceAjmBatchJobRunSplit gathers arrays of AjmBuffer descriptors rather than
|
||||||
|
/// a single pointer/size pair, and its sideband layout is positional: result,
|
||||||
|
/// then only the blocks the job flags asked for.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void BatchJobRunSplit_GathersDescriptorsAndWritesFlaggedSideband()
|
||||||
|
{
|
||||||
|
const ulong inputDescriptors = MemoryBase + 0x600;
|
||||||
|
const ulong outputDescriptors = MemoryBase + 0x640;
|
||||||
|
const ulong inputData = MemoryBase + 0x700;
|
||||||
|
const ulong outputData = MemoryBase + 0x800;
|
||||||
|
const ulong sideband = MemoryBase + 0x900;
|
||||||
|
const ulong streamSideband = 1ul << 47;
|
||||||
|
const ulong multipleFrames = 1ul << 12;
|
||||||
|
|
||||||
|
var contextId = Initialize();
|
||||||
|
Assert.Equal(0, RegisterCodec(contextId, 2));
|
||||||
|
Assert.Equal(0, CreateInstance(contextId, 2, 0x2_0000_0002, InstanceAddress));
|
||||||
|
var instanceId = ReadUInt32(InstanceAddress);
|
||||||
|
|
||||||
|
InitializeBatch(BatchBufferAddress, 0x200, BatchInfoAddress);
|
||||||
|
|
||||||
|
// Two input descriptors totalling 0x30 bytes, one output of 0x40.
|
||||||
|
WriteUInt64(inputDescriptors, inputData);
|
||||||
|
WriteUInt64(inputDescriptors + 8, 0x20);
|
||||||
|
WriteUInt64(inputDescriptors + 16, inputData + 0x20);
|
||||||
|
WriteUInt64(inputDescriptors + 24, 0x10);
|
||||||
|
WriteUInt64(outputDescriptors, outputData);
|
||||||
|
WriteUInt64(outputDescriptors + 8, 0x40);
|
||||||
|
|
||||||
|
Span<byte> dirty = stackalloc byte[0x40];
|
||||||
|
dirty.Fill(0xAB);
|
||||||
|
Assert.True(_memory.TryWrite(outputData, dirty));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = BatchInfoAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = instanceId;
|
||||||
|
_ctx[CpuRegister.Rdx] = streamSideband | multipleFrames;
|
||||||
|
_ctx[CpuRegister.Rcx] = inputDescriptors;
|
||||||
|
_ctx[CpuRegister.R8] = 2;
|
||||||
|
_ctx[CpuRegister.R9] = outputDescriptors;
|
||||||
|
WriteStackArgs(outputCount: 1, sidebandAddress: sideband, sidebandSize: 0x20);
|
||||||
|
|
||||||
|
Assert.Equal(0, AjmExports.AjmBatchJobRunSplit(_ctx));
|
||||||
|
|
||||||
|
// A non-ATRAC9 instance stays a silence stub: the output is cleared and
|
||||||
|
// the whole gathered input is reported consumed.
|
||||||
|
Assert.All(ReadBytes(outputData, 0x40), value => Assert.Equal(0, value));
|
||||||
|
|
||||||
|
var result = ReadBytes(sideband, 0x20);
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(result)); // AjmSidebandResult
|
||||||
|
Assert.Equal(0x30, BinaryPrimitives.ReadInt32LittleEndian(result.AsSpan(8))); // stream.input_consumed
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(result.AsSpan(12))); // stream.output_written
|
||||||
|
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(result.AsSpan(24))); // mframe.num_frames
|
||||||
|
|
||||||
|
// The batch cursor advanced past the job plus its descriptors.
|
||||||
|
Assert.True(ReadUInt64(BatchInfoAddress + 8) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchJobRunSplit_OmitsSidebandBlocksTheJobFlagsDidNotRequest()
|
||||||
|
{
|
||||||
|
const ulong inputDescriptors = MemoryBase + 0x600;
|
||||||
|
const ulong outputDescriptors = MemoryBase + 0x640;
|
||||||
|
const ulong inputData = MemoryBase + 0x700;
|
||||||
|
const ulong outputData = MemoryBase + 0x800;
|
||||||
|
const ulong sideband = MemoryBase + 0x900;
|
||||||
|
|
||||||
|
var contextId = Initialize();
|
||||||
|
Assert.Equal(0, RegisterCodec(contextId, 2));
|
||||||
|
Assert.Equal(0, CreateInstance(contextId, 2, 0x2_0000_0002, InstanceAddress));
|
||||||
|
var instanceId = ReadUInt32(InstanceAddress);
|
||||||
|
|
||||||
|
InitializeBatch(BatchBufferAddress, 0x200, BatchInfoAddress);
|
||||||
|
WriteUInt64(inputDescriptors, inputData);
|
||||||
|
WriteUInt64(inputDescriptors + 8, 0x20);
|
||||||
|
WriteUInt64(outputDescriptors, outputData);
|
||||||
|
WriteUInt64(outputDescriptors + 8, 0x40);
|
||||||
|
|
||||||
|
Span<byte> sentinel = stackalloc byte[0x20];
|
||||||
|
sentinel.Fill(0x5A);
|
||||||
|
Assert.True(_memory.TryWrite(sideband, sentinel));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = BatchInfoAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = instanceId;
|
||||||
|
_ctx[CpuRegister.Rdx] = 0; // No stream sideband, no multiple-frames.
|
||||||
|
_ctx[CpuRegister.Rcx] = inputDescriptors;
|
||||||
|
_ctx[CpuRegister.R8] = 1;
|
||||||
|
_ctx[CpuRegister.R9] = outputDescriptors;
|
||||||
|
WriteStackArgs(outputCount: 1, sidebandAddress: sideband, sidebandSize: 0x20);
|
||||||
|
|
||||||
|
Assert.Equal(0, AjmExports.AjmBatchJobRunSplit(_ctx));
|
||||||
|
|
||||||
|
// Only the 8-byte result block is written; the rest keeps its sentinel.
|
||||||
|
var written = ReadBytes(sideband, 0x20);
|
||||||
|
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(written));
|
||||||
|
Assert.All(written.AsSpan(8).ToArray(), value => Assert.Equal(0x5A, value));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(23u)]
|
[InlineData(23u)]
|
||||||
[InlineData(24u)]
|
[InlineData(24u)]
|
||||||
@@ -155,6 +323,12 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
Assert.Equal("sceAjmInstanceCreate", create.Name);
|
Assert.Equal("sceAjmInstanceCreate", create.Name);
|
||||||
Assert.True(manager.TryGetExport("RbLbuKv8zho", out var destroy));
|
Assert.True(manager.TryGetExport("RbLbuKv8zho", out var destroy));
|
||||||
Assert.Equal("sceAjmInstanceDestroy", destroy.Name);
|
Assert.Equal("sceAjmInstanceDestroy", destroy.Name);
|
||||||
|
Assert.True(manager.TryGetExport("bkRHEYG6lEM", out var memoryRegister));
|
||||||
|
Assert.Equal("sceAjmMemoryRegister", memoryRegister.Name);
|
||||||
|
Assert.True(manager.TryGetExport("pIpGiaYkHkM", out var memoryUnregister));
|
||||||
|
Assert.Equal("sceAjmMemoryUnregister", memoryUnregister.Name);
|
||||||
|
Assert.True(manager.TryGetExport("3cAg7xN995U", out var statistics));
|
||||||
|
Assert.Equal("sceAjmBatchJobGetStatistics", statistics.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +353,21 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
return AjmExports.AjmModuleRegister(_ctx);
|
return AjmExports.AjmModuleRegister(_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int RegisterMemory(uint contextId, ulong address, ulong pages)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = contextId;
|
||||||
|
_ctx[CpuRegister.Rsi] = address;
|
||||||
|
_ctx[CpuRegister.Rdx] = pages;
|
||||||
|
return AjmExports.AjmMemoryRegister(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int UnregisterMemory(uint contextId, ulong address)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = contextId;
|
||||||
|
_ctx[CpuRegister.Rsi] = address;
|
||||||
|
return AjmExports.AjmMemoryUnregister(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
private int CreateInstance(uint contextId, uint codecType, ulong flags, ulong outputAddress)
|
private int CreateInstance(uint contextId, uint codecType, ulong flags, ulong outputAddress)
|
||||||
{
|
{
|
||||||
_ctx[CpuRegister.Rdi] = contextId;
|
_ctx[CpuRegister.Rdi] = contextId;
|
||||||
@@ -202,6 +391,48 @@ public sealed class AjmExportsTests : IDisposable
|
|||||||
return BinaryPrimitives.ReadUInt32LittleEndian(value);
|
return BinaryPrimitives.ReadUInt32LittleEndian(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeBatch(ulong bufferAddress, ulong bufferSize, ulong infoAddress)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = bufferAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = bufferSize;
|
||||||
|
_ctx[CpuRegister.Rdx] = infoAddress;
|
||||||
|
Assert.Equal(0, AjmExports.AjmBatchInitialize(_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places the seventh, eighth and ninth SysV arguments where the export reads
|
||||||
|
/// them: just past the return address slot at [rsp].
|
||||||
|
/// </summary>
|
||||||
|
private void WriteStackArgs(ulong outputCount, ulong sidebandAddress, ulong sidebandSize)
|
||||||
|
{
|
||||||
|
const ulong stackAddress = MemoryBase + 0xA00;
|
||||||
|
_ctx[CpuRegister.Rsp] = stackAddress;
|
||||||
|
WriteUInt64(stackAddress + 8, outputCount);
|
||||||
|
WriteUInt64(stackAddress + 16, sidebandAddress);
|
||||||
|
WriteUInt64(stackAddress + 24, sidebandSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteUInt64(ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||||
|
Assert.True(_memory.TryWrite(address, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong ReadUInt64(ulong address)
|
||||||
|
{
|
||||||
|
Span<byte> value = stackalloc byte[sizeof(ulong)];
|
||||||
|
Assert.True(_memory.TryRead(address, value));
|
||||||
|
return BinaryPrimitives.ReadUInt64LittleEndian(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadBytes(ulong address, int length)
|
||||||
|
{
|
||||||
|
var value = new byte[length];
|
||||||
|
Assert.True(_memory.TryRead(address, value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private void WriteUInt32(ulong address, uint value)
|
private void WriteUInt32(ulong address, uint value)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
|||||||
Reference in New Issue
Block a user