redact_composer_synthesis/
lib.rs

1#![deny(missing_docs, missing_debug_implementations)]
2//! Audio synthesis utilities.
3//!
4//! ## Example
5//! Synthesize a [`Composition`] to WAV format using [`SF2Synthesizer`].
6//! ```no_run
7//! # use redact_composer_core::Composition;
8//! # use redact_composer_synthesis::{SF2Synthesizer, SF2Synthesizable};
9//! let composition: Composition = todo!();
10//! let synth = SF2Synthesizer::new("./path/to/sound_font.sf2")
11//!     .expect("The SoundFont file should exist and be SF2 format");
12//! synth.synthesize(&composition)
13//!     .to_file("./path/to/output.wav")
14//!     .unwrap();
15//!
16//! // Alternatively
17//! composition.synthesize_with(&synth)
18//!     .to_file("./path/to/output.wav")
19//! .unwrap();
20//! ```
21//!
22//! ## Options
23//! [`SF2Synthesizer`] defaults to 44.1kHz sample rate with a bit-depth of 16, but can be customized
24//! if desired.
25//! ```no_run
26//! # use redact_composer_synthesis::{SF2Synthesizer, SoundFontSynthesizerOptions};
27//! let synth = SF2Synthesizer::new_with_options(
28//!     "./path/to/sound_font.sf2",
29//!     SoundFontSynthesizerOptions {
30//!         sample_rate: 96000,
31//!         bit_depth: 32, // This should be one of [8, 16, 24, 32].
32//!     }
33//! ).expect("Custom settings should be applied!");
34//! ```
35
36/// Error types which may occur during synthesis.
37pub mod error;
38#[cfg(test)]
39mod test;
40
41use crate::error::SynthesisError;
42use hound::{SampleFormat, WavSpec, WavWriter};
43use log::{debug, info};
44use midly::Smf;
45use redact_composer_core::Composition;
46use redact_composer_midi::convert::MidiConverter;
47pub use rustysynth::SoundFont;
48use rustysynth::{MidiFile, MidiFileSequencer, Synthesizer, SynthesizerSettings};
49use std::cmp::Ordering::Less;
50use std::fmt::{Debug, Formatter};
51use std::fs;
52use std::fs::File;
53use std::io::{Seek, Write};
54use std::ops::RangeFrom;
55use std::path::Path;
56use std::sync::Arc;
57use std::time::Duration;
58
59/// Result type which may produce [`SynthesisError`].
60pub type Result<T, E = SynthesisError> = std::result::Result<T, E>;
61
62/// A SoundFont [`Composition`] synthesizer (`.sf2` specifically). Outputs as WAV format.
63///
64/// Made possible by [`rustysynth`] and [`hound`] -- special thanks to their authors/contributors.
65pub struct SF2Synthesizer {
66    pub(crate) sound_font: Arc<SoundFont>,
67    pub(crate) options: SoundFontSynthesizerOptions,
68}
69
70impl Debug for SF2Synthesizer {
71    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("SF2Synthesizer")
73            .field("sound_font", &self.sound_font.get_info().get_bank_name())
74            .field("options", &self.options)
75            .finish()
76    }
77}
78
79impl SF2Synthesizer {
80    /// Creates a new SoundFont Synthesizer from a SoundFont (.sf2) file with default options
81    /// (sample_rate = 44.1kHz, bit-depth = 16).
82    pub fn new<P: AsRef<Path>>(sf2_file: P) -> Result<SF2Synthesizer> {
83        Self::new_with_options(sf2_file, SoundFontSynthesizerOptions::default())
84    }
85
86    /// Create a new SoundFont Synthesizer with custom options.
87    pub fn new_with_options<P: AsRef<Path>>(
88        sf2_file: P,
89        options: SoundFontSynthesizerOptions,
90    ) -> Result<SF2Synthesizer> {
91        let mut sound_font_file = File::open(sf2_file)?;
92        let sound_font = SoundFont::new(&mut sound_font_file)?;
93
94        Ok(SF2Synthesizer {
95            sound_font: Arc::new(sound_font),
96            options,
97        })
98    }
99
100    /// Prepares a synthesis request for the given content. Use further chained calls to initiate
101    /// synthesis -- such as [`to_file`](SF2SynthesisRequest::to_file), [`write`](SF2SynthesisRequest::write)
102    /// or [`to_raw_stereo_waveforms`](SF2SynthesisRequest::to_raw_stereo_waveforms).
103    pub fn synthesize<'a, S: MidiBytesProvider>(
104        &'a self,
105        content: &'a S,
106    ) -> SF2SynthesisRequest<'_, S> {
107        content.synthesize_with(self)
108    }
109}
110
111impl MidiBytesProvider for Composition {
112    fn midi_bytes(&self) -> Vec<u8> {
113        let smf = MidiConverter::convert(self);
114        smf.midi_bytes()
115    }
116}
117
118impl MidiBytesProvider for Smf<'_> {
119    fn midi_bytes(&self) -> Vec<u8> {
120        let mut smf_bytes = Vec::new();
121        self.write(&mut smf_bytes).unwrap();
122
123        smf_bytes
124    }
125}
126
127impl<M: MidiBytesProvider> SF2Synthesizable<M> for M {
128    fn synthesize_with<'a>(&'a self, synth: &'a SF2Synthesizer) -> SF2SynthesisRequest<'_, M> {
129        SF2SynthesisRequest {
130            synth,
131            midi_reader: self,
132        }
133    }
134}
135
136/// A trait implemented by types which can be synthesized.
137pub trait SF2Synthesizable<M: MidiBytesProvider> {
138    /// Prepare to synthesize with a [`SF2Synthesizer`].
139    fn synthesize_with<'a>(&'a self, synth: &'a SF2Synthesizer) -> SF2SynthesisRequest<'_, M>;
140}
141
142/// Trait implemented for types which can provide midi file bytes. ([`Composition`], [`Smf`]..)
143pub trait MidiBytesProvider {
144    /// Return the midi file bytes for this type.
145    fn midi_bytes(&self) -> Vec<u8>;
146}
147
148/// A synthesis request, which can be processed to multiple output types.
149#[allow(missing_debug_implementations)]
150pub struct SF2SynthesisRequest<'a, M: MidiBytesProvider> {
151    synth: &'a SF2Synthesizer,
152    midi_reader: &'a M,
153}
154
155impl<M: MidiBytesProvider> SF2SynthesisRequest<'_, M> {
156    /// Synthesizes and writes the WAV output to the given `writer`.
157    pub fn write<W: Write + Seek>(&self, writer: W) -> Result<()> {
158        let (mut left, mut right) = self.to_raw_stereo_waveforms()?;
159
160        info!("Writing WAV output.");
161        let wav_spec = WavSpec {
162            channels: 2,
163            sample_rate: self.synth.options.sample_rate,
164            bits_per_sample: self.synth.options.bit_depth as u16,
165            sample_format: SampleFormat::Int,
166        };
167
168        normalize(&mut left, &mut right);
169
170        let bit_depth_max_val = 2_i64.pow((wav_spec.bits_per_sample - 1).into()) - 1;
171        let mut writer = WavWriter::new(writer, wav_spec)?;
172        for (ls, rs) in left.into_iter().zip(right.into_iter()) {
173            writer.write_sample((ls * bit_depth_max_val as f32) as i32)?;
174            writer.write_sample((rs * bit_depth_max_val as f32) as i32)?;
175        }
176
177        Ok(writer.finalize()?)
178    }
179    /// Synthesizes and writes the WAV output to the given file -- overwriting if already present.
180    pub fn to_file<P: AsRef<Path>>(&self, filename: P) -> Result<()> {
181        let path = filename.as_ref();
182        if let Some(dir) = path.parent() {
183            fs::create_dir_all(dir)?
184        }
185        let file = File::create(path)?;
186        let buf_writer = std::io::BufWriter::new(file);
187        self.write(buf_writer)?;
188
189        info!("Output written to '{}'", path.display());
190
191        Ok(())
192    }
193
194    /// Synthesizes and returns the raw stereo waveforms as `(Vec<f32>, Vec<f32>)` (left and right channels).
195    pub fn to_raw_stereo_waveforms(&self) -> Result<(Vec<f32>, Vec<f32>)> {
196        info!("Synthesizing...");
197        debug!("{:?}", self.synth.options);
198        let start_instant = std::time::Instant::now();
199        let midi_bytes = self.midi_reader.midi_bytes();
200        let midi_file = Arc::new(MidiFile::new(&mut &midi_bytes[..])?);
201
202        // Create a RustySynth MIDI file sequencer.
203        let settings = SynthesizerSettings::new(self.synth.options.sample_rate as i32);
204        let synthesizer = Synthesizer::new(&self.synth.sound_font, &settings)?;
205        let mut sequencer = MidiFileSequencer::new(synthesizer);
206
207        // Play our midi file through the sequencer
208        sequencer.play(&midi_file, false);
209
210        // Create two sample buffers for left and right stereo channels
211        // Adds an additional 10 seconds at the end to account for trailoff
212        let sample_count = (settings.sample_rate as f64 * (midi_file.get_length() + 10.0)) as usize;
213        let mut left: Vec<f32> = vec![0_f32; sample_count];
214        let mut right: Vec<f32> = vec![0_f32; sample_count];
215
216        // Render the waveforms into the sample buffers.
217        sequencer.render(&mut left[..], &mut right[..]);
218
219        // Trim the final period of silence at the end of the buffers
220        let end_trim_range = get_end_trim_range(&left, &right);
221        [&mut left, &mut right].into_iter().for_each(|ch| {
222            ch.drain(end_trim_range.clone());
223        });
224
225        let audio_duration =
226            Duration::from_secs_f32(left.len() as f32 / settings.sample_rate as f32);
227        let duration = std::time::Instant::now().duration_since(start_instant);
228        info!(
229            "Synthesis complete ({:?}). Synthesized {:?} of audio.",
230            duration, audio_duration
231        );
232
233        Ok((left, right))
234    }
235}
236
237// Scales the left/right sample buffers so their samples fit snuggly in the range [-1.0, 1.0].
238fn normalize(left: &mut [f32], right: &mut [f32]) {
239    let abs_max = left
240        .iter()
241        .chain(right.iter())
242        .map(|s| s.abs())
243        .max_by(|a, b| a.partial_cmp(b).unwrap_or(Less));
244
245    if let Some(max) = abs_max {
246        for s in left.iter_mut().chain(right.iter_mut()) {
247            *s /= max;
248        }
249    }
250}
251
252// Finds the tail range of silence in the stereo channel samples
253fn get_end_trim_range(left: &[f32], right: &[f32]) -> RangeFrom<usize> {
254    let end = left
255        .iter()
256        .zip(right.iter())
257        .enumerate()
258        .fold(
259            0,
260            |end, (idx, (ls, rs))| {
261                if ls != &0.0 && rs != &0.0 {
262                    idx
263                } else {
264                    end
265                }
266            },
267        );
268
269    end..
270}
271
272/// Options to configure a [`SF2Synthesizer`].
273#[derive(Debug, Copy, Clone)]
274pub struct SoundFontSynthesizerOptions {
275    /// Sample rate in Hz. Default: 44100
276    pub sample_rate: u32,
277    /// Bit depth of the WAV output, must be one of [8, 16, 24, 32]. Default: 16.
278    pub bit_depth: u8,
279}
280
281impl Default for SoundFontSynthesizerOptions {
282    fn default() -> Self {
283        SoundFontSynthesizerOptions {
284            sample_rate: 44100,
285            bit_depth: 16,
286        }
287    }
288}