veloren_voxygen/audio/
ambience.rs

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