Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
indent_style = space
indent_size = 4

[*.{yml,yaml,json,md,props,csproj}]
indent_style = space
indent_size = 2
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ jobs:

- name: Restore dependencies
run: dotnet restore


- name: Test
run: dotnet test -c Release --no-restore

- name: Publish BydTools.CLI
run: dotnet publish BydTools.CLI/BydTools.CLI.csproj -c Release -r win-x64 -o ./publish
run: dotnet publish BydTools.CLI/BydTools.CLI.csproj -c Release -r win-x64 -o ./publish -p:EnableAotAnalyzer=true -p:EnableTrimAnalyzer=true

- name: Clean up unnecessary files
run: |
Expand Down
57 changes: 57 additions & 0 deletions BydTools.Audio/Bnk/BnkParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Buffers.Binary;
using System.Text;

namespace BydTools.Audio.Bnk;

public readonly record struct BnkWem(uint Id, int Offset, int Size);

public static class BnkParser
{
public static List<BnkWem> Parse(ReadOnlySpan<byte> data)
{
int dataOffset = 0;
var entries = new List<BnkWem>();
int pos = 0;
while (pos + 8 <= data.Length)
{
string sign = Encoding.ASCII.GetString(data.Slice(pos, 4));
uint sectionSize = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(pos + 4, 4));
int sectionStart = pos + 8;
if (sectionSize > data.Length - sectionStart)
throw new InvalidDataException($"BNK section {sign} exceeds the file.");

if (sign == "DIDX")
{
int count = (int)(sectionSize / 12);
for (int i = 0; i < count; i++)
{
int at = sectionStart + i * 12;
uint id = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(at, 4));
uint offset = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(at + 4, 4));
uint size = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(at + 8, 4));
entries.Add(new BnkWem(id, (int)offset, (int)size));
}
}
else if (sign == "DATA")
{
dataOffset = sectionStart;
}

pos = sectionStart + (int)sectionSize;
}

if (dataOffset == 0 || entries.Count == 0)
return [];

for (int i = 0; i < entries.Count; i++)
{
var entry = entries[i];
int absolute = entry.Offset + dataOffset;
if (entry.Size < 0 || absolute < 0 || absolute > data.Length - entry.Size)
throw new InvalidDataException($"BNK WEM {entry.Id} is outside the DATA section.");
entries[i] = entry with { Offset = absolute };
}

return entries;
}
}
10 changes: 10 additions & 0 deletions BydTools.Audio/BydTools.Audio.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BydTools.Core\BydTools.Core.csproj" />
<InternalsVisibleTo Include="BydTools.Tests" />
<Content Include="3rdParty\*.dll" Link="%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>
90 changes: 90 additions & 0 deletions BydTools.Audio/Interop/LibVgmstream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Runtime.InteropServices;

namespace BydTools.Audio.Interop;

internal static partial class LibVgmstream
{
private const string DllName = "libvgmstream";

[LibraryImport(DllName, EntryPoint = "libvgmstream_get_version")]
public static partial uint GetVersion();

[LibraryImport(DllName, EntryPoint = "libvgmstream_init")]
public static partial nint Init();

[LibraryImport(DllName, EntryPoint = "libvgmstream_free")]
public static partial void Free(nint lib);

[LibraryImport(DllName, EntryPoint = "libvgmstream_setup")]
public static partial void Setup(nint lib, ref NativeConfig cfg);

[LibraryImport(DllName, EntryPoint = "libvgmstream_open_stream")]
public static partial int OpenStream(nint lib, nint libsf, int subsong);

[LibraryImport(DllName, EntryPoint = "libvgmstream_render")]
public static partial int Render(nint lib);

public enum SampleFormat : int
{
Pcm16 = 1,
Pcm24 = 2,
Pcm32 = 3,
Float = 4,
}

/// <summary>Matches libvgmstream_config_t from API 1.x.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct NativeConfig
{
public byte disable_config_override;
public byte allow_play_forever;
public byte play_forever;
public byte ignore_loop;
public byte force_loop;
public byte really_force_loop;
public byte ignore_fade;
private byte _pad0;
public double loop_count;
public double fade_time;
public double fade_delay;
public int stereo_track;
public int auto_downmix_channels;
public int force_sfmt;
}

/// <summary>Leading fields of libvgmstream_t. Later fields are not read.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct NativeHandle
{
public nint Priv;
public nint Format;
public nint Decoder;
}

/// <summary>
/// Leading fields of libvgmstream_format_t. stream_samples is int64 at offset 32.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct NativeFormat
{
public int Channels;
public int SampleRate;
public int SampleFormat;
public int SampleSize;
public uint ChannelLayout;
public int SubsongIndex;
public int SubsongCount;
public int InputChannels;
public long StreamSamples;
}

/// <summary>Leading fields of libvgmstream_decoder_t.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct NativeDecoder
{
public nint Buffer;
public int BufferSamples;
public int BufferBytes;
public byte Done;
}
}
167 changes: 167 additions & 0 deletions BydTools.Audio/Interop/MemoryStreamfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace BydTools.Audio.Interop;

