use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
use std::time::Duration;
use playr_core::audio::output::DeviceEvent;
use playr_core::audio::resample::Resample;
use playr_core::audio::{Attachment, Sources, State};
use playr_core::samples;
pub use playr_looper::Filter;
use playr_looper::{Handle, Loop, Looper, Returned, Setting, Window};
use crate::dispatch::Frontend;
use crate::message::Message;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Pos {
Percent(f32),
Time(Duration),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VoiceSetting {
On(bool),
Rate(f32),
Window(Pos, Pos),
Level(f32),
Pan(f32),
Send(f32),
Wear(f32),
Fade(f32),
Ping(bool),
Slew(f32),
Drive(f32),
Cutoff(f32),
Filter(Filter),
Solo(bool),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TapeAction {
Load(Option<u8>),
Take,
Play,
Stop,
Reset,
Save,
Record,
Voice(u8, VoiceSetting),
Write(bool),
WriteWindow(Pos, Pos),
Feedback(f32),
Wear(f32),
Thin(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum TapeMessage {
Loading,
Loaded {
frames: usize,
rate: u32,
},
Done(TapeAction),
NoTape,
NoRange,
EmptySlot(u8),
EmptyWindow,
AlreadySaving,
SaveAborted,
DeviceLost(String),
Took,
TakeWaiting,
PastRange,
TakeDropped,
Saving,
Saved(PathBuf),
Recording(PathBuf),
Recorded {
path: PathBuf,
frames: u64,
rate: u32,
dropped: u64,
},
Failed(String),
}
impl From<TapeMessage> for Message {
fn from(m: TapeMessage) -> Self {
Message::Tape(m)
}
}
enum Output {
Bus(Sources),
Manual,
}
struct Played(Looper);
impl playr_core::audio::bus::Source for Played {
fn process(&mut self, main: &mut [f32], _cue: &mut [f32]) -> playr_core::audio::bus::Cue {
self.0.process(main);
playr_core::audio::bus::Cue::None
}
}
struct Loaded {
lp: Loop,
rate: u32,
}
struct Loading {
result: Receiver<Result<Loaded, String>>,
name: String,
}
struct Live {
handle: Handle,
attached: Option<Attachment>,
looper: Option<Looper>,
name: String,
extent: Extent,
save_to: Option<PathBuf>,
state: TapeState,
volume: f32,
}
const ROLL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Extent {
pub frames: usize,
pub range: Window,
pub rate: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VoiceState {
pub on: bool,
pub rate: f32,
pub window: Window,
pub level: f32,
pub pan: f32,
pub send: f32,
pub wear: f32,
pub fade: f32,
pub ping: bool,
pub slew: f32,
pub drive: f32,
pub cutoff: f32,
pub filter: Filter,
pub solo: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TapeState {
pub voices: [VoiceState; playr_looper::VOICES],
pub playing: bool,
pub write: bool,
pub write_window: Window,
pub feedback: f32,
pub wear: f32,
pub thin: f32,
}
impl TapeState {
pub fn new(range: Window) -> Self {
let whole = range;
let voice = |on| VoiceState {
on,
rate: 1.0,
window: whole,
level: 1.0,
pan: 0.0,
send: 0.0,
wear: 0.0,
fade: playr_looper::DEFAULT_FADE_MS,
ping: false,
slew: playr_looper::DEFAULT_SLEW_MS,
drive: 0.0,
cutoff: 1.0,
filter: Filter::Low,
solo: false,
};
TapeState {
voices: [voice(true), voice(false), voice(false)],
playing: false,
write: false,
write_window: whole,
feedback: 1.0,
wear: 0.0,
thin: 0.0,
}
}
fn apply(&mut self, s: Setting) {
match s {
Setting::Play => self.playing = true,
Setting::Stop => self.playing = false,
Setting::Write(on) => self.write = on,
Setting::WriteWindow(w) => self.write_window = w,
Setting::Feedback(x) => self.feedback = x,
Setting::Wear(x) => self.wear = x,
Setting::On(i, on) => self.voices[i].on = on,
Setting::Rate(i, x) => self.voices[i].rate = x,
Setting::Window(i, w) => self.voices[i].window = w,
Setting::Level(i, x) => self.voices[i].level = x,
Setting::Pan(i, x) => self.voices[i].pan = x,
Setting::Send(i, x) => self.voices[i].send = x,
Setting::Fade(i, x) => self.voices[i].fade = x,
Setting::VoiceWear(i, x) => self.voices[i].wear = x,
Setting::Ping(i, on) => self.voices[i].ping = on,
Setting::Slew(i, x) => self.voices[i].slew = x,
Setting::Drive(i, x) => self.voices[i].drive = x,
Setting::Cutoff(i, x) => self.voices[i].cutoff = x,
Setting::Filter(i, f) => self.voices[i].filter = f,
Setting::Solo(i, on) => self.voices[i].solo = on,
Setting::Thin(x) => self.thin = x,
Setting::Knee(_) | Setting::Head(..) => {}
}
}
}
enum Found {
Note(TapeMessage),
Wrote(TapeMessage, PathBuf),
}
type Saved = Result<PathBuf, String>;
pub struct Deck {
knee: f32,
output: Output,
live: Option<Live>,
loading: Option<Loading>,
saved: (Sender<Saved>, Receiver<Saved>),
errors: (Sender<DeviceEvent>, Receiver<DeviceEvent>),
volume: f32,
taking: Option<Taking>,
}
#[derive(Debug, Clone)]
struct Taking {
path: PathBuf,
rate: u32,
span: (u64, u64),
loaded: bool,
waiting: bool,
}
impl Deck {
pub fn new(sources: Sources, knee: f32) -> Self {
Deck::with(Output::Bus(sources), knee)
}
pub fn manual() -> Self {
Deck::with(Output::Manual, playr_looper::DEFAULT_KNEE)
}
fn with(output: Output, knee: f32) -> Self {
Deck {
knee,
output,
live: None,
loading: None,
saved: channel(),
errors: channel(),
volume: 1.0,
taking: None,
}
}
fn follow_volume(&mut self, volume: f32) {
self.volume = volume;
if let Some(live) = self.live.as_mut().filter(|l| l.volume != volume) {
if live.handle.set_volume(volume).is_ok() {
live.volume = volume;
}
}
}
pub fn loading(&self) -> bool {
self.loading.is_some()
}
pub fn loaded(&self) -> Option<Extent> {
self.live.as_ref().map(|l| l.extent)
}
pub fn state(&self) -> Option<&TapeState> {
self.live.as_ref().map(|l| &l.state)
}
pub fn status(&self) -> Option<&playr_looper::Status> {
self.live.as_ref().map(|l| &**l.handle.status())
}
pub fn recording(&self) -> bool {
self.live.as_ref().is_some_and(|l| l.handle.recording())
}
pub fn saving(&self) -> bool {
self.live.as_ref().is_some_and(|l| l.save_to.is_some())
}
pub fn process(&mut self, out: &mut [f32]) {
if let Some(looper) = self.live.as_mut().and_then(|l| l.looper.as_mut()) {
looper.process(out);
}
}
fn load(&mut self, path: PathBuf, rate: u32, (start, end): (u64, u64)) -> Result<(), String> {
let to = match &self.output {
Output::Bus(sources) => sources.rate_for(rate),
Output::Manual => rate,
};
let (tx, rx) = channel();
let name = samples::name_for(&path);
std::thread::spawn(move || _ = tx.send(read(&path, rate, (start, end), to)));
self.loading = Some(Loading { result: rx, name });
Ok(())
}
fn poll(&mut self) -> Vec<Found> {
let mut found = Vec::new();
let done = match self.loading.as_ref().map(|l| l.result.try_recv()) {
Some(Ok(result)) => Some(result),
Some(Err(TryRecvError::Disconnected)) => Some(Err("the read stopped".into())),
Some(Err(TryRecvError::Empty)) | None => None,
};
if let Some(result) = done {
let loading = self.loading.take().expect("polled above");
if let Some(f) = self.live.as_mut().and_then(stop_recording) {
found.push(f);
}
let started = result.and_then(|l| self.start(loading, l));
match (&started, &mut self.taking) {
(Ok(_), Some(t)) => t.loaded = true,
(Err(_), _) => self.taking = None,
_ => {}
}
found.push(Found::Note(started.unwrap_or_else(TapeMessage::Failed)));
}
if let Some(live) = &mut self.live {
while let Some(r) = live.handle.poll() {
match (r, live.save_to.take()) {
(Returned::Snapshot(s), Some(dir)) => {
let to = (live.name.clone(), live.extent, live.handle.channels());
let tx = self.saved.0.clone();
std::thread::spawn(move || _ = tx.send(save(&dir, to, &s)));
found.push(Found::Note(TapeMessage::Saving));
}
(Returned::Aborted(_), Some(_)) => {
found.push(Found::Note(TapeMessage::SaveAborted));
}
(_, dir) => live.save_to = dir,
}
}
}
while let Ok(r) = self.saved.1.try_recv() {
found.push(match r {
Ok(path) => Found::Wrote(TapeMessage::Saved(path.clone()), path),
Err(e) => Found::Note(TapeMessage::Failed(e)),
});
}
while let Ok(e) = self.errors.1.try_recv() {
match e {
DeviceEvent::Rerouted => {}
DeviceEvent::Error(e) => found.push(Found::Note(TapeMessage::Failed(e))),
DeviceEvent::Lost(e) => {
if let Some(f) = self.live.as_mut().and_then(stop_recording) {
found.push(f);
}
self.live = None;
found.push(Found::Note(TapeMessage::DeviceLost(e)));
}
}
}
let lost = |l: &Live| l.attached.as_ref().is_some_and(Attachment::lost);
if self.live.as_ref().is_some_and(lost) {
if let Some(f) = self.live.as_mut().and_then(stop_recording) {
found.push(f);
}
self.live = None;
found.push(Found::Note(TapeMessage::DeviceLost(
"the output device went away".into(),
)));
}
found
}
pub fn device_events(&self) -> Sender<DeviceEvent> {
self.errors.0.clone()
}
fn start(&mut self, loading: Loading, loaded: Loaded) -> Result<TapeMessage, String> {
let extent = Extent {
frames: loaded.lp.frames(),
range: loaded.lp.range(),
rate: loaded.rate,
};
let (looper, mut handle) = playr_looper::new(extent.rate);
handle.load(loaded.lp).map_err(|e| e.to_string())?;
handle
.set(Setting::Knee(self.knee))
.map_err(|e| e.to_string())?;
handle.set_volume(self.volume).map_err(|e| e.to_string())?;
let (attached, looper) = match &self.output {
Output::Bus(sources) => (
Some(sources.attach(extent.rate, Box::new(Played(looper)))),
None,
),
Output::Manual => (None, Some(looper)),
};
self.live = Some(Live {
handle,
attached,
looper,
name: loading.name,
extent,
save_to: None,
state: TapeState::new(extent.range),
volume: self.volume,
});
Ok(TapeMessage::Loaded {
frames: extent.range.len(),
rate: extent.rate,
})
}
}
fn read(path: &Path, rate: u32, (start, end): (u64, u64), to: u32) -> Result<Loaded, String> {
let roll = (ROLL.as_secs_f64() * f64::from(rate)) as u64;
let before = roll.min(start);
let (mut audio, channels) = samples::read_frames(path, rate, start - before, end + roll)?;
if channels > 2 {
audio = audio
.chunks_exact(channels)
.flat_map(|f| [f[0], f[1]])
.collect();
}
let channels = channels.min(2) as u16;
if to != rate {
let mut r = Resample::new(rate, to, channels, 1.0)
.ok_or_else(|| format!("cannot resample {rate} Hz to {to} Hz"))?;
let mut out = Vec::with_capacity(audio.len() * to as usize / rate as usize + 64);
r.push(&audio, &mut out);
r.flush(&mut out);
audio = out;
}
let frames = audio.len() / usize::from(channels);
let at = |f: u64| ((f as f64 * f64::from(to) / f64::from(rate)).round() as usize).min(frames);
let range = Window::new(at(before), at(before + end - start));
let lp = Loop::new(audio, channels)
.and_then(|lp| lp.with_range(range))
.map_err(|e| e.to_string())?;
Ok(Loaded { lp, rate: to })
}
fn new_file(samples: &Path, name: &str, what: &str) -> Result<PathBuf, String> {
std::fs::create_dir_all(samples)
.map_err(|e| format!("cannot create {}: {e}", samples.display()))?;
let dir = samples::unused_dir(samples, &format!("{name}-tape"))?;
let stem = dir.file_name().unwrap_or_default().to_string_lossy();
Ok(dir.join(format!("{stem}-{what}.wav")))
}
fn save(
samples: &Path,
(name, extent, channels): (String, Extent, Option<u16>),
audio: &[f32],
) -> Result<PathBuf, String> {
let path = new_file(samples, &name, "loop")?;
let c = usize::from(channels.unwrap_or(1));
let range = &audio[extent.range.start * c..extent.range.end * c];
playr_looper::write_wav(&path, range, c as u16, extent.rate)
.map_err(|e| format!("{}: {e}", path.display()))?;
Ok(path)
}
fn stop_recording(live: &mut Live) -> Option<Found> {
if !live.handle.recording() {
return None;
}
Some(match live.handle.stop_recording() {
Ok(r) => Found::Wrote(
TapeMessage::Recorded {
path: r.path.clone(),
frames: r.frames,
rate: live.extent.rate,
dropped: r.dropped,
},
r.path,
),
Err(e) => Found::Note(TapeMessage::Failed(e.to_string())),
})
}
pub fn poll(f: &mut impl Frontend) {
for found in f.tape().poll() {
report(f, found);
}
take_over(f);
let volume = f.mix().gain(crate::mix::Strip::Tape);
f.tape().follow_volume(volume);
}
fn report(f: &mut impl Frontend, found: Found) {
match found {
Found::Note(m) => f.notify(m.into()),
Found::Wrote(m, path) => match f.session_mut().add_to_library(&path) {
Ok(()) => f.notify(m.into()),
Err(e) => f.notify(TapeMessage::Failed(e).into()),
},
}
}
pub fn act(f: &mut impl Frontend, action: TapeAction) {
match action {
TapeAction::Load(slot) => return load(f, slot),
TapeAction::Take => return take(f),
TapeAction::Record => return record(f),
_ => {}
}
let samples = f.session().samples_dir().to_path_buf();
let playing = f.session().player().status().state == State::Playing;
let sent = match f.tape().live.as_mut() {
Some(live) => send(live, action, samples),
None => Err(TapeMessage::NoTape),
};
if let Err(m) = sent {
return f.notify(m.into());
}
if action == TapeAction::Play && playing {
f.session().send(playr_core::audio::Cmd::TogglePause);
}
if action != TapeAction::Save {
f.notify(TapeMessage::Done(action).into());
}
}
fn send(live: &mut Live, action: TapeAction, samples: PathBuf) -> Result<(), TapeMessage> {
let failed = |e: playr_looper::Error| TapeMessage::Failed(e.to_string());
match action {
TapeAction::Reset => live.handle.reset().map_err(failed),
TapeAction::Save if live.save_to.is_some() => Err(TapeMessage::AlreadySaving),
TapeAction::Save => {
live.handle.snapshot().map_err(failed)?;
live.save_to = Some(samples);
Ok(())
}
_ => settings(action, live.extent)?
.into_iter()
.try_for_each(|s| {
live.handle.set(s)?;
live.state.apply(s);
Ok(())
})
.map_err(failed),
}
}
fn load(f: &mut impl Frontend, slot: Option<u8>) {
if let Some((path, rate, span)) = range(f, slot) {
f.tape().taking = None;
start_load(f, path, rate, span);
}
}
fn take(f: &mut impl Frontend) {
if let Some((path, rate, span)) = range(f, None) {
f.tape().taking = Some(Taking {
path: path.clone(),
rate,
span,
loaded: false,
waiting: false,
});
start_load(f, path, rate, span);
}
}
fn range(f: &mut impl Frontend, slot: Option<u8>) -> Option<(PathBuf, u32, (u64, u64))> {
let (path, rate) = match f.session().playing_track() {
Ok(track) => track,
Err(refusal) => {
f.notify(refusal.into());
return None;
}
};
let span = match slot {
Some(n) => f.session_mut().loops_for(Some(&path))[usize::from(n) - 1],
None => f.sampler().range(Some(&path)),
};
match (span, slot) {
(Some(span), _) => Some((path, rate, span)),
(None, Some(n)) => {
f.notify(TapeMessage::EmptySlot(n).into());
None
}
(None, None) => {
f.notify(TapeMessage::NoRange.into());
None
}
}
}
fn start_load(f: &mut impl Frontend, path: PathBuf, rate: u32, span: (u64, u64)) {
match f.tape().load(path, rate, span) {
Ok(()) => f.notify(TapeMessage::Loading.into()),
Err(e) => {
f.tape().taking = None;
f.notify(TapeMessage::Failed(e).into());
}
}
}
fn take_over(f: &mut impl Frontend) {
let Some(t) = f.tape().taking.clone().filter(|t| t.loaded) else {
return;
};
let player = f.session().player();
let state = player.status().state;
let on_track = f.session().playing_track().is_ok_and(|(p, _)| p == t.path);
if !on_track || state == State::Stopped {
f.tape().taking = None;
return f.notify(TapeMessage::TakeDropped.into());
}
let at = player.position().as_secs_f64() * f64::from(t.rate);
let (start, end) = (t.span.0 as f64, t.span.1 as f64);
if at >= end {
f.tape().taking = None;
return f.notify(TapeMessage::PastRange.into());
}
if at < start {
if !t.waiting {
f.tape().taking.as_mut().expect("taken above").waiting = true;
f.notify(TapeMessage::TakeWaiting.into());
}
return;
}
f.tape().taking = None;
let Some(live) = f.tape().live.as_mut() else {
return;
};
let e = live.extent;
let head = e.range.start as f64 + (at - start) * f64::from(e.rate) / f64::from(t.rate);
if let Err(err) = live.handle.set(Setting::Head(0, head)) {
return f.notify(TapeMessage::Failed(err.to_string()).into());
}
if state == State::Playing {
act(f, TapeAction::Play);
}
f.notify(TapeMessage::Took.into());
}
fn record(f: &mut impl Frontend) {
let samples = f.session().samples_dir().to_path_buf();
let Some(live) = f.tape().live.as_mut() else {
return f.notify(TapeMessage::NoTape.into());
};
if let Some(found) = stop_recording(live) {
return report(f, found);
}
let started =
new_file(&samples, &live.name, "mix").and_then(|path| match live.handle.record(&path) {
Ok(()) => Ok(path),
Err(e) => {
_ = std::fs::remove_dir(path.parent().expect("in its directory"));
Err(e.to_string())
}
});
match started {
Ok(path) => f.notify(TapeMessage::Recording(path).into()),
Err(e) => f.notify(TapeMessage::Failed(e).into()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
Window(usize),
Rate(usize),
Level(usize),
Pan(usize),
Send(usize),
Wear(usize),
Fade(usize),
Ping(usize),
Slew(usize),
Drive(usize),
Cutoff(usize),
Filter(usize),
Solo(usize),
Write,
WriteWindow,
Feedback,
WriteWear,
Thin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Idle {
VoiceOff,
WriteOff,
NoSend,
Silent,
Still,
NoRoom,
Unchanging,
Turns,
}
pub fn no_effect(s: &TapeState, e: Extent, control: Control) -> Option<Idle> {
let voice = |i: usize| &s.voices[i];
let off = |i: usize| (!voice(i).on).then_some(Idle::VoiceOff);
let write_off = || (!s.write).then_some(Idle::WriteOff);
match control {
Control::Window(i)
| Control::Rate(i)
| Control::Slew(i)
| Control::Drive(i)
| Control::Cutoff(i)
| Control::Filter(i) => off(i),
Control::Ping(i) => off(i).or((voice(i).rate == 0.0).then_some(Idle::Still)),
Control::Solo(_) => None,
Control::Level(i) => off(i),
Control::Pan(i) => off(i).or((voice(i).level == 0.0).then_some(Idle::Silent)),
Control::Send(i) => off(i).or_else(write_off),
Control::Wear(i) => off(i)
.or_else(write_off)
.or((voice(i).send == 0.0).then_some(Idle::NoSend)),
Control::Fade(i) => {
let v = voice(i);
let x = playr_looper::crossfade(v.window, v.window, e.frames, v.rate, 1000.0, e.rate);
off(i)
.or((v.rate == 0.0).then_some(Idle::Still))
.or(v.ping.then_some(Idle::Turns))
.or((x.frames == 0).then_some(Idle::NoRoom))
}
Control::Write => {
let sending = s.voices.iter().any(|v| v.on && v.send > 0.0);
let still = s.feedback == 1.0 && s.wear == 0.0 && s.thin == 0.0;
(s.write && still && !sending).then_some(Idle::Unchanging)
}
Control::WriteWindow | Control::Feedback | Control::WriteWear | Control::Thin => {
write_off()
}
}
}
pub fn settings(action: TapeAction, e: Extent) -> Result<Vec<Setting>, TapeMessage> {
let frame = |p: Pos| {
let f = e.range.start as f64
+ match p {
Pos::Percent(p) => f64::from(p) / 100.0 * e.range.len() as f64,
Pos::Time(d) => d.as_secs_f64() * f64::from(e.rate),
};
(f.round().max(0.0) as usize).min(e.frames)
};
let window = |a, b| match (frame(a), frame(b)) {
(a, b) if a < b => Ok(Window::new(a, b)),
_ => Err(TapeMessage::EmptyWindow),
};
Ok(vec![match action {
TapeAction::Play => Setting::Play,
TapeAction::Stop => Setting::Stop,
TapeAction::Write(on) => Setting::Write(on),
TapeAction::WriteWindow(a, b) => Setting::WriteWindow(window(a, b)?),
TapeAction::Feedback(v) => Setting::Feedback(v),
TapeAction::Wear(v) => Setting::Wear(v),
TapeAction::Thin(v) => Setting::Thin(v),
TapeAction::Voice(n, s) => {
let i = usize::from(n) - 1;
match s {
VoiceSetting::On(on) => Setting::On(i, on),
VoiceSetting::Rate(r) => Setting::Rate(i, r),
VoiceSetting::Window(a, b) => Setting::Window(i, window(a, b)?),
VoiceSetting::Level(v) => Setting::Level(i, v),
VoiceSetting::Pan(v) => Setting::Pan(i, v),
VoiceSetting::Send(v) => Setting::Send(i, v),
VoiceSetting::Wear(v) => Setting::VoiceWear(i, v),
VoiceSetting::Fade(ms) => Setting::Fade(i, ms),
VoiceSetting::Ping(on) => Setting::Ping(i, on),
VoiceSetting::Slew(ms) => Setting::Slew(i, ms),
VoiceSetting::Drive(v) => Setting::Drive(i, v),
VoiceSetting::Cutoff(v) => Setting::Cutoff(i, v),
VoiceSetting::Filter(f) => Setting::Filter(i, f),
VoiceSetting::Solo(on) => Setting::Solo(i, on),
}
}
TapeAction::Load(_)
| TapeAction::Take
| TapeAction::Reset
| TapeAction::Save
| TapeAction::Record => return Ok(Vec::new()),
}])
}