有没有更好的方法来使用比特流解码文件格式?



我正在尝试制作一个解码mp3文件的程序(作为练习(。重要的部分不是它是mp3文件,而是文件格式的结构方式。某些变量的长度(以位为单位(取决于以前的变量。因此,假设您为一个变量读取 2 位,并根据其值,您必须为下一个变量读取 10 位或 20 位。更糟糕的是,每次连续读取都会被相同的长度偏移。

我正在使用以下比特流实现,它运行良好,我对生成的 asm 非常满意。

问题是此实现需要知道编译期间每个位读取的偏移量和长度。某些变量的可变长度使其很难实现。

有两个想法浮现在脑海中,这两个想法似乎都很糟糕。一种是更改位读取函数,以便我将偏移量作为参数传递给函数作为常规参数而不是模板参数。这显然很糟糕,因为我们失去了美丽的asm。更重要的一点是,这些信息已经为人所知。当信息在编译时可用时,这不应该在运行时发生。我只需要能够更好地向编译器解释它。

另一个想法是使用一些奇怪的模板元编程(检查第二个编码示例(

第一个想法如下:

// the function works like the one in the bitstream implementation that I pointed earlier, but the offset is an argument to the function, instead of a template argument.
template<unsigned count>
uint32_t Read(int offset, const uint8_t* src, uint32_t accum = 0);
uint32_t var1 = Read(0, 2, data);
switch(var1) {
case 0b00: {
uint32_t var2 = Read(2, 10, data);
uint32_t var3 = Read(12, 3, data);
uint32_t var4 = Read(15, 1, data);
//... etc
break;
}
case 0b01: {
uint32_t var2 = Read(2, 20, data);
uint32_t var3 = Read(22,  3, data);
uint32_t var4 = Read(25,  3, data);
// ... etc
break;
}
default: 
break;
}

第二个想法是使用未修改的 Read 函数,但将所有偏移量和长度移动到一个单独的模板类中,并针对每种情况进行专业化。

template<enum DIFFERENT_CASES T> struct offsets_and_lengths {};
struct BitReads { unsigned offset; unsigned length; };
template<> struct offsets_and_lengths<CASE_1> {
static constexpr BitReads var2 = { 2, 10 };
static constexpr BitReads var3 = { 12, 3 };
static constexpr BitReads var4 = { 15, 1 };
};
template<> struct offsets_and_lengths<CASE_2> {
static constexpr BitReads var2 = { 2, 20 };
static constexpr BitReads var3 = { 22, 3 };
static constexpr BitReads var4 = { 25, 1 };
};
template<enum DIFFERENT_CASES T>
void doDifferentReads(uint8_t* data) {
using offsets = offsets_and_legnths<T>;
uint32_t var2 = Read<offsets::var2.offset, offsets.length>(data);
uint32_t var3 = Read<offsets::var3.offset, offsets.length>(data);
uint32_t var4 = Read<offsets::var4.offset, offsets.length>(data);
// ... etc
}

uint32_t var1 = Read(0, 2, data);
switch(var1) {
case 0b00: {
doDifferentReads<CASE_1>(data);
break;
}
case 0b01: {
doDifferentReads<CASE_2>(data);
break;
}
default: 
break;
}

第二个实现的问题在于,我最终可能会得到另一个长度可能不同的变量。在这一点上,我必须创建 4 个不同的专业。8 下一个...等。

所以我想最大的问题是:如果我想使用我知道的静态数据,是否有一种理智的方法可以对这种格式进行解码?这样生成的 asm 就会很小。

您可以使用位掩码和 shift 来执行提取:

int32_t bitmask[2][3] = {
{
?, ?, ?
},
{
?, ?, ?
}
};
int32_t shift[2][3] = {
{
?, ?, ?
},
{
?, ?, ?
}
};

void extraction(int index, uint32_t follow) {
uint32_t var2 = (follow & bitmask[index][0]) >> shift[index][0];
uint32_t var3 = (follow & bitmask[index][1]) >> shift[index][1];
uint32_t var4 = (follow & bitmask[index][2]) >> shift[index][2];
}

int main()
{
int32_t v = 0;

uint32_t var1 = Read(0, 2, data);
int index = 0;
switch (var1) {
case 0b00: {
uint32_t follow = Read(2, 14, data);
extraction(0, follow);
break;
}
case 0b01: {
uint32_t follow = Read(2, 26, data);
extraction(1, follow);
break;
}

}

我不打算搜索掩码和移位的所有值,但我可以向您展示如何计算第一个值。你有 Read(2, 10(,我们已经读取了 14 位,所以移位很容易,因为你需要移位 4 位。二进制掩码是 0011 1111 1111 0000,所以0x3FF0。当然,大端序和小端序会干扰这里,因此您必须注意数据顺序是否正确。请注意,您可以将变量存储在数组中以提高效率,并将 extraction(( 更改为 for 循环。

最新更新