Skip to main content

rpfm_lib/files/sound_bank/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Wwise SoundBank (`.bnk`) file support.
12//!
13//! This module handles Audiokinetic Wwise SoundBank files used in modern Total War games.
14//! SoundBanks contain audio event definitions, hierarchies, and metadata that control
15//! how sounds are played in the game.
16//!
17//! For more info about the Wwise format, check <https://github.com/bnnm/wwiser/>.
18//!
19//! # Supported Sections
20//!
21//! Currently only partial support is implemented:
22//! - `BKHD` (Bank Header) - Contains version and bank metadata
23//! - `HIRC` (Hierarchy) - Contains audio object definitions and event actions
24//!
25//! Other sections like `DATA`, `DIDX`, `STID`, etc. are recognized but not yet supported.
26
27use getset::*;
28use serde_derive::{Serialize, Deserialize};
29
30use crate::binary::{ReadBytes, WriteBytes};
31use crate::error::{Result, RLibError};
32use crate::files::{Decodeable, EncodeableExtraData, Encodeable};
33use crate::utils::check_size_mismatch;
34
35use self::sections::bkhd::BKHD;
36use self::sections::hirc::HIRC;
37
38use super::DecodeableExtraData;
39
40/// Extension used by SoundBank files.
41pub const EXTENSION: &str = ".bnk";
42
43// Hash type identifiers for FNV hashing (used internally by Wwise).
44const FNV_NO: &str = "none";        // Special value, no hashname allowed
45const FNV_BNK: &str = "bank";
46const FNV_LNG: &str = "language";
47const FNV_EVT: &str = "event";
48const FNV_BUS: &str = "bus";
49const FNV_SFX: &str = "sfx";
50const FNV_TRG: &str = "trigger";
51const FNV_GME: &str = "rtpc/game-variable";
52const FNV_VAR: &str = "variable";   // Switches/states names
53const FNV_VAL: &str = "value";      // Switches/states values
54const FNV_UNK: &str = "???";
55
56const FNV_ORDER: [&str; 10] = [
57  FNV_BNK, FNV_LNG, FNV_EVT, FNV_BUS, FNV_SFX, FNV_TRG, FNV_GME, FNV_VAR, FNV_VAL, FNV_UNK
58];
59
60const FNV_ORDER_JOIN: [&str; 3] = [
61  FNV_BNK, FNV_LNG, FNV_BUS
62];
63
64// Section signature constants (4-byte ASCII identifiers).
65const SIGNATURE_AKBK: &str = "AKBK"; // Audiokinetic Bank (container format)
66const SIGNATURE_BKHD: &str = "BKHD"; // Bank Header
67const SIGNATURE_HIRC: &str = "HIRC"; // Hierarchy
68const SIGNATURE_DATA: &str = "DATA"; // Embedded audio data
69const SIGNATURE_FXPR: &str = "FXPR"; // FX Parameters
70const SIGNATURE_ENVS: &str = "ENVS"; // Environment Settings
71const SIGNATURE_STID: &str = "STID"; // String Mappings
72const SIGNATURE_STMG: &str = "STMG"; // Global Settings
73const SIGNATURE_DIDX: &str = "DIDX"; // Media Index
74const SIGNATURE_PLAT: &str = "PLAT"; // Custom Platform
75const SIGNATURE_INIT: &str = "INIT"; // Plugin
76
77mod common;
78mod sections;
79
80#[cfg(test)] mod test_soundbank;
81
82//---------------------------------------------------------------------------//
83//                              Enum & Structs
84//---------------------------------------------------------------------------//
85
86/// A decoded Wwise SoundBank file.
87///
88/// SoundBanks are container files that hold audio events, hierarchies, and optionally
89/// embedded audio data. They are identified by their `.bnk` extension.
90///
91/// The file consists of multiple sections, each with a 4-byte signature and size prefix.
92/// The first section must always be `BKHD` (Bank Header).
93#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
94#[getset(get = "pub", get_mut = "pub", set = "pub")]
95pub struct SoundBank {
96    /// Sections contained in this SoundBank.
97    sections: Vec<Section>,
98}
99
100/// A section within a SoundBank file.
101///
102/// Each section type contains different data:
103/// - `BKHD`: Bank header with version info (always first)
104/// - `HIRC`: Hierarchy of audio objects and events
105#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
106pub enum Section {
107    /// Bank Header section containing version and metadata.
108    BKHD(BKHD),
109    /// Hierarchy section containing audio object definitions.
110    HIRC(HIRC),
111}
112
113//---------------------------------------------------------------------------//
114//                        Implementation of SoundBank
115//---------------------------------------------------------------------------//
116
117impl Decodeable for SoundBank {
118
119    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
120        let mut decoded = Self::default();
121        let data_len = data.len()?;
122
123        while let Ok(section_signature) = data.read_string_u8(4) {
124            let section_size = data.read_u32()? as u64;
125            //let section_start = data.stream_position()?;
126            //let section_end = section_start + section_size;
127
128            dbg!(&section_signature);
129            dbg!(&section_size);
130            decoded.sections.push(match &*section_signature {
131                SIGNATURE_AKBK => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_AKBK.to_string())),
132
133                // First node is always a BKHD (BanK HeaDer?).
134                SIGNATURE_BKHD => Section::BKHD(BKHD::read(data, section_size as usize)?),
135                SIGNATURE_HIRC => {
136                    let header = match decoded.sections.first() {
137                        Some(Section::BKHD(section)) => section,
138                        _ => return Err(RLibError::SoundBankBKHDNotFound),
139                    };
140
141                    Section::HIRC(HIRC::read(data, *header.version())?)
142                },
143                SIGNATURE_DATA => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_DATA.to_string())),
144                SIGNATURE_FXPR => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_FXPR.to_string())),
145                SIGNATURE_ENVS => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_ENVS.to_string())),
146                SIGNATURE_STID => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_STID.to_string())),
147                SIGNATURE_STMG => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_STMG.to_string())),
148                SIGNATURE_DIDX => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_DIDX.to_string())),
149                SIGNATURE_PLAT => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_PLAT.to_string())),
150                SIGNATURE_INIT => return Err(RLibError::SoundBankUnsupportedSectionFound(SIGNATURE_INIT.to_string())),
151                _ => return Err(RLibError::SoundBankUnsupportedSectionFound(section_signature)),
152            });
153        }
154
155        check_size_mismatch(data.stream_position()? as usize, data_len as usize)?;
156
157        Ok(decoded)
158    }
159}
160
161impl Encodeable for SoundBank {
162
163    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
164        let header = match self.sections.first() {
165            Some(Section::BKHD(section)) => section,
166            _ => return Err(RLibError::SoundBankBKHDNotFound),
167        };
168
169        for section in self.sections() {
170            match section {
171                Section::BKHD(data) => data.write(buffer)?,
172                Section::HIRC(data) => data.write(buffer, *header.version())?,
173            }
174        }
175
176        Ok(())
177    }
178}