Android簡易“吹一吹實現”以及錄音和播放示例

Faustino867 8年前發布 | 11K 次閱讀 安卓開發 Android開發 移動開發

最近在做一些跟傳感器相關的東西,有注意到以前騰訊微博以前出過一個吹一吹交互,雖然和傳感器無關,但是感覺也比較有興趣,就寫了一個拙劣的demo,因為接觸媒體文件操作比較少,順帶寫了一個錄音和播放的例子,總結了一下一些小坑的地方,一并在此分享給大家。

主要思路和坑的地方

主要的思路是通過MediaRecorder提供的getMaxAmplitude()函數,獲取一段時間內輸入的音頻最大幅值來進行檢測,所以除了吹的動作,其他聲音也會被錄進來。

“吹”這個動作如果想和其他動作進行區分,其實本質在于吹的時候靠近聽筒,即便吹這個動作本身音量不大,但是麥克風看來它的分貝是很大的,所以我們可以通過檢測分貝來判斷這個動作是否是吹(如果其他聲音更大……那……算了不管了)。

一看到這個網站后面是htm,仿佛就明白了這個網站的框架…

這個東西坑的地方在于Mediaplayer和MediaRecorder這兩個東西stop和start的順序經常是嚴格被限制的,在退出時如果沒有成功釋放資源,有時候Activity再啟動時,由于上次退出沒有stop,再重新start也會拋出異常。

權限添加

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

主要界面

大概想了一個簡單的界面,好吧其實是左下角的音響閃動,忘記修改文字描述了

布局文件:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:id="@+id/activity_sound" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:paddingBottom="@dimen/activity_vertical_margin" 
android:paddingLeft="@dimen/activity_horizontal_margin" 
android:paddingRight="@dimen/activity_horizontal_margin" 
android:paddingTop="@dimen/activity_vertical_margin" 
tools:context=".SoundActivity"> 
<TextView 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="@string/introduction_of_sound"/> 
<LinearLayout 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:orientation="vertical" 
    android:gravity="center" 
    android:layout_centerInParent="true"> 
 
    <Button 
        android:layout_width="70dp" 
        android:layout_height="70dp" 
        android:id="@+id/btn_start_record" 
        android:background="@drawable/ic_mic_none_black_24dp" 
        /> 
    <TextView 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:id="@+id/tv_record_tips"/> 
 
    <Button 
        android:layout_width="70dp" 
        android:layout_height="70dp" 
        android:id="@+id/btn_start_play" 
        android:background="@drawable/ic_play_circle_filled_black_24dp" 
        /> 
</LinearLayout> 
<ImageView 
    android:layout_width="50dp" 
    android:layout_height="50dp" 
    android:src="@drawable/ic_volume_mute_gray_24dp" 
    android:layout_alignParentBottom="true" 
    android:id="@+id/imv_sound"/> 
</RelativeLayout>  

主要代碼

import android.app.ProgressDialog; 
import android.media.AudioManager; 
import android.media.MediaPlayer; 
import android.media.MediaRecorder; 
import android.os.Environment; 
import android.os.Handler; 
import android.os.Message; 
import android.support.v7.app.AppCompatActivity; 
import android.os.Bundle; 
import android.view.View; 
import android.widget.Button; 
import android.widget.ImageView; 
import android.widget.TextView; 
import android.widget.Toast; 
 
import java.io.IOException; 
import java.util.Timer; 
import java.util.TimerTask; 
 
 
public class SoundActivity extends AppCompatActivity { 
    static final int RECORDING = 1; 
    static final int PLAYING = 2; 
    static final int PAUSING = 3; 
    static String TAG = "SoundActivity"; 
    static int STATUS = RECORDING; 
    //用于音頻錄制 
    MediaRecorder mediaRecorder; 
    //用于音頻播放 
    MediaPlayer mediaPlayer; 
 