/// <summary>
/// In-memory libstreamfile_t (vgmstream API 1.x). The name ends in .wem because
/// vgmstream picks the format from the extension. <c>open</c> returns another
/// view of the same buffer, which is what vgmstream does when it reopens a file.
/// </summary>
internal sealed unsafe class MemoryStreamfile : IDisposable
{
private State? _state;
private LibStreamfile* _native;
private int _disposed;

private MemoryStreamfile(State state, LibStreamfile* native)
{
_state = state;
_native = native;
}

internal nint Native => (nint)_native;

public static MemoryStreamfile Create(ReadOnlyMemory<byte> data)
{
if (!MemoryMarshal.TryGetArray(data, out ArraySegment<byte> segment) || segment.Array is null)
throw new NotSupportedException("WEM data must be backed by a byte array.");

var state = new State(segment.Array, segment.Offset, segment.Count);
return new MemoryStreamfile(state, Allocate(state));
}

internal int ReadAt(long offset, Span<byte> destination)
{
var state = _state ?? throw new ObjectDisposedException(nameof(MemoryStreamfile));
if (offset < 0 || offset >= state.Length || destination.IsEmpty)
return 0;
int count = (int)Math.Min(destination.Length, state.Length - offset);
state.Bytes.AsSpan(state.Offset + (int)offset, count).CopyTo(destination);
return count;
}

public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_state = null;
if (_native != null)
{
Release(_native);
_native = null;
}
}

private static LibStreamfile* Allocate(State state)
{
var native = (LibStreamfile*)NativeMemory.AllocZeroed((nuint)sizeof(LibStreamfile));
native->UserData = GCHandle.ToIntPtr(state.Self);
native->Read = &Read;
native->GetSize = &GetSize;
native->GetName = &GetName;
native->Open = &Open;
native->Close = &Close;
return native;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static int Read(nint user, byte* dst, long offset, int length)
{
var state = State.From(user);
if (state == null || offset < 0 || length <= 0 || offset >= state.Length)
return 0;
int count = (int)Math.Min(length, state.Length - offset);
fixed (byte* src = state.Bytes)
{
Buffer.MemoryCopy(src + state.Offset + offset, dst, count, count);
}

return count;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static long GetSize(nint user)
{
var state = State.From(user);
return state?.Length ?? 0;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static byte* GetName(nint user)
{
var state = State.From(user);
return state == null ? null : state.Name;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static LibStreamfile* Open(nint user, byte* filename)
{
var state = State.From(user);
if (state == null)
return null;
var child = new State(state.Bytes, state.Offset, state.Length);
return Allocate(child);
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void Close(LibStreamfile* streamfile) => Release(streamfile);

private static void Release(LibStreamfile* streamfile)
{
if (streamfile == null)
return;
var handle = GCHandle.FromIntPtr(streamfile->UserData);
if (handle.Target is State state)
state.Release();
handle.Free();
NativeMemory.Free(streamfile);
}

private sealed class State
{
public State(byte[] bytes, int offset, int length)
{
Bytes = bytes;
Offset = offset;
Length = length;
Self = GCHandle.Alloc(this);
Name = (byte*)NativeMemory.Alloc(10);
"audio.wem\0"u8.CopyTo(new Span<byte>(Name, 10));
}

public byte[] Bytes { get; }
public int Offset { get; }
public int Length { get; }
public GCHandle Self { get; }
public byte* Name { get; private set; }

public static State? From(nint user)
{
if (user == 0)
return null;
var handle = GCHandle.FromIntPtr(user);
return handle.Target as State;
}

public void Release()
{
if (Name != null)
{
NativeMemory.Free(Name);
Name = null;
}
}
}
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct LibStreamfile
{
public nint UserData;
public delegate* unmanaged[Cdecl]<nint, byte*, long, int, int> Read;
public delegate* unmanaged[Cdecl]<nint, long> GetSize;
public delegate* unmanaged[Cdecl]<nint, byte*> GetName;
public delegate* unmanaged[Cdecl]<nint, byte*, LibStreamfile*> Open;
public delegate* unmanaged[Cdecl]<LibStreamfile*, void> Close;
}
Loading