Android: Audio Recording with voice level visualization

后端 未结 4 1430
再見小時候
再見小時候 2021-02-01 08:32

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

相关标签:
4条回答
  • 2021-02-01 09:08

    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.

    0 讨论(0)
  • 2021-02-01 09:14

    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.

    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);
        }
    }
    
    0 讨论(0)
  • 2021-02-01 09:14

    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.

    0 讨论(0)
  • 2021-02-01 09:29

    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
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题