Convolver1D.java

/*
 * Copyright (C) 2016 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.numerical.signal.processing;

/**
 * Convolves a 1D signal with a 1D kernel.
 */
public class Convolver1D {

    /**
     * Default constant value to use during convolution if CONSTANT_EDGE method
     * is being used for signal's edge extension.
     */
    public static final double DEFAULT_CONSTANT_VALUE = 0.0;

    /**
     * Default position of kernel center.
     */
    public static final int DEFAULT_KERNEL_CENTER = 0;

    /**
     * Default method to use for signal's edge extension.
     */
    public static final ConvolverEdgeMethod DEFAULT_EDGE_METHOD = ConvolverEdgeMethod.ZERO_EDGE;

    /**
     * Signal to be convolved.
     */
    private double[] signal;

    /**
     * Kernel to convolve the signal with.
     */
    private double[] kernel;

    /**
     * Position of kernel center. Typical values are the start of kernel
     * (position 0) or the kernel's center. For the former, the edge extension
     * is applied only at the end of the resulting signal; for the latter, edge
     * extension is applied symmetrically at the beginning and end of the
     * resulting signal.
     */
    private int kernelCenter = DEFAULT_KERNEL_CENTER;

    /**
     * Edge extension method to use during convolution when parts of the kernel
     * are required to lie outside the signal's boundaries.
     */
    private ConvolverEdgeMethod edgeMethod = DEFAULT_EDGE_METHOD;

    /**
     * Constant value to use during edge extension when CONSTANT_EDGE method is
     * being used.
     */
    private double constantValue = DEFAULT_CONSTANT_VALUE;

    /**
     * Listener in charge of attending events generated by this instance.
     */
    private Convolver1DListener listener;

    /**
     * Constructor.
     */
    public Convolver1D() {
    }

    /**
     * Constructor.
     *
     * @param signal signal to be convolved.
     * @param kernel kernel to convolve signal with.
     */
    public Convolver1D(final double[] signal, final double[] kernel) {
        setSignal(signal);
        setKernel(kernel);
    }

    /**
     * Gets signal to be convolved.
     *
     * @return signal to be convolved.
     */
    public double[] getSignal() {
        return signal;
    }

    /**
     * Sets signal to be convolved.
     *
     * @param signal signal to be convolved.
     */
    public final void setSignal(final double[] signal) {
        this.signal = signal;
    }

    /**
     * Gets kernel to convolve the signal with.
     *
     * @return kernel to convolve the signal with.
     */
    public double[] getKernel() {
        return kernel;
    }

    /**
     * Sets kernel to convolve the signal with.
     *
     * @param kernel kernel to convolve the signal with.
     */
    public final void setKernel(final double[] kernel) {
        this.kernel = kernel;
    }

    /**
     * Gets position of kernel center. Typical values are the start of kernel
     * (position 0) or the kernel's center. For the former, the edge extension
     * is applied only at the end of the resulting signal; for the latter, edge
     * extension is applied symmetrically at the beginning and end of the
     * resulting signal.
     *
     * @return position of kernel center.
     */
    public int getKernelCenter() {
        return kernelCenter;
    }

    /**
     * Sets position of kernel center. Typical values are the start of kernel
     * (position 0) or the kernel's center. For the former, the edge extension
     * is applied only at the end of the resulting signal; for the latter, edge
     * extension is applied symmetrically at the beginning and end of the
     * resulting signal.
     *
     * @param kernelCenter position of kernel center.
     * @throws IllegalArgumentException if provided position is negative.
     */
    public void setKernelCenter(final int kernelCenter) {
        if (kernelCenter < 0) {
            throw new IllegalArgumentException("kernel center must be zero or greater and within kernel's boundaries");
        }

        this.kernelCenter = kernelCenter;
    }

    /**
     * Gets edge extension method to use during convolution when parts of the
     * kernel are required to lie outside the signal's boundaries.
     *
     * @return edge extension method.
     */
    public ConvolverEdgeMethod getEdgeMethod() {
        return edgeMethod;
    }

    /**
     * Sets edge extension method to use during convolution when parts of the
     * kernel are required to lie outside the signal's boundaries.
     *
     * @param edgeMethod edge extension method.
     */
    public void setEdgeMethod(final ConvolverEdgeMethod edgeMethod) {
        this.edgeMethod = edgeMethod;
    }

