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