Source browser

examples/signal_filters_checks.hpp

Read the complete file below or download its original bytes. Line numbers provide stable links to a declaration or processing step.

File fingerprint
1/** @file signal_filters_checks.hpp
2 * @brief Independent signal-filter checks for CPU and GPU execution.
3 * Double convolution and direct-form I recurrence provide output oracles for
4 * shaders using float convolution and transposed direct-form II. Frequency
5 * response checks also measure mixed tones after the initial transient.
6 */
7#ifndef SHDR_EXAMPLES_SIGNAL_FILTERS_CHECKS_HPP // Include this example helper only once.
8#define SHDR_EXAMPLES_SIGNAL_FILTERS_CHECKS_HPP // Remember that this example helper is available.
9#include "runner.hpp" // Select CPU execution or the requested graphics profile.
10#include "signal_filters.hpp" // Use the reusable filter designers and pipeline helpers.
11#include <complex> // Evaluate frequency responses with complex arithmetic.
12#include <random> // Create repeatable random fixtures and initial parameters.
13
14namespace shdr_examples { // Keep these example helpers in their own namespace.
15namespace signal_checks { // Keep these example helpers in their own namespace.
16
17inline void require(bool value, const char* message) { // Keep the test checks active in every build configuration.
18    if (!value) // Handle this validation or boundary case explicitly.
19        throw shdr::error(message); // Report the invalid design or failed check clearly.
20} // Finish the require operation.
21template <class Function> void rejects(Function function, const char* message) { // Accept the test function while keeping error checks reusable.
22    bool caught = false; // Remember whether the expected validation error occurs.
23    try { // Run the operation while preserving its failure diagnostic.
24        function(); // Execute the operation whose rejection is being checked.
25    } catch (const shdr::error&) { // Recognize the expected filter validation error.
26        caught = true; // Record that the invalid input was rejected.
27    } // Finish handling this reported failure.
28    require(caught, message); // Require this design property or test invariant.
29} // Finish the rejects operation.
30inline std::complex<double> unit_phase(double frequency, double rate) { // Compute one complex sample of the frequency phase.
31    return std::polar(1.0, -2.0 * signal_detail::pi() * frequency / rate); // Return the unit complex phase for this frequency.
32} // Finish the unit_phase operation.
33inline double fir_gain(const std::vector<float>& coefficients, double frequency, double rate) { // Evaluate FIR gain using complex frequency response.
34    std::complex<double> gain(0, 0), phase(1, 0), step = unit_phase(frequency, rate); // Keep the complex phase and accumulated transfer response.
35    for (float coefficient : coefficients) { // Visit every value needed by this calculation.
36        gain += double(coefficient) * phase; // Add this tap at its complex frequency phase.
37        phase *= step; // Advance to the next tap delay.
38    } // Finish this channel, sample or feature pass.
39    return std::abs(gain); // Return the transfer-function magnitude.
40} // Finish the fir_gain operation.
41inline double biquad_gain(const std::array<float, 5>& coefficients, double frequency, double rate) { // Evaluate the normalized biquad transfer function.
42    const std::complex<double> phase = unit_phase(frequency, rate); // Keep the complex phase and accumulated transfer response.
43    return std::abs( // Return the transfer-function magnitude.
44        (double(coefficients[0]) + double(coefficients[1]) * phase + // Evaluate the feed-forward numerator at this frequency.
45         double(coefficients[2]) * phase * phase) / // Include the second delayed feed-forward coefficient.
46        (1.0 + double(coefficients[3]) * phase + double(coefficients[4]) * phase * phase)); // Divide by the normalized feedback denominator.
47} // Finish the biquad_gain operation.
48inline std::vector<float> fixture(unsigned count, unsigned channels) { // Build separate impulse, step and random test channels.
49    std::vector<float> input(count * channels, 0.0f); // Allocate zero-filled, independent fixture channels.
50    std::mt19937 random(12345u); // Use a fixed seed so every backend receives identical data.
51    std::uniform_real_distribution<float> distribution(-1.0f, 1.0f); // Choose a bounded range for reproducible noise or weights.
52    input[0] = 1.0f; // Put the impulse only in the first channel.
53    for (unsigned i = 0; i < count; ++i) { // Visit every value needed by this calculation.
54        if (channels > 1u) // Keep the step fixture separate from the impulse channel.
55            input[count + i] = 1.0f; // Use a unit step for the second channel.
56        for (unsigned channel = 2u; channel < channels; ++channel) // Process each channel independently.
57            input[channel * count + i] = distribution(random); // Give the remaining channels reproducible random samples.
58    } // Finish this channel, sample or feature pass.
59    return input; // Return the complete channel-major input fixture.
60} // Finish the fixture operation.
61inline std::vector<double> fir_reference(const std::vector<float>& input, // Convolve with double accumulation for the independent oracle.
62                                         unsigned count, // Receive the validated sample, channel or coefficient count.
63                                         unsigned channels, // Receive the validated sample, channel or coefficient count.
64                                         const std::vector<float>& coefficients) { // Use the caller-provided finite FIR coefficients.
65    std::vector<double> output(input.size(), 0.0); // Allocate independent double oracle outputs.
66    for (unsigned channel = 0; channel < channels; ++channel) // Process each channel independently.
67        for (unsigned i = 0; i < count; ++i) // Visit every value needed by this calculation.
68            for (unsigned tap = 0; tap < coefficients.size() && tap <= i; ++tap) // Use only causal taps that lie inside this batch.
69                output[channel * count + i] += // Accumulate this causal tap in double precision.
70                    double(coefficients[tap]) * input[channel * count + i - tap]; // Multiply this causal sample and coefficient in double precision.
71    return output; // Return every independently computed filtered sample.
72} // Finish the fir_reference operation.
73inline std::vector<double> biquad_reference(const std::vector<float>& input, // Use a direct-form I double recurrence as an independent oracle.
74                                            unsigned count, // Receive the validated sample, channel or coefficient count.
75                                            unsigned channels, // Receive the validated sample, channel or coefficient count.
76                                            const std::array<float, 5>& coefficients, // Use five normalized biquad coefficients.
77                                            std::vector<shdr::glsl::vec2>* final_state = 0) { // Keep the independent expected recurrence pair for each channel.
78    std::vector<double> output(input.size()); // Allocate independent double oracle outputs.
79    if (final_state) // Record reference state only when the caller asks for it.
80        final_state->resize(channels); // Reserve one final recurrence pair per channel.
81    for (unsigned channel = 0; channel < channels; ++channel) { // Process each channel independently.
82        double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; // Start the independent direct-form I recurrence with zero history.
83        for (unsigned i = 0; i < count; ++i) { // Visit every value needed by this calculation.
84            const double x = input[channel * count + i]; // Read this input sample for the double recurrence.
85            const double y = coefficients[0] * x + coefficients[1] * x1 + coefficients[2] * x2 - // Apply the independent direct-form I difference equation.
86                             coefficients[3] * y1 - coefficients[4] * y2; // Subtract the two normalized feedback contributions.
87            output[channel * count + i] = y; // Keep this independently computed filtered sample.
88            x2 = x1; // Shift the input history by one sample.
89            x1 = x; // Remember the current input for the next sample.
90            y2 = y1; // Shift the output history by one sample.
91            y1 = y; // Remember the current output for the next sample.
92        } // Finish this channel, sample or feature pass.
93        if (final_state) // Record reference state only when the caller asks for it.
94            (*final_state)[channel] = // Convert direct-form I history into transposed recurrence state.
95                shdr::glsl::vec2(float(coefficients[1] * x1 + coefficients[2] * x2 - // Convert the last input and output histories into the first state value.
96                                       coefficients[3] * y1 - coefficients[4] * y2), // Include the feedback history in the first state value.
97                                 float(coefficients[2] * x1 - coefficients[4] * y1)); // Convert the double reference value for the float comparison.
98    } // Finish this channel, sample or feature pass.
99    return output; // Return every independently computed filtered sample.
100} // Finish the biquad_reference operation.
101inline void compare(const shdr::buffer<float>& actual, // Check every filtered output against the double oracle.
102                    const std::vector<double>& expected, // Receive the independently accumulated double outputs.
103                    const char* label, // Give failures a descriptive check name.
104                    float tolerance = 0.00003f) { // Allow only the stated float error in the result check.
105    const float* output = actual.data(); // Read the resource after any required GPU result synchronization.
106    for (unsigned i = 0; i < expected.size(); ++i) // Visit every value needed by this calculation.
107        require_close(output[i], static_cast<float>(expected[i]), label, tolerance); // Compare this result with its independent expected value.
108} // Finish the compare operation.
109inline double tone_amplitude(const shdr::buffer<float>& samples, // Measure a tone by sine and cosine projection.
110                             unsigned begin, // Begin assembling at the first sample in each channel.
111                             unsigned count, // Receive the validated sample, channel or coefficient count.
112                             double frequency, // Receive the tone, cutoff or center frequency.
113                             double rate) { // Receive the positive sampling rate in frequency units.
114    const float* values = samples.data(); // Read the resource after any required GPU result synchronization.
115    double real = 0.0, imaginary = 0.0; // Start both tone projection accumulators at zero.
116    for (unsigned i = 0; i < count; ++i) { // Visit every value needed by this calculation.
117        const double angle = 2.0 * signal_detail::pi() * frequency * (begin + i) / rate; // Compute the absolute phase of this tone sample.
118        real += values[begin + i] * std::cos(angle); // Accumulate the cosine projection of the filtered signal.
119        imaginary += values[begin + i] * std::sin(angle); // Accumulate the sine projection of the filtered signal.
120    } // Finish this channel, sample or feature pass.
121    return 2.0 * std::sqrt(real * real + imaginary * imaginary) / count; // Build separate impulse, step and random test channels.
122} // Finish the tone_amplitude operation.
123
124inline void coefficient_checks() { // Check designer gains and invalid parameter handling.
125    const filter_mode modes[] = {filter_mode::low_pass, // List every supported filter shape exercised by the checks.
126                                 filter_mode::high_pass, // Include the high-pass response in this group of checks.
127                                 filter_mode::band_pass, // Include the band-pass response in this group of checks.
128                                 filter_mode::band_stop}; // Include the band-stop response in this group of checks.
129    for (filter_mode mode : modes) { // Check every supported filter shape.
130        const bool band = mode == filter_mode::band_pass || mode == filter_mode::band_stop; // Use two cutoff edges only for band-selective filters.
131        const std::vector<float> fir = // Design the FIR coefficients used by this independent check.
132            fir_coefficients(mode, 48000.0, band ? 3000.0 : 4000.0, band ? 9000.0 : 0.0, 65u); // Design the causal FIR coefficients for this configuration.
133        for (unsigned i = 0; i < fir.size(); ++i) // Visit every value needed by this calculation.
134            require_close(fir[i], fir[fir.size() - 1u - i], "FIR symmetry", 0.0000001f); // Compare this result with its independent expected value.
135        const double dc = fir_gain(fir, 0.0, 48000.0); // Measure the FIR response at zero frequency.
136        require_close(float(dc), // Compare this result with its independent expected value.
137                      mode == filter_mode::low_pass || mode == filter_mode::band_stop ? 1.0f : 0.0f, // Expect unit DC gain for low-pass and band-stop designs.
138                      "FIR DC gain", // Identify this check clearly in any failure message.
139                      0.000002f); // Apply the declared float error allowance.
140        if (mode == filter_mode::low_pass) // Choose the coefficient rule for this filter shape.
141            require(fir_gain(fir, 1000, 48000) > .98 && fir_gain(fir, 12000, 48000) < .015, // Require this design property or test invariant.
142                    "low-pass FIR selectivity"); // Identify this check clearly in any failure message.
143        if (mode == filter_mode::high_pass) // Choose the coefficient rule for this filter shape.
144            require(fir_gain(fir, 1000, 48000) < .015 && fir_gain(fir, 12000, 48000) > .98, // Require this design property or test invariant.
145                    "high-pass FIR selectivity"); // Identify this check clearly in any failure message.
146        if (mode == filter_mode::band_pass) // Choose the coefficient rule for this filter shape.
147            require(fir_gain(fir, 6000, 48000) > .99 && fir_gain(fir, 1000, 48000) < .015 && // Require this design property or test invariant.
148                        fir_gain(fir, 16000, 48000) < .015, // Require attenuation above the band-pass upper edge.
149                    "band-pass FIR selectivity"); // Identify this check clearly in any failure message.
150        if (mode == filter_mode::band_stop) // Choose the coefficient rule for this filter shape.
151            require(fir_gain(fir, 6000, 48000) < .000002 && fir_gain(fir, 1000, 48000) > .98 && // Require this design property or test invariant.
152                        fir_gain(fir, 16000, 48000) > .98, // Require gain above the rejected band-stop interval.
153                    "band-stop FIR selectivity"); // Identify this check clearly in any failure message.
154        const std::array<float, 5> biquad = biquad_coefficients(mode, 48000.0, 6000.0); // Design normalized coefficients for this biquad check.
155        if (mode == filter_mode::low_pass) // Choose the coefficient rule for this filter shape.
156            require_close( // Compare this result with its independent expected value.
157                float(biquad_gain(biquad, 0, 48000)), 1.0f, "biquad low-pass DC", 0.000002f); // Convert the double reference value for the float comparison.
158        if (mode == filter_mode::high_pass) // Choose the coefficient rule for this filter shape.
159            require_close(float(biquad_gain(biquad, 24000, 48000)), // Compare this result with its independent expected value.
160                          1.0f, // Expect unit gain at this normalization frequency.
161                          "biquad high-pass Nyquist", // Identify this check clearly in any failure message.
162                          0.000002f); // Apply the declared float error allowance.
163        if (mode == filter_mode::band_pass) // Choose the coefficient rule for this filter shape.
164            require_close(float(biquad_gain(biquad, 6000, 48000)), // Compare this result with its independent expected value.
165                          1.0f, // Expect unit gain at this normalization frequency.
166                          "biquad band-pass center", // Identify this check clearly in any failure message.
167                          0.000002f); // Apply the declared float error allowance.
168        if (mode == filter_mode::band_stop) // Choose the coefficient rule for this filter shape.
169            require(biquad_gain(biquad, 6000, 48000) < .000002, "biquad notch center"); // Require this design property or test invariant.
170    } // Finish this channel, sample or feature pass.
171    const std::vector<float> average = // Design the FIR coefficients used by this independent check.
172        fir_coefficients(filter_mode::moving_average, 48000, 0, 0, 5); // Design the five equal weights used by the average check.
173    for (float value : average) // Inspect each stored float coefficient.
174        require_close(value, 0.2f, "moving-average coefficients", 0.0f); // Compare this result with its independent expected value.
175    rejects( // Confirm that invalid configuration is rejected before dispatch.
176        [] { // Define one invalid configuration to test.
177            fir_coefficients(filter_mode::low_pass, 48000, 4000, 0, 4); // Try an even tap count that the designer must reject.
178        }, // Finish the invalid-configuration test operation.
179        "even FIR taps accepted"); // Identify this check clearly in any failure message.
180    rejects( // Confirm that invalid configuration is rejected before dispatch.
181        [] { // Define one invalid configuration to test.
182            fir_coefficients(filter_mode::low_pass, 0, 4000); // Try an invalid zero sample rate.
183        }, // Finish the invalid-configuration test operation.
184        "zero sample rate accepted"); // Identify this check clearly in any failure message.
185    rejects( // Confirm that invalid configuration is rejected before dispatch.
186        [] { // Define one invalid configuration to test.
187            fir_coefficients(filter_mode::low_pass, 48000, 24000); // Try a cutoff at Nyquist, outside the supported open interval.
188        }, // Finish the invalid-configuration test operation.
189        "Nyquist cutoff accepted"); // Identify this check clearly in any failure message.
190    rejects( // Confirm that invalid configuration is rejected before dispatch.
191        [] { // Define one invalid configuration to test.
192            fir_coefficients(filter_mode::band_pass, 48000, 9000, 3000); // Try band edges in the wrong order.
193        }, // Finish the invalid-configuration test operation.
194        "descending band edges accepted"); // Identify this check clearly in any failure message.
195    rejects( // Confirm that invalid configuration is rejected before dispatch.
196        [] { // Define one invalid configuration to test.
197            biquad_coefficients(filter_mode::low_pass, 48000, 4000, 0); // Exercise the positive-quality validation with a zero value.
198        }, // Finish the invalid-configuration test operation.
199        "zero Q accepted"); // Identify this check clearly in any failure message.
200    rejects( // Confirm that invalid configuration is rejected before dispatch.
201        [] { // Define one invalid configuration to test.
202            biquad_coefficients(filter_mode::low_pass, 48000, 1e-20); // Try a frequency whose rounded feedback loses strict stability.
203        }, // Finish the invalid-configuration test operation.
204        "unstable rounded biquad accepted"); // Identify this check clearly in any failure message.
205} // Finish the coefficient_checks operation.
206
207inline void fir_outputs(runner& run) { // Check causal FIR outputs and measured mixed-tone gains.
208    const unsigned count = 257, channels = 3; // Choose a non-workgroup-aligned sample count and separate channels.
209    const std::vector<float> source = fixture(count, channels); // Keep the shared impulse, step and seeded random inputs.
210    shdr::buffer<float> input(source.size()), output(source.size()); // Allocate independent float resources for this processing check.
211    std::copy(source.begin(), source.end(), input.data()); // Copy these host values into the shared resource.
212    const filter_mode modes[] = {filter_mode::low_pass, // List every supported filter shape exercised by the checks.
213                                 filter_mode::high_pass, // Include the high-pass response in this group of checks.
214                                 filter_mode::band_pass, // Include the band-pass response in this group of checks.
215                                 filter_mode::band_stop, // Include the band-stop response in this group of checks.
216                                 filter_mode::moving_average}; // Include equal-weight moving-average filtering.
217    for (filter_mode mode : modes) { // Check every supported filter shape.
218        const bool band = mode == filter_mode::band_pass || mode == filter_mode::band_stop; // Use two cutoff edges only for band-selective filters.
219        const std::vector<float> coefficients = // Design the FIR coefficients used by this independent check.
220            fir_coefficients(mode, 48000, band ? 3000 : 4000, band ? 9000 : 0, 65); // Design the causal FIR coefficients for this configuration.
221        fir_pipeline filter(input, output, count, channels, coefficients); // Connect the FIR check to its designed coefficients.
222        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
223        compare(output, // Compare every filtered sample with the independent oracle.
224                fir_reference(source, count, channels, coefficients), // Supply the independently accumulated causal convolution.
225                "FIR impulse/step/random oracle"); // Identify this check clearly in any failure message.
226        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
227        compare( // Compare every filtered sample with the independent oracle.
228            output, fir_reference(source, count, channels, coefficients), "FIR batch prefix reset"); // Verify that another FIR run starts with a fresh zero prefix.
229        rejects( // Confirm that invalid configuration is rejected before dispatch.
230            [&] { // Use the existing resources to test invalid rebinding.
231                filter.bind(input, input, count); // Try unsafe input and output storage aliasing.
232            }, // Finish the invalid-configuration test operation.
233            "FIR aliased storage accepted"); // Identify this check clearly in any failure message.
234        rejects( // Confirm that invalid configuration is rejected before dispatch.
235            [&] { // Use the existing resources to test invalid rebinding.
236                filter.bind(input, output, count + 1u); // Try a count that exceeds the allocated channel capacity.
237            }, // Finish the invalid-configuration test operation.
238            "undersized FIR storage accepted"); // Identify this check clearly in any failure message.
239    } // Finish this channel, sample or feature pass.
240    const unsigned tone_count = 5312; // Reserve a steady-state tone window after the filter transient.
241    shdr::buffer<float> tones(tone_count), filtered(tone_count); // Allocate independent float resources for this processing check.
242    const double frequencies[3] = {1000, 6000, 12000}, amplitudes[3] = {.6, .4, .3}; // Mix separated passband and stopband tones with known amplitudes.
243    for (unsigned i = 0; i < tone_count; ++i) { // Visit each temporal sample in the tone mixture.
244        double value = 0.0; // Start this sample or projected-feature accumulator at zero.
245        for (unsigned tone = 0; tone < 3; ++tone) // Visit each frequency in the mixed-tone fixture.
246            value += amplitudes[tone] * // Add this sinusoidal component to the mixed-tone input.
247                     std::cos(2.0 * signal_detail::pi() * frequencies[tone] * i / 48000.0); // Compute this sinusoid at the shared sampling rate.
248        tones.data()[i] = static_cast<float>(value); // Store the same tone mixture for every backend.
249    } // Finish this channel, sample or feature pass.
250    for (filter_mode mode : modes) { // Check every supported filter shape.
251        const bool band = mode == filter_mode::band_pass || mode == filter_mode::band_stop; // Use two cutoff edges only for band-selective filters.
252        const std::vector<float> coefficients = // Design the FIR coefficients used by this independent check.
253            fir_coefficients(mode, 48000, band ? 3000 : 4000, band ? 9000 : 0, 65); // Design the causal FIR coefficients for this configuration.
254        fir_pipeline filter(tones, filtered, tone_count, 1, coefficients); // Connect the FIR check to its designed coefficients.
255        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
256        for (unsigned tone = 0; tone < 3; ++tone) // Visit each frequency in the mixed-tone fixture.
257            require_close( // Compare this result with its independent expected value.
258                float(tone_amplitude(filtered, 512, 4800, frequencies[tone], 48000)), // Convert the double reference value for the float comparison.
259                float(amplitudes[tone] * fir_gain(coefficients, frequencies[tone], 48000)), // Convert the double reference value for the float comparison.
260                "FIR mixed-tone gain", // Identify this check clearly in any failure message.
261                0.00005f); // Apply the declared float error allowance.
262    } // Finish this channel, sample or feature pass.
263} // Finish the fir_outputs operation.
264
265inline void biquad_outputs(runner& run) { // Check biquad outputs, retained state and chunk boundaries.
266    const unsigned count = 257, channels = 3; // Choose a non-workgroup-aligned sample count and separate channels.
267    const std::vector<float> source = fixture(count, channels); // Keep the shared impulse, step and seeded random inputs.
268    shdr::buffer<float> input(source.size()), output(source.size()); // Allocate independent float resources for this processing check.
269    std::copy(source.begin(), source.end(), input.data()); // Copy these host values into the shared resource.
270    const filter_mode modes[] = {filter_mode::low_pass, // List every supported filter shape exercised by the checks.
271                                 filter_mode::high_pass, // Include the high-pass response in this group of checks.
272                                 filter_mode::band_pass, // Include the band-pass response in this group of checks.
273                                 filter_mode::band_stop}; // Include the band-stop response in this group of checks.
274    for (filter_mode mode : modes) { // Check every supported filter shape.
275        const std::array<float, 5> coefficients = biquad_coefficients(mode, 48000, 6000); // Design normalized coefficients for this biquad check.
276        std::vector<shdr::glsl::vec2> expected_state; // Keep one pair of recurrence values for each channel.
277        const std::vector<double> expected = // Compute the independent full-block double output oracle.
278            biquad_reference(source, count, channels, coefficients, &expected_state); // Compute the independent recurrence output and final state.
279        biquad_pipeline filter(input, output, count, channels, coefficients); // Connect the biquad check with fresh zero history.
280        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
281        compare(output, expected, "biquad direct-form I oracle"); // Compare every filtered sample with the independent oracle.
282        const shdr::glsl::vec2* state = // Read the resource after any required GPU result synchronization.
283            static_cast<const shdr::buffer<shdr::glsl::vec2>&>(filter.state).data(); // Read the retained state after any required GPU synchronization.
284        for (unsigned channel = 0; channel < channels; ++channel) // Process each channel independently.
285            for (unsigned component = 0; component < 2; ++component) // Visit every value needed by this calculation.
286                require_close(state[channel][component], // Compare this result with its independent expected value.
287                              expected_state[channel][component], // Supply the independently converted recurrence value.
288                              "biquad state oracle", // Identify this check clearly in any failure message.
289                              0.00003f); // Apply the declared float error allowance.
290        filter.reset_state(); // Start a fresh filter history for this independent comparison.
291        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
292        compare(output, expected, "biquad explicit state reset"); // Compare every filtered sample with the independent oracle.
293
294        shdr::buffer<float> chunk_input(channels), chunk_output(channels); // Allocate independent float resources for this processing check.
295        biquad_pipeline streaming(chunk_input, chunk_output, 1, channels, coefficients); // Keep one stateful filter instance across variable chunks.
296        const unsigned chunks[] = {1, 17, 64, 3, 129, 43}; // Use uneven chunks that exercise state across many boundaries.
297        unsigned begin = 0; // Begin assembling at the first sample in each channel.
298        std::vector<double> assembled(source.size()); // Collect streamed chunks in their full-signal positions.
299        unsigned chunk_index = 0; // Begin the CPU and GPU transition pattern at its first chunk.
300        for (unsigned chunk : chunks) { // Process the next variable-size temporal block.
301            chunk_input = shdr::buffer<float>(chunk * channels); // Allocate this chunk with its channel-major stride.
302            chunk_output = shdr::buffer<float>(chunk * channels); // Allocate this chunk with its channel-major stride.
303            for (unsigned channel = 0; channel < channels; ++channel) // Process each channel independently.
304                for (unsigned i = 0; i < chunk; ++i) // Process the next variable-size temporal block.
305                    chunk_input.data()[channel * chunk + i] = source[channel * count + begin + i]; // Copy this channel segment into the next temporal chunk.
306            streaming.bind(chunk_input, chunk_output, chunk); // Connect this temporal chunk while retaining previous channel state.
307            if (run.gpu() && chunk_index % 2u) // Use the requested GPU path or explicit CPU transition.
308                streaming.processing_pipeline().run(); // Process this chunk on the CPU using the same retained state.
309            else // Process this chunk through the selected runner backend.
310                run.run(streaming.processing_pipeline()); // Execute the linked stages on the selected backend.
311            const float* actual = static_cast<const shdr::buffer<float>&>(chunk_output).data(); // Read the resource after any required GPU result synchronization.
312            for (unsigned channel = 0; channel < channels; ++channel) // Process each channel independently.
313                for (unsigned i = 0; i < chunk; ++i) // Process the next variable-size temporal block.
314                    assembled[channel * count + begin + i] = actual[channel * chunk + i]; // Place the chunk output in the complete reference signal.
315            begin += chunk; // Advance to the next temporal chunk boundary.
316            ++chunk_index; // Alternate the CPU and GPU transition test on the next chunk.
317        } // Finish this channel, sample or feature pass.
318        require(begin == count, "stream fixture did not cover the entire signal"); // Require this design property or test invariant.
319        for (unsigned i = 0; i < assembled.size(); ++i) // Visit every value needed by this calculation.
320            require_close(float(assembled[i]), // Compare this result with its independent expected value.
321                          float(expected[i]), // Convert the computed host value to the shader float type.
322                          "biquad streaming chunk equivalence", // Identify this check clearly in any failure message.
323                          0.00003f); // Apply the declared float error allowance.
324    } // Finish this channel, sample or feature pass.
325    const unsigned tone_count = 5312; // Reserve a steady-state tone window after the filter transient.
326    shdr::buffer<float> tones(tone_count), filtered(tone_count); // Allocate independent float resources for this processing check.
327    const double frequencies[3] = {1000, 6000, 12000}, amplitudes[3] = {.6, .4, .3}; // Mix separated passband and stopband tones with known amplitudes.
328    for (unsigned i = 0; i < tone_count; ++i) { // Visit each temporal sample in the tone mixture.
329        double value = 0.0; // Start this sample or projected-feature accumulator at zero.
330        for (unsigned tone = 0; tone < 3; ++tone) // Visit each frequency in the mixed-tone fixture.
331            value += amplitudes[tone] * // Add this sinusoidal component to the mixed-tone input.
332                     std::cos(2.0 * signal_detail::pi() * frequencies[tone] * i / 48000.0); // Compute this sinusoid at the shared sampling rate.
333        tones.data()[i] = static_cast<float>(value); // Store the same tone mixture for every backend.
334    } // Finish this channel, sample or feature pass.
335    for (filter_mode mode : modes) { // Check every supported filter shape.
336        const std::array<float, 5> coefficients = biquad_coefficients(mode, 48000, 6000); // Design normalized coefficients for this biquad check.
337        biquad_pipeline filter(tones, filtered, tone_count, 1, coefficients); // Connect the biquad check with fresh zero history.
338        run.run(filter.processing_pipeline()); // Execute the linked stages on the selected backend.
339        for (unsigned tone = 0; tone < 3; ++tone) // Visit each frequency in the mixed-tone fixture.
340            require_close( // Compare this result with its independent expected value.
341                float(tone_amplitude(filtered, 512, 4800, frequencies[tone], 48000)), // Convert the double reference value for the float comparison.
342                float(amplitudes[tone] * biquad_gain(coefficients, frequencies[tone], 48000)), // Convert the double reference value for the float comparison.
343                "biquad mixed-tone gain", // Identify this check clearly in any failure message.
344                0.00005f); // Apply the declared float error allowance.
345    } // Finish this channel, sample or feature pass.
346} // Finish the biquad_outputs operation.
347} // namespace signal_checks
348
349/** @brief Check designed gains, causal outputs, independent channels and streamed state. */
350inline void check_signal_filters(runner& run) { // Run all filter design and processing checks.
351    signal_checks::coefficient_checks(); // Run the corresponding independent correctness check.
352    signal_checks::fir_outputs(run); // Run the corresponding independent correctness check.
353    signal_checks::biquad_outputs(run); // Run the corresponding independent correctness check.
354    std::cout // Print the verified result or the reason a check failed.
355        << "FIR low/high/band-pass/band-stop/average and stateful biquads: impulse, step, random and mixed tones verified\n"; // Complete the readable result message.
356} // Finish the check_signal_filters operation.
357} // namespace shdr_examples
358#endif // Finish the conditional compilation block.