8

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);
}
Radiodef
  • 37,180
  • 14
  • 90
  • 125
AliceInChains
  • 167
  • 1
  • 1
  • 9
  • I don't understand how your calculation is intended to work. (Were you just guessing at how to unpack the audio?) What is the AudioFormat of the TargetDataLine? – Radiodef Oct 26 '14 at 17:25
  • I looked at the array of bytes produced by n = targetDataLine.read( tempBuffer , 0 , tempBuffer.length ); and saw that it is alternating between low and high values so I thought that is the amplitude. In fact I really measure the level of the input but it works because it is proportional. The only negative thing is that in the normal range it only deviate a little and is not so precise as the windows level of the same input. Maybe someone has further advice how to do it? – AliceInChains Oct 26 '14 at 17:57

2 Answers2

26

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.

Meter

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:

Community
  • 1
  • 1
Radiodef
  • 37,180
  • 14
  • 90
  • 125
  • 1
    Great explanation! One question, buf is related with a mono channel, or is a stereo L+R data? – Billyjoker Jan 26 '17 at 19:23
  • @Billyjoker It's mono. The number of channels is provided in the constructor arguments to `AudioFormat` (in the first line of the `run` method). – Radiodef Jan 28 '17 at 00:54
  • Thx @Radiodef. In trying to adapt the code to my app, but instead to pass the values to a LevelMeter, i'd like to represent the amplitud between [0-127], but i have some like random data, independent of the volume... :( – Billyjoker Jan 28 '17 at 10:50
  • @Billyjoker The conversion code (from raw bytes to float samples) in my short example here is hard-coded so that it only works for 44.1kHZ, 16-bit, little endian audio. (That's the same as a CD-quality WAV file.) If you have audio that's a different format, the conversion code won't work. That's all I can think of. I explained how that code works in [this Q&A here](http://stackoverflow.com/q/26824663/2891664). I assume there are also libraries that would do it. If you're reading from a mic with the same code as I have here then I'm not sure what the problem is. – Radiodef Jan 28 '17 at 15:42
  • @Radiodef i'm working with audio 44.1kHZ, 16-bit, little endian audio. I'm doing like your example does. When is no sound i get a value -32640 (-0.99609375 normalized dividing it by 32768f). I'd like to convert it to a 0 value, and the loudness value convert it to 127 value o whatever, but i do not know how to do it – Billyjoker Jan 28 '17 at 19:39
  • 1
    @Billyjoker Well, unfortunately I don't think I can help since the problem doesn't appear to be with my code. I tinkered around with it and it does appear to be working correctly on my end. If this helps, though, -32640 is the value `0b11111111111111111000000010000000` which you would get if the buffer contained the pair of bytes `-127, -127` (or `128, 128` if we consider them unsigned). If the buffer did have 0s you would also get 0s after my conversion code. I'd say that either there's a problem with your input system (mic, sound device, etc.) or the code is different from mine in some way. – Radiodef Jan 28 '17 at 22:46
  • Is this actually little endian? The comment seems to be wrong. – Lynne Jun 15 '20 at 20:24
1

The above code will find the data point with highest value but cannot determine the peak value of the reconstructed data samples. To find the reconstructed peak you would have to pass the data samples through a low pass filter. or use a DFT/FFT algorithm.

David
  • 21
  • 1