Skip to content

Commit e860b35

Browse files
FADPCM decoder (#15)
* FADPCM decoder * Very slight perf improvement Co-Authored-By: LongerWarrior <37636768+longerwarrior@users.noreply.github.com> --------- Co-authored-by: LongerWarrior <37636768+longerwarrior@users.noreply.github.com>
1 parent fde4619 commit e860b35

File tree

6 files changed

+155
-0
lines changed

6 files changed

+155
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Fmod5Sharp.FmodTypes;
2+
using Fmod5Sharp.Util;
3+
using NAudio.Wave;
4+
using System;
5+
using System.Buffers.Binary;
6+
using System.IO;
7+
8+
namespace Fmod5Sharp.CodecRebuilders;
9+
10+
// Credits: https://github.com/vgmstream/vgmstream/blob/master/src/coding/fadpcm_decoder.c
11+
public class FmodFadPcmRebuilder
12+
{
13+
private static readonly short[,] FadpcmCoefs = {
14+
{ 0, 0 },
15+
{ 60, 0 },
16+
{ 122, 60 },
17+
{ 115, 52 },
18+
{ 98, 55 },
19+
{ 0, 0 },
20+
{ 0, 0 },
21+
{ 0, 0 }
22+
};
23+
24+
public static short[] DecodeFadpcm(FmodSample sample)
25+
{
26+
const int FrameSize = 0x8C;
27+
const int SamplesPerFrame = (FrameSize - 0x0C) * 2;
28+
29+
ReadOnlySpan<byte> sampleBytes = sample.SampleBytes;
30+
int numChannels = sample.Metadata.NumChannels;
31+
int totalFrames = sampleBytes.Length / FrameSize;
32+
33+
// Total samples across all channels
34+
short[] outputBuffer = new short[totalFrames * SamplesPerFrame];
35+
Span<short> outputSpan = outputBuffer;
36+
37+
int[] hist1 = new int[numChannels];
38+
int[] hist2 = new int[numChannels];
39+
for (int f = 0; f < totalFrames; f++)
40+
{
41+
int channel = f % numChannels;
42+
int frameOffset = f * FrameSize;
43+
44+
ReadOnlySpan<byte> frameSpan = sampleBytes.Slice(frameOffset, FrameSize);
45+
46+
// Parse Header
47+
uint coefsLookup = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[..4]);
48+
uint shiftsLookup = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[0x04..]);
49+
hist1[channel] = BinaryPrimitives.ReadInt16LittleEndian(frameSpan[0x08..]);
50+
hist2[channel] = BinaryPrimitives.ReadInt16LittleEndian(frameSpan[0x0A..]);
51+
52+
int frameIndexInChannel = f / numChannels;
53+
int frameBaseOutIndex = (frameIndexInChannel * SamplesPerFrame * numChannels) + channel;
54+
55+
// Decode nibbles, grouped in 8 sets of 0x10 * 0x04 * 2
56+
for (int i = 0; i < 8; i++)
57+
{
58+
// Each set has its own coefs/shifts (indexes > 7 are repeat, ex. 0x9 is 0x2)
59+
int index = (int)((coefsLookup >> (i * 4)) & 0x0F) % 0x07;
60+
int shift = (int)((shiftsLookup >> (i * 4)) & 0x0F);
61+
62+
int coef1 = FadpcmCoefs[index, 0];
63+
int coef2 = FadpcmCoefs[index, 1];
64+
int finalShift = 22 - shift; // Pre-adjust for 32b sign extend
65+
66+
for (int j = 0; j < 4; j++)
67+
{
68+
uint nibbles = BinaryPrimitives.ReadUInt32LittleEndian(frameSpan[(0x0C + (0x10 * i) + (0x04 * j))..]);
69+
70+
for (int k = 0; k < 8; k++)
71+
{
72+
int sampleValue = (int)((nibbles >> (k * 4)) & 0x0F);
73+
sampleValue = (sampleValue << 28) >> finalShift; // 32b sign extend + scale
74+
sampleValue = (sampleValue - (hist2[channel] * coef2) + (hist1[channel] * coef1)) >> 6;
75+
76+
short finalSample = Utils.ClampToShort(sampleValue);
77+
78+
int outIndex = frameBaseOutIndex + ((i * 32 + j * 8 + k) * numChannels);
79+
80+
if (outIndex < outputSpan.Length)
81+
outputSpan[outIndex] = finalSample;
82+
83+
hist2[channel] = hist1[channel];
84+
hist1[channel] = finalSample;
85+
}
86+
}
87+
}
88+
}
89+
90+
return outputBuffer;
91+
}
92+
93+
public static byte[] Rebuild(FmodSample sample)
94+
{
95+
var format = new WaveFormat(sample.Metadata.Frequency, 16, sample.Metadata.NumChannels);
96+
97+
using var stream = new MemoryStream();
98+
using (var writer = new WaveFileWriter(stream, format))
99+
{
100+
short[] pcmSamples = DecodeFadpcm(sample);
101+
writer.WriteSamples(pcmSamples, 0, pcmSamples.Length);
102+
}
103+
104+
return stream.ToArray();
105+
}
106+
}

