use crate::testing_prelude::*;
#[derive(Debug, Clone, Default)]
pub struct FlacGenerator {
sample_rate: Option<u32>,
bit_depth: Option<u16>,
channels: Option<u8>,
duration_secs: Option<u32>,
frequency: Option<u32>,
filename: Option<String>,
sub_directory: Option<String>,
artist: Option<String>,
album: Option<String>,
title: Option<String>,
track_number: Option<String>,
disc_number: Option<String>,
date: Option<String>,
extra_tags: Vec<(String, String)>,
embed_cover: bool,
omit_vorbis_comments: bool,
}
impl FlacGenerator {
const DEFAULT_SAMPLE_RATE: u32 = 44100;
const DEFAULT_BIT_DEPTH: u16 = 16;
const DEFAULT_CHANNELS: u8 = 2;
const DEFAULT_DURATION_SECS: u32 = 65;
const DEFAULT_FREQUENCY: u32 = 440;
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn mock() -> Self {
Self::new()
.with_artist("Test Artist")
.with_album("Test Album")
.with_title("Test Track")
.with_track_number("1")
.with_date("2000")
.with_duration_secs(3)
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
#[must_use]
pub fn with_sample_rate(mut self, rate: u32) -> Self {
self.sample_rate = Some(rate);
self
}
#[must_use]
pub fn with_bit_depth(mut self, depth: u16) -> Self {
self.bit_depth = Some(depth);
self
}
#[must_use]
#[expect(dead_code)]
pub fn with_channels(mut self, channels: u8) -> Self {
self.channels = Some(channels);
self
}
#[must_use]
pub fn with_duration_secs(mut self, secs: u32) -> Self {
self.duration_secs = Some(secs);
self
}
#[must_use]
pub fn with_frequency(mut self, freq: u32) -> Self {
self.frequency = Some(freq);
self
}
#[must_use]
pub fn with_sub_directory(mut self, sub_dir: impl Into<String>) -> Self {
self.sub_directory = Some(sub_dir.into());
self
}
#[must_use]
pub fn with_artist(mut self, artist: impl Into<String>) -> Self {
self.artist = Some(artist.into());
self
}
#[must_use]
pub fn with_album(mut self, album: impl Into<String>) -> Self {
self.album = Some(album.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_track_number(mut self, track: impl Into<String>) -> Self {
self.track_number = Some(track.into());
self
}
#[must_use]
pub fn with_disc_number(mut self, disc: Option<impl Into<String>>) -> Self {
self.disc_number = disc.map(Into::into);
self
}
#[must_use]
pub fn with_date(mut self, date: impl Into<String>) -> Self {
self.date = Some(date.into());
self
}
#[must_use]
pub fn with_vorbis_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra_tags.push((key.into(), value.into()));
self
}
#[must_use]
pub fn with_cover_image(mut self) -> Self {
self.embed_cover = true;
self
}
#[must_use]
pub fn omit_vorbis_comments(mut self) -> Self {
self.omit_vorbis_comments = true;
self
}
fn build_filename(&self) -> String {
match (&self.track_number, &self.title) {
(Some(track), Some(title)) => format!("{track} - {title}.flac"),
(None, Some(title)) => format!("{title}.flac"),
(Some(track), None) => format!("{track} - track.flac"),
(None, None) => "track.flac".to_owned(),
}
}
pub async fn generate(&self, output_dir: &Path) -> Result<PathBuf, Failure<SampleAction>> {
let dir = match &self.sub_directory {
Some(sub) => output_dir.join(sub),
None => output_dir.to_path_buf(),
};
create_dir_all(&dir).map_err(Failure::wrap(SampleAction::CreateDirectory))?;
let filename = self
.filename
.clone()
.unwrap_or_else(|| self.build_filename());
let path = dir.join(filename);
let sample_rate = self.sample_rate.unwrap_or(Self::DEFAULT_SAMPLE_RATE);
let bit_depth = self.bit_depth.unwrap_or(Self::DEFAULT_BIT_DEPTH);
let channels = self.channels.unwrap_or(Self::DEFAULT_CHANNELS);
let duration_secs = self.duration_secs.unwrap_or(Self::DEFAULT_DURATION_SECS);
let frequency = self.frequency.unwrap_or(Self::DEFAULT_FREQUENCY);
let is_sox_ng = detect_sox_ng();
let binary = if is_sox_ng { SOX_NG } else { SOX };
let mut command = TokioCommand::new(binary);
if is_sox_ng {
command.arg("--single-threaded");
}
command
.args([
"-D",
"-n",
"-r",
&sample_rate.to_string(),
"-b",
&bit_depth.to_string(),
"-c",
&channels.to_string(),
])
.arg(&path)
.args([
"synth",
&duration_secs.to_string(),
"sine",
&frequency.to_string(),
])
.run()
.await
.map_err(Failure::wrap(SampleAction::GenerateFlac))?;
self.apply_metadata(&path).await?;
if self.embed_cover {
let image_path = ImageGenerator::new()
.with_filename("cover_temp.png")
.generate(&dir)?;
self.apply_picture(&path, &image_path).await?;
remove_file(&image_path).map_err(Failure::wrap(SampleAction::RemoveFile))?;
}
Ok(path)
}
async fn apply_metadata(&self, path: &Path) -> Result<(), Failure<SampleAction>> {
if self.omit_vorbis_comments {
TokioCommand::new(METAFLAC)
.arg("--remove")
.arg("--block-type=VORBIS_COMMENT")
.arg("--dont-use-padding")
.arg(path)
.run()
.await
.map_err(Failure::wrap(SampleAction::SetTags))?;
return Ok(());
}
let mut args: Vec<String> = Vec::new();
if let Some(artist) = &self.artist {
args.push(format!("--set-tag=ARTIST={artist}"));
}
if let Some(album) = &self.album {
args.push(format!("--set-tag=ALBUM={album}"));
}
if let Some(title) = &self.title {
args.push(format!("--set-tag=TITLE={title}"));
}
if let Some(track) = &self.track_number {
args.push(format!("--set-tag=TRACKNUMBER={track}"));
}
if let Some(disc) = &self.disc_number {
args.push(format!("--set-tag=DISCNUMBER={disc}"));
}
if let Some(date) = &self.date {
args.push(format!("--set-tag=DATE={date}"));
}
for (key, value) in &self.extra_tags {
args.push(format!("--set-tag={key}={value}"));
}
if args.is_empty() {
return Ok(());
}
TokioCommand::new(METAFLAC)
.args(&args)
.arg(path)
.run()
.await
.map_err(Failure::wrap(SampleAction::SetTags))?;
Ok(())
}
async fn apply_picture(
&self,
flac_path: &Path,
image_path: &Path,
) -> Result<(), Failure<SampleAction>> {
let spec = format!("3|image/png|||{}", image_path.display());
TokioCommand::new(METAFLAC)
.arg(format!("--import-picture-from={spec}"))
.arg(flac_path)
.run()
.await
.map_err(Failure::wrap(SampleAction::ImportPicture))?;
Ok(())
}
}