Skip to main content

encode/
encode.rs

1//! Encode a WAV file to a playable `.opus` file.
2//!
3//!   cargo run --release --example encode -- input.wav output.opus [bitrate]
4//!
5//! The output is a standard Ogg Opus stream: `ffplay`, VLC and `opusdec` will
6//! all open it.
7//!
8//! It is also *gapless*: decoded and trimmed as RFC 7845 says, it comes back as
9//! exactly the samples that went in, not a frame more or less. Two things have
10//! to be right for that, and neither is automatic. See "Ending the file exactly"
11//! below; `examples/decode.rs` is the other half.
12
13#[path = "common/wav.rs"]
14mod wav;
15
16use opus_pure::{Application, MAX_PACKET_BYTES, OggOpusWriter, OpusEncoder, OpusHead, OpusTags};
17
18fn main() -> Result<(), Box<dyn std::error::Error>> {
19    let args: Vec<String> = std::env::args().collect();
20    if args.len() < 3 {
21        eprintln!("usage: {} <input.wav> <output.opus> [bitrate_bps]", args[0]);
22        std::process::exit(2);
23    }
24    let bitrate: i32 = args
25        .get(3)
26        .map(|s| s.parse())
27        .transpose()?
28        .unwrap_or(64_000);
29
30    let input = wav::read(&args[1])?;
31    if input.samples.is_empty() {
32        return Err(format!("{}: no audio samples", args[1]).into());
33    }
34    let channels = input.channels as usize;
35    let rate = input.sample_rate as i32;
36
37    // Opus codes 20 ms frames at the encoder's own rate. The container counts
38    // everything at 48 kHz regardless, and `write_packet` reads that out of the
39    // packet rather than making us convert it.
40    let frame = (rate / 50) as usize;
41
42    let mut encoder = OpusEncoder::new(rate, channels, Application::Audio)?;
43    encoder.bitrate_bps = bitrate;
44
45    let mut tags = OpusTags::new();
46    tags.push("ENCODER", concat!("opus-pure ", env!("CARGO_PKG_VERSION")))?;
47    // Built from the encoder, so the pre-skip is that encoder's real delay
48    // rather than the conventional constant.
49    let head = OpusHead::for_encoder(&encoder, input.sample_rate);
50
51    // ---- Ending the file exactly (RFC 7845 §4.2 and §4.4) ----
52    //
53    // Every Opus rate divides 48 kHz, so one encoder-rate sample is this many
54    // granule ticks and the conversions below are exact.
55    let ticks = 48_000 / rate as usize;
56    let total = input.samples.len() / channels; // sample frames of real audio
57    //
58    // 1. The encoder runs `pre_skip` samples behind its input, so the last
59    //    `pre_skip` samples of the audio are still inside it when the input
60    //    runs out. Feeding that much extra silence is what flushes them; stop
61    //    at the audio and the tail is simply lost. Then round up to a whole
62    //    frame, because Opus has no partial ones.
63    let lookahead = (head.pre_skip as usize).div_ceil(ticks);
64    let frames = (total + lookahead).div_ceil(frame);
65    //
66    // 2. That padding now decodes as real output, so the file has to say where
67    //    the audio stopped. The final granule position is the pre-skip plus the
68    //    audio and nothing else; a player trims back to it. The last packet
69    //    carries the difference between that and what the writer has already
70    //    counted, which is always between 1 and one frame's worth.
71    let final_granule = u64::from(head.pre_skip) + (total * ticks) as u64;
72
73    let file = std::fs::File::create(&args[2])?;
74    let mut writer = OggOpusWriter::with_tags(std::io::BufWriter::new(file), head, tags)?;
75
76    let mut packet = vec![0u8; MAX_PACKET_BYTES];
77    let mut payload_bytes = 0usize;
78    let per_frame = frame * channels;
79    let mut block = vec![0.0f32; per_frame];
80
81    for i in 0..frames {
82        // Whole frames only: past the end of the input the block is silence,
83        // which is the padding that flushes the encoder's delay.
84        let start = (i * per_frame).min(input.samples.len());
85        let end = (start + per_frame).min(input.samples.len());
86        block[..end - start].copy_from_slice(&input.samples[start..end]);
87        block[end - start..].fill(0.0);
88
89        let n = encoder.encode(&block, frame, &mut packet)?;
90        if i + 1 == frames {
91            let duration = final_granule - writer.granule() as u64;
92            writer.write_packet_with_duration(&packet[..n], duration as u32)?;
93        } else {
94            writer.write_packet(&packet[..n])?;
95        }
96        payload_bytes += n;
97    }
98    writer.finish()?;
99
100    let secs = total as f64 / rate as f64;
101    println!(
102        "{} -> {}\n  {channels} ch @ {rate} Hz, {frames} frames ({secs:.2} s of audio)\n  \
103         {payload_bytes} payload bytes = {:.1} kb/s (target {:.1} kb/s)",
104        args[1],
105        args[2],
106        payload_bytes as f64 * 8.0 / secs / 1000.0,
107        bitrate as f64 / 1000.0,
108    );
109    Ok(())
110}