天天看點

語音消息以及未讀提醒連續播放

  微信引入語音聊天後,使得語音聊天非常火。最近做了一個關于語音聊天demo,現在介紹下整個實作過程:包括錄音、儲存音頻檔案至SD卡、發送錄音檔案、接收新語音消息紅點提醒、播放暫停、未讀語音消息連續播放、播放模式切換。

       錄音部分包括:音頻源、音頻格式、編碼器、音頻通道、采樣率、編碼率等步驟,具體實作如下:

try {
	this.recorder = new MediaRecorder();
	this.recorder.setAudioSource(AudioSource.MIC);//音頻源
	this.recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);//音頻格式
	this.recorder.setAudioEncoder(1);//編碼器
	this.recorder.setAudioChannels(1);//音頻通道
	this.recorder.setAudioSamplingRate(8000);//采樣率
	this.recorder.setAudioEncodingBitRate(64);//編碼率
	this.voiceFilePath = getVoiceFilePath();
	this.file = new File(this.voiceFilePath);
	this.recorder.setOutputFile(this.file.getAbsolutePath());//輸出至指定檔案路徑
	this.recorder.prepare();//準備錄音
	this.isRecording = true;
	this.recorder.start();//開始錄音
    }catch(IOException localIOException) {
	Log.e(TAG, "voice prepare() failed");
    }
           

  錄音過程中根據音量大小重新整理動畫,100ms重新整理一遍,開一個子線程操作,通過handler發送消息通知主線程:

new Thread(new Runnable() {
	public void run() {
	try {
		while(VoiceRecorder.this.isRecording) {
			Message localMessage = new Message();
			double ratio = (double)recorder.getMaxAmplitude();
			localMessage.arg1 = (int)((14*ratio)/32768);
			localMessage.what = MessageActivity.VOICE_REFRESH;
			VoiceRecorder.this.handler.sendMessage(localMessage);
			  SystemClock.sleep(100L);
		}
	}
	catch(Exception localException) {
		Log.e(TAG , localException.toString());
	}
	}
}).start();
           

主線程接收消息,重新整理錄音動畫:

private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
          switch (msg.what) {

            case VOICE_REFRESH:
                if (msg.arg1 <= 1)
                    micImage.setImageDrawable(micImages[0]);
                else
                    micImage.setImageDrawable(micImages[msg.arg1 >= 14 ? 14 : msg.arg1 - 1]);
                break;

          }
        }
    };
           

錄音時長可以自己設定,最後的10秒倒計時,也是開一個子線程進行計時,通過handler通知主線程提示使用者:

new Thread(new Runnable() {
	@Override
	public void run() {
		while(isRecording) {
			try {
				voice_duration++;
				if(MAX_DURATION - voice_duration < TIME_TO_COUNT_DOWN) {//開始進入倒計時10s
					Message msg = handler.obtainMessage();
					if(MAX_DURATION - voice_duration < 0) {//達到最大時長,停止錄音
						msg.arg1 = stopRecoding();
						msg.what = MessageActivity.VOICE_LONG;
						voice_duration = 0;
						handler.sendMessage(msg);
					}
					else {
						msg.arg1 = MAX_DURATION - voice_duration;
						msg.what = MessageActivity.VOICE_TIP;
						handler.sendMessage(msg);
					}
				}
				Thread.sleep(1000);//每隔1秒計時1次
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
}).start();
           

錄音完畢,需要停止并釋放recorder,計算錄音時間,儲存錄音檔案。

public int stopRecoding() {
		if(this.recorder != null) {
			this.isRecording = false;
			this.voice_duration = 0;
			this.recorder.stop();//停止錄音
			this.recorder.release();//釋放錄音器
			this.recorder = null;
			int i = (int)(new Date().getTime() - this.startTime) / 1000;//計算錄音時間,并儲存至指定路徑
			Log.e("voice", " voice recording finished. seconds:" + i + " file length:" + new File(this.voiceFilePath).length());
			return i;
		}
		return 0;
	}
           

播放語音消息,使用MediaPayer進行播放,步驟包括:設定報放音頻流類型、播放資料源、準備以及開始播放、設定播放完畢回調監聽:

((Activity)context).setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
	requestAudioFocus();
	mediaPlayer = new MediaPlayer();
	mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);//音頻流類型
	try {
		mediaPlayer.setDataSource(filePath);//播放資料源
		mediaPlayer.prepare();//準備播放
		mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

			@Override
			public void onCompletion(MediaPlayer mp) {//設定播放完畢的回調
				// TODO Auto-generated method stub
				mMediaPlayerCallback.onStop();
				stopPlayVoice(); // stop animation
			}

		});
		isPlaying = true;
		playSource = filePath;
		currentPlayListener = this;
		mediaPlayer.start();//開始播放
		mMediaPlayerCallback.onStart();
	}
	catch(Exception e) {
		Log.d(TAG, "playErr--" + e.toString());
	}
           

在播放過程中,使用者也許會暫停播放,那麼需要暫停目前MediaPlayer播放,并且記錄目前已播放的時長,以便下次續播:

public void stopPlayVoice() {
		if(mediaPlayer != null) {
			mediaPlayer.stop();
			mediaPlayer.release();
			mediaPlayer = null;
			mMediaPlayerCallback.onStop();
		}
		abandonAudioFocus();
		isPlaying = false;
	}
           

