Skip to content
Open
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
106 changes: 106 additions & 0 deletions Fmod5Sharp/CodecRebuilders/FmodFadPcmRebuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Fmod5Sharp.FmodTypes;
using Fmod5Sharp.Util;
using NAudio.Wave;
using System;
using System.Buffers.Binary;
using System.IO;

namespace Fmod5Sharp.CodecRebuilders;

// Credits: https://github.com/vgmstream/vgmstream/blob/master/src/coding/fadpcm_decoder.c
public class FmodFadPcmRebuilder
{
private static readonly short[,] FadpcmCoefs = {
{ 0, 0 },
{ 60, 0 },
{ 122, 60 },
{ 115, 52 },
{ 98, 55 },
{ 0, 0 },
{ 0, 0 },
{ 0, 0 }
};

public static short[] DecodeFadpcm(FmodSample sample)
{
const int FrameSize = 0x8C;
const int SamplesPerFrame = (FrameSize - 0x0C) * 2;

ReadOnlySpan<byte> sampleBytes = sample.SampleBytes;
int numChannels = sample.Metadata.NumChannels;
int totalFrames = sampleBytes.Length / FrameSize;

// Total samples across all channels
short[] outputBuffer = new short[totalFrames * SamplesPerFrame];
Span<short> outputSpan = outputBuffer;

int[] hist1 = new int[numChannels];
int[] hist2 = new int[numChannels];
for (int f = 0; f < totalFrames; f++)
{
int channel = f % numChannels;
int frameOffset = f * FrameSize;

ReadOnlySpan<byte> frameSpan = sampleBytes.Slice(frameOffset, FrameSize);

// Parse Header
uint coefsLookup = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[..4]);
uint shiftsLookup = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[0x04..]);
hist1[channel] = BinaryPrimitives.ReadInt16LittleEndian(frameSpan[0x08..]);
hist2[channel] = BinaryPrimitives.ReadInt16LittleEndian(frameSpan[0x0A..]);

int frameIndexInChannel = f / numChannels;
int frameBaseOutIndex = (frameIndexInChannel * SamplesPerFrame * numChannels) + channel;

// Decode nibbles, grouped in 8 sets of 0x10 * 0x04 * 2
for (int i = 0; i < 8; i++)
{
// Each set has its own coefs/shifts (indexes > 7 are repeat, ex. 0x9 is 0x2)
int index = (int)((coefsLookup >> (i * 4)) & 0x0F) % 0x07;
int shift = (int)((shiftsLookup >> (i * 4)) & 0x0F);

int coef1 = FadpcmCoefs[index, 0];
int coef2 = FadpcmCoefs[index, 1];
int finalShift = 22 - shift; // Pre-adjust for 32b sign extend

for (int j = 0; j < 4; j++)
{
uint nibbles = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[(0x0C + (0x10 * i) + (0x04 * j))..]);

for (int k = 0; k < 8; k++)
{
int sampleValue = (int)((nibbles >> (k * 4)) & 0x0F);
sampleValue = (sampleValue << 28) >> finalShift; // 32b sign extend + scale
sampleValue = (sampleValue - (hist2[channel] * coef2) + (hist1[channel] * coef1)) >> 6;

short finalSample = Utils.ClampToShort(sampleValue);

int outIndex = frameBaseOutIndex + ((i * 32 + j * 8 + k) * numChannels);

if (outIndex < outputSpan.Length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever not true?

outputSpan[outIndex] = finalSample;

hist2[channel] = hist1[channel];
hist1[channel] = finalSample;
}
}
}
}

return outputBuffer;
}

public static byte[] Rebuild(FmodSample sample)
{
var format = new WaveFormat(sample.Metadata.Frequency, 16, sample.Metadata.NumChannels);

using var stream = new MemoryStream();
using (var writer = new WaveFileWriter(stream, format))
{
short[] pcmSamples = DecodeFadpcm(sample);
writer.WriteSamples(pcmSamples, 0, pcmSamples.Length);
}

return stream.ToArray();
}
}
2 changes: 2 additions & 0 deletions Fmod5Sharp/FmodTypes/FmodAudioType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ public enum FmodAudioType : uint
AT9 = 13,
XWMA = 14,
VORBIS = 15,
FADPCM = 16,
OPUS = 17
}
}
4 changes: 4 additions & 0 deletions Fmod5Sharp/FmodTypes/FmodSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public bool RebuildAsStandardFileFormat(out byte[]? data, out string? fileExtens
data = FmodImaAdPcmRebuilder.Rebuild(this);
fileExtension = "wav";
return data.Length > 0;
case FmodAudioType.FADPCM:
data = FmodFadPcmRebuilder.Rebuild(this);
fileExtension = "wav";
return data.Length > 0;
default:
data = null;
fileExtension = null;
Expand Down
2 changes: 2 additions & 0 deletions Fmod5Sharp/Util/FmodAudioTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
FmodAudioType.PCM32 => true,
FmodAudioType.GCADPCM => true,
FmodAudioType.IMAADPCM => true,
FmodAudioType.FADPCM => true,
_ => false
};

Expand All @@ -25,6 +26,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
FmodAudioType.PCM32 => "wav",
FmodAudioType.GCADPCM => "wav",
FmodAudioType.IMAADPCM => "wav",
FmodAudioType.FADPCM => "wav",
_ => null
};
}
Expand Down
9 changes: 9 additions & 0 deletions Fmod5Sharp/Util/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;

namespace Fmod5Sharp.Util
{
Expand All @@ -8,5 +9,13 @@ internal static class Utils
internal static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
internal static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
internal static short Clamp(short val, short min, short max) => Math.Max(Math.Min(val, max), min);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .NET runtime team recommends you never do this unless you benchmark to verify that it actually improves performance.

internal static short ClampToShort(int value)
{
if (value < short.MinValue) return short.MinValue;
if (value > short.MaxValue) return short.MaxValue;
return (short)value;
}
}
}
32 changes: 32 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
This library includes material developed by third-party libraries.

The attached notices are provided for information only.

1. License notice for vgmstream (https://github.com/vgmstream/vgmstream/blob/master/COPYING)
---------------------------------------------------------------------------

Copyright (c) 2008-2025 Adam Gashlin, Fastelbja, Ronny Elfert, bnnm,
Christopher Snowhill, NicknineTheEagle, bxaimc,
Thealexbarney, CyberBotX, et al

Portions Copyright (c) 2004-2008, Marko Kreen
Portions Copyright 2001-2007 jagarl / Kazunori Ueno <[email protected]>
Portions Copyright (c) 1998, Justin Frankel/Nullsoft Inc.
Portions Copyright (C) 2006 Nullsoft, Inc.
Portions Copyright (c) 2005-2007 Paul Hsieh
Portions Copyright (C) 2000-2004 Leshade Entis, Entis-soft.
Portions Public Domain originating with Sun Microsystems

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

---------------------------------------------------------------------------