Android简易“吹一吹实现”以及录音和播放示例

简介:

最近在做一些跟传感器相关的东西,有注意到以前腾讯微博以前出过一个吹一吹交互,虽然和传感器无关,但是感觉也比较有兴趣,就写了一个拙劣的demo,因为接触媒体文件操作比较少,顺带写了一个录音和播放的例子,总结了一下一些小坑的地方,一并在此分享给大家。

主要思路和坑的地方

主要的思路是通过MediaRecorder提供的getMaxAmplitude()函数,获取一段时间内输入的音频最大幅值来进行检测,所以除了吹的动作,其他声音也会被录进来。

“吹”这个动作如果想和其他动作进行区分,其实本质在于吹的时候靠近听筒,即便吹这个动作本身音量不大,但是麦克风看来它的分贝是很大的,所以我们可以通过检测分贝来判断这个动作是否是吹(如果其他声音更大……那……算了不管了)。

这里附上前人的一些参考资料:

http://www.jb51.net/article/6...

一看到这个网站后面是htm,仿佛就明白了这个网站的框架…

这个东西坑的地方在于Mediaplayer和MediaRecorder这两个东西stop和start的顺序经常是严格被限制的,在退出时如果没有成功释放资源,有时候Activity再启动时,由于上次退出没有stop,再重新start也会抛出异常。

权限添加

 
 
  1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
  3. <uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/> 
  4. <uses-permission android:name="android.permission.RECORD_AUDIO" /> 

主要界面

大概想了一个简单的界面,好吧其实是左下角的音响闪动,忘记修改文字描述了

布局文件:

 
 
  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  3. xmlns:tools="http://schemas.android.com/tools" 
  4. android:id="@+id/activity_sound" 
  5. android:layout_width="match_parent" 
  6. android:layout_height="match_parent" 
  7. android:paddingBottom="@dimen/activity_vertical_margin" 
  8. android:paddingLeft="@dimen/activity_horizontal_margin" 
  9. android:paddingRight="@dimen/activity_horizontal_margin" 
  10. android:paddingTop="@dimen/activity_vertical_margin" 
  11. tools:context=".SoundActivity"
  12. <TextView 
  13.     android:layout_width="wrap_content" 
  14.     android:layout_height="wrap_content" 
  15.     android:text="@string/introduction_of_sound"/> 
  16. <LinearLayout 
  17.     android:layout_width="match_parent" 
  18.     android:layout_height="wrap_content" 
  19.     android:orientation="vertical" 
  20.     android:gravity="center" 
  21.     android:layout_centerInParent="true"
  22.  
  23.     <Button 
  24.         android:layout_width="70dp" 
  25.         android:layout_height="70dp" 
  26.         android:id="@+id/btn_start_record" 
  27.         android:background="@drawable/ic_mic_none_black_24dp" 
  28.         /> 
  29.     <TextView 
  30.         android:layout_width="wrap_content" 
  31.         android:layout_height="wrap_content" 
  32.         android:id="@+id/tv_record_tips"/> 
  33.  
  34.     <Button 
  35.         android:layout_width="70dp" 
  36.         android:layout_height="70dp" 
  37.         android:id="@+id/btn_start_play" 
  38.         android:background="@drawable/ic_play_circle_filled_black_24dp" 
  39.         /> 
  40. </LinearLayout> 
  41. <ImageView 
  42.     android:layout_width="50dp" 
  43.     android:layout_height="50dp" 
  44.     android:src="@drawable/ic_volume_mute_gray_24dp" 
  45.     android:layout_alignParentBottom="true" 
  46.     android:id="@+id/imv_sound"/> 
  47. </RelativeLayout>  

