录音及播放

最近项目使用录音功能,特此记录下。以及指出小左遇到的问题。

使用MediaRecorder(录音)、MediaPlayer(播放)、ExecutorService(线程池)实现功能。

一:权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

二:AudioRecoderUtils录音及播放工具


/**
 * 录音工具
 */
public class AudioRecoderUtils {

    private static ExecutorService mExecutorService = newSingleThreadExecutor();
    private boolean isPlaying;//播放状态
    private MediaPlayer mMediaPlayer;//播放控件
    public String filePath;
    public String fileName;
    private MediaRecorder mMediaRecorder;
    private final String TAG = "MediaRecord";
    public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大录音时长1000*60*10;

    public static boolean isStart = false;

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    public AudioRecoderUtils(){
    }

    private long startTime;
    private long endTime;

    /**
     * 开始录音 使用amr格式
     *      录音文件
     * @return
     */
    public void startRecord()  {
        if(!Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)){//存储媒体已经挂载,并且挂载点可读/写。
            Log.e(TAG, "SD is error!");
            return ;
        }
        fileName = System.currentTimeMillis()+".amr";
        //开始录音创建新文件路径
        this.filePath = Environment.getExternalStorageDirectory() + "/" + fileName;
        File file = new File(this.filePath);
        //文件若存在删除,防止录音覆盖问题
        if (!file.exists()){
            try {
                file.createNewFile();//创建新文件
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 开始录音
        /* ①Initial:实例化MediaRecorder对象 */
        if (mMediaRecorder == null) {
            mMediaRecorder = new MediaRecorder();
        }else{
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = new MediaRecorder();
        }
        try {
            /* ②setAudioSource/setVedioSource */
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置麦克风
            /* ②设置音频文件的编码:AAC/AMR_NB/AMR_MB/Default 声音的(波形)的采样 */
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
            /*
             * ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
             * ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)
             */
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            /* ③准备 */
            mMediaRecorder.setOutputFile(filePath);
            mMediaRecorder.setMaxDuration(MAX_LENGTH);

            mMediaRecorder.prepare();
            /* ④开始 */
            mMediaRecorder.start();
            isStart = true;
            // AudioRecord audioRecord.
            /* 获取开始时间* */
            startTime = System.currentTimeMillis();
            updateMicStatus();
            Log.i("ACTION_START", "startTime" + startTime);
        } catch (IllegalStateException e) {
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        } catch (IOException e) {
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        }
    }

    /**
     * 停止(取消)录音
     */
    public long stopRecord() {
        if(AudioRecoderUtils.isStart) {
            AudioRecoderUtils.isStart = false;
            if (mMediaRecorder == null)
                return 0L;
            endTime = System.currentTimeMillis();
            Log.i("ACTION_END", "endTime" + endTime);
            //有一些网友反应在5.0以上在调用stop的时候会报错,翻阅了一下谷歌文档发现上面确实写的有可能会报错的情况,捕获异常清理一下就行了,!
            try {
                mMediaRecorder.stop();
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;

            }catch (RuntimeException e){
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;
                e.printStackTrace();
            }
            return endTime - startTime;
        }
        return 0L;
    }

    private final Handler mHandler = new Handler();

    private Runnable mUpdateMicStatusTimer = new Runnable() {
        public void run() {
            updateMicStatus();
        }
    };

    /**
     * 更新话筒状态
     */
    private int BASE = 1;
    private int SPACE = 100;// 间隔取样时间

    public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
        this.audioStatusUpdateListener = audioStatusUpdateListener;
    }

