diff --git a/Fmod5Sharp/CodecRebuilders/FmodFadPcmRebuilder.cs b/Fmod5Sharp/CodecRebuilders/FmodFadPcmRebuilder.cs new file mode 100644 index 0000000..2e35c50 --- /dev/null +++ b/Fmod5Sharp/CodecRebuilders/FmodFadPcmRebuilder.cs @@ -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 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 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 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) + 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(); + } +} \ No newline at end of file diff --git a/Fmod5Sharp/FmodTypes/FmodAudioType.cs b/Fmod5Sharp/FmodTypes/FmodAudioType.cs index b8143a2..8f4160a 100644 --- a/Fmod5Sharp/FmodTypes/FmodAudioType.cs +++ b/Fmod5Sharp/FmodTypes/FmodAudioType.cs @@ -18,5 +18,7 @@ public enum FmodAudioType : uint AT9 = 13, XWMA = 14, VORBIS = 15, + FADPCM = 16, + OPUS = 17 } } \ No newline at end of file diff --git a/Fmod5Sharp/FmodTypes/FmodSample.cs b/Fmod5Sharp/FmodTypes/FmodSample.cs index 98c4eba..939ca13 100644 --- a/Fmod5Sharp/FmodTypes/FmodSample.cs +++ b/Fmod5Sharp/FmodTypes/FmodSample.cs @@ -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; diff --git a/Fmod5Sharp/Util/FmodAudioTypeExtensions.cs b/Fmod5Sharp/Util/FmodAudioTypeExtensions.cs index 88fda17..7ca7371 100644 --- a/Fmod5Sharp/Util/FmodAudioTypeExtensions.cs +++ b/Fmod5Sharp/Util/FmodAudioTypeExtensions.cs @@ -13,6 +13,7 @@ public static bool IsSupported(this FmodAudioType @this) => FmodAudioType.PCM32 => true, FmodAudioType.GCADPCM => true, FmodAudioType.IMAADPCM => true, + FmodAudioType.FADPCM => true, _ => false }; @@ -25,6 +26,7 @@ public static bool IsSupported(this FmodAudioType @this) => FmodAudioType.PCM32 => "wav", FmodAudioType.GCADPCM => "wav", FmodAudioType.IMAADPCM => "wav", + FmodAudioType.FADPCM => "wav", _ => null }; } diff --git a/Fmod5Sharp/Util/Utils.cs b/Fmod5Sharp/Util/Utils.cs index eeccec5..30639e5 100644 --- a/Fmod5Sharp/Util/Utils.cs +++ b/Fmod5Sharp/Util/Utils.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; namespace Fmod5Sharp.Util { @@ -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)] + internal static short ClampToShort(int value) + { + if (value < short.MinValue) return short.MinValue; + if (value > short.MaxValue) return short.MaxValue; + return (short)value; + } } } \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..760e0b1 --- /dev/null +++ b/NOTICE @@ -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 +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. + +--------------------------------------------------------------------------- \ No newline at end of file