    /**
     * Gets constant value to use during edge extension when CONSTANT_EDGE
     * method is being used.
     *
     * @return constant value to use during edge extension.
     */
    public double getConstantValue() {
        return constantValue;
    }

    /**
     * Sets constant value to use during edge extension when CONSTANT_EDGE
     * method is being used.
     *
     * @param constantValue constant value to use during edge extension.
     */
    public void setConstantValue(final double constantValue) {
        this.constantValue = constantValue;
    }

    /**
     * Gets listener in charge of attending events generated by this instance.
     *
     * @return listener in charge of attending events generated by this
     * instance.
     */
    public Convolver1DListener getListener() {
        return listener;
    }

    /**
     * Sets listener in charge of attending events generated by this instance.
     *
     * @param listener listener in charge of attending events generated by this
     *                 instance.
     */
    public void setListener(final Convolver1DListener listener) {
        this.listener = listener;
    }

    /**
     * Indicates whether this instance is ready to start a convolution.
     *
     * @return true if ready, false otherwise.
     */
    public boolean isReady() {
        return signal != null && kernel != null && kernelCenter >= 0 && kernelCenter < kernel.length;
    }

    /**
     * Convolves provided signal with provided kernel using provided
     * configuration for edge extension.
     *
     * @return a new array containing result of convolution.
     */
    public double[] convolve() {
        return convolve(signal, kernel, kernelCenter, edgeMethod, constantValue, listener);
    }

