1use crate::composition::PulseSong;
2use crate::error::{PulseError, PulseResult};
3use std::path::Path;
4use tunes::engine::AudioEngine;
5use tunes::track::{AudioEvent, Mixer};
6
7const DEFAULT_NORMALIZED_SAMPLE_RATE: u32 = 44_100;
8const DEFAULT_NORMALIZED_FLAC_BITS_PER_SAMPLE: u32 = 16;
9pub(crate) const PCM_CHANNELS: usize = 2;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NormalizeMode {
14 Peak,
16 Rms,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct NormalizeOptions {
23 mode: NormalizeMode,
24 target_db: f32,
25 peak_ceiling_db: f32,
26}
27
28impl NormalizeOptions {
29 pub fn peak(target_db: f32) -> PulseResult<Self> {
31 validate_normalize_db("target_db", target_db)?;
32 Ok(Self {
33 mode: NormalizeMode::Peak,
34 target_db,
35 peak_ceiling_db: target_db,
36 })
37 }
38
39 pub fn rms(target_db: f32, peak_ceiling_db: f32) -> PulseResult<Self> {
41 validate_normalize_db("target_db", target_db)?;
42 validate_normalize_db("true_peak_db", peak_ceiling_db)?;
43 Ok(Self {
44 mode: NormalizeMode::Rms,
45 target_db,
46 peak_ceiling_db,
47 })
48 }
49
50 pub fn mode(self) -> NormalizeMode {
52 self.mode
53 }
54
55 pub fn target_db(self) -> f32 {
57 self.target_db
58 }
59
60 pub fn peak_ceiling_db(self) -> f32 {
62 self.peak_ceiling_db
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct MidiExportOptions {
69 velocity_gain: f32,
70 min_velocity: Option<f32>,
71}
72
73impl Default for MidiExportOptions {
74 fn default() -> Self {
75 Self {
76 velocity_gain: 1.0,
77 min_velocity: None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq)]
84pub struct ExportOptions {
85 gpu: bool,
86 sample_rate: Option<u32>,
87 normalize: Option<NormalizeOptions>,
88 flac_bits_per_sample: Option<u32>,
89 midi: MidiExportOptions,
90}
91
92impl ExportOptions {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 #[must_use]
100 pub fn with_gpu(mut self, gpu: bool) -> Self {
101 self.gpu = gpu;
102 self
103 }
104
105 pub fn with_sample_rate(mut self, sample_rate: u32) -> PulseResult<Self> {
107 validate_export_sample_rate(sample_rate)?;
108 self.sample_rate = Some(sample_rate);
109 Ok(self)
110 }
111
112 pub fn with_peak_normalization(mut self, target_db: f32) -> PulseResult<Self> {
114 self.normalize = Some(NormalizeOptions::peak(target_db)?);
115 Ok(self)
116 }
117
118 pub fn with_rms_normalization(
120 mut self,
121 target_db: f32,
122 peak_ceiling_db: f32,
123 ) -> PulseResult<Self> {
124 self.normalize = Some(NormalizeOptions::rms(target_db, peak_ceiling_db)?);
125 Ok(self)
126 }
127
128 #[must_use]
130 pub fn with_normalization(mut self, normalize: NormalizeOptions) -> Self {
131 self.normalize = Some(normalize);
132 self
133 }
134
135 pub fn with_flac_bits_per_sample(mut self, bits_per_sample: u32) -> PulseResult<Self> {
142 validate_flac_bits_per_sample(bits_per_sample)?;
143 self.flac_bits_per_sample = Some(bits_per_sample);
144 Ok(self)
145 }
146
147 pub fn with_midi_velocity_gain(mut self, gain: f32) -> PulseResult<Self> {
149 validate_midi_velocity_gain(gain)?;
150 self.midi.velocity_gain = gain;
151 Ok(self)
152 }
153
154 pub fn with_midi_min_velocity(mut self, min_velocity: f32) -> PulseResult<Self> {
156 validate_midi_min_velocity(min_velocity)?;
157 self.midi.min_velocity = Some(min_velocity);
158 Ok(self)
159 }
160
161 pub fn gpu(self) -> bool {
163 self.gpu
164 }
165
166 pub fn sample_rate(self) -> Option<u32> {
168 self.sample_rate
169 }
170
171 pub fn normalize(self) -> Option<NormalizeOptions> {
173 self.normalize
174 }
175
176 pub fn flac_bits_per_sample(self) -> u32 {
178 self.flac_bits_per_sample
179 .unwrap_or(DEFAULT_NORMALIZED_FLAC_BITS_PER_SAMPLE)
180 }
181
182 pub fn has_explicit_flac_bits_per_sample(self) -> bool {
184 self.flac_bits_per_sample.is_some()
185 }
186
187 pub fn midi_velocity_gain(self) -> f32 {
189 self.midi.velocity_gain
190 }
191
192 pub fn midi_min_velocity(self) -> Option<f32> {
194 self.midi.min_velocity
195 }
196}
197
198fn validate_export_sample_rate(sample_rate: u32) -> PulseResult<()> {
199 if sample_rate == 0 {
200 return Err(PulseError::InvalidExportSampleRate { sample_rate });
201 }
202
203 Ok(())
204}
205
206fn validate_normalize_db(option: &str, value: f32) -> PulseResult<()> {
207 if value.is_finite() && (-60.0..=0.0).contains(&value) {
208 Ok(())
209 } else {
210 Err(PulseError::InvalidExportNormalizeOption {
211 option: option.to_string(),
212 value: value.to_string(),
213 })
214 }
215}
216
217fn validate_flac_bits_per_sample(bits_per_sample: u32) -> PulseResult<()> {
218 if matches!(bits_per_sample, 16 | 24) {
219 Ok(())
220 } else {
221 Err(PulseError::InvalidExportFlacBitsPerSample { bits_per_sample })
222 }
223}
224
225fn validate_midi_velocity_gain(gain: f32) -> PulseResult<()> {
226 if gain.is_finite() && gain > 0.0 && gain <= 16.0 {
227 Ok(())
228 } else {
229 Err(PulseError::InvalidExportMidiOption {
230 option: "midi_velocity_gain".to_string(),
231 value: gain.to_string(),
232 })
233 }
234}
235
236fn validate_midi_min_velocity(min_velocity: f32) -> PulseResult<()> {
237 if min_velocity.is_finite() && (0.0..=1.0).contains(&min_velocity) {
238 Ok(())
239 } else {
240 Err(PulseError::InvalidExportMidiOption {
241 option: "midi_min_velocity".to_string(),
242 value: min_velocity.to_string(),
243 })
244 }
245}
246
247pub(crate) fn path_to_string(path: &Path) -> PulseResult<String> {
249 path.to_str()
250 .map(str::to_string)
251 .ok_or_else(|| PulseError::InvalidExportPath {
252 path: path.to_path_buf(),
253 })
254}
255
256pub(crate) fn prepare_output_path(path: &Path) -> PulseResult<String> {
258 if let Some(parent) = path.parent() {
259 if !parent.as_os_str().is_empty() {
260 std::fs::create_dir_all(parent).map_err(|error| PulseError::ExportFailed {
261 message: error.to_string(),
262 })?;
263 }
264 }
265
266 path_to_string(path)
267}
268
269fn audio_engine(options: ExportOptions) -> PulseResult<AudioEngine> {
270 if options.gpu() {
271 return gpu_audio_engine();
272 }
273
274 AudioEngine::new().map_err(|error| PulseError::ExportFailed {
275 message: error.to_string(),
276 })
277}
278
279#[cfg(feature = "gpu")]
280fn gpu_audio_engine() -> PulseResult<AudioEngine> {
281 AudioEngine::new_with_gpu().map_err(|error| PulseError::ExportFailed {
282 message: error.to_string(),
283 })
284}
285
286#[cfg(not(feature = "gpu"))]
287fn gpu_audio_engine() -> PulseResult<AudioEngine> {
288 Err(PulseError::GpuFeatureNotEnabled)
289}
290
291fn prepare_mixer_for_export(mixer: &mut Mixer, options: ExportOptions) -> PulseResult<()> {
292 if options.gpu() {
293 enable_mixer_gpu(mixer)?;
294 }
295
296 Ok(())
297}
298
299fn normalized_sample_rate(options: ExportOptions) -> u32 {
300 options
301 .sample_rate()
302 .unwrap_or(DEFAULT_NORMALIZED_SAMPLE_RATE)
303}
304
305fn render_normalized_buffer(
306 mixer: &mut Mixer,
307 options: ExportOptions,
308) -> PulseResult<(Vec<f32>, u32)> {
309 let sample_rate = normalized_sample_rate(options);
310 validate_export_sample_rate(sample_rate)?;
311 prepare_mixer_for_export(mixer, options)?;
312 let mut buffer = mixer.render_to_buffer(sample_rate as f32);
313 if let Some(normalize) = options.normalize() {
314 normalize_buffer(&mut buffer, normalize);
315 }
316 Ok((buffer, sample_rate))
317}
318
319fn normalize_buffer(buffer: &mut [f32], options: NormalizeOptions) {
320 if buffer.is_empty() {
321 return;
322 }
323
324 let current_peak = peak(buffer);
325 if current_peak <= f32::EPSILON {
326 return;
327 }
328
329 let gain = match options.mode() {
330 NormalizeMode::Peak => db_to_linear(options.target_db()) / current_peak,
331 NormalizeMode::Rms => {
332 let current_rms = rms(buffer);
333 if current_rms <= f32::EPSILON {
334 return;
335 }
336 db_to_linear(options.target_db()) / current_rms
337 }
338 };
339
340 if !gain.is_finite() {
341 return;
342 }
343
344 for sample in &mut *buffer {
345 *sample = (*sample * gain).clamp(-1.0, 1.0);
346 }
347
348 if matches!(options.mode(), NormalizeMode::Rms) {
349 limit_buffer_peaks(buffer, db_to_linear(options.peak_ceiling_db()));
350 }
351}
352
353fn peak(buffer: &[f32]) -> f32 {
354 buffer
355 .iter()
356 .fold(0.0_f32, |current, sample| current.max(sample.abs()))
357}
358
359fn rms(buffer: &[f32]) -> f32 {
360 let energy = buffer.iter().map(|sample| sample * sample).sum::<f32>() / buffer.len() as f32;
361 energy.sqrt()
362}
363
364fn db_to_linear(db: f32) -> f32 {
365 10.0_f32.powf(db / 20.0)
366}
367
368fn limit_buffer_peaks(buffer: &mut [f32], ceiling: f32) {
369 if !ceiling.is_finite() || ceiling <= 0.0 {
370 return;
371 }
372
373 let ceiling = ceiling.min(1.0);
374 for sample in buffer {
375 let magnitude = sample.abs();
376 if magnitude > ceiling {
377 *sample = sample.signum() * ceiling;
378 }
379 }
380}
381
382fn write_wav_buffer(path: &str, sample_rate: u32, buffer: &[f32]) -> PulseResult<()> {
383 let spec = hound::WavSpec {
384 channels: 2,
385 sample_rate,
386 bits_per_sample: 16,
387 sample_format: hound::SampleFormat::Int,
388 };
389 let mut writer =
390 hound::WavWriter::create(path, spec).map_err(|error| PulseError::ExportFailed {
391 message: error.to_string(),
392 })?;
393
394 for &sample in buffer {
395 let sample = (sample.clamp(-1.0, 1.0) * 32767.0).round() as i16;
396 writer
397 .write_sample(sample)
398 .map_err(|error| PulseError::ExportFailed {
399 message: error.to_string(),
400 })?;
401 }
402
403 writer.finalize().map_err(|error| PulseError::ExportFailed {
404 message: error.to_string(),
405 })
406}
407
408pub(crate) fn render_mixer_pcm_i16(
410 mixer: &mut Mixer,
411 options: ExportOptions,
412) -> PulseResult<(Vec<i16>, u32)> {
413 let (buffer, sample_rate) = render_normalized_buffer(mixer, options)?;
414 let samples = buffer
415 .iter()
416 .map(|sample| float_sample_to_i16(*sample))
417 .collect();
418 Ok((samples, sample_rate))
419}
420
421fn float_sample_to_i16(sample: f32) -> i16 {
422 (sample.clamp(-1.0, 1.0) * 32767.0).round() as i16
423}
424
425fn write_flac_buffer(
426 path: &str,
427 sample_rate: u32,
428 bits_per_sample: u32,
429 buffer: &[f32],
430) -> PulseResult<()> {
431 use flacenc::component::BitRepr;
432 use flacenc::error::Verify;
433 use flacenc::source::MemSource;
434
435 validate_flac_bits_per_sample(bits_per_sample)?;
436 let scale = ((1_i64 << (bits_per_sample - 1)) - 1) as f32;
437
438 let samples_i32 = buffer
439 .iter()
440 .map(|sample| (sample.clamp(-1.0, 1.0) * scale).round() as i32)
441 .collect::<Vec<_>>();
442 let config = flacenc::config::Encoder::default()
443 .into_verified()
444 .map_err(|(_, error)| PulseError::ExportFailed {
445 message: format!("flac config error: {error:?}"),
446 })?;
447 let source = MemSource::from_samples(
448 &samples_i32,
449 2,
450 bits_per_sample as usize,
451 sample_rate as usize,
452 );
453 let flac_stream = flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
454 .map_err(|error| PulseError::ExportFailed {
455 message: format!("flac encoding failed: {error:?}"),
456 })?;
457 let mut sink = flacenc::bitsink::ByteSink::new();
458 flac_stream
459 .write(&mut sink)
460 .map_err(|error| PulseError::ExportFailed {
461 message: format!("flac write failed: {error:?}"),
462 })?;
463
464 std::fs::write(path, sink.as_slice()).map_err(|error| PulseError::ExportFailed {
465 message: error.to_string(),
466 })
467}
468
469pub(crate) fn apply_midi_export_options(mixer: &mut Mixer, options: ExportOptions) {
471 let gain = options.midi_velocity_gain();
472 let min_velocity = options.midi_min_velocity();
473 if (gain - 1.0).abs() < f32::EPSILON && min_velocity.is_none() {
474 return;
475 }
476
477 for track in mixer.all_tracks_mut() {
478 let track_volume = track.volume;
479 for event in &mut track.events {
480 match event {
481 AudioEvent::Note(note) => {
482 let combined = (note.velocity * track_volume).clamp(0.0, 1.0);
483 note.velocity = lifted_velocity(combined, gain, min_velocity);
484 }
485 AudioEvent::Drum(drum) => {
486 let combined = (drum.velocity * track_volume).clamp(0.0, 1.0);
487 drum.velocity = lifted_velocity(combined, gain, min_velocity);
488 }
489 _ => {}
490 }
491 }
492 track.volume = 1.0;
493 }
494}
495
496fn lifted_velocity(value: f32, gain: f32, min_velocity: Option<f32>) -> f32 {
497 if value <= 0.0 {
498 return 0.0;
499 }
500
501 let boosted = (value * gain).clamp(0.0, 1.0);
502 min_velocity
503 .map(|minimum| boosted.max(minimum))
504 .unwrap_or(boosted)
505}
506
507#[cfg(feature = "gpu")]
508fn enable_mixer_gpu(mixer: &mut Mixer) -> PulseResult<()> {
509 mixer.enable_gpu();
510 Ok(())
511}
512
513#[cfg(not(feature = "gpu"))]
514fn enable_mixer_gpu(_mixer: &mut Mixer) -> PulseResult<()> {
515 Err(PulseError::GpuFeatureNotEnabled)
516}
517
518pub(crate) fn export_mixer_wav_with_options(
520 mixer: &mut Mixer,
521 path: impl AsRef<Path>,
522 options: ExportOptions,
523) -> PulseResult<()> {
524 let path_text = prepare_output_path(path.as_ref())?;
525 if options.normalize().is_some() {
526 let (buffer, sample_rate) = render_normalized_buffer(mixer, options)?;
527 return write_wav_buffer(&path_text, sample_rate, &buffer);
528 }
529
530 if let Some(sample_rate) = options.sample_rate() {
531 prepare_mixer_for_export(mixer, options)?;
532 return mixer.export_wav(&path_text, sample_rate).map_err(|error| {
533 PulseError::ExportFailed {
534 message: error.to_string(),
535 }
536 });
537 }
538
539 let engine = audio_engine(options)?;
540
541 engine
542 .export_wav(mixer, &path_text)
543 .map_err(|error| PulseError::ExportFailed {
544 message: error.to_string(),
545 })
546}
547
548pub(crate) fn export_mixer_flac_with_options(
550 mixer: &mut Mixer,
551 path: impl AsRef<Path>,
552 options: ExportOptions,
553) -> PulseResult<()> {
554 let path_text = prepare_output_path(path.as_ref())?;
555 if options.normalize().is_some() || options.has_explicit_flac_bits_per_sample() {
556 let (buffer, sample_rate) = render_normalized_buffer(mixer, options)?;
557 return write_flac_buffer(
558 &path_text,
559 sample_rate,
560 options.flac_bits_per_sample(),
561 &buffer,
562 );
563 }
564
565 if let Some(sample_rate) = options.sample_rate() {
566 prepare_mixer_for_export(mixer, options)?;
567 return mixer.export_flac(&path_text, sample_rate).map_err(|error| {
568 PulseError::ExportFailed {
569 message: error.to_string(),
570 }
571 });
572 }
573
574 let engine = audio_engine(options)?;
575
576 engine
577 .export_flac(mixer, &path_text)
578 .map_err(|error| PulseError::ExportFailed {
579 message: error.to_string(),
580 })
581}
582
583pub fn export_wav(song: &PulseSong, path: impl AsRef<Path>) -> PulseResult<()> {
585 export_wav_with_options(song, path, ExportOptions::new())
586}
587
588pub fn export_wav_with_options(
590 song: &PulseSong,
591 path: impl AsRef<Path>,
592 options: ExportOptions,
593) -> PulseResult<()> {
594 let mut mixer = song.to_mixer()?;
595 export_mixer_wav_with_options(&mut mixer, path, options)
596}