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