SEQ - PsyQ sequenced-music format Confirmed
SEQ is the sheet music. Every tune in Legaia - the town themes, the battle loop, the world-map fanfare - is stored not as recorded audio but as a list of notes and timings that the console plays live against a VAB instrument bank. It is the PlayStation SDK's sequenced-music format: essentially a single-track MIDI file, close enough to MIDI that generic tools almost work and different enough that they fail in two specific, diagnosable ways.
At a glance
- Magic
pQES(bytes70 51 45 53) at the track's own offset 0- Where on disc
- Behind a VAB inside scene-VAB-prefixed streaming entries, never at the entry's start; 92 tracks. The
music_01bank map is piecewise (sound-test tracki= extraction988+ifori <= 67,990+iabove) - see music tracks - Header
- 15 bytes (Legaia variant) - u32 big-endian version; PPQN always 480
- Parser
crates/seq(binaryseq); playbackengine-audio::Sequencer- Confidence
- Confirmed - public PsyQ shape; the divergences are pinned by the retail loader and by round-BPM arithmetic across the corpus (how we know)
- Used by
- Audio stack, Media player, Asset viewer
Why your MIDI tool chokes
- The version field is a u32 big-endian where the documentation says u16, shifting every later field by two bytes. A 13-byte-header parser reads the wrong PPQN and tempo.
- Meta events carry no length byte. MIDI writes a tempo change as
FF 51 03 tt tt tt; SEQ writesFF 51 tt tt tt. Read a phantom length byte and the first tempo override is lost - and since every retail track ships a 240 BPM placeholder header tempo that the first body event corrects, everything plays ~3x too fast.
Header
Every retail track uses the 15-byte Legaia layout; crates/seq::parse_header also accepts the documented 13-byte PsyQ layout, dispatching on whether the u32 at +4 reads 1.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | magic | pQES |
+0x04 | u32 BE | version | always 1 in retail; the two extra bytes are what shifts everything below (PsyQ: u16 at +4) |
+0x08 | u16 BE | resolution | PPQN, ticks per quarter note - always 480 |
+0x0A | u24 BE | tempo | microseconds per quarter note; a 240 BPM placeholder the first body event overrides |
+0x0D | u8 | time-sig num | e.g. 4 |
+0x0E | u8 | time-sig denom | power of 2 (2 = /4, 3 = /8) |
+0x0F | - | events | event stream |
The retail loader checks version and rejects any other value as an old SEQ format.
Event stream
Each event is a variable-length delta time, a status byte and its data - standard MIDI encoding, with running status (a first byte below 0x80 reuses the previous status). Channel is the low nibble.
| Status | Event | Data | Retail use |
|---|---|---|---|
0x9n | Note On | key, velocity | The note event; velocity == 0 is Note Off (0x8n is never used) |
0xBn | Control Change | controller, value | CC7 volume and CC10 pan are dynamic and busy; CC99 carries loop markers |
0xCn | Program Change | program | Selects a VAB program slot |
0xEn | Pitch Bend | LSB, MSB | Heavy use - thousands of events; scaled over the tone's own pbmin/pbmax |
0xAn / 0xDn | Aftertouch | 2 / 1 | Absent from retail data |
FF 51 | Set Tempo | 3 bytes, u24 BE | No length prefix; overrides future events only |
FF 2F | End of Track | none | Two bytes total; retail writes a trailing 00 the parser never reads |
Any other meta type has an unknown fixed length, so the parser (like the retail reader) cannot skip it and ends the track there.
Loop markers
Looping is encoded as ordinary control changes: controller 99 value 20 = Loop Start, value 30 = Loop Forever (jump back to the last Loop Start). 88 of 92 tracks carry them. The engine Sequencer rewinds to the event after the marker and resets its sample clock, so the loop re-fires on the same sample offset every pass; an end-of-track after a Loop Start also rewinds.
Tempo math
Per-tick duration is tempo / ppqn microseconds. The engine clock keeps every term integer - it accumulates in units of sample × ppqn × 1 000 000 and fires a delta-d event once the accumulator reaches d × tempo × 44100 - so long tracks never drift. legaia_seq::us_per_tick returns the float form for inspection only.
Edge cases the engine reproduces
- Program change to an unused VAB slot. Retail's bank-open writes a running used-program counter into each slot, so a change to an unused slot aliases onto the next used slot's tone page (see VAB). Eight such changes exist across four entries; two are audible (PROT 868 program 5, PROT 996 program 19) and the engine aliases them the same way. Past the last used slot retail reads garbage; the engine stays silent there.
- Truncation. An unknown meta type or a system status byte (
0xF0..=0xFE) stops parsing; the parser appends a synthetic End of Track and marks the track truncated so players and parity oracles can tell it from a clean end.
Details: the one non-clean track (PROT 1045)
The stream is a valid, complete track. A few notes before its real end the parser drifts by exactly one byte inside a dense run of running-status notes (near offset ~4900, beside a non-round-BPM tempo event). Latched one byte ahead, it reads the closing fade's volume CCs as deltas plus note data, walks through the real FF 2F 00, and halts on a 0xF4 eleven bytes past the true end. Every other track in the corpus ends cleanly.
Where the data lives and how it is played
SEQ payloads ride behind a VAB inside the scene streaming containers (scene bundles), so the bytes sit at a non-zero offset. The field VM's BGM opcode (0x35) writes a slot that the BGM loader resolves through the CDNAME map to the containing entry; playback goes through the PsyQ SsSeqOpen family (audio stack).
seq info <PATH> # header summary + event-type histogram
seq events <PATH> # disassemble every event in source order
seq json <PATH> # full parse as JSON
The engine side is legaia_engine_audio::Sequencer: a parsed Seq plus a loaded VabBank driving the from-scratch SPU model.
How we know
| Claim | Function / address | What it proves |
|---|---|---|
| Version check | FUN_80062410 (SsAPI loader, SCUS) | Reads the version word and rejects != 1 |
| No meta length byte | Corpus arithmetic | Read raw, every retail tempo lands on a round BPM (65, 70, 80, 128, 130, 140, 150, 160, 170); a phantom length byte scatters them - real_seq_stream_integrity |
| Event usage, loop markers, pitch bend | Disc-wide sweep of every SEQ-bearing entry | real_seq_expressive_events, real_seq_program_change_coverage |
| BGM slot resolution | _DAT_8007BAC8 → FUN_800243F0 | Field-VM BGM slot resolved through CDNAME to the streaming entry |
| Program-slot aliasing | FUN_80068D94 (bank open) | Used-program counter stored per slot, read back as tone-page index |