主要代码

 
 
  1. import android.app.ProgressDialog; 
  2. import android.media.AudioManager; 
  3. import android.media.MediaPlayer; 
  4. import android.media.MediaRecorder; 
  5. import android.os.Environment; 
  6. import android.os.Handler; 
  7. import android.os.Message; 
  8. import android.support.v7.app.AppCompatActivity; 
  9. import android.os.Bundle; 
  10. import android.view.View
  11. import android.widget.Button; 
  12. import android.widget.ImageView; 
  13. import android.widget.TextView; 
  14. import android.widget.Toast; 
  15.  
  16. import java.io.IOException; 
  17. import java.util.Timer; 
  18. import java.util.TimerTask; 
  19.  
  20.  
  21. public class SoundActivity extends AppCompatActivity { 
  22.     static final int RECORDING = 1; 
  23.     static final int PLAYING = 2; 
  24.     static final int PAUSING = 3; 
  25.     static String TAG = "SoundActivity"
  26.     static int STATUS = RECORDING; 
  27.     //用于音频录制 
  28.     MediaRecorder mediaRecorder; 
  29.     //用于音频播放 
  30.     MediaPlayer mediaPlayer; 
  31.  
  32.     //录制按钮 
  33.     Button btnRecord; 
  34.     //播放按钮 
  35.     Button btnPlay; 
  36.     //提示信息 
  37.     TextView tvTips; 
  38.     //吹一吹小音箱 
  39.     ImageView imvSound; 
  40.     //播放进度条 
  41.     static String PATH_NAME = Environment.getExternalStorageDirectory().getAbsolutePath() + "/SensorDemoRecorder.mp3"
  42.     @Override 
  43.     protected void onCreate(Bundle savedInstanceState) { 
  44.         super.onCreate(savedInstanceState); 
  45.         setContentView(R.layout.activity_sound); 
  46.         init(); 
  47.     } 
  48.  
  49.     public void init(){ 
  50.         //控件初始化 
  51.         btnPlay = (Button)findViewById(R.id.btn_start_play); 
  52.         btnRecord = (Button)findViewById(R.id.btn_start_record); 
  53.         tvTips = (TextView)findViewById(R.id.tv_record_tips); 
  54.         imvSound = (ImageView)findViewById(R.id.imv_sound); 
  55.         mediaplayerPreparingDialog = new ProgressDialog(this); 
  56.         btnPlay.setOnClickListener(new View.OnClickListener() { 
  57.             @Override 
  58.             public void onClick(View v) { 
  59.                 if (STATUS == RECORDING ){ 
  60.                     //如果是在录制,点击则停止录制并且播放 
  61.                     stopRecording(); 
  62.                     startPlay(); 
  63.                 }else if (STATUS == PAUSING){ 
  64.                     startPlay(); 
  65.                 } else { 
  66.                     //如果是在播放,点击则暂停 
  67.                     pausePlay(); 
  68.                 } 
  69.             } 
  70.         }); 
  71.         btnRecord.setOnClickListener(new View.OnClickListener() { 
  72.             @Override 
  73.             public void onClick(View v) { 
  74.                 if (STATUS == PLAYING || STATUS == PAUSING){ 
  75.                     //如果是在播放或者暂停,点击开始录制 
  76.                     startRecording(); 
  77.                 }else { 
  78.                     //如果在录制,点击开始播放 
  79.                     stopRecording(); 
  80.                     startPlay(); 
  81.                 } 
  82.             } 
  83.         }); 
  84.  
  85.         mediaRecorder = new MediaRecorder(); 
  86.         //设置到达最大录制长度时重头开始录制 
  87.         mediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { 
  88.             @Override 
  89.             public void onInfo(MediaRecorder mr, int what, int extra) { 
  90.                 switch (what){ 
  91.                     case MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN: 
  92.                         Toast.makeText(SoundActivity.this, "未知错误", Toast.LENGTH_SHORT).show(); 
  93.                         finish(); 
  94.                         break; 
  95.                     case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED: 
  96.                         Toast.makeText(SoundActivity.this, "已达到最大录制长度,开始重新录制", Toast.LENGTH_SHORT).show( ); 
  97.                         startRecording(); 
  98.                         break; 
  99.                     case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED: 
  100.                         Toast.makeText(SoundActivity.this, "空间不足,无法录制", Toast.LENGTH_SHORT).show(); 
  101.                         mediaRecorder.stop(); 
  102.                         break; 
  103.  
  104.                 } 
  105.             } 
  106.         }); 
  107.         //默认开始录制 
  108.         startRecording(); 
  109.         btnRecord.setBackgroundResource(R.drawable.ic_mic_black_24dp); 
  110.         //默认开始吹一吹检测以及播放进度检测 
  111.         startCheckSound(); 
  112.     } 
  113.  
  114.  
  115.     @Override 
  116.     protected void onDestroy() { 
  117.         super.onDestroy(); 
  118.         if (PLAYING == STATUS){ 
  119.             mediaPlayer.stop(); 
  120.             mediaPlayer.release(); 
  121.         } 
  122.         if (RECORDING == STATUS){ 
  123.             mediaRecorder.stop(); 
  124.             mediaRecorder.release(); 
  125.         } 
  126.         //为了防止Activity结束后有时候这个timer还在定时执行任务(很坑) 
  127.         timer.cancel(); 
  128.     } 
  129.  
  130.     public void startRecording(){ 
  131.         if (PLAYING == STATUS){ 
  132.             stopPlay(); 
  133.         } 
  134.         STATUS = RECORDING; 
  135.         //设置为录制状态 
  136.         tvTips.setText("正在录制,点击播放按钮或者麦克风停止录制"); 
  137.         btnRecord.setBackgroundResource(R.drawable.ic_mic_black_24dp); 
  138.  
  139.         //开始录制的设置 
  140.         mediaRecorder.reset();  // You can reuse the object by going back to setAudioSource() step 
  141.         mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 
  142.         mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 
  143.         mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 
  144.         try{ 
  145.             mediaRecorder.setOutputFile(PATH_NAME); 
  146.             mediaRecorder.prepare(); 
  147.             mediaRecorder.start();   // Recording is now started 
  148.         }catch (IOException e){ 
  149.             Toast.makeText(this, "准备录制文件失败", Toast.LENGTH_SHORT).show(); 
  150.             e.printStackTrace(); 
  151.             finish(); 
  152.         } 
  153.     } 
  154.  
  155.     public void stopRecording(){ 
  156.         if (RECORDING == STATUS){ 
  157.             //说明正在录制,设置停止信息 
  158.             tvTips.setText("已停止录制,开始播放"); 
  159.             btnRecord.setBackgroundResource(R.drawable.ic_mic_none_black_24dp); 
  160.             mediaRecorder.stop(); 
  161.         } 
  162.     } 
  163.  
  164.  
  165.  
  166.     Handler handler= new Handler(new Handler.Callback() { 
  167.         @Override 
  168.         public boolean handleMessage(Message msg) { 
  169.             if ((Double)msg.obj > 70){ 
  170.                 imvSound.setImageResource(R.drawable.ic_volume_mute_valid_24dp); 
  171.             }else { 
  172.                 imvSound.setImageResource(R.drawable.ic_volume_mute_gray_24dp); 
  173.             } 
  174.             return false
  175.         } 
  176.     }); 
  177.  
  178.     Timer timer = new Timer(); 
  179.     public void startCheckSound(){ 
  180.         //定时检测峰值,以及检测播放进度 
  181.         timer.schedule(new TimerTask() { 
  182.             @Override 
  183.             public void run() { 
  184.                 if (mediaRecorder != null) { 
  185.                     double amplitude = (double)mediaRecorder.getMaxAmplitude(); 
  186.                     double db = 0; 
  187.                     //计算分贝 
  188.                     if (amplitude > 1) 
  189.                         db = 20 * Math.log10(amplitude); 
  190.                     Message msg = new Message(); 
  191.                     msg.obj = db; 
  192.                     handler.sendMessage(msg); 
  193.                     //如果需要检测播放进度可以使用 
  194.                     //mediaPlayer.getCurrentPosition()/mediaPlayer.getDuration(); 
  195.                 } 
  196.             } 
  197.         },0,100); 
  198.     } 
  199.  
  200.  
  201.     ProgressDialog mediaplayerPreparingDialog; 
  202.     public void startPlay(){ 
  203.         if (RECORDING == STATUS){ 
  204.             //如果是从录制状态开始播放,则重新读取新的录制文件 
  205.             STATUS = PLAYING; 
  206.             //设置音频播放器 
  207.             mediaPlayer = new MediaPlayer(); 
  208.             mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 
  209.             mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 
  210.                 @Override 
  211.                 public void onCompletion(MediaPlayer mp) { 
  212.                     //播放完设置 
  213.                     tvTips.setText("播放完毕,可点击麦克风重新录制"); 
  214.                     btnPlay.setBackgroundResource(R.drawable.ic_play_circle_filled_black_24dp); 
  215.                 } 
  216.             }); 
  217.             try { 
  218.                 mediaPlayer.setDataSource(PATH_NAME); 
  219.                 mediaPlayer.prepareAsync(); 
  220.             } catch (IOException e) { 
  221.                 e.printStackTrace(); 
  222.                 Toast.makeText(this, "录音文件已丢失", Toast.LENGTH_SHORT).show(); 
  223.                 finish(); 
  224.             } 
  225.             mediaplayerPreparingDialog.setTitle("正在准备播放录音"); 
  226.             mediaplayerPreparingDialog.show(); 
  227.  
  228.             mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 
  229.                 @Override 
  230.                 public void onPrepared(MediaPlayer mp) { 
  231.                     mediaplayerPreparingDialog.dismiss(); 
  232.                     mediaPlayer.start(); 
  233.                 } 
  234.             }); 
  235.  
  236.         }else if(PAUSING == STATUS){ 
  237.             //从暂停状态开始播放则直接播放 
  238.             mediaPlayer.start(); 
  239.         } 
  240.         //开始播放,设置按钮为暂停 
  241.         btnPlay.setBackgroundResource(R.drawable.ic_pause_circle_filled_black_24dp); 
  242.  
  243.     } 
  244.  
  245.     public void pausePlay(){ 
  246.         if (PLAYING == STATUS) { 
  247.             //暂停播放,设置按钮为开始播放 
  248.             mediaPlayer.pause(); 
  249.             btnPlay.setBackgroundResource(R.drawable.ic_play_circle_filled_black_24dp); 
  250.             STATUS = PAUSING; 
  251.         } 
  252.     } 
  253.  
  254.     public void stopPlay(){ 
  255.         if (mediaPlayer != null) mediaPlayer.stop(); 
  256.     } 
  257.  

