14

I need to create a android application which is for recording voice while showing the voice(sound) level visualization.

I already created an audio recording application but I can not add sound level visualization. How can I do it?

please someone help me a giving suggestion or a sample tutorials link or code.

Vivek Thummar
  • 395
  • 4
  • 17
user1972690
  • 141
  • 1
  • 1
  • 3

4 Answers4

36

Create a xml activity_recording.xml like this.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="180dp"
    android:layout_alignParentBottom="true"
    android:background="#231f20" >

    <ali.visualiser.VisualizerView
        android:id="@+id/visualizer"
        android:layout_width="220dp"
        android:layout_height="75dp"
        android:layout_centerHorizontal="true"
        android:layout_margin="5dp" />

    <TextView
        android:id="@+id/txtRecord"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="25dp"
        android:gravity="center"
        android:text="Start Recording"
        android:textColor="@android:color/white"
        android:textSize="30sp" />

</RelativeLayout>

Create a custom visualizerView as given below.

package ali.visualiser;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class VisualizerView extends View {
    private static final int LINE_WIDTH = 1; // width of visualizer lines
    private static final int LINE_SCALE = 75; // scales visualizer lines
    private List<Float> amplitudes; // amplitudes for line lengths
    private int width; // width of this View
    private int height; // height of this View
    private Paint linePaint; // specifies line drawing characteristics

    // constructor
    public VisualizerView(Context context, AttributeSet attrs) {
        super(context, attrs); // call superclass constructor
        linePaint = new Paint(); // create Paint for lines
        linePaint.setColor(Color.GREEN); // set color to green
        linePaint.setStrokeWidth(LINE_WIDTH); // set stroke width
    } 

    // called when the dimensions of the View change
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        width = w; // new width of this View
        height = h; // new height of this View
        amplitudes = new ArrayList<Float>(width / LINE_WIDTH);
    } 

    // clear all amplitudes to prepare for a new visualization
    public void clear() {
        amplitudes.clear();
    } 

    // add the given amplitude to the amplitudes ArrayList
    public void addAmplitude(float amplitude) {
        amplitudes.add(amplitude); // add newest to the amplitudes ArrayList

        // if the power lines completely fill the VisualizerView
        if (amplitudes.size() * LINE_WIDTH >= width) {
            amplitudes.remove(0); // remove oldest power value
        } 
    } 

    // draw the visualizer with scaled lines representing the amplitudes
    @Override
    public void onDraw(Canvas canvas) {
        int middle = height / 2; // get the middle of the View
        float curX = 0; // start curX at zero

        // for each item in the amplitudes ArrayList
        for (float power : amplitudes) {
            float scaledHeight = power / LINE_SCALE; // scale the power
            curX += LINE_WIDTH; // increase X by LINE_WIDTH

            // draw a line representing this item in the amplitudes ArrayList
            canvas.drawLine(curX, middle + scaledHeight / 2, curX, middle
                    - scaledHeight / 2, linePaint);
        } 
    } 

}

Create RecordingActivity class as given below.

package ali.visualiser;

import java.io.File;
import java.io.IOException;

import android.app.Activity;
import android.media.MediaRecorder;
import android.media.MediaRecorder.OnErrorListener;
import android.media.MediaRecorder.OnInfoListener;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;



public class RecordingActivity extends Activity {
    public static final String DIRECTORY_NAME_TEMP = "AudioTemp";
    public static final int REPEAT_INTERVAL = 40;
    private TextView txtRecord;

    VisualizerView visualizerView;

    private MediaRecorder recorder = null;

    File audioDirTemp;
private boolean isRecording = false;


