use std::fmt::Write as _;
use std::path::PathBuf;
use playr_looper::{Window, VOICES, WRITER};
use super::{
model_name, Division, FxState, Layout, Source, TapeState, DELAY_MODELS, REVERB_MODELS,
};
const LONGEST: usize = 3600;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Reads {
#[default]
Empty,
Write,
File(Source),
}
#[derive(Debug, Clone, PartialEq)]
pub struct SavedTape {
pub(super) rate: u32,
pub(super) state: TapeState,
pub(super) reads: [Reads; VOICES],
pub(super) shape: Option<(usize, Window, u16)>,
pub(super) layout: Layout,
}
impl SavedTape {
pub fn files(&self) -> usize {
self.reads
.iter()
.filter(|r| matches!(r, Reads::File(_)))
.count()
}
pub fn state(&self) -> &TapeState {
&self.state
}
pub fn reads(&self) -> &[Reads; VOICES] {
&self.reads
}
pub fn playing(&self) -> bool {
self.state.playing
}
pub fn encode(&self) -> String {
let s = &self.state;
let mut text = format!("rate {}\nplaying {}\n", self.rate, bit(s.playing));
let w = s.write_window;
let _ = writeln!(
text,
"write {} {} {} {} {}",
s.feedback, s.wear, s.thin, w.start, w.end
);
if let Some((frames, range, channels)) = self.shape {
let _ = writeln!(
text,
"shape {frames} {} {} {channels}",
range.start, range.end
);
}
for (i, v) in s.voices.iter().enumerate() {
let _ = writeln!(
text,
"voice {i} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
bit(v.on),
v.rate,
v.window.start,
v.window.end,
v.level,
v.pan,
v.send,
v.wear,
v.fade,
bit(v.ping),
v.slew,
v.drive,
v.high_pass,
v.low_pass,
v.bell_hz,
v.bell_gain,
v.bell_q,
bit(v.solo),
bit(v.mute),
v.delay,
v.reverb,
);
}
let fx = &s.fx;
let _ = writeln!(
text,
"fx {} {} {} {} {} {} {} {} {} {} {} {} {}",
model_name(&DELAY_MODELS, fx.delay_model),
fx.time,
fx.sync.map_or("-", Division::name),
fx.feedback,
fx.tone,
fx.wow,
fx.delay_return,
model_name(&REVERB_MODELS, fx.reverb_model),
fx.size,
fx.decay,
fx.damp,
fx.reverb_return,
fx.print,
);
let folded = self
.layout
.folded
.map(|f| f.map_or("-", |f| if f { "1" } else { "0" }));
let drawer = self.layout.drawer.map(|d| bit(d).to_string());
let _ = writeln!(
text,
"lanes {} {} {}",
folded.join(" "),
drawer.join(" "),
bit(self.layout.fx)
);
for (i, r) in self.reads.iter().enumerate() {
let _ = match r {
Reads::Empty => writeln!(text, "reads {i} empty"),
Reads::Write => writeln!(text, "reads {i} write"),
Reads::File(src) => writeln!(
text,
"reads {i} file {} {} {} {} {} {}",
bit(src.circular),
src.rate,
src.span.0,
src.span.1,
src.beats.map_or("-".into(), |b| b.to_string()),
src.path.to_string_lossy(),
),
};
}
text
}
pub fn decode(text: &str) -> Option<SavedTape> {
let mut rate = None;
let mut state = TapeState::new(Window::new(0, 0));
state.voices[0].on = false;
let mut reads: [Reads; VOICES] = Default::default();
let mut shape = None;
let mut layout = Layout::default();
for line in text.lines() {
let words: Vec<&str> = line.split(' ').collect();
match words.as_slice() {
["rate", r] => rate = r.parse::<u32>().ok().filter(|r| *r > 0),
["playing", b] => state.playing = flag(b).unwrap_or(false),
["write", fb, wear, thin, a, b] => {
if let (Some(fb), Some(wear), Some(thin), Some(w)) =
(num(fb), num(wear), num(thin), window(a, b))
{
(state.feedback, state.wear, state.thin) = (fb, wear, thin);
state.write_window = w;
}
}
["shape", frames, a, b, c] => {
let frames = frames.parse::<usize>().ok();
let channels = c.parse::<u16>().ok().filter(|c| (1..=2).contains(c));
if let (Some(f), Some(w), Some(c)) = (frames, window(a, b), channels) {
shape = (w.end <= f).then_some((f, w, c));
}
}
["voice", i, rest @ ..] => {
if let (Some(i), Some(v)) = (index(i), voice(rest)) {
state.voices[i] = v;
}
}
["fx", rest @ ..] => {
if let Some(fx) = fx(rest) {
state.fx = fx;
}
}
["lanes", rest @ ..] if [2 * VOICES, 2 * VOICES + 1].contains(&rest.len()) => {
let folded: Option<Vec<Option<bool>>> = rest[..VOICES]
.iter()
.map(|w| match *w {
"-" => Some(None),
w => flag(w).map(Some),
})
.collect();
let drawer: Option<Vec<bool>> =
rest[VOICES..2 * VOICES].iter().map(|w| flag(w)).collect();
let fx = rest.get(2 * VOICES).map_or(Some(false), |w| flag(w));
if let (Some(f), Some(d), Some(x)) = (folded, drawer, fx) {
layout.folded.copy_from_slice(&f);
layout.drawer.copy_from_slice(&d);
layout.fx = x;
}
}
["reads", i, "empty"] => {
if let Some(i) = index(i) {
reads[i] = Reads::Empty;
}
}
["reads", i, "write"] => {
if let Some(i) = index(i) {
reads[i] = Reads::Write;
}
}
["reads", i, "file", circular, r, a, b, beats, path @ ..] => {
let path = path.join(" ");
let src = (|| {
let (start, end) = (a.parse::<u64>().ok()?, b.parse::<u64>().ok()?);
let beats = match *beats {
"-" => None,
b => Some(b.parse::<u32>().ok()?),
};
let path = PathBuf::from(path);
if start >= end || path.as_os_str().is_empty() {
return None;
}
Some(Source {
name: playr_core::samples::name_for(&path),
path,
rate: r.parse::<u32>().ok().filter(|r| *r > 0)?,
span: (start, end),
circular: flag(circular)?,
voice: None,
beats,
})
})();
if let (Some(i), Some(src)) = (index(i), src) {
reads[i] = Reads::File(src);
}
}
_ => {}
}
}
let rate = rate?;
let shape = shape.filter(|(f, ..)| *f <= LONGEST * rate as usize);
if matches!(reads[WRITER], Reads::Write) && shape.is_none() {
reads[WRITER] = Reads::Empty;
}
let write = !matches!(reads[WRITER], Reads::Empty);
for (i, r) in reads.iter_mut().enumerate() {
if i != WRITER && *r == Reads::Write && !write {
*r = Reads::Empty;
}
}
for (i, r) in reads.iter().enumerate() {
state.own[i] = i != WRITER && matches!(r, Reads::File(_));
state.empty[i] = *r == Reads::Empty;
}
state.write = false;
let shape = shape.filter(|_| reads[WRITER] == Reads::Write);
let saved = SavedTape {
rate,
state,
reads,
shape,
layout,
};
(saved.files() > 0 || saved.shape.is_some()).then_some(saved)
}
}
fn bit(b: bool) -> u8 {
u8::from(b)
}
fn flag(word: &str) -> Option<bool> {
match word {
"0" => Some(false),
"1" => Some(true),
_ => None,
}
}
fn num(word: &str) -> Option<f32> {
word.parse::<f32>().ok().filter(|v| v.is_finite())
}
fn index(word: &str) -> Option<usize> {
word.parse::<usize>().ok().filter(|i| *i < VOICES)
}
fn window(a: &str, b: &str) -> Option<Window> {
let (a, b) = (a.parse::<usize>().ok()?, b.parse::<usize>().ok()?);
(a <= b).then(|| Window::new(a, b))
}
fn voice(words: &[&str]) -> Option<super::VoiceState> {
let (words, sends) = match words.len() {
19 => (words, ["0", "0"]),
21 => (&words[..19], [words[19], words[20]]),
_ => return None,
};
let [on, rate, a, b, level, pan, send, wear, fade, ping, slew, drive, hp, lp, bell, gain, q, solo, mute] =
words
else {
return None;
};
let [delay, reverb] = sends;
Some(super::VoiceState {
on: flag(on)?,
rate: num(rate)?,
window: window(a, b)?,
level: num(level)?,
pan: num(pan)?,
send: num(send)?,
wear: num(wear)?,
fade: num(fade)?,
ping: flag(ping)?,
slew: num(slew)?,
drive: num(drive)?,
high_pass: num(hp)?,
low_pass: num(lp)?,
bell_hz: num(bell)?,
bell_gain: num(gain)?,
bell_q: num(q)?,
solo: flag(solo)?,
mute: flag(mute)?,
delay: num(delay)?,
reverb: num(reverb)?,
})
}
fn named<M: Copy>(table: &[(&str, M)], word: &str) -> Option<M> {
table.iter().find(|m| m.0 == word).map(|m| m.1)
}
fn fx(words: &[&str]) -> Option<FxState> {
let [dm, time, sync, fb, tone, wow, dr, rm, size, decay, damp, rr, print] = words else {
return None;
};
Some(FxState {
delay_model: named(&DELAY_MODELS, dm)?,
time: num(time)?,
sync: match *sync {
"-" => None,
w => Some(*Division::ALL.iter().find(|d| d.name() == w)?),
},
feedback: num(fb)?,
tone: num(tone)?,
wow: num(wow)?,
delay_return: num(dr)?,
reverb_model: named(&REVERB_MODELS, rm)?,
size: num(size)?,
decay: num(decay)?,
damp: num(damp)?,
reverb_return: num(rr)?,
print: num(print)?,
})
}