1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::dsp;
5
6pub use dsp::{FilterKind, OscKind};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
14#[serde(untagged)]
15pub enum FreqSpec {
16 Fixed(f32),
17 Sweep([f32; 2]),
18}
19
20fn default_q() -> f32 {
21 0.707
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
27#[serde(deny_unknown_fields)]
28pub struct FilterSpec {
29 #[serde(rename = "type")]
30 pub kind: FilterKind,
31 pub freq: f32,
32 #[serde(default = "default_q")]
33 pub q: f32,
34}
35
36fn default_attack() -> f32 {
37 0.002
38}
39
40fn default_gain() -> f32 {
41 1.0
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
52#[serde(deny_unknown_fields)]
53pub struct Voice {
54 #[serde(rename = "type")]
55 pub kind: OscKind,
56 #[serde(default)]
62 pub freq: Option<FreqSpec>,
63 #[serde(default)]
66 pub sweep: Option<f32>,
67 #[serde(default)]
68 pub filter: Option<FilterSpec>,
69 #[serde(default = "default_attack")]
70 pub attack: f32,
71 #[serde(default)]
72 pub decay: f32,
73 #[serde(default)]
74 pub sustain: f32,
75 #[serde(default)]
76 pub hold: f32,
77 #[serde(default)]
78 pub release: f32,
79 #[serde(default = "default_gain")]
80 pub gain: f32,
81 #[serde(default)]
87 pub seed: Option<u32>,
88}
89
90impl Voice {
91 fn envelope(&self) -> dsp::Adsr {
92 dsp::Adsr {
93 attack: self.attack.max(0.0),
94 decay: self.decay.max(0.0),
95 sustain: self.sustain.clamp(0.0, 1.0),
96 hold: self.hold.max(0.0),
97 release: self.release.max(0.0),
98 }
99 }
100
101 pub fn render_grain(&self, sample_rate: u32) -> Vec<f32> {
102 let envelope = self.envelope();
103 let sr = sample_rate as f32;
104 let total = envelope.total_duration().max(1.0 / sr);
105 let n = (total * sr).ceil() as usize;
106
107 let mut filter = self.filter.map(|f| {
108 (
109 dsp::BiquadCoeffs::design(f.kind, f.freq, f.q, sr),
110 dsp::BiquadState::default(),
111 )
112 });
113 let mut noise = dsp::Xorshift32::new(self.seed.unwrap_or(0x1234_5678));
114
115 let (f0, f1, sweep) = match self.freq {
116 Some(FreqSpec::Fixed(f)) => (f, f, 0.0),
117 Some(FreqSpec::Sweep([a, b])) => (a, b, self.sweep.unwrap_or(0.0).max(0.0)),
118 None => (440.0, 440.0, 0.0),
119 };
120
121 let mut phase = 0.0f32;
122 let mut out = Vec::with_capacity(n);
123 for i in 0..n {
124 let t = i as f32 / sr;
125 let freq = if sweep > 0.0 && t < sweep {
126 f0 + (f1 - f0) * (t / sweep)
127 } else {
128 f1
129 };
130
131 let mut sample = match self.kind {
132 OscKind::Sine => dsp::sine(phase),
133 OscKind::Square => dsp::square(phase),
134 OscKind::Saw => dsp::saw(phase),
135 OscKind::Triangle => dsp::triangle(phase),
136 OscKind::Noise => noise.next_f32(),
137 };
138 if self.kind != OscKind::Noise {
139 phase = (phase + freq / sr).rem_euclid(1.0);
140 }
141
142 if let Some((coeffs, state)) = filter.as_mut() {
143 sample = coeffs.process(state, sample);
144 }
145
146 out.push(sample * envelope.level_at(t) * self.gain);
147 }
148 out
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn kick() -> Voice {
157 Voice {
158 kind: OscKind::Sine,
159 freq: Some(FreqSpec::Sweep([150.0, 42.0])),
160 sweep: Some(0.14),
161 filter: None,
162 attack: default_attack(),
163 decay: 0.38,
164 sustain: 0.0,
165 hold: 0.0,
166 release: 0.0,
167 gain: default_gain(),
168 seed: None,
169 }
170 }
171
172 fn hat() -> Voice {
173 Voice {
174 kind: OscKind::Noise,
175 freq: None,
176 sweep: None,
177 filter: Some(FilterSpec {
178 kind: FilterKind::Highpass,
179 freq: 7500.0,
180 q: default_q(),
181 }),
182 attack: default_attack(),
183 decay: 0.05,
184 sustain: 0.0,
185 hold: 0.0,
186 release: 0.0,
187 gain: default_gain(),
188 seed: Some(7),
189 }
190 }
191
192 #[test]
193 fn kick_grain_starts_and_ends_at_silence_and_is_within_range() {
194 let grain = kick().render_grain(48_000);
195 assert!(!grain.is_empty());
196 assert!(
197 grain[0].abs() < 1e-3,
198 "grain must start near silence (attack ramp)"
199 );
200 assert!(
201 grain.last().unwrap().abs() < 1e-3,
202 "grain must decay to silence by the end of its envelope"
203 );
204 assert!(grain.iter().all(|s| s.abs() <= 1.0 + 1e-3));
205 }
206
207 #[test]
208 fn hat_grain_is_filtered_noise_not_silence() {
209 let grain = hat().render_grain(48_000);
210 assert!(!grain.is_empty());
211 assert!(
212 grain.iter().any(|&s| s.abs() > 0.01),
213 "a highpassed noise burst must not be silent"
214 );
215 }
216
217 #[test]
218 fn voice_deserializes_from_the_issues_json_shape() {
219 let json = serde_json::json!({
220 "type": "sine",
221 "freq": [150, 42],
222 "sweep": 0.14,
223 "decay": 0.38
224 });
225 let voice: Voice = serde_json::from_value(json).expect("kick voice parses");
226 assert_eq!(voice.kind, OscKind::Sine);
227 assert_eq!(voice.freq, Some(FreqSpec::Sweep([150.0, 42.0])));
228 assert_eq!(voice.decay, 0.38);
229 assert_eq!(voice.attack, default_attack());
230
231 let json = serde_json::json!({
232 "type": "noise",
233 "filter": {"type": "highpass", "freq": 7500},
234 "decay": 0.05
235 });
236 let voice: Voice = serde_json::from_value(json).expect("hat voice parses");
237 assert_eq!(voice.kind, OscKind::Noise);
238 assert_eq!(voice.filter.unwrap().freq, 7500.0);
239 }
240
241 #[test]
242 fn rendering_the_same_voice_twice_is_byte_identical() {
243 let a = hat().render_grain(48_000);
244 let b = hat().render_grain(48_000);
245 assert_eq!(a, b);
246 }
247}