Offline rendering and WAV helpers
examples/tutorials/src/support/mod.rs · 133 source lines
Read the chapters in source order. Select a numbered source line to expand its explanation; the line numbers and code match this snapshot exactly.
cargo test -p ravet-examples --no-default-features --lockedChapters
- Choose bounded offline constants lines 1–14
- Require explicit output selection lines 15–30
- Validate the entire clip schedule lines 31–49
- Prepare the processor and fixed event scratch lines 50–69
- Partition the schedule without losing boundary events lines 70–80
- Borrow each block without allocating lines 81–97
- End the prepared rendering session lines 98–101
- Create a file without overwriting an existing render lines 102–116
- Interleave and finalize the WAV stream lines 117–133
Choose bounded offline constants
The helper uses 48 kHz, 256-frame blocks and a 32-event fixed array. Stereo stores two complete f32 clips; that allocation and file I/O occur outside the processing callback. The Result alias lets fixture setup propagate different error types.
1//! Offline teaching helpers. File I/O and whole-clip storage belong outside audio callbacks.
Source note: Offline teaching helpers. File I/O and whole-clip storage belong outside audio callbacks.
Link to line 12use ravet_audio_file::{WaveEncoding, WaveFormat, WaveWriter};
Bring these module/type names into scope for “Choose bounded offline constants”; the surrounding chapter explains their responsibilities.
Link to line 23use ravet_foundation::{
Bring these module/type names into scope for “Choose bounded offline constants”; the surrounding chapter explains their responsibilities.
Link to line 34 AudioBuffer, AudioLayout, Event, EventData, ParameterId, ProcessBlock, ProcessConfig,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 45 ProcessStatus, Processor, Transport,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 56};
Close the surrounding scope, value or argument list in “Choose bounded offline constants”.
Link to line 67use std::{fs::File, io, path::PathBuf};
Bring these module/type names into scope for “Choose bounded offline constants”; the surrounding chapter explains their responsibilities.
Link to line 78
Blank line separating the surrounding statements.
Link to line 89pub const SAMPLE_RATE: usize = 48_000;
Declare this compile-time constant or fixed array. The value and physical/capacity meaning are given by its source comment and this chapter.
Link to line 910const BLOCK_FRAMES: usize = 256;
Declare this compile-time constant or fixed array. The value and physical/capacity meaning are given by its source comment and this chapter.
Link to line 1011const MAX_EVENTS: usize = 32;
Declare this compile-time constant or fixed array. The value and physical/capacity meaning are given by its source comment and this chapter.
Link to line 1112pub type Stereo = [Vec<f32>; 2];
Assign the computed value to pub type Stereo. The right-hand expression supplies the new value for this step.
Link to line 1213pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Choose bounded offline constants” explanation above.
Link to line 1314
Blank line separating the surrounding statements.
Link to line 14Require explicit output selection
output_path skips the executable name, requires --render, selects an optional path or the default, and refuses extra arguments. This command-line helper does not enumerate or open audio devices.
15// The only optional argument is the output file; no device or host is opened.
Source note: The only optional argument is the output file; no device or host is opened.
Link to line 1516pub fn output_path(default: &str) -> Result<PathBuf> {
Define output_path. output_path skips the executable name, requires --render, selects an optional path or the default, and refuses extra arguments. This command-line helper does not enumerate or open audio devices.
Link to line 1617 let mut args = std::env::args_os().skip(1);
Bind mutable args to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 1718 if !args.next().is_some_and(|a| a == "--render") {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Require explicit output selection”.
Link to line 1819 return Err("Rendering requires --render".into());
Exit this path with an explicit error/fault; the caller must not treat this branch as successfully applied state.
Link to line 1920 }
Close the surrounding scope, value or argument list in “Require explicit output selection”.
Link to line 2021 let selected = args.next();
Bind selected to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 2122 let path = selected.map_or_else(|| PathBuf::from(default), PathBuf::from);
Bind path to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 2223 if args.next().is_some() {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Require explicit output selection”.
Link to line 2324 return Err(
Exit this path with an explicit error/fault; the caller must not treat this branch as successfully applied state.
Link to line 2425 "usage: cargo run -p ravet-examples --bin <name> -- --render [output.wav]".into(),
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Require explicit output selection” explanation above.
Link to line 2526 );
Close the current expression or scope; no additional processing operation occurs on this line.
Link to line 2627 }
Close the surrounding scope, value or argument list in “Require explicit output selection”.
Link to line 2728 Ok(path)
Construct the success/error variant selected by the surrounding match or function return. The enclosed value becomes its payload.
Link to line 2829}
Close the surrounding scope, value or argument list in “Require explicit output selection”.
Link to line 2930
Blank line separating the surrounding statements.
Link to line 30Validate the entire clip schedule
render rejects zero-length clips, excessive events, offsets beyond the clip, decreasing event order and input channels of the wrong length. These checks precede preparation and rendering. Presence of input selects an effect layout; absence selects an instrument.
31// Events here use absolute clip offsets. Each callback receives block-local offsets.
Source note: Events here use absolute clip offsets. Each callback receives block-local offsets.
Link to line 3132pub fn render(
Define render. render rejects zero-length clips, excessive events, offsets beyond the clip, decreasing event order and input channels of the wrong length. These checks precede preparation and rendering. Presence of input selects an effect layout; absence selects an instrument.
Link to line 3233 processor: &mut impl Processor,
Define or initialize processor with the displayed type/value. Its ownership and use are described in “Validate the entire clip schedule”.
Link to line 3334 input: Option<Stereo>,
Define or initialize input with the displayed type/value. Its ownership and use are described in “Validate the entire clip schedule”.
Link to line 3435 frames: usize,
Cumulative rendered frame count for telemetry.
Link to line 3536 events: &[Event],
Define or initialize events with the displayed type/value. Its ownership and use are described in “Validate the entire clip schedule”.
Link to line 3637) -> Result<Stereo> {
Complete the function’s parameter list and specify the return type. A Result preserves the explicit success/error distinction.
Link to line 3738 if frames == 0
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Validate the entire clip schedule”.
Link to line 3839 || events.len() > MAX_EVENTS
Accept this alternative condition in the surrounding check; evaluation stops once the combined condition is true.
Link to line 3940 || events.iter().any(|event| event.frame_offset >= frames)
Borrow collection entries for iteration without moving or copying the whole collection.
Link to line 4041 || events
Accept this alternative condition in the surrounding check; evaluation stops once the combined condition is true.
Link to line 4142 .windows(2)
Call the displayed operation with these arguments as the next step in “Validate the entire clip schedule”. The chapter describes its role and validation.
Link to line 4243 .any(|pair| pair[0].frame_offset > pair[1].frame_offset)
Return true if any entry satisfies this predicate; stop at the first match.
Link to line 4344 || input
Accept this alternative condition in the surrounding check; evaluation stops once the combined condition is true.
Link to line 4445 .as_ref()
Borrow the contained value without taking ownership; an absent Option stays absent.
Link to line 4546 .is_some_and(|channels| channels.iter().any(|c| c.len() != frames))
Borrow collection entries for iteration without moving or copying the whole collection.
Link to line 4647 {
Close the surrounding scope, value or argument list in “Validate the entire clip schedule”.
Link to line 4748 return Err("invalid offline clip or event schedule".into());
Exit this path with an explicit error/fault; the caller must not treat this branch as successfully applied state.
Link to line 4849 }
Close the surrounding scope, value or argument list in “Validate the entire clip schedule”.
Link to line 49Prepare the processor and fixed event scratch
Allocate or reuse the complete output clip, then prepare the processor once with the helper’s limits. Keep a peekable ordered event iterator and initialize a fixed 32-entry scratch array; only the occupied prefix is passed to a block.
50 let effect = input.is_some();
Bind effect to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 5051 let mut output = input.unwrap_or_else(|| [vec![0.; frames], vec![0.; frames]]);
Bind mutable output to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 5152 processor.prepare(ProcessConfig::new(
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Prepare the processor and fixed event scratch” explanation above.
Link to line 5253 SAMPLE_RATE as f64,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 5354 BLOCK_FRAMES,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 5455 MAX_EVENTS,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 5556 if effect {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Prepare the processor and fixed event scratch”.
Link to line 5657 AudioLayout::STEREO
Define or initialize AudioLayout with the displayed type/value. Its ownership and use are described in “Prepare the processor and fixed event scratch”.
Link to line 5758 } else {
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Prepare the processor and fixed event scratch” explanation above.
Link to line 5859 AudioLayout::STEREO_INSTRUMENT
Define or initialize AudioLayout with the displayed type/value. Its ownership and use are described in “Prepare the processor and fixed event scratch”.
Link to line 5960 },
Close the surrounding scope, value or argument list in “Prepare the processor and fixed event scratch”.
Link to line 6061 )?)?;
The ? operator returns an error immediately on failure and unwraps the success value for the next step.
Link to line 6162 let mut pending = events.iter().peekable();
Bind mutable pending to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 6263 let mut local = [Event {
Bind mutable local to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 6364 frame_offset: 0,
The event’s timing coordinate, interpreted relative to its current clip/block context.
Link to line 6465 data: EventData::ParameterValue {
Typed event payload; timing is kept separately in frame_offset.
Link to line 6566 id: ParameterId(0),
Define or initialize id with the displayed type/value. Its ownership and use are described in “Prepare the processor and fixed event scratch”.
Link to line 6667 value: 0.,
Define or initialize value with the displayed type/value. Its ownership and use are described in “Prepare the processor and fixed event scratch”.
Link to line 6768 },
Close the surrounding scope, value or argument list in “Prepare the processor and fixed event scratch”.
Link to line 6869 }; MAX_EVENTS];
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Prepare the processor and fixed event scratch” explanation above.
Link to line 69Partition the schedule without losing boundary events
For each exclusive block end, consume events strictly before end and subtract the block start. An event at the boundary remains pending for the next block at offset zero. The struct update preserves the event payload while changing its timing coordinate.
70 for start in (0..frames).step_by(BLOCK_FRAMES) {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Partition the schedule without losing boundary events”.
Link to line 7071 let end = (start + BLOCK_FRAMES).min(frames);
Bind end to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 7172 let mut count = 0;
Bind mutable count to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 7273 while pending.peek().is_some_and(|event| event.frame_offset < end) {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Partition the schedule without losing boundary events”.
Link to line 7374 let event = *pending.next().expect("peeked event");
Bind event to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 7475 local[count] = Event {
Assign the computed value to local[count]. The right-hand expression supplies the new value for this step.
Link to line 7576 frame_offset: event.frame_offset - start,
The event’s timing coordinate, interpreted relative to its current clip/block context.
Link to line 7677 ..event
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Partition the schedule without losing boundary events” explanation above.
Link to line 7778 };
Close the surrounding scope, value or argument list in “Partition the schedule without losing boundary events”.
Link to line 7879 count += 1;
Add the computed value to count. The right-hand expression supplies the new value for this step.
Link to line 7980 }
Close the surrounding scope, value or argument list in “Partition the schedule without losing boundary events”.
Link to line 80Borrow each block without allocating
Split the two owned clip vectors into mutable channel slices. Effects use in-place storage; instruments have empty inputs and separate output storage. Process with default transport and the prepared event prefix. Any non-Continue status deactivates and returns an error.
81 let [left, right] = &mut output;
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Borrow each block without allocating” explanation above.
Link to line 8182 let mut channels = [&mut left[start..end], &mut right[start..end]];
Bind mutable channels to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 8283 let audio = if effect {
Bind audio to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 8384 AudioBuffer::in_place(&mut channels, end - start)?
Borrow the active mutable channel slices as both effect input and output. Constructing the view does not allocate audio storage.
Link to line 8485 } else {
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Borrow each block without allocating” explanation above.
Link to line 8586 AudioBuffer::separate(&[], &mut channels, end - start)?
Borrow distinct input/output slices. An instrument supplies an empty input slice; output storage already belongs to the caller.
Link to line 8687 };
Close the surrounding scope, value or argument list in “Borrow each block without allocating”.
Link to line 8788 let status = processor.process(&mut ProcessBlock::new(
Combine borrowed audio, the active input-event slice and explicit transport into one processing view.
Link to line 8889 audio,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 8990 &local[..count],
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Borrow each block without allocating” explanation above.
Link to line 9091 Transport::default(),
Define or initialize Transport with the displayed type/value. Its ownership and use are described in “Borrow each block without allocating”.
Link to line 9192 ));
Close the current expression or scope; no additional processing operation occurs on this line.
Link to line 9293 if status != ProcessStatus::Continue {
Represent a successfully processed block. The offline renderer treats any different status as a failed rendering session.
Link to line 9394 processor.deactivate();
Call the displayed operation with these arguments as the next step in “Borrow each block without allocating”. The chapter describes its role and validation.
Link to line 9495 return Err(io::Error::other(format!("offline processing failed: {status:?}")).into());
Exit this path with an explicit error/fault; the caller must not treat this branch as successfully applied state.
Link to line 9596 }
Close the surrounding scope, value or argument list in “Borrow each block without allocating”.
Link to line 9697 }
Close the surrounding scope, value or argument list in “Borrow each block without allocating”.
Link to line 97End the prepared rendering session
After the complete clip is processed, deactivate the owner and return the rendered storage. This is offline processing: returning a clip does not imply audible device output or a real-time deadline.
98 processor.deactivate();
Call the displayed operation with these arguments as the next step in “End the prepared rendering session”. The chapter describes its role and validation.
Link to line 9899 Ok(output)
Construct the success/error variant selected by the surrounding match or function return. The enclosed value becomes its payload.
Link to line 99100}
Close the surrounding scope, value or argument list in “End the prepared rendering session”.
Link to line 100101
Blank line separating the surrounding statements.
Link to line 101Create a file without overwriting an existing render
Create a parent directory when needed, then use create_new(true) so an existing output fails. Configure stereo PCM16 at 48 kHz with the same 256-frame worker scratch limit.
102pub fn write_wave(path: &std::path::Path, channels: &Stereo) -> Result<()> {
Define write_wave. Create a parent directory when needed, then use create_new(true) so an existing output fails. Configure stereo PCM16 at 48 kHz with the same 256-frame worker scratch limit.
Link to line 102103 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Create a file without overwriting an existing render”.
Link to line 103104 std::fs::create_dir_all(parent)?;
The ? operator returns an error immediately on failure and unwraps the success value for the next step.
Link to line 104105 }
Close the surrounding scope, value or argument list in “Create a file without overwriting an existing render”.
Link to line 105106 // Refuse to overwrite existing files. Use a new path for another render.
Source note: Refuse to overwrite existing files. Use a new path for another render.
Link to line 106107 let file = File::options().write(true).create_new(true).open(path)?;
Refuse to overwrite an existing file. Use a new output path for a second offline rendering.
Link to line 107108 let mut writer = WaveWriter::new(
Bind mutable writer to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 108109 file,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 109110 WaveFormat {
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Create a file without overwriting an existing render” explanation above.
Link to line 110111 channels: 2,
Define or initialize channels with the displayed type/value. Its ownership and use are described in “Create a file without overwriting an existing render”.
Link to line 111112 sample_rate: SAMPLE_RATE as u32,
Define or initialize sample_rate with the displayed type/value. Its ownership and use are described in “Create a file without overwriting an existing render”.
Link to line 112113 encoding: WaveEncoding::Pcm16,
Define or initialize encoding with the displayed type/value. Its ownership and use are described in “Create a file without overwriting an existing render”.
Link to line 113114 },
Close the surrounding scope, value or argument list in “Create a file without overwriting an existing render”.
Link to line 114115 BLOCK_FRAMES,
Supply these names as the remaining imports, arguments or fields of the surrounding declaration.
Link to line 115116 )?;
The ? operator returns an error immediately on failure and unwraps the success value for the next step.
Link to line 116Interleave and finalize the WAV stream
Copy planar left/right samples into an interleaved fixed array for each chunk. Write only the active prefix and call finish to finalize stream headers. Print the written path and frame count after successful finalization.
117 let mut interleaved = [0.; BLOCK_FRAMES * 2];
Bind mutable interleaved to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 117118 for start in (0..channels[0].len()).step_by(BLOCK_FRAMES) {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Interleave and finalize the WAV stream”.
Link to line 118119 let count = BLOCK_FRAMES.min(channels[0].len() - start);
Bind count to the expression on this and any following continuation lines. The chapter above explains how this intermediate value is used.
Link to line 119120 for frame in 0..count {
Evaluate this condition or visit this range in source order. The statements in the following scope implement “Interleave and finalize the WAV stream”.
Link to line 120121 interleaved[2 * frame] = channels[0][start + frame];
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Interleave and finalize the WAV stream” explanation above.
Link to line 121122 interleaved[2 * frame + 1] = channels[1][start + frame];
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Interleave and finalize the WAV stream” explanation above.
Link to line 122123 }
Close the surrounding scope, value or argument list in “Interleave and finalize the WAV stream”.
Link to line 123124 writer.write_frames(&interleaved[..count * 2])?;
Write the active interleaved frame prefix using worker-side I/O; this call is outside audio processing.
Link to line 124125 }
Close the surrounding scope, value or argument list in “Interleave and finalize the WAV stream”.
Link to line 125126 writer.finish()?;
Finalize the WAV stream’s bookkeeping before reporting a successful file write.
Link to line 126127 println!(
Report this explicit fixture result to stdout; printing belongs to the offline/control path, not the audio callback.
Link to line 127128 "Wrote {} ({} frames, stereo PCM16, 48 kHz)",
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Interleave and finalize the WAV stream” explanation above.
Link to line 128129 path.display(),
Continue the surrounding value or expression with this exact operand. Read it together with the preceding statement and the “Interleave and finalize the WAV stream” explanation above.
Link to line 129130 channels[0].len()
Call the displayed operation with these arguments as the next step in “Interleave and finalize the WAV stream”. The chapter describes its role and validation.
Link to line 130131 );
Close the current expression or scope; no additional processing operation occurs on this line.
Link to line 131132 Ok(())
Report successful completion without returning a payload.
Link to line 132133}
Close the surrounding scope, value or argument list in “Interleave and finalize the WAV stream”.
Link to line 133