use super::super::codebook::L_SUBFR;
use super::super::ltp::{low_pass, predict};
use super::super::math::{dot_product12, isqrt_n, median5, scale_sig};
use crate::fixed_point::arith::{add, extract_h, extract_l, mult, negate, round, sub};
use crate::fixed_point::arith32::{l_deposit_h, l_mac, l_msu, l_mult, l_sub};
use crate::fixed_point::div::div_s;
use crate::fixed_point::oper32::{l_comp, l_extract, mpy_32_16};
use crate::fixed_point::shift::{l_shl, l_shr, norm_l, shl, shr};
use crate::fixed_point::types::{DspContext, Word16, Word32};
pub const PIT_MIN: i16 = 34;
pub const PIT_MAX: i16 = 231;
const PIT_FR2: i16 = 128;
const PIT_FR1_9B: i16 = 160;
const PIT_FR1_8B: i16 = 92;
const GP_CLIP: i16 = 15565;
const OPL_DECIM: i16 = 2;
const OL_MIN_LAG: usize = 17;
const OL_MAX_LAG: usize = 115;
const WSP_FRAME: usize = 128;
const WSP_HISTORY: usize = OL_MAX_LAG;
const BITS_6K60: u16 = 132;
const BITS_8K85: u16 = 177;
const L_INTERPOL1: i16 = 4;
const UP_SAMP: i16 = 4;
const CORR_WEIGHT: [i16; 199] = [
10772, 10794, 10816, 10839, 10862, 10885, 10908, 10932, 10955, 10980, 11004, 11029, 11054,
11079, 11105, 11131, 11157, 11183, 11210, 11238, 11265, 11293, 11322, 11350, 11379, 11409,
11439, 11469, 11500, 11531, 11563, 11595, 11628, 11661, 11694, 11728, 11763, 11798, 11834,
11870, 11907, 11945, 11983, 12022, 12061, 12101, 12142, 12184, 12226, 12270, 12314, 12358,
12404, 12451, 12498, 12547, 12596, 12647, 12699, 12751, 12805, 12861, 12917, 12975, 13034,
13095, 13157, 13221, 13286, 13353, 13422, 13493, 13566, 13641, 13719, 13798, 13880, 13965,
14053, 14143, 14237, 14334, 14435, 14539, 14648, 14761, 14879, 15002, 15130, 15265, 15406,
15554, 15710, 15874, 16056, 16384, 16384, 16384, 16384, 16384, 16384, 16384, 16056, 15874,
15710, 15554, 15406, 15265, 15130, 15002, 14879, 14761, 14648, 14539, 14435, 14334, 14237,
14143, 14053, 13965, 13880, 13798, 13719, 13641, 13566, 13493, 13422, 13353, 13286, 13221,
13157, 13095, 13034, 12975, 12917, 12861, 12805, 12751, 12699, 12647, 12596, 12547, 12498,
12451, 12404, 12358, 12314, 12270, 12226, 12184, 12142, 12101, 12061, 12022, 11983, 11945,
11907, 11870, 11834, 11798, 11763, 11728, 11694, 11661, 11628, 11595, 11563, 11531, 11500,
11469, 11439, 11409, 11379, 11350, 11322, 11293, 11265, 11238, 11210, 11183, 11157, 11131,
11105, 11079, 11054, 11029, 11004, 10980, 10955, 10932, 10908, 10885, 10862, 10839, 10816,
10794, 10772, 10750, 10728,
];
const INTER4_1: [i16; 32] = [
-12, -26, 32, 206, 420, 455, 73, -766, -1732, -2142, -1242, 1376, 5429, 9910, 13418, 14746,
13418, 9910, 5429, 1376, -1242, -2142, -1732, -766, 73, 455, 420, 206, 32, -26, -12, 0,
];
const HP_FEEDBACK: [i16; 4] = [8192, 21663, -19258, 5734];
const HP_FORWARD: [i16; 4] = [-3432, 10280, -10280, 3432];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PitchMode {
bits: u16,
}
impl PitchMode {
#[must_use]
pub const fn from_frame_bits(bits: u16) -> Self {
Self { bits }
}
#[must_use]
pub const fn open_loop_spans_frame(self) -> bool {
self.bits == BITS_6K60
}
#[must_use]
pub const fn has_sharp_candidate(self) -> bool {
self.bits > BITS_8K85
}
#[must_use]
pub const fn third_subframe_is_absolute(self) -> bool {
self.bits > BITS_6K60
}
#[must_use]
pub const fn lag_resolution(self) -> LagResolution {
if self.bits > BITS_8K85 {
LagResolution::NINE_BIT
} else {
LagResolution::EIGHT_BIT
}
}
#[must_use]
pub const fn interoperable_clipping(self) -> bool {
self.bits == BITS_6K60 || self.bits == BITS_8K85
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct LagResolution {
half_from: i16,
whole_from: i16,
}
impl LagResolution {
pub const EIGHT_BIT: Self = Self {
half_from: PIT_MIN,
whole_from: PIT_FR1_8B,
};
pub const NINE_BIT: Self = Self {
half_from: PIT_FR2,
whole_from: PIT_FR1_9B,
};
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct LagWindow {
pub min: i16,
pub max: i16,
}
impl LagWindow {
#[must_use]
pub fn around(ctx: &mut DspContext, lag: i16) -> Self {
let mut min = sub(ctx, Word16(lag), Word16(8)).0;
if sub(ctx, Word16(min), Word16(PIT_MIN)).0 < 0 {
min = PIT_MIN;
}
let mut max = add(ctx, Word16(min), Word16(15)).0;
if sub(ctx, Word16(max), Word16(PIT_MAX)).0 > 0 {
max = PIT_MAX;
min = sub(ctx, Word16(max), Word16(15)).0;
}
Self { min, max }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct WeightedSpeechHighPass {
feedback: [(Word16, Word16); 3],
inputs: [Word16; 3],
}
impl WeightedSpeechHighPass {
pub fn filter(&mut self, ctx: &mut DspContext, input: &[Word16], output: &mut [Word16]) {
assert!(
output.len() >= input.len(),
"the high-pass writes one output sample per input sample"
);
let [mut y3, mut y2, mut y1] = self.feedback;
let [mut x0, mut x1, mut x2] = self.inputs;
for (&sample, slot) in input.iter().zip(output.iter_mut()) {
let x3 = x2;
x2 = x1;
x1 = x0;
x0 = sample;
let mut acc = Word32(16384);
acc = l_mac(ctx, acc, y1.1, Word16(HP_FEEDBACK[1]));
acc = l_mac(ctx, acc, y2.1, Word16(HP_FEEDBACK[2]));
acc = l_mac(ctx, acc, y3.1, Word16(HP_FEEDBACK[3]));
acc = l_shr(ctx, acc, 15);
acc = l_mac(ctx, acc, y1.0, Word16(HP_FEEDBACK[1]));
acc = l_mac(ctx, acc, y2.0, Word16(HP_FEEDBACK[2]));
acc = l_mac(ctx, acc, y3.0, Word16(HP_FEEDBACK[3]));
acc = l_mac(ctx, acc, x0, Word16(HP_FORWARD[0]));
acc = l_mac(ctx, acc, x1, Word16(HP_FORWARD[1]));
acc = l_mac(ctx, acc, x2, Word16(HP_FORWARD[2]));
acc = l_mac(ctx, acc, x3, Word16(HP_FORWARD[3]));
acc = l_shl(ctx, acc, 2);
y3 = y2;
y2 = y1;
y1 = l_extract(acc);
let doubled = l_shl(ctx, acc, 1);
*slot = round(ctx, doubled);
}
self.feedback = [y3, y2, y1];
self.inputs = [x0, x1, x2];
}
pub fn rescale(&mut self, ctx: &mut DspContext, exp: i16) {
for pair in &mut self.feedback {
let widened = l_shl(ctx, l_comp(pair.0, pair.1), exp);
*pair = l_extract(widened);
}
for sample in &mut self.inputs {
let widened = l_shl(ctx, l_deposit_h(*sample), exp);
*sample = round(ctx, widened);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct OpenLoopLags {
pub first_half: i16,
pub second_half: i16,
pub smoothed_after_first_half: Option<i16>,
}
#[derive(Clone, Debug)]
pub struct OpenLoopPitch {
history: [Word16; WSP_HISTORY],
high_passed: [Word16; WSP_HISTORY + WSP_FRAME],
high_pass: WeightedSpeechHighPass,
smoothed_lag: i16,
recent_lags: [Word16; 5],
gain: Word16,
confidence: Word16,
weight_towards_history: bool,
history_shift: i16,
}
impl Default for OpenLoopPitch {
fn default() -> Self {
Self::new()
}
}
impl OpenLoopPitch {
#[must_use]
pub const fn new() -> Self {
Self {
history: [Word16(0); WSP_HISTORY],
high_passed: [Word16(0); WSP_HISTORY + WSP_FRAME],
high_pass: WeightedSpeechHighPass {
feedback: [(Word16(0), Word16(0)); 3],
inputs: [Word16(0); 3],
},
smoothed_lag: 40,
recent_lags: [Word16(40); 5],
gain: Word16(0),
confidence: Word16(0),
weight_towards_history: false,
history_shift: 0,
}
}
#[must_use]
pub const fn smoothed_lag(&self) -> i16 {
self.smoothed_lag
}
pub fn rescale(&mut self, ctx: &mut DspContext, q_exp: i16, shift: i16) {
let regained = sub(ctx, Word16(shift), Word16(self.history_shift));
let exp = add(ctx, Word16(q_exp), regained).0;
self.history_shift = shift;
scale_sig(ctx, &mut self.history, exp);
scale_sig(ctx, &mut self.high_passed[..WSP_HISTORY], exp);
self.high_pass.rescale(ctx, exp);
}
pub fn analyse(
&mut self,
ctx: &mut DspContext,
weighted: &[Word16],
mode: PitchMode,
) -> OpenLoopLags {
assert_eq!(
weighted.len(),
WSP_FRAME,
"the open-loop search wants one frame of decimated weighted speech"
);
let mut buffer = [Word16(0); WSP_HISTORY + WSP_FRAME];
buffer[..WSP_HISTORY].copy_from_slice(&self.history);
buffer[WSP_HISTORY..].copy_from_slice(weighted);
let span = if mode.open_loop_spans_frame() {
WSP_FRAME
} else {
WSP_FRAME / 2
};
let first = self.scan(ctx, &buffer, WSP_HISTORY, span);
let smoothed_after_first_half = self.absorb(ctx, first);
let second_half = if mode.open_loop_spans_frame() {
first
} else {
let second = self.scan(ctx, &buffer, WSP_HISTORY + span, span);
self.absorb(ctx, second);
second
};
self.history.copy_from_slice(&buffer[WSP_FRAME..]);
OpenLoopLags {
first_half: first * OPL_DECIM,
second_half: second_half * OPL_DECIM,
smoothed_after_first_half,
}
}
fn scan(&mut self, ctx: &mut DspContext, buffer: &[Word16], base: usize, span: usize) -> i16 {
let lag = self.best_lag(ctx, buffer, base, span);
self.gain = self.correlation_at(ctx, &buffer[base..base + span], lag);
self.high_passed.copy_within(span..span + WSP_HISTORY, 0);
lag
}
fn best_lag(&self, ctx: &mut DspContext, buffer: &[Word16], base: usize, span: usize) -> i16 {
let mut best = Word32(i32::MIN);
let mut best_lag = 0usize;
let mut taper = 198usize;
let towards_history = self.smoothed_lag > 0 && self.weight_towards_history;
let mut bump = 98 + OL_MAX_LAG
- usize::try_from(self.smoothed_lag)
.expect("the smoothed open-loop lag is a decimated lag");
for lag in (OL_MIN_LAG + 1..=OL_MAX_LAG).rev() {
let mut acc = Word32(0);
for j in 0..span {
acc = l_mac(ctx, acc, buffer[base + j], buffer[base + j - lag]);
}
let (hi, lo) = l_extract(acc);
acc = mpy_32_16(hi, lo, Word16(CORR_WEIGHT[taper]));
taper -= 1;
if towards_history {
let (hi, lo) = l_extract(acc);
acc = mpy_32_16(hi, lo, Word16(CORR_WEIGHT[bump]));
bump -= 1;
}
if l_sub(ctx, acc, best).0 >= 0 {
best = acc;
best_lag = lag;
}
}
i16::try_from(best_lag).expect("a decimated lag fits in 16 bits")
}
fn correlation_at(&mut self, ctx: &mut DspContext, input: &[Word16], lag: i16) -> Word16 {
let span = input.len();
let lag = usize::try_from(lag).expect("the open-loop lag is positive");
self.high_pass
.filter(ctx, input, &mut self.high_passed[WSP_HISTORY..]);
let mut cross = Word32(0);
let mut lagged_energy = Word32(1);
let mut energy = Word32(1);
for j in 0..span {
let here = self.high_passed[WSP_HISTORY + j];
let back = self.high_passed[WSP_HISTORY + j - lag];
cross = l_mac(ctx, cross, here, back);
lagged_energy = l_mac(ctx, lagged_energy, back, back);
energy = l_mac(ctx, energy, here, here);
}
let cross_exp = norm_l(cross);
let cross = l_shl(ctx, cross, cross_exp);
let lagged_exp = norm_l(lagged_energy);
let lagged_energy = l_shl(ctx, lagged_energy, lagged_exp);
let energy_exp = norm_l(energy);
let energy = l_shl(ctx, energy, energy_exp);
let lagged_rounded = round(ctx, lagged_energy);
let energy_rounded = round(ctx, energy);
let mut product = l_mult(ctx, lagged_rounded, energy_rounded);
let renorm = norm_l(product);
product = l_shl(ctx, product, renorm);
let mut exp = add(ctx, Word16(lagged_exp), Word16(energy_exp));
exp = add(ctx, exp, Word16(renorm));
exp = sub(ctx, Word16(62), exp);
let (inverse_root, exp) = isqrt_n(ctx, (product, exp.0));
let cross_rounded = round(ctx, cross);
let root_rounded = round(ctx, inverse_root);
let scaled = l_mult(ctx, cross_rounded, root_rounded);
let headroom = sub(ctx, Word16(31), Word16(cross_exp));
let total = add(ctx, headroom, Word16(exp)).0;
let shifted = l_shl(ctx, scaled, total);
round(ctx, shifted)
}
fn absorb(&mut self, ctx: &mut DspContext, lag: i16) -> Option<i16> {
let refreshed = if sub(ctx, self.gain, Word16(19661)).0 > 0 {
for i in (1..5).rev() {
self.recent_lags[i] = self.recent_lags[i - 1];
}
self.recent_lags[0] = Word16(lag);
self.smoothed_lag = median5(&self.recent_lags).0;
self.confidence = Word16(32767);
Some(self.smoothed_lag)
} else {
self.confidence = mult(ctx, self.confidence, Word16(29491));
None
};
self.weight_towards_history = sub(ctx, self.confidence, Word16(26214)).0 >= 0;
refreshed
}
}
#[must_use]
pub fn convolve(ctx: &mut DspContext, input: &[Word16], response: &[Word16]) -> [Word16; L_SUBFR] {
assert!(
response.len() >= input.len(),
"the impulse response must cover the whole subframe"
);
let mut output = [Word16(0); L_SUBFR];
for (n, slot) in output.iter_mut().enumerate().take(input.len()) {
let mut acc = Word32(0);
for i in 0..=n {
acc = l_mac(ctx, acc, input[i], response[n - i]);
}
*slot = round(ctx, acc);
}
output
}
#[must_use]
pub fn update_target(
ctx: &mut DspContext,
target: &[Word16],
filtered: &[Word16],
gain: Word16,
) -> [Word16; L_SUBFR] {
let mut out = [Word16(0); L_SUBFR];
for (i, slot) in out.iter_mut().enumerate() {
let mut acc = l_mult(ctx, target[i], Word16(16384));
acc = l_msu(ctx, acc, filtered[i], gain);
*slot = extract_h(l_shl(ctx, acc, 1));
}
out
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct GainCorrelations {
pub energy: Word16,
pub energy_exp: i16,
pub correlation: Word16,
pub correlation_exp: i16,
}
#[must_use]
pub fn pitch_gain(
ctx: &mut DspContext,
target: &[Word16],
filtered: &[Word16],
) -> (Word16, GainCorrelations) {
let (energy, energy_exp) = dot_product12(ctx, filtered, filtered);
let (correlation, correlation_exp) = dot_product12(ctx, target, filtered);
let energy = extract_h(energy);
let correlation = extract_h(correlation);
let coefficients = GainCorrelations {
energy,
energy_exp,
correlation,
correlation_exp,
};
if correlation.0 < 0 {
return (Word16(0), coefficients);
}
let halved = shr(ctx, correlation, 1);
let mut gain = div_s(halved, energy);
let exp = sub(ctx, Word16(correlation_exp), Word16(energy_exp)).0;
gain = shl(ctx, gain, exp);
if sub(ctx, gain, Word16(19661)).0 > 0 {
gain = Word16(19661);
}
(gain, coefficients)
}
#[derive(Clone, Copy, Debug)]
pub struct LtpDecision {
pub sharp_gain: Word16,
pub smooth_gain: Word16,
pub sharp_response: [Word16; L_SUBFR],
pub smooth_response: [Word16; L_SUBFR],
pub prefer_sharp: bool,
pub codebook_target: [Word16; L_SUBFR],
pub correlations: GainCorrelations,
}
impl LtpDecision {
#[must_use]
pub const fn gain(&self) -> Word16 {
if self.prefer_sharp {
self.sharp_gain
} else {
self.smooth_gain
}
}
#[must_use]
pub const fn response(&self) -> &[Word16; L_SUBFR] {
if self.prefer_sharp {
&self.sharp_response
} else {
&self.smooth_response
}
}
}
pub fn choose_ltp(
ctx: &mut DspContext,
excitation: &mut [Word16],
offset: usize,
target: &[Word16],
response: &[Word16],
clip: bool,
mode: PitchMode,
) -> LtpDecision {
assert!(
offset >= 1 && excitation.len() > offset + L_SUBFR,
"the long-term low-pass reads one sample either side of the subframe"
);
let mut sharp_gain = Word16(0);
let mut sharp_response = [Word16(0); L_SUBFR];
let mut sharp_target = [Word16(0); L_SUBFR];
let mut sharp_correlations = GainCorrelations::default();
if mode.has_sharp_candidate() {
sharp_response = convolve(ctx, &excitation[offset..offset + L_SUBFR], response);
let (gain, coefficients) = pitch_gain(ctx, target, &sharp_response);
sharp_gain = gain;
sharp_correlations = coefficients;
if clip && sub(ctx, sharp_gain, Word16(GP_CLIP)).0 > 0 {
sharp_gain = Word16(GP_CLIP);
}
sharp_target = update_target(ctx, target, &sharp_response, sharp_gain);
}
let smoothed = low_pass(&excitation[offset - 1..=offset + L_SUBFR]);
let smooth_response = convolve(ctx, &smoothed, response);
let (mut smooth_gain, smooth_correlations) = pitch_gain(ctx, target, &smooth_response);
if clip && sub(ctx, smooth_gain, Word16(GP_CLIP)).0 > 0 {
smooth_gain = Word16(GP_CLIP);
}
let smooth_target = update_target(ctx, target, &smooth_response, smooth_gain);
let prefer_sharp = mode.has_sharp_candidate() && {
let mut balance = Word32(0);
for &v in &sharp_target {
balance = l_mac(ctx, balance, v, v);
}
for &v in &smooth_target {
balance = l_msu(ctx, balance, v, v);
}
balance.0 <= 0
};
let codebook_target = if prefer_sharp {
sharp_target
} else {
excitation[offset..offset + L_SUBFR].copy_from_slice(&smoothed);
smooth_target
};
LtpDecision {
sharp_gain,
smooth_gain,
sharp_response,
smooth_response,
prefer_sharp,
codebook_target,
correlations: if prefer_sharp {
sharp_correlations
} else {
smooth_correlations
},
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SearchLimits {
pub window: LagWindow,
pub absolute: bool,
pub resolution: LagResolution,
}
#[must_use]
pub fn closed_loop_lag(
ctx: &mut DspContext,
excitation: &[Word16],
offset: usize,
target: &[Word16],
response: &[Word16],
limits: SearchLimits,
) -> (i16, i16) {
let SearchLimits {
window,
absolute,
resolution,
} = limits;
let t_min = sub(ctx, Word16(window.min), Word16(L_INTERPOL1)).0;
let t_max = add(ctx, Word16(window.max), Word16(L_INTERPOL1)).0;
assert!(
offset >= usize::try_from(t_max).expect("the lag window is positive"),
"the excitation history is shorter than the search window"
);
let mut correlations = [Word16(0); 40];
normalised_correlation(
ctx,
excitation,
offset,
target,
response,
(t_min, t_max),
&mut correlations,
);
let at = |t: i16| usize::try_from(t - t_min).expect("the window is ordered");
let mut best = correlations[at(window.min)];
let mut lag = window.min;
let first = add(ctx, Word16(window.min), Word16(1)).0;
for t in first..=window.max {
if sub(ctx, correlations[at(t)], best).0 >= 0 {
best = correlations[at(t)];
lag = t;
}
}
if absolute && sub(ctx, Word16(lag), Word16(resolution.whole_from)).0 >= 0 {
return (lag, 0);
}
let coarse = (absolute && sub(ctx, Word16(lag), Word16(resolution.half_from)).0 >= 0)
|| sub(ctx, Word16(resolution.half_from), Word16(PIT_MIN)).0 == 0;
let (step, mut fraction) = if coarse { (2i16, -2i16) } else { (1i16, -3i16) };
if sub(ctx, Word16(lag), Word16(window.min)).0 == 0 {
fraction = 0;
}
let centre = at(lag);
let mut best = interpolate(ctx, &correlations, centre, fraction);
let mut candidate = add(ctx, Word16(fraction), Word16(step)).0;
while candidate <= 3 {
let value = interpolate(ctx, &correlations, centre, candidate);
if sub(ctx, value, best).0 > 0 {
best = value;
fraction = candidate;
}
candidate += step;
}
if fraction < 0 {
(
sub(ctx, Word16(lag), Word16(1)).0,
add(ctx, Word16(fraction), Word16(UP_SAMP)).0,
)
} else {
(lag, fraction)
}
}
fn normalised_correlation(
ctx: &mut DspContext,
excitation: &[Word16],
offset: usize,
target: &[Word16],
response: &[Word16],
bounds: (i16, i16),
output: &mut [Word16],
) {
let (t_min, t_max) = bounds;
let back = |t: i16| offset - usize::try_from(t).expect("delays are positive");
let mut filtered = convolve(
ctx,
&excitation[back(t_min)..back(t_min) + L_SUBFR],
response,
);
let mut acc = Word32(1);
for &v in target {
acc = l_mac(ctx, acc, v, v);
}
let mut exp = sub(ctx, Word16(30), Word16(norm_l(acc)));
exp = add(ctx, exp, Word16(2));
let halved = shr(ctx, exp, 1);
let scale = negate(ctx, halved).0;
for t in t_min..=t_max {
let mut acc = Word32(1);
for i in 0..L_SUBFR {
acc = l_mac(ctx, acc, target[i], filtered[i]);
}
let shift = norm_l(acc);
let acc = l_shl(ctx, acc, shift);
let corr_exp = sub(ctx, Word16(30), Word16(shift)).0;
let corr = extract_h(acc);
let mut acc = Word32(1);
for &v in &filtered {
acc = l_mac(ctx, acc, v, v);
}
let shift = norm_l(acc);
let acc = l_shl(ctx, acc, shift);
let norm_exp = sub(ctx, Word16(30), Word16(shift)).0;
let (acc, norm_exp) = isqrt_n(ctx, (acc, norm_exp));
let norm = extract_h(acc);
let mut product = l_mult(ctx, corr, norm);
let exponents = add(ctx, Word16(corr_exp), Word16(norm_exp));
let total = add(ctx, exponents, Word16(scale)).0;
product = l_shl(ctx, product, total);
output[usize::try_from(t - t_min).expect("the window is ordered")] = round(ctx, product);
if t != t_max {
let sample = excitation[back(t + 1)];
for i in (1..L_SUBFR).rev() {
let tap = mult(ctx, sample, response[i]);
filtered[i] = add(ctx, tap, filtered[i - 1]);
}
filtered[0] = mult(ctx, sample, response[0]);
}
}
}
fn interpolate(
ctx: &mut DspContext,
correlations: &[Word16],
centre: usize,
fraction: i16,
) -> Word16 {
let (phase, mut base) = if fraction < 0 {
(add(ctx, Word16(fraction), Word16(UP_SAMP)).0, centre - 1)
} else {
(fraction, centre)
};
base -= usize::try_from(L_INTERPOL1 - 1).expect("the filter half-length is positive");
let mut acc = Word32(0);
let last_phase = sub(ctx, Word16(UP_SAMP), Word16(1));
let start = sub(ctx, last_phase, Word16(phase));
let mut k = usize::try_from(start.0).expect("the phase is 0..=3");
for i in 0..2 * usize::try_from(L_INTERPOL1).expect("the filter half-length is positive") {
acc = l_mac(ctx, acc, correlations[base + i], Word16(INTER4_1[k]));
k += usize::try_from(UP_SAMP).expect("the upsampling factor is positive");
}
let doubled = l_shl(ctx, acc, 1);
round(ctx, doubled)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct GainClipping {
isf_gap: Word16,
mean_gain: Word16,
}
impl Default for GainClipping {
fn default() -> Self {
Self::new()
}
}
impl GainClipping {
#[must_use]
pub const fn new() -> Self {
Self {
isf_gap: Word16(307),
mean_gain: Word16(9830),
}
}
pub fn observe_isf(&mut self, ctx: &mut DspContext, mode: PitchMode, isf: &[Word16]) {
assert!(isf.len() >= 16, "an AMR-WB ISF vector has 16 entries");
let mut smallest = sub(ctx, isf[1], isf[0]);
for i in 2..15 {
let gap = sub(ctx, isf[i], isf[i - 1]);
if sub(ctx, gap, smallest).0 < 0 {
smallest = gap;
}
}
let carried = l_mult(ctx, Word16(26214), self.isf_gap);
let mut smoothed = extract_h(l_mac(ctx, carried, Word16(6554), smallest));
let ceiling = if mode.interoperable_clipping() {
384
} else {
307
};
if sub(ctx, smoothed, Word16(ceiling)).0 > 0 {
smoothed = Word16(ceiling);
}
self.isf_gap = smoothed;
}
pub fn observe_gain(&mut self, ctx: &mut DspContext, mode: PitchMode, gain_pit: Word16) {
let acc = if mode.interoperable_clipping() {
let carried = l_mult(ctx, Word16(32113), self.mean_gain);
l_mac(ctx, carried, Word16(655), gain_pit)
} else {
let carried = l_mult(ctx, Word16(29491), self.mean_gain);
l_mac(ctx, carried, Word16(3277), gain_pit)
};
let mut gain = extract_h(acc);
if sub(ctx, gain, Word16(9830)).0 < 0 {
gain = Word16(9830);
}
self.mean_gain = gain;
}
#[must_use]
pub fn clips(&self, ctx: &mut DspContext, mode: PitchMode) -> bool {
if mode.interoperable_clipping() {
let scaled = extract_l(l_mult(ctx, self.isf_gap, Word16(42)));
let slope = mult(ctx, Word16(1638), scaled);
let threshold = add(ctx, Word16(14746), slope);
sub(ctx, self.mean_gain, threshold).0 > 0
} else {
sub(ctx, self.isf_gap, Word16(154)).0 < 0
&& sub(ctx, self.mean_gain, Word16(14746)).0 > 0
}
}
}
pub fn predict_adaptive(
excitation: &mut [Word16],
offset: usize,
lag: i16,
fraction: i16,
len: usize,
) {
predict(
excitation,
offset,
usize::try_from(lag).expect("the pitch lag is positive"),
u8::try_from(fraction).expect("the pitch fraction is 0..=3"),
len,
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::OnceLock;
const TRACE_BITS: u16 = 253;
const TRACE_FRAMES: usize = 3;
const SUBFRAMES: usize = 4;
const M: usize = 16;
const L_TOTAL: usize = 384;
const SPEECH_CARRY: usize = 116;
const SPEECH_OFFSET: usize = 64;
const NEW_SPEECH_OFFSET: usize = SPEECH_CARRY;
const EXC_HISTORY: usize = 248;
const EXC_TOTAL: usize = 505;
const PREEMPH_MU: i16 = 11141;
type TraceRows = HashMap<(i32, i32, String), Vec<i32>>;
fn trace() -> &'static TraceRows {
static ROWS: OnceLock<TraceRows> = OnceLock::new();
ROWS.get_or_init(|| {
let text = include_str!("../../testdata/wb_enc_trace.txt");
let mut rows = TraceRows::new();
for line in text.lines() {
let mut field = line.split_whitespace();
if field.next() != Some("T") {
continue;
}
let frame: i32 = field.next().expect("frame").parse().expect("frame");
let subframe: i32 = field.next().expect("subframe").parse().expect("subframe");
let name = field.next().expect("name").to_owned();
let values = field.map(|v| v.parse().expect("value")).collect();
rows.insert((frame, subframe, name), values);
}
assert!(!rows.is_empty(), "the encoder trace parsed to nothing");
rows
})
}
fn maybe_row(frame: usize, subframe: i32, name: &str) -> Option<&'static [i32]> {
let key = (
i32::try_from(frame).expect("frame"),
subframe,
name.to_owned(),
);
trace().get(&key).map(Vec::as_slice)
}
fn row(frame: usize, subframe: i32, name: &str) -> Vec<Word16> {
maybe_row(frame, subframe, name)
.unwrap_or_else(|| {
panic!("trace row {name} missing for frame {frame} subframe {subframe}")
})
.iter()
.map(|&v| Word16(i16::try_from(v).expect("a trace row holds Word16 values")))
.collect()
}
fn scalar(frame: usize, subframe: i32, name: &str) -> i16 {
let values = row(frame, subframe, name);
assert_eq!(values.len(), 1, "{name} is not a scalar row");
values[0].0
}
struct Outcome {
frame: usize,
subframe: usize,
lag: i16,
fraction: i16,
window: LagWindow,
adaptive: [Word16; L_SUBFR],
decision: LtpDecision,
codebook_residual: [Word16; L_SUBFR / 2],
}
struct Run {
medians: Vec<Option<i16>>,
outcomes: Vec<Outcome>,
}
fn rebuild_speech(
ctx: &mut DspContext,
decimated: &[Word16],
carry: &[Word16; SPEECH_CARRY],
memory: Word16,
scaling: (i16, i16),
) -> [Word16; L_TOTAL] {
let (q_new, q_exp) = scaling;
let mut speech = [Word16(0); L_TOTAL];
speech[..SPEECH_CARRY].copy_from_slice(carry);
scale_sig(ctx, &mut speech[..SPEECH_CARRY], q_exp);
for (i, &sample) in decimated.iter().enumerate() {
let previous = if i == 0 { memory } else { decimated[i - 1] };
let mut acc = l_mult(ctx, sample, Word16(16384));
acc = l_msu(ctx, acc, previous, Word16(PREEMPH_MU));
acc = l_shl(ctx, acc, q_new);
speech[NEW_SPEECH_OFFSET + i] = round(ctx, acc);
}
speech
}
fn lp_residual(
ctx: &mut DspContext,
speech: &[Word16; L_TOTAL],
subframe: usize,
a: &[Word16],
) -> [Word16; L_SUBFR] {
let mut residual = [Word16(0); L_SUBFR];
for (i, slot) in residual.iter_mut().enumerate() {
let at = SPEECH_OFFSET + subframe * L_SUBFR + i;
let mut acc = l_mult(ctx, speech[at], a[0]);
for (j, &coefficient) in a.iter().enumerate().skip(1) {
acc = l_mac(ctx, acc, coefficient, speech[at - j]);
}
let acc = l_shl(ctx, acc, 4);
*slot = round(ctx, acc);
}
residual
}
fn run_trace() -> Run {
let mode = PitchMode::from_frame_bits(TRACE_BITS);
let resolution = mode.lag_resolution();
let mut ctx = DspContext::default();
let mut open = OpenLoopPitch::new();
let mut clipping = GainClipping::new();
let mut speech_carry = [Word16(0); SPEECH_CARRY];
let mut preemph_memory = Word16(0);
let mut q_old: i16 = 15;
let mut exc_carry = [Word16(0); EXC_HISTORY];
let mut medians = Vec::new();
let mut outcomes = Vec::new();
for frame in 0..TRACE_FRAMES {
let q_new = scalar(frame, -1, "Q_new");
let shift = scalar(frame, -1, "wsp_shift");
let q_exp = q_new - q_old;
q_old = q_new;
let decimated = row(frame, -1, "decimated");
let speech = rebuild_speech(
&mut ctx,
&decimated,
&speech_carry,
preemph_memory,
(q_new, q_exp),
);
preemph_memory = decimated[decimated.len() - 1];
speech_carry.copy_from_slice(&speech[256..256 + SPEECH_CARRY]);
clipping.observe_isf(&mut ctx, mode, &row(frame, -1, "isf_unq46"));
open.rescale(&mut ctx, q_exp, shift);
let lags = open.analyse(&mut ctx, &row(frame, -1, "wsp"), mode);
medians.push(lags.smoothed_after_first_half);
scale_sig(&mut ctx, &mut exc_carry, q_exp);
let mut exc = [Word16(0); EXC_TOTAL];
exc[..EXC_HISTORY].copy_from_slice(&exc_carry);
let quantised_lp = row(frame, -1, "Aq");
let mut window = LagWindow::around(&mut ctx, lags.first_half);
for subframe in 0..SUBFRAMES {
let index = i32::try_from(subframe).expect("subframe");
let base = EXC_HISTORY + subframe * L_SUBFR;
let absolute =
subframe == 0 || (subframe == 2 && mode.third_subframe_is_absolute());
if subframe == 2 && mode.third_subframe_is_absolute() {
window = LagWindow::around(&mut ctx, lags.second_half);
}
let residual = lp_residual(
&mut ctx,
&speech,
subframe,
&quantised_lp[subframe * (M + 1)..(subframe + 1) * (M + 1)],
);
exc[base..base + L_SUBFR].copy_from_slice(&residual);
let target = row(frame, index, "xn");
let response = row(frame, index, "h1");
let (lag, fraction) = closed_loop_lag(
&mut ctx,
&exc,
base,
&target,
&response,
SearchLimits {
window,
absolute,
resolution,
},
);
if absolute {
window = LagWindow::around(&mut ctx, lag);
}
let clip = clipping.clips(&mut ctx, mode);
predict_adaptive(&mut exc, base, lag, fraction, L_SUBFR + 1);
let adaptive: [Word16; L_SUBFR] =
exc[base..base + L_SUBFR].try_into().expect("one subframe");
let decision = choose_ltp(&mut ctx, &mut exc, base, &target, &response, clip, mode);
let winner: [Word16; L_SUBFR] =
exc[base..base + L_SUBFR].try_into().expect("one subframe");
let mut updated = update_target(&mut ctx, &residual, &winner, decision.gain());
scale_sig(&mut ctx, &mut updated, shift);
let codebook_residual: [Word16; L_SUBFR / 2] =
updated[L_SUBFR / 2..].try_into().expect("half a subframe");
outcomes.push(Outcome {
frame,
subframe,
lag,
fraction,
window,
adaptive,
decision,
codebook_residual,
});
let final_excitation = row(frame, index, "exc_total");
exc[base..base + L_SUBFR].copy_from_slice(&final_excitation);
clipping.observe_gain(&mut ctx, mode, Word16(scalar(frame, index, "gain_pit")));
}
exc_carry.copy_from_slice(&exc[256..256 + EXC_HISTORY]);
}
Run { medians, outcomes }
}
fn compare(name: &str, outcome: &Outcome, got: &[Word16]) {
let want = row(
outcome.frame,
i32::try_from(outcome.subframe).expect("subframe"),
name,
);
assert_eq!(want.len(), got.len(), "{name}: length");
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!(
g.0, w.0,
"frame {} subframe {}: {name}[{i}] = {} but TS 26.173 gives {}",
outcome.frame, outcome.subframe, g.0, w.0
);
}
}
#[test]
fn the_open_loop_median_is_bit_exact_against_ts26173() {
let run = run_trace();
assert_eq!(run.medians.len(), TRACE_FRAMES, "expected every frame");
let mut refreshed = 0;
for (frame, got) in run.medians.iter().enumerate() {
let want = maybe_row(frame, -1, "T_op_med")
.map(|v| i16::try_from(v[0]).expect("a decimated lag fits in 16 bits"));
assert_eq!(
*got, want,
"frame {frame}: smoothed open-loop lag {got:?} against TS 26.173's {want:?}"
);
if want.is_some() {
refreshed += 1;
}
}
assert_eq!(
refreshed, 2,
"the committed trace refreshes the median on two of its three frames"
);
}
#[test]
fn the_closed_loop_lag_and_fraction_are_bit_exact_against_ts26173() {
let run = run_trace();
assert_eq!(
run.outcomes.len(),
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
for outcome in &run.outcomes {
let index = i32::try_from(outcome.subframe).expect("subframe");
assert_eq!(
outcome.lag,
scalar(outcome.frame, index, "T0"),
"frame {} subframe {}: integer lag",
outcome.frame,
outcome.subframe
);
assert_eq!(
outcome.fraction,
scalar(outcome.frame, index, "T0_frac"),
"frame {} subframe {}: fraction",
outcome.frame,
outcome.subframe
);
}
}
#[test]
fn the_lag_window_is_bit_exact_against_ts26173() {
let run = run_trace();
let mut compared = 0;
for outcome in &run.outcomes {
let index = i32::try_from(outcome.subframe).expect("subframe");
assert_eq!(
(outcome.window.min, outcome.window.max),
(
scalar(outcome.frame, index, "T0_min"),
scalar(outcome.frame, index, "T0_max")
),
"frame {} subframe {}: lag window",
outcome.frame,
outcome.subframe
);
assert_eq!(
outcome.window.max - outcome.window.min,
15,
"the window is always 16 integer lags wide"
);
compared += 1;
}
assert_eq!(
compared,
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
}
#[test]
fn the_adaptive_codebook_vector_is_bit_exact_against_ts26173() {
let run = run_trace();
assert_eq!(
run.outcomes.len(),
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
for outcome in &run.outcomes {
compare("adapt", outcome, &outcome.adaptive);
}
}
#[test]
fn the_filtered_adaptive_vector_is_bit_exact_against_ts26173() {
let run = run_trace();
assert_eq!(
run.outcomes.len(),
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
for outcome in &run.outcomes {
compare("y1", outcome, &outcome.decision.sharp_response);
}
}
#[test]
fn the_two_candidate_gains_are_bit_exact_against_ts26173() {
let run = run_trace();
let mut compared = 0;
for outcome in &run.outcomes {
let index = i32::try_from(outcome.subframe).expect("subframe");
assert_eq!(
outcome.decision.sharp_gain.0,
scalar(outcome.frame, index, "gain1"),
"frame {} subframe {}: unfiltered pitch gain",
outcome.frame,
outcome.subframe
);
assert_eq!(
outcome.decision.smooth_gain.0,
scalar(outcome.frame, index, "gain2"),
"frame {} subframe {}: filtered pitch gain",
outcome.frame,
outcome.subframe
);
compared += 1;
}
assert_eq!(
compared,
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
assert_eq!(
scalar(0, 1, "gain1"),
0,
"the trace no longer covers the zero gain"
);
assert_eq!(
scalar(0, 2, "gain1"),
19661,
"the trace no longer covers the 1.2 clamp"
);
}
#[test]
fn the_ltp_low_pass_choice_is_bit_exact_against_ts26173() {
let run = run_trace();
let mut compared = 0;
let mut sharp = 0;
for outcome in &run.outcomes {
let index = i32::try_from(outcome.subframe).expect("subframe");
let want = scalar(outcome.frame, index, "select") == 1;
assert_eq!(
outcome.decision.prefer_sharp, want,
"frame {} subframe {}: LTP low-pass choice",
outcome.frame, outcome.subframe
);
sharp += usize::from(want);
compared += 1;
}
assert_eq!(
compared,
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
assert!(
sharp > 0 && sharp < compared,
"the committed trace should exercise both LTP candidates"
);
}
#[test]
fn the_reconstructed_residual_agrees_with_the_traced_codebook_target() {
let run = run_trace();
let mut compared = 0;
for outcome in &run.outcomes {
let want = row(
outcome.frame,
i32::try_from(outcome.subframe).expect("subframe"),
"cn",
);
for (i, &got) in outcome.codebook_residual.iter().enumerate() {
assert_eq!(
got.0,
want[L_SUBFR / 2 + i].0,
"frame {} subframe {}: cn[{}]",
outcome.frame,
outcome.subframe,
L_SUBFR / 2 + i
);
}
compared += 1;
}
assert_eq!(
compared,
TRACE_FRAMES * SUBFRAMES,
"expected every subframe"
);
}
#[test]
fn the_open_loop_search_gives_a_tie_to_the_shorter_lag() {
let mut ctx = DspContext::default();
let mut open = OpenLoopPitch::new();
let lags = open.analyse(
&mut ctx,
&[Word16(0); WSP_FRAME],
PitchMode::from_frame_bits(TRACE_BITS),
);
assert_eq!(
lags.first_half, 36,
"an all-ties open-loop search must return the shortest reachable lag"
);
}
#[test]
fn the_open_loop_search_never_returns_its_lower_bound() {
let mut ctx = DspContext::default();
let mut open = OpenLoopPitch::new();
let lags = open.analyse(
&mut ctx,
&[Word16(0); WSP_FRAME],
PitchMode::from_frame_bits(TRACE_BITS),
);
assert_ne!(
lags.first_half, PIT_MIN,
"PIT_MIN is not an open-loop result"
);
}
fn all_ties_closed_loop(window: LagWindow, absolute: bool) -> (i16, i16) {
let mut ctx = DspContext::default();
let excitation = [Word16(0); EXC_TOTAL];
let target = [Word16(0); L_SUBFR];
let response = [Word16(0); L_SUBFR];
closed_loop_lag(
&mut ctx,
&excitation,
EXC_HISTORY,
&target,
&response,
SearchLimits {
window,
absolute,
resolution: LagResolution::NINE_BIT,
},
)
}
#[test]
fn the_closed_loop_integer_search_gives_a_tie_to_the_longer_lag() {
let (lag, fraction) = all_ties_closed_loop(LagWindow { min: 200, max: 215 }, true);
assert_eq!(
(lag, fraction),
(215, 0),
"an all-ties integer search must keep the longest lag in the window"
);
}
#[test]
fn the_closed_loop_fractional_search_gives_a_tie_to_the_smallest_fraction() {
let (lag, fraction) = all_ties_closed_loop(LagWindow { min: 40, max: 55 }, false);
assert_eq!(
(lag, fraction),
(54, 2),
"the fractional search must keep the earlier of two equal phases"
);
}
#[test]
fn a_lag_on_the_window_floor_cannot_search_below_it() {
let mut ctx = DspContext::default();
let mut excitation = [Word16(0); EXC_TOTAL];
let window = LagWindow { min: 40, max: 55 };
let mut target = [Word16(0); L_SUBFR];
let mut response = [Word16(0); L_SUBFR];
response[0] = Word16(16384);
for (i, slot) in target.iter_mut().enumerate() {
*slot = Word16(if i % 40 == 0 { 4000 } else { 0 });
excitation[EXC_HISTORY - 40 + i] = *slot;
}
let (lag, fraction) = closed_loop_lag(
&mut ctx,
&excitation,
EXC_HISTORY,
&target,
&response,
SearchLimits {
window,
absolute: false,
resolution: LagResolution::NINE_BIT,
},
);
assert_eq!(
lag, window.min,
"the correlation should peak on the window floor"
);
assert!(
(0..=3).contains(&fraction),
"fraction {fraction} out of range"
);
}
#[test]
fn the_lag_window_clamps_at_both_ends() {
let mut ctx = DspContext::default();
assert_eq!(
LagWindow::around(&mut ctx, PIT_MIN),
LagWindow {
min: PIT_MIN,
max: PIT_MIN + 15
},
"a short lag clamps to PIT_MIN"
);
assert_eq!(
LagWindow::around(&mut ctx, PIT_MAX),
LagWindow {
min: PIT_MAX - 15,
max: PIT_MAX
},
"a long lag clamps to PIT_MAX and pulls the floor down with it"
);
assert_eq!(
LagWindow::around(&mut ctx, 100),
LagWindow { min: 92, max: 107 },
"an interior lag sits 8 below and 7 above"
);
}
#[test]
fn the_lag_history_only_shifts_on_a_strong_correlation() {
let mut ctx = DspContext::default();
let mut open = OpenLoopPitch::new();
open.gain = Word16(19661); assert_eq!(
open.absorb(&mut ctx, 77),
None,
"0.6 exactly must not refresh"
);
assert_eq!(
open.recent_lags,
[Word16(40); 5],
"the history moved anyway"
);
open.gain = Word16(19662);
assert_eq!(
open.absorb(&mut ctx, 77),
Some(40),
"one new lag among four 40s"
);
assert_eq!(
open.recent_lags,
[Word16(77), Word16(40), Word16(40), Word16(40), Word16(40)],
"the history should have shifted exactly once"
);
}
#[test]
fn the_confidence_decays_to_zero_and_stays_there() {
let mut ctx = DspContext::default();
let mut open = OpenLoopPitch::new();
open.gain = Word16(0);
open.confidence = Word16(32767);
for _ in 0..200 {
open.absorb(&mut ctx, 50);
}
assert_eq!(
open.confidence.0, 0,
"the confidence should have floored to 0"
);
assert!(
!open.weight_towards_history,
"weighting should be off at zero"
);
}
#[test]
fn the_clipping_threshold_uses_integer_division_by_the_isf_maximum() {
let mut ctx = DspContext::default();
let mode = PitchMode::from_frame_bits(BITS_8K85);
let mut clipping = GainClipping::new();
clipping.isf_gap = Word16(384);
clipping.mean_gain = Word16(16358);
assert!(
!clipping.clips(&mut ctx, mode),
"the threshold is a strict >"
);
clipping.mean_gain = Word16(16359);
assert!(
clipping.clips(&mut ctx, mode),
"one above the threshold must clip"
);
}
#[test]
fn the_wideband_clipping_test_needs_both_halves() {
let mut ctx = DspContext::default();
let mode = PitchMode::from_frame_bits(TRACE_BITS);
let mut clipping = GainClipping::new();
clipping.isf_gap = Word16(153);
clipping.mean_gain = Word16(14747);
assert!(
clipping.clips(&mut ctx, mode),
"resonant and predictive must clip"
);
clipping.isf_gap = Word16(154);
assert!(
!clipping.clips(&mut ctx, mode),
"the gap test is a strict <"
);
clipping.isf_gap = Word16(153);
clipping.mean_gain = Word16(14746);
assert!(
!clipping.clips(&mut ctx, mode),
"the gain test is a strict >"
);
}
#[test]
fn the_clipping_tracker_resets_the_same_way_for_every_mode() {
let fresh = GainClipping::new();
assert_eq!((fresh.isf_gap.0, fresh.mean_gain.0), (307, 9830));
}
#[test]
fn the_correlation_weighting_table_matches_the_reference_shape() {
assert_eq!(CORR_WEIGHT.len(), 199);
assert_eq!(CORR_WEIGHT[0], 10772);
assert_eq!(CORR_WEIGHT[198], 10728);
assert!(
CORR_WEIGHT[95..=101].iter().all(|&v| v == 16384),
"the bump's plateau is indices 95..=101"
);
assert_eq!(CORR_WEIGHT[94], 16056);
assert_eq!(CORR_WEIGHT[102], 16056);
assert!(CORR_WEIGHT[101] > CORR_WEIGHT[198]);
}
#[test]
fn the_interpolation_filter_matches_the_reference_shape() {
assert_eq!(INTER4_1.len(), 32);
assert_eq!(INTER4_1[15], 14746, "the peak tap sits at phase 3 of tap 3");
assert_eq!(INTER4_1[31], 0, "the last tap is the padding zero");
assert_eq!(INTER4_1[0], -12);
}
#[test]
fn the_mode_predicates_split_where_the_reference_does() {
let narrow = PitchMode::from_frame_bits(BITS_6K60);
let second = PitchMode::from_frame_bits(BITS_8K85);
let wide = PitchMode::from_frame_bits(TRACE_BITS);
let sid = PitchMode::from_frame_bits(35);
assert!(narrow.open_loop_spans_frame());
assert!(!second.open_loop_spans_frame());
assert!(!sid.open_loop_spans_frame());
assert!(!sid.interoperable_clipping());
assert!(!narrow.has_sharp_candidate());
assert!(!second.has_sharp_candidate());
assert!(wide.has_sharp_candidate());
assert!(!narrow.third_subframe_is_absolute());
assert!(second.third_subframe_is_absolute());
assert!(narrow.interoperable_clipping());
assert!(second.interoperable_clipping());
assert!(!wide.interoperable_clipping());
assert_eq!(narrow.lag_resolution(), LagResolution::EIGHT_BIT);
assert_eq!(wide.lag_resolution(), LagResolution::NINE_BIT);
}
}