banished_formats/audio.rs
1//! Audio resources: `Audio::SoundEffect` (Runtime), and the game's `MusicList`,
2//! `AnimAudioEvent`, `AmbientEmitterDescription` and `AmbientEnvironmentDescription`.
3//! See docs/AUDIO.md.
4
5use crate::crc::type_id;
6use crate::crs::{self, Reader};
7use crate::error::{format_err, Result};
8
9/// One playable variant of a sound (0x18 bytes: buffer, pitch range, volume).
10#[derive(Debug, Clone, PartialEq)]
11pub struct SoundVariant {
12 /// `Audio\…\X.rsc:resource` SoundBuffer.
13 pub buffer: Option<String>,
14 /// The lowest pitch, in semitones. `GetSoundBuffer` draws uniformly in
15 /// `[pitch_min, pitch_max]`.
16 pub pitch_min: f32,
17 /// The highest pitch, in semitones.
18 pub pitch_max: f32,
19 /// The volume.
20 pub volume: f32,
21}
22
23/// A sound of the effect's timeline: starts `start` seconds after the effect.
24#[derive(Debug, Clone, PartialEq)]
25pub struct EffectSound {
26 /// When it starts, in seconds after the effect.
27 pub start: f32,
28 /// The variants.
29 pub variants: Vec<SoundVariant>,
30}
31
32/// A sound effect: a timeline of sounds, each with variants, a pitch range and a volume, and the
33/// radius within which it can be heard.
34///
35/// Original: `Audio::SoundEffect::Serialize`, Runtime `0x1800e32f0`.
36#[derive(Debug, Clone, PartialEq)]
37pub struct SoundEffect {
38 // +0x20
39 /// Audible distance (3D effects).
40 pub radius: f32,
41 // +0x24
42 /// Full-volume distance.
43 pub inner_radius: f32,
44 /// The timeline's sounds.
45 pub sounds: Vec<EffectSound>,
46 // +0x28
47 /// The effect's length in microseconds; 0 when a sound loops (the effect never ends). The
48 /// engine computes it when it compiles the effect.
49 ///
50 /// Original: `Audio::SoundEffect::Compile`, Runtime `0x1800e31f0`.
51 pub max_length: i64,
52}
53
54/// Read a [`SoundEffect`] from the bytes of its resource.
55pub fn sound_effect(data: &[u8]) -> Result<SoundEffect> {
56 let (_, value) = crs::parse(data, |r: &mut Reader, h| {
57 if h.type_id != type_id("SoundEffect") {
58 return format_err(format!("expected SoundEffect, found {:#010x}", h.type_id));
59 }
60 r.block(|r, version| {
61 let radius = r.f32()?;
62 let inner_radius = r.f32()?;
63 let sounds = (0..r.count()?)
64 .map(|_| {
65 let start = r.f32()?;
66 let variants = (0..r.count()?)
67 .map(|_| {
68 Ok(SoundVariant {
69 buffer: r.external_ref()?,
70 pitch_min: r.f32()?,
71 pitch_max: r.f32()?,
72 volume: r.f32()?,
73 })
74 })
75 .collect::<Result<_>>()?;
76 Ok(EffectSound { start, variants })
77 })
78 .collect::<Result<_>>()?;
79 let max_length = if version >= 1 { r.i64()? } else { 0 };
80 Ok(SoundEffect {
81 radius,
82 inner_radius,
83 sounds,
84 max_length,
85 })
86 })
87 })?;
88 Ok(value)
89}
90
91/// One `MusicList` track (0x18 bytes): played when the month and the town's mood are in range.
92///
93/// Original: `FUN_1400a95b0`.
94#[derive(Debug, Clone, PartialEq)]
95pub struct MusicTrack {
96 /// The music's sound effect.
97 pub effect: Option<String>,
98 // +0x8, +0xc
99 /// Mood range (health and happiness fractions, averaged).
100 pub mood: (f32, f32),
101 // +0x10, +0x14
102 /// Month range, inclusive.
103 pub months: (i32, i32),
104}
105
106/// `MusicList` (`Game/MusicTracks.rsc`).
107pub fn music_list(data: &[u8]) -> Result<Vec<MusicTrack>> {
108 let (_, value) = crs::parse(data, |r: &mut Reader, h| {
109 if h.type_id != type_id("MusicList") {
110 return format_err(format!("expected MusicList, found {:#010x}", h.type_id));
111 }
112 r.block(|r, _| {
113 (0..r.count()?)
114 .map(|_| {
115 let mood = (r.f32()?, r.f32()?);
116 let months = (r.i32()?, r.i32()?);
117 Ok(MusicTrack {
118 effect: r.external_ref()?,
119 mood,
120 months,
121 })
122 })
123 .collect()
124 })
125 })?;
126 Ok(value)
127}
128
129/// `AnimAudioEvent`: an animation event that plays a 3D effect at the entity (sound type 4) with
130/// probability `probability`.
131///
132/// Original: `FUN_14008d310`.
133#[derive(Debug, Clone, PartialEq)]
134pub struct AnimAudioEvent {
135 /// The effect.
136 pub effect: Option<String>,
137 /// The probability that it plays.
138 pub probability: f32,
139}
140
141/// Read a [`AnimAudioEvent`] from the bytes of its resource.
142pub fn anim_audio_event(data: &[u8]) -> Result<AnimAudioEvent> {
143 let (_, value) = crs::parse(data, |r: &mut Reader, h| {
144 if h.type_id != type_id("AnimAudioEvent") {
145 return format_err(format!(
146 "expected AnimAudioEvent, found {:#010x}",
147 h.type_id
148 ));
149 }
150 r.block(|r, _| {
151 Ok(AnimAudioEvent {
152 effect: r.external_ref()?,
153 probability: r.f32()?,
154 })
155 })
156 })?;
157 Ok(value)
158}
159
160// +0x20
161/// `AmbientEmitterDescription`: the effects an ambient emitter feeds, and whether activation starts
162/// them.
163///
164/// Original: `FUN_140043b10`.
165#[derive(Debug, Clone, PartialEq)]
166pub struct AmbientEmitterDescription {
167 /// The effects.
168 pub effects: Vec<Option<String>>,
169 /// Whether activation starts them.
170 pub autostart: bool,
171}
172
173/// Read a [`AmbientEmitterDescription`] from the bytes of its resource.
174pub fn ambient_emitter_description(data: &[u8]) -> Result<AmbientEmitterDescription> {
175 let (_, value) = crs::parse(data, |r: &mut Reader, h| {
176 if h.type_id != type_id("AmbientEmitterDescription") {
177 return format_err(format!(
178 "expected AmbientEmitterDescription, found {:#010x}",
179 h.type_id
180 ));
181 }
182 r.block(|r, _| {
183 let effects = (0..r.count()?)
184 .map(|_| r.external_ref())
185 .collect::<Result<_>>()?;
186 Ok(AmbientEmitterDescription {
187 effects,
188 autostart: r.bool()?,
189 })
190 })
191 })?;
192 Ok(value)
193}
194
195/// One terrain class of an `AmbientEnvironmentDescription` (0x28 bytes).
196#[derive(Debug, Clone, PartialEq)]
197pub struct AmbientTerrain {
198 /// Cell types (bit = map cell type) this class counts.
199 pub cell_mask: u32,
200 /// When set, only cells whose entity uses this template count.
201 pub entity: Option<String>,
202 /// The warm effect, played at `t` of the temperature cross-fade.
203 pub warm: Option<String>,
204 /// The cold effect, played at `1 - t` of the temperature cross-fade.
205 pub cold: Option<String>,
206 /// The weight it adds for each cell counted.
207 pub weight: i32,
208}
209
210/// `AmbientEnvironmentDescription`.
211///
212/// Original: `FUN_1400442b0`.
213#[derive(Debug, Clone, PartialEq)]
214pub struct AmbientEnvironmentDescription {
215 /// The terrain classes.
216 pub terrains: Vec<AmbientTerrain>,
217 // +0x30, +0x34; +0x18
218 /// Environment range of the cross-fade.
219 pub fade: (f32, f32),
220}
221
222/// Read a [`AmbientEnvironmentDescription`] from the bytes of its resource.
223pub fn ambient_environment_description(data: &[u8]) -> Result<AmbientEnvironmentDescription> {
224 let (_, value) = crs::parse(data, |r: &mut Reader, h| {
225 if h.type_id != type_id("AmbientEnvironmentDescription") {
226 return format_err(format!(
227 "expected AmbientEnvironmentDescription, found {:#010x}",
228 h.type_id
229 ));
230 }
231 r.block(|r, _| {
232 let terrains = (0..r.count()?)
233 .map(|_| {
234 Ok(AmbientTerrain {
235 cell_mask: r.u32()?,
236 entity: r.external_ref()?,
237 warm: r.external_ref()?,
238 cold: r.external_ref()?,
239 weight: r.i32()?,
240 })
241 })
242 .collect::<Result<_>>()?;
243 Ok(AmbientEnvironmentDescription {
244 terrains,
245 fade: (r.f32()?, r.f32()?),
246 })
247 })
248 })?;
249 Ok(value)
250}