Am I correctly implementing this basic low pass filter (Phrogz's filter!) in Java?
-
10-06-2021 - |
Pregunta
I have a basic wave generator in java but I need something to remove the clicks I get from when the amplitude of a wave changes sharply. Namely when I start/stop playing a wave, especially if I have a beeping tone.
Phrogz's answer on SO gave a really nice and simple function, but I'm not sure I'm implementing it right.
When I first tried to use it, I couldn't get it to work, but then I seem to remember it working very well... I have since fiddled about a lot with my code and now it doesn't seem to be working very well again.
So here's the closest I could get to an SSCCE:
If you play this you will notice that when the filtering is on (filter = true) the wave is much quieter and the clicks slightly less, but this seems mainly due to the decrease in volume. There is still a noticeable "hit" on each beep, that I don't want, and I don't remember being there before...
import javax.sound.sampled.*;
public class Oscillator{
private static int SAMPLE_RATE = 22050;
private static short MAX_AMPLITUDE = Short.MAX_VALUE;
private static AudioFormat af = null;
private static SourceDataLine line = null;
private int frequency = 440; //Hz
private int numLoops = 1000;
private int beep = 100;
// set to true to apply low-pass filter
private boolean filter = true;
// set the amount of "smoothing" here
private int smoothing = 100;
private double oldValue;
public Oscillator(){
prepareLine();
}
public static void main(String[] args) {
System.out.println("Playing oscillator");
Oscillator osc = new Oscillator();
osc.play();
}
private void prepareLine(){
af = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLE_RATE, 16, 2, 4, SAMPLE_RATE, false);
try {
DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
if (!AudioSystem.isLineSupported(info)) {
System.out.println("Line does not support: " + af);
System.exit(0);
}
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(af);
}
catch (Exception e) {
System.out.println(e.getMessage());
System.exit(0);
}
}
private void play() {
System.out.println("play");
int maxSize = (int) Math.round( (SAMPLE_RATE * af.getFrameSize())/ frequency );
byte[] samples = new byte[maxSize];
line.start();
double volume = 1;
int count = 0;
for (int i = 0; i < numLoops; i ++){
if (count == beep) {
if(volume==1) volume = 0;
else volume = 1;
count = 0;
}
count ++;
playWave(frequency, volume, samples);
}
line.drain();
line.stop();
line.close();
System.exit(0);
}
private void playWave(int frequency, double volLevel, byte[] samples) {
double amplitude = volLevel * MAX_AMPLITUDE;
int numSamplesInWave = (int) Math.round( ((double) SAMPLE_RATE)/frequency );
int index = 0;
for (int i = 0; i < numSamplesInWave; i++) {
double theta = (double)i/numSamplesInWave;
double wave = getWave(theta);
int sample = (int) (wave * amplitude);
if (filter) sample = applyLowPassFilter(sample);
// left sample
samples[index + 0] = (byte) (sample & 0xFF);
samples[index + 1] = (byte) ((sample >> 8) & 0xFF);
// right sample
samples[index + 2] = (byte) (sample & 0xFF);
samples[index + 3] = (byte) ((sample >> 8) & 0xFF);
index += 4;
}
int offset = 0;
while (offset < index){
double increment =line.write(samples, offset, index-offset);
offset += increment;
}
}
private double getWave(double theta){
double value = 0;
theta = theta * 2 * Math.PI;
value = getSin(theta);
//value = getSqr(theta);
return value;
}
private double getSin(double theta){
return Math.sin(theta);
}
private int getSqr(double theta){
if (theta <= Math.PI) return 1;
else return 0;
}
// implementation of basic low-pass filter
private int applyLowPassFilter(int sample){
int newValue = sample;
double filteredValue = oldValue + (newValue - oldValue) / smoothing;
oldValue = filteredValue;
return (int) filteredValue;
}
}
The relevant method is at the end. If anyone does test this, please be careful of the volume if you have headphones!
So either:
- It is working and I'm just expecting too much of such a simple implementation
- I'm doing something wrong, stupid and obvious...
If it's just 1. How should/could I get rid of that harsh beat/hit/click from sudden amplitude changes?
If it's 2. good, should be a v short answer for a too long question.
Solución
A low pass filter will not remove clicks from sudden amplitude changes. Instead you need to avoid sudden amplitude changes.
You could use the lowpass filter to filter your amplitude level.
**Pseudo code**
for i = 0 to numSamplesInWave-1 do
begin
theta = i / numSamplesInWave;
wave = getWave(theta);
currentAmplitude = applyLowPassFilter(TargetAmplitude);
Sample[i] = wave * currentAmplitude;
end;
Using a lowpass filter as above is fine for smoothing input values. For example when the user changes a volume control.
In other situations it might be more appropriate to create an envelope of some sort. For example synthesizers commonly use ADSR envelopes to smooth the amplitude changes when a new Voice/Sound starts and stops.