Skip to main content

oximedia_codec/reconstruct/
film_grain.rs

1//! Film grain synthesis for AV1.
2//!
3//! Film grain synthesis adds realistic grain patterns to decoded video,
4//! allowing encoders to remove grain before encoding and synthesize it
5//! during playback for better compression efficiency.
6
7#![forbid(unsafe_code)]
8#![allow(clippy::unreadable_literal)]
9#![allow(clippy::items_after_statements)]
10#![allow(clippy::unnecessary_wraps)]
11#![allow(clippy::struct_excessive_bools)]
12#![allow(clippy::identity_op)]
13#![allow(clippy::range_plus_one)]
14#![allow(clippy::needless_range_loop)]
15#![allow(clippy::useless_conversion)]
16#![allow(clippy::redundant_closure_for_method_calls)]
17#![allow(clippy::single_match_else)]
18#![allow(dead_code)]
19#![allow(clippy::doc_markdown)]
20#![allow(clippy::unused_self)]
21#![allow(clippy::trivially_copy_pass_by_ref)]
22#![allow(clippy::cast_possible_truncation)]
23#![allow(clippy::cast_sign_loss)]
24#![allow(clippy::cast_possible_wrap)]
25#![allow(clippy::missing_errors_doc)]
26#![allow(clippy::too_many_arguments)]
27#![allow(clippy::similar_names)]
28#![allow(clippy::cast_precision_loss)]
29#![allow(clippy::cast_lossless)]
30
31use super::pipeline::FrameContext;
32use super::{FrameBuffer, PlaneBuffer, PlaneType, ReconstructResult};
33
34// =============================================================================
35// Constants
36// =============================================================================
37
38/// Maximum number of AR (auto-regression) coefficients for luma.
39pub const MAX_AR_COEFFS_Y: usize = 24;
40
41/// Maximum number of AR coefficients for chroma.
42pub const MAX_AR_COEFFS_UV: usize = 25;
43
44/// Maximum AR lag.
45pub const MAX_AR_LAG: usize = 3;
46
47/// Grain block size.
48pub const GRAIN_BLOCK_SIZE: usize = 32;
49
50/// Maximum number of luma scaling points.
51pub const MAX_LUMA_POINTS: usize = 14;
52
53/// Maximum number of chroma scaling points.
54pub const MAX_CHROMA_POINTS: usize = 10;
55
56/// Grain seed base.
57pub const GRAIN_SEED: u16 = 0xB524;
58
59/// LUT size for grain values.
60pub const GRAIN_LUT_SIZE: usize = 82;
61
62// =============================================================================
63// Film Grain Parameters
64// =============================================================================
65
66/// Scaling point for grain intensity.
67#[derive(Clone, Copy, Debug, Default)]
68pub struct ScalingPoint {
69    /// Input value (0-255 for 8-bit).
70    pub value: u8,
71    /// Scaling factor.
72    pub scaling: u8,
73}
74
75impl ScalingPoint {
76    /// Create a new scaling point.
77    #[must_use]
78    pub const fn new(value: u8, scaling: u8) -> Self {
79        Self { value, scaling }
80    }
81}
82
83/// Film grain parameters.
84#[derive(Clone, Debug)]
85pub struct FilmGrainParams {
86    /// Apply grain to this frame.
87    pub apply_grain: bool,
88    /// Random seed for grain generation.
89    pub grain_seed: u16,
90    /// Update grain parameters.
91    pub update_grain: bool,
92    /// Number of Y scaling points.
93    pub num_y_points: usize,
94    /// Y scaling points.
95    pub y_points: [ScalingPoint; MAX_LUMA_POINTS],
96    /// Chroma scaling from luma.
97    pub chroma_scaling_from_luma: bool,
98    /// Number of Cb scaling points.
99    pub num_cb_points: usize,
100    /// Cb scaling points.
101    pub cb_points: [ScalingPoint; MAX_CHROMA_POINTS],
102    /// Number of Cr scaling points.
103    pub num_cr_points: usize,
104    /// Cr scaling points.
105    pub cr_points: [ScalingPoint; MAX_CHROMA_POINTS],
106    /// Grain scaling shift (8-11).
107    pub grain_scaling_minus_8: u8,
108    /// AR coefficients lag (0-3).
109    pub ar_coeff_lag: u8,
110    /// AR coefficients for Y.
111    pub ar_coeffs_y: [i8; MAX_AR_COEFFS_Y],
112    /// AR coefficients for Cb.
113    pub ar_coeffs_cb: [i8; MAX_AR_COEFFS_UV],
114    /// AR coefficients for Cr.
115    pub ar_coeffs_cr: [i8; MAX_AR_COEFFS_UV],
116    /// AR coefficient shift (6-9).
117    pub ar_coeff_shift_minus_6: u8,
118    /// Grain scale shift.
119    pub grain_scale_shift: u8,
120    /// Cb multiplier.
121    pub cb_mult: u8,
122    /// Cb luma multiplier.
123    pub cb_luma_mult: u8,
124    /// Cb offset.
125    pub cb_offset: u16,
126    /// Cr multiplier.
127    pub cr_mult: u8,
128    /// Cr luma multiplier.
129    pub cr_luma_mult: u8,
130    /// Cr offset.
131    pub cr_offset: u16,
132    /// Overlap flag.
133    pub overlap_flag: bool,
134    /// Clip to restricted range.
135    pub clip_to_restricted_range: bool,
136}
137
138impl Default for FilmGrainParams {
139    fn default() -> Self {
140        Self {
141            apply_grain: false,
142            grain_seed: 0,
143            update_grain: false,
144            num_y_points: 0,
145            y_points: [ScalingPoint::default(); MAX_LUMA_POINTS],
146            chroma_scaling_from_luma: false,
147            num_cb_points: 0,
148            cb_points: [ScalingPoint::default(); MAX_CHROMA_POINTS],
149            num_cr_points: 0,
150            cr_points: [ScalingPoint::default(); MAX_CHROMA_POINTS],
151            grain_scaling_minus_8: 0,
152            ar_coeff_lag: 0,
153            ar_coeffs_y: [0; MAX_AR_COEFFS_Y],
154            ar_coeffs_cb: [0; MAX_AR_COEFFS_UV],
155            ar_coeffs_cr: [0; MAX_AR_COEFFS_UV],
156            ar_coeff_shift_minus_6: 0,
157            grain_scale_shift: 0,
158            cb_mult: 0,
159            cb_luma_mult: 0,
160            cb_offset: 0,
161            cr_mult: 0,
162            cr_luma_mult: 0,
163            cr_offset: 0,
164            overlap_flag: false,
165            clip_to_restricted_range: false,
166        }
167    }
168}
169
170impl FilmGrainParams {
171    /// Create new film grain parameters.
172    #[must_use]
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    /// Check if grain should be applied.
178    #[must_use]
179    pub const fn is_enabled(&self) -> bool {
180        self.apply_grain && (self.num_y_points > 0 || self.chroma_scaling_from_luma)
181    }
182
183    /// Get grain scaling value.
184    #[must_use]
185    pub const fn grain_scaling(&self) -> u8 {
186        self.grain_scaling_minus_8 + 8
187    }
188
189    /// Get AR coefficient shift.
190    #[must_use]
191    pub const fn ar_coeff_shift(&self) -> u8 {
192        self.ar_coeff_shift_minus_6 + 6
193    }
194
195    /// Get number of AR coefficients for Y.
196    #[must_use]
197    pub fn num_ar_coeffs_y(&self) -> usize {
198        let lag = self.ar_coeff_lag as usize;
199        2 * lag * (lag + 1)
200    }
201
202    /// Get number of AR coefficients for chroma.
203    #[must_use]
204    pub fn num_ar_coeffs_uv(&self) -> usize {
205        let lag = self.ar_coeff_lag as usize;
206        2 * lag * (lag + 1) + 1
207    }
208
209    /// Add a Y scaling point.
210    pub fn add_y_point(&mut self, value: u8, scaling: u8) {
211        if self.num_y_points < MAX_LUMA_POINTS {
212            self.y_points[self.num_y_points] = ScalingPoint::new(value, scaling);
213            self.num_y_points += 1;
214        }
215    }
216
217    /// Add a Cb scaling point.
218    pub fn add_cb_point(&mut self, value: u8, scaling: u8) {
219        if self.num_cb_points < MAX_CHROMA_POINTS {
220            self.cb_points[self.num_cb_points] = ScalingPoint::new(value, scaling);
221            self.num_cb_points += 1;
222        }
223    }
224
225    /// Add a Cr scaling point.
226    pub fn add_cr_point(&mut self, value: u8, scaling: u8) {
227        if self.num_cr_points < MAX_CHROMA_POINTS {
228            self.cr_points[self.num_cr_points] = ScalingPoint::new(value, scaling);
229            self.num_cr_points += 1;
230        }
231    }
232}
233
234// =============================================================================
235// Grain Block
236// =============================================================================
237
238/// Grain pattern for a block.
239#[derive(Clone, Debug)]
240pub struct GrainBlock {
241    /// Grain values.
242    values: Vec<i16>,
243    /// Block width.
244    width: usize,
245    /// Block height.
246    height: usize,
247}
248
249impl GrainBlock {
250    /// Create a new grain block.
251    #[must_use]
252    pub fn new(width: usize, height: usize) -> Self {
253        Self {
254            values: vec![0; width * height],
255            width,
256            height,
257        }
258    }
259
260    /// Get grain value at position.
261    #[must_use]
262    pub fn get(&self, x: usize, y: usize) -> i16 {
263        if x < self.width && y < self.height {
264            self.values[y * self.width + x]
265        } else {
266            0
267        }
268    }
269
270    /// Set grain value at position.
271    pub fn set(&mut self, x: usize, y: usize, value: i16) {
272        if x < self.width && y < self.height {
273            self.values[y * self.width + x] = value;
274        }
275    }
276
277    /// Get values slice.
278    #[must_use]
279    pub fn values(&self) -> &[i16] {
280        &self.values
281    }
282
283    /// Get mutable values slice.
284    pub fn values_mut(&mut self) -> &mut [i16] {
285        &mut self.values
286    }
287}
288
289// =============================================================================
290// Pseudo-Random Number Generator
291// =============================================================================
292
293/// Linear feedback shift register for grain generation.
294struct GrainRng {
295    state: u16,
296}
297
298impl GrainRng {
299    /// Create a new grain RNG.
300    fn new(seed: u16) -> Self {
301        Self { state: seed }
302    }
303
304    /// Generate next random value.
305    fn next(&mut self) -> i16 {
306        // LFSR with taps at bits 0, 1, 3, 12
307        let bit =
308            ((self.state >> 0) ^ (self.state >> 1) ^ (self.state >> 3) ^ (self.state >> 12)) & 1;
309        self.state = (self.state >> 1) | (bit << 15);
310        (self.state as i16) >> 5 // Return signed 11-bit value
311    }
312
313    /// Generate a Gaussian-distributed value.
314    fn gaussian(&mut self) -> i16 {
315        // Approximate Gaussian using sum of uniform values
316        let mut sum: i32 = 0;
317        for _ in 0..4 {
318            sum += i32::from(self.next());
319        }
320        (sum / 4) as i16
321    }
322}
323
324// =============================================================================
325// Film Grain Synthesizer
326// =============================================================================
327
328/// Film grain synthesizer.
329#[derive(Debug)]
330pub struct FilmGrainSynthesizer {
331    /// Current parameters.
332    params: FilmGrainParams,
333    /// Bit depth.
334    bit_depth: u8,
335    /// Luma grain LUT.
336    luma_grain: Vec<i16>,
337    /// Cb grain LUT.
338    cb_grain: Vec<i16>,
339    /// Cr grain LUT.
340    cr_grain: Vec<i16>,
341    /// Luma scaling LUT.
342    luma_scaling: Vec<u8>,
343    /// Cb scaling LUT.
344    cb_scaling: Vec<u8>,
345    /// Cr scaling LUT.
346    cr_scaling: Vec<u8>,
347}
348
349impl FilmGrainSynthesizer {
350    /// Create a new film grain synthesizer.
351    #[must_use]
352    pub fn new(bit_depth: u8) -> Self {
353        let lut_size = GRAIN_LUT_SIZE * GRAIN_LUT_SIZE;
354        let scaling_size = 1 << bit_depth.min(8);
355
356        Self {
357            params: FilmGrainParams::default(),
358            bit_depth,
359            luma_grain: vec![0; lut_size],
360            cb_grain: vec![0; lut_size],
361            cr_grain: vec![0; lut_size],
362            luma_scaling: vec![0; scaling_size],
363            cb_scaling: vec![0; scaling_size],
364            cr_scaling: vec![0; scaling_size],
365        }
366    }
367
368    /// Set film grain parameters.
369    pub fn set_params(&mut self, params: FilmGrainParams) {
370        self.params = params;
371        if self.params.is_enabled() {
372            self.generate_grain_luts();
373            self.generate_scaling_luts();
374        }
375    }
376
377    /// Get current parameters.
378    #[must_use]
379    pub fn params(&self) -> &FilmGrainParams {
380        &self.params
381    }
382
383    /// Generate grain lookup tables.
384    fn generate_grain_luts(&mut self) {
385        let mut rng = GrainRng::new(self.params.grain_seed ^ GRAIN_SEED);
386
387        // Generate luma grain
388        for val in &mut self.luma_grain {
389            *val = rng.gaussian();
390        }
391
392        // Generate Cb grain
393        for val in &mut self.cb_grain {
394            *val = rng.gaussian();
395        }
396
397        // Generate Cr grain
398        for val in &mut self.cr_grain {
399            *val = rng.gaussian();
400        }
401
402        // Apply AR filtering if coefficients are present
403        if self.params.ar_coeff_lag > 0 {
404            self.apply_ar_filter();
405        }
406    }
407
408    /// Apply auto-regressive filter to grain.
409    fn apply_ar_filter(&mut self) {
410        let lag = self.params.ar_coeff_lag as usize;
411        let shift = self.params.ar_coeff_shift();
412
413        // Apply AR filter to luma grain
414        for y in lag..GRAIN_LUT_SIZE {
415            for x in lag..(GRAIN_LUT_SIZE - lag) {
416                let mut sum: i32 = 0;
417                let mut coeff_idx = 0;
418
419                for dy in 0..=lag {
420                    for dx in 0..(2 * lag + 1) {
421                        if dy == 0 && dx >= lag {
422                            break;
423                        }
424                        let coeff = i32::from(self.params.ar_coeffs_y[coeff_idx]);
425                        let grain_idx = (y - lag + dy) * GRAIN_LUT_SIZE + (x - lag + dx);
426                        sum += coeff * i32::from(self.luma_grain[grain_idx]);
427                        coeff_idx += 1;
428                    }
429                }
430
431                let idx = y * GRAIN_LUT_SIZE + x;
432                self.luma_grain[idx] = (i32::from(self.luma_grain[idx]) + (sum >> shift)) as i16;
433            }
434        }
435    }
436
437    /// Generate scaling lookup tables.
438    fn generate_scaling_luts(&mut self) {
439        // Copy points to avoid borrow issues
440        let y_points: Vec<_> = self.params.y_points[..self.params.num_y_points].to_vec();
441        let cb_points: Vec<_> = self.params.cb_points[..self.params.num_cb_points].to_vec();
442        let cr_points: Vec<_> = self.params.cr_points[..self.params.num_cr_points].to_vec();
443        let chroma_from_luma = self.params.chroma_scaling_from_luma;
444
445        // Generate luma scaling LUT
446        interpolate_scaling_points(&y_points, &mut self.luma_scaling);
447
448        // Generate Cb scaling LUT
449        if chroma_from_luma {
450            self.cb_scaling.copy_from_slice(&self.luma_scaling);
451        } else {
452            interpolate_scaling_points(&cb_points, &mut self.cb_scaling);
453        }
454
455        // Generate Cr scaling LUT
456        if chroma_from_luma {
457            self.cr_scaling.copy_from_slice(&self.luma_scaling);
458        } else {
459            interpolate_scaling_points(&cr_points, &mut self.cr_scaling);
460        }
461    }
462
463    /// Apply film grain to a frame.
464    ///
465    /// # Errors
466    ///
467    /// Returns error if grain application fails.
468    pub fn apply(
469        &mut self,
470        frame: &mut FrameBuffer,
471        _context: &FrameContext,
472    ) -> ReconstructResult<()> {
473        if !self.params.is_enabled() {
474            return Ok(());
475        }
476
477        let bd = frame.bit_depth();
478
479        // Apply to Y plane
480        self.apply_to_plane(frame.y_plane_mut(), PlaneType::Y, bd);
481
482        // Apply to chroma planes
483        if let Some(u) = frame.u_plane_mut() {
484            self.apply_to_plane(u, PlaneType::U, bd);
485        }
486        if let Some(v) = frame.v_plane_mut() {
487            self.apply_to_plane(v, PlaneType::V, bd);
488        }
489
490        Ok(())
491    }
492
493    /// Apply grain to a single plane.
494    fn apply_to_plane(&self, plane: &mut PlaneBuffer, plane_type: PlaneType, bd: u8) {
495        let width = plane.width() as usize;
496        let height = plane.height() as usize;
497        let max_val = (1i32 << bd) - 1;
498
499        let (grain_lut, scaling_lut) = match plane_type {
500            PlaneType::Y => (&self.luma_grain, &self.luma_scaling),
501            PlaneType::U => (&self.cb_grain, &self.cb_scaling),
502            PlaneType::V => (&self.cr_grain, &self.cr_scaling),
503        };
504
505        let grain_scale = self.params.grain_scaling();
506        let grain_shift = self.params.grain_scale_shift;
507
508        // Process each pixel
509        for y in 0..height {
510            for x in 0..width {
511                let pixel = plane.get(x as u32, y as u32);
512
513                // Get scaling factor
514                let scaling_idx = (pixel as usize).min(scaling_lut.len() - 1);
515                let scaling = i32::from(scaling_lut[scaling_idx]);
516
517                // Get grain value
518                let grain_x = x % GRAIN_LUT_SIZE;
519                let grain_y = y % GRAIN_LUT_SIZE;
520                let grain_idx = grain_y * GRAIN_LUT_SIZE + grain_x;
521                let grain = i32::from(grain_lut[grain_idx]);
522
523                // Apply grain
524                let scaled_grain = (grain * scaling) >> grain_scale;
525                let adjusted_grain = scaled_grain >> grain_shift;
526                let result = (i32::from(pixel) + adjusted_grain).clamp(0, max_val);
527
528                plane.set(x as u32, y as u32, result as i16);
529            }
530        }
531    }
532
533    /// Generate a grain block for specific position.
534    #[must_use]
535    pub fn generate_block(&self, x: usize, y: usize, plane: PlaneType) -> GrainBlock {
536        let mut block = GrainBlock::new(GRAIN_BLOCK_SIZE, GRAIN_BLOCK_SIZE);
537
538        let grain_lut = match plane {
539            PlaneType::Y => &self.luma_grain,
540            PlaneType::U => &self.cb_grain,
541            PlaneType::V => &self.cr_grain,
542        };
543
544        for by in 0..GRAIN_BLOCK_SIZE {
545            for bx in 0..GRAIN_BLOCK_SIZE {
546                let grain_x = (x + bx) % GRAIN_LUT_SIZE;
547                let grain_y = (y + by) % GRAIN_LUT_SIZE;
548                let grain_idx = grain_y * GRAIN_LUT_SIZE + grain_x;
549                block.set(bx, by, grain_lut[grain_idx]);
550            }
551        }
552
553        block
554    }
555}
556
557/// Interpolate scaling points to create LUT.
558fn interpolate_scaling_points(points: &[ScalingPoint], lut: &mut [u8]) {
559    if points.is_empty() {
560        lut.fill(0);
561        return;
562    }
563
564    let lut_size = lut.len();
565
566    // Fill before first point
567    let first_scaling = points[0].scaling;
568    for val in lut.iter_mut().take(points[0].value as usize) {
569        *val = first_scaling;
570    }
571
572    // Interpolate between points
573    for i in 0..points.len().saturating_sub(1) {
574        let p0 = &points[i];
575        let p1 = &points[i + 1];
576
577        for x in p0.value as usize..=p1.value as usize {
578            if x < lut_size {
579                let t = (x - p0.value as usize) as f32 / (p1.value - p0.value).max(1) as f32;
580                lut[x] = ((1.0 - t) * p0.scaling as f32 + t * p1.scaling as f32).round() as u8;
581            }
582        }
583    }
584
585    // Fill after last point
586    let last_point = points.last().expect("points is non-empty by construction");
587    for val in lut.iter_mut().skip(last_point.value as usize + 1) {
588        *val = last_point.scaling;
589    }
590}
591
592// =============================================================================
593// Tests
594// =============================================================================
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use crate::reconstruct::ChromaSubsampling;
600
601    #[test]
602    fn test_scaling_point() {
603        let point = ScalingPoint::new(128, 64);
604        assert_eq!(point.value, 128);
605        assert_eq!(point.scaling, 64);
606    }
607
608    #[test]
609    fn test_film_grain_params_default() {
610        let params = FilmGrainParams::default();
611        assert!(!params.apply_grain);
612        assert!(!params.is_enabled());
613        assert_eq!(params.num_y_points, 0);
614    }
615
616    #[test]
617    fn test_film_grain_params_enabled() {
618        let mut params = FilmGrainParams::new();
619        params.apply_grain = true;
620        params.add_y_point(0, 32);
621        params.add_y_point(255, 64);
622
623        assert!(params.is_enabled());
624        assert_eq!(params.num_y_points, 2);
625    }
626
627    #[test]
628    fn test_film_grain_params_scaling_values() {
629        let mut params = FilmGrainParams::new();
630        params.grain_scaling_minus_8 = 2;
631        params.ar_coeff_shift_minus_6 = 3;
632
633        assert_eq!(params.grain_scaling(), 10);
634        assert_eq!(params.ar_coeff_shift(), 9);
635    }
636
637    #[test]
638    fn test_film_grain_params_ar_coeffs() {
639        let mut params = FilmGrainParams::new();
640
641        params.ar_coeff_lag = 0;
642        assert_eq!(params.num_ar_coeffs_y(), 0);
643
644        params.ar_coeff_lag = 1;
645        assert_eq!(params.num_ar_coeffs_y(), 4);
646
647        params.ar_coeff_lag = 2;
648        assert_eq!(params.num_ar_coeffs_y(), 12);
649
650        params.ar_coeff_lag = 3;
651        assert_eq!(params.num_ar_coeffs_y(), 24);
652    }
653
654    #[test]
655    fn test_grain_block() {
656        let mut block = GrainBlock::new(32, 32);
657        block.set(10, 20, 100);
658        assert_eq!(block.get(10, 20), 100);
659        assert_eq!(block.get(0, 0), 0);
660    }
661
662    #[test]
663    fn test_grain_rng() {
664        let mut rng = GrainRng::new(12345);
665
666        // Generate some values and check they're in range
667        for _ in 0..100 {
668            let val = rng.next();
669            assert!(val >= -2048 && val < 2048);
670        }
671    }
672
673    #[test]
674    fn test_grain_rng_gaussian() {
675        let mut rng = GrainRng::new(12345);
676
677        // Gaussian values should be centered around 0
678        let mut sum: i32 = 0;
679        for _ in 0..1000 {
680            sum += i32::from(rng.gaussian());
681        }
682        let mean = sum / 1000;
683        assert!(mean.abs() < 100);
684    }
685
686    #[test]
687    fn test_film_grain_synthesizer_creation() {
688        let synth = FilmGrainSynthesizer::new(8);
689        assert_eq!(synth.bit_depth, 8);
690        assert!(!synth.params().is_enabled());
691    }
692
693    #[test]
694    fn test_film_grain_synthesizer_set_params() {
695        let mut synth = FilmGrainSynthesizer::new(8);
696
697        let mut params = FilmGrainParams::new();
698        params.apply_grain = true;
699        params.grain_seed = 12345;
700        params.add_y_point(0, 32);
701        params.add_y_point(255, 64);
702
703        synth.set_params(params);
704        assert!(synth.params().is_enabled());
705    }
706
707    #[test]
708    fn test_film_grain_apply_disabled() {
709        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
710        let context = FrameContext::new(64, 64);
711
712        let mut synth = FilmGrainSynthesizer::new(8);
713        let result = synth.apply(&mut frame, &context);
714        assert!(result.is_ok());
715    }
716
717    #[test]
718    fn test_film_grain_apply_enabled() {
719        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
720
721        // Set some initial values
722        for y in 0..64 {
723            for x in 0..64 {
724                frame.y_plane_mut().set(x, y, 128);
725            }
726        }
727
728        let context = FrameContext::new(64, 64);
729
730        let mut synth = FilmGrainSynthesizer::new(8);
731        let mut params = FilmGrainParams::new();
732        params.apply_grain = true;
733        params.grain_seed = 12345;
734        params.add_y_point(0, 16);
735        params.add_y_point(255, 32);
736        synth.set_params(params);
737
738        let result = synth.apply(&mut frame, &context);
739        assert!(result.is_ok());
740    }
741
742    #[test]
743    fn test_generate_block() {
744        let mut synth = FilmGrainSynthesizer::new(8);
745
746        let mut params = FilmGrainParams::new();
747        params.apply_grain = true;
748        params.grain_seed = 12345;
749        params.add_y_point(0, 32);
750        params.add_y_point(255, 64);
751        synth.set_params(params);
752
753        let block = synth.generate_block(0, 0, PlaneType::Y);
754        assert_eq!(block.width, GRAIN_BLOCK_SIZE);
755        assert_eq!(block.height, GRAIN_BLOCK_SIZE);
756    }
757
758    #[test]
759    fn test_constants() {
760        assert_eq!(MAX_AR_COEFFS_Y, 24);
761        assert_eq!(MAX_AR_COEFFS_UV, 25);
762        assert_eq!(MAX_AR_LAG, 3);
763        assert_eq!(GRAIN_BLOCK_SIZE, 32);
764        assert_eq!(MAX_LUMA_POINTS, 14);
765        assert_eq!(MAX_CHROMA_POINTS, 10);
766        assert_eq!(GRAIN_LUT_SIZE, 82);
767    }
768}