用ClipDrawable实现音频录制麦克风讲话效果

麦克风

由于最近项目开发需要用到自定义SeekBar,于是又对android下的各种类型drawable进行了一个全面系统的认识,只能感慨drawable的功能还是很强大的。通过自定义SeekBar有感而发,尝试用ClipDrawable实现音频录制过程的一个麦克风录制效果:

在实现开发之前先让我们一起认识两种类型的Drawable:

LayerDrawable

LayerDrawable可以包含一个drawable数组,而且系统会根据drawable数组的前后顺序来绘制所有的drawable,索引最大的drawable也就相应的会被绘制在最上面。使用过PhotoShop的朋友应该会比较容易理解,LayerDrawable和PhotoShop中图层的概念很相似,这里drawable数组中的每一个drawable就相当于PhotoShop中的一个图层,上一个图层会遮住之后所有图层与之重叠的部分。

定义LayerDrawable对象的XML文件的根元素为<layer-list… />,该元素可以包含对个<item… />元素也就是一个drawable对象。

ClipDrawable

ClipDrawable,顾名思义这就是一个可以进行裁切的drawable,在XML文件中定义ClipDrawable对象使用的根元素是<clip… />元素,该元素包含以下几个重要的属性:

  • android:drawable:指定将要被截取的Drawable对象。
  • android:clipOrientation:指定Drawable对象的截取方向可以是水平和竖直方向。
  • android:gravity:表示Drawable对象的对齐方式,例如:left 可以理解为左边部分为保留部分,右边部分为剪切部分,则我们可以看到的就是截取的左边部分。

注意,使用ClipDrawable对象时可以调用setLevel(int level)方法来控制截取区域的大小,而level的取值区间在0~10000之间,则level为0时,表示图片截取部分为空,当了level为10000时,截取整张图片。

了解完毕,下面我们就要用这两种Drawable结合使用开发我们今天的麦克风说话效果:

首先,准备两张位图

top drawable
bottom drawable

然后,在XML中新建一个拥有两个Drawable的LayerDrawable文件layer-microphone.xml,在顶层显示的是可以裁切的ClipDrawable,设置剪切方向为竖直方向,设置对其方式为bottom,底部的则是不通的Drawable作为背景。

<?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="@drawable/icon_microphone_normal" />
    <item android:id="@android:id/progress" >
        <clip android:drawable="@drawable/icon_microphone_recoding" android:gravity="bottom" android:clipOrientation="vertical" />
    </item>
</layer-list>

然后,再自定义一个PopupWindow用于音频录制过程显示麦克风动画效果,关于自定义popupWindow有疑问的朋友可以参考我的上一篇文章"2016-05-10 浅谈PopupWindow在Android开发中的使用"

窗口布局如下layout_microphone.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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-microphone" />

        <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>

从代码中可以看出,我把ImageVie的资源设为layer-microphone.xml,我们获取ImageView的Drawable对象并设置Level值就能实现想要的效果:

imageView.getDrawable().setLevel(5400);
icon_microphone_recoding11.png

从这里我们已经可以看到裁切效果。最后一步,我们只要能够实时获取音频录制过程中的分贝值的变化,再将分贝值变化转换到相应的Level值,就能实现音频录制说话效果啦,于是百度,在网上看到一篇文章"Android中实时获取音量分贝值详解" ,首先,感谢作者的分享,于是我也就照着方法写了一个AudioRecoderUtil.java类,稍加改进,添加了一个监听事件,代码如下:

package com.mariostudio.audiorecoder;

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

import android.media.MediaRecorder;
import android.os.Handler;
import android.util.Log;

/**
 * Created by MarioStudio on 2016/5/12.
 */

public class AudioRecoderUtils {

    private String filePath;
    private MediaRecorder mMediaRecorder;
    private final String TAG = "MediaRecord";
    public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大录音时长1000*60*10;

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    public AudioRecoderUtils(){
        this.filePath = "/dev/null";
    }

    public AudioRecoderUtils(File file) {
        this.filePath = file.getAbsolutePath();
    }

    private long startTime;
    private long endTime;

    /**
     * 开始录音 使用amr格式
     *      录音文件
     * @return
     */
    public void startRecord() {
        // 开始录音
        /* ①Initial:实例化MediaRecorder对象 */
        if (mMediaRecorder == null)
            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();
            // 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 (mMediaRecorder == null)
            return 0L;
        endTime = System.currentTimeMillis();
        Log.i("ACTION_END", "endTime" + endTime);
        mMediaRecorder.stop();
        mMediaRecorder.reset();
        mMediaRecorder.release();
        mMediaRecorder = null;
        Log.i("ACTION_LENGTH", "Time" + (endTime - startTime));
        return endTime - startTime;
    }

    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);
    }
}

进行到这一步,已经基本完成了这个效果,最后只需要在自定义PopupWindow的时候提供方法setLevel(int level)就可以轻松实现PopupWindow实时刷新分贝值啦!!

public void setLevel(int level) {
    imageView.getDrawable().setLevel(3000 + 6000 * level / 100);
}
动态效果图

至于为什么设置level的时候要3000 + 6000 * level / 100以及计时效果,就都留给聪明的你去探索咯!!

Github项目Demo地址

作者申明:如果文中有表述不当或阐述错误的地方,还望正在看文章的您可以帮忙指出,有疑惑也可以在评论区提问或者私信,期待您的意见和建议,欢迎关注交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容