1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Time constants for asry.
//!
//! asry operates on **two timebases**:
//!
//! - **Internal (analysis) timebase = `1/16_000`.** All cut decisions,
//! `SampleBuffer` indexing, and CTC alignment happen in 16 kHz
//! sample-index space.
//! - **External (output) timebase = caller-chosen.** Every public
//! [`mediatime::TimeRange`] asry emits is in the timebase of
//! the caller's first `handle_samples` call.
use NonZeroI32;
use Timebase;
/// Internal analysis sample rate. All audio fed to asry must
/// already be resampled to this rate (caller's responsibility).
pub const SAMPLE_RATE_HZ: u32 = 16_000;
/// `const fn` helper for `NonZeroI32` conversion. Panics on zero
/// input — only used at compile time on statically-nonzero values,
/// so the panic is unreachable in practice. Avoids depending on
/// `Option::unwrap` const stability.
///
/// Signed because a [`Timebase`] denominator is: ffmpeg's `AVRational`
/// is a pair of C `int`s, so mediatime spells both halves `i32`. The
/// sample rate itself stays `u32` — it counts, it does not divide.
const
const SAMPLE_RATE_NZ: NonZeroI32 = nz;
/// Internal analysis timebase (`1 / 16_000`). Used by the cut state
/// machine, the sample buffer, and the alignment pipeline. Not part
/// of asry's public output surface — every emitted `TimeRange`
/// is in the caller's external timebase.
pub const ANALYSIS_TIMEBASE: Timebase = new;
/// The PTS, in `timebase`, of the stream sample `sample`, where the
/// stream's sample 0 is at `base_pts`: the conversion every road from a
/// 16 kHz sample index to a PTS takes. `OutputClock` takes it; so does the
/// sample buffer, for its chunk and word ranges and for the PTS it expects
/// next; and so does a chunk's sub-segment, into the analysis timebase,
/// where a tick is a sample.
///
/// Nothing narrows before the rescale, and nothing saturates before the
/// sum. The whole `u64` index is rescaled from [`ANALYSIS_TIMEBASE`] in
/// `i128`, as mediatime forms the product (`sample * from.num * to.den`
/// over `from.den * to.num`), with the rounding of
/// [`Timebase::saturating_rescale`]: to nearest, halfway cases away from
/// zero, which for a count of samples is up. `base_pts` is added, and only
/// that final PTS saturates, at `i64::MAX`; the offset is never negative, so
/// the sum never falls below `base_pts`. [`sample_pts_exact`] is the same
/// PTS before that saturation. So an index above `i64::MAX` keeps its exact
/// PTS wherever `timebase` can hold it, and the map is monotone, so an
/// ordered pair of indices stays ordered. For an index of at most
/// `i64::MAX` whose rescale fits an `i64`, the result is exactly
/// `base_pts.saturating_add(ANALYSIS_TIMEBASE.saturating_rescale(sample as i64, timebase))`.
///
/// # Panics
///
/// If `timebase.num() == 0`, as `saturating_rescale` panics: a degenerate
/// timebase names one instant, so no count of its ticks measures a sample.
/// Both roads that call this reject it first: `OutputClock::new`, and
/// `Transcriber::handle_samples` and `handle_restart` for the buffer's
/// timebase.
pub
/// [`sample_pts`] before its final saturation: the exact PTS, which an
/// `i128` always holds. The sample buffer reads the stream's next PTS, and
/// the PTS of the sample a packet starts at, through it, so a sample past
/// `i64::MAX` is never read as `i64::MAX`.
///
/// # Panics
///
/// If `timebase.num() == 0`, as [`sample_pts`].
pub
/// The stream sample a PTS names: the sample nearest the instant `pts`
/// names in `timebase`, where the stream's sample 0 is at `base_pts`, before
/// any saturation. It reads [`sample_pts`] backwards, and it measures from
/// `base_pts`, which sample 0 sits on exactly, never from a rounded PTS, so
/// no rounding of another sample's PTS enters it.
///
/// It is mediatime's rescale of `pts - base_pts` into [`ANALYSIS_TIMEBASE`],
/// with its rounding (to nearest, halfway cases away from zero), formed in
/// `i128`, where it is exact for every pair of `i64` PTS. Where the distance
/// and its rescale both fit an `i64`, it is exactly
/// `timebase.saturating_rescale(pts - base_pts, ANALYSIS_TIMEBASE)`.
///
/// For `pts` at or after `base_pts`, the sample lies within half a sample of
/// the instant. On a timebase whose tick is longer than a sample, that is
/// less than half a tick; on one whose tick is a sample, the rescale is
/// exact. Either way the sample's PTS is `pts`. On a finer timebase, `pts`
/// can lie between two samples, and then no sample's PTS is `pts`.
pub