Skip to content

Commit 1261397

Browse files
author
Sam Byass
committed
Add support for IMAADPCM
1 parent 541adaa commit 1261397

11 files changed

Lines changed: 196 additions & 19 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Linq;
2+
using Xunit;
3+
4+
namespace Fmod5Sharp.Tests
5+
{
6+
public class Fmod5ImaAdPcmTests
7+
{
8+
[Fact]
9+
public void BanksCanBeLoaded()
10+
{
11+
var rawData = this.LoadResource("imaadpcm_short.fsb");
12+
13+
var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
14+
15+
Assert.Equal(FmodAudioType.IMAADPCM, fsb.Header.AudioType);
16+
Assert.Equal(2u, fsb.Samples.Single().Metadata.Channels);
17+
}
18+
19+
[Fact]
20+
public void ImaAdPcmBanksCanBeRebuilt()
21+
{
22+
var rawData = this.LoadResource("imaadpcm_short.fsb");
23+
24+
var fsb = FsbLoader.LoadFsbFromByteArray(rawData);
25+
26+
var bytes = FmodImaAdPcmRebuilder.Rebuild(fsb.Samples[0]);
27+
28+
Assert.NotEmpty(bytes);
29+
}
30+
}
31+
}

Fmod5Sharp.Tests/Fmod5Sharp.Tests.csproj

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
</PropertyGroup>
1717

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

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

3535
<ItemGroup>
36-
<None Remove="TestResources\short_vorbis.fsb"/>
37-
<EmbeddedResource Include="TestResources\short_vorbis.fsb"/>
38-
<None Remove="TestResources\long_vorbis.fsb"/>
39-
<EmbeddedResource Include="TestResources\long_vorbis.fsb"/>
40-
<None Remove="TestResources\pcm16.fsb"/>
41-
<EmbeddedResource Include="TestResources\pcm16.fsb"/>
42-
<None Remove="TestResources\gcadpcm.fsb"/>
43-
<EmbeddedResource Include="TestResources\gcadpcm.fsb"/>
36+
<None Remove="TestResources\short_vorbis.fsb" />
37+
<EmbeddedResource Include="TestResources\short_vorbis.fsb" />
38+
<None Remove="TestResources\long_vorbis.fsb" />
39+
<EmbeddedResource Include="TestResources\long_vorbis.fsb" />
40+
<None Remove="TestResources\pcm16.fsb" />
41+
<EmbeddedResource Include="TestResources\pcm16.fsb" />
42+
<None Remove="TestResources\gcadpcm.fsb" />
43+
<EmbeddedResource Include="TestResources\gcadpcm.fsb" />
44+
<None Remove="TestResources\imaadpcm_short.fsb" />
45+
<EmbeddedResource Include="TestResources\imaadpcm_short.fsb" />
46+
<None Remove="TestResources\imaadpcm_long.fsb" />
47+
<EmbeddedResource Include="TestResources\imaadpcm_long.fsb" />
4448
</ItemGroup>
4549

4650
</Project>
133 KB
Binary file not shown.
23.7 KB
Binary file not shown.

Fmod5Sharp/Fmod5Sharp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
77
<Configurations>Release;Debug</Configurations>
88
<Platforms>x86;x64;AnyCPU</Platforms>
9-
<Version>1.1.1</Version>
9+
<Version>1.2.0</Version>
1010
<PackageId>Fmod5Sharp</PackageId>
1111
<RepositoryType>git</RepositoryType>
1212
<RepositoryUrl>https://github.com/SamboyCoding/Fmod5Sharp.git</RepositoryUrl>

Fmod5Sharp/FmodAudioTypeExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
1010
FmodAudioType.PCM16 => true,
1111
FmodAudioType.PCM32 => true,
1212
FmodAudioType.GCADPCM => true,
13+
FmodAudioType.IMAADPCM => true,
1314
_ => false
1415
};
1516

