audio_processor_standalone/
offline.rs

1// Copyright (c) 2022 Pedro Tacla Yamada
2//
3// The MIT License (MIT)
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21// THE SOFTWARE.
22
23use audio_garbage_collector::Handle;
24use audio_processor_traits::{AudioBuffer, AudioContext, AudioProcessor, AudioProcessorSettings};
25#[cfg(feature = "midi")]
26use audio_processor_traits::{MidiEventHandler, MidiMessageLike};
27#[cfg(feature = "midi")]
28use itertools::Itertools;
29
30#[cfg(feature = "midi")]
31use augmented_midi::{
32    MIDIFile, MIDIFileChunk, MIDIMessage, MIDIMessageNote, MIDITrackEvent, MIDITrackInner,
33};
34
35use crate::StandaloneProcessor;
36
37/// Offline rendering options
38pub struct OfflineRenderOptions<'a, Processor: StandaloneProcessor> {
39    /// The audio/MIDI processor
40    pub app: Processor,
41    /// GC handle, see <https://crates.io/crates/audio-garbage-collector>
42    pub handle: Option<&'a Handle>,
43    /// Input audio file path
44    pub input_path: &'a str,
45    /// Output audio file path
46    pub output_path: &'a str,
47    #[cfg(feature = "midi")]
48    /// MIDI input file path
49    pub midi_input_file: Option<MIDIFile<String, Vec<u8>>>,
50}
51
52/// Render a processor offline into a file.
53pub fn run_offline_render<Processor>(options: OfflineRenderOptions<Processor>)
54where
55    Processor: StandaloneProcessor,
56{
57    let OfflineRenderOptions {
58        mut app,
59        handle,
60        input_path,
61        output_path,
62        #[cfg(feature = "midi")]
63        midi_input_file,
64    } = options;
65
66    let _ = wisual_logger::try_init_from_env();
67
68    #[allow(clippy::redundant_closure)]
69    let handle = handle.unwrap_or_else(|| audio_garbage_collector::handle());
70
71    log::info!(
72        "Rendering offline input={} output={}",
73        input_path,
74        output_path
75    );
76
77    let buffer_size = 16;
78    let sample_rate = 44100.0;
79    let audio_processor_settings = AudioProcessorSettings::new(sample_rate, 2, 2, buffer_size);
80    let mut context = AudioContext::from(audio_processor_settings);
81
82    let audio_file_settings = audio_processor_file::InMemoryAudioFile::from_path(input_path)
83        .expect("Failed to read input file");
84
85    // Set-up input file
86    log::info!("Loading input file");
87    let mut audio_file_processor = audio_processor_file::AudioFileProcessor::new(
88        handle,
89        audio_file_settings,
90        audio_processor_settings,
91    );
92    audio_file_processor.prepare(&mut context);
93    let audio_file_buffer = audio_file_processor.buffer();
94    let audio_file_total_samples = audio_file_buffer[0].len();
95
96    // Set-up output file
97    log::info!("Setting-up output buffers");
98    let mut output_file_processor = audio_processor_file::OutputAudioFileProcessor::from_path(
99        audio_processor_settings,
100        output_path,
101    );
102    output_file_processor.prepare(audio_processor_settings);
103
104    // Set-up output buffer
105    let block_size = audio_processor_settings.block_size();
106    let total_blocks = audio_file_total_samples / block_size;
107    let mut buffer = AudioBuffer::empty();
108    buffer.resize(audio_processor_settings.input_channels(), block_size);
109
110    log::info!("Setting-up audio processor");
111    app.processor().prepare(&mut context);
112
113    #[cfg(feature = "midi")]
114    let midi_input_blocks = midi_input_file.map(|midi_input_file| {
115        build_midi_input_blocks(&audio_processor_settings, total_blocks, midi_input_file)
116    });
117
118    log::info!(
119        "Rendering total_blocks={} block_size={} audio_file_total_samples={}",
120        total_blocks,
121        block_size,
122        audio_file_total_samples
123    );
124    for block_num in 0..total_blocks {
125        for sample in buffer.slice_mut() {
126            *sample = 0.0;
127        }
128
129        audio_file_processor.process(&mut context, &mut buffer);
130
131        #[cfg(feature = "midi")]
132        if let Some(midi) = app.midi() {
133            if let Some(midi_input_blocks) = &midi_input_blocks {
134                let midi_block = &midi_input_blocks[block_num];
135                if !midi_block.is_empty() {
136                    log::debug!("Forwarding events {:?}", midi_block);
137                    midi.process_midi_events(midi_block);
138                }
139            }
140        }
141        #[cfg(not(feature = "midi"))]
142        let _ = block_num; // suppress unused error
143
144        app.processor().process(&mut context, &mut buffer);
145
146        output_file_processor
147            .process(&mut buffer)
148            .expect("Failed to write to WAV file");
149    }
150}
151
152#[cfg(feature = "midi")]
153#[derive(Debug)]
154struct MIDIBytes {
155    bytes: Vec<u8>,
156}
157
158#[cfg(feature = "midi")]
159impl MidiMessageLike for MIDIBytes {
160    fn is_midi(&self) -> bool {
161        true
162    }
163
164    fn bytes(&self) -> Option<&[u8]> {
165        Some(&self.bytes)
166    }
167}
168
169#[cfg(feature = "midi")]
170/// Converts a MIDI stream's delta_time into absolute ticks.
171fn convert_to_absolute_time(
172    mut events: Vec<MIDITrackEvent<Vec<u8>>>,
173) -> Vec<MIDITrackEvent<Vec<u8>>> {
174    let mut current_time = 0;
175    for event in &mut events {
176        current_time += event.delta_time;
177        event.delta_time = current_time;
178    }
179    events
180}
181
182#[cfg(feature = "midi")]
183/// Builds chunks containing MIDI messages over each block, aligned with their
184/// timing and a 120bpm tempo.
185fn build_midi_input_blocks(
186    settings: &AudioProcessorSettings,
187    total_blocks: usize,
188    midi_input_file: MIDIFile<String, Vec<u8>>,
189) -> Vec<Vec<MIDIBytes>> {
190    let tempo = 120_f32;
191    let ticks_per_quarter_note = midi_input_file.ticks_per_quarter_note() as f32;
192    let chunks = midi_input_file.chunks;
193    let track_events: Vec<MIDITrackEvent<Vec<u8>>> = chunks
194        .into_iter()
195        .filter_map(|chunk| match chunk {
196            MIDIFileChunk::Track { events } => {
197                let events = convert_to_absolute_time(events);
198                Some(events)
199            }
200            _ => None,
201        })
202        .flatten()
203        .sorted_by_key(|event| event.delta_time)
204        .collect();
205    let mut track_events_position = 0;
206    let mut result = Vec::with_capacity(total_blocks);
207    let block_size = settings.block_size as f32;
208    let inverse_sample_rate = 1.0 / settings.sample_rate;
209
210    for i in 0..total_blocks {
211        let delta_time_ticks = get_delta_time_ticks(
212            tempo,
213            ticks_per_quarter_note,
214            block_size,
215            inverse_sample_rate,
216            i,
217        );
218
219        log::debug!(
220            "Block - {} - ticks_per_beat={} - ticks={} input_len={} dt={}",
221            i,
222            ticks_per_quarter_note,
223            delta_time_ticks,
224            track_events.len(),
225            track_events[track_events_position].delta_time
226        );
227
228        let midi_track_events: Vec<&MIDITrackEvent<Vec<u8>>> = track_events
229            .iter()
230            .skip(track_events_position)
231            .filter(|event| event.delta_time <= delta_time_ticks as u32)
232            .collect();
233
234        let midi_block: Vec<MIDIBytes> = midi_track_events
235            .iter()
236            .filter_map(|event| {
237                log::debug!("Filtering MIDI event {:?}", event);
238                if let MIDITrackInner::Message(inner) = &event.inner {
239                    Some(inner)
240                } else {
241                    None
242                }
243            })
244            .filter_map(|event| match event {
245                MIDIMessage::NoteOn(MIDIMessageNote { velocity, note, .. }) => Some(MIDIBytes {
246                    bytes: vec![0x90, *note, *velocity],
247                }),
248                MIDIMessage::NoteOff(MIDIMessageNote { velocity, note, .. }) => Some(MIDIBytes {
249                    bytes: vec![0x80, *note, *velocity],
250                }),
251                _ => None,
252            })
253            .collect();
254
255        track_events_position += midi_track_events.len();
256        result.push(midi_block);
257    }
258
259    result
260}
261
262#[cfg(feature = "midi")]
263/// Returns the number of elapsed MIDI ticks based on the current block index
264fn get_delta_time_ticks(
265    tempo: f32,
266    ticks_per_quarter_note: f32,
267    block_size: f32,
268    inverse_sample_rate: f32,
269    i: usize,
270) -> f32 {
271    let time_per_block = block_size * inverse_sample_rate;
272    let delta_time_secs = (i as f32) * time_per_block;
273    let beats_per_second = tempo / 60.0;
274    let delta_time_beats = delta_time_secs * beats_per_second;
275
276    ticks_per_quarter_note * delta_time_beats
277}
278
279#[cfg(test)]
280mod test {
281    use audio_processor_testing_helpers::relative_path;
282
283    use audio_processor_traits::{AudioProcessorSettings, NoopAudioProcessor};
284    use augmented_midi::{
285        MIDIFile, MIDIFileChunk, MIDIFileDivision, MIDIFileFormat, MIDIFileHeader, MIDITrackEvent,
286        MIDITrackInner,
287    };
288
289    use crate::StandaloneAudioOnlyProcessor;
290
291    use super::*;
292
293    #[test]
294    fn test_run_offline_render() {
295        let _ = wisual_logger::try_init_from_env();
296        let input_path = relative_path!("../../../../input-files/1sec-sine.mp3");
297        let output_path = relative_path!("./test-output/offline-render-test-output.wav");
298        let options = OfflineRenderOptions {
299            app: StandaloneAudioOnlyProcessor::new(NoopAudioProcessor::new(), Default::default()),
300            handle: Some(audio_garbage_collector::handle()),
301            input_path: &input_path,
302            output_path: &output_path,
303            #[cfg(feature = "midi")]
304            midi_input_file: None,
305        };
306        run_offline_render(options);
307    }
308
309    #[cfg(feature = "midi")]
310    #[test]
311    fn test_build_midi_input_blocks_with_no_blocks() {
312        let chunks = vec![
313            MIDIFileChunk::Header(MIDIFileHeader {
314                format: MIDIFileFormat::Single,
315                num_tracks: 1,
316                division: MIDIFileDivision::TicksPerQuarterNote {
317                    ticks_per_quarter_note: 10,
318                },
319            }),
320            MIDIFileChunk::Track {
321                events: vec![
322                    MIDITrackEvent {
323                        delta_time: 0,
324                        inner: MIDITrackInner::Message(MIDIMessage::NoteOn(MIDIMessageNote {
325                            channel: 1,
326                            note: 120,
327                            velocity: 120,
328                        })),
329                    },
330                    MIDITrackEvent {
331                        delta_time: 40,
332                        inner: MIDITrackInner::Message(MIDIMessage::NoteOn(MIDIMessageNote {
333                            channel: 1,
334                            note: 120,
335                            velocity: 120,
336                        })),
337                    },
338                ],
339            },
340        ];
341
342        let midi_file = MIDIFile::new(chunks);
343        let settings = AudioProcessorSettings::default();
344        let result = build_midi_input_blocks(&settings, 0, midi_file);
345        assert_eq!(result.len(), 0);
346    }
347
348    #[cfg(feature = "midi")]
349    #[test]
350    fn test_build_midi_input_blocks() {
351        let chunks = vec![
352            MIDIFileChunk::Header(MIDIFileHeader {
353                format: MIDIFileFormat::Single,
354                num_tracks: 1,
355                division: MIDIFileDivision::TicksPerQuarterNote {
356                    ticks_per_quarter_note: 1,
357                },
358            }),
359            MIDIFileChunk::Track {
360                events: vec![
361                    MIDITrackEvent {
362                        // 0
363                        delta_time: 0,
364                        inner: MIDITrackInner::Message(MIDIMessage::NoteOn(MIDIMessageNote {
365                            channel: 1,
366                            note: 120,
367                            velocity: 120,
368                        })),
369                    },
370                    MIDITrackEvent {
371                        // 2 quarter note offset (1 secs in)
372                        delta_time: 2,
373                        inner: MIDITrackInner::Message(MIDIMessage::NoteOff(MIDIMessageNote {
374                            channel: 1,
375                            note: 120,
376                            velocity: 120,
377                        })),
378                    },
379                ],
380            },
381        ];
382
383        let midi_file = MIDIFile::new(chunks);
384        // 1000.0 samples a sec
385        // 50.0 50ms per block
386        let settings = AudioProcessorSettings::new(1000.0, 1, 1, 50);
387        let result = build_midi_input_blocks(&settings, 21, midi_file);
388        assert_eq!(result.len(), 21);
389        assert_eq!(
390            result[0].len(),
391            1,
392            "Expected 1st block to have note-on event"
393        );
394        for i in 1..19 {
395            assert!(result[i].is_empty());
396        }
397        assert_eq!(result[20].len(), 1);
398    }
399
400    #[cfg(feature = "midi")]
401    #[test]
402    fn test_get_delta_time_ticks() {
403        let delta_time_ticks = get_delta_time_ticks(
404            // 120bpm (8ms per beat)
405            120.0,
406            // 1/10 beats per tick
407            10.0,
408            // 50 samples per block (50ms per block)
409            50.0,
410            // 1000Hz - 1000 samples a second, 1ms per sample
411            1.0 / 1000.0,
412            0,
413        );
414        // First index.
415        // 0-50ms -> ~5 beats
416        assert!((delta_time_ticks - 0.0).abs() < 0.05);
417
418        let delta_time_ticks = get_delta_time_ticks(
419            // 120bpm - 500ms per beat
420            120.0,
421            // 1/10 beats per tick
422            10.0,
423            // 50 samples per block (50ms per block)
424            50.0,
425            // 1000Hz - 1000 samples a second, 1ms per sample
426            1.0 / 1000.0,
427            1,
428        );
429        // 100-150ms -> ~18 beats
430        // println!("delta_time_ticks={}", delta_time_ticks);
431        assert!((delta_time_ticks - 1.0).abs() < 0.05);
432    }
433}