1use super::wave::{Aliasing, Oscillator, Waveform};
3use crate::math::{exp, exp2, sin_turns};
4use crate::{Audio, Error, Limits, Result};
5
6#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
8pub enum DrumKit {
9 Chiptune,
10 Electronic,
11}
12
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
14pub enum DrumKind {
15 Kick,
16 Snare,
17 ClosedHat,
18 OpenHat,
19 Tom,
20 Clap,
21 Crash,
22}
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
30pub struct Drum {
31 pub kind: DrumKind,
32 pub kit: DrumKit,
33 pub pitch_hz: u16,
35 pub tone_ms: u32,
37 pub noise_ms: u32,
39 pub sweep_cents: u16,
41 pub sweep_ms: u32,
43 pub noise_percent: u8,
45 pub click_percent: u8,
47 pub seed: u32,
49}
50impl Drum {
51 pub fn new(kind: DrumKind, kit: DrumKit) -> Self {
52 use DrumKind::*;
53 let (pitch, tone, noise, sweep, sweep_ms, blend, click) = match kind {
54 Kick => (55, 400, 0, 2400, 35, 0, 25),
55 Snare => (180, 100, 220, 0, 0, 70, 10),
56 ClosedHat => (3400, 45, 60, 0, 0, 80, 0),
57 OpenHat => (3400, 350, 450, 0, 0, 80, 0),
58 Tom => (130, 420, 80, 1200, 50, 15, 10),
59 Clap => (1800, 0, 180, 0, 0, 100, 0),
60 Crash => (2300, 1200, 1800, 0, 0, 65, 0),
61 };
62 let mut drum = Self {
63 kind,
64 kit,
65 pitch_hz: pitch,
66 tone_ms: tone,
67 noise_ms: noise,
68 sweep_cents: sweep,
69 sweep_ms,
70 noise_percent: blend,
71 click_percent: click,
72 seed: 1,
73 };
74 if kit == DrumKit::Chiptune {
75 drum.tone_ms = tone * 3 / 4;
76 drum.noise_ms = noise * 3 / 4;
77 drum.click_percent = click / 2;
78 if matches!(kind, ClosedHat | OpenHat | Crash) {
79 drum.noise_percent = 100;
80 }
81 }
82 drum
83 }
84 pub fn validate(&self) -> Result<()> {
85 if !(20..=20_000).contains(&self.pitch_hz)
86 || self.tone_ms > 10_000
87 || self.noise_ms > 10_000
88 || self.sweep_ms > 1000
89 || self.sweep_cents > 4800
90 || self.noise_percent > 100
91 || self.click_percent > 100
92 {
93 return Err(Error::invalid(
94 "Drum pitch, envelope, sweep or blend is outside its documented range",
95 ));
96 }
97 Ok(())
98 }
99 pub fn duration_ms(&self) -> u32 {
101 let tone = if self.noise_percent < 100 && self.kind != DrumKind::Clap {
102 self.tone_ms
103 } else {
104 0
105 };
106 let noise = if self.noise_percent > 0 && self.noise_ms > 0 {
107 self.noise_ms
108 .saturating_add(if self.kind == DrumKind::Clap { 30 } else { 0 })
109 } else {
110 0
111 };
112 tone.max(noise)
113 .max(if self.click_percent > 0 { 3 } else { 0 })
114 }
115 pub fn render(&self, sample_rate: u32, velocity: u8) -> Result<Audio> {
118 self.validate()?;
119 if velocity > 127 {
120 return Err(Error::invalid("Velocity must be 0..=127"));
121 }
122 Limits::default().check(0, 1, sample_rate)?;
123 let plan = DrumPlan::new(*self, sample_rate);
124 let mut voice = DrumVoice::new(Aliasing::BandLimited);
125 voice.trigger(&plan, 1.0);
126 let mut samples = vec![0.0; plan.frames as usize];
127 for sample in &mut samples {
128 *sample = (voice.sample(&plan) * (velocity as f64 / 127.0)) as f32;
129 }
130 Audio::new(vec![samples], sample_rate)
131 }
132}
133
134#[derive(Clone, Debug)]
135pub(super) struct DrumPlan {
136 settings: Drum,
137 pub(super) frames: u64,
138 rate: u32,
139 tone: u64,
140 noise: u64,
141 sweep: u64,
142 attack: u64,
143 click: u64,
144 burst: u64,
145 offsets: [u64; 4],
146 low: f64,
147 high: f64,
148}
149impl DrumPlan {
150 pub(super) fn new(settings: Drum, rate: u32) -> Self {
151 let samples = |ms: u32| (ms as u64 * rate as u64 + 500) / 1000;
152 let (low, high) = match settings.kind {
153 DrumKind::Kick | DrumKind::Tom => (8000.0, 1500.0),
154 DrumKind::Snare => (8000.0, 700.0),
155 DrumKind::Clap => (6500.0, 1000.0),
156 _ => (14_000.0, 5000.0),
157 };
158 let alpha =
159 |hz: f64| exp(-std::f64::consts::TAU * hz.min(rate as f64 * 0.45) / rate as f64);
160 let tone = samples(settings.tone_ms);
161 let noise = samples(settings.noise_ms);
162 let click = samples(3).max(1);
163 let offsets = [0, samples(10), samples(20), samples(30)];
164 let tone_end = if settings.noise_percent < 100 && settings.kind != DrumKind::Clap {
165 tone
166 } else {
167 0
168 };
169 let noise_end = if settings.noise_percent > 0 && noise > 0 {
170 noise
171 + if settings.kind == DrumKind::Clap {
172 offsets[3]
173 } else {
174 0
175 }
176 } else {
177 0
178 };
179 Self {
180 frames: tone_end
181 .max(noise_end)
182 .max(if settings.click_percent > 0 { click } else { 0 }),
183 rate,
184 tone,
185 noise,
186 sweep: samples(settings.sweep_ms),
187 attack: samples(1).max(1),
188 click,
189 burst: samples(8).max(1),
190 offsets,
191 low: alpha(low),
192 high: alpha(high),
193 settings,
194 }
195 }
196}
197
198#[derive(Clone, Debug)]
199pub(super) struct DrumVoice {
200 age: u64,
201 frames: u64,
202 aliasing: Aliasing,
203 oscillator: Oscillator,
204 phases: [f64; 6],
205 random: u32,
206 noise_phase: f64,
207 low: f64,
208 high: f64,
209 transpose: f64,
210}
211impl DrumVoice {
212 pub(super) fn new(aliasing: Aliasing) -> Self {
213 Self {
214 age: 0,
215 frames: 0,
216 aliasing,
217 oscillator: Oscillator::new(aliasing),
218 phases: [0.0; 6],
219 random: 1,
220 noise_phase: 0.0,
221 low: 0.0,
222 high: 0.0,
223 transpose: 1.0,
224 }
225 }
226 pub(super) fn trigger(&mut self, plan: &DrumPlan, transpose: f64) {
227 *self = Self {
228 frames: plan.frames,
229 random: if plan.settings.kit == DrumKit::Chiptune {
230 (plan.settings.seed & 0x7fff).max(1)
231 } else {
232 plan.settings.seed.max(1)
233 },
234 transpose,
235 ..Self::new(self.aliasing)
236 };
237 }
238 pub(super) fn active(&self) -> bool {
239 self.age < self.frames
240 }
241 pub(super) fn sample(&mut self, plan: &DrumPlan) -> f64 {
242 if !self.active() {
243 return 0.0;
244 }
245 let d = plan.settings;
246 let elapsed = self.age;
247 self.age += 1;
248 let sweep = if plan.sweep == 0 {
249 0.0
250 } else {
251 (1.0 - elapsed as f64 / plan.sweep as f64).max(0.0)
252 };
253 let hz = (d.pitch_hz as f64 * self.transpose * exp2(d.sweep_cents as f64 * sweep / 1200.0))
254 .min(plan.rate as f64 * 0.45);
255 let raw_noise = if d.kit == DrumKit::Chiptune {
256 let value = if self.random & 1 == 0 { 1.0 } else { -1.0 };
257 self.noise_phase += (d.pitch_hz as f64 * 8.0 / plan.rate as f64).min(1.0);
258 if self.noise_phase >= 1.0 {
259 self.noise_phase -= 1.0;
260 let tap = if matches!(
261 d.kind,
262 DrumKind::ClosedHat | DrumKind::OpenHat | DrumKind::Crash
263 ) {
264 6
265 } else {
266 1
267 };
268 let bit = (self.random ^ (self.random >> tap)) & 1;
269 self.random = (self.random >> 1) | (bit << 14);
270 }
271 value
272 } else {
273 self.random ^= self.random << 13;
274 self.random ^= self.random >> 17;
275 self.random ^= self.random << 5;
276 (self.random >> 8) as f64 / (1u32 << 23) as f64 - 1.0
277 };
278 self.low = raw_noise * (1.0 - plan.low) + self.low * plan.low;
279 self.high = self.low * (1.0 - plan.high) + self.high * plan.high;
280 let noise = self.low - self.high;
281 let tone = match d.kind {
282 DrumKind::Clap => 0.0,
283 DrumKind::ClosedHat | DrumKind::OpenHat | DrumKind::Crash => {
284 let mut sum = 0.0;
285 for (phase, ratio) in self
286 .phases
287 .iter_mut()
288 .zip([1.0, 1.342, 1.789, 2.129, 2.731, 3.113])
289 {
290 sum += if *phase < 0.5 { 1.0 } else { -1.0 };
291 advance(
292 phase,
293 (hz * ratio).min(plan.rate as f64 * 0.45) / plan.rate as f64,
294 );
295 }
296 sum / 6.0
297 }
298 _ if d.kit == DrumKit::Chiptune => {
299 self.oscillator.next(Waveform::Triangle, hz, plan.rate)
300 }
301 _ => {
302 let fundamental = sin_turns(self.phases[0]);
303 advance(&mut self.phases[0], hz / plan.rate as f64);
304 if d.kind == DrumKind::Snare {
305 let overtone = sin_turns(self.phases[1]);
306 advance(
307 &mut self.phases[1],
308 (hz * 1.5).min(plan.rate as f64 * 0.45) / plan.rate as f64,
309 );
310 (fundamental + overtone * 0.35) / 1.35
311 } else {
312 fundamental
313 }
314 }
315 };
316 let tone_env = envelope(elapsed, plan.tone, plan.attack);
317 let noise_env = if d.kind == DrumKind::Clap && plan.noise > 0 {
318 let mut bursts = 0.0;
319 for &offset in &plan.offsets[..3] {
320 if let Some(age) = elapsed.checked_sub(offset) {
321 bursts += envelope(age, plan.burst, 1);
322 }
323 }
324 if let Some(age) = elapsed.checked_sub(plan.offsets[3]) {
325 bursts += envelope(age, plan.noise, plan.attack);
326 }
327 bursts
328 } else {
329 envelope(elapsed, plan.noise, plan.attack)
330 };
331 let click_env = envelope(elapsed, plan.click, 1);
332 let blend = d.noise_percent as f64 / 100.0;
333 tone * tone_env * (1.0 - blend)
334 + noise * noise_env * blend
335 + noise * click_env * d.click_percent as f64 / 100.0
336 }
337}
338fn envelope(age: u64, length: u64, attack: u64) -> f64 {
339 if age >= length {
340 return 0.0;
341 }
342 let t = 1.0 - age as f64 / length as f64;
343 t * t * (age as f64 / attack as f64).min(1.0)
344}
345fn advance(phase: &mut f64, step: f64) {
346 *phase += step;
347 *phase -= phase.floor();
348}