    private void updateMicStatus() {
        if (mMediaRecorder != null) {
            double ratio = (double)mMediaRecorder.getMaxAmplitude() / BASE;
            double db = 0;// 分贝
            if (ratio > 1) {
                db = 20 * Math.log10(ratio);
                if(null != audioStatusUpdateListener) {
                    audioStatusUpdateListener.onUpdate(db);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }

    public interface OnAudioStatusUpdateListener {
        public void onUpdate(double db);
    }
    //播放音乐
    public boolean playAudio(){
        if (null != filePath && !isPlaying) {
            isPlaying = true;
            mExecutorService.submit(new Runnable() {
                @Override
                public void run() {
                    startPlay();
                }
            });
            return true;
        }
        return false;
    }
    private void  startPlay(){
        try {
            //初始化播放器
            mMediaPlayer = new MediaPlayer();
            //设置播放音频数据文件
            mMediaPlayer.setDataSource(filePath);
            //设置播放监听事件
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    //播放完成
                    playEndOrFail(true);
                }
            });
            //播放发生错误监听事件
            mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                @Override
                public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
                    playEndOrFail(false);
                    return true;
                }
            });
            //播放器音量配置
            mMediaPlayer.setVolume(1, 1);
            //是否循环播放
            mMediaPlayer.setLooping(false);
            //准备及播放
            mMediaPlayer.prepare();
            mMediaPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
            //播放失败正理
            playEndOrFail(false);
        }
    }
    /**
     * @description 停止播放或播放失败处理
     * @author ldm
     * @time 2017/2/9 16:58
     */
    private void playEndOrFail(boolean isEnd) {
        isPlaying = false;
        //TODO isEnd  用于播放状态提示(使用Handler)
        if (null != mMediaPlayer) {
            mMediaPlayer.setOnCompletionListener(null);
            mMediaPlayer.setOnErrorListener(null);
            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
}

三、activity_main.xml布局

<?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="match_parent">
<LinearLayout
                android:layout_width="0dip"
                android:layout_height="match_parent"
                android:orientation="horizontal"
                android:layout_weight="2">
                <TextView
                    android:id="@+id/tv_recording_content"
                    android:layout_width="0dip"
                    android:layout_height="match_parent"
                    android:maxLines="1"
                    android:layout_marginRight="5dip"
                    android:layout_weight="2"/>
                <TextView
                    android:id="@+id/start_recording"
                    android:background="@drawable/selector_recodering_btn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginRight="5dip"
                    android:paddingLeft="5dip"
                    android:paddingRight="5dip"
                    android:text="@string/start_recording"
                    android:gravity="center"
                    android:maxLines="1"/>
            </LinearLayout>
</RelativeLayout>

四、MainActivity.java调用录音功能

public class MainActivity extends Activity implements View.OnTouchListener, AudioRecoderUtils.OnAudioStatusUpdateListener, View.OnClickListener{
	
    TextView mStartRecording;//开始录音
    
    TextView mRecordingContent;//录音内容
    AudioRecoderDialog recoderDialog;//录音弹框
    AudioRecoderUtils recoderUtils;//录音工具
    private long downT;//按下时间
    private static final long RECORD_SHORT_TIMES = 2000L;//录音最少2秒
	 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		initView();
        initEvent();
        initAudioRecoder();
    }
	private void initView(){
		mStartRecording = findViewById(R.id.start_recording);
		mRecordingContent = findViewById(R.id.tv_recording_content);
	}
	private void initEvent(){
		mRecordingContent.setOnClickListener(this);
	}
	
	private void initAudioRecoder() {
        recoderDialog = new AudioRecoderDialog(this);
        recoderDialog.setShowAlpha(0.85f);

        recoderUtils = new AudioRecoderUtils();
        recoderUtils.setOnAudioStatusUpdateListener(this);
        mStartRecording.setOnTouchListener(this);
    }
	@Override
    public void onClick(View v) {
        switch (view.getId()){//播放
            case R.id.tv_recording_content:
                String content = mRecordingContent.getText().toString().trim();
                if(content!=null&& content.length()>0){
                    recoderUtils.playAudio();
                }
                break;
            default:
                break;
        }

    }
	 /**
     * 录音
     * @param view
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(final View view, MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN://开始录音
                //TODO 动态获取录音权限android.permission.RECORD_AUDIO(没有动态获取权限。Android5.0以后会出现第一次获取权限Bug)
                startAudioRecoder(view);
                
                return true;
            case MotionEvent.ACTION_UP://结束录音
                stopAudioRecoder();
                return true;
        }
        return false;
    }
	/**
     * 开始录音
     * @return
     */
    private void startAudioRecoder(View view){
        recoderUtils.startRecord();
        downT = System.currentTimeMillis();
        recoderDialog.showAtLocation(view, Gravity.CENTER, 0, 0);
        mStartRecording.setBackgroundResource(R.drawable.shape_recoder_btn_recoding);
        mStartRecording.setText(R.string.recording);//录音中
    }

    /**
     * 结束录音
     */
    private void stopAudioRecoder() {
        if(AudioRecoderUtils.isStart) {
            Log.d(TAG, "stop record");
            long time = recoderUtils.stopRecord();
            recoderDialog.dismiss();
            mStartRecording.setBackgroundResource(R.drawable.shape_recoder_btn_normal);
            mStartRecording.setText(R.string.start_recording);//开始录音
            if (time >= RECORD_SHORT_TIMES) {//至少2秒
                mRecordingContent.setText(recoderUtils.fileName);
            } else {
                mRecordingContent.setText("");
                Toast.makeText(this, getResources().getString(R.string.record_short_tips), Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    public void onUpdate(final double db) {
        if(null != recoderDialog) {
            int level = (int) db;
            recoderDialog.setLevel((int)db);
            recoderDialog.setTime(System.currentTimeMillis() - downT);
        }
    }
}

五、弹框AudioRecoderDialog

public class AudioRecoderDialog extends BasePopupWindow {

    private ImageView imageView;
    private TextView textView;

    public AudioRecoderDialog(Context context) {
        super(context);
        View contentView = LayoutInflater.from(context).inflate(R.layout.layout_recoder_dialog, null);
        imageView = (ImageView) contentView.findViewById(android.R.id.progress);
        textView = (TextView) contentView.findViewById(android.R.id.text1);
        setContentView(contentView);
    }

    public void setLevel(int level) {
        Drawable drawable = imageView.getDrawable();
        drawable.setLevel(3000 + 6000 * level / 100);
    }

    public void setTime(long time) {
        textView.setText(getProgressText(time));
    }

    @Override
    public void dismiss() {
        super.dismiss();
        setTime(0L);
    }

    /**
     * 设置时间格式(00:00)
     * @param time
     * @return
     */
    public static String getProgressText(long time) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date(time));
        double minute = calendar.get(Calendar.MINUTE);
        double second = calendar.get(Calendar.SECOND);

        DecimalFormat format = new DecimalFormat("00");
        return format.format(minute) + ":" + format.format(second);
    }
}
六、弹框基类: BasePopupWindow
public class BasePopupWindow extends PopupWindow {

    private Context mContext;
    private float mShowAlpha = 0.88f;
    private Drawable mBackgroundDrawable;

    @Override
    public int getAnimationStyle() {
        return super.getAnimationStyle();
    }

    public BasePopupWindow(Context context) {
        this.mContext = context;
        initBasePopupWindow();
    }

    @Override
    public void setOutsideTouchable(boolean touchable) {
        super.setOutsideTouchable(touchable);
        if(touchable) {
            if(mBackgroundDrawable == null) {
                mBackgroundDrawable = new ColorDrawable(0x00000000);
            }
            super.setBackgroundDrawable(mBackgroundDrawable);
        } else {
            super.setBackgroundDrawable(null);
        }
    }

    @Override
    public void setBackgroundDrawable(Drawable background) {
        mBackgroundDrawable = background;
        setOutsideTouchable(isOutsideTouchable());
    }

    /**
     * 初始化BasePopupWindow的一些信息
     * */
    private void initBasePopupWindow() {
        setAnimationStyle(android.R.style.Animation_Dialog);
        setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
        setOutsideTouchable(false);  //默认设置outside点击无响应
        setFocusable(true);
    }

    @Override
    public void setContentView(View contentView) {
        if(contentView != null) {
            contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
            super.setContentView(contentView);
            addKeyListener(contentView);
        }
    }

    public Context getContext() {
        return mContext;
    }

    @Override
    public void showAtLocation(View parent, int gravity, int x, int y) {
        super.showAtLocation(parent, gravity, x, y);
        ValueAnimator animator = showAnimator();
        if(animator != null) {
            animator.start();
        }
    }

    @Override
    public void showAsDropDown(View anchor) {
        super.showAsDropDown(anchor);
        ValueAnimator animator = showAnimator();
        if(animator != null) {
            animator.start();
        }
    }

    @Override
    public void showAsDropDown(View anchor, int xoff, int yoff) {
        super.showAsDropDown(anchor, xoff, yoff);
        ValueAnimator animator = showAnimator();
        if(animator != null) {
            animator.start();
        }
    }

    @Override
    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        super.showAsDropDown(anchor, xoff, yoff, gravity);
        ValueAnimator animator = showAnimator();
        if(animator != null) {
            animator.start();
        }
    }

    @Override
    public void dismiss() {
        super.dismiss();
        ValueAnimator animator = dismissAnimator();
        if(animator != null) {
            animator.start();
        }
    }

    public void setShowAlpha(float alpha) {
        this.mShowAlpha = alpha;
    }

    /**
     * 窗口显示,窗口背景透明度渐变动画
     * */
    private ValueAnimator showAnimator() {
        if(mShowAlpha != 1.0f) {
            ValueAnimator animator = ValueAnimator.ofFloat(1.0f, mShowAlpha);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float alpha = (float) animation.getAnimatedValue();
                    setWindowBackgroundAlpha(alpha);
                }
            });
            animator.setDuration(360);
            return animator;
        } else {
            return null;
        }
    }

    /**
     * 窗口隐藏,窗口背景透明度渐变动画
     * */
    private ValueAnimator dismissAnimator() {
        if(mShowAlpha != 1.0f) {
            ValueAnimator animator = ValueAnimator.ofFloat(mShowAlpha, 1.0f);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float alpha = (float) animation.getAnimatedValue();
                    setWindowBackgroundAlpha(alpha);
                }
            });
            animator.setDuration(320);
            return animator;
        } else {
            return null;
        }
    }

    /**
     * 为窗体添加outside点击事件
     * */
    private void addKeyListener(View contentView) {
        if(contentView != null) {
            contentView.setFocusable(true);
            contentView.setFocusableInTouchMode(true);
            contentView.setOnKeyListener(new View.OnKeyListener() {

                @Override
                public boolean onKey(View view, int keyCode, KeyEvent event) {
                    switch (keyCode) {
                        case KeyEvent.KEYCODE_BACK:
                            dismiss();
                            return true;
                        default:
                            break;
                    }
                    return false;
                }
            });
        }
    }

    /**
     * 控制窗口背景的不透明度
     * */
    private void setWindowBackgroundAlpha(float alpha) {
        Window window = ((Activity)getContext()).getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.alpha = alpha;
        window.setAttributes(layoutParams);
    }
}

