Pro Pinball Music Reverse Engineering
2020-03-15
This was an absolute pain of a codec to reverse. It’s a simple ADPCM format, but instead of using standard step and index tables, it precalculates a lookup table and uses that. I’m not sure why as the whole purpose of ADPCM was okay-compression and fast decode.
This was done in all of the four games, at least on their PC versions.
ADPCM Tables
| |
Decoder
| |
This doesn’t look like much a ADPCM decoder, but if it’s nop’d-out all the
music and sound effects stop. To find this, poke around the executables starting
at the DirectSound calls.
Looking at XREFs to fj_sound_table, the only place it’s written to is in sub_44F790:
| |
This does look like an ADPCM decoder, albeit an odd one. Looking at the innermost loop, the first part of it can be simplified to:
| |
This looks familiar. v2 is the step index, v6 is the previous step index,
and v7 is presumably the nibble.
| |
The next part is a bit strange. It’s a simple loop, but what is it actually doing? It looks like this:
| |
From this, observe that:
0 ≤ v0 ≤ 60, meaning it’s a step index. This also explains why it’s being used to indexfj_adpcm_step_table;v7is a sign-extended nibble;v4the sample difference, a.k.a.step * nibble;v8is the actual nibble, with the sign-extend lopped off;0 ≤ v3 ≤ 256, in steps of 16. Notice that16 == 1 << 4. By extension:0 >> 4 == 0, and256 >> 4 == 16, suggesting thatv3is an ADPCM nibble right shifted by 4, and we’re looping over each value.
Notice the ++HIBYTE(v8). We were slightly wrong about v8: only the lower byte contains the nibble.
The high byte is looped from 0 to 60, meaning it is probably a step index.
Now, look at fj_sound_table; specifically how it’s indexed. fj_sound_table is indexed by v5,
where v5 == v3 | v8, where v8 == step_index | v7 & 0xF). In essence, the format of the key is:
| |
This type of indexing heavily suggests a multi-dimensional array, and in fact, it is:
| |
becomes:
| |
We’re half-way there. We know the layout of the table, but not its contents. This is the easy part:
| |
Observe:
- The
<< 24tells us that it’s at least a 32-bit type. Assume it is because this is 32-bit game; v2is a step index, and easily fits in 8-bits.v4is the sample difference, and is a 16-bit quantity. This is either a bitfield, or a struct. I’m going with a struct:
| |
Don’t believe me? Check out the tables yourself.
The full-reversed function is:
| |
From here, it’s trivial to reverse the decoder. The completed decoder, from FFmpeg is:
| |
There’s actually a minor bug in this that I missed. Notice the abs(nibble) being used to index
the index table. This is in the range 0 ≤ abs(nibble) ≤ 8, but there’s only 8 elements in the
index table, leading to a potential buffer overrun. This was caught by FFmpeg’s automated fuzzing1.
As explained here:
ff_adpcm_ima_cunning_index_table[abs(nibble)] is wrong in the case where nibble == -8.
If you take the unsigned nibble, and apply f():
f(x) = 16 - x if x > 8 else x & 0x7you’ll get the same value as abs() applied with the signed nibble, except for this one case (abs(-8) == 8, f(8) == 0).
The fix was to simply extend the index table with an extra -1:
| |