I want to create a audio level meter in java for the microphone to check how loud the input is. It should look like the one of the OS. I'm not asking about the gui. It is just about calculating the audio level out of the bytestream produced by
n = targetDataLine.read( tempBuffer , 0 , tempBuffer.length );
So I already have something that is running, but it is not even close to the levelmeter of my OS (windows) It stucks in the middle. I have values between 0 and 100 that is good but in the middle volume it stucks around 60 no matter how loud the input is.
This is how I calculate it now:
amplitude = 0;
for (int j = 0; j < tempBuffer.length; j = j +2 ){
if (tempBuffer[j] > tempBuffer[j+1])
amplitude = amplitude + tempBuffer[j] - tempBuffer[j+1];
else amplitude = amplitude + tempBuffer[j + 1] - tempBuffer[j];
}
amplitude = amplitude / tempBuffer.length * 2;
Is there a better/more precise way to calculate the audio level to monitor it? Or did I maybe do a major mistake?
That is my Audioformat:
public static AudioFormat getAudioFormat(){
float sampleRate = 20000.0F;
//8000,11025,16000,22050,44100
int sampleSizeInBits = 16;
//8,16
int channels = 1;
//1,2
boolean signed = true;
//true,false
boolean bigEndian = false;
//true,false
return new AudioFormat( sampleRate, sampleSizeInBits, channels, signed, bigEndian );
//return new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 8000.0F, 8, 1, 1, 8000.0F, false);
}
Principally the problem seems to be that you are reading the audio data incorrectly.
Specifically I'm not really sure what this excerpt is supposed to mean:
if (tempBuffer[j] > tempBuffer[j+1])
... tempBuffer[j] - tempBuffer[j+1];
else
... tempBuffer[j + 1] - tempBuffer[j];
But anyhow since you are recording 16-bit data the bytes in the byte array aren't meaningful on their own. Each byte only represents 1/2 of the bits in each sample. You need to 'unpack' them to int, float, whatever, before you can do anything with them. For raw LPCM, concatenating the bytes is done by shifting them and ORing them together.
Here is an MCVE to demonstrate a rudimentary level meter (both RMS and simple peak hold) in Java.
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JComponent;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Dimension;
import javax.swing.border.EmptyBorder;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
public class LevelMeter extends JComponent {
private int meterWidth = 10;
private float amp = 0f;
private float peak = 0f;
public void setAmplitude(float amp) {
this.amp = Math.abs(amp);
repaint();
}
public void setPeak(float peak) {
this.peak = Math.abs(peak);
repaint();
}
public void setMeterWidth(int meterWidth) {
this.meterWidth = meterWidth;
}
@Override
protected void paintComponent(Graphics g) {
int w = Math.min(meterWidth, getWidth());
int h = getHeight();
int x = getWidth() / 2 - w / 2;
int y = 0;
g.setColor(Color.LIGHT_GRAY);
g.fillRect(x, y, w, h);
g.setColor(Color.BLACK);
g.drawRect(x, y, w - 1, h - 1);
int a = Math.round(amp * (h - 2));
g.setColor(Color.GREEN);
g.fillRect(x + 1, y + h - 1 - a, w - 2, a);
int p = Math.round(peak * (h - 2));
g.setColor(Color.RED);
g.drawLine(x + 1, y + h - 1 - p, x + w - 1, y + h - 1 - p);
}
@Override
public Dimension getMinimumSize() {
Dimension min = super.getMinimumSize();
if(min.width < meterWidth)
min.width = meterWidth;
if(min.height < meterWidth)
min.height = meterWidth;
return min;
}
@Override
public Dimension getPreferredSize() {
Dimension pref = super.getPreferredSize();
pref.width = meterWidth;
return pref;
}
@Override
public void setPreferredSize(Dimension pref) {
super.setPreferredSize(pref);
setMeterWidth(pref.width);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("Meter");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel content = new JPanel(new BorderLayout());
content.setBorder(new EmptyBorder(25, 50, 25, 50));
LevelMeter meter = new LevelMeter();
meter.setPreferredSize(new Dimension(9, 100));
content.add(meter, BorderLayout.CENTER);
frame.setContentPane(content);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
new Thread(new Recorder(meter)).start();
}
});
}
static class Recorder implements Runnable {
final LevelMeter meter;
Recorder(final LevelMeter meter) {
this.meter = meter;
}
@Override
public void run() {
AudioFormat fmt = new AudioFormat(44100f, 16, 1, true, false);
final int bufferByteSize = 2048;
TargetDataLine line;
try {
line = AudioSystem.getTargetDataLine(fmt);
line.open(fmt, bufferByteSize);
} catch(LineUnavailableException e) {
System.err.println(e);
return;
}
byte[] buf = new byte[bufferByteSize];
float[] samples = new float[bufferByteSize / 2];
float lastPeak = 0f;
line.start();
for(int b; (b = line.read(buf, 0, buf.length)) > -1;) {
// convert bytes to samples here
for(int i = 0, s = 0; i < b;) {
int sample = 0;
sample |= buf[i++] & 0xFF; // (reverse these two lines
sample |= buf[i++] << 8; // if the format is big endian)
// normalize to range of +/-1.0f
samples[s++] = sample / 32768f;
}
float rms = 0f;
float peak = 0f;
for(float sample : samples) {
float abs = Math.abs(sample);
if(abs > peak) {
peak = abs;
}
rms += sample * sample;
}
rms = (float)Math.sqrt(rms / samples.length);
if(lastPeak > peak) {
peak = lastPeak * 0.875f;
}
lastPeak = peak;
setMeterOnEDT(rms, peak);
}
}
void setMeterOnEDT(final float rms, final float peak) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
meter.setAmplitude(rms);
meter.setPeak(peak);
}
});
}
}
}
Note the format conversion is hard-coded there.
You may also see "How do I use audio sample data from Java Sound?" for my detailed explanation of how to unpack audio data from the raw bytes.
Related: