Skip to main content

av_denoise_vs/
lib.rs

1//! VapourSynth plugin exposing av-denoise as `avd` filters.
2
3mod filter;
4pub mod frames;
5pub mod params;
6
7use anyhow::Error;
8use vapoursynth::core::CoreRef;
9use vapoursynth::plugins::{Filter, FilterArgument, Metadata};
10use vapoursynth::prelude::{API, Node};
11use vapoursynth::{export_vapoursynth_plugin, make_filter_function};
12
13use crate::filter::Denoise;
14use crate::params::{AlgorithmKind, RawParams};
15
16/// Reads one optional UTF-8 script argument, naming `field` in the error
17/// when the bytes are not valid UTF-8.
18fn opt_string(bytes: Option<&[u8]>, field: &str) -> Result<Option<String>, Error> {
19    bytes
20        .map(|b| String::from_utf8(b.to_vec()).map_err(|_| anyhow::anyhow!("{field} must be valid UTF-8")))
21        .transpose()
22}
23
24/// Reads the optional `accelerators` script argument, a comma-separated
25/// list of accelerator names, into the `Vec<String>` [`RawParams`]
26/// wants.
27///
28/// VapourSynth script arguments have no native string array type that
29/// fits cleanly into `make_filter_function!`'s generated argument
30/// string, so this reuses the plain `data` type and splits it, matching
31/// how `channel_mode` and `device` already take a single string.
32fn opt_accelerators(bytes: Option<&[u8]>) -> Result<Option<Vec<String>>, Error> {
33    let Some(joined) = opt_string(bytes, "accelerators")? else {
34        return Ok(None);
35    };
36
37    let names: Vec<String> = joined
38        .split(',')
39        .map(str::trim)
40        .filter(|s| !s.is_empty())
41        .map(str::to_string)
42        .collect();
43
44    if names.is_empty() {
45        anyhow::bail!("accelerators must name at least one accelerator when set");
46    }
47
48    Ok(Some(names))
49}
50
51/// Reads the optional `motion_compensation` script argument.
52///
53/// VapourSynth script arguments have no native boolean type, so this
54/// takes the plain `int` type every other on/off knob in the wider
55/// VapourSynth ecosystem uses, and reads it the same way: `0` is off,
56/// anything else is on.
57fn opt_bool(value: Option<i64>) -> Option<bool> {
58    value.map(|v| v != 0)
59}
60
61/// Builds a [`RawParams`] from a filter function's raw script arguments.
62#[expect(clippy::too_many_arguments)]
63fn raw_params(
64    strength: Option<f64>,
65    variant: Option<&[u8]>,
66    preset: Option<&[u8]>,
67    prefilter: Option<&[u8]>,
68    channel_mode: Option<&[u8]>,
69    luma_strength: Option<f64>,
70    chroma_strength: Option<f64>,
71    luma_lambda_ht: Option<f64>,
72    chroma_lambda_ht: Option<f64>,
73    luma_mismatch_scale: Option<f64>,
74    chroma_mismatch_scale: Option<f64>,
75    device: Option<&[u8]>,
76    accelerators: Option<&[u8]>,
77    search_radius: Option<i64>,
78    patch_radius: Option<i64>,
79    temporal_radius: Option<i64>,
80    sigma: Option<f64>,
81    sigma_scale: Option<f64>,
82    motion_compensation: Option<i64>,
83    lambda_ht: Option<f64>,
84    lambda_ht_scale: Option<f64>,
85    spatial_radius: Option<i64>,
86    refine: Option<i64>,
87) -> Result<RawParams, Error> {
88    Ok(RawParams {
89        strength,
90        variant: opt_string(variant, "variant")?,
91        preset: opt_string(preset, "preset")?,
92        prefilter: opt_string(prefilter, "prefilter")?,
93        channel_mode: opt_string(channel_mode, "channel_mode")?,
94        luma_strength,
95        chroma_strength,
96        luma_lambda_ht,
97        chroma_lambda_ht,
98        luma_mismatch_scale,
99        chroma_mismatch_scale,
100        device: opt_string(device, "device")?,
101        accelerators: opt_accelerators(accelerators)?,
102        search_radius,
103        patch_radius,
104        temporal_radius,
105        sigma,
106        sigma_scale,
107        motion_compensation: opt_bool(motion_compensation),
108        lambda_ht,
109        lambda_ht_scale,
110        spatial_radius,
111        refine,
112    })
113}
114
115make_filter_function! {
116    NlmeansFunction, "NLMeans"
117
118    #[expect(clippy::too_many_arguments)]
119    fn create_nlmeans<'core>(
120        api: API,
121        core: CoreRef<'core>,
122        clip: Node<'core>,
123        strength: Option<f64>,
124        variant: Option<&[u8]>,
125        preset: Option<&[u8]>,
126        prefilter: Option<&[u8]>,
127        channel_mode: Option<&[u8]>,
128        luma_strength: Option<f64>,
129        chroma_strength: Option<f64>,
130        device: Option<&[u8]>,
131        accelerators: Option<&[u8]>,
132        search_radius: Option<i64>,
133        patch_radius: Option<i64>,
134        temporal_radius: Option<i64>,
135        sigma: Option<f64>,
136        sigma_scale: Option<f64>,
137        motion_compensation: Option<i64>,
138    ) -> Result<Option<Box<dyn Filter<'core> + 'core>>, Error> {
139        let raw = raw_params(
140            strength,
141            variant,
142            preset,
143            prefilter,
144            channel_mode,
145            luma_strength,
146            chroma_strength,
147            None,
148            None,
149            None,
150            None,
151            device,
152            accelerators,
153            search_radius,
154            patch_radius,
155            temporal_radius,
156            sigma,
157            sigma_scale,
158            motion_compensation,
159            None,
160            None,
161            None,
162            None,
163        )?;
164        let filter = Denoise::create(api, core, clip, AlgorithmKind::Nlmeans, &raw)?;
165        Ok(Some(Box::new(filter)))
166    }
167}
168
169make_filter_function! {
170    Nl4dFunction, "NL4D"
171
172    /// Estimates its automatic noise level fresh from each frame's own
173    /// temporal window, rather than smoothing it across the whole
174    /// stream, so a frame denoises to the same pixels no matter what
175    /// order VapourSynth requests frames in. Passing `sigma` pins the
176    /// noise level and skips that estimator entirely.
177    ///
178    /// The first few frames of a clip may differ slightly from the CLI's
179    /// output for the same parameters. The plugin fills a clip's
180    /// leading edge by repeating its first frame across the whole
181    /// temporal window, while the CLI's streaming mode primes a
182    /// narrower repeat before real frames start arriving. The
183    /// difference is bounded, small, and confined to a clip's first
184    /// `2 * temporal_radius` frames.
185    #[expect(clippy::too_many_arguments)]
186    fn create_nl4d<'core>(
187        api: API,
188        core: CoreRef<'core>,
189        clip: Node<'core>,
190        preset: Option<&[u8]>,
191        channel_mode: Option<&[u8]>,
192        luma_strength: Option<f64>,
193        chroma_strength: Option<f64>,
194        luma_lambda_ht: Option<f64>,
195        chroma_lambda_ht: Option<f64>,
196        luma_mismatch_scale: Option<f64>,
197        chroma_mismatch_scale: Option<f64>,
198        device: Option<&[u8]>,
199        accelerators: Option<&[u8]>,
200        temporal_radius: Option<i64>,
201        sigma: Option<f64>,
202        sigma_scale: Option<f64>,
203        lambda_ht: Option<f64>,
204        lambda_ht_scale: Option<f64>,
205        spatial_radius: Option<i64>,
206        refine: Option<i64>,
207    ) -> Result<Option<Box<dyn Filter<'core> + 'core>>, Error> {
208        let raw = raw_params(
209            None,
210            None,
211            preset,
212            None,
213            channel_mode,
214            luma_strength,
215            chroma_strength,
216            luma_lambda_ht,
217            chroma_lambda_ht,
218            luma_mismatch_scale,
219            chroma_mismatch_scale,
220            device,
221            accelerators,
222            None,
223            None,
224            temporal_radius,
225            sigma,
226            sigma_scale,
227            None,
228            lambda_ht,
229            lambda_ht_scale,
230            spatial_radius,
231            refine,
232        )?;
233        let filter = Denoise::create(api, core, clip, AlgorithmKind::Nl4d, &raw)?;
234        Ok(Some(Box::new(filter)))
235    }
236}
237
238export_vapoursynth_plugin! {
239    Metadata {
240        identifier: "com.chillfish8.avdenoise",
241        namespace: "avd",
242        name: "av-denoise",
243        read_only: true,
244    },
245    [NlmeansFunction::new(), Nl4dFunction::new()]
246}