@@ -21,6 +22,7 @@ public static bool IsSupported(this FmodAudioType @this) =>
2122
FmodAudioType.PCM16 => "wav",
2223
FmodAudioType.PCM32 => "wav",
2324
FmodAudioType.GCADPCM => "wav",
25+
FmodAudioType.IMAADPCM => "wav",
2426
_ => null
2527
};
2628
}

Fmod5Sharp/FmodGcadPcmRebuilder.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private static short[] GetPcmData(FmodSample sample)
5252
for (var s = 0; s < samplesToRead; s++)
5353
{
5454
//Raw value
55-
var adpcmSample = (int) (s % 2 == 0 ? GetHighNibbleSigned(adpcm[inIndex]) : GetLowNibbleSigned(adpcm[inIndex++]));
55+
var adpcmSample = (int) (s % 2 == 0 ? Utils.GetHighNibbleSigned(adpcm[inIndex]) : Utils.GetLowNibbleSigned(adpcm[inIndex++]));
5656

5757
//Adaptive processing
5858
adpcmSample = (adpcmSample * scale) << 11;
@@ -92,7 +92,7 @@ public static byte[] Rebuild(FmodSample sample)
9292

9393
writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
9494

95-
return stream.GetBuffer();
95+
return stream.ToArray();
9696
}
9797

9898
private static int NibbleCountToSampleCount(int nibbleCount)
@@ -106,10 +106,6 @@ private static int NibbleCountToSampleCount(int nibbleCount)
106106

107107
private static int ByteCountToSampleCount(int byteCount) => NibbleCountToSampleCount(byteCount * 2);
108108

