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.