fmod/core/channel_control/
spatialization.rs

1// Copyright (c) 2024 Melody Madeline Lyons
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7use std::{ffi::c_float, mem::MaybeUninit};
8
9use fmod_sys::*;
10
11use crate::{ChannelControl, Vector};
12
13#[cfg(doc)]
14use crate::{Channel, ChannelGroup, Mode};
15use crate::{FmodResultExt, Result};
16
17impl ChannelControl {
18    /// Sets the 3D position and velocity used to apply panning, attenuation and doppler.
19    ///
20    /// The [`Mode::D3`] flag must be set on this object otherwise [`FMOD_RESULT::FMOD_ERR_NEEDS3D`] is returned.
21    ///
22    /// Vectors must be provided in the correct handedness.
23    ///
24    /// For a stereo 3D sound, you can set the spread of the left/right parts in speaker space by using [`ChannelControl::set_3d_spread`].
25    pub fn set_3d_attributes(
26        &self,
27        position: Option<Vector>,
28        velocity: Option<Vector>,
29    ) -> Result<()> {
30        // vector is layout compatible with FMOD_VECTOR
31        let position = position
32            .as_ref()
33            .map_or(std::ptr::null(), std::ptr::from_ref)
34            .cast();
35        let velocity = velocity
36            .as_ref()
37            .map_or(std::ptr::null(), std::ptr::from_ref)
38            .cast();
39        unsafe {
40            FMOD_ChannelControl_Set3DAttributes(self.inner.as_ptr(), position, velocity).to_result()
41        }
42    }
43
44    /// Retrieves the 3D position and velocity used to apply panning, attenuation and doppler.
45    pub fn get_3d_attributes(&self) -> Result<(Vector, Vector)> {
46        let mut position = MaybeUninit::zeroed();
47        let mut velocity = MaybeUninit::zeroed();
48        unsafe {
49            FMOD_ChannelControl_Get3DAttributes(
50                self.inner.as_ptr(),
51                position.as_mut_ptr(),
52                velocity.as_mut_ptr(),
53            )
54            .to_result()?;
55
56            let position = position.assume_init().into();
57            let velocity = velocity.assume_init().into();
58
59            Ok((position, velocity))
60        }
61    }
62
63    /// Sets the orientation of a 3D cone shape, used for simulated occlusion.
64    ///
65    /// The [`Mode::D3`] flag must be set on this object otherwise [`FMOD_RESULT::FMOD_ERR_NEEDS3D`] is returned.
66    ///
67    /// This function has no effect unless [`ChannelControl::set_3d_cone_settings`] has been used to change the cone inside/outside angles from the default.
68    ///
69    /// Vectors must be provided in the correct handedness.
70    pub fn set_3d_cone_orientation(&self, mut orientation: Vector) -> Result<()> {
71        // this probably doesn't need to be mutable? more safe to be mutable just in case
72        let orientation = std::ptr::from_mut(&mut orientation).cast();
73        unsafe {
74            FMOD_ChannelControl_Set3DConeOrientation(self.inner.as_ptr(), orientation).to_result()
75        }
76    }
77
78    /// Retrieves the orientation of a 3D cone shape, used for simulated occlusion.
79    pub fn get_3d_cone_orientation(&self) -> Result<Vector> {
80        let mut orientation = MaybeUninit::zeroed();
81        unsafe {
82            FMOD_ChannelControl_Get3DConeOrientation(self.inner.as_ptr(), orientation.as_mut_ptr())
83                .to_result()?;
84
85            let orientation = orientation.assume_init().into();
86
87            Ok(orientation)
88        }
89    }
90
91    /// Sets the angles and attenuation levels of a 3D cone shape, for simulated occlusion which is based on direction.
92    ///
93    /// The [`Mode::D3`] flag must be set on this object otherwise [`FMOD_RESULT::FMOD_ERR_NEEDS3D`] is returned.
94    ///
95    /// When [`ChannelControl::set_3d_cone_orientation`] is used and a 3D 'cone' is set up,
96    /// attenuation will automatically occur for a sound based on the relative angle of the direction the cone is facing,
97    /// vs the angle between the sound and the listener.
98    /// - If the relative angle is within the `inside_angle`, the sound will not have any attenuation applied.
99    /// - If the relative angle is between the `inside_angle` and `outside_angle`,
100    ///   linear volume attenuation (between 1 and `outside_volume`) is applied between the two angles until it reaches the `outside_angle`.
101    /// - If the relative angle is outside of the `outside_angle` the volume does not attenuate any further.
102    pub fn set_3d_cone_settings(
103        &self,
104        inside_angle: c_float,
105        outside_angle: c_float,
106        outside_volume: c_float,
107    ) -> Result<()> {
108        unsafe {
109            FMOD_ChannelControl_Set3DConeSettings(
110                self.inner.as_ptr(),
111                inside_angle,
112                outside_angle,
113                outside_volume,
114            )
115            .to_result()
116        }
117    }
118
119    /// Retrieves the angles and attenuation levels of a 3D cone shape, for simulated occlusion which is based on direction.
120    ///
121    /// When [`ChannelControl::set_3d_cone_orientation`] is used and a 3D 'cone' is set up,
122    /// attenuation will automatically occur for a sound based on the relative angle of the direction the cone is facing,
123    /// vs the angle between the sound and the listener.
124    /// - If the relative angle is within the `inside_angle`, the sound will not have any attenuation applied.
125    /// - If the relative angle is between the `inside_angle` and `outside_angle`,
126    ///   linear volume attenuation (between 1 and `outside_volume`) is applied between the two angles until it reaches the `outside_angle`.
127    /// - If the relative angle is outside of the `outside_angle` the volume does not attenuate any further.
128    pub fn get_3d_cone_settings(&self) -> Result<(c_float, c_float, c_float)> {
129        let mut inside_angle = 0.0;
130        let mut outside_angle = 0.0;
131        let mut outside_volume = 0.0;
132        unsafe {
133            FMOD_ChannelControl_Get3DConeSettings(
134                self.inner.as_ptr(),
135                &raw mut inside_angle,
136                &raw mut outside_angle,
137                &raw mut outside_volume,
138            )
139            .to_result()?;
140        }
141        Ok((inside_angle, outside_angle, outside_volume))
142    }
143
144    /// Sets a custom roll-off shape for 3D distance attenuation.
145    ///
146    /// Must be used in conjunction with [`Mode::CUSTOM_ROLLOFF_3D`] flag to be activated.
147    ///
148    /// If [`Mode::CUSTOM_ROLLOFF_3D`] is set and the roll-off shape is not set, FMOD will revert to [`Mode::INVERSE_ROLLOFF_3D`] roll-off mode.
149    ///
150    /// When a custom roll-off is specified a [`Channel`] or [`ChannelGroup`]'s 3D 'minimum' and 'maximum' distances are ignored.
151    ///
152    /// The distance in-between point values is linearly interpolated until the final point where the last value is held.
153    ///
154    /// If the points are not sorted by distance, an error will result.
155    ///
156    /// # Safety
157    ///
158    /// This function does not duplicate the memory for the points internally.
159    /// The memory you pass to FMOD must remain valid while in use.
160    pub unsafe fn set_3d_custom_rolloff(&self, points: &mut [Vector]) -> Result<()> {
161        // probably doesn't need to be mutable, but more safe to be mutable just in case
162        unsafe {
163            FMOD_ChannelControl_Set3DCustomRolloff(
164                self.inner.as_ptr(),
165                points.as_mut_ptr().cast(),
166                points.len() as i32,
167            )
168            .to_result()
169        }
170    }
171
172    /// Retrieves the current custom roll-off shape for 3D distance attenuation.
173    pub fn get_3d_custom_rolloff(&self) -> Result<Vec<Vector>> {
174        let mut points = std::ptr::null_mut();
175        let mut num_points = 0;
176        unsafe {
177            FMOD_ChannelControl_Get3DCustomRolloff(
178                self.inner.as_ptr(),
179                &raw mut points,
180                &raw mut num_points,
181            )
182            .to_result()?;
183
184            let points = std::slice::from_raw_parts(points.cast(), num_points as usize).to_vec();
185
186            Ok(points)
187        }
188    }
189
190    /// Sets an override value for the 3D distance filter.
191    ///
192    /// If distance filtering is enabled, by default the 3D engine will automatically attenuate frequencies using a lowpass and a highpass filter, based on 3D distance.
193    /// This function allows the distance filter effect to be set manually, or to be set back to 'automatic' mode.
194    ///
195    /// The `FMOD_3D` flag must be set on this object otherwise `FMOD_ERR_NEEDS3D` is returned.
196    ///
197    /// The System must be initialized with `FMOD_INIT_CHANNEL_DISTANCEFILTER` for this feature to work.
198    ///
199    /// #### NOTE: Currently only supported for [`Channel`], not [`ChannelGroup`].
200    pub fn set_3d_distance_filter(
201        &self,
202        custom: bool,
203        custom_level: c_float,
204        center_freq: c_float,
205    ) -> Result<()> {
206        unsafe {
207            FMOD_ChannelControl_Set3DDistanceFilter(
208                self.inner.as_ptr(),
209                custom,
210                custom_level,
211                center_freq,
212            )
213            .to_result()
214        }
215    }
216
217    /// Retrieves the override values for the 3D distance filter.
218    pub fn get_3d_distance_filter(&self) -> Result<(bool, c_float, c_float)> {
219        let mut custom = false;
220        let mut custom_level = 0.0;
221        let mut center_freq = 0.0;
222        unsafe {
223            FMOD_ChannelControl_Get3DDistanceFilter(
224                self.inner.as_ptr(),
225                &raw mut custom,
226                &raw mut custom_level,
227                &raw mut center_freq,
228            )
229            .to_result()?;
230        }
231        Ok((custom, custom_level, center_freq))
232    }
233
234    /// Sets the amount by which doppler is scaled.
235    ///
236    /// The `FMOD_3D` flag must be set on this object otherwise `FMOD_ERR_NEEDS3D` is returned.
237    ///
238    /// The doppler effect will disabled if `System::set3DNumListeners` is given a value greater than 1.
239    ///
240    /// #### NOTE: Currently only supported for [`Channel`], not [`ChannelGroup`].
241    pub fn set_3d_doppler_level(&self, level: c_float) -> Result<()> {
242        unsafe { FMOD_ChannelControl_Set3DDopplerLevel(self.inner.as_ptr(), level).to_result() }
243    }
244
245    /// Retrieves the amount by which doppler is scaled.
246    pub fn get_3d_doppler_level(&self) -> Result<c_float> {
247        let mut level = 0.0;
248        unsafe {
249            FMOD_ChannelControl_Get3DDopplerLevel(self.inner.as_ptr(), &raw mut level)
250                .to_result()?;
251        }
252        Ok(level)
253    }
254
255    /// Sets the blend between 3D panning and 2D panning.
256    ///
257    /// The `FMOD_3D` flag must be set on this object otherwise `FMOD_ERR_NEEDS3D` is returned.
258    ///
259    /// 2D functions include:
260    ///
261    /// - `ChannelControl::set_pan`
262    /// - `ChannelControl::set_mix_levels_output`
263    /// - `ChannelControl::set_mix_levels_input`
264    /// - `ChannelControl::set_mix_matrix`
265    ///
266    /// 3D functions include:
267    ///
268    /// - `ChannelControl::set3DAttributes`
269    /// - `ChannelControl::set3DConeOrientation`
270    /// - `ChannelControl::set3DCustomRolloff`
271    pub fn set_3d_level(&self, level: c_float) -> Result<()> {
272        unsafe { FMOD_ChannelControl_Set3DLevel(self.inner.as_ptr(), level).to_result() }
273    }
274
275    /// Retrieves the blend between 3D panning and 2D panning.
276    ///
277    /// The `FMOD_3D` flag must be set on this object otherwise `FMOD_ERR_NEEDS3D` is returned.
278    ///
279    /// 2D functions include:
280    ///
281    /// - `ChannelControl::set_pan`
282    /// - `ChannelControl::set_mix_levels_output`
283    /// - `ChannelControl::set_mix_levels_input`
284    /// - `ChannelControl::set_mix_matrix`
285    ///
286    /// 3D functions include:
287    ///
288    /// - `ChannelControl::set3DAttributes`
289    /// - `ChannelControl::set3DConeOrientation`
290    /// - `ChannelControl::set3DCustomRolloff`
291    pub fn get_3d_level(&self) -> Result<c_float> {
292        let mut level = 0.0;
293        unsafe { FMOD_ChannelControl_Get3DLevel(self.inner.as_ptr(), &raw mut level).to_result()? }
294        Ok(level)
295    }
296
297    /// Sets the minimum and maximum distances used to calculate the 3D roll-off attenuation.
298    ///
299    /// When the listener is within the minimum distance of the sound source the 3D volume will be at its maximum. As the listener moves from the minimum distance to the maximum distance the sound will attenuate following the roll-off curve set. When outside the maximum distance the sound will no longer attenuate.
300    ///
301    /// Attenuation in 3D space is controlled by the roll-off mode, these are `FMOD_3D_INVERSEROLLOFF`, `FMOD_3D_LINEARROLLOFF`, `FMOD_3D_LINEARSQUAREROLLOFF`, `FMOD_3D_INVERSETAPEREDROLLOFF`, `FMOD_3D_CUSTOMROLLOFF`.
302    ///
303    /// Minimum distance is useful to give the impression that the sound is loud or soft in 3D space.
304    /// A sound with a small 3D minimum distance in a typical (non custom) roll-off mode will make the sound appear small, and the sound will attenuate quickly.
305    /// A sound with a large minimum distance will make the sound appear larger.
306    ///
307    /// The `FMOD_3D` flag must be set on this object otherwise `FMOD_ERR_NEEDS3D` is returned.
308    ///
309    /// To define the min and max distance per Sound instead of Channel or `ChannelGroup` use `Sound::set3DMinMaxDistance`.
310    ///
311    /// If `FMOD_3D_CUSTOMROLLOFF` has been set on this object these values are stored, but ignored in 3D processing.
312    pub fn set_3d_min_max_distance(&self, min: c_float, max: c_float) -> Result<()> {
313        unsafe {
314            FMOD_ChannelControl_Set3DMinMaxDistance(self.inner.as_ptr(), min, max).to_result()
315        }
316    }
317
318    /// Retrieves the minimum and maximum distances used to calculate the 3D roll-off attenuation.
319    pub fn get_3d_min_max_distance(&self) -> Result<(c_float, c_float)> {
320        let mut min = 0.0;
321        let mut max = 0.0;
322        unsafe {
323            FMOD_ChannelControl_Get3DMinMaxDistance(
324                self.inner.as_ptr(),
325                &raw mut min,
326                &raw mut max,
327            )
328            .to_result()?;
329        }
330        Ok((min, max))
331    }
332
333    /// Sets the 3D attenuation factors for the direct and reverb paths.
334    ///
335    /// There is a reverb path/send when `ChannelControl::setReverbProperties` has been used, reverbocclusion controls its attenuation.
336    ///
337    /// If the System has been initialized with `FMOD_INIT_CHANNEL_DISTANCEFILTER` or
338    /// `FMOD_INIT_CHANNEL_LOWPASS` the directocclusion is applied as frequency filtering rather than volume attenuation.
339    pub fn set_3d_occlusion(&self, direct: c_float, reverb: c_float) -> Result<()> {
340        unsafe {
341            FMOD_ChannelControl_Set3DOcclusion(self.inner.as_ptr(), direct, reverb).to_result()
342        }
343    }
344
345    /// Retrieves the 3D attenuation factors for the direct and reverb paths.
346    pub fn get_3d_occlusion(&self) -> Result<(c_float, c_float)> {
347        let mut direct = 0.0;
348        let mut reverb = 0.0;
349        unsafe {
350            FMOD_ChannelControl_Get3DOcclusion(
351                self.inner.as_ptr(),
352                &raw mut direct,
353                &raw mut reverb,
354            )
355            .to_result()?;
356        }
357        Ok((direct, reverb))
358    }
359
360    /// Sets the spread of a 3D sound in speaker space.
361    ///
362    /// When the spread angle is 0 (default) a multi-channel signal will collapse to mono and be spatialized to a single point based on `ChannelControl::set3DAttributes` calculations.
363    /// As the angle is increased, each channel within a multi-channel signal will be rotated away from that point.
364    /// For 2, 4, 6, 8, and 12 channel signals, the spread is arranged from leftmost speaker to rightmost speaker intelligently,
365    /// for example in 5.1 the leftmost speaker is rear left, followed by front left, center, front right then finally rear right as the rightmost speaker (LFE is not spread).
366    /// For other channel counts the individual channels are spread evenly in the order of the signal.
367    /// As the signal is spread the power will be preserved.
368    ///
369    /// For a stereo signal given different spread angles:
370    /// - 0: Sound is collapsed to mono and spatialized to a single point.
371    /// - 90: Left channel is rotated 45 degrees to the left compared with angle=0 and the right channel 45 degrees to the right.
372    /// - 180: Left channel is rotated 90 degrees to the left compared with angle=0 and the right channel 90 degrees to the right.
373    /// - 360: Left channel is rotated 180 degrees to the left and the right channel 180 degrees to the right. This means the sound is collapsed to mono and spatialized to a single point in the opposite direction compared with (angle=0).
374    pub fn set_3d_spread(&self, angle: c_float) -> Result<()> {
375        unsafe { FMOD_ChannelControl_Set3DSpread(self.inner.as_ptr(), angle).to_result() }
376    }
377
378    /// Retrieves the spread of a 3D sound in speaker space.
379    pub fn get_3d_spread(&self) -> Result<c_float> {
380        let mut angle = 0.0;
381        unsafe { FMOD_ChannelControl_Get3DSpread(self.inner.as_ptr(), &raw mut angle).to_result()? }
382        Ok(angle)
383    }
384}