|
1 | 1 | package com.thealgorithms.audiofilters; |
2 | 2 |
|
| 3 | +import java.util.Arrays; |
| 4 | +import java.util.Objects; |
| 5 | + |
3 | 6 | /** |
4 | | - * Exponential Moving Average (EMA) Filter for smoothing audio signals. |
| 7 | + * EMAFilter - Exponential Moving Average filter for smoothing audio signals. |
| 8 | + * |
| 9 | + * Think of this as a "smart smoothing tool" for audio: it looks at each new sample |
| 10 | + * and gently blends it with the previous smoothed value to reduce sudden spikes or noise. |
5 | 11 | * |
6 | | - * <p>This filter applies an exponential moving average to a sequence of audio |
7 | | - * signal values, making it useful for smoothing out rapid fluctuations. |
8 | | - * The smoothing factor (alpha) controls the degree of smoothing. |
| 12 | + * The smoothing factor (alpha) determines how responsive the filter is: |
| 13 | + * - High alpha (close to 1) → reacts quickly to new samples (less smoothing) |
| 14 | + * - Low alpha (close to 0) → reacts slowly (more smoothing) |
9 | 15 | * |
10 | | - * <p>Based on the definition from |
11 | | - * <a href="https://en.wikipedia.org/wiki/Moving_average">Wikipedia link</a>. |
| 16 | + * This class supports both: |
| 17 | + * 1. Batch processing (arrays of samples) |
| 18 | + * 2. Streaming / real-time processing (sample by sample) |
12 | 19 | */ |
13 | | -public class EMAFilter { |
| 20 | +public final class EMAFilter { |
| 21 | + |
| 22 | + /** How "responsive" the filter is to new data */ |
14 | 23 | private final double alpha; |
15 | | - private double emaValue; |
| 24 | + |
| 25 | + /** Stores the last EMA value for continuous processing */ |
| 26 | + private double lastEma; |
| 27 | + |
16 | 28 | /** |
17 | | - * Constructs an EMA filter with a given smoothing factor. |
| 29 | + * Constructor: sets the smoothing factor (alpha) for the filter. |
18 | 30 | * |
19 | | - * @param alpha Smoothing factor (0 < alpha <= 1) |
20 | | - * @throws IllegalArgumentException if alpha is not in (0, 1] |
| 31 | + * @param alpha smoothing factor between 0 (exclusive) and 1 (inclusive) |
| 32 | + * @throws IllegalArgumentException if alpha is invalid |
21 | 33 | */ |
22 | 34 | public EMAFilter(double alpha) { |
23 | | - if (alpha <= 0 || alpha > 1) { |
24 | | - throw new IllegalArgumentException("Alpha must be between 0 and 1."); |
| 35 | + if (alpha <= 0.0 || alpha > 1.0) { |
| 36 | + throw new IllegalArgumentException("Alpha must be between 0 (exclusive) and 1 (inclusive). Got: " + alpha); |
25 | 37 | } |
26 | 38 | this.alpha = alpha; |
27 | | - this.emaValue = 0.0; |
| 39 | + this.lastEma = Double.NaN; // Indicates that no sample has been processed yet |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * Smooths a whole array of audio samples and returns a new array. |
| 44 | + * |
| 45 | + * Original samples remain unchanged. |
| 46 | + * |
| 47 | + * @param samples input audio samples |
| 48 | + * @return new array with smoothed samples |
| 49 | + */ |
| 50 | + public double[] apply(double[] samples) { |
| 51 | + Objects.requireNonNull(samples, "Input samples cannot be null."); |
| 52 | + if (samples.length == 0) return new double[0]; |
| 53 | + |
| 54 | + double[] smoothed = new double[samples.length]; |
| 55 | + double ema = samples[0]; // Start with the first sample |
| 56 | + smoothed[0] = ema; |
| 57 | + |
| 58 | + for (int i = 1; i < samples.length; i++) { |
| 59 | + // EMA formula: current = alpha * newSample + (1 - alpha) * previousEMA |
| 60 | + ema = alpha * samples[i] + (1 - alpha) * ema; |
| 61 | + smoothed[i] = ema; |
| 62 | + } |
| 63 | + |
| 64 | + lastEma = ema; // Save last EMA for streaming |
| 65 | + return smoothed; |
28 | 66 | } |
| 67 | + |
29 | 68 | /** |
30 | | - * Applies the EMA filter to an audio signal array. |
| 69 | + * Smooths the array **in-place** to save memory. |
31 | 70 | * |
32 | | - * @param audioSignal Array of audio samples to process |
33 | | - * @return Array of processed (smoothed) samples |
| 71 | + * Useful for large audio arrays or memory-sensitive applications. |
| 72 | + * |
| 73 | + * @param samples array to be smoothed (will be overwritten) |
34 | 74 | */ |
35 | | - public double[] apply(double[] audioSignal) { |
36 | | - if (audioSignal.length == 0) { |
37 | | - return new double[0]; |
| 75 | + public void applyInPlace(double[] samples) { |
| 76 | + Objects.requireNonNull(samples, "Input samples cannot be null."); |
| 77 | + if (samples.length == 0) return; |
| 78 | + |
| 79 | + double ema = samples[0]; |
| 80 | + for (int i = 1; i < samples.length; i++) { |
| 81 | + ema = alpha * samples[i] + (1 - alpha) * ema; |
| 82 | + samples[i] = ema; // overwrite original array |
38 | 83 | } |
39 | | - double[] emaSignal = new double[audioSignal.length]; |
40 | | - emaValue = audioSignal[0]; |
41 | | - emaSignal[0] = emaValue; |
42 | | - for (int i = 1; i < audioSignal.length; i++) { |
43 | | - emaValue = alpha * audioSignal[i] + (1 - alpha) * emaValue; |
44 | | - emaSignal[i] = emaValue; |
| 84 | + |
| 85 | + lastEma = ema; |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * Returns the last EMA value computed. |
| 90 | + * |
| 91 | + * Useful for streaming or continuous processing. |
| 92 | + * |
| 93 | + * @return last EMA value, or NaN if filter hasn't processed any data yet |
| 94 | + */ |
| 95 | + public double getLastEma() { |
| 96 | + return lastEma; |
| 97 | + } |
| 98 | + |
| 99 | + /** |
| 100 | + * Updates the EMA with a single new sample (streaming / real-time processing). |
| 101 | + * |
| 102 | + * @param sample next input value |
| 103 | + * @return updated EMA value |
| 104 | + */ |
| 105 | + public double next(double sample) { |
| 106 | + if (Double.isNaN(lastEma)) { |
| 107 | + lastEma = sample; // Initialize with first sample |
| 108 | + } else { |
| 109 | + lastEma = alpha * sample + (1 - alpha) * lastEma; |
45 | 110 | } |
46 | | - return emaSignal; |
| 111 | + return lastEma; |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + public String toString() { |
| 116 | + return "EMAFilter{alpha=" + alpha + ", lastEma=" + lastEma + "}"; |
47 | 117 | } |
48 | 118 | } |
0 commit comments