1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! FunDSP interoperability adapter for `AudioEffect` implementations.
//!
//! Enable with the `fundsp` feature gate:
//! ```toml
//! oximedia-effects = { features = ["fundsp"] }
//! ```
//!
//! # Overview
//!
//! [`FunDspAdapter`] wraps any [`AudioEffect`] as a FunDSP stereo [`AudioNode`]
//! (2-in / 2-out). This lets `AudioEffect` implementations participate in
//! FunDSP signal graphs without any manual FFI or intermediate conversion.
//!
//! # Node Identity
//!
//! FunDSP's `AudioNode::ID` must be a `const u64`. Because `const` associated
//! items cannot depend on a runtime generic type parameter, we set `ID = 0` in
//! the trait impl and provide [`FunDspAdapter::effect_node_id`] as the correct
//! per-type identifier (an FNV-1a hash of `E::EFFECT_ID`). Callers that
//! construct FunDSP graphs programmatically should use `effect_node_id()` when
//! they need a stable, non-zero node tag.
/// FNV-1a 64-bit hash of a string, evaluated at compile time.
///
/// Produces a stable `u64` identifier from a string slice. Suitable for
/// deriving FunDSP node IDs from `EFFECT_ID` slugs.
///
/// # Example
/// ```
/// use oximedia_effects::fundsp_adapter::fnv1a_u64;
/// let id = fnv1a_u64("freeverb");
/// assert_ne!(id, 0);
/// ```
pub const fn fnv1a_u64(s: &str) -> u64 {
const FNV_PRIME: u64 = 1_099_511_628_211;
const OFFSET: u64 = 14_695_981_039_346_656_037;
let bytes = s.as_bytes();
let mut hash = OFFSET;
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
hash
}
#[cfg(feature = "fundsp")]
mod adapter_impl {
use super::fnv1a_u64;
use crate::AudioEffect;
use fundsp::prelude::*;
/// Wraps any `AudioEffect + Clone + Send + Sync` as a FunDSP stereo `AudioNode`.
///
/// The node has 2 inputs (`U2`) and 2 outputs (`U2`), corresponding to the
/// stereo left/right channel pair.
///
/// # Cloning + thread-safety requirement
///
/// FunDSP requires all `AudioNode` implementations to be `Clone + Send + Sync`
/// so that signal graphs can be duplicated and shared across threads.
/// Derive or implement those traits on your effect struct before wrapping.
///
/// # Example
///
/// ```ignore
/// use oximedia_effects::{AudioEffect, reverb::Freeverb, ReverbConfig};
/// use oximedia_effects::fundsp_adapter::FunDspAdapter;
/// use fundsp::prelude::*;
///
/// let reverb = Freeverb::new(ReverbConfig::default(), 44100.0);
/// let mut node = FunDspAdapter::new(reverb);
/// node.set_sample_rate(44100.0);
///
/// let frame: Frame<f32, U2> = [0.5f32, 0.5].into();
/// let out = node.tick(&frame);
/// assert!(out[0].is_finite() && out[1].is_finite());
/// ```
pub struct FunDspAdapter<E>
where
E: AudioEffect + Clone + Send + Sync,
{
inner: E,
sample_rate: f64,
}
impl<E> FunDspAdapter<E>
where
E: AudioEffect + Clone + Send + Sync,
{
/// Wrap an `AudioEffect` as a FunDSP stereo node.
pub fn new(effect: E) -> Self {
Self {
inner: effect,
sample_rate: 44100.0,
}
}
/// Immutable reference to the wrapped effect.
pub fn inner(&self) -> &E {
&self.inner
}
/// Mutable reference to the wrapped effect.
pub fn inner_mut(&mut self) -> &mut E {
&mut self.inner
}
/// Consume the adapter, returning the inner effect.
pub fn into_inner(self) -> E {
self.inner
}
/// Returns the stable FNV-1a hash of this effect's `EFFECT_ID`.
///
/// Use this as the node tag when constructing FunDSP signal graphs that
/// require unique per-type identifiers.
///
/// # Example
/// ```ignore
/// let id = adapter.effect_node_id();
/// assert_eq!(id, oximedia_effects::fundsp_adapter::fnv1a_u64(E::EFFECT_ID));
/// ```
pub fn effect_node_id(&self) -> u64 {
fnv1a_u64(E::EFFECT_ID)
}
}
impl<E> Clone for FunDspAdapter<E>
where
E: AudioEffect + Clone + Send + Sync,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
sample_rate: self.sample_rate,
}
}
}
impl<E> AudioNode for FunDspAdapter<E>
where
E: AudioEffect + Clone + Send + Sync + 'static,
{
/// Const node ID required by FunDSP.
///
/// Set to `0` because const associated items cannot depend on generic
/// type parameters. Use [`FunDspAdapter::effect_node_id`] for the
/// correct per-type FNV-1a hash ID.
const ID: u64 = 0;
/// Stereo input arity (L + R).
type Inputs = U2;
/// Stereo output arity (L + R).
type Outputs = U2;
fn tick(&mut self, input: &Frame<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
let (l, r) = self.inner.process_sample_stereo(input[0], input[1]);
[l, r].into()
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
#[allow(clippy::cast_possible_truncation)]
self.inner.set_sample_rate(sample_rate as f32);
}
fn reset(&mut self) {
self.inner.reset();
}
}
}
#[cfg(feature = "fundsp")]
pub use adapter_impl::FunDspAdapter;
#[cfg(test)]
mod tests {
use super::fnv1a_u64;
#[test]
fn test_fnv1a_u64_non_zero() {
assert_ne!(fnv1a_u64("freeverb"), 0);
assert_ne!(fnv1a_u64("plate_reverb"), 0);
}
#[test]
fn test_fnv1a_u64_distinct() {
let a = fnv1a_u64("freeverb");
let b = fnv1a_u64("plate_reverb");
let c = fnv1a_u64("spring_reverb");
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(a, c);
}
#[test]
fn test_fnv1a_u64_stable() {
// Same input always produces the same output.
assert_eq!(fnv1a_u64("analog_delay"), fnv1a_u64("analog_delay"));
}
#[test]
fn test_fnv1a_u64_empty() {
// The empty string produces the FNV offset basis, which is non-zero.
let empty_hash = fnv1a_u64("");
assert_eq!(empty_hash, 14_695_981_039_346_656_037u64);
}
}