109-
private static readonly sbyte[] SignedNibbles = { 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 };
110-
private static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
111-
private static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
112-
113109
private static short Clamp16(int value)
114110
{
115111
if (value > short.MaxValue)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using NAudio.Wave;
5+
6+
namespace Fmod5Sharp
7+
{
8+
public static class FmodImaAdPcmRebuilder
9+
{
10+
public const int SamplesPerFramePerChannel = 64;
11+
12+
static readonly int[] ADPCMTable = {
13+
7, 8, 9, 10, 11, 12, 13, 14,
14+
16, 17, 19, 21, 23, 25, 28, 31,
15+
34, 37, 41, 45, 50, 55, 60, 66,
16+
73, 80, 88, 97, 107, 118, 130, 143,
17+
157, 173, 190, 209, 230, 253, 279, 307,
18+
337, 371, 408, 449, 494, 544, 598, 658,
19+
724, 796, 876, 963, 1060, 1166, 1282, 1411,
20+
1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
21+
3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
22+
7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
23+
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
24+
32767
25+
};
26+
27+
private static readonly int[] IMA_IndexTable = {
28+
-1, -1, -1, -1, 2, 4, 6, 8,
29+
-1, -1, -1, -1, 2, 4, 6, 8,
30+
};
31+
32+
private static void ExpandNibble(MemoryStream stream, long byteOffset, int nibbleShift, ref int hist, ref int stepIndex)
33+
{
34+
stream.Seek(byteOffset, SeekOrigin.Begin);
35+
var sampleNibble = (stream.ReadByte() >> nibbleShift) & 0xf;
36+
var sampleDecoded = hist;
37+
var step = ADPCMTable[stepIndex];
38+
39+
var delta = step >> 3;
40+
if ((sampleNibble & 1) != 0) delta += step >> 2;
41+
if ((sampleNibble & 2) != 0) delta += step >> 1;
42+
if ((sampleNibble & 4) != 0) delta += step;
43+
if ((sampleNibble & 8) != 0) delta = -delta;
44+
sampleDecoded += delta;
45+
46+
hist = Math.Clamp(sampleDecoded, short.MinValue, short.MaxValue);
47+
stepIndex += IMA_IndexTable[sampleNibble];
48+
stepIndex = Math.Clamp(stepIndex, 0, 88);
49+
}
50+
51+
private static short[] GetPcm(FmodSample sample)
52+
{
53+
var blockSamples = 0x40;
54+
var numChannels = (int)sample.Metadata.Channels;
55+
56+
using var stream = new MemoryStream(sample.SampleBytes);
57+
using var reader = new BinaryReader(stream);
58+
59+
short[] ret = new short[sample.Metadata.SampleCount * 2];
60+
var sampleIndex = 0;
61+
62+
for (var channel = 0; channel < numChannels; channel++)
63+
{
64+
sampleIndex = channel;
65+
66+
var numFrames = (int) sample.Metadata.SampleCount / SamplesPerFramePerChannel;
67+
68+
for (var frameNum = 0; frameNum < numFrames; frameNum++)
69+
{
70+
var frameOffset = 0x24 * numChannels * frameNum;
71+
72+
//Read header
73+
var headerIndex = frameOffset + 4 * channel;
74+
75+
stream.Seek(headerIndex, SeekOrigin.Begin);
76+
int hist = reader.ReadInt16();
77+
stream.Seek(headerIndex + 2, SeekOrigin.Begin);
78+
int stepIndex = reader.ReadByte();
79+
80+
stepIndex = Math.Clamp(stepIndex, 0, 88);
81+
ret[sampleIndex] = (short)hist;
82+
sampleIndex += numChannels;
83+
84+
for (var sampleNum = 1; sampleNum <= SamplesPerFramePerChannel; sampleNum++)
85+
{
86+
// var byteOffset = relativePos + 4 * numChannels + 2 * channel + (i - 1) / 4 * 2 * numChannels + ((i - 1) % 4) / 2;
87+
var byteOffset = frameOffset + 4 * 2 + 4 * (channel % 2) + 4 * 2 * ((sampleNum - 1) / 8) + ((sampleNum - 1) % 8) / 2;
88+
if (numChannels == 0)
89+
byteOffset = frameOffset + 4 + (sampleNum - 1) / 2;
90+
91+
var nibbleShift = ((sampleNum - 1) & 1) != 0 ? 4 : 0;
92+
93+
if (sampleNum < blockSamples)
94+
{
95+
ExpandNibble(stream, byteOffset, nibbleShift, ref hist, ref stepIndex);
96+
ret[sampleIndex] = ((short)hist);
97+
sampleIndex += numChannels;
98+
}
99+
else
100+
{
101+
// relativePos += 0x24 * numChannels;
102+
}
103+
}
104+
}
105+
}
106+
107+
return ret;
108+
}
109+
110+
public static byte[] Rebuild(FmodSample sample)
111+
{
112+
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
113+
var format = WaveFormat.CreateCustomFormat(
114+
WaveFormatEncoding.Pcm,
115+
sample.Metadata.Frequency,
116+
numChannels,
117+
sample.Metadata.Frequency * numChannels * 2,
118+
numChannels * 2,
119+
16
120+
);
121+
using var stream = new MemoryStream();
122+
using var writer = new WaveFileWriter(stream, format);
123+
124+
var pcmShorts = GetPcm(sample);
125+
126+
writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
127+
128+
return stream.ToArray();
129+
}
130+
}
131+
}

Fmod5Sharp/FmodSample.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public bool RebuildAsStandardFileFormat([NotNullWhen(true)] out byte[]? data, [N
3434
data = FmodGcadPcmRebuilder.Rebuild(this);
3535
fileExtension = "wav";
3636
return data.Length > 0;
37+
case FmodAudioType.IMAADPCM:
38+
data = FmodImaAdPcmRebuilder.Rebuild(this);
39+
fileExtension = "wav";
40+
return data.Length > 0;
3741
default:
3842
data = null;
3943
fileExtension = null;

Fmod5Sharp/Utils.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Fmod5Sharp
2+
{
3+
internal class Utils
4+
{
5+
private static readonly sbyte[] SignedNibbles = { 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 };
6+
internal static sbyte GetHighNibbleSigned(byte value) => SignedNibbles[(value >> 4) & 0xF];
7+
internal static sbyte GetLowNibbleSigned(byte value) => SignedNibbles[value & 0xF];
8+
}
9+
}

0 commit comments

Comments
 (0)