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.
140 lines
4.5 KiB
Rust
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,
|
|
})
|
|
}
|