Fmod5Sharp/FmodTypes/FmodAudioType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ public enum FmodAudioType : uint
1818
AT9 = 13,
1919
XWMA = 14,
2020
VORBIS = 15,
21+
FADPCM = 16,
22+
OPUS = 17
2123
}
2224
}

Fmod5Sharp/FmodTypes/FmodSample.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ public bool RebuildAsStandardFileFormat(out byte[]? data, out string? fileExtens
4242
data = FmodImaAdPcmRebuilder.Rebuild(this);
4343
fileExtension = "wav";
4444
return data.Length > 0;
45+
case FmodAudioType.FADPCM:
46+
data = FmodFadPcmRebuilder.Rebuild(this);
47+
fileExtension = "wav";
48+
return data.Length > 0;
4549
default:
4650
data = null;
4751
fileExtension = null;

Fmod5Sharp/Util/FmodAudioTypeExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
1313
FmodAudioType.PCM32 => true,
1414
FmodAudioType.GCADPCM => true,
1515
FmodAudioType.IMAADPCM => true,
16+
FmodAudioType.FADPCM => true,
1617
_ => false
1718
};
1819

@@ -25,6 +26,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
2526
FmodAudioType.PCM32 => "wav",
2627
FmodAudioType.GCADPCM => "wav",
2728
FmodAudioType.IMAADPCM => "wav",
29+
FmodAudioType.FADPCM => "wav",
2830
_ => null
2931
};
3032
}

Fmod5Sharp/Util/Utils.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Runtime.CompilerServices;
23

34
namespace Fmod5Sharp.Util
45
{
@@ -8,5 +9,13 @@ internal static class Utils
89
internal static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
910
internal static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
1011
internal static short Clamp(short val, short min, short max) => Math.Max(Math.Min(val, max), min);
12+
13+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
14+
internal static short ClampToShort(int value)
15+
{
16+
if (value < short.MinValue) return short.MinValue;
17+
if (value > short.MaxValue) return short.MaxValue;
18+
return (short)value;
19+
}
1120
}
1221
}

NOTICE

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
This library includes material developed by third-party libraries.
2+
3+
The attached notices are provided for information only.
4+
5+
1. License notice for vgmstream (https://github.com/vgmstream/vgmstream/blob/master/COPYING)
6+
---------------------------------------------------------------------------
7+
8+
Copyright (c) 2008-2025 Adam Gashlin, Fastelbja, Ronny Elfert, bnnm,
9+
Christopher Snowhill, NicknineTheEagle, bxaimc,
10+
Thealexbarney, CyberBotX, et al
11+
12+
Portions Copyright (c) 2004-2008, Marko Kreen
13+
Portions Copyright 2001-2007 jagarl / Kazunori Ueno <jagarl@creator.club.ne.jp>
14+
Portions Copyright (c) 1998, Justin Frankel/Nullsoft Inc.
15+
Portions Copyright (C) 2006 Nullsoft, Inc.
16+
Portions Copyright (c) 2005-2007 Paul Hsieh
17+
Portions Copyright (C) 2000-2004 Leshade Entis, Entis-soft.
18+
Portions Public Domain originating with Sun Microsystems
19+
20+
Permission to use, copy, modify, and distribute this software for any
21+
purpose with or without fee is hereby granted, provided that the above
22+
copyright notice and this permission notice appear in all copies.
23+
24+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
25+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
26+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
27+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
28+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
29+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
30+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
31+
32+
---------------------------------------------------------------------------

0 commit comments

Comments
 (0)