播放模式切換,兩種模式:聽筒播放、揚聲器播放,并且在onStop()方法裡,應該把目前設定模式寫入sharePreference裡儲存起來,以便下次記住并調用設定後的播放模式。

mMediaPlayerCallback = callback;
	AudioManager audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
	
	if(MessageActivity.outPlayMode) {//揚聲器播放
		audioManager.setSpeakerphoneOn(true);
		audioManager.setMode(AudioManager.MODE_NORMAL);
	} else {//聽筒播放
		audioManager.setSpeakerphoneOn(false);
		audioManager.setMode(AudioManager.MODE_IN_CALL);
	}
           

接收到的未讀語音消息,旁邊有小紅點提示:

private void handleAudioMessage(View view, ViewHolder holder, AudioMessage msg,int read) {
 
        if (msg.getSendOrReceive() == Provider.MessageColumns.MSG_SEND) {//發送的語音消息
            ......

        } else {//接收的語音消息
            holder.imgVoiceReadTip = (ImageView) view.findViewById(R.id.img_voice_unread_tip);

            if (read == Provider.MessageColumns.UNREAD_AUDIO) {
                holder.imgVoiceReadTip.setVisibility(View.VISIBLE);//如果未讀,紅點顯示
            } else {
                holder.imgVoiceReadTip.setVisibility(View.GONE);//如果已讀,紅點消失
            }
        }
    }
           

目前語音消息播放完畢,開一個子線程異步查詢是否有未讀語音消息,如果有就往下連續播放:

/**
     * 通過輸入目前的messageId,查找後面是否有未讀的語音消息,如果有,傳回cursor的位置,沒有傳回-1;
     */
    private int queryUnReadVoice(int id) {
        Cursor cursor = mAdapter.getCursor();
        boolean currentPosition = false;
        int index = -1;
        if (cursor != null) {
            cursor.moveToFirst();
            if (!cursor.isAfterLast()) {
                do {
                    int message_id = cursor.getInt(cursor.getColumnIndex(Provider.MessageColumns._ID));
                    if (message_id == id) {
                        currentPosition = true;
                        continue;
                    }
                    if (currentPosition) {
                        int isRead = cursor.getInt(cursor.getColumnIndexOrThrow(Provider.MessageColumns._MESSAGE_READ));
                        if (isRead == Provider.MessageColumns.UNREAD_AUDIO) {
                            index = cursor.getPosition();
                            break;
                        }
                    }
                } while (cursor.moveToNext());
            }
        }
        return index;
    }
           
/**
     * 查詢到未讀語音消息id,通過handler發送消息通知主線程往下播放
     * @param id
     */
    private void continueToPlay(int id) {
        int index = queryUnReadVoice(id);
        if (index != -1) {
            Message myMsg = Message.obtain();
            myMsg.what = index;
            handler.sendMessage(myMsg);
        }
    }
           

當handler收到消息後,往下播放語音消息,小紅點消失,未讀狀态置為已讀,并且把該消息狀态寫進資料庫:

private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {

            int index = msg.what;
            View childView = mAdapter.getView(index, null, mListView);
            AudioMessage message = (AudioMessage) childView.getTag();
            ContentValues values = new ContentValues();
            values.put(Provider.MessageColumns._MESSAGE_READ, Provider.MessageColumns.READ_AUDIO);
            context.getApplicationContext().getContentResolver().update(Provider.MessageColumns.CONTENT_URI, values, Provider.MessageColumns._ID + "=?",
                    new String[]{message.getId() + ""});
            mAdapter.notifyDataSetChanged();
            childView.invalidate();
            playAudio((ImageView) childView.findViewById(R.id.iv_audio), message);
        }
    };
           

錄音過程中,使用者可能又不想發送了,那麼應該提供取消錄音操作。這個可以在onTouchListener監聽器的onTouch方法裡設定,如果event.getAction() == MotionEvent.ACTION_MOVE,并且檢測到手勢是往上滑動,執行取消操作:

v.setPressed(true);
    if (event.getY() < 0) {
        // 上滑
        notShowTip = true;
        recordingHint.setText(getString(R.string.msg_voice_do_cancel_send_2));
        recordingHint.setBackgroundResource(R.drawable.recording_text_hint_bg);
    } else {
        notShowTip = false;
        if (VoiceRecorder.MAX_DURATION - voiceRecorder.voice_duration > VoiceRecorder.TIME_TO_COUNT_DOWN) {
            recordingHint.setText(getString(R.string.msg_voice_do_cancel_send_1));
            recordingHint.setBackgroundColor(Color.TRANSPARENT);
        }
    }
    return true;<pre name="code" class="java">
           

當Activity失去焦點,回調onPause方法時,如果正在播放語音消息,應該暫停播放,如果正在錄音也應該暫停錄音:

@Override
    protected void onPause() {
        super.onPause();
        audioPresenter.onPauseAudio();
        try {
            // 停止錄音
            if (voiceRecorder.isRecording()) {
                voiceRecorder.discardRecording();
                recordingContainer.setVisibility(View.INVISIBLE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
           

相關的權限:

<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
           

好了,語音消息從錄音到發送再到接收播放的過程介紹完了。希望大家閱讀過後,對android系統的語音相關操作有更深刻了解與認識。如果有什麼問題,大家可以提出來互相探讨。下次對音頻管理器作進一步介紹,敬請期待。

繼續閱讀