    //錄制按鈕 
    Button btnRecord; 
    //播放按鈕 
    Button btnPlay; 
    //提示信息 
    TextView tvTips; 
    //吹一吹小音箱 
    ImageView imvSound; 
    //播放進度條 
    static String PATH_NAME = Environment.getExternalStorageDirectory().getAbsolutePath() + "/SensorDemoRecorder.mp3"; 
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_sound); 
        init(); 
    } 
 
    public void init(){ 
        //控件初始化 
        btnPlay = (Button)findViewById(R.id.btn_start_play); 
        btnRecord = (Button)findViewById(R.id.btn_start_record); 
        tvTips = (TextView)findViewById(R.id.tv_record_tips); 
        imvSound = (ImageView)findViewById(R.id.imv_sound); 
        mediaplayerPreparingDialog = new ProgressDialog(this); 
        btnPlay.setOnClickListener(new View.OnClickListener() { 
            @Override 
            public void onClick(View v) { 
                if (STATUS == RECORDING ){ 
                    //如果是在錄制,點擊則停止錄制并且播放 
                    stopRecording(); 
                    startPlay(); 
                }else if (STATUS == PAUSING){ 
                    startPlay(); 
                } else { 
                    //如果是在播放,點擊則暫停 
                    pausePlay(); 
                } 
            } 
        }); 
        btnRecord.setOnClickListener(new View.OnClickListener() { 
            @Override 
            public void onClick(View v) { 
                if (STATUS == PLAYING || STATUS == PAUSING){ 
                    //如果是在播放或者暫停,點擊開始錄制 
                    startRecording(); 
                }else { 
                    //如果在錄制,點擊開始播放 
                    stopRecording(); 
                    startPlay(); 
                } 
            } 
        }); 
 
        mediaRecorder = new MediaRecorder(); 
        //設置到達最大錄制長度時重頭開始錄制 
        mediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { 
            @Override 
            public void onInfo(MediaRecorder mr, int what, int extra) { 
                switch (what){ 
                    case MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN: 
                        Toast.makeText(SoundActivity.this, "未知錯誤", Toast.LENGTH_SHORT).show(); 
                        finish(); 
                        break; 
                    case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED: 
                        Toast.makeText(SoundActivity.this, "已達到最大錄制長度,開始重新錄制", Toast.LENGTH_SHORT).show( ); 
                        startRecording(); 
                        break; 
                    case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED: 
                        Toast.makeText(SoundActivity.this, "空間不足,無法錄制", Toast.LENGTH_SHORT).show(); 
                        mediaRecorder.stop(); 
                        break; 
 
                } 
            } 
        }); 
        //默認開始錄制 
        startRecording(); 
        btnRecord.setBackgroundResource(R.drawable.ic_mic_black_24dp); 
        //默認開始吹一吹檢測以及播放進度檢測 
        startCheckSound(); 
    } 
 
 
    @Override 
    protected void onDestroy() { 
        super.onDestroy(); 
        if (PLAYING == STATUS){ 
            mediaPlayer.stop(); 
            mediaPlayer.release(); 
        } 
        if (RECORDING == STATUS){ 
            mediaRecorder.stop(); 
            mediaRecorder.release(); 
        } 
        //為了防止Activity結束后有時候這個timer還在定時執行任務(很坑) 
        timer.cancel(); 
    } 
 
    public void startRecording(){ 
        if (PLAYING == STATUS){ 
            stopPlay(); 
        } 
        STATUS = RECORDING; 
        //設置為錄制狀態 
        tvTips.setText("正在錄制,點擊播放按鈕或者麥克風停止錄制"); 
        btnRecord.setBackgroundResource(R.drawable.ic_mic_black_24dp); 
 
        //開始錄制的設置 
        mediaRecorder.reset();  // You can reuse the object by going back to setAudioSource() step 
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 
        try{ 
            mediaRecorder.setOutputFile(PATH_NAME); 
            mediaRecorder.prepare(); 
            mediaRecorder.start();   // Recording is now started 
        }catch (IOException e){ 
            Toast.makeText(this, "準備錄制文件失敗", Toast.LENGTH_SHORT).show(); 
            e.printStackTrace(); 
            finish(); 
        } 
    } 
 
    public void stopRecording(){ 
        if (RECORDING == STATUS){ 
            //說明正在錄制,設置停止信息 
            tvTips.setText("已停止錄制,開始播放"); 
            btnRecord.setBackgroundResource(R.drawable.ic_mic_none_black_24dp); 
            mediaRecorder.stop(); 
        } 
    } 
 
 
 
    Handler handler= new Handler(new Handler.Callback() { 
        @Override 
        public boolean handleMessage(Message msg) { 
            if ((Double)msg.obj > 70){ 
                imvSound.setImageResource(R.drawable.ic_volume_mute_valid_24dp); 
            }else { 
                imvSound.setImageResource(R.drawable.ic_volume_mute_gray_24dp); 
            } 
            return false; 
        } 
    }); 
 
    Timer timer = new Timer(); 
    public void startCheckSound(){ 
        //定時檢測峰值,以及檢測播放進度 
        timer.schedule(new TimerTask() { 
            @Override 
            public void run() { 
                if (mediaRecorder != null) { 
                    double amplitude = (double)mediaRecorder.getMaxAmplitude(); 
                    double db = 0; 
                    //計算分貝 
                    if (amplitude > 1) 
                        db = 20 * Math.log10(amplitude); 
                    Message msg = new Message(); 
                    msg.obj = db; 
                    handler.sendMessage(msg); 
                    //如果需要檢測播放進度可以使用 
                    //mediaPlayer.getCurrentPosition()/mediaPlayer.getDuration(); 
                } 
            } 
        },0,100); 
    } 
 
 
    ProgressDialog mediaplayerPreparingDialog; 
    public void startPlay(){ 
        if (RECORDING == STATUS){ 
            //如果是從錄制狀態開始播放,則重新讀取新的錄制文件 
            STATUS = PLAYING; 
            //設置音頻播放器 
            mediaPlayer = new MediaPlayer(); 
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 
                @Override 
                public void onCompletion(MediaPlayer mp) { 
                    //播放完設置 
                    tvTips.setText("播放完畢,可點擊麥克風重新錄制"); 
                    btnPlay.setBackgroundResource(R.drawable.ic_play_circle_filled_black_24dp); 
                } 
            }); 
            try { 
                mediaPlayer.setDataSource(PATH_NAME); 
                mediaPlayer.prepareAsync(); 
            } catch (IOException e) { 
                e.printStackTrace(); 
                Toast.makeText(this, "錄音文件已丟失", Toast.LENGTH_SHORT).show(); 
                finish(); 
            } 
            mediaplayerPreparingDialog.setTitle("正在準備播放錄音"); 
            mediaplayerPreparingDialog.show(); 
 
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 
                @Override 
                public void onPrepared(MediaPlayer mp) { 
                    mediaplayerPreparingDialog.dismiss(); 
                    mediaPlayer.start(); 
                } 
            }); 
 
        }else if(PAUSING == STATUS){ 
            //從暫停狀態開始播放則直接播放 
            mediaPlayer.start(); 
        } 
        //開始播放,設置按鈕為暫停 
        btnPlay.setBackgroundResource(R.drawable.ic_pause_circle_filled_black_24dp); 
 
    } 
 
    public void pausePlay(){ 
        if (PLAYING == STATUS) { 
            //暫停播放,設置按鈕為開始播放 
            mediaPlayer.pause(); 
            btnPlay.setBackgroundResource(R.drawable.ic_play_circle_filled_black_24dp); 
            STATUS = PAUSING; 
        } 
    } 
 
    public void stopPlay(){ 
        if (mediaPlayer != null) mediaPlayer.stop(); 
    } 
}  