    /**
     * Convolves provided signal with provided kernel using provided
     * configuration for edge extension.
     *
     * @param result array where result of convolution will be stored. Must
     *               have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not have
     *                                  proper length.
     */
    public void convolve(final double[] result) {
        convolve(signal, kernel, kernelCenter, edgeMethod, constantValue, result, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method and kernel center located at its origin.
     *
     * @param signal signal to be convolved.
     * @param kernel kernel used for convolution.
     * @return a new array containing result of convolution.
     */
    public static double[] convolve(final double[] signal, final double[] kernel) {
        return convolve(signal, kernel, DEFAULT_KERNEL_CENTER, (Convolver1DListener) null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method and kernel center located at its origin.
     *
     * @param signal   signal to be convolved.
     * @param kernel   kernel used for convolution.
     * @param listener listener attending events generated during convolution.
     * @return a new array containing result of convolution.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final Convolver1DListener listener) {
        return convolve(signal, kernel, DEFAULT_KERNEL_CENTER, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method and kernel center located at its origin.
     *
     * @param signal signal to be convolved.
     * @param kernel kernel used for convolution.
     * @param result array where result of convolution will be stored. Must
     *               have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length.
     */
    public static void convolve(final double[] signal, final double[] kernel, final double[] result) {
        convolve(signal, kernel, DEFAULT_KERNEL_CENTER, result, null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method and kernel center located at its origin.
     *
     * @param signal   signal to be convolved.
     * @param kernel   kernel used for convolution.
     * @param result   array where result of convolution will be stored. Must
     *                 have the sum of the lengths of signal and kernel minus one.
     * @param listener listener attending events generated during convolution.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length.
     */
    public static void convolve(final double[] signal, final double[] kernel, final double[] result,
                                final Convolver1DListener listener) {
        convolve(signal, kernel, DEFAULT_KERNEL_CENTER, result, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter) {
        return convolve(signal, kernel, kernelCenter, (Convolver1DListener) null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param listener     listener attending events generated during convolution.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                    final Convolver1DListener listener) {
        return convolve(signal, kernel, kernelCenter, DEFAULT_EDGE_METHOD, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param result       array where result of convolution will be stored. Must
     *                     have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final double[] result) {
        convolve(signal, kernel, kernelCenter, result, null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value
     * extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param result       array where result of convolution will be stored. Must
     *                     have the sum of the lengths of signal and kernel minus one.
     * @param listener     listener attending events generated during convolution.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final double[] result, final Convolver1DListener listener) {
        convolve(signal, kernel, kernelCenter, DEFAULT_EDGE_METHOD, result, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value when
     * constant edge extension is used (which makes it equivalent to zero
     * extension method).
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param edgeMethod   edge extension method.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                    final ConvolverEdgeMethod edgeMethod) {
        return convolve(signal, kernel, kernelCenter, edgeMethod, (Convolver1DListener) null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value when
     * constant edge extension is used (which makes it equivalent to zero
     * extension method).
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param edgeMethod   edge extension method.
     * @param listener     listener attending events generated during convolution.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                    final ConvolverEdgeMethod edgeMethod, final Convolver1DListener listener) {
        return convolve(signal, kernel, kernelCenter, edgeMethod, DEFAULT_CONSTANT_VALUE, listener);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value when
     * constant edge extension is used (which makes it equivalent to zero
     * extension method).
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param edgeMethod   edge extension method.
     * @param result       array where result of convolution will be stored. Must
     *                     have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final ConvolverEdgeMethod edgeMethod, final double[] result) {
        convolve(signal, kernel, kernelCenter, edgeMethod, result, null);
    }

    /**
     * Convolves provided signal with provided kernel assuming a zero value when
     * constant edge extension is used (which makes it equivalent to zero
     * extension method).
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel used for convolution.
     * @param kernelCenter position of kernel center. This determines how
     *                     signal's edge extension is applied. Must be a value within kernel's
     *                     boundaries.
     * @param edgeMethod   edge extension method.
     * @param listener     listener attending events generated during convolution.
     * @param result       array where result of convolution will be stored. Must
     *                     have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final ConvolverEdgeMethod edgeMethod, final double[] result,
                                final Convolver1DListener listener) {
        convolve(signal, kernel, kernelCenter, edgeMethod, DEFAULT_CONSTANT_VALUE, result, listener);
    }

    /**
     * Convolves provided signal with provided kernel.
     *
     * @param signal        signal to be convolved.
     * @param kernel        kernel used for convolution.
     * @param kernelCenter  position of kernel center. This determines how
     *                      signal's edge extension is applied. Must be a value within kernel's
     *                      boundaries.
     * @param edgeMethod    edge extension method.
     * @param constantValue constant value to be used for edge extension if
     *                      constant extension method is used.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                    final ConvolverEdgeMethod edgeMethod, final double constantValue) {
        return convolve(signal, kernel, kernelCenter, edgeMethod, constantValue, (Convolver1DListener) null);
    }

    /**
     * Convolves provided signal with provided kernel.
     *
     * @param signal        signal to be convolved.
     * @param kernel        kernel used for convolution.
     * @param kernelCenter  position of kernel center. This determines how
     *                      signal's edge extension is applied. Must be a value within kernel's
     *                      boundaries.
     * @param edgeMethod    edge extension method.
     * @param constantValue constant value to be used for edge extension if
     *                      constant extension method is used.
     * @param listener      listener attending events generated during convolution.
     * @return a new array containing result of convolution.
     * @throws IllegalArgumentException if kernel center is not within kernel's
     *                                  boundaries.
     */
    public static double[] convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                    final ConvolverEdgeMethod edgeMethod, final double constantValue,
                                    final Convolver1DListener listener) {

        final var result = new double[signal.length + kernel.length - 1];
        convolve(signal, kernel, kernelCenter, edgeMethod, constantValue, result, listener);
        return result;
    }

    /**
     * Convolves provided signal with provided kernel.
     *
     * @param signal        signal to be convolved.
     * @param kernel        kernel used for convolution.
     * @param kernelCenter  position of kernel center. This determines how
     *                      signal's edge extension is applied. Must be a value within kernel's
     *                      boundaries.
     * @param edgeMethod    edge extension method.
     * @param constantValue constant value to be used for edge extension if
     *                      constant extension method is used.
     * @param result        array where result of convolution will be stored. Must
     *                      have the sum of the lengths of signal and kernel minus one.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final ConvolverEdgeMethod edgeMethod, final double constantValue,
                                final double[] result) {
        convolve(signal, kernel, kernelCenter, edgeMethod, constantValue, result, null);
    }

    /**
     * Convolves provided signal with provided kernel.
     *
     * @param signal        signal to be convolved.
     * @param kernel        kernel used for convolution.
     * @param kernelCenter  position of kernel center. This determines how
     *                      signal's edge extension is applied. Must be a value within kernel's
     *                      boundaries.
     * @param edgeMethod    edge extension method.
     * @param constantValue constant value to be used for edge extension if
     *                      constant extension method is used.
     * @param result        array where result of convolution will be stored. Must
     *                      have the sum of the lengths of signal and kernel minus one.
     * @param listener      listener attending events generated during convolution.
     * @throws IllegalArgumentException if provided result array does not
     *                                  have proper length or if kernel center is not within kernel's boundaries.
     */
    public static void convolve(final double[] signal, final double[] kernel, final int kernelCenter,
                                final ConvolverEdgeMethod edgeMethod, final double constantValue, final double[] result,
                                final Convolver1DListener listener) {
        if (kernelCenter < 0 || kernelCenter >= kernel.length) {
            throw new IllegalArgumentException("kernel center must be within kernel boundaries");
        }
        if (result.length != signal.length + kernel.length - 1) {
            throw new IllegalArgumentException(
                    " result array must have a length equal to the sum of signal and kernel lengths minus one");
        }

        switch (edgeMethod) {
            case MIRROR_EDGE:
                internalConvolveMirror(signal, kernel, kernelCenter, result, listener);
                break;
            case REPEAT_EDGE:
                internalConvolveRepeat(signal, kernel, kernelCenter, result, listener);
                break;
            case CONSTANT_EDGE:
                internalConvolveConstant(signal, kernel, kernelCenter, constantValue, result, listener);
                break;
            case ZERO_EDGE:
            default:
                internalConvolveZero(signal, kernel, kernelCenter, result, listener);
                break;
        }
    }

    /**
     * Internal method to convolve signal using zero edge extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel to convolve with.
     * @param kernelCenter kernel center.
     * @param result       array where result is stored.
     * @param listener     listener attending events generated during convolution.
     */
    private static void internalConvolveZero(
            final double[] signal, final double[] kernel, final int kernelCenter, final double[] result,
            final Convolver1DListener listener) {
        final var kernelLength = kernel.length;
        final var resultLength = result.length;
        double accum;
        int signalPos;

        if (listener != null) {
            listener.onStartConvolution();
        }

        for (var i = 0; i < resultLength; i++) {

            accum = 0.0;
            signalPos = i - kernelCenter;
            for (var j = 0; j < kernelLength; j++) {
                final var signalValue = getSignalValueZero(signal, signalPos - j);
                accum += signalValue * kernel[j];
            }

            result[i] = accum;

            if (listener != null) {
                final var progress = (float) i / (float) resultLength;
                listener.onConvolveProgressChange(progress);
            }
        }

        if (listener != null) {
            listener.onFinishConvolution();
        }
    }

    /**
     * Internal method to determine signal value even when a position outside
     * its boundaries is requested when zero edge extension is being used.
     *
     * @param signal signal to request a value for.
     * @param pos    requested position.
     * @return signal value when position is within boundaries or zero
     * otherwise.
     */
    protected static double getSignalValueZero(final double[] signal, final int pos) {
        if (pos >= 0 && pos < signal.length) {
            return signal[pos];
        } else {
            return 0.0;
        }
    }

    /**
     * Internal method to convolve signal using constant value edge extension
     * method.
     *
     * @param signal        signal to be convolved.
     * @param kernel        kernel to convolve with.
     * @param kernelCenter  kernel center.
     * @param constantValue constant value to use for positions outside signal
     *                      boundaries.
     * @param result        array where result is stored.
     * @param listener      listener attending events generated during convolution.
     */
    private static void internalConvolveConstant(
            final double[] signal, final double[] kernel, final int kernelCenter, final double constantValue,
            final double[] result, final Convolver1DListener listener) {
        final var kernelLength = kernel.length;
        final var resultLength = result.length;
        double accum;
        int signalPos;

        if (listener != null) {
            listener.onStartConvolution();
        }

        for (var i = 0; i < resultLength; i++) {

            accum = 0.0;
            signalPos = i - kernelCenter;
            for (var j = 0; j < kernelLength; j++) {
                final var signalValue = getSignalValueConstant(signal, signalPos - j, constantValue);
                accum += signalValue * kernel[j];
            }

            result[i] = accum;

            if (listener != null) {
                final var progress = (float) i / (float) resultLength;
                listener.onConvolveProgressChange(progress);
            }
        }

        if (listener != null) {
            listener.onFinishConvolution();
        }
    }

    /**
     * Internal method to determine signal value even when a position outside
     * its boundaries is requested for constant value edge extension.
     *
     * @param signal        signal to request a value for.
     * @param pos           requested position.
     * @param constantValue constant value to use when requesting a position
     *                      outside signal boundaries.
     * @return signal value when position is within boundaries or constant
     * value otherwise.
     */
    protected static double getSignalValueConstant(final double[] signal, final int pos, final double constantValue) {
        if (pos >= 0 && pos < signal.length) {
            return signal[pos];
        } else {
            return constantValue;
        }
    }

    /**
     * Internal method to convolve signal using a repeat extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel to convolve with.
     * @param kernelCenter kernel center.
     * @param result       array where result is stored.
     * @param listener     listener attending events generated during convolution.
     */
    private static void internalConvolveRepeat(
            final double[] signal, final double[] kernel, final int kernelCenter, final double[] result,
            final Convolver1DListener listener) {
        final var kernelLength = kernel.length;
        final var resultLength = result.length;
        double accum;
        int signalPos;

        if (listener != null) {
            listener.onStartConvolution();
        }

        for (var i = 0; i < resultLength; i++) {

            accum = 0.0;
            signalPos = i - kernelCenter;
            for (var j = 0; j < kernelLength; j++) {
                final var signalValue = getSignalValueRepeat(signal, signalPos - j);
                accum += signalValue * kernel[j];
            }

            result[i] = accum;

            if (listener != null) {
                final var progress = (float) i / (float) resultLength;
                listener.onConvolveProgressChange(progress);
            }
        }

        if (listener != null) {
            listener.onFinishConvolution();
        }
    }

    /**
     * Internal method to determine signal value even when a position outside
     * its boundaries is requested for repeat edge extension.
     *
     * @param signal signal to request a value for.
     * @param pos    requested position.
     * @return signal value assuming that signal is repeated indefinitely
     * outside its boundaries.
     */
    protected static double getSignalValueRepeat(final double[] signal, int pos) {
        final var signalLength = signal.length;
        pos = pos % signalLength;
        if (pos < 0) {
            pos += signalLength;
        }
        return signal[pos];
    }

    /**
     * Internal method to convolve signal using a mirror extension method.
     *
     * @param signal       signal to be convolved.
     * @param kernel       kernel to convolve with.
     * @param kernelCenter kernel center.
     * @param result       array where result is stored.
     * @param listener     listener attending events generated during convolution.
     */
    private static void internalConvolveMirror(
            final double[] signal, final double[] kernel, final int kernelCenter, final double[] result,
            final Convolver1DListener listener) {
        final var kernelLength = kernel.length;
        final var resultLength = result.length;
        double accum;
        int signalPos;

        if (listener != null) {
            listener.onStartConvolution();
        }

        for (var i = 0; i < resultLength; i++) {

            accum = 0.0;
            signalPos = i - kernelCenter;
            for (var j = 0; j < kernelLength; j++) {
                final var signalValue = getSignalValueMirror(signal, signalPos - j);
                accum += signalValue * kernel[j];
            }

            result[i] = accum;

            if (listener != null) {
                final var progress = (float) i / (float) resultLength;
                listener.onConvolveProgressChange(progress);
            }
        }

        if (listener != null) {
            listener.onFinishConvolution();
        }
    }

    /**
     * Internal method to determine signal value even when a position outside
     * its boundaries is requested for mirror edge extension.
     *
     * @param signal signal to request a value for.
     * @param pos    requested position.
     * @return signal value when assuming that signal is mirrored outside its
     * boundaries.
     */
    protected static double getSignalValueMirror(double[] signal, int pos) {
        final var signalLength = signal.length;
        var times = pos / signalLength;

        if (pos < 0) {
            times = Math.abs(times);
            if (times == 0) {
                times = 1;
            }
            pos = 2 * signalLength * times + pos;
        }

        times = pos / signalLength;
        final var reversed = (times % 2 != 0);

        pos = pos % signalLength;
        if (pos < 0) {
            pos += signalLength;
        } else {
            if (reversed) {
                // reversed
                pos = signalLength - 1 - pos;
            }
        }

        return signal[pos];
    }

    /**
     * Interface defining events produced by this class.
     */
    public interface Convolver1DListener {
        /**
         * Called when convolution starts.
         */
        void onStartConvolution();

        /**
         * Called when convolution finishes.
         */
        void onFinishConvolution();

        /**
         * Called when convolution progress changes.
         *
         * @param progress convolution progress as a value between 0.0f and
         *                 1.0f.
         */
        void onConvolveProgressChange(final float progress);
    }
}