veloren_voxygen/audio/
ambience.rs

1//! Handles ambient non-positional sounds
2use crate::{
3    audio::{AudioFrontend, channel::AmbienceChannelTag},
4    scene::Camera,
5};
6use client::Client;
7use common::{
8    assets::{self, AssetExt, AssetHandle},
9    terrain::site::SiteKindMeta,
10    vol::ReadVol,
11};
12use common_state::State;
13use serde::Deserialize;
14use strum::IntoEnumIterator;
15use tracing::warn;
16use vek::*;
17
18#[derive(Debug, Default, Deserialize)]
19pub struct AmbienceCollection {
20    tracks: Vec<AmbienceItem>,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct AmbienceItem {
25    path: String,
26    /// Specifies which ambient channel to play on
27    tag: AmbienceChannelTag,
28}
29
30pub struct AmbienceMgr {
31    pub ambience: AssetHandle<AmbienceCollection>,
32}
33
34impl AmbienceMgr {
35    pub fn maintain(
36        &mut self,
37        audio: &mut AudioFrontend,
38        state: &State,
39        client: &Client,
40        camera: &Camera,
41    ) {
42        if !audio.ambience_enabled() {
43            return;
44        }
45
46        let ambience_sounds = self.ambience.read();
47
48        let cam_pos = camera.get_pos_with_focus();
49
50        // Lowpass if underwater
51        if state
52            .terrain()
53            .get(cam_pos.map(|e| e.floor() as i32))
54            .map(|b| b.is_liquid())
55            .unwrap_or(false)
56        {
57            audio.set_ambience_master_filter(888);
58        } else {
59            audio.set_ambience_master_filter(20000);
60        }
61
62        // TODO: The init could be done when the audio context is first created?
63        // Iterate through each tag
64        for tag in AmbienceChannelTag::iter() {
65            // Init: Spawn a channel for each tag
66            // TODO: Find a good way to cull unneeded channels
67            if let Some(inner) = audio.inner.as_mut()
68                && inner.channels.get_ambience_channel(tag).is_none()
69            {
70                inner.new_ambience_channel(tag);
71                let track = ambience_sounds.tracks.iter().find(|track| track.tag == tag);
72                if let Some(track) = track {
73                    audio.play_ambience_looping(tag, &track.path);
74                }
75            }
76            if let Some(inner) = audio.inner.as_mut()
77                && let Some(channel) = inner.channels.get_ambience_channel(tag)
78            {
79                // Maintain: get the correct volume of whatever the tag of the current
80                // channel is
81                let target_volume = get_target_volume(tag, client, camera);
82
83                // Fade to the target volume over a short period of time
84                channel.fade_to(target_volume, 1.0);
85            }
86        }
87    }
88}
89
90impl AmbienceChannelTag {
91    pub fn tag_max_volume(tag: AmbienceChannelTag) -> f32 {
92        match tag {
93            AmbienceChannelTag::Wind => 1.0,
94            AmbienceChannelTag::Rain => 0.95,
95            AmbienceChannelTag::ThunderRumbling => 1.33,
96            AmbienceChannelTag::Leaves => 1.33,
97            AmbienceChannelTag::Cave => 1.0,
98            _ => 1.0,
99        }
100    }
101
102    // Gets appropriate volume for each tag
103    pub fn get_tag_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
104        match tag {
105            AmbienceChannelTag::Wind => {
106                let focus_off = camera.get_focus_pos().map(f32::trunc);
107                let cam_pos = camera.dependents().cam_pos + focus_off;
108
109                let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
110                    (chunk.meta().alt(), chunk.meta().tree_density())
111                } else {
112                    (0.0, 0.0)
113                };
114
115                // Wind volume increases with altitude
116                let alt_factor = (cam_pos.z / 1200.0).abs();
117
118                // Tree density factors into wind volume. The more trees,
119                // the lower wind volume. The trees make more of an impact
120                // the closer the camera is to the ground.
121                let tree_factor = ((1.0 - (tree_density * 0.5))
122                    + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2))
123                .min(1.0);
124
125                // Lastly, we of course have to take into account actual wind speed from
126                // weathersim
127                // Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on
128                // direction)
129                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
130                    / 15.0_f32.powi(2))
131                .min(1.33);
132
133                (alt_factor
134                    * tree_factor
135                    * (wind_speed_factor + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)))
136                    + (alt_factor * 0.15) * tree_factor
137            },
138            AmbienceChannelTag::Rain => {
139                let focus_off = camera.get_focus_pos().map(f32::trunc);
140                let cam_pos = camera.dependents().cam_pos + focus_off;
141
142                let terrain_alt = if let Some(chunk) = client.current_chunk() {
143                    chunk.meta().alt()
144                } else {
145                    0.0
146                };
147                // Make rain diminish with camera distance above terrain
148                let camera_factor = 1.0 - ((cam_pos.z - terrain_alt).abs() / 75.0).powi(2).min(1.0);
149
150                (client.weather_at_player().rain * 3.0) * camera_factor
151            },
152            AmbienceChannelTag::ThunderRumbling => {
153                let rain_intensity = client.weather_at_player().rain * 3.0;
154
155                if rain_intensity < 0.7 {
156                    0.0
157                } else {
158                    rain_intensity
159                }
160            },
161            AmbienceChannelTag::Leaves => {
162                let focus_off = camera.get_focus_pos().map(f32::trunc);
163                let cam_pos = camera.dependents().cam_pos + focus_off;
164
165                let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
166                    (chunk.meta().alt(), chunk.meta().tree_density())
167                } else {
168                    (0.0, 0.0)
169                };
170
171                // Tree density factors into leaves volume. The more trees,
172                // the higher volume. The trees make more of an impact
173                // the closer the camera is to the ground
174                let tree_factor = 1.0
175                    - (((1.0 - tree_density)
176                        + ((cam_pos.z - terrain_alt - 20.0).abs() / 150.0).powi(2))
177                    .min(1.1));
178
179                // Take into account wind speed too, which amplifies tree noise
180                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
181                    / 20.0_f32.powi(2))
182                .min(1.0);
183
184                if tree_factor > 0.1 {
185                    tree_factor * (1.0 + wind_speed_factor)
186                } else {
187                    0.0
188                }
189            },
190            AmbienceChannelTag::Cave => {
191                let focus_off = camera.get_focus_pos().map(f32::trunc);
192                let cam_pos = camera.dependents().cam_pos + focus_off;
193
194                let terrain_alt = if let Some(chunk) = client.current_chunk() {
195                    chunk.meta().alt()
196                } else {
197                    0.0
198                };
199
200                // When the camera is roughly above ground, don't play cave sounds
201                let camera_factor = (-(cam_pos.z - terrain_alt) / 100.0).max(0.0);
202
203                if client.current_site() == SiteKindMeta::Cave {
204                    camera_factor
205                } else {
206                    0.0
207                }
208            },
209            _ => 1.0,
210        }
211    }
212}
213
214/// Checks various factors to determine the target volume to lerp to
215fn get_target_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
216    let focus_off = camera.get_focus_pos().map(f32::trunc);
217    let cam_pos = camera.dependents().cam_pos + focus_off;
218
219    let volume: f32 = AmbienceChannelTag::get_tag_volume(tag, client, camera);
220
221    let terrain_alt = if let Some(chunk) = client.current_chunk() {
222        chunk.meta().alt()
223    } else {
224        0.0
225    };
226
227    // Is the camera underneath the terrain? Fade out the lower it goes beneath.
228    // Unless, of course, the player is in a cave.
229    if tag != AmbienceChannelTag::Cave {
230        (volume * ((cam_pos.z - terrain_alt) / 50.0 + 1.0).clamped(0.0, 1.0))
231            .min(AmbienceChannelTag::tag_max_volume(tag))
232    } else {
233        volume.min(AmbienceChannelTag::tag_max_volume(tag))
234    }
235}
236
237pub fn load_ambience_items() -> AssetHandle<AmbienceCollection> {
238    AmbienceCollection::load_or_insert_with("voxygen.audio.ambience", |error| {
239        warn!(
240            "Error reading ambience config file, ambience will not be available: {:#?}",
241            error
242        );
243        AmbienceCollection::default()
244    })
245}
246
247impl assets::Asset for AmbienceCollection {
248    type Loader = assets::RonLoader;
249
250    const EXTENSION: &'static str = "ron";
251}