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.0,
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.0,
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.2,
98                    cond: |_| true,
99                },
100                BlockSounds {
101                    blocks: |boi| &boi.fast_river,
102                    range: 1,
103                    sfx: SfxEvent::RunningWaterFast,
104                    volume: 1.5,
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: 0.8,
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.33,
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.33,
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.33,
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.995))
173                    || (sounds.sfx == SfxEvent::Owl && rng.gen_bool(0.998))
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))
177                    || (sounds.sfx == SfxEvent::Cricket2 && (temp < -0.33))
178                    || (sounds.sfx == SfxEvent::Cricket3 && (temp < -0.33))
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                                    );
241                                }
242                                internal_state.time = Instant::now();
243                                internal_state.event = sounds.sfx.clone();
244                            }
245                        }
246                    });
247                }
248            }
249        }
250    }
251}
252
253impl BlockEventMapper {
254    pub fn new() -> Self {
255        Self {
256            history: HashMap::new(),
257        }
258    }
259
260    /// Ensures that:
261    /// 1. An sfx.ron entry exists for an SFX event
262    /// 2. The sfx has not been played since it's timeout threshold has elapsed,
263    ///    which prevents firing every tick. Note that with so many blocks to
264    ///    choose from and different blocks being selected each time, this is
265    ///    not perfect, but does reduce the number of plays from blocks that
266    ///    have already emitted sfx and are stored in the BlockEventMapper
267    ///    history.
268    fn should_emit(
269        previous_state: &PreviousBlockState,
270        sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
271        temp: f32,
272    ) -> bool {
273        let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
274
275        if let Some((event, item)) = sfx_trigger_item {
276            //The interval between cricket chirps calculated by converting chunk
277            // temperature to centigrade (we should create a function for this) and applying
278            // the "cricket formula" to it
279            let cricket_interval = (25.0 / (3.0 * ((temp * 30.0) + 15.0))).max(0.5);
280            if &previous_state.event == event {
281                //In case certain sounds need modification to their threshold,
282                //use match event
283                match event {
284                    SfxEvent::Cricket1 => {
285                        previous_state.time.elapsed().as_secs_f32()
286                            >= cricket_interval + rng.gen_range(-0.1..0.1)
287                    },
288                    SfxEvent::Cricket2 => {
289                        //the length and manner of this sound is quite different
290                        if cricket_interval < 0.75 {
291                            previous_state.time.elapsed().as_secs_f32() >= 0.75
292                        } else {
293                            previous_state.time.elapsed().as_secs_f32()
294                                >= cricket_interval + rng.gen_range(-0.1..0.1)
295                        }
296                    },
297                    SfxEvent::Cricket3 => {
298                        previous_state.time.elapsed().as_secs_f32()
299                            >= cricket_interval + rng.gen_range(-0.1..0.1)
300                    },
301                    //Adds random factor to frogs (probably doesn't do anything most of the time)
302                    SfxEvent::Frog => {
303                        previous_state.time.elapsed().as_secs_f32() >= rng.gen_range(-2.0..2.0)
304                    },
305                    _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
306                }
307            } else {
308                true
309            }
310        } else {
311            false
312        }
313    }
314}