Skip to content

Commit acba2bc

Browse files
author
Sam Byass
committed
Add support for GcAdPcm
1 parent a911709 commit acba2bc

11 files changed

Lines changed: 279 additions & 38 deletions

Fmod5Sharp.Tests/Fmod5Sharp.Tests.csproj

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
</PropertyGroup>
1313

1414
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
15-
<PlatformTarget>x86</PlatformTarget>
15+
<PlatformTarget>x86</PlatformTarget>
1616
</PropertyGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
20-
<PackageReference Include="xunit" Version="2.4.1" />
19+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4"/>
20+
<PackageReference Include="xunit" Version="2.4.1"/>
2121
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
2222
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2323
<PrivateAssets>all</PrivateAssets>
@@ -29,16 +29,18 @@
2929
</ItemGroup>
3030

3131
<ItemGroup>
32-
<ProjectReference Include="..\Fmod5Sharp\Fmod5Sharp.csproj" />
32+
<ProjectReference Include="..\Fmod5Sharp\Fmod5Sharp.csproj"/>
3333
</ItemGroup>
3434

3535
<ItemGroup>
36-
<None Remove="TestResources\short_vorbis.fsb" />
37-
<EmbeddedResource Include="TestResources\short_vorbis.fsb" />
38-
<None Remove="TestResources\long_vorbis.fsb" />
39-
<EmbeddedResource Include="TestResources\long_vorbis.fsb" />
40-
<None Remove="TestResources\pcm16.fsb" />
41-
<EmbeddedResource Include="TestResources\pcm16.fsb" />
36+
<None Remove="TestResources\short_vorbis.fsb"/>
37+
<EmbeddedResource Include="TestResources\short_vorbis.fsb"/>
38+
<None Remove="TestResources\long_vorbis.fsb"/>
39+
<EmbeddedResource Include="TestResources\long_vorbis.fsb"/>
40+
<None Remove="TestResources\pcm16.fsb"/>
41+
<EmbeddedResource Include="TestResources\pcm16.fsb"/>
42+
<None Remove="TestResources\gcadpcm.fsb"/>
43+
<EmbeddedResource Include="TestResources\gcadpcm.fsb"/>
4244
</ItemGroup>
4345

4446
</Project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Xunit;
2+
3+
namespace Fmod5Sharp.Tests
4+
{
5+
public class Fmod5SharpGcadPcmTests
6+
{
7+
[Fact]
8+
public void GcadPcmBanksCanBeLoaded()
9+
{
10+
var rawData = this.LoadResource("gcadpcm.fsb");
11+
12+
var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
13+
14+
Assert.Equal(FmodAudioType.GCADPCM, fsb.Header.AudioType);
15+
}
16+
17+
[Fact]
18+
public void GcadPcmBanksCanBeRebuilt()
19+
{
20+
var rawData = this.LoadResource("gcadpcm.fsb");
21+
22+
var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
23+
24+
var bytes = FmodGcadPcmRebuilder.Rebuild(fsb.Samples[0]);
25+
26+
Assert.NotEmpty(bytes);
27+
}
28+
}
29+
}
4.63 KB
Binary file not shown.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Fmod5Sharp.ChunkData
7+
{
8+
public class DspCoefficientsBlockData : IChunkData
9+
{
10+
public List<short>[] ChannelData;
11+
private readonly FmodSampleMetadata _sampleMetadata;
12+
13+
public DspCoefficientsBlockData(FmodSampleMetadata sampleMetadata)
14+
{
15+
_sampleMetadata = sampleMetadata;
16+
ChannelData = new List<short>[_sampleMetadata.Channels];
17+
for (var i = 0; i < _sampleMetadata.Channels; i++)
18+
ChannelData[i] = new();
19+
}
20+
21+
public void Read(BinaryReader reader, uint expectedSize)
22+
{
23+
for (var ch = 0; ch < _sampleMetadata.Channels; ch++)
24+
{
25+
//0x2E bytes per channel. First 0x20 (=> 0x10 shorts) are the coefficients.
26+
for (var i = 0; i < 16; i++)
27+
{
28+
//We can't use ReadInt16 here because BinaryReader is little-endian, and FSB5 encodes this data big-endian
29+
//So instead, read 2 bytes, reverse, then convert to short.
30+
ChannelData[ch].Add(BitConverter.ToInt16(reader.ReadBytes(2).Reverse().ToArray()));
31+
}
32+
//Extra 0xE = 14 bytes
33+
reader.ReadInt64();
34+
reader.ReadInt32();
35+
reader.ReadInt16();
36+
}
37+
}
38+
}
39+
}

