Skip to main content

oxideav_basic/
slin.rs

1//! Asterisk-style headerless signed-linear PCM container (`.sln` / `.slin*`).
2//!
3//! An `.sln` file is the degenerate container: the entire file body is raw
4//! interleaved 16-bit little-endian PCM at a sample rate implied by the
5//! extension. No header, no magic, no trailer. Mono only in Asterisk's own
6//! usage, and that is what this demuxer assumes.
7//!
8//! Extension → sample rate (the Asterisk convention):
9//!
10//! | extension                 | sample rate |
11//! |---------------------------|-------------|
12//! | `sln` / `slin`            |  8_000 Hz   |
13//! | `sln8` / `slin8`          |  8_000 Hz   |
14//! | `sln12` / `slin12`        | 12_000 Hz   |
15//! | `sln16` / `slin16`        | 16_000 Hz   |
16//! | `sln24` / `slin24`        | 24_000 Hz   |
17//! | `sln32` / `slin32`        | 32_000 Hz   |
18//! | `sln44` / `slin44`        | 44_100 Hz   |
19//! | `sln48` / `slin48`        | 48_000 Hz   |
20//! | `sln96` / `slin96`        | 96_000 Hz   |
21//! | `sln192` / `slin192`      | 192_000 Hz  |
22//!
23//! The muxer writes raw S16LE bytes verbatim with no framing.
24
25use oxideav_core::{
26    CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, Result, SampleFormat,
27    StreamInfo, TimeBase,
28};
29use oxideav_core::{
30    ContainerRegistry, Demuxer, Muxer, ProbeData, ReadSeek, WriteSeek, PROBE_SCORE_EXTENSION,
31};
32use std::io::{Read, Seek, SeekFrom, Write};
33
34/// `(extension_without_dot, sample_rate_hz)`. The demuxer uses this table
35/// to infer the sample rate; the container registry uses it to map every
36/// listed extension to the `"slin"` container name.
37pub(crate) const EXTENSIONS: &[(&str, u32)] = &[
38    ("sln", 8_000),
39    ("slin", 8_000),
40    ("sln8", 8_000),
41    ("slin8", 8_000),
42    ("sln12", 12_000),
43    ("slin12", 12_000),
44    ("sln16", 16_000),
45    ("slin16", 16_000),
46    ("sln24", 24_000),
47    ("slin24", 24_000),
48    ("sln32", 32_000),
49    ("slin32", 32_000),
50    ("sln44", 44_100),
51    ("slin44", 44_100),
52    ("sln48", 48_000),
53    ("slin48", 48_000),
54    ("sln96", 96_000),
55    ("slin96", 96_000),
56    ("sln192", 192_000),
57    ("slin192", 192_000),
58];
59
60pub fn register(reg: &mut ContainerRegistry) {
61    reg.register_demuxer("slin", open_demuxer);
62    reg.register_muxer("slin", open_muxer);
63    for (ext, _) in EXTENSIONS {
64        reg.register_extension(ext, "slin");
65    }
66    reg.register_probe("slin", probe);
67}
68
69/// Raw PCM has no magic bytes, so this probe is only able to fire on the
70/// file extension. Returns [`PROBE_SCORE_EXTENSION`] (25) when an
71/// `.sln*` / `.slin*` extension is supplied and `0` otherwise — a weak
72/// hint that any real container probe will outrank.
73pub fn probe(p: &ProbeData) -> u8 {
74    let Some(ext) = p.ext else {
75        return 0;
76    };
77    if sample_rate_for_ext(ext).is_some() {
78        PROBE_SCORE_EXTENSION
79    } else {
80        0
81    }
82}
83
84/// Look up the sample rate implied by a bare extension (no leading dot,
85/// case-insensitive).
86fn sample_rate_for_ext(ext: &str) -> Option<u32> {
87    let lower = ext.to_ascii_lowercase();
88    EXTENSIONS
89        .iter()
90        .find_map(|(e, sr)| if *e == lower { Some(*sr) } else { None })
91}
92
93// --- Demuxer ---------------------------------------------------------------
94
95/// Default: 8 kHz — matches plain `.sln` / `.slin` (the Asterisk 1990s
96/// default). Callers that know better should open via an explicit
97/// sample-rate-bearing extension.
98const DEFAULT_SAMPLE_RATE: u32 = 8_000;
99
100fn open_demuxer(input: Box<dyn ReadSeek>, _codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
101    open_demuxer_with_rate(input, DEFAULT_SAMPLE_RATE)
102}
103
104/// Programmatic entry point: open a `.sln*` stream at an explicit sample
105/// rate. Useful when the caller demuxes from memory and cannot supply a
106/// filename hint through the registry's extension lookup.
107pub fn open_demuxer_with_rate(
108    mut input: Box<dyn ReadSeek>,
109    sample_rate: u32,
110) -> Result<Box<dyn Demuxer>> {
111    if sample_rate == 0 {
112        return Err(Error::invalid("slin demuxer: sample_rate must be > 0"));
113    }
114    // Determine total size for duration reporting without consuming the
115    // stream — non-seekable inputs will just get an empty duration.
116    let start = input.stream_position()?;
117    let end = input.seek(SeekFrom::End(0))?;
118    input.seek(SeekFrom::Start(start))?;
119    let total_bytes = end.saturating_sub(start);
120
121    let channels: u16 = 1;
122    let block_align: u64 = (SampleFormat::S16.bytes_per_sample() as u64) * channels as u64;
123    let total_samples = total_bytes / block_align;
124    let duration_micros: i64 = if sample_rate > 0 {
125        (total_samples as i128 * 1_000_000 / sample_rate as i128) as i64
126    } else {
127        0
128    };
129
130    let codec_id = CodecId::new("pcm_s16le");
131    let mut params = CodecParameters::audio(codec_id);
132    params.channels = Some(channels);
133    params.sample_rate = Some(sample_rate);
134    params.sample_format = Some(SampleFormat::S16);
135    params.bit_rate = Some(
136        (SampleFormat::S16.bytes_per_sample() as u64) * 8 * (channels as u64) * sample_rate as u64,
137    );
138
139    let time_base = TimeBase::new(1, sample_rate as i64);
140    let stream = StreamInfo {
141        index: 0,
142        time_base,
143        duration: Some(total_samples as i64),
144        start_time: Some(0),
145        params,
146    };
147
148    Ok(Box::new(SlinDemuxer {
149        input,
150        streams: vec![stream],
151        data_start: start,
152        data_end: end,
153        cursor: start,
154        block_align,
155        chunk_frames: 1024,
156        samples_emitted: 0,
157        duration_micros,
158    }))
159}
160
161struct SlinDemuxer {
162    input: Box<dyn ReadSeek>,
163    streams: Vec<StreamInfo>,
164    #[allow(dead_code)]
165    data_start: u64,
166    data_end: u64,
167    cursor: u64,
168    block_align: u64,
169    chunk_frames: u64,
170    samples_emitted: i64,
171    duration_micros: i64,
172}
173
174impl Demuxer for SlinDemuxer {
175    fn format_name(&self) -> &str {
176        "slin"
177    }
178
179    fn streams(&self) -> &[StreamInfo] {
180        &self.streams
181    }
182
183    fn next_packet(&mut self) -> Result<Packet> {
184        if self.cursor >= self.data_end {
185            return Err(Error::Eof);
186        }
187        let remaining = self.data_end - self.cursor;
188        let want_bytes = (self.chunk_frames * self.block_align).min(remaining);
189        let want_bytes = (want_bytes / self.block_align) * self.block_align;
190        if want_bytes == 0 {
191            // Trailing partial frame — drop it; raw PCM has no framing.
192            return Err(Error::Eof);
193        }
194
195        self.input.seek(SeekFrom::Start(self.cursor))?;
196        let mut buf = vec![0u8; want_bytes as usize];
197        self.input.read_exact(&mut buf)?;
198        self.cursor += want_bytes;
199
200        let stream = &self.streams[0];
201        let frames = want_bytes / self.block_align;
202        let pts = self.samples_emitted;
203        self.samples_emitted += frames as i64;
204
205        let mut pkt = Packet::new(0, stream.time_base, buf);
206        pkt.pts = Some(pts);
207        pkt.dts = Some(pts);
208        pkt.duration = Some(frames as i64);
209        pkt.flags.keyframe = true;
210        Ok(pkt)
211    }
212
213    fn duration_micros(&self) -> Option<i64> {
214        if self.duration_micros > 0 {
215            Some(self.duration_micros)
216        } else {
217            None
218        }
219    }
220}
221
222// --- Muxer -----------------------------------------------------------------
223
224fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
225    if streams.len() != 1 {
226        return Err(Error::unsupported("slin supports exactly one audio stream"));
227    }
228    let s = &streams[0];
229    if s.params.media_type != MediaType::Audio {
230        return Err(Error::invalid("slin stream must be audio"));
231    }
232    let fmt = s
233        .params
234        .sample_format
235        .or_else(|| super::pcm::sample_format_for(&s.params.codec_id))
236        .ok_or_else(|| Error::invalid("slin muxer: unknown sample format"))?;
237    if fmt != SampleFormat::S16 {
238        return Err(Error::unsupported(
239            "slin muxer requires S16LE samples (pcm_s16le or slin* codec id)",
240        ));
241    }
242    Ok(Box::new(SlinMuxer {
243        output,
244        header_written: false,
245        trailer_written: false,
246    }))
247}
248
249struct SlinMuxer {
250    output: Box<dyn WriteSeek>,
251    header_written: bool,
252    trailer_written: bool,
253}
254
255impl Muxer for SlinMuxer {
256    fn format_name(&self) -> &str {
257        "slin"
258    }
259
260    fn write_header(&mut self) -> Result<()> {
261        // No-op: slin is headerless. Tracked only so that ordering errors
262        // (write_packet before write_header) stay symmetric with other
263        // muxers in this crate.
264        if self.header_written {
265            return Err(Error::other("slin header already written"));
266        }
267        self.header_written = true;
268        Ok(())
269    }
270
271    fn write_packet(&mut self, packet: &Packet) -> Result<()> {
272        if !self.header_written {
273            return Err(Error::other("slin muxer: write_header not called"));
274        }
275        self.output.write_all(&packet.data)?;
276        Ok(())
277    }
278
279    fn write_trailer(&mut self) -> Result<()> {
280        if self.trailer_written {
281            return Ok(());
282        }
283        self.output.flush()?;
284        self.trailer_written = true;
285        Ok(())
286    }
287}