Skip to main content

dspk_ops/resampler/kernels/
poly.rs

1use cubecl::prelude::*;
2use dspk_core::compute::{channel_position, element_position, sample_position};
3
4use crate::core::kernels::store_lanes;
5use crate::core::read_extended;
6use crate::core::edge::EDGE_ZEROS;
7
8/// `output[c, m] = input[c, phase + m · step]` (keep every `step`-th sample).
9#[cube(launch)]
10pub fn downsample_kernel<F: Float>(
11    input: &[F],
12    output: &mut [F],
13    num_channels: u32,
14    in_len: u32,
15    out_len: u32,
16    step: u32,
17    phase: u32,
18) {
19    let m = sample_position();
20    let ch = channel_position();
21    if m < out_len && ch < num_channels {
22        output[(ch * out_len + m) as usize] = input[(ch * in_len + phase + m * step) as usize];
23    }
24}
25
26/// Polyphase FIR resampling (`upfirdn` restricted to the outputs `resample_poly` keeps): output `m`
27/// is `Σ_j taps[j] · u[i₀ − j]` with `i₀ = m · down + start`, where `u` is the input up-sampled by
28/// `up` (`u[i] = x[i / up]` when `up` divides `i`, else 0). Only taps aligned with input samples are
29/// visited (`j ≡ i₀ mod up`). Input positions outside the row come from `edge` (an `EdgeMode` id;
30/// `resample_poly` pads with zeros); `ext_pad` is how far outside they can reach.
31#[cube(launch)]
32#[allow(clippy::too_many_arguments)]
33pub fn upfirdn_kernel<F: Float>(
34    input: &[F],
35    taps: &[F],
36    output: &mut [F],
37    num_channels: u32,
38    in_len: u32,
39    out_len: u32,
40    num_taps: u32,
41    up: u32,
42    down: u32,
43    start: i32,
44    ext_pad: u32,
45    #[comptime] edge: u32,
46) {
47    let m = sample_position();
48    let ch = channel_position();
49    if m < out_len && ch < num_channels {
50        let base = (ch * in_len) as usize;
51        let up_i = i32::cast_from(up);
52        let i0 = i32::cast_from(m) * i32::cast_from(down) + start;
53        let phase = ((i0 % up_i) + up_i) % up_i;
54        let n_taps = i32::cast_from(num_taps);
55        let len = i32::cast_from(in_len);
56        let mut acc = F::new(0.0f32);
57        // Tap `j = phase + k · up` reads input `s = (i0 − j) / up = s0 − k`: one exact division
58        // (`i0 − phase` is a multiple of `up`), then a decrement per tap
59        let s0 = (i0 - phase) / up_i;
60        let visits = if phase < n_taps { (n_taps - phase + up_i - 1) / up_i } else { 0i32 };
61        let mut j = phase;
62        let mut s = s0;
63        if s0 < len && s0 - visits + 1 >= 0i32 {
64            // The whole window lies inside the row (nearly every output): no bounds checks
65            let mut k = 0i32;
66            while k < visits {
67                acc += taps[u32::cast_from(j) as usize] * input[base + u32::cast_from(s) as usize];
68                j += up_i;
69                s -= 1i32;
70                k += 1i32;
71            }
72        } else {
73            while j < n_taps {
74                if s >= 0i32 && s < len {
75                    acc += taps[u32::cast_from(j) as usize] * input[base + u32::cast_from(s) as usize];
76                } else if comptime!(edge != EDGE_ZEROS) {
77                    let ext = u32::cast_from(s + i32::cast_from(ext_pad));
78                    acc += taps[u32::cast_from(j) as usize] * read_extended::<F>(input, base, in_len, ext_pad, ext, edge);
79                }
80                j += up_i;
81                s -= 1i32;
82            }
83        }
84        output[(ch * out_len + m) as usize] = acc;
85    }
86}
87
88/// `⌊a / b⌋` for `b > 0` (integer division truncates toward zero).
89#[cube]
90fn floor_div(a: i32, b: i32) -> i32 {
91    let q = a / b;
92    if a % b != 0i32 && a < 0i32 { q - 1i32 } else { q }
93}
94
95/// Outputs `m … m + N − 1` of the row at `input[base]`. Output `m + l` meets input sample `s` at
96/// tap `(m + l) · down + start − s · up`; `table[d + reach]` holds in lane `l` the tap
97/// `d + l · down` (zero outside the filter), `reach = (N − 1) · down`, so every load is aligned.
98/// Input samples are visited newest first, so each lane adds its taps in the scalar kernel's order
99/// (plus exact zeros): outputs are bit-identical to it. Lanes past the row's end stay in bounds
100/// (zeros or the edge extension).
101#[cube]
102#[allow(clippy::too_many_arguments)]
103fn upfirdn_vector_at<F: Float, N: Size>(
104    input: &[F],
105    table: &[Vector<F, N>],
106    base: usize,
107    m: u32,
108    in_len: u32,
109    num_taps: u32,
110    up: u32,
111    down: u32,
112    start: i32,
113    ext_pad: u32,
114    #[comptime] edge: u32,
115) -> Vector<F, N> {
116    let n = N::value() as u32;
117    let up_i = i32::cast_from(up);
118    let down_i = i32::cast_from(down);
119    let len = i32::cast_from(in_len);
120    let i0 = i32::cast_from(m) * down_i + start;
121    let reach = i32::cast_from(n - 1u32) * down_i;
122    // Inputs some lane meets: s · up in [i0 − (K − 1), i0 + reach]
123    let s_hi = floor_div(i0 + reach, up_i);
124    let s_lo = 0i32 - floor_div(i32::cast_from(num_taps) - 1i32 - i0, up_i);
125    let mut acc = Vector::<F, N>::new(F::new(0.0f32));
126    let mut s = s_hi;
127    if s_lo >= 0i32 && s_hi < len {
128        while s >= s_lo {
129            let d = i0 - s * up_i + reach;
130            acc += table[u32::cast_from(d) as usize] * Vector::new(input[base + u32::cast_from(s) as usize]);
131            s -= 1i32;
132        }
133    } else {
134        while s >= s_lo {
135            let d = i0 - s * up_i + reach;
136            if s >= 0i32 && s < len {
137                acc += table[u32::cast_from(d) as usize] * Vector::new(input[base + u32::cast_from(s) as usize]);
138            } else if comptime!(edge != EDGE_ZEROS) {
139                let ext = u32::cast_from(s + i32::cast_from(ext_pad));
140                acc += table[u32::cast_from(d) as usize] * Vector::new(read_extended::<F>(input, base, in_len, ext_pad, ext, edge));
141            }
142            s -= 1i32;
143        }
144    }
145    acc
146}
147
148/// [`fn@upfirdn_kernel`] with `N` consecutive outputs per unit (`out_len` a multiple of `N`),
149/// computed by [`fn@upfirdn_vector_at`].
150#[cube(launch)]
151#[allow(clippy::too_many_arguments)]
152pub fn upfirdn_vector_kernel<F: Float, N: Size>(
153    input: &[F],
154    table: &[Vector<F, N>],
155    output: &mut [Vector<F, N>],
156    in_len: u32,
157    out_len: u32,
158    num_taps: u32,
159    up: u32,
160    down: u32,
161    start: i32,
162    ext_pad: u32,
163    #[comptime] edge: u32,
164) {
165    let v = element_position();
166    if v < output.len() {
167        let n = N::value() as u32;
168        let per_row = out_len / n;
169        let m = (v as u32 % per_row) * n;
170        let base = ((v as u32 / per_row) * in_len) as usize;
171        output[v] = upfirdn_vector_at::<F, N>(input, table, base, m, in_len, num_taps, up, down, start, ext_pad, edge);
172    }
173}
174
175/// [`fn@upfirdn_vector_kernel`] for any `out_len`: `units_per_row = ceil(out_len / N)` units per
176/// row, outputs stored lane by lane ([`fn@store_lanes`]).
177#[cube(launch)]
178#[allow(clippy::too_many_arguments)]
179pub fn upfirdn_vector_unaligned_kernel<F: Float, N: Size>(
180    input: &[F],
181    table: &[Vector<F, N>],
182    output: &mut [F],
183    in_len: u32,
184    out_len: u32,
185    num_taps: u32,
186    up: u32,
187    down: u32,
188    start: i32,
189    ext_pad: u32,
190    units_per_row: u32,
191    num_units: u32,
192    #[comptime] edge: u32,
193) {
194    let v = element_position() as u32;
195    if v < num_units {
196        let n = N::value() as u32;
197        let row = v / units_per_row;
198        let m = (v % units_per_row) * n;
199        let acc = upfirdn_vector_at::<F, N>(input, table, (row * in_len) as usize, m, in_len, num_taps, up, down, start, ext_pad, edge);
200        store_lanes::<F, N>(output, (row * out_len + m) as usize, acc, u32::min(n, out_len - m));
201    }
202}