    private Handler handler; // Handler for updating the visualizer
    // private boolean recording; // are we currently recording?

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recording);

        visualizerView = (VisualizerView) findViewById(R.id.visualizer);

        txtRecord = (TextView) findViewById(R.id.txtRecord);
        txtRecord.setOnClickListener(recordClick);

        audioDirTemp = new File(Environment.getExternalStorageDirectory(),
                DIRECTORY_NAME_TEMP);
        if (audioDirTemp.exists()) {
            deleteFilesInDir(audioDirTemp);
        } else {
            audioDirTemp.mkdirs();
        }

        // create the Handler for visualizer update
        handler = new Handler();
    }   

    OnClickListener recordClick = new OnClickListener() {

        @Override
        public void onClick(View v) {

            if (!isRecording) {
                // isRecording = true;

                txtRecord.setText("Stop Recording");

                recorder = new MediaRecorder();

                recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
                recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
                recorder.setOutputFile(audioDirTemp + "/audio_file"
                        + ".mp3");

                OnErrorListener errorListener = null;
                recorder.setOnErrorListener(errorListener);
                OnInfoListener infoListener = null;
                recorder.setOnInfoListener(infoListener);

                try {
                    recorder.prepare();
                    recorder.start();
                    isRecording = true; // we are currently recording
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                handler.post(updateVisualizer);

            } else {

                txtRecord.setText("Start Recording");

                releaseRecorder();
            }

        }
    };

    private void releaseRecorder() {
        if (recorder != null) {
            isRecording = false; // stop recording
            handler.removeCallbacks(updateVisualizer);
            visualizerView.clear();
            recorder.stop();
            recorder.reset();
            recorder.release();
            recorder = null;
        }
    }   

    public static boolean deleteFilesInDir(File path) {

        if( path.exists() ) {
            File[] files = path.listFiles();
            if (files == null) {
                return true;
            }
            for(int i=0; i<files.length; i++) {

                if(files[i].isDirectory()) {                

                }
                else {
                    files[i].delete();
                }
            }
        }
        return true;
    }

    @Override
    protected void onDestroy() {

        super.onDestroy();
        releaseRecorder();
    }

    // updates the visualizer every 50 milliseconds
    Runnable updateVisualizer = new Runnable() {
        @Override
        public void run() {
            if (isRecording) // if we are already recording
            {
                // get the current amplitude
                int x = recorder.getMaxAmplitude();
                visualizerView.addAmplitude(x); // update the VisualizeView
                visualizerView.invalidate(); // refresh the VisualizerView

                // update in 40 milliseconds
                handler.postDelayed(this, REPEAT_INTERVAL);
            }
        }
    };


}

Result

This is how it looks: https://www.youtube.com/watch?v=BoFG6S02GH0

When it reaches the end, the animation continues as expected: erasing the beginning of the graph.

Antilope
  • 340
  • 2
  • 10
Ali
  • 1,857
  • 24
  • 25
  • 2
    do not forget to add permission – Ali May 07 '15 at 07:24
  • 2
    It would be just great if you, or anyone using your solution, added a screenshot of a working example – Vedant Agarwala Oct 12 '15 at 13:26
  • This is not working . when start recording recording not starting.There is an exception when stop it .Please help me . The exception is given below – Barun Kumar Nov 13 '15 at 11:47
  • Caused by: java.lang.IllegalStateException at android.media.MediaRecorder.stop(Native Method) at com.example.lenovo.recordaudio.MainActivity.releaseRecorder(MainActivity.java:107) at com.example.lenovo.recordaudio.MainActivity.onDestroy(MainActivity.java:138) – Barun Kumar Nov 13 '15 at 11:53
  • Have you added required permission in your manifest file. In first comment I have mentioned it. – Ali Nov 16 '15 at 05:09
  • I added a video to see the final result on my Nexus 6 (thought it was better than just a screenshot). Also, @Ali, i cant thank you enough, saved me lots of hours. – Antilope May 05 '16 at 18:45
  • What changes required to work in case of playing audio instead of recording? – Faizan Mubasher Nov 03 '16 at 14:14
  • @Ali what changes has to be made to use this component while playing the audio file? – Harshith Feb 01 '17 at 06:26
  • @Harshith google have provided android samples for various functions. for audio playing sample this is the link https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/media/AudioFxDemo.java – Ali Feb 03 '17 at 09:38
  • @Ali I have implemented other visualiser for playing audio clip. Just wanted to know if i can use the above component for playing. – Harshith Feb 04 '17 at 03:40
  • 5
    Video link is dead – Nguyen Minh Binh Jul 23 '18 at 14:19
  • 1
    @Ali where to create RecordingActivity class. Thanks in advance. Highly appreciated if replied. – Mubeen Khan Apr 19 '19 at 13:07
  • @MubeenKhan It is normal activity class of android. It can be created in any package of your project. Two permission are mandatory to be added in Manifest file namely RECORD_AUDIO and WRITE_EXTERNAL_STORAGE. – Ali Apr 21 '19 at 02:57
