Skip to content

Commit

Permalink
✨ Add motion huffman tables
Browse files Browse the repository at this point in the history
  • Loading branch information
pleonex committed Nov 3, 2023
1 parent 5418de9 commit ae5b522
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 108 deletions.
32 changes: 32 additions & 0 deletions src/PlayMobic.Tests/Video/HuffmanFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace PlayMobic.Tests.Video;
using PlayMobic.Video.Mobiclip;

[TestFixture]
public class HuffmanFactoryTests
{
[Test]
[TestCase(0, 0x000, 0x03, 8)]
[TestCase(0, 0x001, 0x02, 3)]
[TestCase(0, 0x002, 0x0F, 5)]
[TestCase(0, 0x021, 0x06, 4)]
[TestCase(0, 0x081, 0x0C, 6)]
[TestCase(0, 0x801, 0x07, 5)]
[TestCase(0, 0x802, 0x19, 10)]
[TestCase(0, 0x821, 0x0F, 7)]
[TestCase(0, 0x841, 0x0E, 7)]
[TestCase(0, 0x8E1, 0x11, 8)]
[TestCase(1, 0x000, 0x03, 8)]
[TestCase(1, 0x801, 0x07, 5)]
public void TestTreeFromTable(int tableIdx, int value, int expectedCode, int expectedBitCount)
{
string name = typeof(Huffman).Namespace + $".huffman_residual_table{tableIdx}.bin";
var huffman = HuffmanFactory.CreateFromResidualTable(name);

HuffmanCodeword codeword = huffman.GetCodeword(value);
Assert.Multiple(() => {
Assert.That(codeword.Value, Is.EqualTo(value));
Assert.That(codeword.Code, Is.EqualTo(expectedCode));
Assert.That(codeword.BitCount, Is.EqualTo(expectedBitCount));
});
}
}
26 changes: 1 addition & 25 deletions src/PlayMobic.Tests/Video/HuffmanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,6 @@
[TestFixture]
internal class HuffmanTests
{
[Test]
[TestCase(0, 0x000, 0x03, 8)]
[TestCase(0, 0x001, 0x02, 3)]
[TestCase(0, 0x002, 0x0F, 5)]
[TestCase(0, 0x021, 0x06, 4)]
[TestCase(0, 0x081, 0x0C, 6)]
[TestCase(0, 0x801, 0x07, 5)]
[TestCase(0, 0x802, 0x19, 10)]
[TestCase(0, 0x821, 0x0F, 7)]
[TestCase(0, 0x841, 0x0E, 7)]
[TestCase(0, 0x8E1, 0x11, 8)]
[TestCase(1, 0x000, 0x03, 8)]
[TestCase(1, 0x801, 0x07, 5)]
public void TestTreeFromTable(int tableIdx, int value, int expectedCode, int expectedBitCount)
{
var huffman = Huffman.LoadFromFullIndexTable(tableIdx);

(int actualCode, int actualBitCount) = huffman.GetCodeword(value);
Assert.Multiple(() => {
Assert.That(actualCode, Is.EqualTo(expectedCode));
Assert.That(actualBitCount, Is.EqualTo(expectedBitCount));
});
}

[Test]
[TestCase(new byte[] { 0b0000011_0 }, 0x000)]
[TestCase(new byte[] { 0b10_000000 }, 0x001)]
Expand All @@ -42,7 +18,7 @@ public void ReadCodewordBits(byte[] data, int expectedValue)
{
using DataStream stream = DataStreamFactory.FromArray(data);
var reader = new BitReader(stream, EndiannessMode.LittleEndian);
var huffman = Huffman.LoadFromFullIndexTable(0);
var huffman = HuffmanFactory.CreateFromResidualTable(typeof(Huffman).Namespace + ".huffman_residual_table0.bin");

int actualValue = huffman.ReadCodeword(reader);

Expand Down
4 changes: 2 additions & 2 deletions src/PlayMobic/Video/Mobiclip/EntropyVlcEncoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ internal class EntropyVlcEncoding
#pragma warning restore SA1137

private static readonly Huffman[] HuffmanTables = new[] {
Huffman.LoadFromFullIndexTable(0),
Huffman.LoadFromFullIndexTable(1),
HuffmanFactory.CreateFromResidualTable(typeof(Huffman).Namespace + ".huffman_residual_table0.bin"),
HuffmanFactory.CreateFromResidualTable(typeof(Huffman).Namespace + ".huffman_residual_table1.bin"),
};

private readonly Huffman huffman;
Expand Down
68 changes: 6 additions & 62 deletions src/PlayMobic/Video/Mobiclip/Huffman.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,22 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using PlayMobic.IO;
using Yarhl.IO;

/// <summary>
/// Huffman implementation similar to the original decoder.
/// </summary>
/// <remarks>
/// It builds the tree from the original block of data to showcase its format.
/// </remarks>
internal class Huffman
{
private static readonly string[] MobiclipTable = new string[] {
typeof(Huffman).Namespace + ".huffman0.bin",
typeof(Huffman).Namespace + ".huffman1.bin",
};

private readonly int codewordMaxLength;
private readonly Node root;
private readonly Dictionary<int, Codeword> codewords;
private readonly Dictionary<int, HuffmanCodeword> codewords;

public Huffman(int codewordMaxLength)
{
this.codewordMaxLength = codewordMaxLength;
root = Node.CreateNode();
codewords = new Dictionary<int, Codeword>();
}

public static Huffman LoadFromFullIndexTable(int tableIdx)
{
string tablePath = MobiclipTable[tableIdx];
using Stream tableStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(tablePath)
?? throw new FileNotFoundException("Missing huffman table");

// Format is for each item:
// index (13 bits max): codeword
// bit0-3: number of codeword bits (to clean up the index)
// bit4-15: value
// this is a trick to decode huffman via a hash table lookup (fast reads)
// it repeats the same value for all the variant of short codewords
// so for a codeword of 10 bits, it will repeat the value for all combinations
// of that codeword and its 3 remaining bits.
int numItems = (int)(tableStream.Length / 2);
var reader = new DataReader(tableStream);

const int MaxCodewordLength = 13;
var huffman = new Huffman(MaxCodewordLength);

for (int i = 0; i < numItems; i++) {
ushort item = reader.ReadUInt16();

int bitCount = item & 0xF;
int value = item >> 4;
int codeword = i >> (MaxCodewordLength - bitCount);

if (bitCount == 1) {
// padding
continue;
}

huffman.InsertCodeword(codeword, bitCount, value);
}

// the codeword for 0 is "hard-coded" in code
huffman.InsertCodeword(0b00000011, 8, 0);

return huffman;
codewords = new Dictionary<int, HuffmanCodeword>();
}

public int ReadCodeword(BitReader reader)
Expand Down Expand Up @@ -99,13 +45,12 @@ public int ReadCodeword(BitReader reader)
throw new FormatException("codeword not found");
}

public (int Codeword, int BitCount) GetCodeword(int value)
public HuffmanCodeword GetCodeword(int value)
{
Codeword item = codewords[value];
return (item.Code, item.BitCount);
return codewords[value];
}

private void InsertCodeword(int codeword, int bitCount, int value)
public void InsertCodeword(int codeword, int bitCount, int value)
{
// Find the parent
Node current = root;
Expand Down Expand Up @@ -140,10 +85,9 @@ private void InsertCodeword(int codeword, int bitCount, int value)
throw new InvalidOperationException("Invalid huffman tree");
}

codewords[value] = new Codeword(codeword, bitCount);
codewords[value] = new HuffmanCodeword(codeword, bitCount, value);
}

private sealed record Codeword(int Code, int BitCount);
private sealed record Node
{
private Node()
Expand Down
17 changes: 17 additions & 0 deletions src/PlayMobic/Video/Mobiclip/HuffmanCodeword.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace PlayMobic.Video.Mobiclip;

public readonly struct HuffmanCodeword
{
public HuffmanCodeword(int code, int bitCount, int value)
{
Code = code;
BitCount = bitCount;
Value = value;
}

public int Code { get; }

public int BitCount { get; }

public int Value { get; }
}
68 changes: 68 additions & 0 deletions src/PlayMobic/Video/Mobiclip/HuffmanFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace PlayMobic.Video.Mobiclip;
using System;
using System.Reflection;
using Yarhl.IO;

/// <summary>
/// Factory of Huffman trees following the original binary tree definitions.
/// </summary>
internal static class HuffmanFactory
{
public static Huffman CreateFromResidualTable(string resourceName)
{
using Stream tableStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)
?? throw new FileNotFoundException("Missing huffman table");

// Format is for each 16-bits item:
// index (13 bits max): codeword
// bit0-3: number of codeword bits (to clean up the index)
// bit4-15: value
// this is a trick to decode huffman via a hash table lookup (fast reads)
// it repeats the same value for all the variant of short codewords
// so for a codeword of 10 bits, it will repeat the value for all combinations
// of that codeword and its 3 remaining bits.
int numItems = (int)(tableStream.Length / 2);
var reader = new DataReader(tableStream);

const int MaxCodewordLength = 13;
var huffman = new Huffman(MaxCodewordLength);

for (int i = 0; i < numItems; i++) {
ushort item = reader.ReadUInt16();

int bitCount = item & 0xF;
int value = item >> 4;
int codeword = i >> (MaxCodewordLength - bitCount);

if (bitCount == 1) {
// padding
continue;
}

huffman.InsertCodeword(codeword, bitCount, value);
}

// the codeword for 0 is "hard-coded" in code
huffman.InsertCodeword(0b00000011, 8, 0);

return huffman;
}

public static Huffman CreateFromSymbolsAndCountLists(byte[] symbols, byte[] bitCounts)
{
// similar to above, the index gives the codeword for each symbol.
// the value points also to the bits count of the codeword
int maxCodewordLength = (int)Math.Log2(symbols.Length);

var huffman = new Huffman(maxCodewordLength);
for (int i = 0; i < symbols.Length; i++) {
int symbol = symbols[i];
int bitCount = bitCounts[symbol];
int codeword = i >> (maxCodewordLength - bitCount);

huffman.InsertCodeword(codeword, bitCount, symbol);
}

return huffman;
}
}
Loading

0 comments on commit ae5b522

Please sign in to comment.