1#[cfg(not(feature = "std"))]
6use num_traits::Float;
7
8use firewheel_core::{
9 channel_config::{ChannelConfig, ChannelCount},
10 diff::{Diff, Patch},
11 dsp::{
12 coeff_update::CoeffUpdateFactor,
13 distance_attenuation::{
14 DistanceAttenuation, DistanceAttenuatorStereoDsp, MUFFLE_CUTOFF_HZ_MAX,
15 },
16 fade::FadeCurve,
17 filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
18 volume::Volume,
19 },
20 event::ProcEvents,
21 mask::ConnectedMask,
22 node::{
23 AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
24 ProcBuffers, ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
25 },
26 param::smoother::{SmoothedParam, SmootherConfig},
27 vector::Vec3,
28};
29
30#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
33#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
34#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct SpatialBasicNode {
37 pub volume: Volume,
39
40 pub offset: Vec3,
51
52 pub panning_threshold: f32,
61
62 pub downmix: bool,
70
71 pub muffle_cutoff_hz: f32,
82
83 pub distance_attenuation: DistanceAttenuation,
86
87 pub smooth_seconds: f32,
91 pub min_gain: f32,
96 pub coeff_update_factor: CoeffUpdateFactor,
108}
109
110impl Default for SpatialBasicNode {
111 fn default() -> Self {
112 Self {
113 volume: Volume::default(),
114 offset: Vec3::new(0.0, 0.0, 0.0),
115 panning_threshold: 0.6,
116 downmix: true,
117 distance_attenuation: DistanceAttenuation::default(),
118 muffle_cutoff_hz: MUFFLE_CUTOFF_HZ_MAX,
119 smooth_seconds: DEFAULT_SMOOTH_SECONDS,
120 min_gain: 0.0001,
121 coeff_update_factor: CoeffUpdateFactor::default(),
122 }
123 }
124}
125
126impl SpatialBasicNode {
127 pub fn from_volume_offset(volume: Volume, offset: impl Into<Vec3>) -> Self {
128 Self {
129 volume,
130 offset: offset.into(),
131 ..Default::default()
132 }
133 }
134
135 pub const fn set_volume_linear(&mut self, linear: f32) {
141 self.volume = Volume::Linear(linear);
142 }
143
144 pub const fn set_volume_percent(&mut self, percent: f32) {
149 self.volume = Volume::from_percent(percent);
150 }
151
152 pub const fn set_volume_decibels(&mut self, decibels: f32) {
155 self.volume = Volume::Decibels(decibels);
156 }
157
158 fn compute_values(&self) -> ComputedValues {
159 let x2_z2 = (self.offset.x * self.offset.x) + (self.offset.z * self.offset.z);
160 let xz_distance = x2_z2.sqrt();
161 let distance = (x2_z2 + (self.offset.y * self.offset.y)).sqrt();
162
163 let pan = if xz_distance > 0.0 {
164 (self.offset.x / xz_distance) * self.panning_threshold.clamp(0.0, 1.0)
165 } else {
166 0.0
167 };
168 let (pan_gain_l, pan_gain_r) = FadeCurve::EqualPower3dB.compute_gains_neg1_to_1(pan);
169
170 let mut volume_gain = self.volume.amp();
171 if volume_gain > 0.99999 && volume_gain < 1.00001 {
172 volume_gain = 1.0;
173 }
174
175 let mut gain_l = pan_gain_l * volume_gain;
176 let mut gain_r = pan_gain_r * volume_gain;
177
178 if gain_l <= self.min_gain {
179 gain_l = 0.0;
180 }
181 if gain_r <= self.min_gain {
182 gain_r = 0.0;
183 }
184
185 ComputedValues {
186 distance,
187 gain_l,
188 gain_r,
189 }
190 }
191}
192
193struct ComputedValues {
194 distance: f32,
195 gain_l: f32,
196 gain_r: f32,
197}
198
199impl AudioNode for SpatialBasicNode {
200 type Configuration = EmptyConfig;
201
202 fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
203 AudioNodeInfo::new()
204 .debug_name("spatial_basic")
205 .channel_config(ChannelConfig {
206 num_inputs: ChannelCount::STEREO,
207 num_outputs: ChannelCount::STEREO,
208 })
209 }
210
211 fn construct_processor(
212 &self,
213 _config: &Self::Configuration,
214 cx: ConstructProcessorContext,
215 ) -> impl AudioNodeProcessor {
216 let computed_values = self.compute_values();
217
218 Processor {
219 gain_l: SmoothedParam::new(
220 computed_values.gain_l,
221 SmootherConfig {
222 smooth_seconds: self.smooth_seconds,
223 ..Default::default()
224 },
225 cx.stream_info.sample_rate,
226 ),
227 gain_r: SmoothedParam::new(
228 computed_values.gain_r,
229 SmootherConfig {
230 smooth_seconds: self.smooth_seconds,
231 ..Default::default()
232 },
233 cx.stream_info.sample_rate,
234 ),
235 distance_attenuator: DistanceAttenuatorStereoDsp::new(
236 SmootherConfig {
237 smooth_seconds: self.smooth_seconds,
238 ..Default::default()
239 },
240 cx.stream_info.sample_rate,
241 self.coeff_update_factor,
242 ),
243 params: *self,
244 }
245 }
246}
247
248struct Processor {
249 gain_l: SmoothedParam,
250 gain_r: SmoothedParam,
251
252 distance_attenuator: DistanceAttenuatorStereoDsp,
253
254 params: SpatialBasicNode,
255}
256
257impl AudioNodeProcessor for Processor {
258 fn process(
259 &mut self,
260 info: &ProcInfo,
261 buffers: ProcBuffers,
262 events: &mut ProcEvents,
263 extra: &mut ProcExtra,
264 ) -> ProcessStatus {
265 let mut updated = false;
266 for mut patch in events.drain_patches::<SpatialBasicNode>() {
267 match &mut patch {
268 SpatialBasicNodePatch::Offset(offset) => {
269 if !(offset.x.is_finite() && offset.y.is_finite() && offset.z.is_finite()) {
270 *offset = Vec3::default();
271 }
272 }
273 SpatialBasicNodePatch::PanningThreshold(threshold) => {
274 *threshold = threshold.clamp(0.0, 1.0);
275 }
276 SpatialBasicNodePatch::SmoothSeconds(seconds) => {
277 self.gain_l.set_smooth_seconds(*seconds, info.sample_rate);
278 self.gain_r.set_smooth_seconds(*seconds, info.sample_rate);
279 self.distance_attenuator
280 .set_smooth_seconds(*seconds, info.sample_rate);
281 }
282 SpatialBasicNodePatch::MinGain(g) => {
283 *g = g.clamp(0.0, 1.0);
284 }
285 SpatialBasicNodePatch::CoeffUpdateFactor(f) => {
286 self.distance_attenuator.set_coeff_update_factor(*f);
287 }
288 _ => {}
289 }
290
291 self.params.apply(patch);
292 updated = true;
293 }
294
295 if updated {
296 let computed_values = self.params.compute_values();
297
298 self.gain_l.set_value(computed_values.gain_l);
299 self.gain_r.set_value(computed_values.gain_r);
300
301 self.distance_attenuator.compute_values(
302 computed_values.distance,
303 &self.params.distance_attenuation,
304 self.params.muffle_cutoff_hz,
305 self.params.min_gain,
306 );
307
308 if info.prev_output_was_silent {
309 self.gain_l.reset_to_target();
311 self.gain_r.reset_to_target();
312 self.distance_attenuator.reset();
313 }
314 }
315
316 if info.in_silence_mask.all_channels_silent(2) {
317 self.gain_l.reset_to_target();
318 self.gain_r.reset_to_target();
319 self.distance_attenuator.reset();
320
321 return ProcessStatus::ClearAllOutputs;
322 }
323
324 let scratch_buffer = extra.scratch_buffers.first_mut();
325
326 let (in1, in2) = if info.in_connected_mask == ConnectedMask::STEREO_CONNECTED {
327 if self.params.downmix {
328 for (scratch_s, (&in1, &in2)) in scratch_buffer[..info.frames].iter_mut().zip(
330 buffers.inputs[0][..info.frames]
331 .iter()
332 .zip(buffers.inputs[1][..info.frames].iter()),
333 ) {
334 *scratch_s = (in1 + in2) * 0.5;
335 }
336
337 (
338 &scratch_buffer[..info.frames],
339 &scratch_buffer[..info.frames],
340 )
341 } else {
342 (
343 &buffers.inputs[0][..info.frames],
344 &buffers.inputs[1][..info.frames],
345 )
346 }
347 } else {
348 (
351 &buffers.inputs[0][..info.frames],
352 &buffers.inputs[0][..info.frames],
353 )
354 };
355
356 let in1 = &in1[..info.frames];
359 let in2 = &in2[..info.frames];
360
361 let (out1, out2) = buffers.outputs.split_first_mut().unwrap();
362 let out1 = &mut out1[..info.frames];
363 let out2 = &mut out2[0][..info.frames];
364
365 if self.gain_l.has_settled() && self.gain_r.has_settled() {
366 if self.gain_l.target_value() <= self.params.min_gain
367 && self.gain_r.target_value() <= self.params.min_gain
368 && self.distance_attenuator.is_silent()
369 {
370 self.gain_l.reset_to_target();
371 self.gain_r.reset_to_target();
372 self.distance_attenuator.reset();
373
374 return ProcessStatus::ClearAllOutputs;
375 } else {
376 for i in 0..info.frames {
377 out1[i] = in1[i] * self.gain_l.target_value();
378 out2[i] = in2[i] * self.gain_r.target_value();
379 }
380 }
381 } else {
382 for i in 0..info.frames {
383 let gain_l = self.gain_l.next_smoothed();
384 let gain_r = self.gain_r.next_smoothed();
385
386 out1[i] = in1[i] * gain_l;
387 out2[i] = in2[i] * gain_r;
388 }
389
390 self.gain_l.settle();
391 self.gain_r.settle();
392 }
393
394 let clear_outputs =
395 self.distance_attenuator
396 .process(info.frames, out1, out2, info.sample_rate_recip);
397
398 if clear_outputs {
399 self.gain_l.reset_to_target();
400 self.gain_r.reset_to_target();
401 self.distance_attenuator.reset();
402
403 return ProcessStatus::ClearAllOutputs;
404 } else {
405 ProcessStatus::OutputsModified
406 }
407 }
408
409 fn new_stream(
410 &mut self,
411 stream_info: &firewheel_core::StreamInfo,
412 _context: &mut ProcStreamCtx,
413 ) {
414 self.gain_l.update_sample_rate(stream_info.sample_rate);
415 self.gain_r.update_sample_rate(stream_info.sample_rate);
416 self.distance_attenuator
417 .update_sample_rate(stream_info.sample_rate);
418 }
419}