9

I like Ali's answer, but here's a simpler version that performs much better. The real speed comes from making the view class's onDraw method as fast as possible. Store the correct values in memory first by doing any computations not required for drawing outside the draw loop, and pass fully populated structures to draw routines to allow the hardware to optimize drawing many lines.

I launched my RecordingActivity and set it full screen, but you can create a layout resource or add the view anywhere.

enter image description here

Actvity:

public class RecordingActivity extends Activity {
    private VisualizerView visualizerView;
    private MediaRecorder recorder = new MediaRecorder();
    private Handler handler = new Handler();
    final Runnable updater = new Runnable() {
        public void run() {
            handler.postDelayed(this, 1);
            int maxAmplitude = recorder.getMaxAmplitude();
            if (maxAmplitude != 0) {
                visualizerView.addAmplitude(maxAmplitude);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recording);
        visualizerView = (VisualizerView) findViewById(R.id.visualizer);
        try {
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            recorder.setOutputFile("/dev/null");
            recorder.prepare();
            recorder.start();
        } catch (IllegalStateException | IOException ignored) {
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacks(updater);
        recorder.stop();
        recorder.reset();
        recorder.release();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        handler.post(updater);
    }
}

View:

public class VisualizerView extends View {
    private static final int MAX_AMPLITUDE = 32767;

    private float[] amplitudes;
    private float[] vectors;
    private int insertIdx = 0;
    private Paint pointPaint;
    private Paint linePaint;
    private int width;
    private int height;

    public VisualizerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        linePaint = new Paint();
        linePaint.setColor(Color.GREEN);
        linePaint.setStrokeWidth(1);
        pointPaint = new Paint();
        pointPaint.setColor(Color.BLUE);
        pointPaint.setStrokeWidth(1);
    }

    @Override
    protected void onSizeChanged(int width, int h, int oldw, int oldh) {
        this.width = width;
        height = h;
        amplitudes = new float[this.width * 2]; // xy for each point across the width
        vectors = new float[this.width * 4]; // xxyy for each line across the width
    }

    /**
     * modifies draw arrays. cycles back to zero when amplitude samples reach max screen size
     */
    public void addAmplitude(int amplitude) {
        invalidate();
        float scaledHeight = ((float) amplitude / MAX_AMPLITUDE) * (height - 1);
        int ampIdx = insertIdx * 2;
        amplitudes[ampIdx++] = insertIdx;   // x
        amplitudes[ampIdx] = scaledHeight;  // y
        int vectorIdx = insertIdx * 4;
        vectors[vectorIdx++] = insertIdx;   // x0
        vectors[vectorIdx++] = 0;           // y0
        vectors[vectorIdx++] = insertIdx;   // x1
        vectors[vectorIdx] = scaledHeight;  // y1
        // insert index must be shorter than screen width
        insertIdx = ++insertIdx >= width ? 0 : insertIdx;
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.drawLines(vectors, linePaint);
        canvas.drawPoints(amplitudes, pointPaint);
    }
}
activedecay
  • 10,129
  • 5
  • 47
  • 71
  • Your solution is perfect. I just want to add an additional thing to this, each time when the user clicks on record button I want to refresh the visualizer and redraw it from the beginning. I have tried many solutions but it didn't work out, can you help me out – karthik vishnu kumar Dec 12 '17 at 07:13
  • Great! This is solution is brilliant. Thank you a lot lifesaver! – Serg Burlaka Mar 24 '20 at 17:14
2

If you're using the MediaRecorder class and visualization based on peak amplitude is ok you can use the getMaxAmplitude() method to continuously poll for the "maximum absolute amplitude that was sampled since the last call".
Scale that amplitude down into an index that determines how many of your app's graphical volume bars to light up and you're set.

Michael
  • 57,169
  • 9
  • 80
  • 125
  • 1
    I don't have any sample code for that. But most of it would seem like straightforward Android UI coding. Start a `Handler` that [you run periodically](http://stackoverflow.com/questions/6242268/repeat-a-task-with-a-time-delay/6242292#6242292), call the `getMaxAmplitude` method for your `MediaRecorder` to get the current peak level, update the `View` that contains your volume bars based on the new peak level, and finally do a [postInvalidate](http://stackoverflow.com/questions/5521596/what-does-postinvalidate-do) to request that the modified View is redrawn. – Michael Jan 13 '13 at 08:51
  • @Michael This seems to make a lot of sense, I'm just not sure what audioSession id to use for initializing the Visualizer. – Drazen Bjelovuk May 21 '14 at 15:20
  • @JoshBjelovuk: The implementation I suggested here does not use the `Visualizer` class. My suggestion was to use the `MediaRecorder`'s `getMaxAmplitude` method. – Michael May 21 '14 at 15:26
0

My approach to this is based on activedecay's and Ali's answers, and I added a display DPI scale, since dp is scaled by the screen density so, 1 pixel in 320 dpi is not 1 pixel in 420 dpi. I had that problem that the visualizer was not moving at the same rate in different screens.

I also haven't found out, why the canvas doesn't start drawing from the beginning of the view API 28 only. But is not looking bad in any way.

Info about dpi scaling: Android Developers/Support different pixel densities

package com.example.mediarecorderdemo.views;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

import java.util.ArrayList;

import static com.example.mediarecorderdemo.RecordingActivity.DEBUG;

public class VisualizerView extends View {
    private static final int MAX_AMPLITUDE = 32767;

