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.
The meta-event difference, byte for byte:
tt bytes are the tempo, big-endian microseconds per quarter note.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.
pQES4 B| Field | Meaning |
|---|---|
version | Always 1 in retail. It is a u32 where the PsyQ documentation has a u16 at +4; the two extra bytes shift every field below. |
| PPQN | Ticks per quarter note - always 480. |
tempo | Microseconds per quarter note. A 240 BPM placeholder that the first body event overrides. |
| time signature | Numerator (e.g. 4) and a power-of-two denominator (2 = /4, 3 = /8). |
The event stream starts at +0x0F.
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.
A meta is a running status too. The retail decoder (FUN_80063CEC) latches 0xFF as the channel's running status when it reads a meta, so a data byte after a meta is the next meta's kind: FF 51 t t t <delta> 51 t t t is two tempo events, and a channel event after a meta needs its own status byte. A parser that keeps the previous channel status across a meta falls one byte out of phase on the one track that relies on this.
| 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 as written. 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.
Retail does not play the written tempo exactly: its driver turns each tempo into an integer BPM and a per-vsync tick budget rounded to tenths of a tick, so a track runs at the rate that budget implies - 80 BPM plays at 80.25, and the slow closing tempos drift furthest. The engine Sequencer plays that retail rate by default and the written score behind set_exact_tempo(true); the audio internals page has the derivation.
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 track that needs the meta latch (PROT 1045)
Every stream in the corpus parses to a clean end. PROT 1045 (sound test 57, the "sorrowful event" requiem) closes on a ritardando written with the meta latch: FF 51 0E C4 3E (62 BPM), then 51 0F 42 40 (60 BPM) with no second FF. Read 51 0F 42 as a note instead and the parser falls one byte out of phase, reads the closing volume fade (B5 07 nn / B6 07 nn) as long deltas and notes, walks through the real FF 2F 00 and halts on a 0xF4 eleven bytes past it - the last bars garbled and the loop never reached.
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).
In the port
seq info <PATH>seq events <PATH>seq json <PATH>info prints the header plus an event-type histogram, events disassembles every event, json dumps the full parse. The engine side is legaia_engine_audio::Sequencer: a parsed Seq plus a loaded VabBank driving the from-scratch SPU model.
How we know
Provenance by claim
| 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 |
| A meta latches the running status | FUN_80063CEC (sb v0,0x16(s3) at 0x80063EE4) | 0xFF stored as the channel's running status; a data byte under it dispatches as a meta kind (0x80063F44) |
| Retail tempo quantisation | FUN_80062410 / FUN_80061954 (shared tail) | Integer BPM plus a rounded tenths-of-a-tick budget per SsSeqCalc |
| Event usage, loop markers, pitch bend | Disc-wide sweep of every SEQ-bearing entry | Which status bytes occur, how often, and where program changes land on unused slots |
| 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 |