Skip to content

PPM format

james edited this page Nov 12, 2018 · 24 revisions

❗️❗️❗️ This page is a work in progress ❗️❗️❗️

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").

Header

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.

Thumbnail Images

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.

Thumbnail Palette

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

Animation Header

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.

Animation Header Flags

Bit index (0 = lowest) Details
20 Hide layer 1 if set
21 Hide layer 2 if set
30 Loop Flipnote playback if set

Animation Data

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.

Frame Header

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

Layer Data

Each line in a layer can use one of 4 different types, which indicates how it should be unpacked.

Line Decompression

Type 0

No data is stored for this line, it is empty and can skipped.

Type 1

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

Type 2

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

Type 3

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

Sound Effect Flags

The sound effect flags begin at 0x6A0 + the animation data size.

Sound Header

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.

Playback Speeds

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 Data

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]

Signature

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.