-
Notifications
You must be signed in to change notification settings - Fork 0
PPM format
Animations created within Flipnote Studio are stored in the PPM format. The file extension comes from Flipnote Studio's original working title, Para Para Manga Koubou (translated as "Flipbook Workshop").
Offset | Type | Details |
---|---|---|
0x0 | char[4] | File magic (PARA ) |
0x4 | uint32 | Animation data size |
0x8 | uint32 | Sound data size |
0xC | uint16 | Frame count |
0xE | uint16 | Unknown, always 0x2400
|
0x10 | uint16 | Lock, 0 if unlocked, 1 if locked |
0x12 | uint16 | Thumbnail frame index |
0x14 | uint16[11] | Root author name |
0x2A | uint16[11] | Parent author name |
0x40 | uint16[11] | Current author name |
0x56 | hex[8] | Parent author ID |
0x5E | hex[8] | Current author ID |
0x66 | char[18] | Parent filename |
0x78 | char[18] | Current filename |
0x8A | hex[8] | Root author ID |
0x92 | char[8] | Partial filename |
0x9A | uint32 | Timestamp |
0x9E | uint16 | Unknown, seen as 0
|
Frame count starts at 0, and should be incremented by 1 when displayed.
Author names are null-padded UTF-16 LE strings. Author IDs are also stored in little-endian byte order, so you may need to reverse that.
Timestamps are stored as the number of seconds since midnight on the 1st of January, 2000.
The Flipnote thumbnail starts at 0xA0 and is 1536 bytes long.
Thumbnail images are 64 x 48 and arranged in a series of 8 x 8 tiles. Pixels are stored as 4-bit indexes, referencing a hardcoded color palette.
When unpacking these images from bytes, it's important note that the first pixel is stored in the upper 4 bits of the byte and the second pixel is the lower 4 bits.
Index | RGB |
---|---|
0 | 255, 255, 255 |
1 | 82, 82, 82 |
2 | 255, 255, 255 |
3 | 165, 165, 165 |
4 | 255, 0, 0 |
5 | 123, 0, 0 |
6 | 255, 123, 123 |
7 | 0, 255, 0 |
8 | 0, 0, 255 |
9 | 0, 0, 123 |
10 | 123, 123, 255 |
11 | 0, 255, 0 |
12 | 255, 0, 255 |
13 | 0, 255, 0 |
14 | 0, 255, 0 |
15 | 0, 255, 0 |
The animation header starts at 0x06A0
.
Type | Details |
---|---|
uint16 | Size of the frame offset table |
uint16 | Unknown, always seen as 0 |
uint32 | Flags |
Following the animation header is a table of uint32 offsets for each frame. These offsets are relative to the start of the animation data section.
Bit index (0 = lowest) | Details |
---|---|
20 | Hide layer 1 if set |
21 | Hide layer 2 if set |
30 | Loop Flipnote playback if set |
The animation data begins at 0x06A8 + the size of the frame offset table
. Frames are not necessarily stored in playback sequence, and can sometimes share the same offset.
Frames are 256 x 192 pixels and comprise of two image layers plus a "paper" background. The paper is either black or white, and layers can be red, blue, or the inverse of the paper color.
Each layer is a 1-bit monochrome bitmap with some basic compression done on a (horizontal) line-by-line basis to make the file more space-efficient.
Every frame begins with a one-byte header:
Bit index (0 = lowest) | Details |
---|---|
0 | Frame type |
1 - 2 | Frame translate flag |
3 - 4 | 2-bit color index for Layer 2 |
5 - 6 | 2-bit color index for Layer 1 |
7 | Paper color |
Each line in a layer can use one of 4 different types, which indicates how it should be unpacked.
No data is stored for this line, it is empty and can skipped.
This line is compressed. Compression works by splitting each line into 8-pixel 'chunks' (32 in total) with bitflags to indicate whether a particular chunk is used or not. The line data begins with 32 bits for the chunk flags, followed by the chunk data. If a chunk flag is 1 then you read one byte from the chunk data, otherwise you can skip ahead 8 pixels and try the next chunk flag.
Pseudocode:
line = Array(256)
pixel = 0
# read chunk flags
# they're easier to work with if read as a single big-endian uint32
chunk_flags = file.read_uint32(bigendian=true)
while chunk_flags & 0xFFFFFFFF:
# check the highest chunk flag is set
if chunk_flags & 0x80000000:
chunk = file.read_uint8()
# unpack each bit of the chunk
for bit = 0; bit < 8; bit += 1:
line[pixel] = chunk >> bit & 0x1
pixel += 1
else:
# skip -- no data is stored for this chunk
pixel += 8
chunk_flags <<= 1
The same as type 1, except the pixels in this line are first set to 1 before decoding.
Pseudocode:
line = Array(256)
pixel = 0
for i = 0; i < 256; i += 1:
line[i] = 1
# ... continue reading the line the same way as line type 1
Like line type 1 except every chunk is used, so there's no need for the chunk flags.
Pseudocode:
line = Array(256)
pixel = 0
while pixel < 256:
chunk = file.read_uint8()
# unpack each bit of the chunk
for bit = 0; bit < 8; bit += 1:
line[pixel] = chunk >> bit & 0x1
pixel += 1
The sound effect flags begin at 0x6A0 + the animation data size
.
The sound header offset can be calculated from 0x6A0 + the animation data size + the number of frames
, rounded up to the nearest multiple of 4.
Type | Details |
---|---|
uint32 | BGM track size |
uint32 | SE1 track size |
uint32 | SE2 track size |
uint32 | SE3 track size |
uint8 | Frame playback speed |
uint8 | Frame playback speed when recording bgm |
char[14] | Null padding |
Frame speed values are reversed for whatever reason, you must subtract them from 8 to get the real frame speed.
Value | Frames per second |
---|---|
1 | 1 / 2 |
2 | 1 / 1 |
3 | 2 / 1 |
4 | 4 / 1 |
5 | 6 / 1 |
6 | 12 / 1 |
7 | 20 / 1 |
8 | 30 / 1 |
Sound tracks are stored in the order of BGM, SE1, SE2 then SE3. Each track is monochannel IMA ADPCM audio sampled at 8192 Hz with the nibbles reversed. 1 second of audio is about 4096 bytes long.
You can decode raw Flipnote audio using sox:
sox -t ima -N -r 8192 [input] [output.wav]
The last 144 bytes of a PPM is an RSA-1024 SHA-1 signature over the rest of the file, followed by 16 bytes of null padding.