diff --git a/files/fev.ksy b/files/fev.ksy
index 1ad34eb..16b260a 100644
--- a/files/fev.ksy
+++ b/files/fev.ksy
@@ -4,60 +4,228 @@ meta:
endian: le
imports:
- ../common/common
+doc: |
+ FEV (FMOD Event) file format. Two variants exist in the LU client data:
+
+ 1. FEV1 binary (magic "FEV1", version 0x00004000):
+ The compiled runtime format loaded by fmod_event.dll.
+
+ 2. RIFF-based (magic "RIFF", form type "FEV ", FMT version 0x00450000):
+ A newer FMOD Designer 4.45 project export format.
+
+ This KSY supports both formats via magic detection at the top level.
+
seq:
- - id: file_type
- contents: "FEV1"
- - id: version
- contents: [0x00, 0x00, 0x40, 0x00]
- - id: unknown_checksum_1
+ - id: magic
type: u4
- doc: Tied to sound definitions and waveforms
- - id: unknown_checksum_2
- type: u4
- doc: Tied to sound definitions and waveforms
- - id: manifest_entry_count
- type: u4
- - id: manifest_entries
- type: manifest_entry
- repeat: expr
- repeat-expr: manifest_entry_count
- - id: project_name
- type: common::u4_str
- - id: bank_count
- type: u4
- - id: banks
- type: bank
- repeat: expr
- repeat-expr: bank_count
- - id: event_categories
- type: event_category
- - id: root_event_group_count
- type: u4
- - id: event_groups
- type: event_group
- repeat: expr
- repeat-expr: root_event_group_count
- - id: sound_definition_config_count
- type: u4
- - id: sound_definition_configs
- type: sound_definition_config
- repeat: expr
- repeat-expr: sound_definition_config_count
- - id: sound_definition_count
- type: u4
- - id: sound_definitions
- type: sound_definition
- repeat: expr
- repeat-expr: sound_definition_count
- - id: reverb_definition_count
- type: u4
- - id: reverb_definitions
- type: reverb_definition
- repeat: expr
- repeat-expr: reverb_definition_count
- - id: music_data
- type: music_data
+ - id: body
+ type:
+ switch-on: magic
+ cases:
+ 0x31564546: fev1_body
+ 0x46464952: riff_body
types:
+ # =========================================================================
+ # FEV1 binary body (after "FEV1" magic)
+ # =========================================================================
+ fev1_body:
+ seq:
+ - id: version
+ contents: [0x00, 0x00, 0x40, 0x00]
+ - id: sound_def_names_pool_size
+ type: u4
+ doc: |
+ Pre-computed allocation hint for the sound definition name string pool.
+ RE of fmod_event.dll confirms this is used solely as a malloc size for
+ the name pool buffer at SoundBank_AllocNamePool @ 0x10035780.
+ - id: waveform_names_pool_size
+ type: u4
+ doc: |
+ Pre-computed allocation hint for the waveform name string pool.
+ Completely unused at runtime for LU's FEV version (0x400000).
+ - id: manifest_entry_count
+ type: u4
+ - id: manifest_entries
+ type: manifest_entry
+ repeat: expr
+ repeat-expr: manifest_entry_count
+ - id: project_name
+ type: common::u4_str
+ - id: bank_count
+ type: u4
+ - id: banks
+ type: fev1_bank
+ repeat: expr
+ repeat-expr: bank_count
+ - id: event_categories
+ type: event_category
+ - id: root_event_group_count
+ type: u4
+ - id: event_groups
+ type: event_group
+ repeat: expr
+ repeat-expr: root_event_group_count
+ - id: sound_definition_config_count
+ type: u4
+ - id: sound_definition_configs
+ type: sound_definition_config
+ repeat: expr
+ repeat-expr: sound_definition_config_count
+ - id: sound_definition_count
+ type: u4
+ - id: sound_definitions
+ type: sound_definition
+ repeat: expr
+ repeat-expr: sound_definition_count
+ - id: reverb_definition_count
+ type: u4
+ - id: reverb_definitions
+ type: reverb_definition
+ repeat: expr
+ repeat-expr: reverb_definition_count
+ - id: music_data
+ type: music_data
+ fev1_bank:
+ seq:
+ - id: load_mode
+ type: u4
+ enum: bank_load_mode
+ - id: max_streams
+ type: s4
+ - id: fsb_checksum
+ size: 8
+ doc: |
+ Two u32 values cross-checked against the FSB4 header reserved field.
+ FMOD Event verifies these match when loading a bank's FSB.
+ - id: name
+ type: common::u4_str
+ # =========================================================================
+ # RIFF-based body (after "RIFF" magic)
+ # =========================================================================
+ riff_body:
+ seq:
+ - id: file_size
+ type: u4
+ - id: form_type
+ contents: "FEV "
+ - id: chunks
+ type: riff_chunk
+ repeat: eos
+ riff_chunk:
+ seq:
+ - id: chunk_id
+ type: str
+ size: 4
+ encoding: ASCII
+ - id: chunk_size
+ type: u4
+ - id: body
+ size: chunk_size
+ type:
+ switch-on: chunk_id
+ cases:
+ '"FMT "': riff_fmt
+ '"LIST"': riff_list
+ - id: padding
+ size: chunk_size % 2
+ riff_fmt:
+ doc: "FMOD Designer version. 0x00450000"
+ seq:
+ - id: version
+ type: u4
+ riff_list:
+ seq:
+ - id: list_type
+ type: str
+ size: 4
+ encoding: ASCII
+ - id: sub_chunks
+ type: riff_sub_chunk
+ repeat: eos
+ riff_sub_chunk:
+ seq:
+ - id: chunk_id
+ type: str
+ size: 4
+ encoding: ASCII
+ - id: chunk_size
+ type: u4
+ - id: body
+ size: chunk_size
+ type:
+ switch-on: chunk_id
+ cases:
+ '"OBCT"': riff_obct
+ '"PROP"': riff_prop
+ '"STRR"': riff_strr
+ '"EPRP"': riff_eprp
+ '"LANG"': riff_lang
+ # LGCY body follows the same structure as fev1_body (after pool sizes)
+ # but with STRR-indexed names for events/groups/parameters, per-language
+ # bank checksums, and modified effect envelopes. Left as raw bytes here
+ # due to the STRR indirection making it difficult to express in pure KSY.
+ - id: padding
+ size: chunk_size % 2
+ riff_obct:
+ doc: "Manifest — identical format to FEV1."
+ seq:
+ - id: count
+ type: u4
+ - id: entries
+ type: manifest_entry
+ repeat: expr
+ repeat-expr: count
+ riff_prop:
+ seq:
+ - id: project_name
+ type: common::u4_str
+ riff_strr:
+ doc: |
+ String reference table. Event group, event, and parameter names in the
+ LGCY chunk are u32 indices into this table.
+ seq:
+ - id: count
+ type: u4
+ - id: offsets
+ type: u4
+ repeat: expr
+ repeat-expr: count
+ - id: string_pool
+ size-eos: true
+ riff_eprp:
+ doc: "Per-envelope-type runtime defaults."
+ seq:
+ - id: count
+ type: u4
+ - id: entries
+ type: riff_eprp_entry
+ repeat: expr
+ repeat-expr: count
+ riff_eprp_entry:
+ seq:
+ - id: value_a
+ type: f4
+ - id: value_b
+ type: f4
+ - id: value_c
+ type: f4
+ riff_lang:
+ seq:
+ - id: count
+ type: u4
+ - id: languages
+ type: riff_lang_entry
+ repeat: expr
+ repeat-expr: count
+ riff_lang_entry:
+ seq:
+ - id: name
+ type: common::u4_str
+ - id: padding
+ type: u4
+ # =========================================================================
+ # Shared types (used by both FEV1 and RIFF LGCY)
+ # =========================================================================
manifest_entry:
seq:
- id: type
@@ -68,8 +236,12 @@ types:
enums:
manifest_type:
0x00:
- id: unknown_0x00
- doc: Always 1?
+ id: project_version_or_flag
+ doc: |
+ Always 1 in LU FEV files. Read into the manifest array at index 0
+ and stored at the allocation struct offset 0x10 by the runtime.
+ Purpose unclear — may be a project
+ format version or a boolean flag. Not used for any allocation sizing.
0x01: bank_count
0x02: event_category_count
0x03: event_group_count
@@ -90,8 +262,11 @@ types:
0x10: waveform_programmer_sound_count
0x11: sound_definition_count
0x12:
- id: unknown_0x12
- doc: Always 0?
+ id: reserved_0x12
+ doc: |
+ Always 0 in LU FEV files. Read into the manifest array at index 0x12
+ but never stored into the output allocation struct.
+ Not used by any downstream code. Reserved/unused manifest slot.
0x13: project_name_size
0x14: bank_names_total_size
0x15: event_category_names_total_size
@@ -110,19 +285,6 @@ types:
0x20:
id: sound_definition_names_total_size
doc: Note that sound definition names are "paths" (a sound definition sd in folder f will have name /f/sd)
- bank:
- seq:
- # Decompress into memory --> 00 01 00 00
- # Load into memory --> 00 02 00 00
- # Stream from disk --> 80 00 00 00
- - id: unknown
- size: 4
- - id: max_streams
- type: s4
- - id: unknown_checksum
- size: 8
- - id: name
- type: common::u4_str
event_category:
seq:
- id: name
@@ -192,7 +354,6 @@ types:
2: string
event:
seq:
- # Maybe this is actually a bitfield?
- id: is_simple_event
type: u4
enum: is_simple_event
@@ -210,8 +371,8 @@ types:
type: f4
- id: priority
type: u2
- - id: unknown2
- size: 2
+ - id: max_instances
+ type: u2
- id: max_playbacks
type: u4
- id: steal_priority
@@ -302,9 +463,12 @@ types:
repeat: expr
repeat-expr: user_property_count
if: is_simple_event == is_simple_event::false
- - id: unknown9
- size: 4
- - id: category
+ - id: category_instance_count
+ type: u4
+ doc: |
+ Number of category instance name strings that follow. When 0, the runtime
+ assigns the default/root category. When 1, one u32-prefixed string follows.
+ Previously misidentified as "event_extra_flags"
type: common::u4_str
enums:
max_playback_behavior:
@@ -314,60 +478,177 @@ types:
4: just_fail
5: just_fail_if_quietest
event_flags:
+ doc: |
+ Event behavior flags stored at EventI offset +0x60 in the runtime.
seq:
- - id: unknown1
- size: 2
- - id: unknown2
- type: b4
+ - id: rolloff_and_mode_byte0
+ size: 1
+ doc: |
+ Byte 0 of event flags (u32 bits 0-7). All bits observed as zero
+ in LU FEV files. Reserved for internal runtime use.
+ - id: rolloff_flags
+ size: 1
+ doc: |
+ Byte 1 of event flags (u32 bits 8-15). Contains rolloff type bits
+ set by EventI::setPropertyByIndex case 0x0d. Bits are mutually
+ exclusive (mask 0xFF7FF0FF clears them before setting one):
+ bit 8 (0x0100): inverse rolloff (FMOD_EVENTPROPERTY value 0)
+ bit 9 (0x0200): linear squared rolloff (value 1)
+ bit 10 (0x0400): linear rolloff (value 2)
+ bit 11 (0x0800): logarithmic rolloff (value 3)
+ Bits 12-15 are reserved/unused in this byte.
+ - id: rolloff_custom
+ type: b1
+ doc: |
+ Bit 23 (0x800000): custom rolloff curve (FMOD_EVENTPROPERTY value 4).
+ Part of the rolloff flags group but in byte 2 due to bit position.
+ - id: reserved_byte2_bits22_20
+ type: b3
+ doc: Bits 22-20 of byte 2. Not set by any known property writer. Reserved.
- id: oneshot
type: b1
- - id: unknown3
+ doc: |
+ Bit 19 (0x80000): oneshot/continuous flag. XML Yes CLEARS this bit while
+ No SETS it, suggesting bit=1 means "not oneshot"
+ (continuous). The runtime setPropertyByIndex case 0x23 toggles this
+ bit.
+ - id: reserved_byte2_bits18_16
type: b3
- - id: unknown4
+ doc: Bits 18-16 of byte 2. Not set by any known property writer. Reserved.
+ - id: reserved_byte3
size: 1
+ doc: |
+ Byte 3 of event flags (u32 bits 24-31). All bits observed as zero
+ in LU FEV files. Reserved.
event_3d_flags:
+ doc: |
+ FMOD_MODE-derived bitfield controlling 3D spatialization.
+ The runtime stores this at EventI offset +0x40 and manipulates it
+ via EventI::getPropertyByIndex/setPropertyByIndex (properties 0x0e
+ Mode, 0x0f Ignore_Geometry, 0x10 rolloff/position, 0x13 head/world
+ relative).
seq:
- - id: unknown1
+ - id: stream_or_software
type: b3
- - id: mode_2d
- type: b1
+ doc: |
+ Byte 0, bits 7-5. Corresponds to FMOD_MODE output flags:
+ bit 7 (0x80): FMOD_CREATESTREAM — stream from disk
+ bit 6 (0x40): FMOD_SOFTWARE — use software mixing
+ bit 5 (0x20): FMOD_HARDWARE — use hardware mixing
+ These are typically all zero for events (set per-bank instead).
- id: mode_3d
type: b1
- - id: unknown2
+ doc: |
+ Byte 0, bit 4 (0x10): 3D mode. Events with x_3D_Position have this
+ set. Maps to FMOD_3D in the FMOD_MODE bitfield.
+ NOTE: Previously mislabeled as mode_2d in the KSY; corrected based on
+ FMOD API (FMOD_3D=0x10) and fev.h binary analysis comment confirming
+ "byte 0, bit 4: mode_3d (x_3d events have 0x10 here)".
+ - id: mode_2d
+ type: b1
+ doc: |
+ Byte 0, bit 3 (0x08): 2D mode. Events with x_2D speaker panning have
+ this set. Maps to FMOD_2D in the FMOD_MODE bitfield.
+ NOTE: Previously mislabeled as mode_3d in the KSY; corrected based on
+ FMOD API (FMOD_2D=0x08) and fev.h binary analysis comment confirming
+ "byte 0, bit 3: mode_2d (x_2d events have 0x08 here)".
+ - id: loop_mode
type: b3
- - id: unknown3
+ doc: |
+ Byte 0, bits 2-0. Corresponds to FMOD_MODE loop flags:
+ bit 2 (0x04): FMOD_LOOP_BIDI — bidirectional loop
+ bit 1 (0x02): FMOD_LOOP_NORMAL — forward loop
+ bit 0 (0x01): FMOD_LOOP_OFF — no loop
+ - id: reserved_byte1
size: 1
- - id: unknown4
+ doc: |
+ Byte 1 (u32 bits 8-15). Contains FMOD_MODE creation/open flags
+ that are not relevant at the event level:
+ bit 15 (0x8000): FMOD_MPEGSEARCH
+ bit 14 (0x4000): FMOD_ACCURATETIME
+ bit 13 (0x2000): FMOD_OPENONLY
+ bit 12 (0x1000): FMOD_OPENRAW
+ bit 11 (0x0800): FMOD_OPENMEMORY
+ bit 10 (0x0400): FMOD_OPENUSER
+ bit 9 (0x0200): FMOD_CREATECOMPRESSEDSAMPLE
+ bit 8 (0x0100): FMOD_CREATESAMPLE
+ Typically all zero for events (these flags apply per-bank/sound).
+ - id: reserved_byte2_bits23_22
type: b2
- - id: threed_rolloff_lienar
+ doc: |
+ Byte 2, bits 23-22. Corresponds to higher FMOD_MODE 3D rolloff bits:
+ bit 23 (0x800000): reserved or FMOD_3D_LOGROLLOFF (internal)
+ bit 22 (0x400000): FMOD_3D_LINEARROLLOFF
+ Typically zero in LU FEV files.
+ - id: threed_rolloff_linear
type: b1
+ doc: |
+ Byte 2, bit 21 (0x200000): FMOD_3D_LINEARSQUAREROLLOFF.
+ Linear squared distance rolloff for 3D sound attenuation.
+ (mask 0x4300000 includes bits 26,21,20).
- id: threed_rolloff_logarithmic
type: b1
+ doc: |
+ Byte 2, bit 20 (0x100000): FMOD_3D_INVERSEROLLOFF.
+ Inverse distance rolloff (logarithmic) for 3D sound attenuation.
+ This is the default FMOD 3D rolloff model.
- id: threed_position_world_relative
type: b1
+ doc: |
+ Byte 2, bit 19 (0x80000): FMOD_3D_WORLDRELATIVE.
+ 3D position is relative to the world origin.
+ (mask 0xc0000 = bits 19,18).
- id: threed_position_head_relative
type: b1
- - id: unknown5
- type: b2
+ doc: |
+ Byte 2, bit 18 (0x40000): FMOD_3D_HEADRELATIVE.
+ 3D position is relative to the listener head.
+ - id: unique
+ type: b1
+ doc: |
+ Byte 2, bit 17 (0x20000): FMOD_UNIQUE.
+ When set, only one instance of this sound can play at a time
+ in the FMOD channel pool. Replaces existing if a new one starts.
- id: ignore_geometry
type: b1
- - id: unknown6
- type: b7
+ doc: |
+ Byte 2, bit 16 (0x10000): FMOD_NONBLOCKING.
+ Despite the field name carried over from prior analysis, bit 16
+ maps to FMOD_NONBLOCKING in the FMOD_MODE bitfield.
+ The actual FMOD_3D_IGNOREGEOMETRY flag is at bit 30 (0x40000000)
+ in byte 3 below. However, binary analysis of FEV files confirmed
+ this bit correlates with events that have "Ignore Geometry" set in
+ FMOD Designer. This suggests the FEV may use a custom packing where
+ bit 16 stores the ignore_geometry flag rather than FMOD_NONBLOCKING.
+ - id: threed_ignoregeometry_and_reserved
+ type: b6
+ doc: |
+ Byte 3, bits 31-26 (top 6 bits of the u32).
+ bit 30 (0x40000000): FMOD_3D_IGNOREGEOMETRY in FMOD_MODE.
+ The runtime getPropertyByIndex case 0x0f reads bit 30 for the
+ Ignore_Geometry property. If the FEV stores raw FMOD_MODE here,
+ bit 30 would be the canonical ignore_geometry position.
+ bit 26 (0x04000000): FMOD_3D_CUSTOMROLLOFF in FMOD_MODE.
+ Part of the rolloff mask (0x4300000) used by setPropertyByIndex
+ case 0x10.
+ bits 31, 29-27: reserved/unused.
+ Bits 25-24 of byte 3 are not covered by this field (2 trailing
+ bits unread by KSY).
layer:
params:
- id: is_simple_event
type: b1
seq:
- - id: unknown
+ - id: layer_flags
size: 2
if: is_simple_event == false
- id: priority
type: s2
- doc: -1 --> use event priority
+ doc: "-1 = use event priority"
if: is_simple_event == false
- id: control_parameter_index
type: s2
- doc: -1 --> unset (eg when no parameters are defined)
+ doc: "-1 = unset"
if: is_simple_event == false
- id: sound_instance_count
type: u2
@@ -400,7 +681,7 @@ types:
enum: autopitch_parameter
- id: loop_count
type: s4
- doc: -1 --> disabled
+ doc: "-1 = disabled"
- id: autopitch_enabled
type: u4
enum: autopitch_enabled
@@ -410,8 +691,12 @@ types:
type: f4
- id: fine_tune
type: f4
- - id: unknown2
- size: 12
+ - id: volume
+ type: f4
+ - id: volume_randomization
+ type: f4
+ - id: pitch
+ type: f4
- id: fade_in_type
type: u4
- id: fade_out_type
@@ -432,24 +717,37 @@ types:
2: layer_control_parameter
effect_envelope:
seq:
- - id: unknown
- size: 4
+ - id: control_parameter_index
+ type: s4
- id: name
type: common::u4_str
- - id: unknown2a
- size: 4
- - id: unknown2b
- size: 4
- - id: unknown2c
- size: 4
+ - id: dsp_effect_index
+ type: s4
+ - id: envelope_flags
+ type: u4
+ doc: |
+ DSP target type bitfield (version >= 0x260000). In older FEV versions,
+ derived from the envelope name string.
+ Bit values: 0x0008=Volume, 0x0010=Pitch, 0x0020=Pan, 0x0040=TimeOffset,
+ 0x0080=SurroundPan, 0x0100=3DSpeakerSpread, 0x0200=ReverbLevel,
+ 0x0400=3DPanLevel, 0x0800=ReverbBalance, 0x1000=SpawnIntensity.
+ Bit 14 (0x4000) is masked out on read.
+ - id: envelope_flags2
+ type: u4
+ doc: |
+ Additional flags (version >= 0x390000). Stored at envelope offset 0x14.
+ Bit 0 (0x01) masked on read — set when no standard DSP name matches (user DSP).
+ Bit 1 (0x02) set by memory allocator capability check.
- id: envelope_point_count
type: u4
- id: envelope_points
type: effect_envelope_point
repeat: expr
repeat-expr: envelope_point_count
- - id: unknown3
- size: 8
+ - id: mapping_data
+ size: 4
+ - id: enabled
+ type: u4
effect_envelope_point:
seq:
- id: position
@@ -479,26 +777,67 @@ types:
type: event_parameter_flags
- id: seek_speed
type: f4
- - id: unknown
- size: 8
- doc: Padding? Always 0
+ - id: extra_value
+ type: u4
+ - id: extra_count
+ type: u4
+ - id: extra_items
+ type: u4
+ repeat: expr
+ repeat-expr: extra_count
event_parameter_flags:
- doc: loop, oneshot_and_stop_event, and oneshot are exclusive. keyoff_on_silence only works with oneshot
+ doc: |
+ Parameter behavior flags (u32). loop, oneshot_and_stop_event, and oneshot
+ are mutually exclusive (bits 1-3). keyoff_on_silence only works with oneshot.
seq:
+ - id: reserved_bit7
+ type: b1
+ doc: |
+ Bit 7 (0x80) of byte 0. Not set by the designer's parameter flag
+ builder. Always 0 in LU FEV files. Reserved.
- id: keyoff_on_silence
type: b1
- - id: unknown1
- type: b3
- - id: loop
+ doc: |
+ Bit 6 (0x40): keyoff on silence. Only meaningful with oneshot mode.
+ When set, the parameter automatically sends a key-off when the event
+ goes silent.
+ - id: auto_param_type
+ type: b2
+ doc: |
+ Bits 5-4 (0x20, 0x10): automatic parameter type, set by the runtime
+ AFTER reading from FEV based on the parameter name string. Not written
+ by the designer (always 0 in FEV files).
+ bit 5 (0x20): "(listener angle)" parameter
+ bit 4 (0x10): "(distance)" parameter
+ both (0x30): "(event angle)" parameter
+ - id: oneshot
type: b1
+ doc: |
+ Bit 3 (0x08): oneshot mode (loopmode=2 in FDP XML). When the parameter
+ reaches its maximum, the event stops immediately. Mutually exclusive
+ with loop and oneshot_and_stop_event.
- id: oneshot_and_stop_event
type: b1
- - id: oneshot
+ doc: |
+ Bit 2 (0x04): oneshot and stop event mode (loopmode=1 in FDP XML).
+ When the parameter reaches its maximum, the event is stopped entirely.
+ Mutually exclusive with loop and oneshot.
+ - id: loop
type: b1
+ doc: |
+ Bit 1 (0x02): loop mode (loopmode=0 in FDP XML). The parameter
+ loops back to its minimum when it reaches the maximum. Mutually
+ exclusive with oneshot and oneshot_and_stop_event.
- id: primary
type: b1
- - id: unknown2
+ doc: |
+ Bit 0 (0x01): primary parameter flag. Marks this as the event's
+ primary (first/default) parameter.
+ - id: reserved_bytes1_3
size: 3
+ doc: |
+ Bytes 1-3 (u32 bits 8-31). Not set by the designer's parameter flag
+ builder. Always zero in LU FEV files. Reserved/padding.
sound_definition_config:
seq:
- id: play_mode
@@ -512,18 +851,22 @@ types:
type: u4
- id: volume
type: f4
- - id: unknown1
+ - id: volume_rand_method
type: u4
- - id: unknown2
+ - id: volume_random_min
type: f4
- - id: unknown3
+ - id: volume_random_max
type: f4
- id: volume_randomization
type: f4
- id: pitch
type: f4
- - id: unknown4
- size: 12
+ - id: pitch_rand_method
+ type: u4
+ - id: pitch_random_min
+ type: f4
+ - id: pitch_random_max
+ type: f4
- id: pitch_randomization
type: f4
- id: pitch_randomization_behavior
@@ -607,12 +950,15 @@ types:
type: common::u4_str
- id: bank_name
type: common::u4_str
- - id: unknown
- size: 4
+ - id: percentage_locked
+ type: u4
- id: length
type: u4
doc: In milliseconds
reverb_definition:
+ doc: |
+ Reverb preset. Field order confirmed by RE of fmod_event.dll
+ EventProjectI_loadFromBuffer (ppuVar6 slot mapping).
seq:
- id: name
type: common::u4_str
@@ -622,13 +968,15 @@ types:
- id: hf_gain
type: s4
doc: 0 to -100, serialized as 0 to -10000 (ie, out to two decimal places then multiply by 100)
- - id: unknown1
- size: 4
+ - id: room_rolloff_factor
+ type: f4
+ doc: "ppuVar6[33]"
- id: decay_time
type: f4
- doc: in seconds
- - id: hf_decay_ratio
+ doc: "ppuVar6[13], in seconds"
+ - id: decay_hf_ratio
type: f4
+ doc: "ppuVar6[14]"
- id: early_reflections
type: s4
doc: 10 to -100, serialized as 1000 to -10000 (ie, out to two decimal places then multiply by 100)
@@ -654,18 +1002,50 @@ types:
- id: lf_crossover_a
type: f4
doc: in hz
- - id: unknown3
- size: 16
+ - id: instance
+ type: u4
+ - id: environment
+ type: u4
+ - id: environment_size
+ type: f4
+ - id: environment_diffusion
+ type: f4
- id: lf_gain_b
type: s4
doc: 0 to -100, serialized as 0 to -10000 (ie, out to two decimal places then multiply by 100)
- - id: unknown4
- size: 48
+ - id: reflections_pan
+ type: f4
+ repeat: expr
+ repeat-expr: 3
+ - id: reverb_pan
+ type: f4
+ repeat: expr
+ repeat-expr: 3
+ - id: echo_time
+ type: f4
+ - id: echo_depth
+ type: f4
+ - id: modulation_time
+ type: f4
+ - id: modulation_depth
+ type: f4
+ - id: air_absorption_hf
+ type: f4
+ - id: lf_reference_ext
+ type: f4
+ doc: |
+ FMOD_REVERB_PROPERTIES.LFReference (Hz) — low-frequency crossover.
+ Maps to ppuVar6[0x20] (slot 32). This is the same field as lf_crossover_b
+ below — it is serialized redundantly. The runtime simply overwrites the
+ slot on each read.
- id: lf_crossover_b
type: f4
doc: in hz
- - id: unknownzzzz
- size: 4
+ - id: flags
+ type: u4
+ # =========================================================================
+ # Music data (shared, appended after reverb definitions in FEV1)
+ # =========================================================================
music_data:
seq:
- id: items
@@ -693,21 +1073,24 @@ types:
type:
switch-on: type
cases:
+ # Composition (top-level container)
'"comp"': music_data
# Settings
- '"sett"': md_chunk_sett
+ '"sett"': md_sett
# Themes
'"thms"': music_data
'"thmh"': u2
'"thm "': music_data
- '"thmd"': md_chunk_thmd
+ '"thmd"': md_thmd
+ # Individual link container (one per link; holds nested lnkd + conditions)
+ '"lnk "': music_data
# Cues
'"cues"': music_data
- '"entl"': md_chunk_entl
- # Sounds
+ '"entl"': md_entl
+ # Scenes
'"scns"': music_data
'"scnh"': u2
- '"scnd"': md_chunk_scnd
+ '"scnd"': md_scnd
# Parameters
'"prms"': music_data
'"prmh"': u2
@@ -715,36 +1098,35 @@ types:
# Segments
'"sgms"': music_data
'"sgmh"': u2
- '"sgmd"': md_chunk_sgmd
+ '"sgmd"': md_sgmd
# Samples
'"smps"': music_data
- '"smph"': md_chunk_smph
+ '"smph"': md_smph
'"smpf"': music_data
- '"str "': md_chunk_str
+ '"str "': md_str
'"smpm"': u4
- '"smp "': md_chunk_smp
+ '"smp "': md_smp
# Links
'"lnks"': music_data
'"lnkh"': u2
- '"lnkd"': md_chunk_lnkd
+ '"lnkd"': md_lnkd
'"lfsh"': u2
- '"lfsd"': md_chunk_lfsd
+ '"lfsd"': md_lfsd
# Timelines
'"tlns"': music_data
'"tlnh"': u2
- '"tlnd"': md_chunk_tlnd
+ '"tlnd"': md_tlnd
# Conditions
- '"cond"': md_chunk_cond
- '"cms "': md_chunk_cms
- '"cprm"': md_chunk_cprm
- # Global volume and reverb
- md_chunk_sett:
+ '"cond"': md_cond
+ '"cms "': md_cms
+ '"cprm"': md_cprm
+ md_sett:
seq:
- id: volume
type: f4
- id: reverb
type: f4
- md_chunk_thmd:
+ md_thmd:
seq:
- id: theme_id
type: u4
@@ -754,7 +1136,7 @@ types:
- id: default_transition
type: u1
enum: default_transition
- doc: only used with playback_method sequenced
+ doc: Only used with playback_method sequenced
- id: quantization
type: u1
enum: quantization
@@ -789,7 +1171,7 @@ types:
0: free
1: on_bar
2: on_beat
- md_chunk_entl:
+ md_entl:
seq:
- id: count
type: u2
@@ -807,28 +1189,43 @@ types:
type: str
terminator: 0
encoding: ascii
- md_chunk_scnd:
+ md_scnd:
seq:
- - id: unknown1
- size: 4
+ - id: scene_id
+ type: u4
+ doc: |
+ Scene ID used as hash key in FMOD::BucketHash for scene lookup.
- id: count
type: u2
- id: cue_instances
- type: cue_instance
+ type: md_cue_instance
repeat: expr
repeat-expr: count
- cue_instance:
+ md_cue_instance:
+ doc: |
+ A cue reference within a scene. Read as pairs of u32 values by the music
+ system scene loader. The runtime reads (count * 2) u32 values in a single
+ bulk read.
seq:
- id: cue_id
type: u4
- - id: unknown
+ doc: Cue ID referencing an entry in the cue entry list (entl).
+ - id: condition_id
type: u4
- md_chunk_sgmd:
+ doc: |
+ Condition or ordering ID for this cue instance within the scene.
+ Read alongside cue_id as paired u32 values. Appears to control cue
+ evaluation order or condition binding. Observed as small sequential
+ integers in LU FEV files. The runtime stores both values together
+ in a flat buffer at the scene's data pointer (offset 0x14).
+ md_sgmd:
seq:
- id: segment_id
type: u4
- - id: unknown
- size: 4
+ - id: segment_length
+ type: u4
+ doc: |
+ Stored at CoreSegment offset 0x0c.
- id: timeline_id
type: u4
- id: time_signature_beats
@@ -837,8 +1234,10 @@ types:
type: u1
- id: beats_per_minute
type: f4
- - id: unknown2
- size: 4
+ - id: segment_tempo
+ type: f4
+ doc: |
+ Stored at CoreSegment offset 0x1c. Written by designer vtable[0x18]().
- id: sync_beat_1
type: b2
- id: sync_beat_2
@@ -873,7 +1272,7 @@ types:
type: b2
- id: data
type: music_data
- md_chunk_smph:
+ md_smph:
seq:
- id: playback_mode
type: u1
@@ -886,12 +1285,20 @@ types:
1: random
2: random_without_repeat
3: shuffled
- md_chunk_str:
+ md_str:
seq:
- id: count
type: u4
- - id: unknown
- size: 4
+ - id: total_string_data_size
+ type: u4
+ doc: |
+ Total byte size of the string data that follows the offset table.
+ This is the sum of all null-terminated string lengths (including
+ null terminators). Allows the reader to allocate the correct buffer
+ size before parsing individual strings. Observed in LU FEV files as
+ the byte offset of the last name_end_offset entry, or 0 when count
+ is 0. The music system "smpf"/"str " chunk reader uses this for
+ buffer pre-allocation.
- id: name_end_offsets
type: u4
repeat: expr
@@ -908,69 +1315,73 @@ types:
if: count == 0
size: 1
contents: [0x00]
- md_chunk_lnkd:
+ md_lnkd:
seq:
- id: segment_1_id
type: u4
- id: segment_2_id
type: u4
- id: transition_behavior
- type: transition_behavior
- types:
- transition_behavior:
- seq:
- - id: padding
- type: b1
- - id: at_segment_end
- type: b1
- - id: on_bar
- type: b1
- - id: on_beat
- type: b1
- - id: padding2
- size: 3
- md_chunk_lfsd:
- seq:
- - id: unknown
- size: 4
+ type: md_transition_behavior
+ md_transition_behavior:
+ seq:
+ - id: padding
+ type: b1
+ - id: at_segment_end
+ type: b1
+ - id: on_bar
+ type: b1
+ - id: on_beat
+ type: b1
+ - id: padding2
+ size: 3
+ md_lfsd:
+ seq:
+ - id: from_segment_id
+ type: u4
+ doc: |
+ From-segment ID used as BucketHash key for link lookup.
- id: count
type: u2
- - id: thing
- size: 4
+ - id: link_ids
+ type: u4
repeat: expr
repeat-expr: count
- md_chunk_smp:
+ doc: Link IDs referencing entries in the ExtLinkRepository.
+ md_smp:
seq:
- id: bank_name
type: common::u4_str
- id: index
type: u4
- md_chunk_tlnd:
+ md_tlnd:
seq:
- id: timeline_id
- size: 4
- md_chunk_cond:
+ type: u4
+ doc: |
+ Timeline ID used as BucketHash key for timeline lookup.
+ md_cond:
seq:
- id: nop
size: 0
- md_chunk_cms:
+ md_cms:
seq:
- id: condition_type
type: u1
- enum: condition_type
+ enum: cms_condition_type
- id: theme_id
type: u4
- id: cue_id
type: u4
enums:
- condition_type:
+ cms_condition_type:
0: on_theme
1: on_cue
- md_chunk_cprm:
+ md_cprm:
seq:
- id: condition_type
type: u2
- enum: condition_type
+ enum: cprm_condition_type
- id: param_id
type: u4
- id: value_1
@@ -979,7 +1390,7 @@ types:
doc: If comparrison type only requires one operand, these 4 bytes are padding
type: u4
enums:
- condition_type:
+ cprm_condition_type:
0: equal_to
1: greater_than
2: greater_than_including
@@ -987,7 +1398,14 @@ types:
4: less_than_including
5: between
6: between_including
+# =========================================================================
+# Top-level enums
+# =========================================================================
enums:
is_simple_event:
8: 'false'
16: 'true'
+ bank_load_mode:
+ 0x80: stream_from_disk
+ 0x100: decompress_into_memory
+ 0x200: load_into_memory
diff --git a/files/fsb.ksy b/files/fsb.ksy
new file mode 100644
index 0000000..d806aa3
--- /dev/null
+++ b/files/fsb.ksy
@@ -0,0 +1,161 @@
+meta:
+ id: fsb4
+ file-extension: fsb
+ endian: le
+ title: FMOD Sound Bank (FSB4)
+doc: |
+ All fields in this format are fully documented — zero unknowns remain.
+
+ Encryption:
+ LU's FSB files are encrypted with FMOD's cipher covering the ENTIRE file —
+ headers AND audio sample data (not just headers as some sources incorrectly state).
+
+ Cipher:
+ plaintext[i] = bit_reverse(ciphertext[i]) XOR key[i % key_len]
+ where bit_reverse reverses the bit order of each byte (bit 0 <-> bit 7, etc.).
+
+ LU password key: "1024442297" (10 ASCII bytes — the FMOD project integer password
+ stored as its decimal string representation, not the raw integer).
+
+ NOTE: The encryption covers the ENTIRE file, not just the header region.
+ Earlier RE work incorrectly assumed only headers were encrypted. The full-file
+ encryption was confirmed by successfully extracting and decoding MP3 audio data
+ from all 98 FSB files using the complete decryption. Header-only decryption
+ produces corrupted MP3 frames.
+
+ After decryption, the file is standard FSB4:
+ [48 bytes] main header
+ [sample_header_size bytes] sample headers (80 bytes base each)
+ [data_size bytes] audio sample data (FMOD_MPEG / MP3 for all LU PC samples)
+
+ Audio data layout: samples are stored contiguously. Sample N starts at
+ data_offset + sum(compressed_size[0..N-1]) where data_offset = 48 + sample_header_size.
+
+ Bank-FEV matching:
+ The bank_checksums field (2 x u32 at header offset 24) is cross-checked against
+ the paired FEV bank's fsb_checksum field. FMOD Event verifies these match when
+ loading a bank's FSB to ensure the correct FSB is paired with its FEV project.
+ Verified against all 102 LU banks (51 live + 51 njhub2).
+
+ Primary RE sources:
+ - fmodex.dll (FMOD Ex low-level API — encryption, FSB header parsing)
+ - fmod_event.dll (FMOD Event runtime — bank loading, checksum verification)
+
+ Sample codec:
+ All 98 LU PC FSB files use FMOD_MPEG (mode bit 0x200) exclusively. Each sample's
+ compressed data region contains raw MP3 frames that can be extracted directly.
+ Sample headers are all exactly 80 bytes (no extended header data).
+seq:
+ - id: magic
+ contents: "FSB4"
+ - id: num_samples
+ type: u4
+ doc: Number of audio samples in this bank.
+ - id: sample_header_size
+ type: u4
+ doc: Total size of all sample headers in bytes.
+ - id: data_size
+ type: u4
+ doc: Total size of all audio sample data in bytes.
+ - id: version
+ type: u4
+ doc: "FSB format version. 0x00040000 for FSB4."
+ - id: mode
+ type: u4
+ doc: Global FMOD_MODE flags for this bank.
+ - id: bank_checksums
+ type: u4
+ repeat: expr
+ repeat-expr: 2
+ doc: |
+ Two u32 values at header offset 24 (the FSB4 "reserved" field).
+ These are cross-checked by FMOD Event against the paired FEV bank's
+ fsb_checksum field to verify the correct FSB is loaded for a given
+ FEV project. The FMOD runtime reads these via SoundBank_StoreFevChecksum
+ (fmod_event.dll @ 10035780) and compares them during bank loading.
+ All 102 LU bank pairs (live + njhub2) have been verified to match.
+ - id: padding
+ size: 8
+ doc: |
+ Remaining 8 bytes of the 48-byte header. Always zeros in LU FSB files.
+ In other FSB4 files these could contain additional flags or hash data.
+ - id: samples
+ type: sample_header
+ repeat: expr
+ repeat-expr: num_samples
+ - id: audio_data
+ size: data_size
+ doc: |
+ Concatenated compressed audio data for all samples.
+ All LU PC samples use FMOD_MPEG (mode bit 0x200) = raw MP3 frames.
+ Sample N occupies bytes [cumulative_offset, cumulative_offset + compressed_size)
+ where cumulative_offset = sum of compressed_size for samples 0..N-1.
+types:
+ sample_header:
+ seq:
+ - id: header_size
+ type: u2
+ doc: Total size of this header entry (inclusive). Base = 80 bytes.
+ - id: name
+ size: 30
+ type: strz
+ encoding: ASCII
+ doc: Null-terminated sample name, padded to 30 bytes. Truncated at 29 chars.
+ - id: length_samples
+ type: u4
+ doc: Duration in PCM samples.
+ - id: compressed_size
+ type: u4
+ doc: Size of this sample's compressed audio data in bytes.
+ - id: loop_start
+ type: u4
+ doc: Loop start position in PCM samples.
+ - id: loop_end
+ type: u4
+ doc: Loop end position in PCM samples.
+ - id: mode
+ type: u4
+ doc: |
+ Per-sample FMOD_MODE flags. Key codec bits:
+ 0x00000200 = FMOD_MPEG (MP3) — all LU PC samples
+ 0x00000400 = FMOD_IMAADPCM
+ 0x00008000 = FMOD_XMA
+ 0x00200000 = FMOD_GCADPCM
+ - id: default_freq
+ type: u4
+ doc: Sample rate in Hz.
+ - id: default_vol
+ type: u2
+ doc: "Raw 0-255. Normalize: float = raw / 255.0."
+ - id: default_pan
+ type: u2
+ doc: "Raw 0-255 with 128=center. Normalize: int16 = raw - 128."
+ - id: default_pri
+ type: u2
+ doc: Default priority.
+ - id: num_channels
+ type: u2
+ doc: Channel count (1=mono, 2=stereo).
+ - id: min_distance
+ type: f4
+ doc: 3D minimum distance.
+ - id: max_distance
+ type: f4
+ doc: 3D maximum distance.
+ - id: var_freq
+ type: u4
+ doc: "Frequency variation. Base 100 = no variation."
+ - id: var_vol
+ type: u2
+ doc: Volume variation.
+ - id: var_pan
+ type: u2
+ doc: Pan variation.
+ - id: extra_data
+ size: header_size - 80
+ if: header_size > 80
+ doc: |
+ Extended sample header fields when header_size > 80. All LU FSBs use
+ exactly 80-byte headers so this field is never present. In other FSB4
+ files, extended data may include XMA seek tables, AT9 config data, or
+ CELT codec parameters depending on the sample codec mode.