Files
switchnote/FORMAT.md
T
2026-10-29 03:55:40 +00:00

189 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Switchnote `.sppm` format
`.sppm` is Switchnote Studio's own offline animation format. It is not a Nintendo `.ppm`
file. All integers are unsigned little-endian unless stated otherwise. Readers
must check every length before allocating or advancing. `include/sppm.hpp` and
`source/sppm.cpp` are the reference implementation.
The Fakenote Studio repository supplies 1280×720 UI mockups and 640×360 sample
artwork, with editor buttons for layers A through D. It does **not** specify a
binary format or an exact ink palette. The payload uses a 640×360
canvas, four indexed layers, and 256 editable RGBA palette entries. Entry 0 is
transparent. The four layers composite D, C, B, A from back to front; pattern
brushes write ordinary palette indices in their chosen pattern.
## Header
| Offset | Size | Value |
| ---: | ---: | --- |
| 0 | 4 | ASCII `SPPM` |
| 4 | 2 | Payload version `2` (`1` remains readable) |
| 6 | 2 | Header size `36` |
| 8 | 8 | Total file byte length |
| 16 | 4 | CRC-32/IEEE of bytes from offset 36 to end |
| 20 | 2 | Canvas width `640` |
| 22 | 2 | Canvas height `360` |
| 24 | 1 | Layer count `4` |
| 25 | 2 | Palette count `256` |
| 27 | 1 | Playback FPS, 1–30 |
| 28 | 1 | Flags: bit 0 locked, bit 1 allows spin-offs, bit 2 BGM follows animation speed, bit 3 stops after the final frame (clear = loop) |
| 29 | 4 | Frame count, 1–9999 |
| 33 | 1 | Audio track count, 0–4 |
| 34 | 2 | Reserved zero |
Maximum file size is 512 MiB. The CRC protects against accidental damage;
it is not an authentication tag.
## Legacy signed v2 envelope
Legacy files may use the binary/RLE payload with the header version set to `2`,
followed by the four-byte ASCII marker `SPPS` and a 32-byte HMAC-SHA-256 tag.
The tag covers the complete v2 payload, including its header, metadata, frames
and audio. Readers verify the tag in constant time before decoding the payload.
The HMAC key is retained in an encrypted, console-bound `identity-v2-*.bin`
record. This is a legacy local-file integrity scheme; current app saves use the
publicly verifiable Ed25519-signed v3 envelope below. Neither scheme is
Nintendo's signature system or proof of a real-world creator identity.
Readers accept both old v1 and current v2 payloads inside an authenticated
envelope.
`.sppm` is a compact binary encoding with RLE layers, not plaintext and not an
encrypted format.
## Public v3 signatures
Gallery files use the same SPPM payload with its marker set to version `3`,
followed by `SPP3`, public author/original-author/editor identities, and a
64-byte Ed25519 signature. The signed digest covers the complete payload and
all three identities. The server validates the signature and checks each
available FSID against its included public identity. This is self-certifying:
it proves that the included editor key signed those bytes, not that the key
belongs to a Nintendo account or a verified human.
The Ed25519 secret is encrypted at rest with XSalsa20-Poly1305 and a wrapping
key derived from system-specific console material. On CFW, this material is
available to homebrew and is not a hardware-backed, non-exportable secret; a
compromised console can still expose its signing key. The protection is
intended to make a copied SD identity file unusable on an ordinary different
console, not to resist a compromised device.
## Palette and metadata
Immediately after the header are 256 RGBA entries, each four bytes in R, G,
B, A order. The next five fields are UTF-8 strings with `u16 byte length` then
that many bytes: title (at most 256 bytes), description (4096), author name
(128), original author name (128), and last editor name (128). NUL bytes are
forbidden. The reader rejects malformed UTF-8 and preserves valid bytes;
the frontend is responsible for display and editing.
The default drawing palette uses pink at index 6 and purple at index 10.
When reading older SPPMs, the exact opaque legacy colors `(92,224,36)` and
`(26,40,72)` are remapped to `(239,68,133)` and `(132,66,183)`. Palette indices
are unchanged; saving writes the migrated colors back into the SPPM.
Six IDs follow, each exactly 16 uppercase hexadecimal ASCII characters:
1. Author FSID, the current Switchnote creator.
2. Original author FSID, retained through all spin-offs.
3. Last editor FSID.
4. Current note ID, newly allocated for a spin-off.
5. Root note ID, retained through all spin-offs.
6. Parent note ID, the immediate source Switchnote. Sixteen ASCII zeroes encode
an absent parent on an original note. Zeroes are never a valid allocated ID.
The metadata ends with creation time (`u64` Unix seconds UTC), modification
time (`u64` Unix seconds UTC), and cover frame index (`u32`, zero-based).
Original notes have `currentId == rootId`, no parent, and author FSID equal to
original author FSID. A spin-off must have a new current ID different from its
parent and root IDs. Editing changes the last editor and modification time;
the original author and root ID do not change.
## Frames
Frames appear sequentially. Each frame contains four layer records in A–D
order. A layer record has a codec byte (`1` for RLE), a `u32` compressed byte
length, a `u32` CRC-32/IEEE of the compressed bytes, then the compressed data.
Decoded data is exactly 230,400 row-major `u8` palette indices. The decoder
rejects output beyond that size, truncated packets, unknown codecs, and excess
data. A compressed layer may contain at most 460,800 bytes.
RLE packets have a one-byte command. Bit 7 set means repeat the following
single palette index `(command & 0x7F) + 1` times. Bit 7 clear means copy that
many literal palette indices from the following bytes. Both packet types allow
lengths 1–128. The writer uses repeat packets for runs of at least three.
## Audio
Audio track 0 is background music, starting at frame 0. Tracks 1–3 are sound
effects with frame cues. Tracks are stored in order and each record contains:
| Field | Encoding |
| --- | --- |
| Track index | `u8`, zero-based |
| Channels | `u8`, mono `1` or stereo `2` |
| BGM reference FPS | `u8`, 1–30 for BGM (or 0 to use note speed); zero for SE. Added in payload v2. |
| Sample rate | `u32`, 8000–48000 Hz |
| Name | `u16` byte length + up to 64 UTF-8 bytes |
| Trigger count | `u16` |
| Trigger frames | That many `u16` zero-based frame indices, strictly increasing |
| PCM sample count | `u32`, counts interleaved samples, not stereo pairs |
| PCM CRC | `u32` CRC-32/IEEE of sample bytes |
| PCM | Signed 16-bit little-endian interleaved samples |
BGM has no triggers. A sound effect may be cued multiple times. Every cue must
refer to an existing frame. Total decoded PCM is limited to 32 Mi samples.
To store SE without music, make track 0 an empty BGM entry with a valid rate
and channel count.
With the BGM speed flag set, playback rate is `current FPS / reference FPS`:
changing animation speed changes music tempo and pitch together. With the flag
clear, BGM keeps its imported timing. SE tracks always play at their recorded
speed. The v1 payload defaults to speed-linked BGM.
The playback-loop flag defaults to looping for older files. When set, playback
stops all Switchnote tracks at the end of the final frame; when clear, frame zero
and its BGM/SE cues start together on each loop.
## IDs and spin-offs
Lineage IDs are **64-bit pseudonymous identifiers**, generated as the first
eight HMAC-SHA-256 digest bytes rendered in uppercase hex. The HMAC key is a
32-byte per-install secret generated with the platform cryptographic random
generator and kept in the encrypted console-local identity record. The
message is:
```
ASCII("FlipnoteNX-FSID-v1") || 00 || ASCII(domain) || 00 || 16-byte nonce
```
This historical domain string is retained to keep existing Switchnote identities, signatures, and saved files compatible.
The portable core exposes `deriveFsid(secret, nonce, domain)` so the Switch
frontend can provide randomness through libnx. It uses a fresh random nonce
for each new note ID. Creator FSIDs are the first eight SHA-256 bytes of a
domain-separated Ed25519 public key and a 16-byte one-way device tag. These
values are still 16 hexadecimal characters (64 bits): an FSID is a public
identifier, not a password or proof of authorship.
Gallery coin and comment writes require a random, single-use, 30-second server
challenge signed with the console's Ed25519 key. The server verifies the
signature, FSID/key binding, request method/path/body, and requesting IP. Coin
limits and poster ownership use the full 256-bit signing-key fingerprint,
independent of the shortened FSID and public device tag. Gallery HTTP is LAN-only and does not encrypt public data in
transit; do not expose the development service to the public internet.
Creating a spin-off is an explicit operation. It requires the source to allow
spin-offs and not be locked. It copies frames, palette, and audio; keeps the
root note ID and original author credit; sets parent ID to the source current
ID; allocates a new current ID; sets the new author and last editor; resets
creation/modification time; and makes the child editable. The child may be
saved independently without modifying the parent.
## Safe writing
Signed saves serialize and validate the complete document, write a sibling
temporary file, then replace the target. If the SD filesystem refuses to
replace an existing file by rename, the old file is moved to a unique sibling
backup, the new file is installed, and the old file is restored if installation
fails. Signed loads authenticate the envelope before replacing the caller's
document; legacy loads still validate all bounds, IDs, frame data, CRCs, and
metadata.