use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::midi;
pub const DEFAULT_MAX_SAMPLE_VOICES: u32 = 32;
pub const DEFAULT_VELOCITY: u8 = 100;
#[derive(Deserialize, Clone, Serialize, Debug)]
pub struct SampleDefinition {
file: Option<String>,
#[serde(default)]
output_channels: Vec<u16>,
#[serde(default)]
output_track: Option<String>,
#[serde(default)]
velocity: VelocityConfig,
#[serde(default, alias = "note_off")]
release_behavior: ReleaseBehavior,
#[serde(default)]
retrigger: RetriggerBehavior,
max_voices: Option<u32>,
#[serde(default = "default_fade_time_ms")]
fade_time_ms: u32,
}
fn default_fade_time_ms() -> u32 {
50
}
impl SampleDefinition {
pub fn output_channels(&self) -> &[u16] {
&self.output_channels
}
pub fn output_track(&self) -> Option<&str> {
self.output_track.as_deref()
}
pub fn release_behavior(&self) -> ReleaseBehavior {
self.release_behavior
}
pub fn retrigger(&self) -> RetriggerBehavior {
self.retrigger
}
pub fn max_voices(&self) -> Option<u32> {
self.max_voices
}
#[allow(dead_code)]
pub fn fade_time_ms(&self) -> u32 {
self.fade_time_ms
}
pub fn file_for_velocity(&self, velocity: u8) -> Option<(&str, f32)> {
match &self.velocity.mode {
VelocityMode::Ignore => {
let volume = self.velocity.default.unwrap_or(DEFAULT_VELOCITY) as f32 / 127.0;
self.file.as_deref().map(|f| (f, volume))
}
VelocityMode::Scale => {
let volume = velocity as f32 / 127.0;
self.file.as_deref().map(|f| (f, volume))
}
VelocityMode::Layers => {
for layer in &self.velocity.layers {
if velocity >= layer.range[0] && velocity <= layer.range[1] {
let volume = if self.velocity.scale.unwrap_or(false) {
velocity as f32 / 127.0
} else {
1.0
};
return Some((&layer.file, volume));
}
}
None
}
}
}
pub fn all_files(&self) -> Vec<&str> {
let mut files = Vec::new();
if let Some(file) = &self.file {
files.push(file.as_str());
}
for layer in &self.velocity.layers {
files.push(&layer.file);
}
files
}
}
#[cfg(test)]
impl SampleDefinition {
#[allow(clippy::too_many_arguments)]
pub fn new(
file: Option<String>,
output_channels: Vec<u16>,
velocity: VelocityConfig,
release_behavior: ReleaseBehavior,
retrigger: RetriggerBehavior,
max_voices: Option<u32>,
fade_time_ms: u32,
) -> Self {
Self {
file,
output_channels,
output_track: None,
velocity,
release_behavior,
retrigger,
max_voices,
fade_time_ms,
}
}
pub fn file(&self) -> Option<&str> {
self.file.as_deref()
}
}
#[derive(Deserialize, Clone, Serialize, Debug, Default)]
pub struct VelocityConfig {
#[serde(default)]
mode: VelocityMode,
default: Option<u8>,
scale: Option<bool>,
#[serde(default)]
layers: Vec<VelocityLayer>,
}
#[cfg(test)]
impl VelocityConfig {
pub fn ignore(default: Option<u8>) -> Self {
Self {
mode: VelocityMode::Ignore,
default,
scale: None,
layers: Vec::new(),
}
}
pub fn scale() -> Self {
Self {
mode: VelocityMode::Scale,
default: None,
scale: None,
layers: Vec::new(),
}
}
pub fn with_layers(layers: Vec<VelocityLayer>, scale: bool) -> Self {
Self {
mode: VelocityMode::Layers,
default: None,
scale: Some(scale),
layers,
}
}
}
#[derive(Deserialize, Clone, Copy, Serialize, Debug, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VelocityMode {
#[default]
Ignore,
Scale,
Layers,
}
#[derive(Deserialize, Clone, Serialize, Debug)]
pub struct VelocityLayer {
range: [u8; 2],
file: String,
}
#[cfg(test)]
impl VelocityLayer {
pub fn new(range: [u8; 2], file: String) -> Self {
Self { range, file }
}
}
#[derive(Deserialize, Clone, Copy, Serialize, Debug, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReleaseBehavior {
#[default]
PlayToCompletion,
Stop,
Fade,
}
#[derive(Deserialize, Clone, Copy, Serialize, Debug, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RetriggerBehavior {
#[default]
Cut,
Polyphonic,
}
#[derive(Deserialize, Clone, Serialize, Debug)]
pub struct SampleTrigger {
trigger: midi::Event,
sample: String,
}
impl SampleTrigger {
pub fn new(trigger: midi::Event, sample: String) -> Self {
Self { trigger, sample }
}
pub fn trigger(&self) -> &midi::Event {
&self.trigger
}
pub fn sample(&self) -> &str {
&self.sample
}
}
#[derive(Deserialize, Clone, Serialize, Debug, Default)]
pub struct SamplesConfig {
#[serde(default)]
samples: HashMap<String, SampleDefinition>,
#[serde(default)]
sample_triggers: Vec<SampleTrigger>,
#[serde(default = "default_max_sample_voices")]
max_sample_voices: u32,
}
fn default_max_sample_voices() -> u32 {
DEFAULT_MAX_SAMPLE_VOICES
}
impl SamplesConfig {
pub fn new(
samples: HashMap<String, SampleDefinition>,
sample_triggers: Vec<SampleTrigger>,
max_sample_voices: u32,
) -> Self {
Self {
samples,
sample_triggers,
max_sample_voices,
}
}
pub fn samples(&self) -> &HashMap<String, SampleDefinition> {
&self.samples
}
pub fn sample_triggers(&self) -> &[SampleTrigger] {
&self.sample_triggers
}
pub fn add_triggers(&mut self, triggers: Vec<SampleTrigger>) {
for trigger in triggers {
self.sample_triggers
.retain(|t| t.trigger != trigger.trigger);
self.sample_triggers.push(trigger);
}
}
pub fn merge(&mut self, other: SamplesConfig) {
for (name, definition) in other.samples {
self.samples.insert(name, definition);
}
for other_trigger in other.sample_triggers {
self.sample_triggers
.retain(|t| t.trigger != other_trigger.trigger);
self.sample_triggers.push(other_trigger);
}
}
}
#[cfg(test)]
mod tests {
use config::{Config, File, FileFormat};
use super::*;
#[test]
fn test_velocity_ignore() {
let def = SampleDefinition::new(
Some("test.wav".to_string()),
vec![1, 2],
VelocityConfig::ignore(Some(100)),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
);
let (file, volume) = def.file_for_velocity(50).unwrap();
assert_eq!(file, "test.wav");
assert!((volume - 100.0 / 127.0).abs() < 0.001);
let (_, volume2) = def.file_for_velocity(127).unwrap();
assert!((volume - volume2).abs() < 0.001);
}
#[test]
fn test_velocity_scale() {
let def = SampleDefinition::new(
Some("test.wav".to_string()),
vec![1, 2],
VelocityConfig::scale(),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
);
let (file, volume) = def.file_for_velocity(64).unwrap();
assert_eq!(file, "test.wav");
assert!((volume - 64.0 / 127.0).abs() < 0.001);
let (_, volume2) = def.file_for_velocity(127).unwrap();
assert!((volume2 - 1.0).abs() < 0.001);
}
#[test]
fn test_velocity_layers() {
let layers = vec![
VelocityLayer::new([1, 60], "soft.wav".to_string()),
VelocityLayer::new([61, 100], "medium.wav".to_string()),
VelocityLayer::new([101, 127], "hard.wav".to_string()),
];
let def = SampleDefinition::new(
None,
vec![1, 2],
VelocityConfig::with_layers(layers, false),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Polyphonic,
Some(4),
50,
);
let (file, volume) = def.file_for_velocity(45).unwrap();
assert_eq!(file, "soft.wav");
assert!((volume - 1.0).abs() < 0.001);
let (file, _) = def.file_for_velocity(80).unwrap();
assert_eq!(file, "medium.wav");
let (file, _) = def.file_for_velocity(120).unwrap();
assert_eq!(file, "hard.wav");
}
#[test]
fn test_velocity_layers_with_scale() {
let layers = vec![
VelocityLayer::new([1, 60], "soft.wav".to_string()),
VelocityLayer::new([61, 127], "hard.wav".to_string()),
];
let def = SampleDefinition::new(
None,
vec![1, 2],
VelocityConfig::with_layers(layers, true), ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Polyphonic,
None,
50,
);
let (file, volume) = def.file_for_velocity(45).unwrap();
assert_eq!(file, "soft.wav");
assert!((volume - 45.0 / 127.0).abs() < 0.001);
let (file, volume) = def.file_for_velocity(100).unwrap();
assert_eq!(file, "hard.wav");
assert!((volume - 100.0 / 127.0).abs() < 0.001);
}
#[test]
fn test_all_files() {
let layers = vec![
VelocityLayer::new([1, 60], "soft.wav".to_string()),
VelocityLayer::new([61, 127], "hard.wav".to_string()),
];
let def = SampleDefinition::new(
Some("default.wav".to_string()),
vec![1, 2],
VelocityConfig::with_layers(layers, false),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
);
let files = def.all_files();
assert_eq!(files.len(), 3);
assert!(files.contains(&"default.wav"));
assert!(files.contains(&"soft.wav"));
assert!(files.contains(&"hard.wav"));
}
#[test]
fn test_merge_configs() {
let mut config1 = SamplesConfig::new(
HashMap::from([
(
"kick".to_string(),
SampleDefinition::new(
Some("kick1.wav".to_string()),
vec![1],
VelocityConfig::ignore(None),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
),
),
(
"snare".to_string(),
SampleDefinition::new(
Some("snare1.wav".to_string()),
vec![2],
VelocityConfig::ignore(None),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
),
),
]),
vec![],
32,
);
let config2 = SamplesConfig::new(
HashMap::from([(
"kick".to_string(),
SampleDefinition::new(
Some("kick2.wav".to_string()), vec![1, 2],
VelocityConfig::scale(),
ReleaseBehavior::Stop,
RetriggerBehavior::Polyphonic,
Some(4),
100,
),
)]),
vec![],
32,
);
config1.merge(config2);
assert_eq!(
config1.samples.get("kick").unwrap().file(),
Some("kick2.wav")
);
assert_eq!(
config1.samples.get("snare").unwrap().file(),
Some("snare1.wav")
);
}
#[test]
fn test_release_behavior_yaml_keys() {
let yaml = r#"
samples:
kick:
file: kick.wav
output_channels: [1]
release_behavior: stop
"#;
let config: SamplesConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(
config.samples.get("kick").unwrap().release_behavior(),
ReleaseBehavior::Stop,
);
let yaml = r#"
samples:
kick:
file: kick.wav
output_channels: [1]
note_off: fade
"#;
let config: SamplesConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert_eq!(
config.samples.get("kick").unwrap().release_behavior(),
ReleaseBehavior::Fade,
);
}
#[test]
fn test_output_track_deserialization() {
let yaml = r#"
samples:
kick:
file: kick.wav
output_track: kick-out
"#;
let config: SamplesConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
let kick = config.samples.get("kick").unwrap();
assert_eq!(kick.output_track(), Some("kick-out"));
assert!(kick.output_channels().is_empty());
let yaml = r#"
samples:
snare:
file: snare.wav
output_channels: [3, 4]
"#;
let config: SamplesConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
let snare = config.samples.get("snare").unwrap();
assert_eq!(snare.output_track(), None);
assert_eq!(snare.output_channels(), &[3, 4]);
let yaml = r#"
samples:
both:
file: both.wav
output_track: both-out
output_channels: [5, 6]
"#;
let config: SamplesConfig = Config::builder()
.add_source(File::from_str(yaml, FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
let both = config.samples.get("both").unwrap();
assert_eq!(both.output_track(), Some("both-out"));
assert_eq!(both.output_channels(), &[5, 6]);
}
#[test]
fn test_retrigger_getter() {
let def = SampleDefinition::new(
Some("test.wav".to_string()),
vec![1],
VelocityConfig::ignore(None),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Polyphonic,
None,
50,
);
assert_eq!(def.retrigger(), RetriggerBehavior::Polyphonic);
let def2 = SampleDefinition::new(
Some("test.wav".to_string()),
vec![1],
VelocityConfig::ignore(None),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
);
assert_eq!(def2.retrigger(), RetriggerBehavior::Cut);
}
#[test]
fn test_fade_time_ms_getter() {
let def = SampleDefinition::new(
Some("test.wav".to_string()),
vec![1],
VelocityConfig::ignore(None),
ReleaseBehavior::Fade,
RetriggerBehavior::Cut,
None,
200,
);
assert_eq!(def.fade_time_ms(), 200);
}
#[test]
fn test_velocity_layers_no_match_returns_none() {
let layers = vec![VelocityLayer::new([10, 50], "mid.wav".to_string())];
let def = SampleDefinition::new(
None,
vec![1],
VelocityConfig::with_layers(layers, false),
ReleaseBehavior::PlayToCompletion,
RetriggerBehavior::Cut,
None,
50,
);
assert!(def.file_for_velocity(5).is_none());
assert!(def.file_for_velocity(51).is_none());
}
#[test]
fn test_add_triggers_replaces_matching() {
let trigger1 = SampleTrigger::new(midi::note_on(1, 60, 127), "kick".to_string());
let trigger2 = SampleTrigger::new(midi::note_on(1, 61, 127), "snare".to_string());
let mut config = SamplesConfig::new(HashMap::new(), vec![trigger1], 32);
assert_eq!(config.sample_triggers().len(), 1);
config.add_triggers(vec![trigger2]);
assert_eq!(config.sample_triggers().len(), 2);
let trigger1_replacement =
SampleTrigger::new(midi::note_on(1, 60, 127), "kick_v2".to_string());
config.add_triggers(vec![trigger1_replacement]);
assert_eq!(config.sample_triggers().len(), 2);
let kick_trigger = config
.sample_triggers()
.iter()
.find(|t| t.sample() == "kick_v2");
assert!(kick_trigger.is_some());
}
#[test]
fn test_merge_triggers_dedup() {
let trigger1 = SampleTrigger::new(midi::note_on(1, 60, 127), "kick".to_string());
let trigger2 = SampleTrigger::new(midi::note_on(1, 61, 127), "snare".to_string());
let mut base = SamplesConfig::new(HashMap::new(), vec![trigger1.clone()], 32);
let override_trigger =
SampleTrigger::new(midi::note_on(1, 60, 127), "kick_override".to_string());
let other = SamplesConfig::new(HashMap::new(), vec![override_trigger, trigger2], 32);
base.merge(other);
assert_eq!(base.sample_triggers().len(), 2);
let kick = base
.sample_triggers()
.iter()
.find(|t| t.sample() == "kick_override");
assert!(kick.is_some(), "override trigger should be present");
let original_kick = base.sample_triggers().iter().find(|t| t.sample() == "kick");
assert!(original_kick.is_none(), "original should be replaced");
}
}