Media和IllegalStateException

这个就是之前提到的由于没有按顺序释放资源或者stop掉这两个破玩意儿,可能会导致的各种错误,所以我很无奈地设置了一个STATUS变量,并且在Activity的OnDestoy里对两个东西进行了stop,其实一般还会使用release释放掉资源…大家随意吧…

QCMediaPlayer mediaplayer NOT present

!!!我就知道,如果你看到这个地方,一定也对这个错误感到莫名其妙。我记得好像上古时期,也就是上次我写这个的时候也被坑了。

论坛上有人说这个东西在4.4以下的系统就容易出现,但是我也只能感觉不明觉厉,我一开始用的是MediaPlayer.create(this,Uri.parse(PATH_NAME))来创建MediaPlayer,于是换成了

 
 
  1. mediaPlayer = new MediaPlayer(); 
  2. mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 
  3. mediaPlayer.setDataSource(PATH_NAME);  

好吧,然后问题就解决了,我也是无语了。我觉得这个地方是一个很久远的坑了,查原因一时也没查到。我只能推测大概因为create函数创建时没有指定AudioStreamType导致使用了默认的

 
 
  1. private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; 

在某些设备上可能不支持,于是就出了问题= =好吧,我也不知道还能说啥,就酱…

Vector Asset添加的图标颜色不变化

