Skip to content

Commit 96155ae

Browse files
committed
Better handle invalid banks, support multi-sample banks + sample names
1 parent 96ac26b commit 96155ae

File tree

6 files changed

+130
-77
lines changed

6 files changed

+130
-77
lines changed

Fmod5Sharp/Fmod5Sharp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
</PropertyGroup>
2020

2121
<ItemGroup>
22+
<PackageReference Include="IndexRange" Version="1.0.2" />
2223
<PackageReference Include="NAudio.Core" Version="2.1.0" />
2324
<PackageReference Include="OggVorbisEncoder" Version="1.2.0" />
2425
<PackageReference Include="System.Text.Json" Version="6.0.5" />

Fmod5Sharp/FmodTypes/FmodAudioHeader.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,50 @@ namespace Fmod5Sharp.FmodTypes
99
public class FmodAudioHeader
1010
{
1111
private static readonly object ChunkReadingLock = new();
12+
13+
internal readonly bool IsValid;
1214

13-
public FmodAudioType AudioType;
14-
public uint Version;
15-
public uint NumSamples;
15+
public readonly FmodAudioType AudioType;
16+
public readonly uint Version;
17+
public readonly uint NumSamples;
18+
19+
internal readonly uint SizeOfThisHeader;
20+
internal readonly uint SizeOfSampleHeaders;
21+
internal readonly uint SizeOfNameTable;
22+
internal readonly uint SizeOfData;
1623

17-
internal uint DataSize;
18-
internal List<FmodSampleMetadata> Samples = new();
24+
internal readonly List<FmodSampleMetadata> Samples = new();
1925

2026
public FmodAudioHeader(BinaryReader reader)
2127
{
2228
string magic = reader.ReadString(4);
2329

2430
if (magic != "FSB5")
2531
{
32+
IsValid = false;
2633
return;
2734
}
2835

2936
Version = reader.ReadUInt32(); //0x04
3037
NumSamples = reader.ReadUInt32(); //0x08
31-
var sizeOfSampleHeaders = reader.ReadUInt32(); //0x0C
32-
var nameTableSize = reader.ReadUInt32(); //0x10
33-
DataSize = reader.ReadUInt32(); //0x14
38+
SizeOfSampleHeaders = reader.ReadUInt32();
39+
SizeOfNameTable = reader.ReadUInt32();
40+
SizeOfData = reader.ReadUInt32(); //0x14
3441
AudioType = (FmodAudioType) reader.ReadUInt32(); //0x18
42+
43+
reader.ReadUInt32(); //Skip 0x1C which is always 0
3544

3645
if (Version == 0)
3746
{
38-
reader.ReadUInt32(); //Version 0 has an extra field at 0x1C
47+
SizeOfThisHeader = 0x40;
48+
reader.ReadUInt32(); //Version 0 has an extra field at 0x20 before flags
49+
}
50+
else
51+
{
52+
SizeOfThisHeader = 0x3C;
3953
}
4054

41-
reader.ReadUInt64(); //Skip 0x1C (zero) and 0x20 (flags)
55+
reader.ReadUInt32(); //Skip 0x20 (flags)
4256

4357
//128-bit hash
4458
var hashLower = reader.ReadUInt64(); //0x24
@@ -81,14 +95,8 @@ public FmodAudioHeader(BinaryReader reader)
8195
Samples.Add(sampleMetadata);
8296
}
8397
}
84-
85-
var actualSampleHeadersLength = reader.Position() - sampleHeadersStart;
86-
87-
if (actualSampleHeadersLength != sizeOfSampleHeaders)
88-
{
89-
//Skip zero-padding so we're in the right place for data.
90-
reader.ReadBytes((int)(sizeOfSampleHeaders - actualSampleHeadersLength));
91-
}
98+
99+
IsValid = true;
92100
}
93101
}
94102
}

Fmod5Sharp/FmodTypes/FmodSample.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class FmodSample
77
{
88
public FmodSampleMetadata Metadata;
99
public byte[] SampleBytes;
10+
public string? Name;
1011
internal FmodSoundBank? MyBank;
1112

1213
public FmodSample(FmodSampleMetadata metadata, byte[] sampleBytes)

Fmod5Sharp/FmodTypes/FmodSampleMetadata.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,28 @@ public class FmodSampleMetadata : IBinaryReadable
1010
internal uint FrequencyId;
1111
internal ulong DataOffset;
1212
internal List<FmodSampleChunk> Chunks = new();
13+
internal int NumChannels;
1314

1415
public bool IsStereo;
1516
public ulong SampleCount;
17+
1618
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 => IsStereo ? 2u : 1u;
19+
public uint Channels => (uint)NumChannels;
1820

1921
void IBinaryReadable.Read(BinaryReader reader)
2022
{
2123
var encoded = reader.ReadUInt64();
2224

23-
HasAnyChunks = (encoded & 1) == 1;
24-
FrequencyId = (uint) encoded.Bits(1, 4);
25-
IsStereo = encoded.Bits(5, 1) == 1;
26-
DataOffset = encoded.Bits(6, 28) * 16;
25+
HasAnyChunks = (encoded & 1) == 1; //Bit 0
26+
FrequencyId = (uint) encoded.Bits( 1, 4); //Bits 1-4
27+
var pow2 = (int) encoded.Bits(5, 2); //Bits 5-6
28+
NumChannels = 1 << pow2;
29+
if (NumChannels > 2)
30+
throw new("> 2 channels not supported");
31+
32+
IsStereo = NumChannels == 2;
33+
34+
DataOffset = encoded.Bits(7, 27) * 32;
2735
SampleCount = encoded.Bits(34, 30);
2836
}
2937
}

