├── .github
└── workflows
│ └── default.yml
├── .gitignore
├── BankExtractor
├── BankExtractor.csproj
└── Program.cs
├── Fmod5Sharp.Tests
├── Extensions.cs
├── Fmod5ImaAdPcmTests.cs
├── Fmod5Sharp.Tests.csproj
├── Fmod5SharpGcadPcmTests.cs
├── Fmod5SharpPcmTests.cs
├── Fmod5SharpVorbisTests.cs
└── TestResources
│ ├── gcadpcm.fsb
│ ├── imaadpcm_long.fsb
│ ├── imaadpcm_short.fsb
│ ├── long_vorbis.fsb
│ ├── pcm16.fsb
│ ├── previously_unrecoverable_vorbis.fsb
│ ├── short_vorbis.fsb
│ ├── vorbis_with_blockflag_exception.fsb
│ └── xbox_imaad.fsb
├── Fmod5Sharp.sln
├── Fmod5Sharp
├── BitStreams
│ ├── Bit.cs
│ ├── BitExtensions.cs
│ ├── BitStream.cs
│ ├── Int24.cs
│ ├── Int48.cs
│ ├── UInt24.cs
│ └── UInt48.cs
├── ChunkData
│ ├── ChannelChunkData.cs
│ ├── DspCoefficientsBlockData.cs
│ ├── FrequencyChunkData.cs
│ ├── IChunkData.cs
│ ├── LoopChunkData.cs
│ ├── UnknownChunkData.cs
│ └── VorbisChunkData.cs
├── CodecRebuilders
│ ├── FmodGcadPcmRebuilder.cs
│ ├── FmodImaAdPcmRebuilder.cs
│ ├── FmodPcmRebuilder.cs
│ └── FmodVorbisRebuilder.cs
├── Fmod5Sharp.csproj
├── FmodTypes
│ ├── FmodAudioHeader.cs
│ ├── FmodAudioType.cs
│ ├── FmodSample.cs
│ ├── FmodSampleChunk.cs
│ ├── FmodSampleChunkType.cs
│ ├── FmodSampleMetadata.cs
│ └── FmodSoundBank.cs
├── FsbLoader.cs
└── Util
│ ├── Extensions.cs
│ ├── Fmod5SharpJsonContext.cs
│ ├── FmodAudioTypeExtensions.cs
│ ├── FmodVorbisData.cs
│ ├── IBinaryReadable.cs
│ ├── Utils.cs
│ └── vorbis_headers_converted.json
├── HeaderGenerator
├── HeaderGenerator.csproj
└── Program.cs
├── LICENSE
└── README.md
/.github/workflows/default.yml:
--------------------------------------------------------------------------------
1 | name: .NET - Build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 |
7 | jobs:
8 | release:
9 |
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Setup .NET Core
14 | uses: actions/setup-dotnet@v1
15 | with:
16 | dotnet-version: 6.x
17 | - name: Install dependencies
18 | run: dotnet restore
19 | - name: Tests
20 | run: dotnet test
21 | - name: Build
22 | run: dotnet build -c Release
23 | - name: Upload NuGet Artifact
24 | uses: actions/upload-artifact@v2
25 | with:
26 | name: Fmod5Sharp.nupkg
27 | path: Fmod5Sharp/bin/Release/*.nupkg
28 | - name: Upload to NuGet
29 | if: contains(github.event.head_commit.message, '[publish]') == true
30 | env:
31 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
32 | run: dotnet nuget push ./Fmod5Sharp/bin/Release/*.nupkg -s https://api.nuget.org/v3/index.json -k $NUGET_API_KEY
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | /packages/
4 | riderModule.iml
5 | /_ReSharper.Caches/
6 | .idea/
7 | *.DotSettings.user
8 | .vs/
9 | .vscode/
10 | launchSettings.json
--------------------------------------------------------------------------------
/BankExtractor/BankExtractor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/BankExtractor/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Fmod5Sharp;
3 |
4 | ///
5 | /// Mainly serves as an example of how to use fmod5sharp
6 | ///
7 | public static class Program
8 | {
9 | public static void Main(string[] args)
10 | {
11 | var bankPath = args[0];
12 | var outPath = args.Length > 1 ? args[1] : $"{Path.GetFileNameWithoutExtension(bankPath)}-extracted";
13 |
14 | Console.WriteLine("Loading bank...");
15 |
16 | var bytes = File.ReadAllBytes(bankPath);
17 |
18 | var index = bytes.AsSpan().IndexOf(Encoding.ASCII.GetBytes("FSB5"));
19 |
20 | if (index > 0)
21 | {
22 | bytes = bytes.AsSpan(index).ToArray();
23 | }
24 |
25 | var bank = FsbLoader.LoadFsbFromByteArray(bytes);
26 |
27 | if (Directory.Exists(outPath))
28 | {
29 | Console.WriteLine("Removing existing output directory...");
30 | Directory.Delete(outPath, true);
31 | }
32 |
33 | var outDir = Directory.CreateDirectory(outPath);
34 |
35 | Console.WriteLine("Extracting...");
36 | var i = 0;
37 | foreach (var bankSample in bank.Samples)
38 | {
39 | i++;
40 | var name = bankSample.Name ?? $"sample-{i}";
41 |
42 | if(!bankSample.RebuildAsStandardFileFormat(out var data, out var extension))
43 | {
44 | Console.WriteLine($"Failed to extract sample {name}");
45 | continue;
46 | }
47 |
48 | var filePath = Path.Combine(outDir.FullName, $"{name}.{extension}");
49 | File.WriteAllBytes(filePath, data);
50 | Console.WriteLine($"Extracted sample {name}");
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 |
5 | namespace Fmod5Sharp.Tests
6 | {
7 | public static class Extensions
8 | {
9 | public static byte[] LoadResource(this object _, string filename)
10 | {
11 | using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Fmod5Sharp.Tests.TestResources.{filename}") ?? throw new Exception($"File {filename} not found.");
12 | using BinaryReader reader = new BinaryReader(stream);
13 |
14 | return reader.ReadBytes((int)stream.Length);
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Fmod5ImaAdPcmTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Fmod5Sharp.CodecRebuilders;
3 | using Fmod5Sharp.FmodTypes;
4 | using Xunit;
5 |
6 | namespace Fmod5Sharp.Tests
7 | {
8 | public class Fmod5ImaAdPcmTests
9 | {
10 | [Fact]
11 | public void BanksCanBeLoaded()
12 | {
13 | var rawData = this.LoadResource("imaadpcm_short.fsb");
14 |
15 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
16 |
17 | Assert.Equal(FmodAudioType.IMAADPCM, fsb.Header.AudioType);
18 | Assert.Equal(2u, fsb.Samples.Single().Metadata.Channels);
19 | }
20 |
21 | [Fact]
22 | public void ImaAdPcmBanksCanBeRebuilt()
23 | {
24 | var rawData = this.LoadResource("imaadpcm_short.fsb");
25 |
26 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
27 |
28 | var bytes = FmodImaAdPcmRebuilder.Rebuild(fsb.Samples[0]);
29 |
30 | Assert.NotEmpty(bytes);
31 | }
32 |
33 | [Fact]
34 | public void XboxImaAdPcmBanksCanBeRebuilt()
35 | {
36 | var rawData = this.LoadResource("xbox_imaad.fsb");
37 |
38 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
39 |
40 | Assert.Equal(FmodAudioType.IMAADPCM, fsb.Header.AudioType);
41 |
42 | var bytes = FmodImaAdPcmRebuilder.Rebuild(fsb.Samples[0]);
43 |
44 | Assert.NotEmpty(bytes);
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Fmod5Sharp.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 | Release;Debug
10 |
11 | AnyCPU;x64
12 |
13 |
14 |
15 | x86
16 |
17 |
18 |
19 |
20 |
21 |
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 | all
24 |
25 |
26 | runtime; build; native; contentfiles; analyzers; buildtransitive
27 | all
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Fmod5SharpGcadPcmTests.cs:
--------------------------------------------------------------------------------
1 | using Fmod5Sharp.CodecRebuilders;
2 | using Fmod5Sharp.FmodTypes;
3 | using Xunit;
4 |
5 | namespace Fmod5Sharp.Tests
6 | {
7 | public class Fmod5SharpGcadPcmTests
8 | {
9 | [Fact]
10 | public void GcadPcmBanksCanBeLoaded()
11 | {
12 | var rawData = this.LoadResource("gcadpcm.fsb");
13 |
14 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
15 |
16 | Assert.Equal(FmodAudioType.GCADPCM, fsb.Header.AudioType);
17 | }
18 |
19 | [Fact]
20 | public void GcadPcmBanksCanBeRebuilt()
21 | {
22 | var rawData = this.LoadResource("gcadpcm.fsb");
23 |
24 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
25 |
26 | var bytes = FmodGcadPcmRebuilder.Rebuild(fsb.Samples[0]);
27 |
28 | Assert.NotEmpty(bytes);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Fmod5SharpPcmTests.cs:
--------------------------------------------------------------------------------
1 | using Fmod5Sharp.CodecRebuilders;
2 | using Fmod5Sharp.FmodTypes;
3 | using Xunit;
4 |
5 | namespace Fmod5Sharp.Tests
6 | {
7 | public class Fmod5SharpPcmTests
8 | {
9 | [Fact]
10 | public void Pcm16FsbFileCanBeLoaded()
11 | {
12 | var rawData = this.LoadResource("pcm16.fsb");
13 |
14 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
15 |
16 | Assert.Equal(FmodAudioType.PCM16, fsb.Header.AudioType);
17 | }
18 |
19 | [Fact]
20 | public void PcmFilesCanBeReconstructed()
21 | {
22 | var rawData = this.LoadResource("pcm16.fsb");
23 |
24 | var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
25 |
26 | var wavFile = FmodPcmRebuilder.Rebuild(fsb.Samples[0], fsb.Header.AudioType);
27 |
28 | Assert.NotEmpty(wavFile);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/Fmod5SharpVorbisTests.cs:
--------------------------------------------------------------------------------
1 | using Fmod5Sharp.CodecRebuilders;
2 | using Xunit;
3 |
4 | namespace Fmod5Sharp.Tests
5 | {
6 |
7 | public class Fmod5SharpVorbisTests
8 | {
9 | [Fact]
10 | public void SoundBanksCanBeLoaded()
11 | {
12 | var rawData = this.LoadResource("short_vorbis.fsb");
13 |
14 | var samples = FsbLoader.LoadFsbFromByteArray(rawData).Samples;
15 |
16 | Assert.Single(samples, s => !s.Metadata.IsStereo && s.SampleBytes.Length > 0);
17 | }
18 |
19 | [Fact]
20 | public void VorbisAudioCanBeRestoredWithoutExceptions()
21 | {
22 | var rawData = this.LoadResource("short_vorbis.fsb");
23 |
24 | var samples = FsbLoader.LoadFsbFromByteArray(rawData).Samples;
25 |
26 | var sample = samples[0];
27 |
28 | var oggBytes = FmodVorbisRebuilder.RebuildOggFile(sample);
29 |
30 | Assert.NotEmpty(oggBytes);
31 |
32 | //Cannot assert on length output bytes because it changes with the version of libvorbis you use.
33 | }
34 |
35 | [Fact]
36 | public void LongerFilesWorkToo()
37 | {
38 | var rawData = this.LoadResource("long_vorbis.fsb");
39 |
40 | var samples = FsbLoader.LoadFsbFromByteArray(rawData).Samples;
41 |
42 | var sample = samples[0];
43 |
44 | var oggBytes = FmodVorbisRebuilder.RebuildOggFile(sample);
45 |
46 | Assert.NotEmpty(oggBytes);
47 | }
48 |
49 | [Fact]
50 | public void PreviouslyUnrecoverableVorbisFilesWorkWithOurCustomRebuilder()
51 | {
52 | var rawData = this.LoadResource("previously_unrecoverable_vorbis.fsb");
53 |
54 | var samples = FsbLoader.LoadFsbFromByteArray(rawData).Samples;
55 |
56 | var sample = samples[0];
57 |
58 | var oggBytes = FmodVorbisRebuilder.RebuildOggFile(sample);
59 |
60 | Assert.NotEmpty(oggBytes);
61 | }
62 |
63 | [Fact]
64 | public void VorbisFilesThatPreviouslyThrewExceptionsDoNot()
65 | {
66 | var rawData = this.LoadResource("vorbis_with_blockflag_exception.fsb");
67 |
68 | var samples = FsbLoader.LoadFsbFromByteArray(rawData).Samples;
69 |
70 | var sample = samples[0];
71 |
72 | var oggBytes = FmodVorbisRebuilder.RebuildOggFile(sample);
73 |
74 | Assert.NotEmpty(oggBytes);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/gcadpcm.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/gcadpcm.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/imaadpcm_long.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/imaadpcm_long.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/imaadpcm_short.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/imaadpcm_short.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/long_vorbis.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/long_vorbis.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/pcm16.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/pcm16.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/previously_unrecoverable_vorbis.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/previously_unrecoverable_vorbis.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/short_vorbis.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/short_vorbis.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/vorbis_with_blockflag_exception.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/vorbis_with_blockflag_exception.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.Tests/TestResources/xbox_imaad.fsb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SamboyCoding/Fmod5Sharp/510ddfcdaa9b69764816cdd6dd6534aff0a95df4/Fmod5Sharp.Tests/TestResources/xbox_imaad.fsb
--------------------------------------------------------------------------------
/Fmod5Sharp.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32611.2
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fmod5Sharp", "Fmod5Sharp\Fmod5Sharp.csproj", "{6D7552B5-7A84-4F8D-9714-11C43E080234}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fmod5Sharp.Tests", "Fmod5Sharp.Tests\Fmod5Sharp.Tests.csproj", "{28048C08-DAC2-4DC2-82EB-C2CF5C9DC772}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeaderGenerator", "HeaderGenerator\HeaderGenerator.csproj", "{433DFB57-A200-4238-B3EE-B4C5FB378E83}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{05ED0354-9E7F-43BA-8F4C-0E05FBBB86F4}"
13 | ProjectSection(SolutionItems) = preProject
14 | .gitignore = .gitignore
15 | README.md = README.md
16 | .github\workflows\default.yml = .github\workflows\default.yml
17 | EndProjectSection
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BankExtractor", "BankExtractor\BankExtractor.csproj", "{38AD6124-9F67-48BC-9EB2-0B0C10A87DD7}"
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Release|Any CPU = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {6D7552B5-7A84-4F8D-9714-11C43E080234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {6D7552B5-7A84-4F8D-9714-11C43E080234}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {6D7552B5-7A84-4F8D-9714-11C43E080234}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {6D7552B5-7A84-4F8D-9714-11C43E080234}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {28048C08-DAC2-4DC2-82EB-C2CF5C9DC772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {28048C08-DAC2-4DC2-82EB-C2CF5C9DC772}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {28048C08-DAC2-4DC2-82EB-C2CF5C9DC772}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {28048C08-DAC2-4DC2-82EB-C2CF5C9DC772}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {433DFB57-A200-4238-B3EE-B4C5FB378E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {433DFB57-A200-4238-B3EE-B4C5FB378E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {433DFB57-A200-4238-B3EE-B4C5FB378E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {433DFB57-A200-4238-B3EE-B4C5FB378E83}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {38AD6124-9F67-48BC-9EB2-0B0C10A87DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {38AD6124-9F67-48BC-9EB2-0B0C10A87DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {38AD6124-9F67-48BC-9EB2-0B0C10A87DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {38AD6124-9F67-48BC-9EB2-0B0C10A87DD7}.Release|Any CPU.Build.0 = Release|Any CPU
43 | EndGlobalSection
44 | GlobalSection(SolutionProperties) = preSolution
45 | HideSolutionNode = FALSE
46 | EndGlobalSection
47 | GlobalSection(ExtensibilityGlobals) = postSolution
48 | SolutionGuid = {7474598F-4859-4192-911A-10B23D98D924}
49 | EndGlobalSection
50 | EndGlobal
51 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/Bit.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace BitStreams
4 | {
5 | [Serializable]
6 | internal struct Bit
7 | {
8 | private readonly byte value;
9 |
10 | private Bit(int value)
11 | {
12 | this.value = (byte)(value & 1);
13 | }
14 |
15 | public static implicit operator Bit(int value)
16 | {
17 | return new Bit(value);
18 | }
19 |
20 | public static implicit operator Bit(bool value)
21 | {
22 | return new Bit(value ? 1 : 0);
23 | }
24 |
25 | public static implicit operator int (Bit bit)
26 | {
27 | return bit.value;
28 | }
29 |
30 | public static implicit operator byte (Bit bit)
31 | {
32 | return (byte)bit.value;
33 | }
34 |
35 | public static implicit operator bool (Bit bit)
36 | {
37 | return bit.value == 1;
38 | }
39 |
40 | public static Bit operator &(Bit x, Bit y)
41 | {
42 | return x.value & y.value;
43 | }
44 |
45 | public static Bit operator |(Bit x, Bit y)
46 | {
47 | return x.value | y.value;
48 | }
49 |
50 | public static Bit operator ^(Bit x, Bit y)
51 | {
52 | return x.value ^ y.value;
53 | }
54 |
55 | public static Bit operator ~(Bit bit)
56 | {
57 | return (~(bit.value) & 1);
58 | }
59 |
60 | public static implicit operator string (Bit bit)
61 | {
62 | return bit.value.ToString();
63 | }
64 |
65 | public int AsInt()
66 | {
67 | return this.value;
68 | }
69 |
70 | public bool AsBool()
71 | {
72 | return this.value == 1;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/BitExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace BitStreams
2 | {
3 | internal static class BitExtensions
4 | {
5 |
6 | #region GetBit
7 |
8 | public static Bit GetBit(this byte n, int index)
9 | {
10 | return n >> index;
11 | }
12 |
13 | public static Bit GetBit(this sbyte n, int index)
14 | {
15 | return n >> index;
16 | }
17 |
18 | public static Bit GetBit(this short n, int index)
19 | {
20 | return n >> index;
21 | }
22 |
23 | public static Bit GetBit(this ushort n, int index)
24 | {
25 | return n >> index;
26 | }
27 |
28 | public static Bit GetBit(this int n, int index)
29 | {
30 | return n >> index;
31 | }
32 |
33 | public static Bit GetBit(this uint n, int index)
34 | {
35 | return (byte)(n >> index);
36 | }
37 |
38 | public static Bit GetBit(this long n, int index)
39 | {
40 | return (byte)(n >> index);
41 | }
42 |
43 | public static Bit GetBit(this ulong n, int index)
44 | {
45 | return (byte)(n >> index);
46 | }
47 |
48 | #endregion
49 |
50 | #region CircularShift
51 |
52 | public static byte CircularShift(this byte n, int bits, bool leftShift)
53 | {
54 | if (leftShift)
55 | {
56 | n = (byte)(n << bits | n >> (8 - bits));
57 | }
58 | else
59 | {
60 | n = (byte)(n >> bits | n << (8 - bits));
61 | }
62 | return n;
63 | }
64 |
65 | public static sbyte CircularShift(this sbyte n, int bits, bool leftShift)
66 | {
67 | if (leftShift)
68 | {
69 | n = (sbyte)(n << bits | n >> (8 - bits));
70 | }
71 | else
72 | {
73 | n = (sbyte)(n >> bits | n << (8 - bits));
74 | }
75 | return n;
76 | }
77 |
78 | public static short CircularShift(this short n, int bits, bool leftShift)
79 | {
80 | if (leftShift)
81 | {
82 | n = (short)(n << bits | n >> (16 - bits));
83 | }
84 | else
85 | {
86 | n = (short)(n >> bits | n << (16 - bits));
87 | }
88 | return n;
89 | }
90 |
91 | public static ushort CircularShift(this ushort n, int bits, bool leftShift)
92 | {
93 | if (leftShift)
94 | {
95 | n = (ushort)(n << bits | n >> (16 - bits));
96 | }
97 | else
98 | {
99 | n = (ushort)(n >> bits | n << (16 - bits));
100 | }
101 | return n;
102 | }
103 |
104 | public static int CircularShift(this int n, int bits, bool leftShift)
105 | {
106 | if (leftShift)
107 | {
108 | n = (n << bits | n >> (32 - bits));
109 | }
110 | else
111 | {
112 | n = (n >> bits | n << (32 - bits));
113 | }
114 | return n;
115 | }
116 |
117 | public static uint CircularShift(this uint n, int bits, bool leftShift)
118 | {
119 | if (leftShift)
120 | {
121 | n = (uint)(n << bits | n >> (32 - bits));
122 | }
123 | else
124 | {
125 | n = (uint)(n >> bits | n << (32 - bits));
126 | }
127 | return n;
128 | }
129 |
130 | public static long CircularShift(this long n, int bits, bool leftShift)
131 | {
132 | if (leftShift)
133 | {
134 | n = (n << bits | n >> (64 - bits));
135 | }
136 | else
137 | {
138 | n = (n >> bits | n << (64 - bits));
139 | }
140 | return n;
141 | }
142 |
143 | public static ulong CircularShift(this ulong n, int bits, bool leftShift)
144 | {
145 | if (leftShift)
146 | {
147 | n = (ulong)(n << bits | n >> (64 - bits));
148 | }
149 | else
150 | {
151 | n = (ulong)(n >> bits | n << (64 - bits));
152 | }
153 | return n;
154 | }
155 |
156 | #endregion
157 |
158 | #region Reverse
159 |
160 | public static byte ReverseBits(this byte b)
161 | {
162 | return (byte)(((b & 1) << 7) + ((((b >> 1) & 1) << 6)) + (((b >> 2) & 1) << 5) + (((b >> 3) & 1) << 4) + (((b >> 4) & 1) << 3) +(((b >> 5) & 1) << 2) +(((b >> 6) & 1) << 1) + ((b >> 7)&1));
163 | }
164 |
165 | #endregion
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/BitStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.IO;
5 |
6 | namespace BitStreams
7 | {
8 | ///
9 | /// Stream wrapper to use bit-level operations
10 | ///
11 | internal class BitStream
12 | {
13 | private long offset { get; set; }
14 | private int bit { get; set; }
15 | private bool MSB { get; set; }
16 | private Stream stream;
17 | private Encoding encoding;
18 |
19 | ///
20 | /// Allows the auto increase in size when needed
21 | ///
22 | public bool AutoIncreaseStream { get; set; }
23 |
24 | ///
25 | /// Get the stream length
26 | ///
27 | public long Length
28 | {
29 | get
30 | {
31 | return stream.Length;
32 | }
33 | }
34 |
35 | ///
36 | /// Get the current bit position in the stream
37 | ///
38 | public long BitPosition
39 | {
40 | get
41 | {
42 | return bit;
43 | }
44 | }
45 |
46 | ///
47 | /// Check if offset is inside the stream length
48 | ///
49 | private bool ValidPosition
50 | {
51 | get
52 | {
53 | return offset < Length;
54 | }
55 | }
56 |
57 | #region Constructors
58 |
59 | ///
60 | /// Creates a using a Stream
61 | ///
62 | /// Stream to use
63 | /// true if Most Significant Bit will be used, if false LSB will be used
64 | public BitStream(Stream stream, bool MSB = false)
65 | {
66 | this.stream = new MemoryStream();
67 | stream.CopyTo(this.stream);
68 | this.MSB = MSB;
69 | offset = 0;
70 | bit = 0;
71 | encoding = Encoding.UTF8;
72 | AutoIncreaseStream = false;
73 | }
74 |
75 | ///
76 | /// Creates a using a Stream
77 | ///
78 | /// Stream to use
79 | /// Encoding to use with chars
80 | /// true if Most Significant Bit will be used, if false LSB will be used
81 | public BitStream(Stream stream, Encoding encoding, bool MSB = false)
82 | {
83 | this.stream = new MemoryStream();
84 | stream.CopyTo(this.stream);
85 | this.MSB = MSB;
86 | offset = 0;
87 | bit = 0;
88 | this.encoding = encoding;
89 | AutoIncreaseStream = false;
90 | }
91 |
92 | ///
93 | /// Creates a using a byte[]
94 | ///
95 | /// byte[] to use
96 | /// true if Most Significant Bit will be used, if false LSB will be used
97 | public BitStream(byte[] buffer, bool MSB = false)
98 | {
99 | this.stream = new MemoryStream();
100 | MemoryStream m = new MemoryStream(buffer);
101 | m.CopyTo(this.stream);
102 | this.MSB = MSB;
103 | offset = 0;
104 | bit = 0;
105 | encoding = Encoding.UTF8;
106 | AutoIncreaseStream = false;
107 | }
108 |
109 | ///
110 | /// Creates a using a byte[]
111 | ///
112 | /// byte[] to use
113 | /// Encoding to use with chars
114 | /// true if Most Significant Bit will be used, if false LSB will be used
115 | public BitStream(byte[] buffer, Encoding encoding, bool MSB = false)
116 | {
117 | this.stream = new MemoryStream();
118 | MemoryStream m = new MemoryStream(buffer);
119 | m.CopyTo(this.stream);
120 | this.MSB = MSB;
121 | offset = 0;
122 | bit = 0;
123 | this.encoding = encoding;
124 | AutoIncreaseStream = false;
125 | }
126 |
127 | ///
128 | /// Creates a using a byte[]
129 | ///
130 | /// byte[] to use
131 | /// true if Most Significant Bit will be used, if false LSB will be used
132 | public static BitStream Create(byte[] buffer, bool MSB = false)
133 | {
134 | return new BitStream(buffer, MSB);
135 | }
136 |
137 | ///
138 | /// Creates a using a byte[]
139 | ///
140 | /// byte[] to use
141 | /// Encoding to use with chars/param>
142 | /// true if Most Significant Bit will be used, if false LSB will be used
143 | public static BitStream Create(byte[] buffer, Encoding encoding, bool MSB = false)
144 | {
145 | return new BitStream(buffer, encoding, MSB);
146 | }
147 |
148 | ///
149 | /// Creates a using a file path, throws IOException if file doesn't exists or path is not a file
150 | ///
151 | /// File path
152 | /// Encoding of the file, if null default will be used
153 | ///
154 | public static BitStream CreateFromFile(string path, Encoding? encoding = null)
155 | {
156 | if (!File.Exists(path))
157 | {
158 | throw new IOException("File doesn't exists!");
159 | }
160 | if(File.GetAttributes(path) == FileAttributes.Directory)
161 | {
162 | throw new IOException("Path is a directory!");
163 | }
164 | if (encoding == null)
165 | {
166 | encoding = Encoding.UTF8;
167 | }
168 | return new BitStream(File.ReadAllBytes(path), encoding);
169 | }
170 |
171 | #endregion
172 |
173 | #region Methods
174 |
175 | ///
176 | /// Seek to the specified offset and check if it is a valid position for reading in the stream
177 | ///
178 | /// offset on the stream
179 | /// bit position
180 | /// true if offset is valid to do reading, false otherwise
181 | public bool this[long offset, int bit]
182 | {
183 | get
184 | {
185 | Seek(offset, bit);
186 | return ValidPosition;
187 | }
188 | //set {
189 | // Seek(offset, bit);
190 | //}
191 | private set { }
192 | }
193 |
194 | ///
195 | /// Seek through the stream selecting the offset and bit using
196 | ///
197 | /// offset on the stream
198 | /// bit position
199 | public void Seek(long offset, int bit)
200 | {
201 | if (offset > Length)
202 | {
203 | this.offset = Length;
204 | }
205 | else
206 | {
207 | if (offset >= 0)
208 | {
209 | this.offset = offset;
210 | }
211 | else
212 | {
213 | offset = 0;
214 | }
215 | }
216 | if (bit >= 8)
217 | {
218 | int n = (int)(bit / 8);
219 | this.offset += n;
220 | this.bit = bit % 8;
221 | }
222 | else
223 | {
224 | this.bit = bit;
225 | }
226 | stream.Seek(offset, SeekOrigin.Begin);
227 | }
228 |
229 | ///
230 | /// Advances the stream by one bit
231 | ///
232 | public void AdvanceBit()
233 | {
234 | bit = (bit + 1) % 8;
235 | if (bit == 0)
236 | {
237 | offset++;
238 | }
239 | }
240 |
241 | ///
242 | /// Returns the stream by one bit
243 | ///
244 | public void ReturnBit()
245 | {
246 | bit = ((bit - 1) == -1 ? 7 : bit - 1);
247 | if (bit == 7)
248 | {
249 | offset--;
250 | }
251 | if(offset < 0)
252 | {
253 | offset = 0;
254 | }
255 | }
256 |
257 | ///
258 | /// Get the edited stream
259 | ///
260 | /// Modified stream
261 | public Stream GetStream()
262 | {
263 | return stream;
264 | }
265 |
266 | ///
267 | /// Get the stream data as a byte[]
268 | ///
269 | /// Stream as byte[]
270 | public byte[] GetStreamData()
271 | {
272 | stream.Seek(0, SeekOrigin.Begin);
273 | MemoryStream s = new MemoryStream();
274 | stream.CopyTo(s);
275 | Seek(offset, bit);
276 | return s.ToArray();
277 | }
278 |
279 | ///
280 | /// Get the used for chars and strings
281 | ///
282 | /// used
283 | public Encoding GetEncoding()
284 | {
285 | return encoding;
286 | }
287 |
288 | ///
289 | /// Set the that will be used for chars and strings
290 | ///
291 | /// to use
292 | public void SetEncoding(Encoding encoding)
293 | {
294 | this.encoding = encoding;
295 | }
296 |
297 | ///
298 | /// Changes the length of the stream, if new length is less than current length stream data will be truncated
299 | ///
300 | /// New stream length
301 | /// return true if stream changed length, false if it wasn't possible
302 | public bool ChangeLength(long length)
303 | {
304 | if (stream.CanSeek && stream.CanWrite)
305 | {
306 | stream.SetLength(length);
307 | return true;
308 | }
309 | else
310 | {
311 | return false;
312 | }
313 | }
314 |
315 | ///
316 | /// Cuts the from the specified offset and given length, will throw an exception when length + offset is higher than stream's length, offset and bit will be set to 0
317 | ///
318 | /// Offset to start
319 | /// Length of the new
320 | public void CutStream(long offset, long length)
321 | {
322 | byte[] data = GetStreamData();
323 | byte[] buffer = new byte[length];
324 | Array.Copy(data, offset, buffer, 0, length);
325 | this.stream = new MemoryStream();
326 | MemoryStream m = new MemoryStream(buffer);
327 | this.stream = new MemoryStream();
328 | m.CopyTo(this.stream);
329 | this.offset = 0;
330 | bit = 0;
331 | }
332 |
333 | ///
334 | /// Copies the current buffer to another
335 | ///
336 | /// to copy buffer
337 | public void CopyStreamTo(Stream stream)
338 | {
339 | Seek(0, 0);
340 | stream.SetLength(this.stream.Length);
341 | this.stream.CopyTo(stream);
342 | }
343 |
344 | ///
345 | /// Copies the current buffer to another
346 | ///
347 | /// to copy buffer
348 | public void CopyStreamTo(BitStream stream)
349 | {
350 | Seek(0, 0);
351 | stream.ChangeLength(this.stream.Length);
352 | this.stream.CopyTo(stream.stream);
353 | stream.Seek(0, 0);
354 | }
355 |
356 | ///
357 | /// Saves current buffer into a file
358 | ///
359 | /// File to write data, if it exists it will be overwritten
360 | public void SaveStreamAsFile(string filename)
361 | {
362 | File.WriteAllBytes(filename, GetStreamData());
363 | }
364 |
365 | ///
366 | /// Returns the current content of the stream as a
367 | ///
368 | /// containing current data
369 | public MemoryStream CloneAsMemoryStream()
370 | {
371 | return new MemoryStream(GetStreamData());
372 | }
373 |
374 | ///
375 | /// Returns the current content of the stream as a
376 | ///
377 | /// containing current data
378 | public BufferedStream CloneAsBufferedStream()
379 | {
380 | BufferedStream bs = new BufferedStream(stream);
381 | StreamWriter sw = new StreamWriter(bs);
382 | sw.Write(GetStreamData());
383 | bs.Seek(0, SeekOrigin.Begin);
384 | return bs;
385 | }
386 |
387 |
388 | ///
389 | /// Checks if the will be in a valid position on its last bit read/write
390 | ///
391 | /// Number of bits it will advance
392 | /// true if will be inside the stream length
393 | private bool ValidPositionWhen(int bits)
394 | {
395 | long o = offset;
396 | int b = bit;
397 | b = (b + 1) % 8;
398 | if (b == 0)
399 | {
400 | o++;
401 | }
402 | return o < Length;
403 | }
404 |
405 |
406 | #endregion
407 |
408 | #region BitRead/Write
409 |
410 | ///
411 | /// Read current position bit and advances the position within the stream by one bit
412 | ///
413 | /// Returns the current position bit as 0 or 1
414 | public Bit ReadBit()
415 | {
416 | if (!ValidPosition)
417 | {
418 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
419 | }
420 | stream.Seek(offset, SeekOrigin.Begin);
421 | byte value;
422 | if (!MSB)
423 | {
424 | value = (byte)((stream.ReadByte() >> (bit)) & 1);
425 | }
426 | else
427 | {
428 | value = (byte)((stream.ReadByte() >> (7 - bit)) & 1);
429 | }
430 | AdvanceBit();
431 | stream.Seek(offset, SeekOrigin.Begin);
432 | return value;
433 | }
434 |
435 | ///
436 | /// Read from current position the specified number of bits
437 | ///
438 | /// Bits to read
439 | /// [] containing read bits
440 | public Bit[] ReadBits(int length)
441 | {
442 | Bit[] bits = new Bit[length];
443 | for(int i=0;i< length; i++)
444 | {
445 | bits[i] = ReadBit();
446 | }
447 | return bits;
448 | }
449 |
450 | ///
451 | /// Writes a bit in the current position
452 | ///
453 | /// Bit to write, it data is not 0 or 1 data = data & 1
454 | public void WriteBit(Bit data)
455 | {
456 | stream.Seek(offset, SeekOrigin.Begin);
457 | byte value = (byte)stream.ReadByte();
458 | stream.Seek(offset, SeekOrigin.Begin);
459 | if (!MSB)
460 | {
461 | value &= (byte)~(1 << bit);
462 | value |= (byte)(data << bit);
463 | }
464 | else
465 | {
466 | value &= (byte)~(1 << (7 - bit));
467 | value |= (byte)(data << (7 - bit));
468 | }
469 | if (ValidPosition)
470 | {
471 | stream.WriteByte(value);
472 | }
473 | else
474 | {
475 | if (AutoIncreaseStream)
476 | {
477 | if (ChangeLength(Length + (offset - Length) + 1))
478 | {
479 | stream.WriteByte(value);
480 | }
481 | else
482 | {
483 | throw new IOException("Cannot write in an offset bigger than the length of the stream");
484 | }
485 | }
486 | else
487 | {
488 | throw new IOException("Cannot write in an offset bigger than the length of the stream");
489 | }
490 | }
491 | AdvanceBit();
492 | stream.Seek(offset, SeekOrigin.Begin);
493 | }
494 |
495 | ///
496 | /// Write a sequence of bits into the stream
497 | ///
498 | /// [] to write
499 | public void WriteBits(ICollection bits)
500 | {
501 | foreach(Bit b in bits)
502 | {
503 | WriteBit(b);
504 | }
505 | }
506 |
507 | ///
508 | /// Write a sequence of bits into the stream
509 | ///
510 | /// [] to write
511 | /// Number of bits to write
512 | public void WriteBits(ICollection bits, int length)
513 | {
514 | Bit[] b = new Bit[bits.Count];
515 | bits.CopyTo(b, 0);
516 | for (int i=0;i< length;i++)
517 | {
518 | WriteBit(b[i]);
519 | }
520 | }
521 |
522 | ///
523 | /// Write a sequence of bits into the stream
524 | ///
525 | /// [] to write
526 | /// Offset to begin bit writing
527 | /// Number of bits to write
528 | public void WriteBits(Bit[] bits, int offset, int length)
529 | {
530 | for (int i = offset; i < length; i++)
531 | {
532 | WriteBit(bits[i]);
533 | }
534 | }
535 |
536 | #endregion
537 |
538 | #region Read
539 |
540 | ///
541 | /// Read from the current position bit the specified number of bits or bytes and creates a byte[]
542 | ///
543 | /// Number of bits or bytes
544 | /// if true will consider length as byte length, if false it will count the specified length of bits
545 | /// byte[] containing bytes created from current position
546 | public byte[] ReadBytes(long length, bool isBytes = false)
547 | {
548 | if (isBytes)
549 | {
550 | length *= 8;
551 | }
552 | List data = new List();
553 | for (long i = 0; i < length;)
554 | {
555 | byte value = 0;
556 | for (int p = 0; p < 8 && i < length; i++, p++)
557 | {
558 | if (!MSB)
559 | {
560 | value |= (byte)(ReadBit() << p);
561 | }
562 | else
563 | {
564 | value |= (byte)(ReadBit() << (7 - p));
565 | }
566 | }
567 | data.Add(value);
568 | }
569 | return data.ToArray();
570 | }
571 |
572 | ///
573 | /// Read a byte based on the current stream and bit position
574 | ///
575 | public byte ReadByte()
576 | {
577 | return ReadBytes(8)[0];
578 | }
579 |
580 | ///
581 | /// Read a byte made of specified number of bits (1-8)
582 | ///
583 | public byte ReadByte(int bits)
584 | {
585 | if (bits < 0)
586 | {
587 | bits = 0;
588 | }
589 | if(bits > 8)
590 | {
591 | bits = 8;
592 | }
593 | return ReadBytes(bits)[0];
594 | }
595 |
596 | ///
597 | /// Read a signed byte based on the current stream and bit position
598 | ///
599 | public sbyte ReadSByte()
600 | {
601 | return (sbyte)ReadBytes(8)[0];
602 | }
603 |
604 | ///
605 | /// Read a sbyte made of specified number of bits (1-8)
606 | ///
607 | public sbyte ReadSByte(int bits)
608 | {
609 | if (bits < 0)
610 | {
611 | bits = 0;
612 | }
613 | if (bits > 8)
614 | {
615 | bits = 8;
616 | }
617 | return (sbyte)ReadBytes(bits)[0];
618 | }
619 |
620 | ///
621 | /// Read a byte based on the current stream and bit position and check if it is 0
622 | ///
623 | public bool ReadBool()
624 | {
625 | return ReadBytes(8)[0] == 0 ? false : true;
626 | }
627 |
628 | ///
629 | /// Read a char based on the current stream and bit position and the encoding
630 | ///
631 | public char ReadChar()
632 | {
633 | return encoding.GetChars(ReadBytes(encoding.GetMaxByteCount(1) * 8))[0];
634 | }
635 |
636 | ///
637 | /// Read a string based on the current stream and bit position and the encoding
638 | ///
639 | /// Length of the string to read
640 | public string ReadString(int length)
641 | {
642 | int bitsPerChar = encoding.GetByteCount(" ") * 8;
643 | return encoding.GetString(ReadBytes(bitsPerChar*length));
644 | }
645 |
646 | ///
647 | /// Read a short based on the current stream and bit position
648 | ///
649 | public short ReadInt16()
650 | {
651 | short value = BitConverter.ToInt16(ReadBytes(16), 0);
652 | return value;
653 | }
654 |
655 | ///
656 | /// Read a 24bit value based on the current stream and bit position
657 | ///
658 | public Int24 ReadInt24()
659 | {
660 | byte[] bytes = ReadBytes(24);
661 | Array.Resize(ref bytes, 4);
662 | Int24 value = BitConverter.ToInt32(bytes, 0);
663 | return value;
664 | }
665 |
666 | ///
667 | /// Read an int based on the current stream and bit position
668 | ///
669 | public int ReadInt32()
670 | {
671 | int value = BitConverter.ToInt32(ReadBytes(32), 0);
672 | return value;
673 | }
674 |
675 | ///
676 | /// Read a 48bit value based on the current stream and bit position
677 | ///
678 | public Int48 ReadInt48()
679 | {
680 | byte[] bytes = ReadBytes(48);
681 | Array.Resize(ref bytes, 8);
682 | Int48 value = BitConverter.ToInt64(bytes, 0);
683 | return value;
684 | }
685 |
686 | ///
687 | /// Read a long based on the current stream and bit position
688 | ///
689 | public long ReadInt64()
690 | {
691 | long value = BitConverter.ToInt64(ReadBytes(64), 0);
692 | return value;
693 | }
694 |
695 | ///
696 | /// Read a ushort based on the current stream and bit position
697 | ///
698 | public ushort ReadUInt16()
699 | {
700 | ushort value = BitConverter.ToUInt16(ReadBytes(16), 0);
701 | return value;
702 | }
703 |
704 | ///
705 | /// Read an unsigned 24bit value based on the current stream and bit position
706 | ///
707 | public UInt24 ReadUInt24()
708 | {
709 | byte[] bytes = ReadBytes(24);
710 | Array.Resize(ref bytes, 4);
711 | UInt24 value = BitConverter.ToUInt32(bytes, 0);
712 | return value;
713 | }
714 |
715 | ///
716 | /// Read an uint based on the current stream and bit position
717 | ///
718 | public uint ReadUInt32()
719 | {
720 | uint value = BitConverter.ToUInt32(ReadBytes(32), 0);
721 | return value;
722 | }
723 |
724 | ///
725 | /// Read an unsigned 48bit value based on the current stream and bit position
726 | ///
727 | public UInt48 ReadUInt48()
728 | {
729 | byte[] bytes = ReadBytes(48);
730 | Array.Resize(ref bytes, 8);
731 | UInt48 value = BitConverter.ToUInt64(bytes, 0);
732 | return value;
733 | }
734 |
735 | ///
736 | /// Read an ulong based on the current stream and bit position
737 | ///
738 | public ulong ReadUInt64()
739 | {
740 | ulong value = BitConverter.ToUInt64(ReadBytes(64), 0);
741 | return value;
742 | }
743 |
744 | #endregion
745 |
746 | #region Write
747 |
748 | ///
749 | /// Writes as bits a byte[] by a specified number of bits or bytes
750 | ///
751 | /// byte[] to write
752 | /// Number of bits or bytes to use from the array
753 | /// if true will consider length as byte length, if false it will count the specified length of bits
754 | public void WriteBytes(byte[] data, long length, bool isBytes = false)
755 | {
756 | if (isBytes)
757 | {
758 | length *= 8;
759 | }
760 | int position = 0;
761 | for (long i = 0; i < length;)
762 | {
763 | byte value = 0;
764 | for (int p = 0; p < 8 && i < length; i++, p++)
765 | {
766 | if (!MSB)
767 | {
768 | value = (byte)((data[position] >> p) & 1);
769 | }
770 | else
771 | {
772 | value = (byte)((data[position] >> (7 - p)) & 1);
773 | }
774 | WriteBit(value);
775 | }
776 | position++;
777 | }
778 | }
779 |
780 | ///
781 | /// Write a byte value based on the current stream and bit position
782 | ///
783 | public void WriteByte(byte value)
784 | {
785 | WriteBytes(new byte[] { value }, 8);
786 | }
787 |
788 | ///
789 | /// Write a byte value based on the current stream and bit position
790 | ///
791 | public void WriteByte(byte value, int bits)
792 | {
793 | if (bits < 0)
794 | {
795 | bits = 0;
796 | }
797 | if (bits > 8)
798 | {
799 | bits = 8;
800 | }
801 | WriteBytes(new byte[] { value }, bits);
802 | }
803 |
804 | ///
805 | /// Write a byte value based on the current stream and bit position
806 | ///
807 | public void WriteSByte(sbyte value)
808 | {
809 | WriteBytes(new byte[] { (byte)value }, 8);
810 | }
811 |
812 | ///
813 | /// Write a byte value based on the current stream and bit position
814 | ///
815 | public void WriteSByte(sbyte value, int bits)
816 | {
817 | if (bits < 0)
818 | {
819 | bits = 0;
820 | }
821 | if (bits > 8)
822 | {
823 | bits = 8;
824 | }
825 | WriteBytes(new byte[] { (byte)value }, bits);
826 | }
827 |
828 | ///
829 | /// Write a bool value as 0:false, 1:true as byte based on the current stream and bit position
830 | ///
831 | public void WriteBool(bool value)
832 | {
833 | WriteBytes(new byte[] { value ? (byte)1 : (byte)0 }, 8);
834 | }
835 |
836 | ///
837 | /// Write a char value based on the encoding
838 | ///
839 | public void WriteChar(char value)
840 | {
841 | byte[] bytes = encoding.GetBytes(new char[] { value }, 0, 1);
842 | WriteBytes(bytes, bytes.Length*8);
843 | }
844 |
845 | ///
846 | /// Write a string based on the encoding
847 | ///
848 | public void WriteString(string value)
849 | {
850 | byte[] bytes = encoding.GetBytes(value);
851 | WriteBytes(bytes, bytes.Length * 8);
852 | }
853 |
854 | ///
855 | /// Write a short value based on the current stream and bit position
856 | ///
857 | public void WriteInt16(short value)
858 | {
859 | WriteBytes(BitConverter.GetBytes(value), 16);
860 | }
861 |
862 | ///
863 | /// Write a 24bit value based on the current stream and bit position
864 | ///
865 | public void WriteInt24(Int24 value)
866 | {
867 | WriteBytes(BitConverter.GetBytes(value), 24);
868 | }
869 |
870 | ///
871 | /// Write an int value based on the current stream and bit position
872 | ///
873 | public void WriteInt32(int value)
874 | {
875 | WriteBytes(BitConverter.GetBytes(value), 32);
876 | }
877 |
878 | ///
879 | /// Write a 48bit value based on the current stream and bit position
880 | ///
881 | public void WriteInt48(Int48 value)
882 | {
883 | WriteBytes(BitConverter.GetBytes(value), 48);
884 | }
885 |
886 | ///
887 | /// Write a long value based on the current stream and bit position
888 | ///
889 | public void WriteInt64(long value)
890 | {
891 | WriteBytes(BitConverter.GetBytes(value), 64);
892 | }
893 |
894 | ///
895 | /// Write an ushort value based on the current stream and bit position
896 | ///
897 | public void WriteUInt16(ushort value)
898 | {
899 | WriteBytes(BitConverter.GetBytes(value), 16);
900 | }
901 |
902 | ///
903 | /// Write an unsigned 24bit value based on the current stream and bit position
904 | ///
905 | public void WriteUInt24(UInt24 value)
906 | {
907 | WriteBytes(BitConverter.GetBytes(value), 24);
908 | }
909 |
910 | ///
911 | /// Write an uint value based on the current stream and bit position
912 | ///
913 | public void WriteUInt32(uint value)
914 | {
915 | WriteBytes(BitConverter.GetBytes(value), 32);
916 | }
917 |
918 | ///
919 | /// Write an unsigned 48bit value based on the current stream and bit position
920 | ///
921 | public void WriteUInt48(UInt48 value)
922 | {
923 | WriteBytes(BitConverter.GetBytes(value), 48);
924 | }
925 |
926 | ///
927 | /// Write an ulong value based on the current stream and bit position
928 | ///
929 | public void WriteUInt64(ulong value)
930 | {
931 | WriteBytes(BitConverter.GetBytes(value), 64);
932 | }
933 |
934 | #endregion
935 |
936 | #region Shifts
937 |
938 | ///
939 | /// Do a bitwise shift on the current position of the stream on bit 0
940 | ///
941 | /// bits to shift
942 | /// true to left shift, false to right shift
943 | public void bitwiseShift(int bits, bool leftShift)
944 | {
945 | if (!ValidPositionWhen(8))
946 | {
947 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
948 | }
949 | Seek(offset, 0);
950 | if (bits != 0 && bits <= 7)
951 | {
952 | byte value = (byte)stream.ReadByte();
953 | if (leftShift)
954 | {
955 | value = (byte)(value << bits);
956 | }
957 | else
958 | {
959 | value = (byte)(value >> bits);
960 | }
961 | Seek(offset, 0);
962 | stream.WriteByte(value);
963 | }
964 | bit = 0;
965 | offset++;
966 | }
967 |
968 | ///
969 | /// Do a bitwise shift on the current position of the stream on current bit
970 | ///
971 | /// bits to shift
972 | /// true to left shift, false to right shift
973 | public void bitwiseShiftOnBit(int bits, bool leftShift)
974 | {
975 | if (!ValidPositionWhen(8))
976 | {
977 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
978 | }
979 | Seek(offset, bit);
980 | if (bits != 0 && bits <= 7)
981 | {
982 | byte value = ReadByte();
983 | if (leftShift)
984 | {
985 | value = (byte)(value << bits);
986 | }
987 | else
988 | {
989 | value = (byte)(value >> bits);
990 | }
991 | offset--;
992 | Seek(offset, bit);
993 | WriteByte(value);
994 | }
995 | offset++;
996 | }
997 |
998 | ///
999 | /// Do a circular shift on the current position of the stream on bit 0
1000 | ///
1001 | /// bits to shift
1002 | /// true to left shift, false to right shift
1003 | public void circularShift(int bits, bool leftShift)
1004 | {
1005 | if (!ValidPositionWhen(8))
1006 | {
1007 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1008 | }
1009 | Seek(offset, 0);
1010 | if (bits != 0 && bits <= 7)
1011 | {
1012 | byte value = (byte)stream.ReadByte();
1013 | if (leftShift)
1014 | {
1015 | value = (byte)(value << bits | value >> (8 - bits));
1016 | }
1017 | else
1018 | {
1019 | value = (byte)(value >> bits | value << (8 - bits));
1020 | }
1021 | Seek(offset, 0);
1022 | stream.WriteByte(value);
1023 | }
1024 | bit = 0;
1025 | offset++;
1026 | }
1027 |
1028 | ///
1029 | /// Do a circular shift on the current position of the stream on current bit
1030 | ///
1031 | /// bits to shift
1032 | /// true to left shift, false to right shift
1033 | public void circularShiftOnBit(int bits, bool leftShift)
1034 | {
1035 | if (!ValidPositionWhen(8))
1036 | {
1037 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1038 | }
1039 | Seek(offset, bit);
1040 | if (bits != 0 && bits <= 7)
1041 | {
1042 | byte value = ReadByte();
1043 | if (leftShift)
1044 | {
1045 | value = (byte)(value << bits | value >> (8 - bits));
1046 | }
1047 | else
1048 | {
1049 | value = (byte)(value >> bits | value << (8 - bits));
1050 | }
1051 | offset--;
1052 | Seek(offset, bit);
1053 | WriteByte(value);
1054 | }
1055 | offset++;
1056 | }
1057 |
1058 | #endregion
1059 |
1060 | #region Bitwise Operators
1061 |
1062 | ///
1063 | /// Apply an and operator on the current stream and bit position byte and advances one byte position
1064 | ///
1065 | /// Byte value to apply and
1066 | public void And(byte x)
1067 | {
1068 | if (!ValidPositionWhen(8))
1069 | {
1070 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1071 | }
1072 | Seek(offset, bit);
1073 | byte value = ReadByte();
1074 | offset--;
1075 | Seek(offset, bit);
1076 | WriteByte((byte)(value & x));
1077 | }
1078 |
1079 | ///
1080 | /// Apply an or operator on the current stream and bit position byte and advances one byte position
1081 | ///
1082 | /// Byte value to apply or
1083 | public void Or(byte x)
1084 | {
1085 | if (!ValidPositionWhen(8))
1086 | {
1087 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1088 | }
1089 | Seek(offset, bit);
1090 | byte value = ReadByte();
1091 | offset--;
1092 | Seek(offset, bit);
1093 | WriteByte((byte)(value | x));
1094 | }
1095 |
1096 | ///
1097 | /// Apply a xor operator on the current stream and bit position byte and advances one byte position
1098 | ///
1099 | /// Byte value to apply xor
1100 | public void Xor(byte x)
1101 | {
1102 | if (!ValidPositionWhen(8))
1103 | {
1104 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1105 | }
1106 | Seek(offset, bit);
1107 | byte value = ReadByte();
1108 | offset--;
1109 | Seek(offset, bit);
1110 | WriteByte((byte)(value ^ x));
1111 | }
1112 |
1113 | ///
1114 | /// Apply a not operator on the current stream and bit position byte and advances one byte position
1115 | ///
1116 | public void Not()
1117 | {
1118 | if (!ValidPositionWhen(8))
1119 | {
1120 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1121 | }
1122 | Seek(offset, bit);
1123 | byte value = ReadByte();
1124 | offset--;
1125 | Seek(offset, bit);
1126 | WriteByte((byte)(~value));
1127 | }
1128 |
1129 | ///
1130 | /// Apply an and operator on the current stream and bit position and advances one bit position
1131 | ///
1132 | /// Bit value to apply and
1133 | public void BitAnd(Bit x)
1134 | {
1135 | if (!ValidPosition)
1136 | {
1137 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1138 | }
1139 | Seek(offset, bit);
1140 | Bit value = ReadBit();
1141 | ReturnBit();
1142 | WriteBit(x & value);
1143 | }
1144 |
1145 | ///
1146 | /// Apply an or operator on the current stream and bit position and advances one bit position
1147 | ///
1148 | /// Bit value to apply or
1149 | public void BitOr(Bit x)
1150 | {
1151 | if (!ValidPosition)
1152 | {
1153 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1154 | }
1155 | Seek(offset, bit);
1156 | Bit value = ReadBit();
1157 | ReturnBit();
1158 | WriteBit(x | value);
1159 | }
1160 |
1161 | ///
1162 | /// Apply a xor operator on the current stream and bit position and advances one bit position
1163 | ///
1164 | /// Bit value to apply xor
1165 | public void BitXor(Bit x)
1166 | {
1167 | if (!ValidPosition)
1168 | {
1169 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1170 | }
1171 | Seek(offset, bit);
1172 | Bit value = ReadBit();
1173 | ReturnBit();
1174 | WriteBit(x ^ value);
1175 | }
1176 |
1177 | ///
1178 | /// Apply a not operator on the current stream and bit position and advances one bit position
1179 | ///
1180 | public void BitNot()
1181 | {
1182 | if (!ValidPosition)
1183 | {
1184 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1185 | }
1186 | Seek(offset, bit);
1187 | Bit value = ReadBit();
1188 | ReturnBit();
1189 | WriteBit(~value);
1190 | }
1191 |
1192 | ///
1193 | /// Reverses the bit order on the byte in the current position of the stream
1194 | ///
1195 | public void ReverseBits()
1196 | {
1197 | if (!ValidPosition)
1198 | {
1199 | throw new IOException("Cannot read in an offset bigger than the length of the stream");
1200 | }
1201 | Seek(offset, 0);
1202 | byte value = ReadByte();
1203 | offset--;
1204 | Seek(offset, 0);
1205 | WriteByte(value.ReverseBits());
1206 | }
1207 |
1208 | #endregion
1209 |
1210 | }
1211 | }
1212 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/Int24.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace BitStreams
4 | {
5 | ///
6 | /// Represents a 24-bit signed integer
7 | ///
8 | [Serializable]
9 | internal struct Int24
10 | {
11 | private byte b0, b1, b2;
12 | private Bit sign;
13 |
14 | private Int24(int value)
15 | {
16 | this.b0 = (byte)(value & 0xFF);
17 | this.b1 = (byte)((value >> 8) & 0xFF);
18 | this.b2 = (byte)((value >> 16) & 0x7F);
19 | this.sign = (byte)((value >> 23) & 1);
20 | }
21 |
22 | public static implicit operator Int24(int value)
23 | {
24 | return new Int24(value);
25 | }
26 |
27 | public static implicit operator int (Int24 i)
28 | {
29 | int value = (i.b0 | (i.b1 << 8) | (i.b2 << 16));
30 | return -(i.sign << 23) + value;
31 | }
32 |
33 | public Bit GetBit(int index)
34 | {
35 | return (this >> index);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/Int48.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace BitStreams
4 | {
5 | ///
6 | /// Represents a 48-bit signed integer
7 | ///
8 | [Serializable]
9 | internal struct Int48
10 | {
11 | private byte b0, b1, b2, b3, b4, b5;
12 | private Bit sign;
13 |
14 | private Int48(long value)
15 | {
16 | this.b0 = (byte)(value & 0xFF);
17 | this.b1 = (byte)((value >> 8) & 0xFF);
18 | this.b2 = (byte)((value >> 16) & 0xFF);
19 | this.b3 = (byte)((value >> 24) & 0xFF);
20 | this.b4 = (byte)((value >> 32) & 0xFF);
21 | this.b5 = (byte)((value >> 40) & 0x7F);
22 | this.sign = (byte)((value >> 47) & 1);
23 | }
24 |
25 | public static implicit operator Int48(long value)
26 | {
27 | return new Int48(value);
28 | }
29 |
30 | public static implicit operator long (Int48 i)
31 | {
32 | long value = i.b0 + (i.b1 << 8) + (i.b2 << 16) + ((long)i.b3 << 24) + ((long)i.b4 << 32) + ((long)i.b5 << 40);
33 | return -((long)i.sign << 47) + value;
34 | }
35 |
36 | public Bit GetBit(int index)
37 | {
38 | return (byte)(this >> index);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/UInt24.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace BitStreams
4 | {
5 | ///
6 | /// Represents a 24-bit unsigned integer
7 | ///
8 | [Serializable]
9 | internal struct UInt24
10 | {
11 | private byte b0, b1, b2;
12 |
13 | private UInt24(uint value)
14 | {
15 | this.b0 = (byte)(value & 0xFF);
16 | this.b1 = (byte)((value >> 8) & 0xFF);
17 | this.b2 = (byte)((value >> 16) & 0xFF);
18 | }
19 |
20 | public static implicit operator UInt24(uint value)
21 | {
22 | return new UInt24(value);
23 | }
24 |
25 | public static implicit operator uint (UInt24 i)
26 | {
27 | return (uint)(i.b0 | (i.b1 << 8) | (i.b2 << 16));
28 | }
29 |
30 | public Bit GetBit(int index)
31 | {
32 | return (byte)(this >> index);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Fmod5Sharp/BitStreams/UInt48.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace BitStreams
4 | {
5 | ///
6 | /// Represents a 48-bit unsigned integer
7 | ///
8 | [Serializable]
9 | internal struct UInt48
10 | {
11 | private byte b0, b1, b2, b3, b4, b5;
12 |
13 | private UInt48(ulong value)
14 | {
15 | this.b0 = (byte)(value & 0xFF);
16 | this.b1 = (byte)((value >> 8) & 0xFF);
17 | this.b2 = (byte)((value >> 16) & 0xFF);
18 | this.b3 = (byte)((value >> 24) & 0xFF);
19 | this.b4 = (byte)((value >> 32) & 0xFF);
20 | this.b5 = (byte)((value >> 40) & 0xFF);
21 | }
22 |
23 | public static implicit operator UInt48(ulong value)
24 | {
25 | return new UInt48(value);
26 | }
27 |
28 | public static implicit operator ulong (UInt48 i)
29 | {
30 | ulong value = (i.b0 + ((ulong)i.b1 << 8) + ((ulong)i.b2 << 16) + ((ulong)i.b3 << 24) + ((ulong)i.b4 << 32) + ((ulong)i.b5 << 40));
31 | return value;
32 | }
33 |
34 | public Bit GetBit(int index)
35 | {
36 | return (byte)(this >> index);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/ChannelChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal class ChannelChunkData : IChunkData
6 | {
7 | public byte NumChannels;
8 |
9 | public void Read(BinaryReader reader, uint expectedSize)
10 | {
11 | NumChannels = reader.ReadByte();
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/DspCoefficientsBlockData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Fmod5Sharp.FmodTypes;
6 |
7 | namespace Fmod5Sharp.ChunkData
8 | {
9 | public class DspCoefficientsBlockData : IChunkData
10 | {
11 | public List[] ChannelData;
12 | private readonly FmodSampleMetadata _sampleMetadata;
13 |
14 | public DspCoefficientsBlockData(FmodSampleMetadata sampleMetadata)
15 | {
16 | _sampleMetadata = sampleMetadata;
17 | ChannelData = new List[_sampleMetadata.Channels];
18 | for (var i = 0; i < _sampleMetadata.Channels; i++)
19 | ChannelData[i] = new();
20 | }
21 |
22 | public void Read(BinaryReader reader, uint expectedSize)
23 | {
24 | for (var ch = 0; ch < _sampleMetadata.Channels; ch++)
25 | {
26 | //0x2E bytes per channel. First 0x20 (=> 0x10 shorts) are the coefficients.
27 | for (var i = 0; i < 16; i++)
28 | {
29 | //We can't use ReadInt16 here because BinaryReader is little-endian, and FSB5 encodes this data big-endian
30 | //So instead, read 2 bytes, reverse, then convert to short.
31 | ChannelData[ch].Add(BitConverter.ToInt16(reader.ReadBytes(2).Reverse().ToArray(), 0));
32 | }
33 | //Extra 0xE = 14 bytes
34 | reader.ReadInt64();
35 | reader.ReadInt32();
36 | reader.ReadInt16();
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/FrequencyChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal class FrequencyChunkData : IChunkData
6 | {
7 | public uint ActualFrequencyId;
8 |
9 | public void Read(BinaryReader reader, uint expectedSize)
10 | {
11 | ActualFrequencyId = reader.ReadUInt32();
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/IChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal interface IChunkData
6 | {
7 | public void Read(BinaryReader reader, uint expectedSize);
8 | }
9 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/LoopChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal class LoopChunkData : IChunkData
6 | {
7 | public uint LoopStart;
8 | public uint LoopEnd;
9 |
10 | public void Read(BinaryReader reader, uint expectedSize)
11 | {
12 | LoopStart = reader.ReadUInt32();
13 | LoopEnd = reader.ReadUInt32();
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/UnknownChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal class UnknownChunkData : IChunkData
6 | {
7 | public byte[] UnknownData = new byte[0];
8 |
9 | public void Read(BinaryReader reader, uint expectedSize)
10 | {
11 | UnknownData = reader.ReadBytes((int)expectedSize);
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/ChunkData/VorbisChunkData.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.ChunkData
4 | {
5 | internal class VorbisChunkData : IChunkData
6 | {
7 | public uint Crc32;
8 |
9 | public void Read(BinaryReader reader, uint expectedSize)
10 | {
11 | Crc32 = reader.ReadUInt32();
12 | byte[] unknown = reader.ReadBytes((int)(expectedSize - 4));
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/CodecRebuilders/FmodGcadPcmRebuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using Fmod5Sharp.ChunkData;
5 | using Fmod5Sharp.FmodTypes;
6 | using Fmod5Sharp.Util;
7 | using NAudio.Wave;
8 |
9 | namespace Fmod5Sharp.CodecRebuilders
10 | {
11 | public static class FmodGcadPcmRebuilder
12 | {
13 | private const int BytesPerFrame = 8;
14 | private const int SamplesPerFrame = 14;
15 | private const int NibblesPerFrame = 16;
16 |
17 | private static short[] GetPcmData(FmodSample sample)
18 | {
19 | //Constants for this sample
20 | var sampleCount = ByteCountToSampleCount(sample.SampleBytes.Length);
21 | var frameCount = Math.Ceiling((double)sampleCount / SamplesPerFrame);
22 |
23 | //Result array
24 | var pcmData = new short[sampleCount];
25 |
26 | //Read the data we need from the stream
27 | var adpcm = sample.SampleBytes;
28 | var coeffChunk = (DspCoefficientsBlockData)sample.Metadata.Chunks.First(c => c.ChunkType == FmodSampleChunkType.DSPCOEFF).ChunkData;
29 | var coeffs = coeffChunk.ChannelData[0];
30 |
31 | //Initialize indices
32 | var currentSample = 0;
33 | var outIndex = 0;
34 | var inIndex = 0;
35 |
36 | //History values - current value is based on previous ones
37 | short hist1 = 0;
38 | short hist2 = 0;
39 |
40 | for (var i = 0; i < frameCount; i++)
41 | {
42 | //Each byte is a scale and a predictor
43 | var combined = adpcm[inIndex++];
44 | var scale = 1 << (combined & 0xF);
45 | var predictor = combined >> 4;
46 |
47 | //Coefficients are based on the predictor value
48 | var coeff1 = coeffs[predictor * 2];
49 | var coeff2 = coeffs[predictor * 2 + 1];
50 |
51 | //Either read 14 - all the samples in this frame - or however many are left, if this is a partial frame
52 | var samplesToRead = Math.Min(SamplesPerFrame, sampleCount - currentSample);
53 |
54 | for (var s = 0; s < samplesToRead; s++)
55 | {
56 | //Raw value
57 | var adpcmSample = (int) (s % 2 == 0 ? Utils.GetHighNibbleSigned(adpcm[inIndex]) : Utils.GetLowNibbleSigned(adpcm[inIndex++]));
58 |
59 | //Adaptive processing
60 | adpcmSample = (adpcmSample * scale) << 11;
61 | adpcmSample = (adpcmSample + 1024 + coeff1 * hist1 + coeff2 * hist2) >> 11;
62 | var clampedSample = Clamp16(adpcmSample);
63 |
64 | //Bump history along
65 | hist2 = hist1;
66 | hist1 = clampedSample;
67 |
68 | //Set result
69 | pcmData[outIndex++] = clampedSample;
70 |
71 | //Move to next sample
72 | currentSample++;
73 | }
74 | }
75 |
76 | return pcmData;
77 | }
78 |
79 | public static byte[] Rebuild(FmodSample sample)
80 | {
81 | var numChannels = sample.Metadata.IsStereo ? 2 : 1;
82 | var format = WaveFormat.CreateCustomFormat(
83 | WaveFormatEncoding.Pcm,
84 | sample.Metadata.Frequency,
85 | numChannels,
86 | sample.Metadata.Frequency * numChannels * 2,
87 | numChannels * 2,
88 | 16
89 | );
90 | using var stream = new MemoryStream();
91 | using var writer = new WaveFileWriter(stream, format);
92 |
93 | var pcmShorts = GetPcmData(sample);
94 |
95 | writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
96 |
97 | return stream.ToArray();
98 | }
99 |
100 | private static int NibbleCountToSampleCount(int nibbleCount)
101 | {
102 | var frames = nibbleCount / NibblesPerFrame;
103 | var extraNibbles = nibbleCount % NibblesPerFrame;
104 | var extraSamples = extraNibbles < 2 ? 0 : extraNibbles - 2;
105 |
106 | return SamplesPerFrame * frames + extraSamples;
107 | }
108 |
109 | private static int ByteCountToSampleCount(int byteCount) => NibbleCountToSampleCount(byteCount * 2);
110 |
111 | private static short Clamp16(int value)
112 | {
113 | if (value > short.MaxValue)
114 | return short.MaxValue;
115 | if (value < short.MinValue)
116 | return short.MinValue;
117 | return (short)value;
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/CodecRebuilders/FmodImaAdPcmRebuilder.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Fmod5Sharp.FmodTypes;
3 | using Fmod5Sharp.Util;
4 | using NAudio.Wave;
5 |
6 | namespace Fmod5Sharp.CodecRebuilders
7 | {
8 | public static class FmodImaAdPcmRebuilder
9 | {
10 | public const int SamplesPerFramePerChannel = 0x40;
11 |
12 | static readonly int[] ADPCMTable =
13 | {
14 | 7, 8, 9, 10, 11, 12, 13, 14,
15 | 16, 17, 19, 21, 23, 25, 28, 31,
16 | 34, 37, 41, 45, 50, 55, 60, 66,
17 | 73, 80, 88, 97, 107, 118, 130, 143,
18 | 157, 173, 190, 209, 230, 253, 279, 307,
19 | 337, 371, 408, 449, 494, 544, 598, 658,
20 | 724, 796, 876, 963, 1060, 1166, 1282, 1411,
21 | 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
22 | 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
23 | 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
24 | 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
25 | 32767
26 | };
27 |
28 | private static readonly int[] IMA_IndexTable =
29 | {
30 | -1, -1, -1, -1, 2, 4, 6, 8,
31 | -1, -1, -1, -1, 2, 4, 6, 8,
32 | };
33 |
34 | private static void ExpandNibble(MemoryStream stream, long byteOffset, int nibbleShift, ref int hist, ref int stepIndex)
35 | {
36 | //Read the raw nibble
37 | stream.Seek(byteOffset, SeekOrigin.Begin);
38 | var sampleNibble = (stream.ReadByte() >> nibbleShift) & 0xf;
39 |
40 | //Initial value for the sample is the previous value
41 | var sampleDecoded = hist;
42 |
43 | //Apply the step from the table of values above
44 | var step = ADPCMTable[stepIndex];
45 |
46 | var delta = step >> 3;
47 | if ((sampleNibble & 1) != 0) delta += step >> 2;
48 | if ((sampleNibble & 2) != 0) delta += step >> 1;
49 | if ((sampleNibble & 4) != 0) delta += step;
50 | if ((sampleNibble & 8) != 0) delta = -delta;
51 |
52 | //Sample changes by the delta
53 | sampleDecoded += delta;
54 |
55 | //New sample becomes the previous value, but clamped to a short.
56 | hist = Utils.Clamp((short)sampleDecoded, short.MinValue, short.MaxValue);
57 |
58 | //Step index changes based on what was stored in the file, clamped to fit in the array
59 | stepIndex += IMA_IndexTable[sampleNibble];
60 | stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
61 | }
62 |
63 | private static short[] DecodeSamplesFsbIma(FmodSample sample)
64 | {
65 | const int blockSamples = 0x40;
66 |
67 | var numChannels = (int)sample.Metadata.Channels;
68 |
69 | using var stream = new MemoryStream(sample.SampleBytes);
70 | using var reader = new BinaryReader(stream);
71 |
72 | var ret = new short[sample.Metadata.SampleCount * 2];
73 |
74 | // Calculate frame count from sample count
75 | var numFrames = (int)sample.Metadata.SampleCount / SamplesPerFramePerChannel;
76 |
77 | for (var channel = 0; channel < numChannels; channel++)
78 | {
79 | var sampleIndex = channel;
80 |
81 | for (var frameNum = 0; frameNum < numFrames; frameNum++)
82 | {
83 | //Offset of this frame in the entire sample data
84 | var frameOffset = 0x24 * numChannels * frameNum;
85 |
86 | //Read frame header
87 | var headerIndex = frameOffset + 4 * channel;
88 | stream.Seek(headerIndex, SeekOrigin.Begin);
89 | int hist = reader.ReadInt16();
90 | stream.Seek(headerIndex + 2, SeekOrigin.Begin);
91 | int stepIndex = reader.ReadByte();
92 |
93 | //Calculate initial sample value for this frame
94 | stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
95 | ret[sampleIndex] = (short)hist;
96 | sampleIndex += numChannels;
97 |
98 | for (var sampleNum = 1; sampleNum <= SamplesPerFramePerChannel; sampleNum++)
99 | {
100 | //Offset of this sample in the entire sample data
101 | //Note that this is, slightly confusingly, two different definitions of the word sample.
102 | //What i mean is "index of this value within the current frame which is part of one of the channels in the FMOD 'sample', which should really be called a sound file"
103 | var byteOffset = frameOffset + 4 * 2 + 4 * (channel % 2) + 4 * 2 * ((sampleNum - 1) / 8) + ((sampleNum - 1) % 8) / 2;
104 | if (numChannels == 0)
105 | byteOffset = frameOffset + 4 + (sampleNum - 1) / 2;
106 |
107 | //Each sample is only half a byte, so odd samples use the upper half of the byte, and even samples use the lower half.
108 | var nibbleShift = ((sampleNum - 1) & 1) != 0 ? 4 : 0;
109 |
110 | if (sampleNum < blockSamples)
111 | {
112 | //Apply the IMA algorithm to convert this nibble into a full byte of data.
113 | ExpandNibble(stream, byteOffset, nibbleShift, ref hist, ref stepIndex);
114 |
115 | //Move to next sample
116 | ret[sampleIndex] = ((short)hist);
117 | sampleIndex += numChannels;
118 | }
119 | }
120 | }
121 | }
122 |
123 | return ret;
124 | }
125 |
126 | private static short[] DecodeSamplesXboxIma(FmodSample sample)
127 | {
128 | //This is a simplified version of the algorithm, because we know that this will only ever be called if we have one channel.
129 |
130 | const int frameSize = 0x24;
131 |
132 | using var stream = new MemoryStream(sample.SampleBytes);
133 | using var reader = new BinaryReader(stream);
134 |
135 | var numFrames = (int)sample.Metadata.SampleCount / SamplesPerFramePerChannel;
136 |
137 | var ret = new short[sample.Metadata.SampleCount];
138 | var sampleIndex = 0;
139 |
140 | for (var frameNum = 0; frameNum < numFrames; frameNum++)
141 | {
142 |
143 |
144 | //Offset of this frame in the entire sample data
145 | var frameOffset = frameSize * frameNum;
146 |
147 | //Read frame header
148 | stream.Seek(frameOffset, SeekOrigin.Begin);
149 | int hist = reader.ReadInt16();
150 | stream.Seek(frameOffset + 2, SeekOrigin.Begin);
151 | int stepIndex = reader.ReadByte();
152 |
153 | //Calculate initial sample value for this frame
154 | stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
155 | ret[sampleIndex] = (short)hist;
156 | sampleIndex ++;
157 |
158 | for (var sampleNum = 1; sampleNum <= SamplesPerFramePerChannel; sampleNum++)
159 | {
160 | //Offset of this sample in the entire sample data
161 | //Note that this is, slightly confusingly, two different definitions of the word sample.
162 | //What i mean is "index of this value within the current frame which is part of one of the channels in the FMOD 'sample', which should really be called a sound file"
163 | var byteOffset = frameOffset + 4 + (sampleNum - 1) / 2;
164 |
165 | //Each sample is only half a byte, so odd samples use the upper half of the byte, and even samples use the lower half.
166 | var nibbleShift = ((sampleNum - 1) & 1) != 0 ? 4 : 0;
167 |
168 | if (sampleNum < SamplesPerFramePerChannel)
169 | {
170 | //Apply the IMA algorithm to convert this nibble into a full byte of data.
171 | ExpandNibble(stream, byteOffset, nibbleShift, ref hist, ref stepIndex);
172 |
173 | //Move to next sample
174 | ret[sampleIndex] = ((short)hist);
175 | sampleIndex ++;
176 | }
177 | }
178 | }
179 |
180 | return ret;
181 | }
182 |
183 | public static byte[] Rebuild(FmodSample sample)
184 | {
185 | var numChannels = sample.Metadata.IsStereo ? 2 : 1;
186 | var format = WaveFormat.CreateCustomFormat(
187 | WaveFormatEncoding.Pcm,
188 | sample.Metadata.Frequency,
189 | numChannels,
190 | sample.Metadata.Frequency * numChannels * 2,
191 | numChannels * 2,
192 | 16
193 | );
194 | using var stream = new MemoryStream();
195 | using var writer = new WaveFileWriter(stream, format);
196 |
197 | var pcmShorts = numChannels == 1 ? DecodeSamplesXboxIma(sample) : DecodeSamplesFsbIma(sample);
198 |
199 | writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
200 |
201 | return stream.ToArray();
202 | }
203 | }
204 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/CodecRebuilders/FmodPcmRebuilder.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Fmod5Sharp.FmodTypes;
3 | using NAudio.Wave;
4 |
5 | namespace Fmod5Sharp.CodecRebuilders
6 | {
7 | public static class FmodPcmRebuilder
8 | {
9 | public static byte[] Rebuild(FmodSample sample, FmodAudioType type)
10 | {
11 | var width = type switch
12 | {
13 | FmodAudioType.PCM8 => 1,
14 | FmodAudioType.PCM16 => 2,
15 | FmodAudioType.PCM32 => 4,
16 | _ => throw new($"FmodPcmRebuilder does not support encoding of type {type}"),
17 | };
18 |
19 | var numChannels = sample.Metadata.IsStereo ? 2 : 1;
20 | var format = WaveFormat.CreateCustomFormat(
21 | WaveFormatEncoding.Pcm,
22 | sample.Metadata.Frequency,
23 | numChannels,
24 | sample.Metadata.Frequency * numChannels * width,
25 | numChannels * width,
26 | width * 8
27 | );
28 | using var stream = new MemoryStream();
29 | using var writer = new WaveFileWriter(stream, format);
30 |
31 | writer.Write(sample.SampleBytes, 0, sample.SampleBytes.Length);
32 |
33 | return stream.GetBuffer();
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/CodecRebuilders/FmodVorbisRebuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text.Json;
7 | using Fmod5Sharp.ChunkData;
8 | using Fmod5Sharp.FmodTypes;
9 | using Fmod5Sharp.Util;
10 | using OggVorbisEncoder;
11 |
12 | namespace Fmod5Sharp.CodecRebuilders
13 | {
14 | public static class FmodVorbisRebuilder
15 | {
16 | private static Dictionary? headers;
17 |
18 | private static void LoadVorbisHeaders()
19 | {
20 | using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Fmod5Sharp.Util.vorbis_headers_converted.json")
21 | ?? throw new Exception($"Embedded resources for vorbis header data not found, has the assembly been tampered with?");
22 | using StreamReader reader = new(stream);
23 |
24 | var jsonString = reader.ReadToEnd();
25 | headers = JsonSerializer.Deserialize(jsonString, Fmod5SharpJsonContext.Default.DictionaryUInt32FmodVorbisData);
26 | }
27 |
28 | public static byte[] RebuildOggFile(FmodSample sample)
29 | {
30 | //Need to rebuild the vorbis header, which requires reading the known blobs from the json file.
31 | //This requires knowing the crc32 of the data, which is in a VORBISDATA chunk.
32 | var dataChunk = sample.Metadata.Chunks.FirstOrDefault(f => f.ChunkType == FmodSampleChunkType.VORBISDATA);
33 |
34 | if (dataChunk == null)
35 | {
36 | throw new Exception("Rebuilding Vorbis data requires a VORBISDATA chunk, which wasn't found");
37 | }
38 |
39 | var chunkData = (VorbisChunkData)dataChunk.ChunkData;
40 | var crc32 = chunkData.Crc32;
41 |
42 | //Ok, we have the crc32, now we need to find the header data.
43 | if (headers == null)
44 | LoadVorbisHeaders();
45 | var vorbisData = headers![crc32];
46 |
47 | vorbisData.InitBlockFlags();
48 |
49 | var infoPacket = BuildInfoPacket((byte)sample.Metadata.Channels, sample.Metadata.Frequency);
50 | var commentPacket = BuildCommentPacket("Fmod5Sharp (Samboy063)");
51 | var setupPacket = new OggPacket(vorbisData.HeaderBytes, false, 0, 2);
52 |
53 | //Begin building the final stream
54 | var oggStream = new OggStream(1);
55 | using var outputStream = new MemoryStream();
56 |
57 | oggStream.PacketIn(infoPacket);
58 | oggStream.PacketIn(commentPacket);
59 | oggStream.PacketIn(setupPacket);
60 |
61 | oggStream.FlushAndCopyTo(outputStream, true);
62 |
63 | CopySampleData(vorbisData, sample.SampleBytes, oggStream, outputStream);
64 |
65 | return outputStream.ToArray();
66 | }
67 |
68 | private static void FlushAndCopyTo(this OggStream stream, Stream other, bool force = false)
69 | {
70 | while (stream.PageOut(out var page, force))
71 | {
72 | other.Write(page.Header, 0, page.Header.Length);
73 | other.Write(page.Body, 0, page.Body.Length);
74 | }
75 | }
76 |
77 | private static OggPacket BuildInfoPacket(byte channels, int frequency)
78 | {
79 | using var memStream = new MemoryStream(30);
80 | using var writer = new BinaryWriter(memStream);
81 |
82 | // Packet Type (1)
83 | writer.Write((byte)1);
84 | // Codec (vorbis)
85 | writer.Write(System.Text.Encoding.UTF8.GetBytes("vorbis"));
86 | // Version (0)
87 | writer.Write(0);
88 | // Num channels
89 | writer.Write(channels);
90 | // Frequency
91 | writer.Write(frequency);
92 |
93 | //Leave max, nominal, and min bitrate at 0 (to be auto calculated)
94 | writer.Write(0);
95 | writer.Write(0);
96 | writer.Write(0);
97 | // Block Size
98 | // 4 bits - Short Blocksize [-3]
99 | // 4 bits - Long Blocksize [-3]
100 | writer.Write((byte)0b1011_1000);
101 | // Framing Bit (1)
102 | writer.Write((byte)1);
103 |
104 | return new(memStream.ToArray(), false, 0, 0);
105 | }
106 |
107 | private static OggPacket BuildCommentPacket(string vendor)
108 | {
109 | using MemoryStream memoryStream = new();
110 | using BinaryReader binaryReader = new(memoryStream);
111 |
112 | using MemoryStream oggMs = new();
113 | using BinaryWriter fm = new(oggMs);
114 |
115 | fm.Seek(0, SeekOrigin.Begin);
116 |
117 | // Packet Type (3)
118 | fm.Write((byte)3);
119 | fm.Write(System.Text.Encoding.UTF8.GetBytes("vorbis")); // Codec (vorbis)
120 |
121 | //Length-prefixed vendor string
122 | fm.Write(vendor.Length);
123 | fm.Write(System.Text.Encoding.UTF8.GetBytes(vendor));
124 |
125 | // Number of comments (0)
126 | fm.Write(0);
127 | // Framing Bit (1)
128 | fm.Write((byte)1);
129 |
130 | return new(oggMs.ToArray(), false, 0, 1);
131 | }
132 |
133 | private static void CopySampleData(FmodVorbisData vorbisData, byte[] sampleBytes, OggStream oggStream, Stream outputStream)
134 | {
135 | using var inputStream = new MemoryStream(sampleBytes);
136 | using var inputReader = new BinaryReader(inputStream);
137 |
138 | ReadSamplePackets(inputReader, out var packetLength, out var packets);
139 |
140 | var packetNum = 1;
141 | var granulePos = 0;
142 | var previousBlockSize = 0;
143 |
144 | var finalPacketNum = packetLength.Count - 1;
145 |
146 | for (var i = 0; i < packets.Count; i++)
147 | {
148 | var isLast = i == finalPacketNum;
149 | var packet = packets[i];
150 | packetNum++;
151 |
152 | //If the input packet is empty, so is the output block, otherwise calculate based on the vorbis data.
153 | var blockSize = packetLength[i] == 0 ? 0 : vorbisData.GetPacketBlockSize(packet);
154 |
155 | //Calculate next granule position
156 | if (previousBlockSize == 0)
157 | granulePos = 0;
158 | else
159 | granulePos += (blockSize + previousBlockSize) / 4;
160 |
161 | //Set previous block size
162 | previousBlockSize = blockSize;
163 |
164 | //Write the packet to the stream
165 | oggStream.PacketIn(new(packet, isLast, granulePos, packetNum));
166 | oggStream.FlushAndCopyTo(outputStream, isLast);
167 | }
168 | }
169 |
170 | private static void ReadSamplePackets(BinaryReader inputReader, out List packetLengths, out List packets)
171 | {
172 | packetLengths = new();
173 | packets = new();
174 |
175 | while (inputReader.BaseStream.Position + sizeof(ushort) < inputReader.BaseStream.Length)
176 | {
177 | var packetSize = inputReader.ReadUInt16();
178 |
179 | if (packetSize == 0)
180 | break; //EOS
181 |
182 | packetLengths.Add(packetSize);
183 | packets.Add(inputReader.ReadBytes(packetSize));
184 | }
185 | }
186 | }
187 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Fmod5Sharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | Sam Byass (Samboy063)
6 | Release;Debug
7 | embedded
8 | Decoder for FMOD 5 sound banks (FSB files)
9 | true
10 | true
11 | true
12 | 10
13 | enable
14 | Fmod5Sharp
15 | MIT
16 | https://github.com/SamboyCoding/Fmod5Sharp
17 | fmod;audio
18 | x86;x64;AnyCPU
19 | true
20 | git
21 | https://github.com/SamboyCoding/Fmod5Sharp.git
22 | net6.0;netstandard2.0
23 | FMOD5 Sharp
24 | 3.0.1
25 | true
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodAudioHeader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using Fmod5Sharp.ChunkData;
5 | using Fmod5Sharp.Util;
6 |
7 | namespace Fmod5Sharp.FmodTypes
8 | {
9 | public class FmodAudioHeader
10 | {
11 | private static readonly object ChunkReadingLock = new();
12 |
13 | internal readonly bool IsValid;
14 |
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;
23 |
24 | internal readonly List Samples = new();
25 |
26 | public FmodAudioHeader(BinaryReader reader)
27 | {
28 | string magic = reader.ReadString(4);
29 |
30 | if (magic != "FSB5")
31 | {
32 | IsValid = false;
33 | return;
34 | }
35 |
36 | Version = reader.ReadUInt32(); //0x04
37 | NumSamples = reader.ReadUInt32(); //0x08
38 | SizeOfSampleHeaders = reader.ReadUInt32();
39 | SizeOfNameTable = reader.ReadUInt32();
40 | SizeOfData = reader.ReadUInt32(); //0x14
41 | AudioType = (FmodAudioType) reader.ReadUInt32(); //0x18
42 |
43 | reader.ReadUInt32(); //Skip 0x1C which is always 0
44 |
45 | if (Version == 0)
46 | {
47 | SizeOfThisHeader = 0x40;
48 | reader.ReadUInt32(); //Version 0 has an extra field at 0x20 before flags
49 | }
50 | else
51 | {
52 | SizeOfThisHeader = 0x3C;
53 | }
54 |
55 | reader.ReadUInt32(); //Skip 0x20 (flags)
56 |
57 | //128-bit hash
58 | var hashLower = reader.ReadUInt64(); //0x24
59 | var hashUpper = reader.ReadUInt64(); //0x30
60 |
61 | reader.ReadUInt64(); //Skip unknown value at 0x34
62 |
63 | var sampleHeadersStart = reader.Position();
64 | for (var i = 0; i < NumSamples; i++)
65 | {
66 | var sampleMetadata = reader.ReadEndian();
67 |
68 | if (!sampleMetadata.HasAnyChunks)
69 | {
70 | Samples.Add(sampleMetadata);
71 | continue;
72 | }
73 |
74 | lock (ChunkReadingLock)
75 | {
76 | List chunks = new();
77 | FmodSampleChunk.CurrentSample = sampleMetadata;
78 |
79 | FmodSampleChunk nextChunk;
80 | do
81 | {
82 | nextChunk = reader.ReadEndian();
83 | chunks.Add(nextChunk);
84 | } while (nextChunk.MoreChunks);
85 |
86 | FmodSampleChunk.CurrentSample = null;
87 |
88 | if (chunks.FirstOrDefault(c => c.ChunkType == FmodSampleChunkType.FREQUENCY) is { ChunkData: FrequencyChunkData fcd })
89 | {
90 | sampleMetadata.FrequencyId = fcd.ActualFrequencyId;
91 | }
92 |
93 | sampleMetadata.Chunks = chunks;
94 |
95 | Samples.Add(sampleMetadata);
96 | }
97 | }
98 |
99 | IsValid = true;
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodAudioType.cs:
--------------------------------------------------------------------------------
1 | namespace Fmod5Sharp.FmodTypes
2 | {
3 | public enum FmodAudioType : uint
4 | {
5 | NONE = 0,
6 | PCM8 = 1,
7 | PCM16 = 2,
8 | PCM24 = 3,
9 | PCM32 = 4,
10 | PCMFLOAT = 5,
11 | GCADPCM = 6,
12 | IMAADPCM = 7,
13 | VAG = 8,
14 | HEVAG = 9,
15 | XMA = 10,
16 | MPEG = 11,
17 | CELT = 12,
18 | AT9 = 13,
19 | XWMA = 14,
20 | VORBIS = 15,
21 | }
22 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodSample.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using Fmod5Sharp.CodecRebuilders;
3 |
4 | namespace Fmod5Sharp.FmodTypes
5 | {
6 | public class FmodSample
7 | {
8 | public FmodSampleMetadata Metadata;
9 | public byte[] SampleBytes;
10 | public string? Name;
11 | internal FmodSoundBank? MyBank;
12 |
13 | public FmodSample(FmodSampleMetadata metadata, byte[] sampleBytes)
14 | {
15 | Metadata = metadata;
16 | SampleBytes = sampleBytes;
17 | }
18 |
19 | #if NET6_0
20 | public bool RebuildAsStandardFileFormat([NotNullWhen(true)] out byte[]? data, [NotNullWhen(true)] out string? fileExtension)
21 | #else
22 | public bool RebuildAsStandardFileFormat(out byte[]? data, out string? fileExtension)
23 | #endif
24 | {
25 | switch (MyBank!.Header.AudioType)
26 | {
27 | case FmodAudioType.VORBIS:
28 | data = FmodVorbisRebuilder.RebuildOggFile(this);
29 | fileExtension = "ogg";
30 | return data.Length > 0;
31 | case FmodAudioType.PCM8:
32 | case FmodAudioType.PCM16:
33 | case FmodAudioType.PCM32:
34 | data = FmodPcmRebuilder.Rebuild(this, MyBank.Header.AudioType);
35 | fileExtension = "wav";
36 | return data.Length > 0;
37 | case FmodAudioType.GCADPCM:
38 | data = FmodGcadPcmRebuilder.Rebuild(this);
39 | fileExtension = "wav";
40 | return data.Length > 0;
41 | case FmodAudioType.IMAADPCM:
42 | data = FmodImaAdPcmRebuilder.Rebuild(this);
43 | fileExtension = "wav";
44 | return data.Length > 0;
45 | default:
46 | data = null;
47 | fileExtension = null;
48 | return false;
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodSampleChunk.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Fmod5Sharp.ChunkData;
4 | using Fmod5Sharp.Util;
5 |
6 | namespace Fmod5Sharp.FmodTypes
7 | {
8 | internal class FmodSampleChunk : IBinaryReadable
9 | {
10 | internal static FmodSampleMetadata? CurrentSample;
11 |
12 | public FmodSampleChunkType ChunkType;
13 | public uint ChunkSize;
14 | public bool MoreChunks;
15 | #pragma warning disable 8618 //Non-nullable value is not defined.
16 | internal IChunkData ChunkData;
17 | #pragma warning restore 8618
18 |
19 | void IBinaryReadable.Read(BinaryReader reader)
20 | {
21 | var chunkInfoRaw = reader.ReadUInt32();
22 | MoreChunks = chunkInfoRaw.Bits(0, 1) == 1;
23 | ChunkSize = (uint)chunkInfoRaw.Bits(1, 24);
24 | ChunkType = (FmodSampleChunkType) chunkInfoRaw.Bits(25, 7);
25 |
26 | ChunkData = ChunkType switch
27 | {
28 | FmodSampleChunkType.VORBISDATA => new VorbisChunkData(),
29 | FmodSampleChunkType.FREQUENCY => new FrequencyChunkData(),
30 | FmodSampleChunkType.CHANNELS => new ChannelChunkData(),
31 | FmodSampleChunkType.LOOP => new LoopChunkData(),
32 | FmodSampleChunkType.DSPCOEFF => new DspCoefficientsBlockData(CurrentSample!),
33 | _ => new UnknownChunkData(),
34 | };
35 |
36 | var startPos = reader.Position();
37 |
38 | ChunkData.Read(reader, ChunkSize);
39 |
40 | var actualBytesRead = reader.Position() - startPos;
41 |
42 | if (actualBytesRead != ChunkSize)
43 | {
44 | throw new Exception($"Expected fmod sample chunk to read {ChunkSize} bytes, but it only read {actualBytesRead}");
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodSampleChunkType.cs:
--------------------------------------------------------------------------------
1 | namespace Fmod5Sharp.FmodTypes
2 | {
3 | internal enum FmodSampleChunkType : uint
4 | {
5 | CHANNELS = 1,
6 | FREQUENCY = 2,
7 | LOOP = 3,
8 | COMMENT = 4,
9 | XMASEEK = 6,
10 | DSPCOEFF = 7,
11 | ATRAC9CFG = 9,
12 | XWMADATA = 10,
13 | VORBISDATA = 11,
14 | PEAKVOLUME = 13,
15 | VORBISINTRALAYERS = 14,
16 | OPUSDATALEN = 15,
17 | }
18 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodSampleMetadata.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using Fmod5Sharp.Util;
4 |
5 | namespace Fmod5Sharp.FmodTypes
6 | {
7 | public class FmodSampleMetadata : IBinaryReadable
8 | {
9 | internal bool HasAnyChunks;
10 | internal uint FrequencyId;
11 | internal ulong DataOffset;
12 | internal List Chunks = new();
13 | internal int NumChannels;
14 |
15 | public bool IsStereo;
16 | public ulong SampleCount;
17 |
18 | public int Frequency => FsbLoader.Frequencies.TryGetValue(FrequencyId, out var actualFrequency) ? actualFrequency : (int)FrequencyId; //If set by FREQUENCY chunk, id is actual frequency
19 | public uint Channels => (uint)NumChannels;
20 |
21 | void IBinaryReadable.Read(BinaryReader reader)
22 | {
23 | var encoded = reader.ReadUInt64();
24 |
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;
35 | SampleCount = encoded.Bits(34, 30);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FmodTypes/FmodSoundBank.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Fmod5Sharp.FmodTypes
4 | {
5 | public class FmodSoundBank
6 | {
7 | public FmodAudioHeader Header;
8 | public List Samples;
9 |
10 | internal FmodSoundBank(FmodAudioHeader header, List samples)
11 | {
12 | Header = header;
13 | Samples = samples;
14 | Samples.ForEach(s => s.MyBank = this);
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/FsbLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using Fmod5Sharp.FmodTypes;
5 | using Fmod5Sharp.Util;
6 |
7 | namespace Fmod5Sharp
8 | {
9 | public static class FsbLoader
10 | {
11 | internal static readonly Dictionary 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 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 | }
85 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 |
5 | namespace Fmod5Sharp.Util
6 | {
7 | internal static class Extensions
8 | {
9 | internal static T ReadEndian(this BinaryReader reader) where T : IBinaryReadable, new()
10 | {
11 | var t = new T();
12 | t.Read(reader);
13 |
14 | return t;
15 | }
16 |
17 | internal static long Position(this BinaryReader reader) => reader.BaseStream.Position;
18 |
19 | internal static string ReadString(this BinaryReader reader, int length, Encoding? encoding = null)
20 | {
21 | if (encoding == null)
22 | encoding = Encoding.UTF8;
23 |
24 | var bytes = reader.ReadBytes(length);
25 |
26 | return encoding.GetString(bytes);
27 | }
28 |
29 | internal static ulong Bits(this uint raw, int lowestBit, int numBits) => ((ulong)raw).Bits(lowestBit, numBits);
30 |
31 | internal static ulong Bits(this ulong raw, int lowestBit, int numBits)
32 | {
33 | ulong mask = 1;
34 | for (var i = 1; i < numBits; i++)
35 | {
36 | mask = (mask << 1) | 1;
37 | }
38 |
39 | mask <<= lowestBit;
40 |
41 | return (raw & mask) >> lowestBit;
42 | }
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 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/Fmod5SharpJsonContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Fmod5Sharp.Util;
5 |
6 | [JsonSerializable(typeof(Dictionary))]
7 | internal partial class Fmod5SharpJsonContext : JsonSerializerContext
8 | {
9 |
10 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/FmodAudioTypeExtensions.cs:
--------------------------------------------------------------------------------
1 | using Fmod5Sharp.FmodTypes;
2 |
3 | namespace Fmod5Sharp.Util
4 | {
5 | public static class FmodAudioTypeExtensions
6 | {
7 | public static bool IsSupported(this FmodAudioType @this) =>
8 | @this switch
9 | {
10 | FmodAudioType.VORBIS => true,
11 | FmodAudioType.PCM8 => true,
12 | FmodAudioType.PCM16 => true,
13 | FmodAudioType.PCM32 => true,
14 | FmodAudioType.GCADPCM => true,
15 | FmodAudioType.IMAADPCM => true,
16 | _ => false
17 | };
18 |
19 | public static string? FileExtension(this FmodAudioType @this) =>
20 | @this switch
21 | {
22 | FmodAudioType.VORBIS => "ogg",
23 | FmodAudioType.PCM8 => "wav",
24 | FmodAudioType.PCM16 => "wav",
25 | FmodAudioType.PCM32 => "wav",
26 | FmodAudioType.GCADPCM => "wav",
27 | FmodAudioType.IMAADPCM => "wav",
28 | _ => null
29 | };
30 | }
31 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/FmodVorbisData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Text.Json.Serialization;
4 | using BitStreams;
5 |
6 | namespace Fmod5Sharp.Util;
7 |
8 | internal class FmodVorbisData
9 | {
10 | [JsonPropertyName("headerBytes")]
11 | public byte[] HeaderBytes { get; set; }
12 |
13 | [JsonPropertyName("seekBit")]
14 | public int SeekBit { get; set; }
15 |
16 | [JsonConstructor]
17 | public FmodVorbisData(byte[] headerBytes, int seekBit)
18 | {
19 | HeaderBytes = headerBytes;
20 | SeekBit = seekBit;
21 | }
22 |
23 | [JsonIgnore] private byte[] BlockFlags { get; set; } = Array.Empty();
24 |
25 | private bool _initialized;
26 |
27 | internal void InitBlockFlags()
28 | {
29 | if(_initialized)
30 | return;
31 |
32 | _initialized = true;
33 |
34 | var bitStream = new BitStream(HeaderBytes);
35 |
36 | if (bitStream.ReadByte() != 5) //packing type 5 == books
37 | return;
38 |
39 | if (bitStream.ReadString(6) != "vorbis") //validate magic
40 | return;
41 |
42 | //Whole bytes, bit remainder
43 | bitStream.Seek(SeekBit / 8, SeekBit % 8);
44 |
45 | //Read 6 bits and add one
46 | var numModes = bitStream.ReadByte(6) + 1;
47 |
48 | //Read the first bit of each mode and skip the rest of the mode data. These are our flags.
49 | BlockFlags = Enumerable.Range(0, numModes).Select(_ =>
50 | {
51 | var flag = (byte)bitStream.ReadBit();
52 |
53 | //Skip the bits we don't care about
54 | bitStream.ReadBits(16);
55 | bitStream.ReadBits(16);
56 | bitStream.ReadBits(8);
57 |
58 | return flag;
59 | }).ToArray();
60 | }
61 |
62 | public int GetPacketBlockSize(byte[] packetBytes)
63 | {
64 | var bitStream = new BitStream(packetBytes);
65 |
66 | if (bitStream.ReadBit())
67 | return 0;
68 |
69 | var mode = 0;
70 |
71 | if (BlockFlags.Length > 1)
72 | mode = bitStream.ReadByte(BlockFlags.Length - 1);
73 |
74 | if (BlockFlags[mode] == 1)
75 | return 2048;
76 |
77 | return 256;
78 | }
79 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/IBinaryReadable.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Fmod5Sharp.Util
4 | {
5 | internal interface IBinaryReadable
6 | {
7 | internal void Read(BinaryReader reader);
8 | }
9 | }
--------------------------------------------------------------------------------
/Fmod5Sharp/Util/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Fmod5Sharp.Util
4 | {
5 | internal static class Utils
6 | {
7 | private static readonly sbyte[] SignedNibbles = { 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 };
8 | internal static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
9 | internal static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
10 | internal static short Clamp(short val, short min, short max) => Math.Max(Math.Min(val, max), min);
11 | }
12 | }
--------------------------------------------------------------------------------
/HeaderGenerator/HeaderGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/HeaderGenerator/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.IO;
5 | using System.Text;
6 | using System.Text.Json;
7 | using System.Text.RegularExpressions;
8 |
9 | //Intending for parsing https://github.com/HearthSim/python-fsb5/blob/master/fsb5/vorbis_headers.py
10 | namespace HeaderGenerator
11 | {
12 | public class Program
13 | {
14 | private const string FILE_START = "lookup = {";
15 | private const string SEARCH_TERM = "',\n";
16 | private const string DUMMY_HEX_ESCAPE = @"\x00";
17 | private static readonly Regex EscapeRegex = new Regex(@"\\x([a-f0-9]{2})", RegexOptions.Compiled);
18 |
19 | public static void Main(string[] args)
20 | {
21 | if (args.Length < 1)
22 | {
23 | Console.WriteLine("Need to provide path to vorbis_headers.py as an arg");
24 | return;
25 | }
26 |
27 | var fileContent = File.ReadAllText(args[0]);
28 | fileContent = fileContent[FILE_START.Length..];
29 |
30 | //Each entry should be of the format "{id}: b'first line'\nb'second line'\n...\nb'last line
31 | //Note there is no closing quote on the last line as it is part of the search term.
32 | var entries = fileContent.Split(SEARCH_TERM);
33 |
34 | var result = new Dictionary();
35 | foreach (var entry in entries)
36 | {
37 | var colonPos = entry.IndexOf(':');
38 | var num = entry[..colonPos];
39 | var body = entry[(colonPos + 1)..].TrimStart().TrimEnd('}', '\n', '\r');
40 |
41 | var contentStringBuilder = new StringBuilder();
42 | foreach (var line in body.Split("\n"))
43 | {
44 | var trimmed = line.Trim().EndsWith("'") || line.Trim().EndsWith("\"")
45 | ? line.Trim()
46 | : line.TrimStart(); //Preserve whitespace at end if this is the last line of the block.
47 |
48 | if (trimmed.StartsWith("b"))
49 | trimmed = trimmed[2..];
50 | if (trimmed.EndsWith("'") || trimmed.EndsWith('"'))
51 | trimmed = trimmed[..^1];
52 |
53 | contentStringBuilder.Append(trimmed);
54 | }
55 |
56 | var contentString = contentStringBuilder.ToString();
57 |
58 | contentString = contentString.Replace("\\n", "\n")
59 | .Replace("\\t", "\t")
60 | .Replace("\\r", "\r")
61 | .Replace("\\0", "\0")
62 | .Replace("\\'", "\'")
63 | .Replace("\\\"", "\"")
64 | .Replace("\\\\", "\\");
65 |
66 | var matches = EscapeRegex.Matches(contentString);
67 |
68 | var offsetDict = new Dictionary();
69 | foreach (Match match in matches)
70 | {
71 | var hexString = match.Groups[1].Value;
72 | var charValue = byte.Parse(hexString, NumberStyles.HexNumber);
73 | offsetDict[match.Index] = charValue;
74 | }
75 |
76 | List headerBytes = new List();
77 | for (var i = 0; i < contentString.Length; i++)
78 | {
79 | if (offsetDict.ContainsKey(i))
80 | {
81 | headerBytes.Add(offsetDict[i]);
82 | i += DUMMY_HEX_ESCAPE.Length - 1;
83 | continue;
84 | }
85 |
86 | headerBytes.Add((byte) contentString[i]);
87 | }
88 |
89 | result[uint.Parse(num)] = headerBytes.ToArray();
90 | Console.WriteLine($"Parsed {num} => {headerBytes.ToArray().Length} bytes");
91 | }
92 |
93 | var jsonValue = JsonSerializer.Serialize(result, new JsonSerializerOptions
94 | {
95 | WriteIndented = true
96 | });
97 |
98 | File.WriteAllText("vorbis_headers.json", jsonValue);
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Sam Byass
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fmod5Sharp
2 | ## Managed decoder for FMOD 5 sound banks (FSB files).
3 |
4 | [](https://www.nuget.org/packages/Fmod5Sharp/)
5 |
6 | This library allows you to read FMOD 5 sound bank files (they start with the characters FSB5) into their contained samples,
7 | and then export those samples to standard file formats (assuming the contained data format is supported).
8 |
9 | Support for more encodings can be added as requested.
10 |
11 | ## Usage
12 |
13 | The Fmod file can be read like this
14 | ```c#
15 | //Will throw if the bank is not valid.
16 | FmodSoundBank bank = FsbLoader.LoadFsbFromByteArray(rawData);
17 | ```
18 |
19 | Or if you don't want it to throw if the file is invalid, you can use
20 | ```c#
21 | bool success = FsbLoader.TryLoadFsbFromByteArray(rawData, out FmodSoundBank bank);
22 | ```
23 |
24 | You can then query some properties about the bank:
25 | ```c#
26 | FmodAudioType type = bank.Header.AudioType;
27 | uint fmodSubVersion = bank.Header.Version; //0 or 1 have been observed
28 | ```
29 |
30 | And get the samples stored inside it:
31 | ```c#
32 | List samples = bank.Samples;
33 | int frequency = samples[0].Metadata.Frequency; //E.g. 44100
34 | uint numChannels = samples[0].Metadata.Channels; //2 for stereo, 1 for mono.
35 |
36 | string name = samples[0].Name; //Null if not present in the bank file (which is usually the case).
37 | ```
38 |
39 | And, you can convert the audio data back to a standard format.
40 | ```c#
41 | var success = samples[0].RebuildAsStandardFileFormat(out var dataBytes, out var fileExtension);
42 | //Assuming success == true, then this file format was supported and you should have some data and an extension (without the leading .).
43 | //Now you can save dataBytes to an file with the given extension on your disk and play it using your favourite audio player.
44 | //Or you can use any standard library to convert the byte array to a different format, if you so desire.
45 | ```
46 |
47 | You can also check if a given format type is supported and, if so, what extension it will result in, like so:
48 | ```c#
49 | bool isSupported = bank.Header.AudioType.IsSupported();
50 |
51 | //Null if not supported
52 | string? extension = bank.Header.AudioType.FileExtension();
53 | ```
54 |
55 | Alternatively, you can consult the table below:
56 |
57 | | Format | Supported? | Extension | Notes |
58 | | :-----: | :--------------: | :---------: | :----------: |
59 | | PCM8 | ✔️ | wav | |
60 | | PCM16 | ✔️ | wav | |
61 | | PCM24 | ❌ | | No games have ever been observed in the wild using this format. |
62 | | PCM32 | ✔️ | wav | Supported in theory. No games have ever been observed in the wild using this format. |
63 | | PCMFLOAT | ❌ | | Seen in at least one JRPG. |
64 | | GCADPCM | ✔️ | wav | Tested with single-channel files. Not tested with stereo, but should work in theory. Seen in Unity games. |
65 | | IMAADPCM | ✔️ | wav | Seen in Unity games. |
66 | | VAG | ❌ | | No games have ever been observed in the wild using this format. |
67 | | HEVAG | ❌ | | Very rarely used - only example I know of is a game for the PS Vita. |
68 | | XMA | ❌ | | Mostly used on Xbox 360. |
69 | | MPEG | ❌ | | Used in some older games. |
70 | | CELT | ❌ | | Used in some older indie games. |
71 | | AT9 | ❌ | | Native format for PlayStation Audio, including in Unity games. |
72 | | XWMA | ❌ | | No games have ever been observed in the wild using this format. |
73 | | VORBIS | ✔️ | ogg | Very commonly used in Unity games. |
74 |
75 | # Acknowledgements
76 |
77 | This project uses:
78 | - [OggVorbisEncoder](https://github.com/SteveLillis/.NET-Ogg-Vorbis-Encoder) to build Ogg Vorbis output streams.
79 | - [NAudio.Core](https://github.com/naudio/NAudio) to do the same thing but for WAV files.
80 | - [BitStreams](https://github.com/rubendal/BitStream) for parsing vorbis header data.
81 | - [IndexRange](https://github.com/bgrainger/IndexRange) to make my life easier when supporting .NET Standard 2.0.
82 |
83 | It also uses System.Text.Json.
84 |
--------------------------------------------------------------------------------