veloren_voxygen/audio/sfx/event_mapper/block.rs
1/// EventMapper::Block watches the sound emitting blocks within
2/// chunk range of the player and emits ambient sfx
3use crate::{
4 AudioFrontend,
5 audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTriggerItem, SfxTriggers},
6 scene::{Camera, Terrain, terrain::BlocksOfInterest},
7};
8
9use super::EventMapper;
10use client::Client;
11use common::{comp::Pos, spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
12use common_state::State;
13use hashbrown::HashMap;
14use rand::{Rng, prelude::*, seq::SliceRandom, thread_rng};
15use rand_chacha::ChaCha8Rng;
16use std::time::{Duration, Instant};
17use vek::*;
18
19#[derive(Clone, PartialEq)]
20struct PreviousBlockState {
21 event: SfxEvent,
22 time: Instant,
23}
24
25impl Default for PreviousBlockState {
26 fn default() -> Self {
27 Self {
28 event: SfxEvent::Idle,
29 time: Instant::now()
30 .checked_add(Duration::from_millis(thread_rng().gen_range(0..500)))
31 .unwrap_or_else(Instant::now),
32 }
33 }
34}
35
36pub struct BlockEventMapper {
37 history: HashMap<Vec3<i32>, PreviousBlockState>,
38}
39
40impl EventMapper for BlockEventMapper {
41 fn maintain(
42 &mut self,
43 audio: &mut AudioFrontend,
44 state: &State,
45 player_entity: specs::Entity,
46 camera: &Camera,
47 triggers: &SfxTriggers,
48 terrain: &Terrain<TerrainChunk>,
49 client: &Client,
50 ) {
51 let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
52
53 // Get the player position and chunk
54 if let Some(player_pos) = state.read_component_copied::<Pos>(player_entity) {
55 let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
56 (e.floor() as i32).div_euclid(sz as i32)
57 });
58
59 // For determining if crickets should chirp
60 let (terrain_alt, temp) = match client.current_chunk() {
61 Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()),
62 None => (0.0, 0.0),
63 };
64
65 struct BlockSounds<'a> {
66 // The function to select the blocks of interest that we should emit from
67 blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
68 // The range, in chunks, that the particles should be generated in from the player
69 range: usize,
70 // The sound of the generated particle
71 sfx: SfxEvent,
72 // The volume of the sfx
73 volume: f32,
74 // Condition that must be true to play
75 cond: fn(&State) -> bool,
76 }
77
78 let sounds: &[BlockSounds] = &[
79 BlockSounds {
80 blocks: |boi| &boi.leaves,
81 range: 1,
82 sfx: SfxEvent::Birdcall,
83 volume: 1.5,
84 cond: |st| st.get_day_period().is_light(),
85 },
86 BlockSounds {
87 blocks: |boi| &boi.leaves,
88 range: 1,
89 sfx: SfxEvent::Owl,
90 volume: 1.5,
91 cond: |st| st.get_day_period().is_dark(),
92 },
93 BlockSounds {
94 blocks: |boi| &boi.slow_river,
95 range: 1,
96 sfx: SfxEvent::RunningWaterSlow,
97 volume: 1.0,
98 cond: |_| true,
99 },
100 BlockSounds {
101 blocks: |boi| &boi.fast_river,
102 range: 1,
103 sfx: SfxEvent::RunningWaterFast,
104 volume: 1.25,
105 cond: |_| true,
106 },
107 BlockSounds {
108 blocks: |boi| &boi.lavapool,
109 range: 1,
110 sfx: SfxEvent::Lavapool,
111 volume: 1.8,
112 cond: |_| true,
113 },
114 //BlockSounds {
115 // blocks: |boi| &boi.embers,
116 // range: 1,
117 // sfx: SfxEvent::Embers,
118 // volume: 0.15,
119 // //volume: 0.05,
120 // cond: |_| true,
121 // //cond: |st| st.get_day_period().is_dark(),
122 //},
123 BlockSounds {
124 blocks: |boi| &boi.frogs,
125 range: 1,
126 sfx: SfxEvent::Frog,
127 volume: 1.0,
128 cond: |st| st.get_day_period().is_dark(),
129 },
130 //BlockSounds {
131 // blocks: |boi| &boi.flowers,
132 // range: 4,
133 // sfx: SfxEvent::LevelUp,
134 // volume: 1.0,
135 // cond: |st| st.get_day_period().is_dark(),
136 //},
137 BlockSounds {
138 blocks: |boi| &boi.cricket1,
139 range: 1,
140 sfx: SfxEvent::Cricket1,
141 volume: 0.5,
142 cond: |st| st.get_day_period().is_dark(),
143 },
144 BlockSounds {
145 blocks: |boi| &boi.cricket2,
146 range: 1,
147 sfx: SfxEvent::Cricket2,
148 volume: 0.5,
149 cond: |st| st.get_day_period().is_dark(),
150 },
151 BlockSounds {
152 blocks: |boi| &boi.cricket3,
153 range: 1,
154 sfx: SfxEvent::Cricket3,
155 volume: 0.5,
156 cond: |st| st.get_day_period().is_dark(),
157 },
158 BlockSounds {
159 blocks: |boi| &boi.beehives,
160 range: 1,
161 sfx: SfxEvent::Bees,
162 volume: 0.5,
163 cond: |st| st.get_day_period().is_light(),
164 },
165 ];
166 // Iterate through each kind of block of interest
167 for sounds in sounds.iter() {
168 // If the timing condition is false, continue
169 // TODO Address bird hack properly. See TODO below
170 if !(sounds.cond)(state)
171 || (!(sounds.sfx == SfxEvent::Lavapool) && player_pos.0.z < (terrain_alt - 30.0))
172 || (sounds.sfx == SfxEvent::Birdcall && (rng.gen_bool(0.9925) || client.weather_at_player().rain >= 0.07))
173 || (sounds.sfx == SfxEvent::Owl && (rng.gen_bool(0.997) || client.weather_at_player().rain >= 0.14))
174 || (sounds.sfx == SfxEvent::Frog && rng.gen_bool(0.95))
175 //Crickets will not chirp below 5 Celsius
176 || (sounds.sfx == SfxEvent::Cricket1 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
177 || (sounds.sfx == SfxEvent::Cricket2 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
178 || (sounds.sfx == SfxEvent::Cricket3 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
179 {
180 continue;
181 }
182
183 // For chunks surrounding the player position
184 for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) {
185 let chunk_pos = player_chunk + offset;
186
187 // Get all the blocks of interest in this chunk
188 terrain.get(chunk_pos).map(|chunk_data| {
189 // Get the positions of the blocks of type sounds
190 let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest);
191
192 let absolute_pos: Vec3<i32> =
193 Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
194
195 // Replace all RunningWater blocks with just one random one per tick
196 let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow
197 || sounds.sfx == SfxEvent::RunningWaterFast
198 || sounds.sfx == SfxEvent::Lavapool
199 {
200 blocks
201 .choose(&mut rng)
202 .map(std::slice::from_ref)
203 .unwrap_or(&[])
204 } else {
205 blocks
206 };
207
208 // Iterate through each individual block
209 for block in blocks {
210 // TODO Address this hack properly, potentially by making a new
211 // block of interest type which picks fewer leaf blocks
212 // Hack to reduce the number of bird, frog, and water sounds
213 if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
214 && rng.gen_bool(0.9995))
215 || (sounds.sfx == SfxEvent::Frog && rng.gen_bool(0.75))
216 || (sounds.sfx == SfxEvent::RunningWaterSlow && rng.gen_bool(0.5))
217 || (sounds.sfx == SfxEvent::Lavapool && rng.gen_bool(0.99))
218 {
219 continue;
220 }
221 let block_pos: Vec3<i32> = absolute_pos + block;
222 let internal_state = self.history.entry(block_pos).or_default();
223
224 let cam_pos = camera.get_pos_with_focus();
225
226 let block_pos = block_pos.map(|x| x as f32);
227
228 if Self::should_emit(
229 internal_state,
230 triggers.get_key_value(&sounds.sfx),
231 temp,
232 ) {
233 // If the camera is within SFX distance
234 if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
235 let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
236 audio.emit_sfx(
237 sfx_trigger_item,
238 block_pos,
239 Some(sounds.volume),
240 player_pos.0,
241 );
242 }
243 internal_state.time = Instant::now();
244 internal_state.event = sounds.sfx.clone();
245 }
246 }
247 });
248 }
249 }
250 }
251 }
252}
253
254impl BlockEventMapper {
255 pub fn new() -> Self {
256 Self {
257 history: HashMap::new(),
258 }
259 }
260
261 /// Ensures that:
262 /// 1. An sfx.ron entry exists for an SFX event
263 /// 2. The sfx has not been played since it's timeout threshold has elapsed,
264 /// which prevents firing every tick. Note that with so many blocks to
265 /// choose from and different blocks being selected each time, this is
266 /// not perfect, but does reduce the number of plays from blocks that
267 /// have already emitted sfx and are stored in the BlockEventMapper
268 /// history.
269 fn should_emit(
270 previous_state: &PreviousBlockState,
271 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
272 temp: f32,
273 ) -> bool {
274 let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
275
276 if let Some((event, item)) = sfx_trigger_item {
277 //The interval between cricket chirps calculated by converting chunk
278 // temperature to centigrade (we should create a function for this) and applying
279 // the "cricket formula" to it
280 let cricket_interval = (25.0 / (3.0 * ((temp * 30.0) + 15.0))).max(0.5);
281 if &previous_state.event == event {
282 //In case certain sounds need modification to their threshold,
283 //use match event
284 match event {
285 SfxEvent::Cricket1 => {
286 previous_state.time.elapsed().as_secs_f32()
287 >= cricket_interval + rng.gen_range(-0.1..0.1)
288 },
289 SfxEvent::Cricket2 => {
290 //the length and manner of this sound is quite different
291 if cricket_interval < 0.75 {
292 previous_state.time.elapsed().as_secs_f32() >= 0.75
293 } else {
294 previous_state.time.elapsed().as_secs_f32()
295 >= cricket_interval + rng.gen_range(-0.1..0.1)
296 }
297 },
298 SfxEvent::Cricket3 => {
299 previous_state.time.elapsed().as_secs_f32()
300 >= cricket_interval + rng.gen_range(-0.1..0.1)
301 },
302 //Adds random factor to frogs (probably doesn't do anything most of the time)
303 SfxEvent::Frog => {
304 previous_state.time.elapsed().as_secs_f32() >= rng.gen_range(-2.0..2.0)
305 },
306 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
307 }
308 } else {
309 true
310 }
311 } else {
312 false
313 }
314 }
315}