use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
use std::sync::Arc;
use playr_core::analysis::tempo;
use playr_core::analysis::TempoFix;
use playr_core::audio::bus::Cue;
use playr_core::audio::output::DeviceEvent;
use playr_core::audio::resample::Resample;
use playr_core::audio::{Attachment, Sources, State};
use playr_core::db::Track;
use playr_core::event::JobId;
use playr_core::notice::Notice;
use playr_core::samples;
use playr_core::wave::Peaks;
pub use playr_dj::{Band, CueOut, Curve, Nudge, Range, Side, EQ_DB, HOT_CUES, LOOP_BEATS};
use playr_dj::{BusSetting, DelayParam, Engine, Handle, Returned, Setting};
use crate::dispatch::Frontend;
use crate::fx::{Division, FxSetting, FxState};
use crate::message::Message;
use crate::mix::Strip;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GridEdit {
Double,
Halve,
Earlier,
Later,
Offset(f32),
Tap,
Reset,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DjAction {
Load(Side),
Take(Side),
Play(Side),
Pause(Side),
Cue(Side),
CueHold(Side, bool),
Sync(Side, bool),
Rate(Side, f32),
Range(Side, Range),
Nudge(Side, Nudge),
Gain(Side, f32),
Level(Side, f32),
Xfade(f32),
XfadeTo(Option<Side>),
Quantize(bool),
CueBus(Option<Side>),
Grid(Side, GridEdit),
Eq(Side, Band, f32),
Kill(Side, Band, bool),
Filter(Side, f32),
HotCue(Side, u8),
HotClear(Side, u8),
Jump(Side, f32),
Loop(Side, Option<f32>),
CueOut(CueOut),
Curve(Curve),
Seek(Side, crate::tape::Pos),
Mute(Side, bool),
Unqueue(Side),
Mark(Side, bool),
Strict(bool),
DelaySend(Side, f32),
ReverbSend(Side, f32),
Fx(FxSetting),
}
#[derive(Debug, Clone, PartialEq)]
pub enum DjMessage {
Loading(Side),
Loaded {
side: Side,
title: String,
bpm: Option<f64>,
},
FindingGrid(Side),
NoGrid(Side),
Done(DjAction),
Empty(Side),
NoTrack,
Playing(Side),
OutOfReach(Side),
SpeedOutOfReach(i32),
Took(Side),
Tapped(Side),
NoCueChannels(u16),
NoMark(Side),
Queued {
side: Side,
title: String,
},
Grid {
side: Side,
bpm: f64,
t0: f64,
},
DeviceLost(String),
Failed(String),
}
impl From<DjMessage> for Message {
fn from(m: DjMessage) -> Self {
Message::Dj(m)
}
}
enum Output {
Bus(Sources),
Manual,
}
struct Played(Engine);
impl playr_core::audio::bus::Source for Played {
fn process(&mut self, main: &mut [f32], cue: &mut [f32]) -> Cue {
match self.0.process_buses(main, cue) {
None => Cue::None,
Some((CueOut::Split, swap)) => Cue::Split { swap },
Some((CueOut::Channels, swap)) => Cue::Channels { swap },
}
}
}
struct Live {
handle: Handle,
attached: Option<Attachment>,
engine: Option<Engine>,
rate: u32,
interp: Option<playr_dsp::Interp>,
}
struct Read {
samples: Vec<f32>,
channels: u16,
peaks: Peaks,
}
struct Loading {
path: PathBuf,
title: String,
result: Receiver<Result<Read, String>>,
}
type Sent = [Option<Track>; 2];
#[derive(Debug, Clone)]
pub struct Loaded {
pub path: PathBuf,
pub title: String,
pub frames: usize,
pub peaks: Arc<Peaks>,
pub grid: Option<tempo::Grid>,
pub hot: [Option<f64>; HOT_CUES],
pub marks: Vec<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DjState {
pub gain: [f32; 2],
pub level: [f32; 2],
pub xfade: f32,
pub quantize: bool,
pub cue_bus: Option<Side>,
pub eq: [[f32; 3]; 2],
pub kill: [[bool; 3]; 2],
pub filter: [f32; 2],
pub cue_out: CueOut,
pub curve: Curve,
pub mute: [bool; 2],
pub strict: bool,
pub sends: [[f32; 2]; 2],
pub fx: FxState,
}
impl Default for DjState {
fn default() -> Self {
DjState {
gain: [0.0; 2],
level: [1.0; 2],
xfade: 0.5,
quantize: false,
cue_bus: None,
eq: [[0.0; 3]; 2],
kill: [[false; 3]; 2],
filter: [0.0; 2],
cue_out: CueOut::default(),
curve: Curve::default(),
mute: [false; 2],
strict: false,
sends: [[0.0; 2]; 2],
fx: FxState::default(),
}
}
}
const TAP_GAP: f64 = 2.0;
pub struct Decks {
knee: f32,
output: Output,
live: Option<Live>,
loaded: [Option<Loaded>; 2],
loading: [Option<Loading>; 2],
arriving: [Option<Loaded>; 2],
analysing: Vec<(JobId, Side, PathBuf)>,
taps: [Vec<f64>; 2],
loads: [u64; 2],
next: [Option<Track>; 2],
sent: Sent,
crossfade: [bool; 2],
handover: [bool; 2],
volume: (f32, f32),
state: DjState,
errors: (Sender<DeviceEvent>, Receiver<DeviceEvent>),
}
fn i(side: Side) -> usize {
match side {
Side::A => 0,
Side::B => 1,
}
}
impl Decks {
pub fn new(sources: Sources, knee: f32) -> Self {
Decks::with(Output::Bus(sources), knee)
}
pub fn manual() -> Self {
Decks::with(Output::Manual, playr_dj::DEFAULT_KNEE)
}
fn with(output: Output, knee: f32) -> Self {
Decks {
knee,
output,
live: None,
loaded: [None, None],
loading: [None, None],
arriving: [None, None],
analysing: Vec::new(),
taps: [Vec::new(), Vec::new()],
loads: [0; 2],
next: [None, None],
sent: [None, None],
crossfade: [false; 2],
handover: [false; 2],
volume: (f32::NAN, f32::NAN),
state: DjState::default(),
errors: channel(),
}
}
pub fn loaded(&self, side: Side) -> Option<&Loaded> {
self.loaded[i(side)].as_ref()
}
pub fn loading(&self, side: Side) -> bool {
self.loading[i(side)].is_some()
}
pub fn state(&self) -> &DjState {
&self.state
}
pub fn next(&self, side: Side) -> Option<&Track> {
self.next[i(side)].as_ref()
}
pub fn status(&self) -> Option<&playr_dj::Status> {
self.live.as_ref().map(|l| &**l.handle.status())
}
pub fn rate(&self) -> Option<u32> {
self.live.as_ref().map(|l| l.rate)
}
pub fn gliding(&self) -> bool {
self.status()
.is_some_and(|s| (s.xfade() - f64::from(self.state.xfade)).abs() > 1e-6)
}
pub fn playing(&self) -> bool {
self.status()
.is_some_and(|s| s.deck(Side::A).playing() || s.deck(Side::B).playing())
}
pub fn process(&mut self, out: &mut [f32]) {
if let Some(engine) = self.live.as_mut().and_then(|l| l.engine.as_mut()) {
engine.process(out);
}
}
fn live(&mut self, rate: u32) -> Result<&mut Live, String> {
if self.live.is_none() {
let rate = match &self.output {
Output::Bus(sources) => sources.rate_for(rate),
Output::Manual => rate,
};
let (engine, mut handle) = playr_dj::new(rate);
handle
.set(Setting::Knee(self.knee))
.map_err(|e| e.to_string())?;
let (attached, engine) = match &self.output {
Output::Bus(sources) => {
(Some(sources.attach(rate, Box::new(Played(engine)))), None)
}
Output::Manual => (None, Some(engine)),
};
self.live = Some(Live {
handle,
attached,
engine,
rate,
interp: None,
});
}
Ok(self.live.as_mut().expect("opened above"))
}
fn load(&mut self, side: Side, track: &Track) -> Result<(), String> {
let path = PathBuf::from(&track.path);
let from = match track.sample_rate {
Some(rate) => rate,
None => native_rate(&path)?,
};
let to = self.live(from)?.rate;
let (tx, rx) = channel();
let read_path = path.clone();
std::thread::spawn(move || _ = tx.send(read(&read_path, from, to)));
self.loading[i(side)] = Some(Loading {
path,
title: track.display_title(),
result: rx,
});
Ok(())
}
fn follow_volume(&mut self, main: f32, cue: f32) {
let Some(live) = &mut self.live else { return };
if main != self.volume.0 && live.handle.set(Setting::Volume(f64::from(main))).is_ok() {
self.volume.0 = main;
}
if cue != self.volume.1 && live.handle.set(Setting::Headphones(f64::from(cue))).is_ok() {
self.volume.1 = cue;
}
}
fn follow_interp(&mut self, interp: playr_dsp::Interp) {
let Some(live) = &mut self.live else { return };
if live.interp != Some(interp) && live.handle.set(Setting::Interp(interp)).is_ok() {
live.interp = Some(interp);
}
}
pub fn device_events(&self) -> Sender<DeviceEvent> {
self.errors.0.clone()
}
fn lose_device(&mut self) {
let output = std::mem::replace(&mut self.output, Output::Manual);
let analysing = std::mem::take(&mut self.analysing);
*self = Decks::with(output, self.knee);
self.analysing = analysing;
}
fn set(&mut self, s: Setting) -> Result<(), String> {
let live = self.live.as_mut().ok_or("no deck is loaded")?;
live.handle.set(s).map_err(|e| e.to_string())
}
fn synced(&self, division: Division) -> Option<f32> {
let status = self.status()?;
let st = &self.state;
let bpm = |side: Side| {
let grid = self.loaded(side)?.grid?;
let d = status.deck(side);
let reach = match side {
Side::A => 1.0 - st.xfade,
Side::B => st.xfade,
};
let heard = st.level[i(side)] * reach * f32::from(u8::from(!st.mute[i(side)]));
(d.playing() && heard > 0.0).then(|| (heard, grid.bpm * (1.0 + d.pct() / 100.0)))
};
let (_, bpm) = [bpm(Side::A), bpm(Side::B)]
.into_iter()
.flatten()
.reduce(|a, b| if b.0 > a.0 { b } else { a })?;
let ms = 4.0 * 60_000.0 / bpm * division.fraction();
let ms_of = |s: f32| s * 1000.0;
let (lo, hi) = (
ms_of(playr_looper::DELAY_MIN),
ms_of(playr_looper::DELAY_SECONDS),
);
Some((ms as f32).clamp(lo, hi))
}
fn fed(&self, b: usize) -> bool {
let st = &self.state;
[Side::A, Side::B].into_iter().any(|side| {
let k = i(side);
let reach = match side {
Side::A => st.xfade < 1.0,
Side::B => st.xfade > 0.0,
};
self.loaded(side).is_some()
&& reach
&& !st.mute[k]
&& st.level[k] > 0.0
&& st.sends[k][b] > 0.0
})
}
pub fn fx_idle(&self, field: crate::fx::FxField) -> Option<crate::tape::Idle> {
crate::fx::idle(&self.state.fx, [self.fed(0), self.fed(1)], field)
}
pub fn send_idle(&self, side: Side) -> Option<crate::tape::Idle> {
use crate::tape::Idle;
let st = &self.state;
let k = i(side);
st.mute[k]
.then_some(Idle::Muted)
.or((st.level[k] == 0.0).then_some(Idle::Silent))
}
fn resync(&mut self) {
let fx = self.state.fx;
let Some(ms) = fx.sync.and_then(|d| self.synced(d)) else {
return;
};
let s = Setting::Fx(BusSetting::Delay(DelayParam::Time, ms / 1000.0));
if (ms - fx.time).abs() > 0.5 && self.set(s).is_ok() {
self.state.fx.time = ms;
}
}
}
fn native_rate(path: &Path) -> Result<u32, String> {
let fail = |e: playr_core::audio::decode::DecodeError| format!("{}: {e}", path.display());
let mut stream = playr_core::audio::decode::AudioStream::open(path).map_err(fail)?;
stream.next_chunk().map_err(fail)?;
Ok(stream.spec().rate)
}
fn read(path: &Path, from: u32, to: u32) -> Result<Read, String> {
let (mut audio, channels) = samples::read_frames(path, from, 0, u64::MAX)?;
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 != from {
let mut r = Resample::new(from, to, channels, 1.0)
.ok_or_else(|| format!("cannot resample {from} Hz to {to} Hz"))?;
let mut out = Vec::with_capacity(audio.len() * to as usize / from as usize + 64);
r.push(&audio, &mut out);
r.flush(&mut out);
audio = out;
}
let peaks = Peaks::from_interleaved(&audio, channels.into(), to);
Ok(Read {
samples: audio,
channels,
peaks,
})
}
fn dj_grid(g: tempo::Grid) -> Option<playr_dj::Grid> {
playr_dj::Grid::new(g.bpm, g.t0).ok()
}
pub fn poll(f: &mut impl Frontend) {
for side in [Side::A, Side::B] {
let done = match f.dj().loading[i(side)]
.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 = f.dj().loading[i(side)].take().expect("polled above");
if let Err(e) = result.and_then(|r| start(f, side, loading, r)) {
f.notify(DjMessage::Failed(e).into());
}
}
}
while let Some(r) = f.dj().live.as_mut().and_then(|l| l.handle.poll()) {
let message = match r {
Returned::Replaced(side, _) | Returned::Fading(side) => taken(f, side),
Returned::Faded(..) => None,
Returned::Refused(side, _) => {
let d = f.dj();
d.arriving[i(side)] = None;
match d.sent[i(side)].take() {
Some(t) => {
d.next[i(side)] = Some(t);
None
}
None => Some(DjMessage::Playing(side)),
}
}
};
if let Some(m) = message {
f.notify(m.into());
}
}
if f.dj()
.live
.as_ref()
.and_then(|l| l.attached.as_ref())
.is_some_and(Attachment::lost)
{
f.dj().lose_device();
let e = "the output device went away".to_string();
f.notify(DjMessage::DeviceLost(e).into());
}
while let Ok(e) = f.dj().errors.1.try_recv() {
match e {
DeviceEvent::Rerouted => {}
DeviceEvent::Error(e) => f.notify(DjMessage::Failed(e).into()),
DeviceEvent::Lost(e) => {
f.dj().lose_device();
f.notify(DjMessage::DeviceLost(e).into());
}
}
}
let mix = f.mix();
let (main, cue) = (mix.gain(Strip::Decks), mix.gain(Strip::Headphones));
f.dj().follow_volume(main, cue);
let interp = f.mix().interp();
f.dj().follow_interp(interp);
for side in [Side::A, Side::B] {
if let Err(e) = keep_hot_cues(f, side) {
f.notify(DjMessage::Failed(e).into());
}
load_next(f, side);
}
f.dj().resync();
}
fn load_next(f: &mut impl Frontend, side: Side) {
let d = f.dj();
let k = i(side);
let stopped = d.status().is_some_and(|s| {
let s = s.deck(side);
!s.playing() && s.loads() == d.loads[k]
});
let busy = d.loading[k].is_some() || d.arriving[k].is_some() || d.sent[k].is_some();
if !stopped || busy {
return;
}
let Some(track) = d.next[k].take() else {
return;
};
match d.load(side, &track) {
Ok(()) => {
d.sent[k] = Some(track);
f.notify(DjMessage::Loading(side).into());
}
Err(e) => f.notify(DjMessage::Failed(e).into()),
}
}
fn keep_hot_cues(f: &mut impl Frontend, side: Side) -> Result<(), String> {
let d = f.dj();
let (Some(status), Some(loaded), Some(rate)) = (d.status(), d.loaded(side), d.rate()) else {
return Ok(());
};
let s = status.deck(side);
if s.loads() != d.loads[i(side)] || s.hot_cues() == loaded.hot {
return Ok(());
}
let (now, was, path) = (s.hot_cues(), loaded.hot, loaded.path.clone());
for (slot, n) in now.iter().enumerate().filter(|(k, n)| **n != was[*k]) {
let at = n.map(|frames| frames / f64::from(rate));
f.session_mut().set_hot_cue(&path, slot as u8 + 1, at)?;
}
if let Some(l) = f.dj().loaded[i(side)].as_mut() {
l.hot = now;
}
Ok(())
}
fn start(f: &mut impl Frontend, side: Side, loading: Loading, read: Read) -> Result<(), String> {
let grid = f.session().grid(&loading.path);
let rate = f64::from(f.dj().rate().ok_or("no engine")?);
let mut hot = [None; HOT_CUES];
for (slot, at) in f.session().hot_cues(&loading.path) {
if let Some(h) = hot.get_mut(usize::from(slot).wrapping_sub(1)) {
*h = Some(at * rate);
}
}
let frames = read.samples.len() / usize::from(read.channels);
let track = playr_dj::Track::new(read.samples, read.channels, grid.and_then(dj_grid))
.map_err(|e| e.to_string())?
.with_hot_cues(hot);
let crossfade = std::mem::take(&mut f.dj().crossfade[i(side)]);
let live = f.dj().live.as_mut().ok_or("no engine")?;
match crossfade {
true => live.handle.replace(side, track),
false => live.handle.load(side, track),
}
.map_err(|e| e.to_string())?;
f.dj().arriving[i(side)] = Some(Loaded {
path: loading.path,
title: loading.title,
frames,
peaks: Arc::new(read.peaks),
grid,
hot,
marks: Vec::new(),
});
Ok(())
}
fn taken(f: &mut impl Frontend, side: Side) -> Option<DjMessage> {
f.dj().sent[i(side)] = None;
let mut loaded = f.dj().arriving[i(side)].take()?;
let rate = f64::from(f.dj().rate()?);
loaded.marks = f
.session()
.marks_of(&loaded.path)
.iter()
.map(|m| m.time().as_secs_f64() * rate)
.collect();
let d = f.dj();
d.taps[i(side)].clear();
d.loads[i(side)] += 1;
d.loaded[i(side)] = Some(loaded.clone());
let took = match std::mem::take(&mut f.dj().handover[i(side)]) {
true => hand_over(f, side, &loaded.path),
false => Ok(false),
};
match took {
Ok(true) => return Some(DjMessage::Took(side)),
Ok(false) => {}
Err(m) => return Some(m),
}
let analysed = f.session().analysis_of(&loaded.path).is_some();
if loaded.grid.is_none() && !analysed {
if let Ok(job) = f.session_mut().analyze(Some(loaded.path.clone())) {
f.dj().analysing.push((job, side, loaded.path));
return Some(DjMessage::FindingGrid(side));
}
}
Some(DjMessage::Loaded {
side,
title: loaded.title,
bpm: loaded.grid.map(|g| g.bpm),
})
}
pub fn analysed(f: &mut impl Frontend, job: JobId) -> bool {
let Some(at) = f.dj().analysing.iter().position(|(j, ..)| *j == job) else {
return false;
};
let (_, side, path) = f.dj().analysing.remove(at);
if f.dj().loaded(side).map(|l| &l.path) != Some(&path) {
return true;
}
let grid = f.session().grid(&path);
let message = match grid {
Some(g) => match send_grid(f, side, Some(g)) {
Ok(()) => DjMessage::Loaded {
side,
title: f
.dj()
.loaded(side)
.map(|l| l.title.clone())
.unwrap_or_default(),
bpm: Some(g.bpm),
},
Err(e) => DjMessage::Failed(e),
},
None => DjMessage::NoGrid(side),
};
f.notify(message.into());
true
}
fn send_grid(f: &mut impl Frontend, side: Side, grid: Option<tempo::Grid>) -> Result<(), String> {
f.dj().set(Setting::Grid(side, grid.and_then(dj_grid)))?;
if let Some(l) = f.dj().loaded[i(side)].as_mut() {
l.grid = grid;
}
Ok(())
}
pub fn act(f: &mut impl Frontend, action: DjAction) {
let result = match action {
DjAction::Load(side) => return load(f, side),
DjAction::Take(side) => return take(f, side),
DjAction::Grid(side, edit) => return grid(f, side, edit),
_ => send(f, action),
};
match result {
Ok(()) => f.notify(DjMessage::Done(action).into()),
Err(m) => f.notify(m.into()),
}
}
fn load(f: &mut impl Frontend, side: Side) {
let Some(track) = crate::dispatch::cursor_track(f) else {
return f.notify(DjMessage::NoTrack.into());
};
let d = f.dj();
if d.status().is_some_and(|s| s.deck(side).playing()) {
if d.state.strict {
let title = track.display_title();
d.next[i(side)] = Some(track);
return f.notify(DjMessage::Queued { side, title }.into());
}
d.next[i(side)] = None;
d.crossfade[i(side)] = true;
d.handover[i(side)] = false;
return match d.load(side, &track) {
Ok(()) => {
d.sent[i(side)] = Some(track);
f.notify(DjMessage::Loading(side).into())
}
Err(e) => {
d.crossfade[i(side)] = false;
f.notify(DjMessage::Failed(e).into())
}
};
}
d.next[i(side)] = None;
d.crossfade[i(side)] = false;
d.handover[i(side)] = false;
match f.dj().load(side, &track) {
Ok(()) => f.notify(DjMessage::Loading(side).into()),
Err(e) => f.notify(DjMessage::Failed(e).into()),
}
}
fn rate_for(cents: i32) -> Option<(f64, Range)> {
let pct = (playr_core::audio::speed_at(cents) - 1.0) * 100.0;
[Range::Narrow, Range::Medium, Range::Wide]
.into_iter()
.find(|r| pct.abs() <= r.percent() + 1e-9)
.map(|r| (pct, r))
}
fn take(f: &mut impl Frontend, side: Side) {
if f.dj().status().is_some_and(|s| s.deck(side).playing()) {
return f.notify(DjMessage::Playing(side).into());
}
let (path, rate) = match f.session().playing_track() {
Ok(track) => track,
Err(refusal) => return f.notify(refusal.into()),
};
let cents = f.session().player().status().cents;
if rate_for(cents).is_none() {
return f.notify(DjMessage::SpeedOutOfReach(cents).into());
}
let track = (f.session().tracks_at(std::slice::from_ref(&path)).pop()).unwrap_or(Track {
path: path.to_string_lossy().into(),
sample_rate: Some(rate),
..Default::default()
});
let d = f.dj();
d.next[i(side)] = None;
d.crossfade[i(side)] = false;
match d.load(side, &track) {
Ok(()) => {
d.handover[i(side)] = true;
f.notify(DjMessage::Loading(side).into());
}
Err(e) => f.notify(DjMessage::Failed(e).into()),
}
}
fn hand_over(f: &mut impl Frontend, side: Side, path: &Path) -> Result<bool, DjMessage> {
if f.session().playing_track().map(|(p, _)| p).as_deref() != Ok(path) {
return Ok(false);
}
let status = f.session().player().status();
let Some((pct, range)) = rate_for(status.cents) else {
return Err(DjMessage::SpeedOutOfReach(status.cents));
};
let trim = status.gain_db.unwrap_or(0.0);
let playing = status.state == State::Playing;
let rate = f64::from(f.dj().rate().ok_or(DjMessage::Empty(side))?);
let at = f.session().player().position().as_secs_f64() * rate;
let start = match playing {
true => Setting::PlayFrom(side, at),
false => Setting::Seek(side, at),
};
let d = f.dj();
let alone = !d.status().is_some_and(|s| s.deck(side.other()).playing());
let x = if side == Side::A { 0.0 } else { 1.0 };
let xfade = alone.then_some(Setting::Xfade(x));
let settings = [
Some(Setting::Range(side, range)),
Some(Setting::Rate(side, pct)),
Some(Setting::Gain(side, f64::from(trim))),
xfade,
Some(start),
];
for s in settings.into_iter().flatten() {
d.set(s).map_err(DjMessage::Failed)?;
}
d.state.gain[i(side)] = trim;
if alone {
d.state.xfade = x as f32;
}
if playing {
f.session().send(playr_core::audio::Cmd::TogglePause);
}
Ok(true)
}
fn deck_of(action: DjAction) -> Option<Side> {
use DjAction as D;
match action {
D::Load(s)
| D::Take(s)
| D::Play(s)
| D::Pause(s)
| D::Cue(s)
| D::CueHold(s, _)
| D::Sync(s, _)
| D::Rate(s, _)
| D::Range(s, _)
| D::Nudge(s, _)
| D::Gain(s, _)
| D::Level(s, _)
| D::Grid(s, _)
| D::Eq(s, ..)
| D::Kill(s, ..)
| D::Filter(s, _)
| D::HotCue(s, _)
| D::HotClear(s, _)
| D::Jump(s, _)
| D::Loop(s, _)
| D::Seek(s, _)
| D::Mark(s, _) => Some(s),
D::Xfade(_)
| D::XfadeTo(_)
| D::Quantize(_)
| D::CueBus(_)
| D::CueOut(_)
| D::Curve(_)
| D::Mute(..)
| D::Unqueue(_)
| D::Strict(_)
| D::DelaySend(..)
| D::ReverbSend(..)
| D::Fx(_) => None,
}
}
fn send(f: &mut impl Frontend, action: DjAction) -> Result<(), DjMessage> {
use DjAction as D;
if let Some(side) = deck_of(action) {
if f.dj().loaded(side).is_none() {
return Err(DjMessage::Empty(side));
}
}
match action {
D::Sync(side, true) => sync_check(f, side)?,
D::Jump(side, _) | D::Loop(side, Some(_))
if f.dj().loaded(side).is_some_and(|l| l.grid.is_none()) =>
{
return Err(DjMessage::NoGrid(side));
}
D::CueOut(CueOut::Channels) => {
let n = match &f.dj().output {
Output::Bus(sources) => sources.widen(4),
Output::Manual => 2,
};
if n < 4 {
return Err(DjMessage::NoCueChannels(n));
}
}
_ => {}
}
let slot = |n: u8| usize::from(n) - 1;
if let D::Mark(side, next) = action {
let d = f.dj();
let (Some(l), Some(s), Some(rate)) = (d.loaded(side), d.status(), d.rate()) else {
return Err(DjMessage::Empty(side));
};
let pos = s.deck(side).pos();
let back = pos - 0.5 * f64::from(rate);
let to = match next {
true => l.marks.iter().find(|&&m| m > pos + 1.0),
false => l.marks.iter().rev().find(|&&m| m < back),
};
let Some(&to) = to else {
return Err(DjMessage::NoMark(side));
};
return d.set(Setting::Seek(side, to)).map_err(DjMessage::Failed);
}
let seek;
let fx;
let seek_to = match action {
D::Seek(s, pos) => {
let d = f.dj();
let frames = d.loaded(s).map_or(0, |l| l.frames) as f64;
let rate = f64::from(d.rate().unwrap_or(1));
match pos {
crate::tape::Pos::Percent(p) => frames * f64::from(p) / 100.0,
crate::tape::Pos::Time(t) => t.as_secs_f64() * rate,
}
}
_ => 0.0,
};
let settings: &[Setting] = match action {
D::Play(s) => &[Setting::Play(s)],
D::Pause(s) => &[Setting::Pause(s)],
D::Cue(s) => &[Setting::Cue(s, true), Setting::Cue(s, false)],
D::CueHold(s, down) => &[Setting::Cue(s, down)],
D::Sync(s, on) => &[Setting::Sync(s, on)],
D::Rate(s, pct) => &[Setting::Rate(s, f64::from(pct))],
D::Range(s, r) => &[Setting::Range(s, r)],
D::Nudge(s, n) => &[Setting::Nudge(s, n)],
D::Gain(s, db) => &[Setting::Gain(s, f64::from(db))],
D::Level(s, l) => &[Setting::Level(s, f64::from(l))],
D::Xfade(x) => &[Setting::Xfade(f64::from(x))],
D::XfadeTo(None) => &[Setting::XfadeGlide(0.5)],
D::XfadeTo(Some(Side::A)) => &[Setting::XfadeGlide(0.0)],
D::XfadeTo(Some(Side::B)) => &[Setting::XfadeGlide(1.0)],
D::Quantize(on) => &[Setting::Quantize(on)],
D::CueBus(c) => &[Setting::CueBus(c)],
D::Eq(s, b, db) => &[Setting::Eq(s, b, f64::from(db))],
D::Kill(s, b, on) => &[Setting::Kill(s, b, on)],
D::Filter(s, k) => &[Setting::Filter(s, f64::from(k))],
D::HotCue(s, n) => &[Setting::HotCue(s, slot(n))],
D::HotClear(s, n) => &[Setting::HotClear(s, slot(n))],
D::Jump(s, beats) => &[Setting::BeatJump(s, f64::from(beats))],
D::Loop(s, beats) => &[Setting::Loop(s, beats.map(f64::from))],
D::CueOut(c) => &[Setting::CueOut(c)],
D::Curve(c) => &[Setting::Curve(c)],
D::Seek(s, _) => {
seek = [Setting::Seek(s, seek_to)];
&seek
}
D::Mute(s, on) => &[Setting::Mute(s, on)],
D::Unqueue(s) => {
let d = f.dj();
d.next[i(s)] = None;
if std::mem::take(&mut d.crossfade[i(s)]) {
d.loading[i(s)] = None;
d.sent[i(s)] = None;
}
&[]
}
D::Strict(on) => {
f.dj().state.strict = on;
&[]
}
D::Mark(..) => &[],
D::Load(_) | D::Take(_) | D::Grid(..) => &[],
D::DelaySend(s, v) => {
fx = [Setting::DelaySend(s, v)];
&fx
}
D::ReverbSend(s, v) => {
fx = [Setting::ReverbSend(s, v)];
&fx
}
D::Fx(setting) => match setting.bus() {
Some(b) => {
fx = [Setting::Fx(b)];
&fx
}
None => &[],
},
};
let d = f.dj();
for s in settings {
d.set(*s).map_err(DjMessage::Failed)?;
}
let st = &mut d.state;
match action {
D::Gain(s, db) => st.gain[i(s)] = db,
D::Level(s, l) => st.level[i(s)] = l,
D::Xfade(x) => st.xfade = x,
D::XfadeTo(to) => {
st.xfade = match to {
None => 0.5,
Some(Side::A) => 0.0,
Some(Side::B) => 1.0,
}
}
D::Quantize(on) => st.quantize = on,
D::CueBus(c) => st.cue_bus = c,
D::Eq(s, b, db) => st.eq[i(s)][b as usize] = db,
D::Kill(s, b, on) => st.kill[i(s)][b as usize] = on,
D::Filter(s, k) => st.filter[i(s)] = k,
D::CueOut(c) => st.cue_out = c,
D::Curve(c) => st.curve = c,
D::Mute(s, on) => st.mute[i(s)] = on,
D::DelaySend(s, v) => st.sends[i(s)][0] = v,
D::ReverbSend(s, v) => st.sends[i(s)][1] = v,
D::Fx(FxSetting::Sync(division)) => st.fx.sync = division,
D::Fx(setting) => {
if let Some(b) = setting.bus() {
st.fx.apply(b);
}
if matches!(setting, FxSetting::Time(_)) {
st.fx.sync = None;
}
}
_ => {}
}
d.resync();
Ok(())
}
fn sync_check(f: &mut impl Frontend, side: Side) -> Result<(), DjMessage> {
let d = f.dj();
let grid = |s: Side| d.loaded(s).and_then(|l| l.grid);
let Some(gf) = grid(side) else {
return Err(DjMessage::NoGrid(side));
};
let Some(gl) = grid(side.other()) else {
return Err(DjMessage::NoGrid(side.other()));
};
let pct = d.status().map_or(0.0, |s| s.deck(side.other()).pct());
let lead = gl.bpm * (1.0 + pct / 100.0);
match playr_dj::sync_pct(gf.bpm, lead).abs() <= Range::Wide.percent() {
true => Ok(()),
false => Err(DjMessage::OutOfReach(side)),
}
}
fn grid(f: &mut impl Frontend, side: Side, edit: GridEdit) {
let Some(loaded) = f.dj().loaded(side).cloned() else {
return f.notify(DjMessage::Empty(side).into());
};
let path = loaded.path;
let stored = match edit {
GridEdit::Double | GridEdit::Halve => {
let fix = match edit {
GridEdit::Double => TempoFix::Double,
_ => TempoFix::Halve,
};
match f.session_mut().fix_tempo_of(&path, fix) {
Notice::Done(_) => Ok(()),
other => return f.notify(other.into()),
}
}
GridEdit::Reset => f
.session_mut()
.set_grid(&path, None)
.map(|()| _ = f.session_mut().fix_tempo_of(&path, TempoFix::Reset)),
GridEdit::Tap => match tap(f, side, loaded.grid) {
Some(g) => f.session_mut().set_grid(&path, Some(g)),
None => return f.notify(DjMessage::Tapped(side).into()),
},
GridEdit::Earlier | GridEdit::Later | GridEdit::Offset(_) => {
let Some(g) = loaded.grid else {
return f.notify(DjMessage::NoGrid(side).into());
};
let by = match edit {
GridEdit::Earlier => -g.bpm.recip() * 60.0,
GridEdit::Later => g.bpm.recip() * 60.0,
GridEdit::Offset(ms) => f64::from(ms) / 1000.0,
_ => unreachable!("matched above"),
};
let moved = tempo::Grid { t0: g.t0 + by, ..g };
f.session_mut().set_grid(&path, Some(moved))
}
};
if let Err(e) = stored {
return f.notify(DjMessage::Failed(e).into());
}
let grid = f.session().grid(&path);
if let Err(e) = send_grid(f, side, grid) {
return f.notify(DjMessage::Failed(e).into());
}
match grid {
Some(g) => f.notify(
DjMessage::Grid {
side,
bpm: g.bpm,
t0: g.t0,
}
.into(),
),
None => f.notify(DjMessage::NoGrid(side).into()),
}
}
fn tap(f: &mut impl Frontend, side: Side, had: Option<tempo::Grid>) -> Option<tempo::Grid> {
let d = f.dj();
let rate = f64::from(d.rate()?);
let at = d.status()?.deck(side).pos() / rate;
let taps = &mut d.taps[i(side)];
if taps.last().is_some_and(|&t| at <= t || at - t > TAP_GAP) {
taps.clear();
}
taps.push(at);
let n = taps.len();
let bpm = match (n, had) {
(1, _) => return None,
(2..4, Some(g)) => g.bpm,
_ => 60.0 * (n - 1) as f64 / (taps[n - 1] - taps[0]),
};
Some(tempo::Grid { bpm, t0: at })
}