Fmod5Sharp/FsbLoader.cs

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,85 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.IO;
3-
using System.Linq;
44
using Fmod5Sharp.FmodTypes;
55
using Fmod5Sharp.Util;
66

77
namespace Fmod5Sharp
88
{
9-
public static class FsbLoader
10-
{
11-
internal static readonly Dictionary<uint, int> Frequencies = new()
12-
{
13-
{ 1, 8000 },
14-
{ 2, 11_000 },
15-
{ 3, 11_025 },
16-
{ 4, 16_000 },
17-
{ 5, 22_050 },
18-
{ 6, 24_000 },
19-
{ 7, 32_000 },
20-
{ 8, 44_100 },
21-
{ 9, 48_000 },
22-
{ 10, 96_000 },
23-
};
24-
25-
public static FmodSoundBank LoadFsbFromByteArray(byte[] bankBytes)
26-
{
27-
using MemoryStream stream = new(bankBytes);
28-
using BinaryReader reader = new(stream);
29-
30-
FmodAudioHeader header = new(reader);
31-
32-
List<FmodSample> samples = new();
33-
34-
//Remove header from data block.
35-
bankBytes = bankBytes.Skip((int)reader.Position()).ToArray();
36-
37-
for (var i = 0; i < header.Samples.Count; i++)
38-
{
39-
FmodSampleMetadata sampleMetadata = header.Samples[i];
40-
41-
var firstByteOfSample = sampleMetadata.DataOffset;
42-
ulong lastByteOfSample = header.DataSize;
43-
44-
if (i < header.Samples.Count - 1)
45-
{
46-
lastByteOfSample = header.Samples[i + 1].DataOffset;
47-
}
48-
49-
samples.Add(
50-
new FmodSample(
51-
sampleMetadata,
52-
bankBytes.Skip((int)firstByteOfSample).Take((int)(lastByteOfSample - firstByteOfSample)).ToArray()
53-
)
54-
);
55-
}
56-
57-
return new FmodSoundBank(header, samples);
58-
}
59-
}
9+
public static class FsbLoader
10+
{
11+
internal static readonly Dictionary<uint, int> Frequencies = new()
12+
{
13+
{ 1, 8000 },
14+
{ 2, 11_000 },
15+
{ 3, 11_025 },
16+
{ 4, 16_000 },
17+
{ 5, 22_050 },
18+
{ 6, 24_000 },
19+
{ 7, 32_000 },
20+
{ 8, 44_100 },
21+
{ 9, 48_000 },
22+
{ 10, 96_000 },
23+
};
24+
25+
private static FmodSoundBank? LoadInternal(byte[] bankBytes, bool throwIfError)
26+
{
27+
using MemoryStream stream = new(bankBytes);
28+
using BinaryReader reader = new(stream);
29+
30+
FmodAudioHeader header = new(reader);
31+
32+
if (!header.IsValid)
33+
{
34+
if (throwIfError)
35+
throw new("File is probably not an FSB file (magic number mismatch)");
36+
37+
return null;
38+
}
39+
40+
List<FmodSample> samples = new();
41+
42+
//Remove header from data block.
43+
var bankData = bankBytes.AsSpan((int)(header.SizeOfThisHeader + header.SizeOfNameTable + header.SizeOfSampleHeaders));
44+
45+
for (var i = 0; i < header.Samples.Count; i++)
46+
{
47+
var sampleMetadata = header.Samples[i];
48+
49+
var firstByteOfSample = (int)sampleMetadata.DataOffset;
50+
var lastByteOfSample = (int)header.SizeOfData;
51+
52+
if (i < header.Samples.Count - 1)
53+
{
54+
lastByteOfSample = (int)header.Samples[i + 1].DataOffset;
55+
}
56+
57+
var sample = new FmodSample(sampleMetadata, bankData[firstByteOfSample..lastByteOfSample].ToArray());
58+
59+
if (header.SizeOfNameTable > 0)
60+
{
61+
var nameOffsetOffset = header.SizeOfThisHeader + header.SizeOfSampleHeaders + 4 * i;
62+
reader.BaseStream.Position = nameOffsetOffset;
63+
var nameOffset = reader.ReadUInt32();
64+
65+
nameOffset += header.SizeOfThisHeader + header.SizeOfSampleHeaders;
66+
67+
sample.Name = bankBytes.ReadNullTerminatedString((int)nameOffset);
68+
}
69+
70+
samples.Add(sample);
71+
}
72+
73+
return new FmodSoundBank(header, samples);
74+
}
75+
76+
public static bool TryLoadFsbFromByteArray(byte[] bankBytes, out FmodSoundBank? bank)
77+
{
78+
bank = LoadInternal(bankBytes, false);
79+
return bank != null;
80+
}
81+
82+
public static FmodSoundBank LoadFsbFromByteArray(byte[] bankBytes)
83+
=> LoadInternal(bankBytes, true)!;
84+
}
6085
}

Fmod5Sharp/Util/Extensions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System;
2+
using System.IO;
23
using System.Text;
34

45
namespace Fmod5Sharp.Util
@@ -39,5 +40,14 @@ internal static ulong Bits(this ulong raw, int lowestBit, int numBits)
3940

4041
return (raw & mask) >> lowestBit;
4142
}
43+
44+
internal static string ReadNullTerminatedString(this byte[] bytes, int startOffset)
45+
{
46+
var strLen = bytes.AsSpan(startOffset).IndexOf((byte)0);
47+
if (strLen == -1)
48+
throw new("Could not find null terminator");
49+
50+
return Encoding.UTF8.GetString(bytes, startOffset, strLen);
51+
}
4252
}
4353
}

0 commit comments

Comments
 (0)