Files
bass-foundry/plugins/crucible/core/src/export.rs
T
nvrl 6d868a63c4 feat(crucible): keep every captured take
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.
2026-11-06 12:24:10 +01:00

140 lines
4.5 KiB
Rust

//! Writes a take as ready-to-use files: the full loop, one file per slice and a wavetable.
use crate::settings::Settings;
use crate::take::{render, wavetable, Take, TABLE};
use common_dsp::{note_name, wav};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub struct Exported {
pub folder: PathBuf,
pub files: usize,
/// Why no wavetable was written although one was requested.
pub wavetable_skipped: Option<&'static str>,
}
/// UTC date and time for folder names, from the Unix time in seconds.
pub fn stamp(unix: u64) -> String {
let days = (unix / 86400) as i64;
let secs = unix % 86400;
// Civil-from-days (H. Hinnant), proleptic Gregorian calendar.
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + i64::from(month <= 2);
format!(
"{year:04}-{month:02}-{day:02}_{:02}{:02}{:02}",
secs / 3600,
secs / 60 % 60,
secs % 60
)
}
/// Writes a captured take's loop to `root/takes/` right away, so it survives a reload or a
/// crash. Never overwrites: a name already taken gets a counter. Returns the file written.
pub fn save_take(take: &Take, root: &Path, unix: u64) -> std::io::Result<PathBuf> {
let folder = root.join("takes");
std::fs::create_dir_all(&folder)?;
let base = format!(
"{}_{:.0}bpm_{}bar{}",
stamp(unix),
take.tempo(),
take.bars,
if take.bars == 1 { "" } else { "s" }
);
let wave = wav::Wave {
channels: 2,
rate: take.rate.round() as u32,
samples: take.audio.clone(),
root: None,
frame: None,
};
let bytes = wav::encode(&wave);
for n in 1..1000 {
let name = if n == 1 {
format!("{base}.wav")
} else {
format!("{base}_{n}.wav")
};
let path = folder.join(name);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut file) => {
std::io::Write::write_all(&mut file, &bytes)?;
return Ok(path);
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"too many takes with the same name",
))
}
pub fn export(
take: &Take,
settings: &Settings,
selected: Option<usize>,
root: &Path,
unix: u64,
) -> std::io::Result<Exported> {
let folder = root.join(format!("{}_{:.0}bpm", stamp(unix), take.tempo()));
std::fs::create_dir_all(&folder)?;
let rate = take.rate.round() as u32;
let stereo = |samples: Vec<f32>, root: Option<u8>| wav::Wave {
channels: 2,
rate,
samples,
root,
frame: None,
};
wav::write(&folder.join("loop.wav"), &stereo(take.audio.clone(), None))?;
let mut files = 1;
for (i, s) in take.slices.iter().enumerate().filter(|(_, s)| s.enabled) {
let name = match s.root {
Some(n) => format!("s{:02}_{}.wav", i + 1, note_name(n)),
None => format!("s{:02}.wav", i + 1),
};
wav::write(
&folder.join(name),
&stereo(render(take, s, settings), s.root),
)?;
files += 1;
}
let mut wavetable_skipped = None;
if settings.wavetable {
let source = selected
.and_then(|i| take.slices.get(i))
.or_else(|| take.slices.iter().find(|s| s.enabled && s.root.is_some()));
match source.map(|s| wavetable(&take.mono(s.start, s.end), take.rate)) {
Some(Some(table)) => {
let w = wav::Wave {
channels: 1,
rate,
samples: table,
root: None,
frame: Some(TABLE as u32),
};
wav::write(&folder.join("wavetable.wav"), &w)?;
files += 1;
}
Some(None) => wavetable_skipped = Some("the slice has no clear pitch"),
None => wavetable_skipped = Some("no slice selected"),
}
}
Ok(Exported {
folder,
files,
wavetable_skipped,
})
}