From f5c7ce011fd64da390862e4a12c0b74d94018adf Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Thu, 9 Mar 2023 19:44:20 +0100 Subject: [PATCH] Cook encoder --- nihav-allstuff/src/lib.rs | 1 + nihav-realmedia/Cargo.toml | 10 +- nihav-realmedia/src/codecs/cookenc.rs | 1213 +++++++++++++++++++++++++ nihav-realmedia/src/codecs/mod.rs | 19 +- nihav-realmedia/src/lib.rs | 4 +- 5 files changed, 1244 insertions(+), 3 deletions(-) create mode 100644 nihav-realmedia/src/codecs/cookenc.rs diff --git a/nihav-allstuff/src/lib.rs b/nihav-allstuff/src/lib.rs index fc60450..c016d54 100644 --- a/nihav-allstuff/src/lib.rs +++ b/nihav-allstuff/src/lib.rs @@ -71,6 +71,7 @@ pub fn nihav_register_all_encoders(re: &mut RegisteredEncoders) { duck_register_all_encoders(re); llaudio_register_all_encoders(re); ms_register_all_encoders(re); + realmedia_register_all_encoders(re); } /// Registers all known demuxers. diff --git a/nihav-realmedia/Cargo.toml b/nihav-realmedia/Cargo.toml index 84a07e5..2d096be 100644 --- a/nihav-realmedia/Cargo.toml +++ b/nihav-realmedia/Cargo.toml @@ -12,7 +12,7 @@ path = "../nihav-codec-support" features = ["h263", "mdct", "blockdsp"] [features] -default = ["all_decoders", "all_demuxers", "all_muxers"] +default = ["all_decoders", "all_demuxers", "all_encoders", "all_muxers"] demuxers = [] all_demuxers = ["demuxer_real"] demuxer_real = ["demuxers"] @@ -36,3 +36,11 @@ decoder_realaudio144 = ["decoders"] decoder_realaudio288 = ["decoders"] decoder_cook = ["decoders"] decoder_ralf = ["decoders"] + +all_encoders = ["all_video_encoders", "all_audio_encoders"] +encoders = [] + +all_video_encoders = [] + +all_audio_encoders = ["encoder_cook"] +encoder_cook = ["encoders"] diff --git a/nihav-realmedia/src/codecs/cookenc.rs b/nihav-realmedia/src/codecs/cookenc.rs new file mode 100644 index 0000000..70f471b --- /dev/null +++ b/nihav-realmedia/src/codecs/cookenc.rs @@ -0,0 +1,1213 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use nihav_core::io::bitwriter::*; +use nihav_codec_support::dsp::fft::*; +use super::cookdata::*; + +/* + TODO: + - calculate gains? +*/ +const MAX_FRAME_SIZE: usize = 1024; + +const EPS: f32 = 1.0e-6; + +fn get_block_size(srate: u32) -> usize { + match srate { + 8000 | 11025 => 256, + 22050 => 512, + _ => 1024, + } +} + +trait ClipCat { + fn clip_cat(&self) -> usize; +} + +impl ClipCat for i32 { + fn clip_cat(&self) -> usize { ((*self).max(0) as usize).min(NUM_CATEGORIES - 1) } +} + +fn bitalloc(samples: usize, bits: usize, vector_bits: u8, total_subbands: usize, qindex: &[i8], category: &mut [u8], cat_index: &mut [u8]) { + let avail_bits = (if bits > samples { samples + ((bits - samples) * 5) / 8 } else { bits }) as i32; + + let mut bias: i32 = -32; + for i in 0..6 { + let mut sum = 0; + for j in 0..total_subbands { + let idx = ((32 >> i) + bias - (qindex[j] as i32)) / 2; + sum += COOK_EXP_BITS[idx.clip_cat()]; + } + if sum >= (avail_bits - 32) { + bias += 32 >> i; + } + } + + let mut exp_index1: [usize; MAX_SUBBANDS * 2] = [0; MAX_SUBBANDS * 2]; + let mut exp_index2: [usize; MAX_SUBBANDS * 2] = [0; MAX_SUBBANDS * 2]; + let mut sum = 0; + for i in 0..total_subbands { + let idx = ((bias - (qindex[i] as i32)) / 2).clip_cat(); + sum += COOK_EXP_BITS[idx]; + exp_index1[i] = idx; + exp_index2[i] = idx; + } + + let mut tbias1 = sum; + let mut tbias2 = sum; + let mut tcat: [usize; 128*2] = [0; 128*2]; + let mut tcat_idx1 = 128; + let mut tcat_idx2 = 128; + for _ in 1..(1 << vector_bits) { + if tbias1 + tbias2 > avail_bits * 2 { + let mut max = -999999; + let mut idx = total_subbands + 1; + for j in 0..total_subbands { + if exp_index1[j] >= (NUM_CATEGORIES - 1) { continue; } + let t = -2 * (exp_index1[j] as i32) - (qindex[j] as i32) + bias; + if t >= max { + max = t; + idx = j; + } + } + if idx >= total_subbands { break; } + tcat[tcat_idx1] = idx; + tcat_idx1 += 1; + tbias1 -= COOK_EXP_BITS[exp_index1[idx]] - COOK_EXP_BITS[exp_index1[idx] + 1]; + exp_index1[idx] += 1; + } else { + let mut min = 999999; + let mut idx = total_subbands + 1; + for j in 0..total_subbands { + if exp_index2[j] == 0 { continue; } + let t = -2 * (exp_index2[j] as i32) - (qindex[j] as i32) + bias; + if t < min { + min = t; + idx = j; + } + } + if idx >= total_subbands { break; } + tcat_idx2 -= 1; + tcat[tcat_idx2] = idx; + tbias2 -= COOK_EXP_BITS[exp_index2[idx]] - COOK_EXP_BITS[exp_index2[idx] - 1]; + exp_index2[idx] -= 1; + } + } + for i in 0..total_subbands { + category[i] = exp_index2[i] as u8; + } + for el in cat_index.iter_mut() { + *el = 255; + } + for (dst, &src) in cat_index.iter_mut().zip(tcat[tcat_idx2..tcat_idx1].iter()) { + *dst = src as u8; + } +} + +fn map_coef(val: f32, centroids: &[f32]) -> usize { + if val < centroids[1] * 0.5 { + 0 + } else { + let len = centroids.len(); + if val < centroids[len - 1] { + for (i, pair) in centroids.windows(2).enumerate().skip(1) { + if val <= (pair[0] + pair[1]) * 0.5 { + return i; + } + } + } + len - 1 + } +} + +fn couple_bands(left: &[f32], right: &[f32], dst: &mut [f32], cpl_scales: &[f32]) -> u8 { + let nrg0 = left.iter().fold(0.0f32, |acc, &v| acc + v * v); + let nrg1 = right.iter().fold(0.0f32, |acc, &v| acc + v * v); + let last_idx = cpl_scales.len() - 3; + match (nrg0 > EPS, nrg1 > EPS) { + (true, true) => { + let tgt_scale0 = (nrg0 / (nrg0 + nrg1)).sqrt(); + let tgt_scale1 = (nrg1 / (nrg0 + nrg1)).sqrt(); + + let mut best_dist = 42.0; + let mut best_idx = 0; + for i in 0..=last_idx { + let scale0 = cpl_scales[i]; + let scale1 = cpl_scales[cpl_scales.len() - 1 - i]; + let dist = (scale0 - tgt_scale0).abs() + (scale1 - tgt_scale1).abs(); + if dist < best_dist { + best_dist = dist; + best_idx = i; + } + } + let scale_l = cpl_scales[best_idx]; + let scale_r = cpl_scales[cpl_scales.len() - 1 - best_idx]; + + if best_idx == 0 { + dst.copy_from_slice(left); + } else { + for (dst, (&ll, &rr)) in dst.iter_mut().zip(left.iter().zip(right.iter())) { + *dst = (ll / scale_l + rr / scale_r) * 0.5; + } + } + best_idx as u8 + }, + (false, true) => { + dst.copy_from_slice(right); + last_idx as u8 + }, + (true, false) => { + dst.copy_from_slice(left); + 0 + }, + _ => { + for (dst, (&ll, &rr)) in dst.iter_mut().zip(left.iter().zip(right.iter())) { + *dst = (ll + rr) * std::f32::consts::FRAC_1_SQRT_2; + } + (cpl_scales.len() / 2) as u8 + } + } +} + +#[derive(Clone,Copy,Default)] +struct PackedCoeffs { + cw: [u16; 10], + bits: [u8; 10], + signs: [u16; 10], + nnz: [u8; 10], + num: usize, + cat: usize, + nbits: u16, +} + +impl PackedCoeffs { + fn pack(&mut self, coeffs: &[f32], cat: usize) -> u16 { + self.cat = cat; + self.nbits = 0; + + if cat >= COOK_NUM_VQ_GROUPS.len() { + self.num = 0; + return 0; + } + + let group_size = COOK_VQ_GROUP_SIZE[cat]; + let multiplier = COOK_VQ_MULT[cat] as usize + 1; + let (code_words, code_bits) = match cat { + 0 => (&COOK_VQ0_CODES[..], &COOK_VQ0_BITS[..]), + 1 => (&COOK_VQ1_CODES[..], &COOK_VQ1_BITS[..]), + 2 => (&COOK_VQ2_CODES[..], &COOK_VQ2_BITS[..]), + 3 => (&COOK_VQ3_CODES[..], &COOK_VQ3_BITS[..]), + 4 => (&COOK_VQ4_CODES[..], &COOK_VQ4_BITS[..]), + 5 => (&COOK_VQ5_CODES[..], &COOK_VQ5_BITS[..]), + 6 => (&COOK_VQ6_CODES[..], &COOK_VQ6_BITS[..]), + _ => unreachable!(), + }; + let centroids = &COOK_QUANT_CENTROID[cat][..multiplier]; + self.num = COOK_NUM_VQ_GROUPS[cat]; + + for (group_no, group) in coeffs.chunks(group_size).enumerate() { + let mut cw = 0; + let mut cvals = [0; 5]; + let mut sarr = [0; 5]; + for ((dval, sign), &el) in cvals.iter_mut().zip(sarr.iter_mut()).zip(group.iter()) { + let cur_val = map_coef(el.abs(), centroids); + *dval = cur_val; + *sign = (el < 0.0) as u16; + cw = cw * multiplier + cur_val; + } + while cw >= code_bits.len() || code_bits[cw] == 0 { + let mut max_pos = 0; + let mut max_val = cvals[0]; + for (i, &val) in cvals.iter().enumerate().skip(1) { + if val > max_val { + max_val = val; + max_pos = i; + } + } + cvals[max_pos] -= 1; + cw = 0; + for &dval in cvals.iter().take(group_size) { + cw = cw * multiplier + dval; + } + } + let mut signs = 0; + let mut nnz = 0u8; + for (&sign, &val) in sarr.iter().zip(cvals.iter()) { + if val != 0 { + signs = (signs << 1) | sign; + nnz += 1; + } + } + + self.cw [group_no] = code_words[cw]; + self.bits [group_no] = code_bits[cw]; + self.signs[group_no] = signs; + self.nnz [group_no] = nnz; + self.nbits += u16::from(code_bits[cw]) + u16::from(nnz); + } + self.nbits + } + fn write(&self, bw: &mut BitWriter, mut bits_left: u16) { + for ((&cw, &bits), (&signs, &nnz)) in + self.cw.iter().zip(self.bits.iter()).zip( + self.signs.iter().zip(self.nnz.iter())).take(self.num) { + let cur_bits = u16::from(bits + nnz); + if cur_bits > bits_left { + break; + } + bits_left -= cur_bits; + bw.write(cw.into(), bits); + if nnz > 0 { + bw.write(signs.into(), nnz); + } + } + } +} + +struct TempData { + bands: [PackedCoeffs; MAX_SUBBANDS * 2], +} + +impl Default for TempData { + fn default() -> Self { + Self { + bands: [PackedCoeffs::default(); MAX_SUBBANDS * 2], + } + } +} + +struct ChannelDataParams<'a> { + size: usize, + frame_size: usize, + hpow_tab: &'a [f32; 128], + coupling: &'a [u8], + js_bits: u8, + js_start: usize, + vector_bits: u8, +} + +struct CookChannelPair { + br_info: &'static BitrateParams, + delay: [[f32; MAX_FRAME_SIZE]; 2], +} + +fn calc_qindex(nrg: f32) -> i8 { + let mut nrg0 = nrg * 0.05; + if nrg0 <= 1.0 { + nrg0 *= std::f32::consts::FRAC_1_SQRT_2; + } else { + nrg0 *= std::f32::consts::SQRT_2; + } + nrg0.log2().max(-31.0).min(47.0) as i8 +} + +impl CookChannelPair { + fn new(br_info: &'static BitrateParams) -> Self { + Self { + br_info, + delay: [[0.0; MAX_FRAME_SIZE]; 2], + } + } + fn encode_bands(params: ChannelDataParams, dbuf: Vec, coeffs: &mut [f32], total_bands: usize, tmp: &mut TempData) -> EncoderResult> { + let output_start = dbuf.len(); + let mut bw = BitWriter::new(dbuf, BitWriterMode::BE); + let data_end = bw.tell() + params.frame_size; + + let mut qindex = [0i8; MAX_SUBBANDS * 2]; + for (qscale, band) in qindex.iter_mut().zip(coeffs.chunks(BAND_SIZE)).take(total_bands) { + let nrg = band.iter().fold(0.0f32, |acc, &v| acc + v * v); + *qscale = calc_qindex(nrg); + } + qindex[0] = qindex[0].max(-6); + + bw.write0(); // no gains + //todo gains + + if params.js_bits > 0 { + let (cpl_cb_codes, cpl_cb_bits) = match params.js_bits { + 2 => (&COOK_CPL_2BITS_CODES[..], &COOK_CPL_2BITS_BITS[..]), + 3 => (&COOK_CPL_3BITS_CODES[..], &COOK_CPL_3BITS_BITS[..]), + 4 => (&COOK_CPL_4BITS_CODES[..], &COOK_CPL_4BITS_BITS[..]), + 5 => (&COOK_CPL_5BITS_CODES[..], &COOK_CPL_5BITS_BITS[..]), + 6 => (&COOK_CPL_6BITS_CODES[..], &COOK_CPL_6BITS_BITS[..]), + _ => unreachable!(), + }; + let mut bit_size = 0; + let mut raw_bit_size = 0; + for &el in params.coupling.iter() { + bit_size += cpl_cb_bits[usize::from(el)]; + raw_bit_size += params.js_bits; + } + if bit_size < raw_bit_size { + bw.write1(); + for &el in params.coupling.iter() { + let idx = usize::from(el); + bw.write(cpl_cb_codes[idx].into(), cpl_cb_bits[idx]); + } + } else { + bw.write0(); + for &el in params.coupling.iter() { + bw.write(u32::from(el), params.js_bits); + } + } + } + + let mut last_q = qindex[0]; + bw.write((qindex[0] + 6) as u32, 6); + if params.js_bits == 0 { + for (i, qscale) in qindex[..total_bands].iter_mut().enumerate().skip(1) { + let cb_idx = (i - 1).min(12); + let diff = (*qscale - last_q).max(-12).min(11); + *qscale = last_q + diff; + last_q = *qscale; + + let idx2 = (diff + 12) as usize; + bw.write(COOK_QUANT_CODES[cb_idx][idx2].into(), COOK_QUANT_BITS[cb_idx][idx2]); + } + } else { + for (i, qscale) in qindex[..total_bands].iter_mut().enumerate().skip(1) { + let band_no = if i < params.js_start * 2 { i >> 1 } else { i - params.js_start }; + let cb_idx = band_no.saturating_sub(1).min(12); + let diff = (*qscale - last_q).max(-12).min(11); + *qscale = last_q + diff; + last_q = *qscale; + + let idx2 = (diff + 12) as usize; + bw.write(COOK_QUANT_CODES[cb_idx][idx2].into(), COOK_QUANT_BITS[cb_idx][idx2]); + } + } + + let mut category = [0; MAX_SUBBANDS * 2]; + let mut cat_index = [0; 127]; + let bits_avail = data_end - bw.tell() - usize::from(params.vector_bits); + + bitalloc(params.size, bits_avail, params.vector_bits, total_bands, &qindex, &mut category, &mut cat_index); + + let mut tot_bits = 0; + for ((band, packed), (&qindex, &cat)) in + coeffs.chunks_exact_mut(BAND_SIZE).zip(tmp.bands.iter_mut()) + .zip(qindex.iter().zip(category.iter())).take(total_bands) { + for coef in band.iter_mut() { + *coef *= params.hpow_tab[(64 - qindex) as usize]; + } + tot_bits += packed.pack(band, cat.into()); + } + + let mut bits_left = bits_avail as u16; + let mut bands_corrected = 0; + let max_corr_bands = (1 << params.vector_bits) - 1; + for &index in cat_index.iter() { + if bands_corrected >= max_corr_bands || tot_bits <= bits_left || index == 255 { + break; + } + let index = usize::from(index); + let pband = &mut tmp.bands[index]; + let band_coeffs = &coeffs[index * BAND_SIZE..][..BAND_SIZE]; + let new_cat = (pband.cat + 1).min(NUM_CATEGORIES - 1); + tot_bits -= pband.nbits; + pband.pack(band_coeffs, new_cat); + tot_bits += pband.nbits; + bands_corrected += 1; + } + + bw.write(bands_corrected, params.vector_bits); + for packed in tmp.bands.iter().take(total_bands) { + packed.write(&mut bw, bits_left); + bits_left = bits_left.saturating_sub(packed.nbits); + if bits_left == 0 { + break; + } + } + + pad(&mut bw, data_end); + + let mut dbuf = bw.end(); + for (i, el) in dbuf[output_start..].iter_mut().enumerate() { + *el ^= COOK_XOR_KEY[i & 3]; + } + + Ok(dbuf) + } + fn encode_mono(&mut self, dbuf: Vec, dsp: &mut CookDSP, src: &[f32], ch_no: usize, tmp: &mut TempData) -> EncoderResult> { + dsp.mdct(&mut self.delay[ch_no], src, true); + let coeffs = &mut dsp.coeffs; + + let frame_size = (self.br_info.frame_bits / u32::from(self.br_info.channels)) as usize; + let total_bands: usize = self.br_info.max_subbands.into(); + + let params = ChannelDataParams { + size: dsp.size, + hpow_tab: &dsp.hpow_tab, + coupling: &[], + js_bits: 0, + js_start: 0, + vector_bits: 5, + frame_size, + }; + Self::encode_bands(params, dbuf, coeffs, total_bands, tmp) + } + fn encode_jstereo(&mut self, dbuf: Vec, dsp: &mut CookDSP, l_ch: &[f32], r_ch: &[f32], tmp: &mut TempData) -> EncoderResult> { + let frame_size = self.br_info.frame_bits as usize; + let low_bands = usize::from(self.br_info.js_start); + let total_bands = usize::from(self.br_info.max_subbands) + low_bands; + + dsp.mdct(&mut self.delay[0], l_ch, true); + dsp.mdct(&mut self.delay[1], r_ch, false); + + let mut decouple = [0; 20]; + let cpl_scales = COOK_CPL_SCALES[usize::from(self.br_info.js_bits - 2)]; + let cpl_start_band = COOK_CPL_BAND[usize::from(self.br_info.js_start)] as usize; + let cpl_end_band = COOK_CPL_BAND[usize::from(self.br_info.max_subbands) - 1] as usize; + + let coeffs = &mut dsp.coeffs; + let coeffs2 = &mut dsp.coeffs2; + let coupled = &mut dsp.coupled; + for (dst, (src0, src1)) in coupled.chunks_exact_mut(BAND_SIZE * 2).zip(coeffs.chunks(BAND_SIZE).zip(coeffs2.chunks(BAND_SIZE))).take(low_bands) { + let (dst0, dst1) = dst.split_at_mut(BAND_SIZE); + dst0.copy_from_slice(src0); + dst1.copy_from_slice(src1); + } + for el in coupled[total_bands * BAND_SIZE..].iter_mut() { + *el = 0.0; + } + + let mut band = low_bands; + let mut start_band = band; + let mut cpl_band = cpl_start_band; + let mut last_cpl_band = cpl_band; + let end_band = usize::from(self.br_info.max_subbands); + while band < end_band { + cpl_band = usize::from(COOK_CPL_BAND[band]); + if cpl_band != last_cpl_band { + let length = (band - start_band) * BAND_SIZE; + decouple[last_cpl_band] = couple_bands(&coeffs[start_band * BAND_SIZE..][..length], + &coeffs2[start_band * BAND_SIZE..][..length], + &mut coupled[(start_band + low_bands) * BAND_SIZE..][..length], + cpl_scales); + last_cpl_band = cpl_band; + start_band = band; + } + band += 1; + } + if band != start_band { + let length = (band - start_band) * BAND_SIZE; + decouple[last_cpl_band] = couple_bands(&coeffs[start_band * BAND_SIZE..][..length], + &coeffs2[start_band * BAND_SIZE..][..length], + &mut coupled[(start_band + low_bands) * BAND_SIZE..][..length], + cpl_scales); + } + + let vector_bits = match dsp.size { + 1024 => 7, + 512 => 6, + _ => 5, + }; + let params = ChannelDataParams { + size: dsp.size, + hpow_tab: &dsp.hpow_tab, + coupling: &decouple[cpl_start_band..=cpl_end_band], + js_bits: self.br_info.js_bits, + js_start: self.br_info.js_start.into(), + frame_size, + vector_bits, + }; + + Self::encode_bands(params, dbuf, coupled, total_bands, tmp) + } +} + +fn pad(bw: &mut BitWriter, data_end: usize) { + while (bw.tell() & 7) != 0 { + bw.write0(); + } + while bw.tell() + 32 <= data_end { + bw.write(0, 32); + } + while bw.tell() + 8 <= data_end { + bw.write(0, 8); + } +} +pub struct MDCT { + table: Vec, + tmp: Vec, + fft: FFT, + size: usize, +} + +impl MDCT { + pub fn new(size: usize) -> Self { + let fft = FFTBuilder::new_fft(size / 4, false); + let mut table = Vec::with_capacity(size / 4); + let factor = std::f32::consts::PI * 2.0 / (size as f32); + for i in 0..(size / 4) { + let val = factor * ((i as f32) + 1.0 / 8.0); + table.push(FFTComplex::exp(val)); + } + Self { + fft, + tmp: vec![FFTC_ZERO; size / 4], + table, + size, + } + } + pub fn mdct(&mut self, src: &[f32], dst: &mut [f32]) { + let size1_8 = self.size / 8; + let size1_4 = self.size / 4; + let size1_2 = self.size / 2; + let size3_4 = size1_4 * 3; + + for i in 0..size1_8 { + let a0 = FFTComplex{re: -src[2 * i + size3_4] - src[size3_4 - 1 - 2 * i], + im: -src[size1_4 + 2 * i] + src[size1_4 - 1 - 2 * i]}; + let t0 = !self.table[i]; + let a1 = FFTComplex{re: src[2 * i] - src[size1_2 - 1 - 2 * i], + im: -src[size1_2 + 2 * i] - src[self.size - 1 - 2 * i]}; + let t1 = !self.table[size1_8 + i]; + self.tmp[i] = a0 * t0; + self.tmp[i + size1_8] = a1 * t1; + } + self.fft.do_fft_inplace(&mut self.tmp); + + for i in 0..size1_8 { + let a0 = self.tmp[size1_8 - 1 - i] * !self.table[size1_8 - 1 - i].rotate(); + let a1 = self.tmp[size1_8 + i] * !self.table[size1_8 + i].rotate(); + dst[size1_4 - 2 - i * 2] = a0.im; + dst[size1_4 - 1 - i * 2] = a1.re; + dst[size1_4 + i * 2] = a1.im; + dst[size1_4 + 1 + i * 2] = a0.re; + } + } +} + + +struct CookDSP { + size: usize, + mdct: MDCT, + tmp: [f32; MAX_FRAME_SIZE * 2], + window: [f32; MAX_FRAME_SIZE * 2], + pow_tab: [f32; 128], + hpow_tab: [f32; 128], + gain_tab: [f32; 23], + + coeffs: [f32; MAX_FRAME_SIZE], + coeffs2: [f32; MAX_FRAME_SIZE], + coupled: [f32; MAX_FRAME_SIZE * 2], +} + +impl CookDSP { + fn init(&mut self, frame_size: usize) { + if self.size == frame_size { + return; + } + self.size = frame_size; + self.mdct = MDCT::new(self.size * 2); + + let fsamples = self.size as f32; + let factor = std::f32::consts::PI / (2.0 * fsamples); + let scale = fsamples; + for (k, dst) in self.window[..self.size * 2].iter_mut().enumerate() { + *dst = (factor * ((k as f32) + 0.5)).sin() * scale; + } + + for (dst, &pow_val) in self.gain_tab.iter_mut().zip(self.pow_tab[53..].iter()) { + *dst = pow_val.powf(8.0 / fsamples); + } + } + fn mdct(&mut self, delay: &mut [f32], src: &[f32], mono: bool) { + let (w0, w1) = self.window[..self.size * 2].split_at(self.size); + let (d0, d1) = self.tmp[..self.size * 2].split_at_mut(self.size); + + for (dst, (&src, &win)) in d1.iter_mut().zip(delay.iter().zip(w0.iter()).rev()) { + *dst = src * win; + } + for (dst, (&src, &win)) in d0.iter_mut().zip(src.iter().zip(w1.iter()).rev()) { + *dst = src * win; + } + self.mdct.mdct(&self.tmp, if mono { &mut self.coeffs } else { &mut self.coeffs2 }); + + delay[..self.size].copy_from_slice(&src[..self.size]); + } +} + +impl Default for CookDSP { + fn default() -> Self { + let mut pow_tab: [f32; 128] = [0.0; 128]; + let mut hpow_tab: [f32; 128] = [0.0; 128]; + for i in 0..128 { + pow_tab[i] = 2.0f32.powf((i as f32) - 64.0); + hpow_tab[i] = 2.0f32.powf(((i as f32) - 64.0) * 0.5); + } + Self { + size: 0, + mdct: MDCT::new(64), + tmp: [0.0; MAX_FRAME_SIZE * 2], + window: [0.0; MAX_FRAME_SIZE * 2], + gain_tab: [0.0; 23], + pow_tab, hpow_tab, + + coeffs: [0.0; MAX_FRAME_SIZE], + coeffs2: [0.0; MAX_FRAME_SIZE], + coupled: [0.0; MAX_FRAME_SIZE * 2], + } + } +} + +#[derive(Default)] +struct CookEncoder { + stream: Option, + flavour: usize, + force_flv: bool, + g8_only: bool, + + samples: Vec>, + tgt_size: usize, + nframes: usize, + frm_per_blk: usize, + frm_size: usize, + + rd_pos: usize, + wr_pos: usize, + apts: u64, + + dsp: CookDSP, + pairs: Vec, + tmp: TempData, +} + +impl CookEncoder { + fn new() -> Self { Self::default() } + fn encode_packet(&mut self) -> EncoderResult { + if self.rd_pos == self.nframes { + let mut start = self.wr_pos * self.frm_size; + let mut dbuf = Vec::with_capacity(self.tgt_size * self.frm_per_blk); + for _ in 0..self.frm_per_blk { + let mut channels = self.samples.iter(); + + for ch_pair in self.pairs.iter_mut() { + if ch_pair.br_info.channels == 1 { + let ch = channels.next().unwrap(); + dbuf = ch_pair.encode_mono(dbuf, &mut self.dsp, &ch[start..][..self.frm_size], 0, &mut self.tmp)?; + } else { + let l_ch = channels.next().unwrap(); + let r_ch = channels.next().unwrap(); + if ch_pair.br_info.js_bits == 0 { + dbuf = ch_pair.encode_mono(dbuf, &mut self.dsp, &l_ch[start..][..self.frm_size], 0, &mut self.tmp)?; + dbuf = ch_pair.encode_mono(dbuf, &mut self.dsp, &r_ch[start..][..self.frm_size], 1, &mut self.tmp)?; + } else { + dbuf = ch_pair.encode_jstereo(dbuf, &mut self.dsp, &l_ch[start..][..self.frm_size], &r_ch[start..][..self.frm_size], &mut self.tmp)?; + } + } + } + + start += self.frm_size; + } + + let first = self.wr_pos == 0; + + self.wr_pos += self.frm_per_blk; + if self.wr_pos == self.nframes { + self.wr_pos = 0; + self.rd_pos = 0; + } + + let stream = self.stream.clone().unwrap(); + let (tb_num, tb_den) = stream.get_timebase(); + let ts = NATimeInfo::new(Some(self.apts), None, Some(1), tb_num, tb_den); + self.apts += self.frm_per_blk as u64; + Ok(NAPacket::new(self.stream.clone().unwrap(), ts, first, dbuf)) + } else { + Err(EncoderError::TryAgain) + } + } +} + +impl NAEncoder for CookEncoder { + fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => { + Ok(EncodeParameters { + format: NACodecTypeInfo::Audio(NAAudioInfo::new(0, 1, SND_F32P_FORMAT, 512)), + ..Default::default() }) + }, + NACodecTypeInfo::Video(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(ainfo) => { + if !self.force_flv { + let mut outinfo = ainfo; + outinfo.format = SND_F32P_FORMAT; + outinfo.sample_rate = match ainfo.sample_rate { + 0..= 8000 => 8000, + 8001..=11025 => 11025, + 11026..=22050 => 22050, + _ => 44100, + }; + if !matches!(outinfo.channels, 1 | 2) && (outinfo.sample_rate != 44100 || !matches!(outinfo.channels, 4 | 5)) { + outinfo.channels = 2; + } + outinfo.block_len = get_block_size(outinfo.sample_rate); + let mut ofmt = *encinfo; + ofmt.format = NACodecTypeInfo::Audio(outinfo); + Ok(ofmt) + } else { + let flavour = &COOK_FLAVOURS[self.flavour]; + let blk_len = match flavour.sample_rate { + 44100 => 1024, + 22050 => 512, + _ => 256, + }; + let newinfo = NAAudioInfo::new(flavour.sample_rate, flavour.channels, SND_F32P_FORMAT, blk_len); + let ofmt = EncodeParameters { + format: NACodecTypeInfo::Audio(newinfo), + tb_num: blk_len as u32, + tb_den: flavour.sample_rate, + bitrate: u32::from(flavour.bitrate) * 1000, + flags: ENC_MODE_CBR, + quality: 0, + }; + Ok(ofmt) + } + } + } + } + fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult { + match encinfo.format { + NACodecTypeInfo::None => Err(EncoderError::FormatError), + NACodecTypeInfo::Video(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(ainfo) => { + if ainfo.format != SND_F32P_FORMAT || !matches!(ainfo.sample_rate, 8000 | 11025 | 22050 | 44100) { + return Err(EncoderError::FormatError); + } + let blk_size = get_block_size(ainfo.sample_rate); + if ainfo.block_len != blk_size { + return Err(EncoderError::FormatError); + } + + let bitrate = if encinfo.bitrate > 0 { Some(encinfo.bitrate / 1000) } else { None }; + + let flavours = if self.g8_only { &COOK_FLAVOURS[..RM8_AUDIO] } else { COOK_FLAVOURS }; + let mut cand_id = None; + for (i, flavour) in flavours.iter().enumerate().rev() { + if flavour.sample_rate == ainfo.sample_rate && flavour.channels == ainfo.channels { + if let Some(target) = bitrate { + if target <= flavour.bitrate.into() { + cand_id = Some(i); + if target == flavour.bitrate.into() { + break; + } + } + } else { + cand_id = Some(i); + break; + } + } + } + + if let Some(id) = cand_id { + self.flavour = id; + } else { + return Err(EncoderError::FormatError); + } + + let flavour = &COOK_FLAVOURS[self.flavour]; + self.nframes = flavour.frames_per_blk * flavour.factor; + self.frm_per_blk = flavour.frames_per_blk; + self.samples.clear(); + self.frm_size = blk_size; + for _ in 0..flavour.channels { + self.samples.push(vec![0.0; self.nframes * self.frm_size]); + } + self.dsp.init(self.frm_size); + + self.pairs.clear(); + for br_info in flavour.br_infos() { + self.pairs.push(CookChannelPair::new(br_info)); + } + + self.tgt_size = 0; + for br_info in flavour.br_infos() { + self.tgt_size += br_info.frame_bits as usize / 8; + } + + let mut edata = Vec::new(); + let mut gw = GrowableMemoryWriter::new_write(&mut edata); + let mut bw = ByteWriter::new(&mut gw); + if flavour.channels <= 2 { + let br_info = &BITRATE_PARAMS[flavour.br_ids[0]]; + let ch_mode = if br_info.channels == 1 { + 1 + } else if br_info.js_bits == 0 { + 2 + } else { + 3 + }; + bw.write_u32be((1 << 24) | ch_mode)?; + bw.write_u16be((self.frm_size as u16) * u16::from(br_info.channels))?; + bw.write_u16be(br_info.max_subbands.into())?; + if ch_mode == 3 { + bw.write_u32be(0)?; // delay + bw.write_u16be(br_info.js_start.into())?; + bw.write_u16be(br_info.js_bits.into())?; + } + } else { + let chmap: &[u32] = match flavour.channels { + 1 => &[ 0x04 ], // C + 2 => &[ 0x03 ], // L,R + 5 => &[ 0x03, 0x04, 0x30 ], // L,R C Ls,Rs + 6 => &[ 0x03, 0x04, 0x08, 0x30 ], // L,R C LFE Ls,Rs + _ => unreachable!(), + }; + for (br_info, &channel_map) in flavour.br_infos().zip(chmap.iter()) { + bw.write_u32be(2 << 24)?; + bw.write_u16be((self.frm_size as u16) * u16::from(br_info.channels))?; + bw.write_u16be(br_info.max_subbands.into())?; + bw.write_u32be(0)?; // delay + bw.write_u16be(br_info.js_start.into())?; + bw.write_u16be(br_info.js_bits.into())?; + bw.write_u32be(channel_map)?; + } + } + + let soniton = NASoniton::new(16, 0); + let out_ainfo = NAAudioInfo::new(ainfo.sample_rate, ainfo.channels, soniton, self.tgt_size); + let info = NACodecInfo::new("cook", NACodecTypeInfo::Audio(out_ainfo), Some(edata)); + let mut stream = NAStream::new(StreamType::Audio, stream_id, info, blk_size as u32, ainfo.sample_rate, 0); + stream.set_num(stream_id as usize); + let stream = stream.into_ref(); + + self.stream = Some(stream.clone()); + self.wr_pos = 0; + self.rd_pos = 0; + + Ok(stream) + }, + } + } + fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> { + if let Some(ref abuf) = frm.get_buffer().get_abuf_f32() { + if self.rd_pos == self.nframes { + return Err(EncoderError::TryAgain); + } + let stride = abuf.get_stride(); + let src = abuf.get_data(); + let start = self.rd_pos * self.frm_size; + + for (dst, src) in self.samples.iter_mut().zip(src.chunks(stride)) { + dst[start..][..self.frm_size].copy_from_slice(&src[..self.frm_size]); + } + self.rd_pos += 1; + + Ok(()) + } else { + Err(EncoderError::InvalidParameters) + } + } + fn get_packet(&mut self) -> EncoderResult> { + if let Ok(pkt) = self.encode_packet() { + Ok(Some(pkt)) + } else { + Ok(None) + } + } + fn flush(&mut self) -> EncoderResult<()> { + if self.wr_pos != 0 { + let start = self.wr_pos * self.frm_size; + for channel in self.samples.iter_mut() { + for el in channel[start..].iter_mut() { + *el = 0.0; + } + } + self.wr_pos = self.nframes; + } + Ok(()) + } +} + +const FLAVOUR_OPTION: &str = "flavor"; +const G8_ONLY_OPTION: &str = "g8_only"; + +const ENCODER_OPTS: &[NAOptionDefinition] = &[ + NAOptionDefinition { + name: FLAVOUR_OPTION, description: "Codec-specific profile (0..32, -1 = auto)", + opt_type: NAOptionDefinitionType::Int(Some(-1), Some((COOK_FLAVOURS.len() - 1) as i64)) }, + NAOptionDefinition { + name: G8_ONLY_OPTION, description: "Force formats present in RealMedia G8", + opt_type: NAOptionDefinitionType::Bool }, +]; + +impl NAOptionHandler for CookEncoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { ENCODER_OPTS } + fn set_options(&mut self, options: &[NAOption]) { + for option in options.iter() { + for opt_def in ENCODER_OPTS.iter() { + if opt_def.check(option).is_ok() { + match option.name { + FLAVOUR_OPTION => { + if let NAValue::Int(val) = option.value { + if val >= 0 { + self.flavour = val as usize; + self.force_flv = true; + } else { + self.force_flv = false; + } + } + }, + G8_ONLY_OPTION => { + if let NAValue::Bool(val) = option.value { + self.g8_only = val; + } + }, + _ => {}, + }; + } + } + } + } + fn query_option_value(&self, name: &str) -> Option { + match name { + FLAVOUR_OPTION => Some(NAValue::Int(self.flavour as i64)), + G8_ONLY_OPTION => Some(NAValue::Bool(self.g8_only)), + _ => None, + } + } +} + +pub fn get_encoder() -> Box { Box::new(CookEncoder::new()) } + +struct FlavourInfo { + sample_rate: u32, + channels: u8, + bitrate: u16, + frames_per_blk: usize, + factor: usize, + br_ids: [usize; 4], +} + +impl FlavourInfo { + fn br_infos(&self) -> BitrateParamsIterator { + BitrateParamsIterator::new(self.br_ids, self.channels) + } +} + +struct BitrateParamsIterator { + ids: [usize; 4], + cur: usize, + len: usize, +} + +impl BitrateParamsIterator { + fn new(ids: [usize; 4], chans: u8) -> Self { + Self { + ids, + cur: 0, + len: usize::from((chans + 1) / 2) + } + } +} + +impl Iterator for BitrateParamsIterator { + type Item = &'static BitrateParams; + fn next(&mut self) -> Option { + if self.cur < self.len { + let ret = &BITRATE_PARAMS[self.ids[self.cur]]; + self.cur += 1; + Some(ret) + } else { + None + } + } +} + +macro_rules! flavour_desc { + ($srate:expr, $ch:expr, $br:expr, $fpb:expr, $factor:expr, $br_ids:expr) => { + FlavourInfo { + sample_rate: $srate, + channels: $ch, + bitrate: $br, + frames_per_blk: $fpb, + factor: $factor, + br_ids: $br_ids, + } + } +} + +const RM8_AUDIO: usize = 17; // newer variants that use joint-stereo coding and support multichannel +const COOK_FLAVOURS: &[FlavourInfo] = &[ + flavour_desc!( 8000, 1, 8, 9, 8, [ 0, 0, 0, 0]), + flavour_desc!(11025, 1, 11, 11, 8, [ 1, 0, 0, 0]), + flavour_desc!(22050, 1, 16, 12, 8, [ 2, 0, 0, 0]), + flavour_desc!(22050, 1, 20, 10, 9, [ 3, 0, 0, 0]), + flavour_desc!(44100, 1, 32, 7, 14, [ 4, 0, 0, 0]), + flavour_desc!(44100, 1, 44, 5, 16, [ 5, 0, 0, 0]), + flavour_desc!(44100, 1, 64, 4, 20, [ 6, 0, 0, 0]), + flavour_desc!(22050, 1, 32, 6, 16, [ 7, 0, 0, 0]), + flavour_desc!( 8000, 1, 6, 12, 6, [ 8, 0, 0, 0]), + flavour_desc!(11025, 2, 19, 10, 10, [ 9, 0, 0, 0]), + flavour_desc!(22050, 2, 32, 6, 14, [10, 0, 0, 0]), + flavour_desc!(22050, 2, 44, 5, 16, [11, 0, 0, 0]), + flavour_desc!(44100, 2, 64, 4, 20, [12, 0, 0, 0]), + flavour_desc!(44100, 2, 95, 3, 30, [13, 0, 0, 0]), + flavour_desc!(44100, 1, 64, 4, 20, [14, 0, 0, 0]), + flavour_desc!(44100, 1, 20, 10, 9, [15, 0, 0, 0]), + flavour_desc!(44100, 1, 32, 7, 14, [16, 0, 0, 0]), + flavour_desc!(22050, 2, 16, 10, 10, [17, 0, 0, 0]), + flavour_desc!(22050, 2, 20, 10, 10, [18, 0, 0, 0]), + flavour_desc!(22050, 2, 20, 10, 10, [19, 0, 0, 0]), + flavour_desc!(22050, 2, 32, 5, 16, [20, 0, 0, 0]), + flavour_desc!(44100, 2, 32, 5, 16, [21, 0, 0, 0]), + flavour_desc!(44100, 2, 44, 5, 16, [22, 0, 0, 0]), + flavour_desc!(44100, 2, 44, 5, 16, [23, 0, 0, 0]), + flavour_desc!(44100, 2, 64, 5, 16, [24, 0, 0, 0]), + flavour_desc!(44100, 2, 96, 5, 16, [25, 0, 0, 0]), + flavour_desc!(11025, 2, 12, 11, 8, [26, 0, 0, 0]), + flavour_desc!(44100, 2, 64, 5, 16, [27, 0, 0, 0]), + flavour_desc!(44100, 2, 96, 5, 16, [28, 0, 0, 0]), + flavour_desc!(22050, 2, 44, 5, 16, [29, 0, 0, 0]), + flavour_desc!(44100, 5, 96, 5, 16, [21, 16, 21, 0]), + flavour_desc!(44100, 6, 131, 3, 10, [23, 5, 30, 21]), + flavour_desc!(44100, 6, 183, 2, 10, [24, 6, 30, 23]), + flavour_desc!(44100, 6, 268, 1, 1, [25, 6, 30, 25]) +]; + +struct BitrateParams { + channels: u8, + max_subbands: u8, + frame_bits: u32, + js_start: u8, + js_bits: u8, +} + +macro_rules! br_desc { + ($ch:expr, $bits:expr, $subbands:expr, $js_start:expr, $js_bits:expr) => { + BitrateParams { + channels: $ch, + max_subbands: $subbands, + frame_bits: $bits, + js_start: $js_start, + js_bits: $js_bits, + } + } +} + +const BITRATE_PARAMS: &[BitrateParams] = &[ + br_desc!(1, 256, 12, 0, 0), + br_desc!(1, 256, 12, 0, 0), + br_desc!(1, 376, 18, 0, 0), + br_desc!(1, 480, 23, 0, 0), + br_desc!(1, 744, 37, 0, 0), + br_desc!(1, 1024, 47, 0, 0), + br_desc!(1, 1488, 47, 0, 0), + br_desc!(1, 744, 24, 0, 0), + + br_desc!(1, 192, 9, 0, 0), + br_desc!(2, 464, 11, 0, 0), + br_desc!(2, 752, 18, 0, 0), + br_desc!(2, 1024, 24, 0, 0), + br_desc!(2, 1488, 37, 0, 0), + br_desc!(2, 2224, 47, 0, 0), + br_desc!(1, 1488, 47, 0, 0), + br_desc!(1, 480, 47, 0, 0), + + br_desc!(1, 744, 47, 0, 0), + br_desc!(2, 384, 16, 1, 3), + br_desc!(2, 480, 20, 1, 3), + br_desc!(2, 480, 23, 1, 3), + br_desc!(2, 744, 24, 2, 4), + br_desc!(2, 744, 32, 2, 4), + br_desc!(2, 1024, 32, 5, 5), + br_desc!(2, 1024, 37, 2, 4), + + br_desc!(2, 1488, 37, 6, 5), + br_desc!(2, 2240, 37, 8, 5), + br_desc!(2, 288, 9, 1, 2), + br_desc!(2, 1488, 30, 17, 5), + br_desc!(2, 2240, 34, 19, 5), + br_desc!(2, 1024, 23, 17, 5), + br_desc!(1, 256, 1, 0, 0) +]; + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::demuxers::*; + use nihav_core::muxers::*; + use nihav_codec_support::test::enc_video::*; + use crate::*; + + fn test_encoder(name: &'static str, enc_options: &[NAOption], bitrate: u32, channels: u8, hash: &[u32; 4]) { + let mut dmx_reg = RegisteredDemuxers::new(); + realmedia_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + realmedia_register_all_decoders(&mut dec_reg); + let mut mux_reg = RegisteredMuxers::new(); + realmedia_register_all_muxers(&mut mux_reg); + let mut enc_reg = RegisteredEncoders::new(); + realmedia_register_all_encoders(&mut enc_reg); + + // sample from a private collection + let dec_config = DecoderTestParams { + demuxer: "realmedia", + in_name: "assets/RV/rv30_weighted_mc.rm", + stream_type: StreamType::Audio, + limit: None, + dmx_reg, dec_reg, + }; + let enc_config = EncoderTestParams { + muxer: "realmedia", + enc_name: "cook", + out_name: name, + mux_reg, enc_reg, + }; + let dst_ainfo = NAAudioInfo { + sample_rate: 0, + channels, + format: SND_F32P_FORMAT, + block_len: 1024, + }; + let enc_params = EncodeParameters { + format: NACodecTypeInfo::Audio(dst_ainfo), + quality: 0, + bitrate, + tb_num: 0, + tb_den: 0, + flags: 0, + }; + //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options); + + test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash); + } + #[test] + fn test_cook_encoder_mono() { + let enc_options = &[ + ]; + test_encoder("cook-mono.rma", enc_options, 0, 1, + &[0xa07cffaa, 0x257e8ca1, 0x27f58960, 0xb48f64f4]); + } + #[test] + fn test_cook_encoder_stereo() { + let enc_options = &[ + NAOption { name: super::G8_ONLY_OPTION, value: NAValue::Bool(true) }, + ]; + test_encoder("cook-stereo.rma", enc_options, 0, 2, + &[0xbc407879, 0x18c7b334, 0xc0299482, 0x89c6b953]); + } + #[test] + fn test_cook_encoder_jstereo_32kbps() { + let enc_options = &[ + ]; + test_encoder("cook-jstereo32.rma", enc_options, 32000, 2, + &[0xe7526c4e, 0xda095156, 0x840a74f7, 0x0e9a1603]); + } + #[test] + fn test_cook_encoder_jstereo_64kbps() { + let enc_options = &[ + ]; + test_encoder("cook-jstereo64.rma", enc_options, 64000, 2, + &[0x09688175, 0x9abe1aac, 0x3c3f15fb, 0x066b99f0]); + } + #[test] + fn test_cook_encoder_jstereo_96kbps() { + let enc_options = &[ + ]; + test_encoder("cook-jstereo96.rma", enc_options, 96000, 2, + &[0x51829ff8, 0x00017191, 0x5bc95490, 0xd1d03faf]); + } +} diff --git a/nihav-realmedia/src/codecs/mod.rs b/nihav-realmedia/src/codecs/mod.rs index c55e6ae..055853f 100644 --- a/nihav-realmedia/src/codecs/mod.rs +++ b/nihav-realmedia/src/codecs/mod.rs @@ -52,7 +52,7 @@ pub mod ra144; pub mod ra288; #[cfg(feature="decoder_cook")] pub mod cook; -#[cfg(feature="decoder_cook")] +#[cfg(any(feature="decoder_cook", feature="encoder_cook"))] pub mod cookdata; #[cfg(feature="decoder_ralf")] pub mod ralf; @@ -85,3 +85,20 @@ pub fn realmedia_register_all_decoders(rd: &mut RegisteredDecoders) { rd.add_decoder(*decoder); } } + +#[cfg(feature="encoder_cook")] +mod cookenc; + +#[cfg(feature="encoders")] +const ENCODERS: &[EncoderInfo] = &[ +#[cfg(feature="encoder_cook")] + EncoderInfo { name: "cook", get_encoder: cookenc::get_encoder }, +]; + +/// Registers all available encoders provided by this crate. +#[cfg(feature="encoders")] +pub fn realmedia_register_all_encoders(re: &mut RegisteredEncoders) { + for encoder in ENCODERS.iter() { + re.add_encoder(*encoder); + } +} diff --git a/nihav-realmedia/src/lib.rs b/nihav-realmedia/src/lib.rs index 8012992..9cacc28 100644 --- a/nihav-realmedia/src/lib.rs +++ b/nihav-realmedia/src/lib.rs @@ -2,7 +2,7 @@ extern crate nihav_core; extern crate nihav_codec_support; -#[cfg(feature="decoders")] +#[cfg(any(feature="decoders", feature="encoders"))] #[allow(clippy::cast_lossless)] #[allow(clippy::collapsible_if)] #[allow(clippy::comparison_chain)] @@ -16,6 +16,8 @@ extern crate nihav_codec_support; mod codecs; #[cfg(feature="decoders")] pub use crate::codecs::realmedia_register_all_decoders; +#[cfg(feature="encoders")] +pub use crate::codecs::realmedia_register_all_encoders; #[cfg(feature="demuxers")] #[allow(clippy::cast_lossless)] -- 2.30.2