1use crate::effects::PulseEffect;
4use crate::error::{PulseError, PulseResult};
5use tunes::composition::Composition;
6use tunes::instruments::drums::DrumType;
7
8#[derive(Clone, Copy)]
9struct DrumPreset {
10 name: &'static str,
11 drum_type: DrumType,
12}
13
14#[derive(Clone, Copy)]
15struct LayeredDrumPreset {
16 name: &'static str,
17 drum_types: &'static [DrumType],
18}
19
20macro_rules! drum_presets {
21 ($(($name:literal, $variant:ident)),+ $(,)?) => {
22 const DRUM_PRESETS: &[DrumPreset] = &[
23 $(DrumPreset {
24 name: $name,
25 drum_type: DrumType::$variant,
26 },)+
27 ];
28
29 const DRUM_NAMES: &[&str] = &[
30 $($name,)+
31 ];
32 };
33}
34
35drum_presets!(
36 ("kick", Kick),
37 ("kick_808", Kick808),
38 ("sub_kick", SubKick),
39 ("snare", Snare),
40 ("snare_808", Snare808),
41 ("hihat_closed", HiHatClosed),
42 ("hihat_open", HiHatOpen),
43 ("hihat_808_closed", HiHat808Closed),
44 ("hihat_808_open", HiHat808Open),
45 ("clap", Clap),
46 ("clap_808", Clap808),
47 ("tom", Tom),
48 ("tom_high", TomHigh),
49 ("tom_low", TomLow),
50 ("rimshot", Rimshot),
51 ("cowbell", Cowbell),
52 ("crash", Crash),
53 ("ride", Ride),
54 ("china", China),
55 ("splash", Splash),
56 ("tambourine", Tambourine),
57 ("shaker", Shaker),
58 ("bass_drop", BassDrop),
59 ("boom", Boom),
60 ("claves", Claves),
61 ("triangle", Triangle),
62 ("side_stick", SideStick),
63 ("wood_block", WoodBlock),
64 ("kick_909", Kick909),
65 ("snare_909", Snare909),
66 ("conga_high", CongaHigh),
67 ("conga_low", CongaLow),
68 ("bongo_high", BongoHigh),
69 ("bongo_low", BongoLow),
70 ("ride_bell", RideBell),
71 ("floor_tom_low", FloorTomLow),
72 ("floor_tom_high", FloorTomHigh),
73 ("hihat_pedal", HiHatPedal),
74 ("crash_2", Crash2),
75 ("vibraslap", Vibraslap),
76 ("timbale_high", TimbaleHigh),
77 ("timbale_low", TimbaleLow),
78 ("agogo_high", AgogoHigh),
79 ("agogo_low", AgogoLow),
80 ("cabasa", Cabasa),
81 ("guiro_short", GuiroShort),
82 ("guiro_long", GuiroLong),
83 ("wood_block_high", WoodBlockHigh),
84 ("timpani", Timpani),
85 ("gong", Gong),
86 ("chimes", Chimes),
87 ("djembe", Djembe),
88 ("tabla_bayan", TablaBayan),
89 ("tabla_dayan", TablaDayan),
90 ("cajon", Cajon),
91 ("fingersnap", Fingersnap),
92 ("maracas", Maracas),
93 ("castanet", Castanet),
94 ("sleigh_bells", SleighBells),
95 ("laser_zap", LaserZap),
96 ("reverse_cymbal", ReverseCymbal),
97 ("white_noise_hit", WhiteNoiseHit),
98 ("stick_click", StickClick),
99 ("kick_tight", KickTight),
100 ("kick_deep", KickDeep),
101 ("kick_acoustic", KickAcoustic),
102 ("kick_click", KickClick),
103 ("snare_rim", SnareRim),
104 ("snare_tight", SnareTight),
105 ("snare_loose", SnareLoose),
106 ("snare_piccolo", SnarePiccolo),
107 ("hihat_half_open", HiHatHalfOpen),
108 ("hihat_sizzle", HiHatSizzle),
109 ("clap_dry", ClapDry),
110 ("clap_room", ClapRoom),
111 ("clap_group", ClapGroup),
112 ("clap_snare", ClapSnare),
113 ("crash_short", CrashShort),
114 ("ride_tip", RideTip),
115 ("egg_shaker", EggShaker),
116 ("tube_shaker", TubeShaker),
117 ("tom_808_low", Tom808Low),
118 ("tom_808_mid", Tom808Mid),
119 ("tom_808_high", Tom808High),
120 ("cowbell_808", Cowbell808),
121 ("clave_808", Clave808),
122 ("hihat_909_closed", HiHat909Closed),
123 ("hihat_909_open", HiHat909Open),
124 ("clap_909", Clap909),
125 ("cowbell_909", Cowbell909),
126 ("rim_909", Rim909),
127 ("reverse_snare", ReverseSnare),
128 ("cymbal_swell", CymbalSwell),
129);
130
131const LAYERED_DRUM_PRESETS: &[LayeredDrumPreset] = &[
132 LayeredDrumPreset {
133 name: "kick_808_deep_layer",
134 drum_types: &[DrumType::Kick808, DrumType::SubKick],
135 },
136 LayeredDrumPreset {
137 name: "kick_909_click_layer",
138 drum_types: &[DrumType::Kick909, DrumType::KickClick],
139 },
140 LayeredDrumPreset {
141 name: "kick_acoustic_room_layer",
142 drum_types: &[DrumType::KickAcoustic, DrumType::Boom],
143 },
144 LayeredDrumPreset {
145 name: "snare_808_body_layer",
146 drum_types: &[DrumType::Snare808, DrumType::SnareLoose],
147 },
148 LayeredDrumPreset {
149 name: "snare_909_snap_layer",
150 drum_types: &[DrumType::Snare909, DrumType::SnareRim],
151 },
152 LayeredDrumPreset {
153 name: "snare_piccolo_clap_layer",
154 drum_types: &[DrumType::SnarePiccolo, DrumType::ClapDry],
155 },
156 LayeredDrumPreset {
157 name: "clap_808_stack_layer",
158 drum_types: &[DrumType::Clap808, DrumType::ClapGroup],
159 },
160 LayeredDrumPreset {
161 name: "clap_909_wide_layer",
162 drum_types: &[DrumType::Clap909, DrumType::ClapRoom],
163 },
164 LayeredDrumPreset {
165 name: "hihat_808_sizzle_layer",
166 drum_types: &[DrumType::HiHat808Closed, DrumType::HiHatSizzle],
167 },
168 LayeredDrumPreset {
169 name: "hihat_909_sizzle_layer",
170 drum_types: &[DrumType::HiHat909Closed, DrumType::HiHatSizzle],
171 },
172 LayeredDrumPreset {
173 name: "tom_808_stack_layer",
174 drum_types: &[DrumType::Tom808Low, DrumType::Tom808Mid],
175 },
176 LayeredDrumPreset {
177 name: "crash_swell_layer",
178 drum_types: &[DrumType::Crash, DrumType::CymbalSwell],
179 },
180];
181
182pub fn drum_names() -> Vec<&'static str> {
184 DRUM_NAMES
185 .iter()
186 .copied()
187 .chain(LAYERED_DRUM_PRESETS.iter().map(|preset| preset.name))
188 .collect()
189}
190
191pub fn drum_by_name(name: &str) -> PulseResult<DrumType> {
193 let normalized = normalize_name(name);
194 DRUM_PRESETS
195 .iter()
196 .find(|preset| normalize_name(preset.name) == normalized)
197 .map(|preset| preset.drum_type)
198 .ok_or_else(|| PulseError::InvalidDrumType {
199 value: name.to_string(),
200 })
201}
202
203pub fn drum_voices_by_name(name: &str) -> PulseResult<Vec<DrumType>> {
205 let normalized = normalize_name(name);
206 if let Some(preset) = DRUM_PRESETS
207 .iter()
208 .find(|preset| normalize_name(preset.name) == normalized)
209 {
210 return Ok(vec![preset.drum_type]);
211 }
212
213 LAYERED_DRUM_PRESETS
214 .iter()
215 .find(|preset| normalize_name(preset.name) == normalized)
216 .map(|preset| preset.drum_types.to_vec())
217 .ok_or_else(|| PulseError::InvalidDrumType {
218 value: name.to_string(),
219 })
220}
221
222fn normalize_name(value: &str) -> String {
223 value
224 .trim()
225 .to_ascii_lowercase()
226 .replace(['_', '-', ' '], "")
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct PulseDrumHit {
232 drum: String,
233 pattern: Vec<usize>,
234}
235
236impl PulseDrumHit {
237 pub fn new(drum: impl Into<String>, pattern: Vec<usize>) -> Self {
239 Self {
240 drum: drum.into(),
241 pattern,
242 }
243 }
244
245 pub fn drum(&self) -> &str {
247 &self.drum
248 }
249
250 pub fn pattern(&self) -> &[usize] {
252 &self.pattern
253 }
254}
255
256#[derive(Debug, Clone, PartialEq)]
258pub struct PulseDrumGrid {
259 track_name: String,
260 steps: usize,
261 step_duration: f32,
262 hits: Vec<PulseDrumHit>,
263 accents: Option<Vec<usize>>,
264 velocities: Option<Vec<f32>>,
265 repeats: usize,
266 effects: Vec<PulseEffect>,
267 volume: f32,
268 pan: f32,
269}
270
271impl Default for PulseDrumGrid {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl PulseDrumGrid {
278 pub fn new() -> Self {
280 Self {
281 track_name: "drums".to_string(),
282 steps: 16,
283 step_duration: 0.125,
284 hits: Vec::new(),
285 accents: None,
286 velocities: None,
287 repeats: 0,
288 effects: Vec::new(),
289 volume: 1.0,
290 pan: 0.0,
291 }
292 }
293
294 #[must_use]
296 pub fn with_track(mut self, track_name: impl Into<String>) -> Self {
297 self.track_name = track_name.into();
298 self
299 }
300
301 pub fn with_steps(mut self, steps: usize) -> PulseResult<Self> {
307 validate_drum_steps(steps)?;
308 self.steps = steps;
309 Ok(self)
310 }
311
312 pub fn with_step_duration(mut self, duration: f32) -> PulseResult<Self> {
318 if !duration.is_finite() || duration <= 0.0 {
319 return Err(PulseError::InvalidStepDuration { duration });
320 }
321 self.step_duration = duration;
322 Ok(self)
323 }
324
325 pub fn sound(mut self, drum: impl Into<String>, pattern: Vec<usize>) -> PulseResult<Self> {
331 let drum = drum.into();
332 drum_voices_by_name(&drum)?;
333 self.hits.push(PulseDrumHit::new(drum, pattern));
334 Ok(self)
335 }
336
337 #[must_use]
339 pub fn accent(mut self, pattern: Vec<usize>) -> Self {
340 self.accents = Some(pattern);
341 self
342 }
343
344 #[must_use]
346 pub fn velocity(mut self, velocities: Vec<f32>) -> Self {
347 self.velocities = Some(velocities);
348 self
349 }
350
351 #[must_use]
353 pub fn with_repeat(mut self, repeats: usize) -> Self {
354 self.repeats = repeats;
355 self
356 }
357
358 pub fn with_repeat_times(mut self, repeat_times: usize) -> PulseResult<Self> {
364 if repeat_times == 0 {
365 return Err(PulseError::InvalidRepeatTimes { repeat_times });
366 }
367 validate_drum_repeat_expansion(self.steps, repeat_times)?;
368 self.repeats = repeat_times - 1;
369 Ok(self)
370 }
371
372 #[must_use]
374 pub fn with_effect(mut self, effect: PulseEffect) -> Self {
375 self.effects.push(effect);
376 self
377 }
378
379 pub fn with_volume(mut self, volume: f32) -> PulseResult<Self> {
385 validate_drum_range("volume", volume, 0.0, 2.0)?;
386 self.volume = volume;
387 Ok(self)
388 }
389
390 pub fn with_pan(mut self, pan: f32) -> PulseResult<Self> {
396 validate_drum_range("pan", pan, -1.0, 1.0)?;
397 self.pan = pan;
398 Ok(self)
399 }
400
401 pub fn apply_to_composition(&self, composition: &mut Composition) -> PulseResult<()> {
407 self.apply_to_composition_at(composition, 0.0)
408 }
409
410 pub fn apply_to_composition_at(
416 &self,
417 composition: &mut Composition,
418 start_at: f32,
419 ) -> PulseResult<()> {
420 validate_drum_steps(self.steps)?;
421 if !self.step_duration.is_finite() || self.step_duration <= 0.0 {
422 return Err(PulseError::InvalidStepDuration {
423 duration: self.step_duration,
424 });
425 }
426 validate_drum_range("volume", self.volume, 0.0, 2.0)?;
427 validate_drum_range("pan", self.pan, -1.0, 1.0)?;
428 validate_drum_repeat_expansion(self.steps, self.repeats.saturating_add(1))?;
429
430 let resolved_hits = self
431 .hits
432 .iter()
433 .map(|hit| drum_voices_by_name(hit.drum()).map(|drums| (drums, hit.pattern().to_vec())))
434 .collect::<PulseResult<Vec<_>>>()?;
435 let accents = self.accents.clone();
436 let velocities = self.velocities.clone();
437 let repeats = self.repeats;
438
439 let mut builder = composition
440 .track(&self.track_name)
441 .volume(self.volume)
442 .pan(self.pan)
443 .at(start_at)
444 .drum_grid(self.steps, self.step_duration, |grid| {
445 let mut grid = resolved_hits.iter().fold(grid, |grid, (drums, pattern)| {
446 drums
447 .iter()
448 .fold(grid, |grid, drum| grid.sound(*drum, pattern))
449 });
450 if let Some(pattern) = &accents {
451 grid = grid.accent(pattern);
452 }
453 if let Some(values) = &velocities {
454 grid = grid.velocity(values);
455 }
456 if repeats > 0 {
457 grid = grid.repeat(repeats);
458 }
459 grid
460 });
461
462 for effect in &self.effects {
463 builder = effect.apply_to_track_builder(builder);
464 }
465 let _ = builder;
466
467 Ok(())
468 }
469
470 pub fn track_name(&self) -> &str {
472 &self.track_name
473 }
474
475 pub fn steps(&self) -> usize {
477 self.steps
478 }
479
480 pub fn step_duration(&self) -> f32 {
482 self.step_duration
483 }
484
485 pub fn hits(&self) -> &[PulseDrumHit] {
487 &self.hits
488 }
489
490 pub fn repeats(&self) -> usize {
492 self.repeats
493 }
494
495 pub fn duration(&self) -> f32 {
497 self.steps as f32 * self.step_duration * self.repeats.saturating_add(1) as f32
498 }
499
500 pub fn effects(&self) -> &[PulseEffect] {
502 &self.effects
503 }
504
505 pub fn volume(&self) -> f32 {
507 self.volume
508 }
509
510 pub fn pan(&self) -> f32 {
512 self.pan
513 }
514}
515
516fn validate_drum_range(option: &str, value: f32, min: f32, max: f32) -> PulseResult<()> {
517 if value.is_finite() && value >= min && value <= max {
518 Ok(())
519 } else {
520 Err(PulseError::InvalidDrumOption {
521 option: option.to_string(),
522 value: value.to_string(),
523 })
524 }
525}
526
527fn validate_drum_steps(steps: usize) -> PulseResult<()> {
528 if steps == 0 || steps > isize::MAX as usize {
529 return Err(PulseError::InvalidDrumGridSteps { steps });
530 }
531 Ok(())
532}
533
534fn validate_drum_repeat_expansion(steps: usize, repeat_times: usize) -> PulseResult<()> {
535 let total_steps = steps
536 .checked_mul(repeat_times)
537 .ok_or(PulseError::InvalidRepeatTimes { repeat_times })?;
538 if total_steps > isize::MAX as usize {
539 return Err(PulseError::InvalidRepeatTimes { repeat_times });
540 }
541 Ok(())
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub enum DrumKit {
547 Tr808,
549}
550
551#[derive(Debug, Clone, PartialEq)]
553pub struct PulseDrumMachine {
554 kit: DrumKit,
555 grid: PulseDrumGrid,
556}
557
558impl PulseDrumMachine {
559 pub fn new(kit: &str) -> PulseResult<Self> {
565 match normalize_name(kit).as_str() {
566 "808" | "tr808" => Ok(Self {
567 kit: DrumKit::Tr808,
568 grid: PulseDrumGrid::new(),
569 }),
570 _ => Err(PulseError::InvalidDrumType {
571 value: kit.to_string(),
572 }),
573 }
574 }
575
576 pub fn steps(mut self, steps: usize) -> PulseResult<Self> {
582 self.grid = self.grid.with_steps(steps)?;
583 Ok(self)
584 }
585
586 pub fn step_duration(mut self, duration: f32) -> PulseResult<Self> {
592 self.grid = self.grid.with_step_duration(duration)?;
593 Ok(self)
594 }
595
596 pub fn kick(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
602 self.grid = self.grid.sound("kick_808", pattern)?;
603 Ok(self)
604 }
605
606 pub fn snare(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
612 self.grid = self.grid.sound("snare_808", pattern)?;
613 Ok(self)
614 }
615
616 pub fn hihat(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
622 self.grid = self.grid.sound("hihat_808_closed", pattern)?;
623 Ok(self)
624 }
625
626 pub fn open_hihat(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
632 self.grid = self.grid.sound("hihat_808_open", pattern)?;
633 Ok(self)
634 }
635
636 pub fn clap(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
642 self.grid = self.grid.sound("clap_808", pattern)?;
643 Ok(self)
644 }
645
646 #[must_use]
648 pub fn grid(self) -> PulseDrumGrid {
649 self.grid
650 }
651
652 #[must_use]
654 pub fn kit(&self) -> DrumKit {
655 self.kit
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 fn test_compressor_effect() -> crate::effects::PulseEffect {
664 crate::effects::effect_from_options("compressor", crate::effects::EffectOptions::Default)
665 .expect("default compressor should parse")
666 }
667
668 #[test]
669 fn exposes_all_current_tunes_drum_types() {
670 let names = drum_names();
671 assert!(names.len() >= 100);
672 assert!(names.contains(&"kick_808"));
673 assert!(names.contains(&"snare_808"));
674 assert!(names.contains(&"hihat_808_closed"));
675 assert!(names.contains(&"clap_808"));
676 assert!(names.contains(&"kick_808_deep_layer"));
677 assert!(names.contains(&"snare_808_body_layer"));
678 assert!(names.contains(&"clap_909_wide_layer"));
679
680 for name in names {
681 drum_voices_by_name(name).expect("catalog entry should resolve");
682 }
683 }
684
685 #[test]
686 fn layered_drum_aliases_resolve_to_multiple_tunes_drum_types() {
687 let voices =
688 drum_voices_by_name("kick_808_deep_layer").expect("layered kick should resolve");
689
690 assert_eq!(voices, vec![DrumType::Kick808, DrumType::SubKick]);
691 assert_eq!(
692 drum_by_name("kick_808_deep_layer")
693 .expect_err("layered aliases should require voice resolution")
694 .to_string(),
695 "invalid drum type: kick_808_deep_layer"
696 );
697 }
698
699 #[test]
700 fn rejects_unknown_drum_alias() {
701 let error = drum_by_name("kick_707").expect_err("unknown drum should fail");
702 assert_eq!(error.to_string(), "invalid drum type: kick_707");
703 }
704
705 #[test]
706 fn drum_grid_validates_and_builds_patterns() {
707 let grid = PulseDrumGrid::new()
708 .with_steps(16)
709 .unwrap()
710 .with_step_duration(0.125)
711 .unwrap()
712 .sound("kick_808", vec![0, 4, 8, 12])
713 .unwrap()
714 .accent(vec![0, 4, 8, 12])
715 .with_repeat(1);
716
717 assert_eq!(grid.steps(), 16);
718 assert_eq!(grid.hits().len(), 1);
719 assert_eq!(grid.repeats(), 1);
720 }
721
722 #[test]
723 fn drum_grid_repeat_times_uses_total_play_count() {
724 let single_pass = PulseDrumGrid::new()
725 .with_steps(4)
726 .unwrap()
727 .with_step_duration(0.25)
728 .unwrap()
729 .with_repeat_times(1)
730 .unwrap();
731
732 assert_eq!(single_pass.repeats(), 0);
733 assert_eq!(single_pass.duration(), 1.0);
734
735 let double_pass = single_pass.clone().with_repeat_times(2).unwrap();
736 assert_eq!(double_pass.repeats(), 1);
737 assert_eq!(double_pass.duration(), 2.0);
738
739 assert_eq!(
740 PulseDrumGrid::new()
741 .with_repeat_times(0)
742 .unwrap_err()
743 .to_string(),
744 "invalid repeat times: 0"
745 );
746 }
747
748 #[test]
749 fn drum_grid_rejects_unrenderable_repeat_expansion() {
750 let error = PulseDrumGrid::new()
751 .with_repeat_times(usize::MAX)
752 .expect_err("unrenderable drum repeat expansion should fail");
753
754 assert!(matches!(error, PulseError::InvalidRepeatTimes { .. }));
755 }
756
757 #[test]
758 fn drum_grid_rejects_unrenderable_step_counts() {
759 let error = PulseDrumGrid::new()
760 .with_steps(usize::MAX)
761 .expect_err("unrenderable drum step count should fail");
762
763 assert!(matches!(error, PulseError::InvalidDrumGridSteps { .. }));
764 }
765
766 #[test]
767 fn drum_machine_808_builds_grid() {
768 let grid = PulseDrumMachine::new("808")
769 .unwrap()
770 .steps(16)
771 .unwrap()
772 .kick(vec![0, 4, 8, 12])
773 .unwrap()
774 .snare(vec![4, 12])
775 .unwrap()
776 .hihat(vec![0, 2, 4, 6, 8, 10, 12, 14])
777 .unwrap()
778 .grid();
779
780 assert_eq!(grid.hits().len(), 3);
781 }
782
783 #[test]
784 fn drum_grid_stores_and_applies_track_effects() {
785 let grid = PulseDrumGrid::new()
786 .sound("kick_808", vec![0, 4, 8, 12])
787 .unwrap()
788 .with_effect(test_compressor_effect());
789
790 assert_eq!(grid.effects().len(), 1);
791
792 let song = crate::composition::PulseSong::new().add_drum_grid(grid);
793 let mixer = song.to_mixer().expect("drum grid should build");
794 let tracks = mixer.all_tracks();
795
796 assert_eq!(tracks.len(), 1);
797 assert!(tracks[0].effects.compressor.is_some());
798 }
799
800 #[test]
801 fn drum_grid_applies_volume_and_pan_to_exported_track() {
802 let grid = PulseDrumGrid::new()
803 .sound("kick_808", vec![0])
804 .unwrap()
805 .with_volume(0.8)
806 .unwrap()
807 .with_pan(0.2)
808 .unwrap();
809
810 let song = crate::composition::PulseSong::new().add_drum_grid(grid);
811 let mixer = song.to_mixer().expect("drum grid should build");
812 let tracks = mixer.all_tracks();
813
814 assert_eq!(tracks.len(), 1);
815 assert_eq!(tracks[0].volume, 0.8);
816 assert_eq!(tracks[0].pan, 0.2);
817 }
818
819 #[test]
820 fn drum_grid_expands_layered_aliases_into_exported_events() {
821 let grid = PulseDrumGrid::new()
822 .sound("kick_808_deep_layer", vec![0])
823 .unwrap();
824
825 let song = crate::composition::PulseSong::new().add_drum_grid(grid);
826 let mixer = song.to_mixer().expect("layered drum grid should build");
827 let tracks = mixer.all_tracks();
828 let drum_events = tracks[0]
829 .events
830 .iter()
831 .filter(|event| matches!(event, tunes::track::AudioEvent::Drum(_)))
832 .count();
833
834 assert_eq!(drum_events, 2);
835 }
836}