    private ArrayList<Float> amplitudes;
    private Paint linePaint;
    private int width;
    private int height;
    private int density;
    private float stroke;

    public VisualizerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        density = this.getResources().getDisplayMetrics().densityDpi;   //Get the display DPI

        linePaint = new Paint();
        linePaint.setColor(Color.GREEN);
        linePaint.setAntiAlias(true);  //Add AntiAlias for displaying strokes that are less than 1
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
        width = w;
        height = h;
        amplitudes = new ArrayList<>(width * 2);
        stroke =(width * ((float)density / 160)) / 1000; //Calculate actual pixel size for the view based on view width and dpi
        linePaint.setStrokeWidth(stroke);
    }

    /**
     * Add a new value of int to the visualizer array
     * @param amplitude Int value
     */
    public void addAmplitude(int amplitude){
        invalidate();
        float scaledHeight = ((float) amplitude / MAX_AMPLITUDE) * (height -1);
        amplitudes.add(scaledHeight);
    }

    /**
     * Clears Visualization
     */
    public void clear(){
        amplitudes.clear();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int middle = height / 2; // get the middle of the View
        float curX = 0; // start curX at zero

        // for each item in the amplitudes ArrayList
        for (float power : amplitudes) {

            // draw a line representing this item in the amplitudes ArrayList
            canvas.drawLine(curX, middle + power / 2, curX, middle
                    - power / 2, linePaint);

            curX += stroke; // increase X by line width
        }
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
gmonster1st
  • 75
  • 1
  • 9