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
;v7
is a sign-extended nibble;v4
the sample difference, a.k.a.step * nibble
;v8
is 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 thatv3
is 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
<< 24
tells us that it’s at least a 32-bit type. Assume it is because this is 32-bit game; v2
is a step index, and easily fits in 8-bits.v4
is 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 & 0x7
you’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
:
|
|