From c6cde8ffbfde8bd7f2a5704e559c1b0a9122fcc4 Mon Sep 17 00:00:00 2001 From: brevalferrari Date: Thu, 5 Jun 2025 17:47:28 +0200 Subject: [PATCH] export mode --- .gitignore | 7 ++- src/cli/cli.rs | 1 + src/cli/main.rs | 138 +++++++++++++++++++++++++++++++----------------- 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index b4836d2..6f5d1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,9 @@ out/ *.data # Samply -*.json.gz \ No newline at end of file +*.json.gz + +# audio files +*.mp3 +*.raw +*.wav \ No newline at end of file diff --git a/src/cli/cli.rs b/src/cli/cli.rs index 1d662b0..38b834e 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -268,6 +268,7 @@ pub(super) struct ExportOpts { #[arg(short, long, value_parser = audio_format_parser)] pub(super) format: AudioFormat, /// Output file [default: stdout] + #[arg(short, long)] pub(super) output: Option>, } diff --git a/src/cli/main.rs b/src/cli/main.rs index b694a63..652efe7 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,5 +1,10 @@ mod cli; -use std::{collections::HashMap, io::read_to_string, iter::once}; +use std::{ + collections::HashMap, + fs::File, + io::{Cursor, Write, read_to_string, stdout}, + iter::once, +}; use anyhow::{Context as _, anyhow}; use bliplib::{ @@ -9,9 +14,12 @@ use bliplib::{ use clap::Parser as _; use cli::Cli; use dasp_sample::Sample; +use hound::{SampleFormat, WavSpec, WavWriter}; use log::{debug, warn}; use rodio::{OutputStream, Sink, buffer::SamplesBuffer}; +use crate::cli::{ExportOpts, PlayOpts}; + fn main() -> anyhow::Result<()> { env_logger::init(); let cli = Cli::parse(); @@ -25,53 +33,7 @@ fn main() -> anyhow::Result<()> { let sink = Sink::try_new(&stream_handle).context("Epic audio playback failure")?; debug!("audio sink acquired"); - let default_variables = [ - ('l', 4f64), - ('L', 0.0), - ('t', 0.0), - ('T', 60.0), - ('N', opts.notes().len() as f64), - ]; - - debug!("building parser"); - let parser = Parser::new( - opts.notes(), - opts.slopes() - .map(|(s, (v, e))| (s, VariableChange(*v, e.clone()))) - .collect::>(), - opts.variables() - .chain(HashMap::from(default_variables).iter()) - .map(|(v, _)| *v) - .collect::>(), - ); - debug!("reading input"); - let input = read_to_string(opts.input().get()).context("Failed to read input")?; - debug!("parsing tokens"); - let tokens = parser - .parse_all(&input) - .map_err(|e| anyhow!("{e}")) - .context("Failed to parse input")?; - debug!("found {} tokens", tokens.as_ref().len()); - if tokens.as_ref().is_empty() { - warn!("0 tokens parsed"); - } - - debug!("building compiler"); - let compiler = Compiler::from(Context::new( - 'L'.to_string(), - 'n'.to_string(), - opts.variables() - .map(|(a, b)| (a.to_string(), *b)) - .chain(default_variables.map(|(c, v)| (c.to_string(), v))), - opts.instrument().clone(), - opts.slopes() - .map(|(_, (a, b))| (a.to_string(), b.clone())) - .chain(once(('L'.to_string(), opts.length().clone()))), - )); - debug!("compiling to samples"); - let samples: Vec = compiler - .compile_all(tokens) - .context("Failed to process input tokens")? + let samples: Vec = parse_and_compile(&opts)? .into_iter() .map(Sample::to_sample) .collect(); @@ -85,8 +47,86 @@ fn main() -> anyhow::Result<()> { debug!("sleeping until end of sink"); sink.sleep_until_end(); } - Export(_opts) => todo!(), + Export(ExportOpts { + playopts, + format: _format, + output, + }) => { + let samples = parse_and_compile(&playopts)?; + let mut buff = Cursor::new(Vec::with_capacity(samples.len() * 8)); + { + let mut writer = WavWriter::new( + &mut buff, + WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE as u32, + bits_per_sample: 32, + sample_format: SampleFormat::Float, + }, + ) + .context("Failed to create WAV writer")?; + for sample in samples { + writer.write_sample(sample.to_sample::())?; + } + } + let mut writer: Box = output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())); + writer.write_all(buff.get_ref())?; + } Memo(_opts) => todo!(), } Ok(()) } + +fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result> { + let default_variables = [ + ('l', 4f64), + ('L', 0.0), + ('t', 0.0), + ('T', 60.0), + ('N', opts.notes().len() as f64), + ]; + + debug!("building parser"); + let parser = Parser::new( + opts.notes(), + opts.slopes() + .map(|(s, (v, e))| (s, VariableChange(*v, e.clone()))) + .collect::>(), + opts.variables() + .chain(HashMap::from(default_variables).iter()) + .map(|(v, _)| *v) + .collect::>(), + ); + debug!("reading input"); + let input = read_to_string(opts.input().get()).context("Failed to read input")?; + debug!("parsing tokens"); + let tokens = parser + .parse_all(&input) + .map_err(|e| anyhow!("{e}")) + .context("Failed to parse input")?; + debug!("found {} tokens", tokens.as_ref().len()); + if tokens.as_ref().is_empty() { + warn!("0 tokens parsed"); + } + + debug!("building compiler"); + let compiler = Compiler::from(Context::new( + 'L'.to_string(), + 'n'.to_string(), + opts.variables() + .map(|(a, b)| (a.to_string(), *b)) + .chain(default_variables.map(|(c, v)| (c.to_string(), v))), + opts.instrument().clone(), + opts.slopes() + .map(|(_, (a, b))| (a.to_string(), b.clone())) + .chain(once(('L'.to_string(), opts.length().clone()))), + )); + debug!("compiling to samples"); + compiler + .compile_all(tokens) + .context("Failed to process input tokens") +}