With Keep takes on (default), each capture is written to <folder>/takes/ as a WAV named by time, tempo and length, so a take is not lost when the next capture replaces it. Files are never overwritten: a clash gets a numbered suffix. Saving runs off the UI thread and the status line shows the path or the error.
124 lines
3.5 KiB
Rust
124 lines
3.5 KiB
Rust
//! Persisted editor choices. Takes are not part of plugin state; exported files are.
|
||
use serde::{Deserialize, Serialize};
|
||
use std::path::PathBuf;
|
||
|
||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||
pub enum Mode {
|
||
/// Cut on the bar/beat grid.
|
||
#[default]
|
||
Grid,
|
||
/// Cut where a new hit starts.
|
||
Transient,
|
||
}
|
||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||
pub enum Division {
|
||
Quarter,
|
||
Eighth,
|
||
#[default]
|
||
Sixteenth,
|
||
EighthTriplet,
|
||
SixteenthTriplet,
|
||
}
|
||
impl Division {
|
||
pub const ALL: [Division; 5] = [
|
||
Division::Quarter,
|
||
Division::Eighth,
|
||
Division::Sixteenth,
|
||
Division::EighthTriplet,
|
||
Division::SixteenthTriplet,
|
||
];
|
||
/// Slices per quarter note.
|
||
pub fn per_quarter(self) -> u32 {
|
||
match self {
|
||
Division::Quarter => 1,
|
||
Division::Eighth => 2,
|
||
Division::Sixteenth => 4,
|
||
Division::EighthTriplet => 3,
|
||
Division::SixteenthTriplet => 6,
|
||
}
|
||
}
|
||
pub fn label(self) -> &'static str {
|
||
match self {
|
||
Division::Quarter => "1/4",
|
||
Division::Eighth => "1/8",
|
||
Division::Sixteenth => "1/16",
|
||
Division::EighthTriplet => "1/8T",
|
||
Division::SixteenthTriplet => "1/16T",
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||
#[serde(default)]
|
||
pub struct Settings {
|
||
pub bars: u32,
|
||
pub mode: Mode,
|
||
pub division: Division,
|
||
/// Transient sensitivity, 0–100 %.
|
||
pub sensitivity: f32,
|
||
/// Fade length at both slice edges in milliseconds.
|
||
pub declick: f32,
|
||
/// Peak-normalise each slice to −0.3 dBFS.
|
||
pub normalize: bool,
|
||
/// Remove leading silence below −60 dBFS.
|
||
pub trim: bool,
|
||
/// Also write a wavetable from the selected slice.
|
||
pub wavetable: bool,
|
||
/// Save every captured take's loop to the `takes` subfolder at once.
|
||
pub keep_takes: bool,
|
||
/// Export folder; empty selects the default.
|
||
pub folder: String,
|
||
}
|
||
impl Default for Settings {
|
||
fn default() -> Self {
|
||
Self {
|
||
bars: 4,
|
||
mode: Mode::Grid,
|
||
division: Division::Sixteenth,
|
||
sensitivity: 50.,
|
||
declick: 2.,
|
||
normalize: true,
|
||
trim: true,
|
||
wavetable: false,
|
||
keep_takes: true,
|
||
folder: String::new(),
|
||
}
|
||
}
|
||
}
|
||
impl Settings {
|
||
pub const BARS: [u32; 5] = [1, 2, 4, 8, 16];
|
||
/// Clamps values from old or hand-edited state into their ranges.
|
||
pub fn sanitize(&mut self) {
|
||
if !Self::BARS.contains(&self.bars) {
|
||
self.bars = 4;
|
||
}
|
||
self.sensitivity = if self.sensitivity.is_finite() {
|
||
self.sensitivity.clamp(0., 100.)
|
||
} else {
|
||
50.
|
||
};
|
||
self.declick = if self.declick.is_finite() {
|
||
self.declick.clamp(0., 20.)
|
||
} else {
|
||
2.
|
||
};
|
||
}
|
||
pub fn folder(&self) -> PathBuf {
|
||
if !self.folder.trim().is_empty() {
|
||
return PathBuf::from(self.folder.trim());
|
||
}
|
||
default_folder()
|
||
}
|
||
}
|
||
pub fn default_folder() -> PathBuf {
|
||
if cfg!(windows) {
|
||
if let Some(home) = std::env::var_os("USERPROFILE") {
|
||
return PathBuf::from(home).join("Documents").join("Crucible");
|
||
}
|
||
}
|
||
match std::env::var_os("HOME") {
|
||
Some(home) => PathBuf::from(home).join("Crucible"),
|
||
None => PathBuf::from("Crucible"),
|
||
}
|
||
}
|