use crate::effects::PulseEffect;
use crate::error::{PulseError, PulseResult};
use tunes::composition::Composition;
use tunes::instruments::drums::DrumType;
#[derive(Clone, Copy)]
struct DrumPreset {
name: &'static str,
drum_type: DrumType,
}
#[derive(Clone, Copy)]
struct LayeredDrumPreset {
name: &'static str,
drum_types: &'static [DrumType],
}
macro_rules! drum_presets {
($(($name:literal, $variant:ident)),+ $(,)?) => {
const DRUM_PRESETS: &[DrumPreset] = &[
$(DrumPreset {
name: $name,
drum_type: DrumType::$variant,
},)+
];
const DRUM_NAMES: &[&str] = &[
$($name,)+
];
};
}
drum_presets!(
("kick", Kick),
("kick_808", Kick808),
("sub_kick", SubKick),
("snare", Snare),
("snare_808", Snare808),
("hihat_closed", HiHatClosed),
("hihat_open", HiHatOpen),
("hihat_808_closed", HiHat808Closed),
("hihat_808_open", HiHat808Open),
("clap", Clap),
("clap_808", Clap808),
("tom", Tom),
("tom_high", TomHigh),
("tom_low", TomLow),
("rimshot", Rimshot),
("cowbell", Cowbell),
("crash", Crash),
("ride", Ride),
("china", China),
("splash", Splash),
("tambourine", Tambourine),
("shaker", Shaker),
("bass_drop", BassDrop),
("boom", Boom),
("claves", Claves),
("triangle", Triangle),
("side_stick", SideStick),
("wood_block", WoodBlock),
("kick_909", Kick909),
("snare_909", Snare909),
("conga_high", CongaHigh),
("conga_low", CongaLow),
("bongo_high", BongoHigh),
("bongo_low", BongoLow),
("ride_bell", RideBell),
("floor_tom_low", FloorTomLow),
("floor_tom_high", FloorTomHigh),
("hihat_pedal", HiHatPedal),
("crash_2", Crash2),
("vibraslap", Vibraslap),
("timbale_high", TimbaleHigh),
("timbale_low", TimbaleLow),
("agogo_high", AgogoHigh),
("agogo_low", AgogoLow),
("cabasa", Cabasa),
("guiro_short", GuiroShort),
("guiro_long", GuiroLong),
("wood_block_high", WoodBlockHigh),
("timpani", Timpani),
("gong", Gong),
("chimes", Chimes),
("djembe", Djembe),
("tabla_bayan", TablaBayan),
("tabla_dayan", TablaDayan),
("cajon", Cajon),
("fingersnap", Fingersnap),
("maracas", Maracas),
("castanet", Castanet),
("sleigh_bells", SleighBells),
("laser_zap", LaserZap),
("reverse_cymbal", ReverseCymbal),
("white_noise_hit", WhiteNoiseHit),
("stick_click", StickClick),
("kick_tight", KickTight),
("kick_deep", KickDeep),
("kick_acoustic", KickAcoustic),
("kick_click", KickClick),
("snare_rim", SnareRim),
("snare_tight", SnareTight),
("snare_loose", SnareLoose),
("snare_piccolo", SnarePiccolo),
("hihat_half_open", HiHatHalfOpen),
("hihat_sizzle", HiHatSizzle),
("clap_dry", ClapDry),
("clap_room", ClapRoom),
("clap_group", ClapGroup),
("clap_snare", ClapSnare),
("crash_short", CrashShort),
("ride_tip", RideTip),
("egg_shaker", EggShaker),
("tube_shaker", TubeShaker),
("tom_808_low", Tom808Low),
("tom_808_mid", Tom808Mid),
("tom_808_high", Tom808High),
("cowbell_808", Cowbell808),
("clave_808", Clave808),
("hihat_909_closed", HiHat909Closed),
("hihat_909_open", HiHat909Open),
("clap_909", Clap909),
("cowbell_909", Cowbell909),
("rim_909", Rim909),
("reverse_snare", ReverseSnare),
("cymbal_swell", CymbalSwell),
);
const LAYERED_DRUM_PRESETS: &[LayeredDrumPreset] = &[
LayeredDrumPreset {
name: "kick_808_deep_layer",
drum_types: &[DrumType::Kick808, DrumType::SubKick],
},
LayeredDrumPreset {
name: "kick_909_click_layer",
drum_types: &[DrumType::Kick909, DrumType::KickClick],
},
LayeredDrumPreset {
name: "kick_acoustic_room_layer",
drum_types: &[DrumType::KickAcoustic, DrumType::Boom],
},
LayeredDrumPreset {
name: "snare_808_body_layer",
drum_types: &[DrumType::Snare808, DrumType::SnareLoose],
},
LayeredDrumPreset {
name: "snare_909_snap_layer",
drum_types: &[DrumType::Snare909, DrumType::SnareRim],
},
LayeredDrumPreset {
name: "snare_piccolo_clap_layer",
drum_types: &[DrumType::SnarePiccolo, DrumType::ClapDry],
},
LayeredDrumPreset {
name: "clap_808_stack_layer",
drum_types: &[DrumType::Clap808, DrumType::ClapGroup],
},
LayeredDrumPreset {
name: "clap_909_wide_layer",
drum_types: &[DrumType::Clap909, DrumType::ClapRoom],
},
LayeredDrumPreset {
name: "hihat_808_sizzle_layer",
drum_types: &[DrumType::HiHat808Closed, DrumType::HiHatSizzle],
},
LayeredDrumPreset {
name: "hihat_909_sizzle_layer",
drum_types: &[DrumType::HiHat909Closed, DrumType::HiHatSizzle],
},
LayeredDrumPreset {
name: "tom_808_stack_layer",
drum_types: &[DrumType::Tom808Low, DrumType::Tom808Mid],
},
LayeredDrumPreset {
name: "crash_swell_layer",
drum_types: &[DrumType::Crash, DrumType::CymbalSwell],
},
];
pub fn drum_names() -> Vec<&'static str> {
DRUM_NAMES
.iter()
.copied()
.chain(LAYERED_DRUM_PRESETS.iter().map(|preset| preset.name))
.collect()
}
pub fn drum_by_name(name: &str) -> PulseResult<DrumType> {
let normalized = normalize_name(name);
DRUM_PRESETS
.iter()
.find(|preset| normalize_name(preset.name) == normalized)
.map(|preset| preset.drum_type)
.ok_or_else(|| PulseError::InvalidDrumType {
value: name.to_string(),
})
}
pub fn drum_voices_by_name(name: &str) -> PulseResult<Vec<DrumType>> {
let normalized = normalize_name(name);
if let Some(preset) = DRUM_PRESETS
.iter()
.find(|preset| normalize_name(preset.name) == normalized)
{
return Ok(vec![preset.drum_type]);
}
LAYERED_DRUM_PRESETS
.iter()
.find(|preset| normalize_name(preset.name) == normalized)
.map(|preset| preset.drum_types.to_vec())
.ok_or_else(|| PulseError::InvalidDrumType {
value: name.to_string(),
})
}
fn normalize_name(value: &str) -> String {
value
.trim()
.to_ascii_lowercase()
.replace(['_', '-', ' '], "")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PulseDrumHit {
drum: String,
pattern: Vec<usize>,
}
impl PulseDrumHit {
pub fn new(drum: impl Into<String>, pattern: Vec<usize>) -> Self {
Self {
drum: drum.into(),
pattern,
}
}
pub fn drum(&self) -> &str {
&self.drum
}
pub fn pattern(&self) -> &[usize] {
&self.pattern
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseDrumGrid {
track_name: String,
steps: usize,
step_duration: f32,
hits: Vec<PulseDrumHit>,
accents: Option<Vec<usize>>,
velocities: Option<Vec<f32>>,
repeats: usize,
effects: Vec<PulseEffect>,
volume: f32,
pan: f32,
}
impl Default for PulseDrumGrid {
fn default() -> Self {
Self::new()
}
}
impl PulseDrumGrid {
pub fn new() -> Self {
Self {
track_name: "drums".to_string(),
steps: 16,
step_duration: 0.125,
hits: Vec::new(),
accents: None,
velocities: None,
repeats: 0,
effects: Vec::new(),
volume: 1.0,
pan: 0.0,
}
}
#[must_use]
pub fn with_track(mut self, track_name: impl Into<String>) -> Self {
self.track_name = track_name.into();
self
}
pub fn with_steps(mut self, steps: usize) -> PulseResult<Self> {
validate_drum_steps(steps)?;
self.steps = steps;
Ok(self)
}
pub fn with_step_duration(mut self, duration: f32) -> PulseResult<Self> {
if !duration.is_finite() || duration <= 0.0 {
return Err(PulseError::InvalidStepDuration { duration });
}
self.step_duration = duration;
Ok(self)
}
pub fn sound(mut self, drum: impl Into<String>, pattern: Vec<usize>) -> PulseResult<Self> {
let drum = drum.into();
drum_voices_by_name(&drum)?;
self.hits.push(PulseDrumHit::new(drum, pattern));
Ok(self)
}
#[must_use]
pub fn accent(mut self, pattern: Vec<usize>) -> Self {
self.accents = Some(pattern);
self
}
#[must_use]
pub fn velocity(mut self, velocities: Vec<f32>) -> Self {
self.velocities = Some(velocities);
self
}
#[must_use]
pub fn with_repeat(mut self, repeats: usize) -> Self {
self.repeats = repeats;
self
}
pub fn with_repeat_times(mut self, repeat_times: usize) -> PulseResult<Self> {
if repeat_times == 0 {
return Err(PulseError::InvalidRepeatTimes { repeat_times });
}
validate_drum_repeat_expansion(self.steps, repeat_times)?;
self.repeats = repeat_times - 1;
Ok(self)
}
#[must_use]
pub fn with_effect(mut self, effect: PulseEffect) -> Self {
self.effects.push(effect);
self
}
pub fn with_volume(mut self, volume: f32) -> PulseResult<Self> {
validate_drum_range("volume", volume, 0.0, 2.0)?;
self.volume = volume;
Ok(self)
}
pub fn with_pan(mut self, pan: f32) -> PulseResult<Self> {
validate_drum_range("pan", pan, -1.0, 1.0)?;
self.pan = pan;
Ok(self)
}
pub fn apply_to_composition(&self, composition: &mut Composition) -> PulseResult<()> {
self.apply_to_composition_at(composition, 0.0)
}
pub fn apply_to_composition_at(
&self,
composition: &mut Composition,
start_at: f32,
) -> PulseResult<()> {
validate_drum_steps(self.steps)?;
if !self.step_duration.is_finite() || self.step_duration <= 0.0 {
return Err(PulseError::InvalidStepDuration {
duration: self.step_duration,
});
}
validate_drum_range("volume", self.volume, 0.0, 2.0)?;
validate_drum_range("pan", self.pan, -1.0, 1.0)?;
validate_drum_repeat_expansion(self.steps, self.repeats.saturating_add(1))?;
let resolved_hits = self
.hits
.iter()
.map(|hit| drum_voices_by_name(hit.drum()).map(|drums| (drums, hit.pattern().to_vec())))
.collect::<PulseResult<Vec<_>>>()?;
let accents = self.accents.clone();
let velocities = self.velocities.clone();
let repeats = self.repeats;
let mut builder = composition
.track(&self.track_name)
.volume(self.volume)
.pan(self.pan)
.at(start_at)
.drum_grid(self.steps, self.step_duration, |grid| {
let mut grid = resolved_hits.iter().fold(grid, |grid, (drums, pattern)| {
drums
.iter()
.fold(grid, |grid, drum| grid.sound(*drum, pattern))
});
if let Some(pattern) = &accents {
grid = grid.accent(pattern);
}
if let Some(values) = &velocities {
grid = grid.velocity(values);
}
if repeats > 0 {
grid = grid.repeat(repeats);
}
grid
});
for effect in &self.effects {
builder = effect.apply_to_track_builder(builder);
}
let _ = builder;
Ok(())
}
pub fn track_name(&self) -> &str {
&self.track_name
}
pub fn steps(&self) -> usize {
self.steps
}
pub fn step_duration(&self) -> f32 {
self.step_duration
}
pub fn hits(&self) -> &[PulseDrumHit] {
&self.hits
}
pub fn repeats(&self) -> usize {
self.repeats
}
pub fn duration(&self) -> f32 {
self.steps as f32 * self.step_duration * self.repeats.saturating_add(1) as f32
}
pub fn effects(&self) -> &[PulseEffect] {
&self.effects
}
pub fn volume(&self) -> f32 {
self.volume
}
pub fn pan(&self) -> f32 {
self.pan
}
}
fn validate_drum_range(option: &str, value: f32, min: f32, max: f32) -> PulseResult<()> {
if value.is_finite() && value >= min && value <= max {
Ok(())
} else {
Err(PulseError::InvalidDrumOption {
option: option.to_string(),
value: value.to_string(),
})
}
}
fn validate_drum_steps(steps: usize) -> PulseResult<()> {
if steps == 0 || steps > isize::MAX as usize {
return Err(PulseError::InvalidDrumGridSteps { steps });
}
Ok(())
}
fn validate_drum_repeat_expansion(steps: usize, repeat_times: usize) -> PulseResult<()> {
let total_steps = steps
.checked_mul(repeat_times)
.ok_or(PulseError::InvalidRepeatTimes { repeat_times })?;
if total_steps > isize::MAX as usize {
return Err(PulseError::InvalidRepeatTimes { repeat_times });
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrumKit {
Tr808,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseDrumMachine {
kit: DrumKit,
grid: PulseDrumGrid,
}
impl PulseDrumMachine {
pub fn new(kit: &str) -> PulseResult<Self> {
match normalize_name(kit).as_str() {
"808" | "tr808" => Ok(Self {
kit: DrumKit::Tr808,
grid: PulseDrumGrid::new(),
}),
_ => Err(PulseError::InvalidDrumType {
value: kit.to_string(),
}),
}
}
pub fn steps(mut self, steps: usize) -> PulseResult<Self> {
self.grid = self.grid.with_steps(steps)?;
Ok(self)
}
pub fn step_duration(mut self, duration: f32) -> PulseResult<Self> {
self.grid = self.grid.with_step_duration(duration)?;
Ok(self)
}
pub fn kick(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
self.grid = self.grid.sound("kick_808", pattern)?;
Ok(self)
}
pub fn snare(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
self.grid = self.grid.sound("snare_808", pattern)?;
Ok(self)
}
pub fn hihat(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
self.grid = self.grid.sound("hihat_808_closed", pattern)?;
Ok(self)
}
pub fn open_hihat(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
self.grid = self.grid.sound("hihat_808_open", pattern)?;
Ok(self)
}
pub fn clap(mut self, pattern: Vec<usize>) -> PulseResult<Self> {
self.grid = self.grid.sound("clap_808", pattern)?;
Ok(self)
}
#[must_use]
pub fn grid(self) -> PulseDrumGrid {
self.grid
}
#[must_use]
pub fn kit(&self) -> DrumKit {
self.kit
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_compressor_effect() -> crate::effects::PulseEffect {
crate::effects::effect_from_options("compressor", crate::effects::EffectOptions::Default)
.expect("default compressor should parse")
}
#[test]
fn exposes_all_current_tunes_drum_types() {
let names = drum_names();
assert!(names.len() >= 100);
assert!(names.contains(&"kick_808"));
assert!(names.contains(&"snare_808"));
assert!(names.contains(&"hihat_808_closed"));
assert!(names.contains(&"clap_808"));
assert!(names.contains(&"kick_808_deep_layer"));
assert!(names.contains(&"snare_808_body_layer"));
assert!(names.contains(&"clap_909_wide_layer"));
for name in names {
drum_voices_by_name(name).expect("catalog entry should resolve");
}
}
#[test]
fn layered_drum_aliases_resolve_to_multiple_tunes_drum_types() {
let voices =
drum_voices_by_name("kick_808_deep_layer").expect("layered kick should resolve");
assert_eq!(voices, vec![DrumType::Kick808, DrumType::SubKick]);
assert_eq!(
drum_by_name("kick_808_deep_layer")
.expect_err("layered aliases should require voice resolution")
.to_string(),
"invalid drum type: kick_808_deep_layer"
);
}
#[test]
fn rejects_unknown_drum_alias() {
let error = drum_by_name("kick_707").expect_err("unknown drum should fail");
assert_eq!(error.to_string(), "invalid drum type: kick_707");
}
#[test]
fn drum_grid_validates_and_builds_patterns() {
let grid = PulseDrumGrid::new()
.with_steps(16)
.unwrap()
.with_step_duration(0.125)
.unwrap()
.sound("kick_808", vec![0, 4, 8, 12])
.unwrap()
.accent(vec![0, 4, 8, 12])
.with_repeat(1);
assert_eq!(grid.steps(), 16);
assert_eq!(grid.hits().len(), 1);
assert_eq!(grid.repeats(), 1);
}
#[test]
fn drum_grid_repeat_times_uses_total_play_count() {
let single_pass = PulseDrumGrid::new()
.with_steps(4)
.unwrap()
.with_step_duration(0.25)
.unwrap()
.with_repeat_times(1)
.unwrap();
assert_eq!(single_pass.repeats(), 0);
assert_eq!(single_pass.duration(), 1.0);
let double_pass = single_pass.clone().with_repeat_times(2).unwrap();
assert_eq!(double_pass.repeats(), 1);
assert_eq!(double_pass.duration(), 2.0);
assert_eq!(
PulseDrumGrid::new()
.with_repeat_times(0)
.unwrap_err()
.to_string(),
"invalid repeat times: 0"
);
}
#[test]
fn drum_grid_rejects_unrenderable_repeat_expansion() {
let error = PulseDrumGrid::new()
.with_repeat_times(usize::MAX)
.expect_err("unrenderable drum repeat expansion should fail");
assert!(matches!(error, PulseError::InvalidRepeatTimes { .. }));
}
#[test]
fn drum_grid_rejects_unrenderable_step_counts() {
let error = PulseDrumGrid::new()
.with_steps(usize::MAX)
.expect_err("unrenderable drum step count should fail");
assert!(matches!(error, PulseError::InvalidDrumGridSteps { .. }));
}
#[test]
fn drum_machine_808_builds_grid() {
let grid = PulseDrumMachine::new("808")
.unwrap()
.steps(16)
.unwrap()
.kick(vec![0, 4, 8, 12])
.unwrap()
.snare(vec![4, 12])
.unwrap()
.hihat(vec![0, 2, 4, 6, 8, 10, 12, 14])
.unwrap()
.grid();
assert_eq!(grid.hits().len(), 3);
}
#[test]
fn drum_grid_stores_and_applies_track_effects() {
let grid = PulseDrumGrid::new()
.sound("kick_808", vec![0, 4, 8, 12])
.unwrap()
.with_effect(test_compressor_effect());
assert_eq!(grid.effects().len(), 1);
let song = crate::composition::PulseSong::new().add_drum_grid(grid);
let mixer = song.to_mixer().expect("drum grid should build");
let tracks = mixer.all_tracks();
assert_eq!(tracks.len(), 1);
assert!(tracks[0].effects.compressor.is_some());
}
#[test]
fn drum_grid_applies_volume_and_pan_to_exported_track() {
let grid = PulseDrumGrid::new()
.sound("kick_808", vec![0])
.unwrap()
.with_volume(0.8)
.unwrap()
.with_pan(0.2)
.unwrap();
let song = crate::composition::PulseSong::new().add_drum_grid(grid);
let mixer = song.to_mixer().expect("drum grid should build");
let tracks = mixer.all_tracks();
assert_eq!(tracks.len(), 1);
assert_eq!(tracks[0].volume, 0.8);
assert_eq!(tracks[0].pan, 0.2);
}
#[test]
fn drum_grid_expands_layered_aliases_into_exported_events() {
let grid = PulseDrumGrid::new()
.sound("kick_808_deep_layer", vec![0])
.unwrap();
let song = crate::composition::PulseSong::new().add_drum_grid(grid);
let mixer = song.to_mixer().expect("layered drum grid should build");
let tracks = mixer.all_tracks();
let drum_events = tracks[0]
.events
.iter()
.filter(|event| matches!(event, tunes::track::AudioEvent::Drum(_)))
.count();
assert_eq!(drum_events, 2);
}
}