mod node;
#[cfg(test)]
mod tests;
mod tracks;
mod validate;
pub use node::Node;
pub use tracks::{AutoLane, AutoPoint, AutoTarget, Track};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 2;
pub const ENGINE_VERSION: u32 = 4;
fn default_sample_rate() -> u32 {
44_100
}
fn default_duration() -> f32 {
0.3
}
fn default_gain() -> f32 {
1.0
}
fn default_haas_ms() -> f32 {
12.0
}
fn default_wide_amount() -> f32 {
0.6
}
fn default_ceiling_dbtp() -> f32 {
-1.0
}
fn default_crossfade() -> f32 {
0.1
}
fn default_mode_decay() -> f32 {
0.4
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SoundDoc {
pub name: String,
#[serde(default = "default_duration")]
pub duration: f32,
#[serde(default = "default_sample_rate")]
pub sample_rate: u32,
#[serde(default)]
pub seed: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<u32>,
#[serde(default)]
pub stereo: Stereo,
#[serde(default)]
pub normalize: Option<Normalize>,
#[serde(default)]
pub playback: Playback,
pub root: Node,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "mode", rename_all = "lowercase")]
pub enum Playback {
#[default]
OneShot,
#[serde(rename = "loop")]
Loop {
#[serde(default)]
start_secs: f32,
#[serde(default)]
end_secs: Option<f32>,
#[serde(default = "default_crossfade")]
crossfade_secs: f32,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct Normalize {
#[serde(default)]
pub target_lufs: Option<f32>,
#[serde(default = "default_ceiling_dbtp")]
pub ceiling_dbtp: f32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "mode", rename_all = "lowercase")]
pub enum Stereo {
#[default]
Mono,
Haas {
#[serde(default = "default_haas_ms")]
ms: f32,
#[serde(default)]
pan: f32,
},
Wide {
#[serde(default = "default_wide_amount")]
amount: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Value {
Const(f32),
Note(String),
Modulated(Modulator),
}
pub fn note_to_hz(s: &str) -> Option<f32> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(num) = s
.strip_prefix("midi:")
.or_else(|| s.strip_prefix(['m', 'M']))
&& let Ok(n) = num.trim().parse::<f32>()
{
return Some(midi_to_hz(n));
}
let mut chars = s.chars().peekable();
let mut semis: i32 = match chars.next()?.to_ascii_uppercase() {
'C' => 0,
'D' => 2,
'E' => 4,
'F' => 5,
'G' => 7,
'A' => 9,
'B' => 11,
_ => return None,
};
loop {
match chars.peek() {
Some('#') => semis += 1,
Some('b') => semis -= 1,
_ => break,
}
chars.next();
}
let rest: String = chars.collect();
let octave: i32 = if rest.is_empty() {
4
} else {
rest.parse().ok()?
};
Some(midi_to_hz(((octave + 1) * 12 + semis) as f32))
}
fn midi_to_hz(m: f32) -> f32 {
440.0 * 2f32.powf((m - 69.0) / 12.0)
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Curve {
#[default]
Lin,
Exp,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Shape {
#[default]
Sine,
Square,
Triangle,
Saw,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum Modulator {
#[serde(rename = "slide")]
Slide {
from: f32,
to: f32,
secs: f32,
#[serde(default)]
curve: Curve,
},
#[serde(rename = "lfo")]
Lfo {
#[serde(default)]
shape: Shape,
rate: f32,
depth: f32,
center: f32,
},
#[serde(rename = "arp")]
Arp {
steps: Vec<f32>,
rate: f32,
},
#[serde(rename = "env")]
EnvMod {
#[serde(flatten)]
adsr: Adsr,
from: f32,
to: f32,
},
#[serde(rename = "rand")]
Rand {
from: f32,
to: f32,
rate: f32,
#[serde(default)]
seed: u64,
},
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DriveShape {
#[default]
Tanh,
Hard,
Fold,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NoiseColor {
#[default]
White,
Pink,
Brown,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SuperWave {
#[default]
Sawtooth,
Square,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SeqWave {
#[default]
Square,
Triangle,
Sawtooth,
Sine,
Noise,
Fm,
Pluck,
Piano,
Epiano,
Organ,
Strings,
Bass,
Kit,
Cowbell,
Sampler,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum KitStyle {
#[default]
Classic,
Acoustic,
Electronic,
#[serde(rename = "808")]
Eight08,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Adsr {
#[serde(default)]
pub a: f32,
#[serde(default)]
pub d: f32,
#[serde(default)]
pub s: f32,
#[serde(default)]
pub r: f32,
#[serde(default)]
pub punch: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Mode {
pub freq: f32,
#[serde(default = "default_mode_decay")]
pub decay: f32,
#[serde(default = "default_gain")]
pub gain: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SeqNote {
pub step: u32,
pub len: u32,
pub pitch: Value,
#[serde(default = "default_gain")]
pub gain: f32,
}
impl SoundDoc {
pub fn effective_version(&self) -> u32 {
self.version.unwrap_or(1)
}
pub fn effective_engine(&self) -> u32 {
self.engine.unwrap_or(0)
}
pub fn ensure_track_ids(&mut self) -> bool {
let Node::Tracks { tracks, .. } = &mut self.root else {
return false;
};
let used: std::collections::HashSet<String> =
tracks.iter().filter_map(|t| t.id.clone()).collect();
let mut changed = false;
for (i, t) in tracks.iter_mut().enumerate() {
if t.id.is_none() {
let mut id = format!("layer_{i}");
let mut n = 2;
while used.contains(&id) {
id = format!("layer_{i}_{n}");
n += 1;
}
t.id = Some(id);
changed = true;
}
}
changed
}
}