七、弹框布局:layout_recoder_dialog

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:background="@drawable/shape_window_background"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="16dp" >

        <ImageView
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:id="@android:id/progress"
            android:src="@drawable/layer_recording_animation" />

        <TextView
            android:layout_height="wrap_content"
            android:layout_marginTop="6dp"
            android:id="@android:id/text1"
            android:layout_width="114dp"
            android:textColor="#FFFFFF"
            android:gravity="center"
            android:textSize="16sp"
            android:text="00:00" />
    </LinearLayout>
</LinearLayout>

八、drawable下相关资源

shape_window_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#553e3c3c" />
    <corners android:radius="6dp" />
</shape>
layer_recording_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@android:id/background" android:drawable="@mipmap/icon_microphone_normal" />
    <item android:id="@android:id/progress" >
        <clip android:drawable="@mipmap/icon_microphone_recoding" android:gravity="bottom" android:clipOrientation="vertical" />
    </item>
</layer-list>
selector_recodering_btn.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@drawable/shape_recoder_btn_recoding"/>
    <item android:state_pressed="false" android:drawable="@drawable/shape_recoder_btn_normal"/>
</selector>
shape_recoder_btn_recoding.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="6dp" />
    <solid android:color="#A9B7B7" />
</shape>
shape_recoder_btn_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="6dp" />
    <solid android:color="#00BB9C" />
</shape>

九、strings.xml资源文件

<resources>
<string name="start_recording">开始录音</string>
    <string name="recording">录音中...</string>
    <string name="record_short_tips">录音太短,至少时长为2秒!</string>
</resources>

十、图片资源

                                                 

icon_microphone_normal.png                                         icon_microphone_recoding.png

相关问题总结:

1、点击录音按钮出现崩溃:因为点击事件短暂,MediaRecorder对象未初始化完毕就执行stop(),可通过try...catch捕获异常解决。

2、第一次录音时,出现权限弹框。当点击同意时(手指已经离开屏幕),录音继续进行的Bug。可以通过动态获取权限解决此问题(此处未上传,动态获取权限可以看其他大神文章)。








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值