Skip to main content

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