如上,我的播放按钮啊,音响啊,之类的图标都是通过Vector Asset添加的,这也是一个比较久远的坑了,但是以前也没有记下来,即在Android L以下的版本中,Vector Asset添加的图标,修改颜色时不能使用颜色的引用,而要直接写颜色,例如:

 
 
  1. <vector xmlns:android="http://schemas.android.com/apk/res/android" 
  2.     android:width="24dp" 
  3.     android:height="24dp" 
  4.     android:viewportWidth="24.0" 
  5.     android:viewportHeight="24.0"
  6. <path 
  7.     android:fillColor="#3F51B5" 
  8.     android:pathData="***"/> 
  9. </vector>  

使用

 
 
  1. <vector xmlns:android="http://schemas.android.com/apk/res/android" 
  2.     android:width="24dp" 
  3.     android:height="24dp" 
  4.     android:viewportWidth="24.0" 
  5.     android:viewportHeight="24.0"
  6. <path 
  7.     android:fillColor="@color/colorPrimary" 
  8.     android:pathData="***"/> 
  9. </vector>  

则导致颜色并不会修改,依然是黑色



作者:两岸风景
来源:51CTO
目录
相关文章
|
3月前
|
存储 算法 开发工具
OpenCV 安卓编程示例:1~6 全
OpenCV 安卓编程示例:1~6 全
|
8月前
|
编解码 Android开发
Android 使用VideoView播放本地视频详解
Android 使用VideoView播放本地视频详解
301 2
|
7月前
|
编解码 Android开发 数据安全/隐私保护
Android平台外部编码数据(H264/H265/AAC/PCMA/PCMU)实时预览播放技术实现
好多开发者可能疑惑,外部数据实时预览播放,到底有什么用? 是的,一般场景是用不到的,我们在开发这块前几年已经开发了非常稳定的RTMP、RTSP直播播放模块,不过也遇到这样的场景,部分设备输出编码后(视频:H.264/H.265,音频:AAC/PCMA/PCMU)的数据,比如无人机或部分智能硬件设备,回调出来的H.264/H.265数据,除了想转推到RTMP、轻量级RTSP服务或GB28181外,还需要本地预览甚至对数据做二次处理(视频分析、实时水印字符叠加等,然后二次编码),基于这样的场景诉求,我们开发了Android平台外部编码数据实时预览播放模块。
|
4月前
|
XML Java Android开发
Android Studio App开发中使用录音机、MediaRecorder录制音频和MediaPlayer播放音频讲解及实战(附源码)
Android Studio App开发中使用录音机、MediaRecorder录制音频和MediaPlayer播放音频讲解及实战(附源码)
72 0
|
7月前
|
编解码 开发工具 Android开发
安卓端/iOS端如何播放4K分辨率的RTMP/RTSP流
4K分辨率即4096×2160的像素分辨率,它是2K投影机和高清电视分辨率的4倍,属于超高清分辨率。在此分辨率下,观众将可以看清画面中的每一个细节,每一个特写。影院如果采用惊人的4096×2160像素,无论在影院的哪个位置,观众都可以清楚的看到画面的每一个细节,影片色彩鲜艳、文字清晰锐丽,再配合超真实音效,这种感觉真的是一种难以言传的享受。
246 0
安卓端/iOS端如何播放4K分辨率的RTMP/RTSP流
|
8月前
|
编解码 开发工具 Android开发
Android平台如何实现第三方模块编码后(H.264/H.265/AAC/PCMA/PCMU)数据实时预览播放
Android平台如何实现第三方模块编码后(H.264/H.265/AAC/PCMA/PCMU)数据实时预览播放
|
4月前
|
XML Java 调度
Android开发音效增强中铃声播放Ringtone及声音池调度SoundPool的讲解及实战(超详细 附源码)
Android开发音效增强中铃声播放Ringtone及声音池调度SoundPool的讲解及实战(超详细 附源码)
48 0
|
4月前
|
XML Java Android开发
Android App开发实战项目之仿手机QQ动感影集动画播放(附源码和演示视频 可直接使用)
Android App开发实战项目之仿手机QQ动感影集动画播放(附源码和演示视频 可直接使用)
28 0
|
4月前
|
XML 算法 Java
Android Studio App开发之利用图像解码器ImageDecoder播放GIF动图、Webp、HEIF图片(附源码 简单实用)
Android Studio App开发之利用图像解码器ImageDecoder播放GIF动图、Webp、HEIF图片(附源码 简单实用)
105 0
|
7月前
|
编解码 Android开发 数据安全/隐私保护
Android平台如何实现外部编码后(H.264/H.265)数据实时预览播放
我们在对接开发者的时候,遇到这样的诉求:除了正常的RTMP、RTSP直播播放外,有些硬件设备输出编码后(H.264/H.265)的数据,比如无人机或类似硬件产品,回调出来的H.264/H.265数据,除了正常转推到RTMP、轻量级RTSP服务或GB28181外,还需要本地预览甚至重新对数据做二次处理,基于这样的场景诉求,我们开发了外部编码后数据实时预览播放模块。