Volume Meter for microphone input volume

7th Aug 2017

Projects

java

Wearing noise-cancelling headphones whilst speaking into a microphone often means that you can't really tell how loud your voice is. When inputting into any sound capture software like Audacity you can see a readout for your mic volume. However, if you're for example in a fullscreen game or other such application where you are talking into a microphone you can't see such a readout.

To solve this problem I wrote a small Java application that takes the microphone input, loads a small frame in realtime, performs an RMS calculation on that frame in order to get a relative volume level and displays that as a series of bars.

The resulting java application was packaged using JSmooth (http://jsmooth.sourceforge.net/) into an exe and can be downloaded here. Or the code can be found on github if you'd like to modify/compile it yourself.

Some pictures of the output:

volume1 volume2 volume3

These show: nominal levels, i.e. no speaking with just background noise; medium levels, i.e. regular speech; and extreme levels; i.e. pops from plosives, excessive volume etc.

Code breakdown

Some of the core functionality is pretty interesting to look at and so I've broken it down below:

Taking in mic input

To take in microphone input in Java, the javax.sound.sampled package was used. With this package, you first need to specify the format that the microphone data line will be in, by default the default line will be used, so make sure the microphone you want to use is the default input device for your system. The info of the line then needs to be gathered and checked to make sure it is supported, the line needs to be opened and then it can be read into a buffer.

The Java code to do this is as follows:

// Data line for mic input
protected TargetDataLine line = null;

/** VolumeMeter::startListening
 * Open audio channel and start working with stream
 */
public void startListening() {
    // Open a TargetDataLine for getting mic input level
    AudioFormat format = new AudioFormat(42000.0f, 16, 1, true, true); // Get default line
    DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
    if (!AudioSystem.isLineSupported(info)) { // If no default line
        System.out.println("The TargetDataLine is unavailable");
    }

    // Obtain and open the line.
    try {
        line = (TargetDataLine) AudioSystem.getLine(info);
        line.open(format);
        line.start();
    } catch (LineUnavailableException ex) {
        System.out.println("The TargetDataLine is Unavailable.");
    }

    int level = 0; // Hold calculated RMS volume level
    byte tempBuffer[] = new byte[6000]; // Data buffer for raw audio
    try {
        // Continually read in mic data into buffer and calculate RMS
        while (true) {
            // If read in enough, calculate RMS
            if (line.read(tempBuffer, 0, tempBuffer.length) > 0) {
                level = calculateRMSLevel(tempBuffer);
                updateBars(level); // Update bar display
            }
        }
    } catch (Exception e) {
        System.err.println(e);
        System.exit(0);
    }
}

I decided to use a buffer size of 6000 bytes, with some experimentation I found that this gave me enough of a buffer size to get an accurate reading whilst still updating fast enough to be visually nice. After reading in a full buffer the RMS can be calculated and the visual bars updated. Calculating the RMS is a fairly common formula and you can find pseudocode all around the internet, so my implementation will seem fairly similar to everyone elses:

/** VolumeMeter::calculateRMSLevel
 * Calculate the RMS of the raw audio in buffer
 * @param byte[] audioData  The buffer containing snippet of raw audio data
 * @return int  The RMS value of the buffer
 */
protected static int calculateRMSLevel(byte[] audioData) {
    long lSum = 0;
    for(int i = 0; i < audioData.length; i++)
        lSum = lSum + audioData[i];

    double dAvg = lSum / audioData.length;

    double sumMeanSquare = 0d;
    for(int j = 0; j < audioData.length; j++)
        sumMeanSquare = sumMeanSquare + Math.pow(audioData[j] - dAvg, 2d);

    double averageMeanSquare = sumMeanSquare / audioData.length;
    return (int)(Math.pow(averageMeanSquare, 0.5d) + 0.5) - 50;
}

Beyond this, there was various statements to make sure the window matched what I wanted. I am using Swing so it was simply a matter of making sure the JFrame was windowless, had a set type of utility and was set to always be on top, the Java code for this is:

setUndecorated(true);
setAlwaysOnTop(true);
setType(JFrame.Type.UTILITY);

To view the full code listing, go to https://github.com/DanFoad/VolumeMeter