phonon_fmod/
lib.rs

1//
2// Copyright 2017-2023 Valve Corporation.
3// Copyright 2024 phonon_rs contributors.
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17
18//! FMOD Plugin for the phonon crate.
19
20pub(crate) mod callbacks;
21mod fmod_state;
22mod parameter_init;
23pub mod parameter_spec;
24
25use crate::callbacks::{
26    create_callback, get_data_callback, get_float_callback, get_int_callback, process_callback,
27    release_callback, set_data_callback, set_float_callback, set_int_callback,
28    sys_deregister_callback, sys_register_callback,
29};
30use crate::parameter_init::init_parameters;
31use glam::Vec3;
32use libfmod::ffi::{
33    FMOD_DSP_DESCRIPTION, FMOD_DSP_PAN_3D_ROLLOFF_TYPE, FMOD_DSP_PARAMETER_3DATTRIBUTES,
34    FMOD_DSP_PARAMETER_ATTENUATION_RANGE, FMOD_DSP_PARAMETER_OVERALLGAIN, FMOD_PLUGIN_SDK_VERSION,
35};
36use libfmod::DspDescription;
37use phonon::dsp::audio_buffer::{AudioBuffer, AudioSettings};
38use phonon::effects::direct::{
39    DirectApplyFlags, DirectEffect, DirectEffectParameters, TransmissionType,
40};
41use phonon::effects::panning::{PanningEffect, PanningEffectParameters};
42use phonon::simulators::direct::DirectSoundPath;
43use std::ffi::CString;
44use std::os::raw::{c_char, c_int};
45use std::ptr::null_mut;
46
47#[derive(Copy, Clone)]
48enum ParameterApplyType {
49    Disable,
50    SimulationDefined,
51    UserDefined,
52}
53
54impl From<c_int> for ParameterApplyType {
55    fn from(value: c_int) -> Self {
56        match value {
57            0 => ParameterApplyType::Disable,
58            1 => ParameterApplyType::SimulationDefined,
59            2 => ParameterApplyType::UserDefined,
60            _ => ParameterApplyType::Disable,
61        }
62    }
63}
64
65impl Into<c_int> for ParameterApplyType {
66    fn into(self) -> c_int {
67        match self {
68            ParameterApplyType::Disable => 0,
69            ParameterApplyType::SimulationDefined => 1,
70            ParameterApplyType::UserDefined => 2,
71        }
72    }
73}
74
75#[expect(dead_code, reason = "Not everything is implemented yet")]
76pub(crate) struct EffectState {
77    source: FMOD_DSP_PARAMETER_3DATTRIBUTES,
78    overall_gain: FMOD_DSP_PARAMETER_OVERALLGAIN,
79
80    apply_distance_attenuation: ParameterApplyType,
81    apply_air_absorption: ParameterApplyType,
82    apply_directivity: ParameterApplyType,
83    apply_occlusion: ParameterApplyType,
84    apply_transmission: ParameterApplyType,
85
86    distance_attenuation: f32,
87    distance_attenuation_rolloff_type: FMOD_DSP_PAN_3D_ROLLOFF_TYPE,
88    distance_attenuation_min_distance: f32,
89    distance_attenuation_max_distance: f32,
90
91    // todo: I added this one. Consider another one for user settings and then remove all the individual params below.
92    direct_sound_path: DirectSoundPath,
93
94    air_absorption: [f32; 3],
95    directivity: f32,
96    dipole_weight: f32, // See Directivity docs
97    dipole_power: f32,  // See Directivity docs
98    occlusion: f32,
99    transmission_type: TransmissionType,
100    transmission: [f32; 3],
101
102    attenuation_range: FMOD_DSP_PARAMETER_ATTENUATION_RANGE,
103    attenuation_range_set: bool, // todo: Original is atomic
104
105    audio_settings: AudioSettings,
106
107    in_buffer_stereo: AudioBuffer<2>,
108    in_buffer_mono: AudioBuffer<1>,
109    out_buffer: AudioBuffer<2>,
110    direct_buffer: AudioBuffer<1>,
111    mono_buffer: AudioBuffer<1>,
112
113    panning_effect: PanningEffect,
114    direct_effect: DirectEffect,
115}
116
117fn apply_flag(
118    apply: ParameterApplyType,
119    target_flag: DirectApplyFlags,
120    current_flags: &mut DirectApplyFlags,
121) {
122    match apply {
123        ParameterApplyType::Disable => current_flags.set(target_flag, false),
124        ParameterApplyType::SimulationDefined => current_flags.set(target_flag, true),
125        ParameterApplyType::UserDefined => current_flags.set(target_flag, true), //todo
126    }
127}
128
129impl EffectState {
130    fn create_flags(&mut self) -> DirectApplyFlags {
131        let mut flags = DirectApplyFlags::empty();
132
133        apply_flag(
134            self.apply_distance_attenuation,
135            DirectApplyFlags::DistanceAttenuation,
136            &mut flags,
137        );
138
139        apply_flag(
140            self.apply_air_absorption,
141            DirectApplyFlags::AirAbsorption,
142            &mut flags,
143        );
144
145        apply_flag(
146            self.apply_directivity,
147            DirectApplyFlags::Directivity,
148            &mut flags,
149        );
150
151        apply_flag(
152            self.apply_occlusion,
153            DirectApplyFlags::Occlusion,
154            &mut flags,
155        );
156
157        flags
158    }
159
160    fn process(
161        &mut self,
162        in_buffer: &[f32],
163        out_buffer: &mut [f32],
164        length: usize,
165        channels: usize,
166    ) {
167        let _num_samples = length * channels;
168
169        // update parameters
170        let position = self.source.relative.position;
171        let direction = Vec3::new(position.x, position.y, position.z);
172        let panning_params = PanningEffectParameters { direction };
173
174        let direct_params = DirectEffectParameters {
175            direct_sound_path: self.direct_sound_path,
176            flags: self.create_flags(),
177            transmission_type: TransmissionType::FrequencyDependent,
178        };
179
180        // do the actual processing
181        self.in_buffer_stereo.read_interleaved(in_buffer);
182        self.in_buffer_stereo.downmix(&mut self.in_buffer_mono);
183
184        self.direct_effect
185            .apply(direct_params, &self.in_buffer_mono, &mut self.direct_buffer);
186
187        self.panning_effect
188            .apply(panning_params, &self.direct_buffer, &mut self.out_buffer);
189
190        self.out_buffer.write_interleaved(out_buffer);
191    }
192}
193
194pub fn create_dsp_description() -> DspDescription {
195    DspDescription {
196        pluginsdkversion: FMOD_PLUGIN_SDK_VERSION,
197        name: str_to_c_char_array("Phonon Spatializer"),
198        version: 1,
199        numinputbuffers: 1,
200        numoutputbuffers: 1,
201        create: Some(create_callback),
202        release: Some(release_callback),
203        reset: None,
204        read: None,
205        process: Some(process_callback),
206        setposition: None,
207        paramdesc: init_parameters(),
208        setparameterfloat: Some(set_float_callback),
209        setparameterint: Some(set_int_callback),
210        setparameterbool: None, //todo
211        setparameterdata: Some(set_data_callback),
212        getparameterfloat: Some(get_float_callback),
213        getparameterint: Some(get_int_callback),
214        getparameterbool: None, // todo
215        getparameterdata: Some(get_data_callback),
216        shouldiprocess: None,
217        userdata: null_mut(),
218        sys_register: Some(sys_register_callback),
219        sys_deregister: Some(sys_deregister_callback),
220        sys_mix: None,
221    }
222}
223
224/// FMOD will call this function load the plugin defined by FMOD_DSP_DESCRIPTION.
225/// See https://fmod.com/docs/2.02/api/white-papers-dsp-plugin-api.html#building-a-plug-in
226#[no_mangle]
227extern "C" fn FMODGetDSPDescription() -> *mut FMOD_DSP_DESCRIPTION {
228    let description: FMOD_DSP_DESCRIPTION = create_dsp_description().into();
229    let boxed = Box::new(description);
230    Box::into_raw(boxed)
231}
232
233fn str_to_c_char_array<const LEN: usize>(input: &str) -> [c_char; LEN] {
234    let mut array: [c_char; LEN] = [0; LEN];
235
236    // Convert the input &str to a CString, adding a null terminator
237    let c_string = CString::new(input).expect("CString::new failed");
238
239    // Get the byte slice of the CString
240    let bytes = c_string.as_bytes();
241
242    // Ensure the byte slice fits within the array
243    if bytes.len() > LEN {
244        panic!("String is too long to fit in [c_char; LEN]");
245    }
246
247    // Copy the bytes into the array
248    for (i, &byte) in bytes.iter().enumerate() {
249        array[i] = byte as c_char;
250    }
251
252    array
253}