├── .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 | [![NuGet](https://img.shields.io/nuget/v/Fmod5Sharp?)](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 | --------------------------------------------------------------------------------