audio_processor_standalone/
offline.rs1use 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
37pub struct OfflineRenderOptions<'a, Processor: StandaloneProcessor> {
39 pub app: Processor,
41 pub handle: Option<&'a Handle>,
43 pub input_path: &'a str,
45 pub output_path: &'a str,
47 #[cfg(feature = "midi")]
48 pub midi_input_file: Option<MIDIFile<String, Vec<u8>>>,
50}
51
52pub 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 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 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 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; 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")]
170fn 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")]
183fn 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")]
263fn 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 delta_time: 0,
364 inner: MIDITrackInner::Message(MIDIMessage::NoteOn(MIDIMessageNote {
365 channel: 1,
366 note: 120,
367 velocity: 120,
368 })),
369 },
370 MIDITrackEvent {
371 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 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 120.0,
406 10.0,
408 50.0,
410 1.0 / 1000.0,
412 0,
413 );
414 assert!((delta_time_ticks - 0.0).abs() < 0.05);
417
418 let delta_time_ticks = get_delta_time_ticks(
419 120.0,
421 10.0,
423 50.0,
425 1.0 / 1000.0,
427 1,
428 );
429 assert!((delta_time_ticks - 1.0).abs() < 0.05);
432 }
433}