Media和IllegalStateException

這個就是之前提到的由于沒有按順序釋放資源或者stop掉這兩個破玩意兒,可能會導致的各種錯誤,所以我很無奈地設置了一個STATUS變量,并且在Activity的OnDestoy里對兩個東西進行了stop,其實一般還會使用release釋放掉資源…大家隨意吧…

QCMediaPlayer mediaplayer NOT present

!!!我就知道,如果你看到這個地方,一定也對這個錯誤感到莫名其妙。我記得好像上古時期,也就是上次我寫這個的時候也被坑了。

論壇上有人說這個東西在4.4以下的系統就容易出現,但是我也只能感覺不明覺厲,我一開始用的是MediaPlayer.create(this,Uri.parse(PATH_NAME))來創建MediaPlayer,于是換成了

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

好吧,然后問題就解決了,我也是無語了。我覺得這個地方是一個很久遠的坑了,查原因一時也沒查到。我只能推測大概因為create函數創建時沒有指定AudioStreamType導致使用了默認的

private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; 

在某些設備上可能不支持,于是就出了問題= =好吧,我也不知道還能說啥,就醬…

Vector Asset添加的圖標顏色不變化

如上,我的播放按鈕啊,音響啊,之類的圖標都是通過Vector Asset添加的,這也是一個比較久遠的坑了,但是以前也沒有記下來,即在Android L以下的版本中,Vector Asset添加的圖標,修改顏色時不能使用顏色的引用,而要直接寫顏色,例如:

<vector xmlns:android="http://schemas.android.com/apk/res/android" 
    android:width="24dp" 
    android:height="24dp" 
    android:viewportWidth="24.0" 
    android:viewportHeight="24.0"> 
<path 
    android:fillColor="#3F51B5" 
    android:pathData="***"/> 
</vector>  

使用

<vector xmlns:android="http://schemas.android.com/apk/res/android" 
    android:width="24dp" 
    android:height="24dp" 
    android:viewportWidth="24.0" 
    android:viewportHeight="24.0"> 
<path 
    android:fillColor="@color/colorPrimary" 
    android:pathData="***"/> 
</vector>  

則導致顏色并不會修改,依然是黑色

 

來自:http://mobile.51cto.com/android-525624.htm

 

 本文由用戶 Faustino867 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!