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