Skip to main content

veloren_voxygen/audio/
ambience.rs

1//! Handles ambient non-positional sounds
2
3use crate::{
4    audio::{AudioFrontend, channel::AmbienceChannelTag},
5    scene::{Camera, Terrain},
6    settings::AudioSettings,
7};
8use client::Client;
9use common::{
10    assets::{AssetExt, AssetHandle, Ron},
11    terrain::{Block, CoordinateConversions, TerrainChunk, site::SiteKindMeta},
12    vol::{ReadVol, RectRasterableVol},
13};
14use common_state::State;
15use kira::Tween;
16use serde::Deserialize;
17use std::time::{Duration, Instant};
18use strum::IntoEnumIterator;
19use tracing::warn;
20use vek::*;
21
22const RIVER_BLOCK_UPDATE_FREQ: f32 = 1.0 / 3.0;
23
24#[derive(Debug, Default, Deserialize)]
25pub struct AmbienceCollection {
26    tracks: Vec<AmbienceItem>,
27}
28
29#[derive(Debug, Deserialize)]
30pub struct AmbienceItem {
31    path: String,
32    /// Specifies which ambient channel to play on
33    tag: AmbienceChannelTag,
34    start: usize,
35    end: usize,
36}
37
38pub struct AmbienceMgr {
39    pub ambience: AssetHandle<Ron<AmbienceCollection>>,
40    // Some tracked structures to avoid unnecessary repeated calculations and allocations
41    cam_pos: Vec3<f32>,
42    cam_pos_i32: Vec3<i32>,
43    terrain_alt: f32,
44    river_blocks: Vec<Vec3<i32>>,
45    river_blocks_last_update: Instant,
46    distance_to_closest_river_block: Option<f32>,
47    river_strength: f32,
48}
49
50impl AmbienceMgr {
51    pub fn new(assets: AssetHandle<Ron<AmbienceCollection>>) -> Self {
52        Self {
53            ambience: assets,
54            cam_pos: Vec3::zero(),
55            cam_pos_i32: Vec3::zero(),
56            terrain_alt: 0.0,
57            river_blocks: Vec::new(),
58            river_blocks_last_update: Instant::now(),
59            distance_to_closest_river_block: None,
60            river_strength: 0.0,
61        }
62    }
63
64    pub fn maintain(
65        &mut self,
66        audio: &mut AudioFrontend,
67        audio_settings: &AudioSettings,
68        state: &State,
69        client: &Client,
70        camera: &Camera,
71        terrain: &Terrain,
72    ) {
73        if !audio.ambience_enabled() {
74            return;
75        }
76
77        let ambience_sounds = self.ambience.read();
78
79        self.cam_pos = camera.get_pos_with_focus();
80        self.cam_pos_i32 = self.cam_pos.map(|e| e.floor() as i32);
81
82        self.terrain_alt = if let Some(chunk) = client.current_chunk() {
83            chunk.meta().alt()
84        } else {
85            0.0
86        };
87
88        // Lowpass if underwater
89        let underwater = state
90            .terrain()
91            .get(self.cam_pos.map(|e| e.floor() as i32))
92            .map(|b| b.is_liquid())
93            .unwrap_or(false);
94
95        if underwater {
96            audio.set_ambience_master_filter(888, Tween { ..Tween::default() });
97        } else if audio_settings.indoor_ambience_enabled && is_indoors(self.cam_pos_i32, state) {
98            audio.set_ambience_master_filter(3000, Tween {
99                duration: Duration::from_millis(500),
100                ..Tween::default()
101            });
102        } else {
103            audio.set_ambience_master_filter(20000, Tween {
104                // keep this similar to the indoor tween duration so that it
105                // transitions in & out of the filter smoothly in both
106                // situations. Otherwise the ambience will be very jumpy.
107                duration: Duration::from_millis(500),
108                ..Tween::default()
109            });
110        }
111
112        // Periodically check nearby chunks for river blocks
113        if self.river_blocks_last_update.elapsed()
114            > Duration::from_secs_f32(RIVER_BLOCK_UPDATE_FREQ)
115        {
116            self.get_river_blocks(client, terrain);
117            self.river_blocks_last_update = Instant::now();
118        }
119
120        let closest_water_block = self.river_blocks.iter().min_by(|a, b| {
121            let distance_a = self
122                .cam_pos
123                .distance(Vec3::new(a.x as f32, a.y as f32, a.z as f32));
124            let distance_b = self
125                .cam_pos
126                .distance(Vec3::new(b.x as f32, b.y as f32, b.z as f32));
127            distance_a.total_cmp(&distance_b)
128        });
129        self.distance_to_closest_river_block = if let Some(block) = closest_water_block {
130            Some(Vec3::new(block.x as f32, block.y as f32, block.z as f32).distance(self.cam_pos))
131        } else {
132            None
133        };
134
135        // TODO: The init could be done when the audio context is first created?
136        // Iterate through each tag
137        for tag in AmbienceChannelTag::iter() {
138            // Init: Spawn a channel for each tag
139            // TODO: Find a good way to cull unneeded channels
140            if let Some(inner) = audio.inner.as_mut()
141                && inner.channels.get_ambience_channel(tag).is_none()
142            {
143                inner.new_ambience_channel(tag);
144                let track = ambience_sounds
145                    .0
146                    .tracks
147                    .iter()
148                    .find(|track| track.tag == tag);
149                if let Some(track) = track {
150                    audio.play_ambience_looping(tag, &track.path, track.start, track.end);
151                }
152            }
153            if let Some(inner) = audio.inner.as_mut()
154                && let Some(channel) = inner.channels.get_ambience_channel(tag)
155            {
156                // Maintain: get the correct volume of whatever the tag of the current
157                // channel is
158                let target_volume = if !audio_settings.rain_ambience_enabled
159                    && tag == AmbienceChannelTag::Rain
160                {
161                    0.0
162                } else {
163                    let volume = self.get_tag_volume(tag, client, audio_settings);
164
165                    // Is the camera underneath the terrain? Fade out the lower it goes beneath.
166                    // Unless, of course, the player is in a cave.
167                    if tag != AmbienceChannelTag::Cave {
168                        (volume
169                            * ((self.cam_pos.z - self.terrain_alt) / 50.0 + 1.0).clamped(0.0, 1.0))
170                        .min(tag.get_max_volume())
171                    } else {
172                        volume.min(tag.get_max_volume())
173                    }
174                };
175
176                // Fade to the target volume over a short period of time
177                channel.fade_to(target_volume, 1.0);
178            }
179        }
180    }
181
182    /// Gets appropriate volume for each tag
183    pub fn get_tag_volume(
184        &self,
185        tag: AmbienceChannelTag,
186        client: &Client,
187        audio_settings: &AudioSettings,
188    ) -> f32 {
189        match tag {
190            AmbienceChannelTag::Wind => {
191                let tree_density = if let Some(chunk) = client.current_chunk() {
192                    chunk.meta().tree_density()
193                } else {
194                    0.0
195                };
196
197                // Wind volume increases with altitude
198                let alt_factor = (self.cam_pos.z / 1200.0).abs();
199
200                // Tree density factors into wind volume. The more trees,
201                // the lower wind volume. The trees make more of an impact
202                // the closer the camera is to the ground.
203                let tree_factor = ((1.0 - (tree_density * 0.5))
204                    + ((self.cam_pos.z - self.terrain_alt).abs() / 150.0).powi(2))
205                .min(1.0);
206
207                // Lastly, we of course have to take into account actual wind speed from
208                // weathersim
209                // Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on
210                // direction)
211                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
212                    / 15.0_f32.powi(2))
213                .min(1.33);
214
215                (alt_factor
216                    * tree_factor
217                    * (wind_speed_factor
218                        + ((self.cam_pos.z - self.terrain_alt).abs() / 150.0).powi(2)))
219                    + (alt_factor * 0.15) * tree_factor
220            },
221            AmbienceChannelTag::Rain => {
222                // Make rain diminish with camera distance above terrain
223                let camera_factor = 1.0
224                    - ((self.cam_pos.z - self.terrain_alt).abs() / 75.0)
225                        .powi(2)
226                        .min(1.0);
227
228                let indoor_factor = if audio_settings.indoor_ambience_enabled
229                    && is_indoors(self.cam_pos_i32, client.state())
230                {
231                    0.7
232                } else {
233                    1.0
234                };
235
236                (client.weather_at_player().rain * 3.0) * camera_factor * indoor_factor
237            },
238            AmbienceChannelTag::ThunderRumbling => {
239                let rain_intensity = client.weather_at_player().rain * 3.0;
240
241                if rain_intensity < 0.7 {
242                    0.0
243                } else {
244                    rain_intensity
245                }
246            },
247            AmbienceChannelTag::Leaves => {
248                let tree_density = if let Some(chunk) = client.current_chunk() {
249                    chunk.meta().tree_density()
250                } else {
251                    0.0
252                };
253
254                // Tree density factors into leaves volume. The more trees,
255                // the higher volume. The trees make more of an impact
256                // the closer the camera is to the ground
257                let tree_factor = 1.0
258                    - (((1.0 - tree_density)
259                        + ((self.cam_pos.z - self.terrain_alt - 20.0).abs() / 150.0).powi(2))
260                    .min(1.1));
261
262                // Take into account wind speed too, which amplifies tree noise
263                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
264                    / 20.0_f32.powi(2))
265                .min(1.0);
266
267                if tree_factor > 0.1 {
268                    tree_factor * (1.0 + wind_speed_factor)
269                } else {
270                    0.0
271                }
272            },
273            AmbienceChannelTag::Cave => {
274                // When the camera is roughly above ground, don't play cave sounds
275                let camera_factor = (-(self.cam_pos.z - self.terrain_alt) / 100.0).max(0.0);
276
277                if client.current_site() == SiteKindMeta::Cave {
278                    camera_factor
279                } else {
280                    0.0
281                }
282            },
283            AmbienceChannelTag::RiverLoud => {
284                if let Some(distance) = self.distance_to_closest_river_block {
285                    let hearing_distance = (TerrainChunk::RECT_SIZE.x * 2) as f32;
286                    let listener_factor = (hearing_distance - distance) / hearing_distance;
287                    // A linear upward slope starting at 0.1 and ending with a volume of 1.2.
288                    listener_factor.max(0.0) * (self.river_strength - 0.1) * 1.1
289                } else {
290                    0.0
291                }
292            },
293            AmbienceChannelTag::RiverQuiet => {
294                if let Some(distance) = self.distance_to_closest_river_block {
295                    let hearing_distance = (TerrainChunk::RECT_SIZE.x * 2) as f32;
296                    let listener_factor = (hearing_distance - distance) / hearing_distance;
297                    if self.river_strength < 0.3 {
298                        // A parabolic swing starting fast at approx. 0 and easing to 1 at 0.3.
299                        listener_factor.max(0.0)
300                            * (-11.0 * (self.river_strength - 0.3).powi(2) + 1.0)
301                    } else {
302                        // A linear slide starting with 1 at 0.3 and hitting 0 at 0.9.
303                        listener_factor.max(0.0) * ((-self.river_strength + 0.9) / 0.6)
304                    }
305                } else {
306                    0.0
307                }
308            },
309            _ => 1.0,
310        }
311    }
312
313    fn get_river_blocks(&mut self, client: &Client, terrain: &Terrain) {
314        self.river_blocks.clear();
315        let mut river_velocities = Vec::new();
316        // Sample a chunk spiral of radius 2 around the player character
317        if let Some(chonks) = client.chunks_around(2) {
318            // Skip if no river nearby
319            if chonks
320                .iter()
321                .all(|(chonk, _)| !chonk.meta().contains_river())
322            {
323                self.river_strength = 0.0;
324                return;
325            }
326
327            for (chonk, chunk_pos) in &chonks {
328                if let Some(block_data) = terrain.get(*chunk_pos) {
329                    if block_data.blocks_of_interest.water.is_empty() {
330                        continue;
331                    }
332                    let wpos = Vec3::<i32>::from(chunk_pos.cpos_to_wpos());
333                    self.river_blocks.extend(
334                        block_data
335                            .blocks_of_interest
336                            .water
337                            .iter()
338                            .map(|b| wpos + *b),
339                    );
340                    river_velocities.push(chonk.meta().river_velocity());
341                }
342            }
343        }
344        // Calculate the average velocity, counting chunks not containing the river (but
345        // still containing water) as zero velocity. This makes the system more
346        // resilient to sudden chunk-to-chunk changes in river velocity.
347        let avg_river_velocity = if river_velocities.is_empty() {
348            Vec3::<f32>::zero()
349        } else {
350            let total_velocity: Vec3<f32> = river_velocities.iter().copied().sum();
351            total_velocity / river_velocities.len() as f32
352        };
353        self.river_strength = avg_river_velocity.magnitude_squared() * 2.0;
354    }
355}
356
357impl AmbienceChannelTag {
358    pub fn get_max_volume(&self) -> f32 {
359        match *self {
360            AmbienceChannelTag::Wind => 1.0,
361            AmbienceChannelTag::Rain => 0.95,
362            AmbienceChannelTag::ThunderRumbling => 1.33,
363            AmbienceChannelTag::Leaves => 1.33,
364            AmbienceChannelTag::Cave => 1.0,
365            AmbienceChannelTag::Thunder => 1.0,
366            AmbienceChannelTag::RiverLoud => 1.2,
367            AmbienceChannelTag::RiverQuiet => 1.0,
368        }
369    }
370}
371pub fn is_indoors(cam_pos: Vec3<i32>, state: &State) -> bool {
372    const INDOOR_CHECK_DISTANCE: f32 = 50.0;
373
374    let directions: [Vec3<f32>; 5] = [
375        -Vec3::unit_x(),
376        Vec3::unit_x(),
377        -Vec3::unit_y(),
378        Vec3::unit_y(),
379        Vec3::unit_z(),
380    ];
381    let cam_pos_f32 = cam_pos.map(|e| e as f32);
382
383    directions.iter().all(|dir| {
384        state
385            .terrain()
386            .ray(cam_pos_f32, cam_pos_f32 + dir * INDOOR_CHECK_DISTANCE)
387            .until(Block::is_solid)
388            .cast()
389            .0
390            < INDOOR_CHECK_DISTANCE
391    })
392}
393
394pub fn load_ambience_items() -> AssetHandle<Ron<AmbienceCollection>> {
395    Ron::load_or_insert_with("voxygen.audio.ambience", |error| {
396        warn!(
397            "Error reading ambience config file, ambience will not be available: {:#?}",
398            error
399        );
400        Ron(AmbienceCollection::default())
401    })
402}