use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use dawkey_media::{Analysis, ExportProgress, ExportSpec, FileFormat, MediaError, SampleFormat};
use dawkey_studio::RenderLeftovers;
use crate::App;
use crate::state::{ExportRange, PlatformRequest};
const RATES: [u32; 4] = [44_100, 48_000, 88_200, 96_000];
const TAILS: [f64; 5] = [0.0, 1.0, 2.0, 5.0, 10.0];
pub(crate) struct RunningExport {
pub(crate) path: PathBuf,
pub(crate) progress: Arc<ExportProgress>,
pub(crate) frames: usize,
done: mpsc::Receiver<(Result<Analysis, MediaError>, RenderLeftovers)>,
}
impl App {
pub(crate) fn on_export_click(&mut self, parts: &[&str]) -> Option<Result<(), String>> {
let settings = &mut self.view.export;
Some(match parts {
["toolbar", "export"] => {
self.open_export_dialog();
Ok(())
}
["export"] => Ok(()),
["export", "cancel"] | ["export", "backdrop"] => {
self.view.export_dialog = false;
Ok(())
}
["export", "format"] => {
settings.format = match settings.format {
FileFormat::Wav => FileFormat::Flac,
FileFormat::Flac => FileFormat::Wav,
};
if settings.format == FileFormat::Flac && settings.sample == SampleFormat::Float32 {
settings.sample = SampleFormat::Int24;
}
Ok(())
}
["export", "bits"] => {
settings.sample = match (settings.sample, settings.format) {
(SampleFormat::Int16, _) => SampleFormat::Int24,
(SampleFormat::Int24, FileFormat::Wav) => SampleFormat::Float32,
_ => SampleFormat::Int16,
};
Ok(())
}
["export", "rate"] => {
settings.sample_rate = match settings.sample_rate {
None => Some(RATES[0]),
Some(rate) => RATES
.iter()
.position(|&r| r == rate)
.and_then(|i| RATES.get(i + 1).copied()),
};
Ok(())
}
["export", "range"] => {
let loop_range = self.studio.session().project().loop_region.range;
let has_loop = loop_range.is_some_and(|r| !r.is_empty());
settings.range = match settings.range {
ExportRange::Song if has_loop => ExportRange::Loop,
_ => ExportRange::Song,
};
Ok(())
}
["export", "tail"] => {
let next = TAILS
.iter()
.position(|&t| t == settings.tail_s)
.map_or(0, |i| i + 1);
settings.tail_s = TAILS[next % TAILS.len()];
Ok(())
}
["export", "start"] => self.choose_export_location(),
["status", "export_cancel"] => {
if let Some(export) = &self.export {
export.progress.cancel.store(true, Ordering::Relaxed);
}
Ok(())
}
_ => return None,
})
}
pub(crate) fn on_export_key(&mut self, key: &str) -> Option<Result<(), String>> {
if !self.view.export_dialog {
return None;
}
Some(match key {
"return" => self.choose_export_location(),
"escape" => {
self.view.export_dialog = false;
Ok(())
}
_ => Ok(()),
})
}
pub(crate) fn open_export_dialog(&mut self) {
self.close_overlays();
self.view.export_dialog = true;
}
fn choose_export_location(&mut self) -> Result<(), String> {
self.export_range()?;
if self.export.is_some() {
return Err("an export is already running".into());
}
self.view.export_dialog = false;
let format = self.view.export.format;
let name = format!(
"{}.{}",
self.studio.session().project().name,
format.extension()
);
self.platform_requests
.push(PlatformRequest::ChooseExportLocation { name, format });
Ok(())
}
fn export_range(&self) -> Result<(f64, f64), String> {
let project = self.studio.session().project();
let settings = &self.view.export;
let (start, end) = match settings.range {
ExportRange::Song => (0, project.end()),
ExportRange::Loop => project
.loop_region
.range
.map_or((0, 0), |r| (r.start, r.end)),
};
if end <= start {
return Err(match settings.range {
ExportRange::Song => "there's nothing to export: no clips play".into(),
ExportRange::Loop => "set a loop range to export it".into(),
});
}
Ok((
project.frames_to_seconds(start),
project.frames_to_seconds(end) + settings.tail_s,
))
}
pub fn start_export(&mut self, path: &Path) -> Result<(), String> {
if self.export.is_some() {
return Err("an export is already running".into());
}
let (start_s, end_s) = self.export_range()?;
let settings = self.view.export;
let spec = ExportSpec {
file: FileFormat::from_path(path).unwrap_or(settings.format),
sample: settings.sample,
sample_rate: settings
.sample_rate
.unwrap_or(self.studio.session().project().sample_rate),
};
spec.check()?;
let job = self
.studio
.export_job(start_s, end_s)
.map_err(|e| e.to_string())?;
let frames = job.frames();
let progress = Arc::new(ExportProgress::default());
let (tx, rx) = mpsc::channel();
let (worker_progress, worker_path) = (Arc::clone(&progress), path.to_owned());
std::thread::Builder::new()
.name("dawkey-export".into())
.spawn(move || {
let _ = tx.send(job.run(&spec, &worker_path, &worker_progress));
})
.map_err(|e| format!("starting the export: {e}"))?;
self.export = Some(RunningExport {
path: path.to_owned(),
progress,
frames,
done: rx,
});
Ok(())
}
pub fn export_progress(&self) -> Option<f64> {
self.export.as_ref().map(|e| {
let done = e.progress.frames.load(Ordering::Relaxed) as f64;
(done / e.frames.max(1) as f64).min(1.0)
})
}
pub(crate) fn poll_export(&mut self) {
let Some(export) = &self.export else {
return;
};
let (result, leftovers) = match export.done.try_recv() {
Ok(done) => done,
Err(mpsc::TryRecvError::Empty) => {
self.redraw = true;
return;
}
Err(mpsc::TryRecvError::Disconnected) => {
self.export = None;
self.message = Some("The export stopped unexpectedly".into());
return;
}
};
let export = self.export.take().expect("checked above");
self.studio.finish_render(leftovers);
let name = export
.path
.file_name()
.map_or_else(String::new, |n| n.to_string_lossy().into_owned());
self.message = Some(match result {
Ok(analysis) => match analysis.lufs {
Some(lufs) => format!("Exported {name} ({lufs:.1} LUFS)"),
None => format!("Exported {name}"),
},
Err(MediaError::Cancelled) => "Export cancelled".into(),
Err(e) => e.to_string(),
});
self.redraw = true;
}
}