Skip to content

Commit 91457d9

Browse files
authored
Merge pull request #1 from FModel/improved
2 parents 23769fa + 2080618 commit 91457d9

File tree

13 files changed

+264
-101
lines changed

13 files changed

+264
-101
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/CodecRebuilders/FmodGcadPcmRebuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private static short[] GetPcmData(FmodSample sample)
7878

7979
public static byte[] Rebuild(FmodSample sample)
8080
{
81-
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
81+
var numChannels = (int)sample.Metadata.Channels;
8282
var format = WaveFormat.CreateCustomFormat(
8383
WaveFormatEncoding.Pcm,
8484
sample.Metadata.Frequency,

Fmod5Sharp/CodecRebuilders/FmodImaAdPcmRebuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ private static short[] DecodeSamplesXboxIma(FmodSample sample)
182182

183183
public static byte[] Rebuild(FmodSample sample)
184184
{
185-
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
185+
var numChannels = (int)sample.Metadata.Channels;
186186
var format = WaveFormat.CreateCustomFormat(
187187
WaveFormatEncoding.Pcm,
188188
sample.Metadata.Frequency,

Fmod5Sharp/CodecRebuilders/FmodPcmRebuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static byte[] Rebuild(FmodSample sample, FmodAudioType type)
1616
_ => throw new($"FmodPcmRebuilder does not support encoding of type {type}"),
1717
};
1818

19-
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
19+
var numChannels = (int)sample.Metadata.Channels;
2020
var format = WaveFormat.CreateCustomFormat(
2121
WaveFormatEncoding.Pcm,
2222
sample.Metadata.Frequency,

Fmod5Sharp/Fmod5Sharp.csproj

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,41 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
4-
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
5-
</PropertyGroup>
3+
<PropertyGroup>
4+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
5+
<Authors>Sam Byass (Samboy063)</Authors>
6+
<Configurations>Release;Debug</Configurations>
7+
<DebugType>embedded</DebugType>
8+
<Description>Decoder for FMOD 5 sound banks (FSB files)</Description>
9+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
10+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
11+
<IsTrimmable>true</IsTrimmable>
12+
<LangVersion>10</LangVersion>
13+
<Nullable>enable</Nullable>
14+
<PackageId>Fmod5Sharp</PackageId>
15+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
16+
<PackageProjectUrl>https://github.com/SamboyCoding/Fmod5Sharp</PackageProjectUrl>
17+
<PackageTags>fmod;audio</PackageTags>
18+
<Platforms>x86;x64;AnyCPU</Platforms>
19+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
20+
<RepositoryType>git</RepositoryType>
21+
<RepositoryUrl>https://github.com/SamboyCoding/Fmod5Sharp.git</RepositoryUrl>
22+
<TargetFrameworks>net10.0;netstandard2.0</TargetFrameworks>
23+
<Title>FMOD5 Sharp</Title>
24+
<Version>3.0.1</Version>
25+
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
26+
</PropertyGroup>
627

7-
<PropertyGroup>
8-
<TargetFrameworks>net10.0</TargetFrameworks>
9-
<LangVersion>latest</LangVersion>
10-
<Nullable>enable</Nullable>
11-
<IsTrimmable>true</IsTrimmable>
12-
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
13-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
14-
<GenerateDocumentationFile>false</GenerateDocumentationFile>
15-
<Version>3.1.0</Version>
16-
<PackageId>FModel.Fmod5Sharp</PackageId>
17-
<PackageTags>fmod;audio</PackageTags>
18-
<Title>FMOD5 Sharp</Title>
19-
<Description>Decoder for FMOD 5 sound banks (FSB files)</Description>
20-
<Copyright>Copyright © 2026 FModel</Copyright>
21-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
22-
<NeutralLanguage>en</NeutralLanguage>
23-
<PublishRepositoryUrl>true</PublishRepositoryUrl>
24-
<EmbedUntrackedSources>true</EmbedUntrackedSources>
25-
<IncludeSymbols>true</IncludeSymbols>
26-
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
27-
<PackageReadmeFile>README.md</PackageReadmeFile>
28-
</PropertyGroup>
28+
<ItemGroup>
29+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
30+
<PackageReference Include="IndexRange" Version="1.0.2" />
31+
<PackageReference Include="NAudio.Core" Version="2.1.0" />
32+
<PackageReference Include="OggVorbisEncoder" Version="1.2.0" />
33+
<PackageReference Include="System.Text.Json" Version="10.0.1" />
34+
</ItemGroup>
2935

30-
<ItemGroup>
31-
<None Include="..\README.md" Pack="true" Visible="false" PackagePath="\" />
32-
</ItemGroup>
33-
34-
<ItemGroup>
35-
<PackageReference Include="NAudio.Core" Version="2.2.1" />
36-
<PackageReference Include="OggVorbisEncoder" Version="1.2.2" />
37-
</ItemGroup>
38-
39-
<ItemGroup>
40-
<None Remove="FmodVorbis\vorbis_headers_converted.json" />
41-
<EmbeddedResource Include="Util\vorbis_headers_converted.json" />
42-
</ItemGroup>
36+
<ItemGroup>
37+
<None Remove="FmodVorbis\vorbis_headers_converted.json" />
38+
<EmbeddedResource Include="Util\vorbis_headers_converted.json" />
39+
</ItemGroup>
4340

4441
</Project>

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/FmodTypes/FmodSampleMetadata.cs

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,36 @@
44

55
namespace Fmod5Sharp.FmodTypes
66
{
7-
//Can be verified against "FMOD::CodecFSB5::decodeSubSoundHeader" in fmod.dll
87
public class FmodSampleMetadata : IBinaryReadable
9-
{
10-
internal bool HasAnyChunks;
11-
internal uint FrequencyId;
12-
internal ulong DataOffset;
13-
internal List<FmodSampleChunk> Chunks = new();
14-
internal int NumChannels;
8+
{
9+
internal bool HasAnyChunks;
10+
internal uint FrequencyId;
11+
internal ulong DataOffset;
12+
internal List<FmodSampleChunk> Chunks = new();
13+
internal int NumChannels;
14+
public ulong SampleCount;
1515

16-
public bool IsStereo;
17-
public ulong SampleCount;
16+
public int Frequency => FsbLoader.Frequencies.TryGetValue(FrequencyId, out var actualFrequency) ? actualFrequency : (int)FrequencyId; //If set by FREQUENCY chunk, id is actual frequency
17+
public uint Channels => (uint)NumChannels;
1818

19-
public int Frequency => FsbLoader.Frequencies.TryGetValue(FrequencyId, out var actualFrequency) ? actualFrequency : (int)FrequencyId; //If set by FREQUENCY chunk, id is actual frequency
20-
public uint Channels => (uint)NumChannels;
19+
void IBinaryReadable.Read(BinaryReader reader)
20+
{
21+
var encoded = reader.ReadUInt64();
2122

22-
void IBinaryReadable.Read(BinaryReader reader)
23-
{
24-
var encoded = reader.ReadUInt64();
25-
26-
HasAnyChunks = (encoded & 1) == 1; //Bit 0
27-
FrequencyId = (uint) encoded.Bits( 1, 4); //Bits 1-4
23+
HasAnyChunks = (encoded & 1) == 1; //Bit 0
24+
FrequencyId = (uint)encoded.Bits(1, 4); //Bits 1-4
25+
int channelBits = (int)encoded.Bits(5, 2); //Bits 5-6
26+
NumChannels = channelBits switch
27+
{
28+
0 => 1,
29+
1 => 2,
30+
2 => 6,
31+
3 => 8,
32+
_ => 0
33+
};
2834

29-
int channelBits = (int)encoded.Bits(5, 2); //Bits 5-6
30-
NumChannels = channelBits switch
31-
{
32-
0 => 1,
33-
1 => 2,
34-
2 => 6,
35-
3 => 8,
36-
_ => 0
37-
};
38-
39-
IsStereo = NumChannels == 2;
40-
41-
DataOffset = encoded.Bits(7, 27) * 32;
42-
SampleCount = encoded.Bits(34, 30);
43-
}
44-
}
35+
DataOffset = encoded.Bits(7, 27) * 32;
36+
SampleCount = encoded.Bits(34, 30);
37+
}
38+
}
4539
}

0 commit comments

Comments
 (0)