Fmod5Sharp/Extensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Fmod5Sharp
55
{
66
internal static class Extensions
77
{
8-
internal static T ReadEnadian<T>(this BinaryReader reader) where T : IBinaryReadable, new()
8+
internal static T ReadEndian<T>(this BinaryReader reader) where T : IBinaryReadable, new()
99
{
1010
var t = new T();
1111
t.Read(reader);

Fmod5Sharp/FmodAudioHeader.cs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,42 @@ public FmodAudioHeader(BinaryReader reader)
2323
return;
2424
}
2525

26-
Version = reader.ReadUInt32();
27-
NumSamples = reader.ReadUInt32();
28-
var sizeOfSampleHeaders = reader.ReadUInt32();
29-
var nameTableSize = reader.ReadUInt32();
30-
DataSize = reader.ReadUInt32();
31-
AudioType = (FmodAudioType) reader.ReadUInt32();
26+
Version = reader.ReadUInt32(); //0x04
27+
NumSamples = reader.ReadUInt32(); //0x08
28+
var sizeOfSampleHeaders = reader.ReadUInt32(); //0x0C
29+
var nameTableSize = reader.ReadUInt32(); //0x10
30+
DataSize = reader.ReadUInt32(); //0x14
31+
AudioType = (FmodAudioType) reader.ReadUInt32(); //0x18
3232

33-
reader.ReadUInt64(); //Ignored, called "zero" in python
34-
35-
//128-bit hash
36-
var hashLower = reader.ReadUInt64();
37-
var hashUpper = reader.ReadUInt64();
38-
39-
reader.ReadUInt64(); //Ignored, called "dummy" in python
40-
4133
if (Version == 0)
4234
{
43-
reader.ReadUInt32(); //Ignored, called "unknown" in python
35+
reader.ReadUInt32(); //Version 0 has an extra field at 0x1C
4436
}
37+
38+
reader.ReadUInt64(); //Skip 0x1C (zero) and 0x20 (flags)
39+
40+
//128-bit hash
41+
var hashLower = reader.ReadUInt64(); //0x24
42+
var hashUpper = reader.ReadUInt64(); //0x30
43+
44+
reader.ReadUInt64(); //Skip unknown value at 0x34
4545

4646
var sampleHeadersStart = reader.Position();
4747
for (var i = 0; i < NumSamples; i++)
4848
{
49-
FmodSampleMetadata sampleMetadata = reader.ReadEnadian<FmodSampleMetadata>();
49+
FmodSampleMetadata sampleMetadata = reader.ReadEndian<FmodSampleMetadata>();
5050

51+
FmodSampleChunk.CurrentSample = sampleMetadata;
5152
var continueReadingChunks = sampleMetadata.HasAnyChunks;
5253
List<FmodSampleChunk> chunks = new();
5354
while (continueReadingChunks)
5455
{
55-
FmodSampleChunk nextChunk = reader.ReadEnadian<FmodSampleChunk>();
56+
FmodSampleChunk nextChunk = reader.ReadEndian<FmodSampleChunk>();
5657
continueReadingChunks = nextChunk.MoreChunks;
5758
chunks.Add(nextChunk);
5859
}
60+
61+
FmodSampleChunk.CurrentSample = null;
5962

6063
if (chunks.FirstOrDefault(c => c.ChunkType == FmodSampleChunkType.FREQUENCY) is { ChunkData: FrequencyChunkData fcd })
6164
{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Fmod5Sharp
2+
{
3+
public class FmodAudioTypeExtensions
4+
{
5+
public static bool IsSupported(FmodAudioType @this) =>
6+
@this switch
7+
{
8+
FmodAudioType.VORBIS => true,
9+
FmodAudioType.PCM8 => true,
10+
FmodAudioType.PCM16 => true,
11+
FmodAudioType.PCM32 => true,
12+
FmodAudioType.GCADPCM => true,
13+
_ => false
14+
};
15+
16+
public static string? FileExtension(FmodAudioType @this) =>
17+
@this switch
18+
{
19+
FmodAudioType.VORBIS => "ogg",
20+
FmodAudioType.PCM8 => "wav",
21+
FmodAudioType.PCM16 => "wav",
22+
FmodAudioType.PCM32 => "wav",
23+
FmodAudioType.GCADPCM => "wav",
24+
_ => null
25+
};
26+
}
27+
}

Fmod5Sharp/FmodGcadPcmRebuilder.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using Fmod5Sharp.ChunkData;
5+
using NAudio.Wave;
6+
7+
namespace Fmod5Sharp
8+
{
9+
public static class FmodGcadPcmRebuilder
10+
{
11+
private const int BytesPerFrame = 8;
12+
private const int SamplesPerFrame = 14;
13+
private const int NibblesPerFrame = 16;
14+
15+
private static short[] GetPcmData(FmodSample sample)
16+
{
17+
//Constants for this sample
18+
var sampleCount = ByteCountToSampleCount(sample.SampleBytes.Length);
19+
var frameCount = Math.Ceiling((double)sampleCount / SamplesPerFrame);
20+
21+
//Result array
22+
var pcmData = new short[sampleCount];
23+
24+
//Read the data we need from the stream
25+
var adpcm = sample.SampleBytes;
26+
var coeffChunk = (DspCoefficientsBlockData)sample.Metadata.Chunks.First(c => c.ChunkType == FmodSampleChunkType.DSPCOEFF).ChunkData;
27+
var coeffs = coeffChunk.ChannelData[0];
28+
29+
//Initialize indices
30+
var currentSample = 0;
31+
var outIndex = 0;
32+
var inIndex = 0;
33+
34+
//History values - current value is based on previous ones
35+
short hist1 = 0;
36+
short hist2 = 0;
37+
38+
for (var i = 0; i < frameCount; i++)
39+
{
40+
//Each byte is a scale and a predictor
41+
var combined = adpcm[inIndex++];
42+
var scale = (1 << (combined & 0xF));
43+
var predictor = combined >> 4;
44+
45+
//Coefficients are based on the predictor value
46+
var coeff1 = coeffs[predictor * 2];
47+
var coeff2 = coeffs[predictor * 2 + 1];
48+
49+
//Either read 14 - all the samples in this frame - or however many are left, if this is a partial frame
50+
var samplesToRead = Math.Min(SamplesPerFrame, sampleCount - currentSample);
51+
52+
for (var s = 0; s < samplesToRead; s++)
53+
{
54+
//Raw value
55+
var adpcmSample = (int) (s % 2 == 0 ? GetHighNibbleSigned(adpcm[inIndex]) : GetLowNibbleSigned(adpcm[inIndex++]));
56+
57+
//Adaptive processing
58+
adpcmSample = ((adpcmSample * scale) << 11);
59+
adpcmSample = (adpcmSample + 1024 + coeff1 * hist1 + coeff2 * hist2) >> 11;
60+
var clampedSample = Clamp16(adpcmSample);
61+
62+
//Bump history along
63+
hist2 = hist1;
64+
hist1 = clampedSample;
65+
66+
//Set result
67+
pcmData[outIndex++] = clampedSample;
68+
69+
//Move to next sample
70+
currentSample++;
71+
}
72+
}
73+
74+
return pcmData;
75+
}
76+
77+
public static byte[] Rebuild(FmodSample sample)
78+
{
79+
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
80+
var format = WaveFormat.CreateCustomFormat(
81+
WaveFormatEncoding.Pcm,
82+
sample.Metadata.Frequency,
83+
numChannels,
84+
sample.Metadata.Frequency * numChannels * 2,
85+
numChannels * 2,
86+
16
87+
);
88+
using var stream = new MemoryStream();
89+
using var writer = new WaveFileWriter(stream, format);
90+
91+
var pcmShorts = GetPcmData(sample);
92+
93+
writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
94+
95+
return stream.GetBuffer();
96+
}
97+
98+
private static int NibbleCountToSampleCount(int nibbleCount)
99+
{
100+
var frames = nibbleCount / NibblesPerFrame;
101+
var extraNibbles = nibbleCount % NibblesPerFrame;
102+
var extraSamples = extraNibbles < 2 ? 0 : extraNibbles - 2;
103+
104+
return SamplesPerFrame * frames + extraSamples;
105+
}
106+
107+
private static int ByteCountToSampleCount(int byteCount) => NibbleCountToSampleCount(byteCount * 2);
108+
109+
private static readonly sbyte[] SignedNibbles = { 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 };
110+
private static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
111+
private static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
112+
113+
private static short Clamp16(int value)
114+
{
115+
if (value > short.MaxValue)
116+
return short.MaxValue;
117+
if (value < short.MinValue)
118+
return short.MinValue;
119+
return (short)value;
120+
}
121+
}
122+
}

Fmod5Sharp/FmodPcmRebuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public static byte[] Rebuild(FmodSample sample, FmodAudioType type)
2020
WaveFormatEncoding.Pcm,
2121
sample.Metadata.Frequency,
2222
numChannels,
23-
sample.Metadata.Frequency * numChannels,
24-
numChannels,
23+
sample.Metadata.Frequency * numChannels * width,
24+
numChannels * width,
2525
width * 8
2626
);
2727
using var stream = new MemoryStream();

Fmod5Sharp/FmodSample.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using Fmod5Sharp.FmodVorbis;
34

45
namespace Fmod5Sharp
@@ -15,14 +16,29 @@ public FmodSample(FmodSampleMetadata metadata, byte[] sampleBytes)
1516
SampleBytes = sampleBytes;
1617
}
1718

18-
public byte[] RebuildAsStandardFileFormat() =>
19-
MyBank!.Header.AudioType switch
19+
public bool RebuildAsStandardFileFormat([NotNullWhen(true)] out byte[]? data, [NotNullWhen(true)] out string? fileExtension)
20+
{
21+
switch(MyBank!.Header.AudioType)
2022
{
21-
FmodAudioType.VORBIS => FmodVorbisRebuilder.RebuildOggFile(this),
22-
FmodAudioType.PCM8 => FmodPcmRebuilder.Rebuild(this, MyBank.Header.AudioType),
23-
FmodAudioType.PCM16 => FmodPcmRebuilder.Rebuild(this, MyBank.Header.AudioType),
24-
FmodAudioType.PCM32 => FmodPcmRebuilder.Rebuild(this, MyBank.Header.AudioType),
25-
_ => throw new NotSupportedException($"Rebuilding of audio type {MyBank.Header.AudioType} not yet implemented. Please open a ticket on the GitHub repository for support.")
26-
};
23+
case FmodAudioType.VORBIS:
24+
data = FmodVorbisRebuilder.RebuildOggFile(this);
25+
fileExtension = "ogg";
26+
return data.Length > 0;
27+
case FmodAudioType.PCM8:
28+
case FmodAudioType.PCM16:
29+
case FmodAudioType.PCM32:
30+
data = FmodPcmRebuilder.Rebuild(this, MyBank.Header.AudioType);
31+
fileExtension = "wav";
32+
return data.Length > 0;
33+
case FmodAudioType.GCADPCM:
34+
data = FmodGcadPcmRebuilder.Rebuild(this);
35+
fileExtension = "wav";
36+
return data.Length > 0;
37+
default:
38+
data = null;
39+
fileExtension = null;
40+
return false;
41+
}
42+
}
2743
}
2844
}

0 commit comments

Comments
 (0)