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::{AssetExt, AssetHandle, Ron},
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<Ron<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
76                    .0
77                    .tracks
78                    .iter()
79                    .find(|track| track.tag == tag);
80                if let Some(track) = track {
81                    audio.play_ambience_looping(tag, &track.path, track.start, track.end);
82                }
83            }
84            if let Some(inner) = audio.inner.as_mut()
85                && let Some(channel) = inner.channels.get_ambience_channel(tag)
86            {
87                // Maintain: get the correct volume of whatever the tag of the current
88                // channel is
89                let target_volume =
90                    if !audio_settings.rain_ambience_enabled && tag == AmbienceChannelTag::Rain {
91                        0.0
92                    } else {
93                        get_target_volume(tag, client, camera)
94                    };
95
96                // Fade to the target volume over a short period of time
97                channel.fade_to(target_volume, 1.0);
98            }
99        }
100    }
101}
102
103impl AmbienceChannelTag {
104    pub fn tag_max_volume(tag: AmbienceChannelTag) -> f32 {
105        match tag {
106            AmbienceChannelTag::Wind => 1.0,
107            AmbienceChannelTag::Rain => 0.95,
108            AmbienceChannelTag::ThunderRumbling => 1.33,
109            AmbienceChannelTag::Leaves => 1.33,
110            AmbienceChannelTag::Cave => 1.0,
111            _ => 1.0,
112        }
113    }
114
115    // Gets appropriate volume for each tag
116    pub fn get_tag_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
117        match tag {
118            AmbienceChannelTag::Wind => {
119                let focus_off = camera.get_focus_pos().map(f32::trunc);
120                let cam_pos = camera.dependents().cam_pos + focus_off;
121
122                let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
123                    (chunk.meta().alt(), chunk.meta().tree_density())
124                } else {
125                    (0.0, 0.0)
126                };
127
128                // Wind volume increases with altitude
129                let alt_factor = (cam_pos.z / 1200.0).abs();
130
131                // Tree density factors into wind volume. The more trees,
132                // the lower wind volume. The trees make more of an impact
133                // the closer the camera is to the ground.
134                let tree_factor = ((1.0 - (tree_density * 0.5))
135                    + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2))
136                .min(1.0);
137
138                // Lastly, we of course have to take into account actual wind speed from
139                // weathersim
140                // Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on
141                // direction)
142                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
143                    / 15.0_f32.powi(2))
144                .min(1.33);
145
146                (alt_factor
147                    * tree_factor
148                    * (wind_speed_factor + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)))
149                    + (alt_factor * 0.15) * tree_factor
150            },
151            AmbienceChannelTag::Rain => {
152                let focus_off = camera.get_focus_pos().map(f32::trunc);
153                let cam_pos = camera.dependents().cam_pos + focus_off;
154
155                let terrain_alt = if let Some(chunk) = client.current_chunk() {
156                    chunk.meta().alt()
157                } else {
158                    0.0
159                };
160                // Make rain diminish with camera distance above terrain
161                let camera_factor = 1.0 - ((cam_pos.z - terrain_alt).abs() / 75.0).powi(2).min(1.0);
162
163                (client.weather_at_player().rain * 3.0) * camera_factor
164            },
165            AmbienceChannelTag::ThunderRumbling => {
166                let rain_intensity = client.weather_at_player().rain * 3.0;
167
168                if rain_intensity < 0.7 {
169                    0.0
170                } else {
171                    rain_intensity
172                }
173            },
174            AmbienceChannelTag::Leaves => {
175                let focus_off = camera.get_focus_pos().map(f32::trunc);
176                let cam_pos = camera.dependents().cam_pos + focus_off;
177
178                let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
179                    (chunk.meta().alt(), chunk.meta().tree_density())
180                } else {
181                    (0.0, 0.0)
182                };
183
184                // Tree density factors into leaves volume. The more trees,
185                // the higher volume. The trees make more of an impact
186                // the closer the camera is to the ground
187                let tree_factor = 1.0
188                    - (((1.0 - tree_density)
189                        + ((cam_pos.z - terrain_alt - 20.0).abs() / 150.0).powi(2))
190                    .min(1.1));
191
192                // Take into account wind speed too, which amplifies tree noise
193                let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
194                    / 20.0_f32.powi(2))
195                .min(1.0);
196
197                if tree_factor > 0.1 {
198                    tree_factor * (1.0 + wind_speed_factor)
199                } else {
200                    0.0
201                }
202            },
203            AmbienceChannelTag::Cave => {
204                let focus_off = camera.get_focus_pos().map(f32::trunc);
205                let cam_pos = camera.dependents().cam_pos + focus_off;
206
207                let terrain_alt = if let Some(chunk) = client.current_chunk() {
208                    chunk.meta().alt()
209                } else {
210                    0.0
211                };
212
213                // When the camera is roughly above ground, don't play cave sounds
214                let camera_factor = (-(cam_pos.z - terrain_alt) / 100.0).max(0.0);
215
216                if client.current_site() == SiteKindMeta::Cave {
217                    camera_factor
218                } else {
219                    0.0
220                }
221            },
222            _ => 1.0,
223        }
224    }
225}
226
227/// Checks various factors to determine the target volume to lerp to
228fn get_target_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
229    let focus_off = camera.get_focus_pos().map(f32::trunc);
230    let cam_pos = camera.dependents().cam_pos + focus_off;
231
232    let volume: f32 = AmbienceChannelTag::get_tag_volume(tag, client, camera);
233
234    let terrain_alt = if let Some(chunk) = client.current_chunk() {
235        chunk.meta().alt()
236    } else {
237        0.0
238    };
239
240    // Is the camera underneath the terrain? Fade out the lower it goes beneath.
241    // Unless, of course, the player is in a cave.
242    if tag != AmbienceChannelTag::Cave {
243        (volume * ((cam_pos.z - terrain_alt) / 50.0 + 1.0).clamped(0.0, 1.0))
244            .min(AmbienceChannelTag::tag_max_volume(tag))
245    } else {
246        volume.min(AmbienceChannelTag::tag_max_volume(tag))
247    }
248}
249
250pub fn load_ambience_items() -> AssetHandle<Ron<AmbienceCollection>> {
251    Ron::load_or_insert_with("voxygen.audio.ambience", |error| {
252        warn!(
253            "Error reading ambience config file, ambience will not be available: {:#?}",
254            error
255        );
256        Ron(AmbienceCollection::default())
257    })
258}