Android8.0 FmRadio源码分析之录音篇

FmRadio录音流程

录音界面如图

通过FmMainActivity的 Start recording 菜单选项即可进入FmRecordActivity直接进行录音,当FmRecordActivity销毁时,录音则停止。如果要实现后台也可以录音,只需保证录音的状态从FmService中读取并且Activity销毁时不停止录音即可。

下面从界面管理,后台Service以及录音实现三部分分析

界面管理 FmRecordActivity

进入Record界面执行过程
FmRecordActivity

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate");
    mContext = getApplicationContext();
    mFragmentManager = getFragmentManager();
    setContentView(R.layout.fm_record_activity);

    mMintues = (TextView) findViewById(R.id.minutes);
    mSeconds = (TextView) findViewById(R.id.seconds);

    mFrequency = (TextView) findViewById(R.id.frequency);
    mStationInfoLayout = findViewById(R.id.station_name_rt);
    mStationName = (TextView) findViewById(R.id.station_name);
    mRadioText = (TextView) findViewById(R.id.radio_text);

    mStopRecordButton = (Button) findViewById(R.id.btn_stop_record);
    mStopRecordButton.setEnabled(false);
    mStopRecordButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Stop recording and wait service notify stop record state to show dialog
            mService.stopRecordingAsync();
        }
    });

    mPlayIndicator = (FmVisualizerView) findViewById(R.id.fm_play_indicator);

    if (savedInstanceState != null) {
        mCurrentStation = savedInstanceState.getInt(FmStation.CURRENT_STATION);
        mIsActivityRecreate = true;
    } else {
        Intent intent = getIntent();
        mCurrentStation = intent.getIntExtra(FmStation.CURRENT_STATION,
                FmUtils.DEFAULT_STATION);
        mRecordState = intent.getIntExtra("last_record_state", FmRecorder.STATE_INVALID);
    }
    // 绑定FmService来与FmService交互
    bindService(new Intent(this, FmService.class), mServiceConnection,
            Context.BIND_AUTO_CREATE);
    // wangyannan  begin
    // should called after getting lateset state
    //updateUi();

    if(FM_STOP_RECORDING.equals(getIntent().getAction())){
        needStopRecording = true;
    }
    // wangyannan  end
}

绑定成功会调用onServiceConnected

private final ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, android.os.IBinder service) {
        // 返回的Binder对象通过getService方法得到FmService对象,从而实现调用其方法达到交互
        mService = ((FmService.ServiceBinder) service).getService();
        Log.d(TAG, "onServiceConnected " + mService);
        if (mIsPermissionsRevoked == true) {
            Log.w(TAG, "onServiceConnected: return due to permissions revoked in bg.");
            return;
        }
        mService.registerFmRadioListener(mFmListener);
        // wangyannan  begin
        // 判断是否点击通知中的停止按钮,如果点击则停止录音
        if(needStopRecording) {
            if(!isStopRecording()){
                mService.stopRecordingAsync();
            }
        }
        // mCurrentStation should read from FMService when it is default station.
        // 从FmService刷新当前录音频道
        if(mCurrentStation == FmUtils.DEFAULT_STATION) {
            mCurrentStation = mService.getFrequency();
        }
        Log.d(TAG, "wangyannan onServiceConnected, mCurrentStation = " + mCurrentStation);
        // wangyannan  end
        mService.setFmRecordActivityForeground(!mIsInBackground);
        // When Activity re-launch, need get latest record state from service.
        // wangyannan  begin
        // 从FmService中刷新当前录音状态在FmRecorder中定义
        if (mIsActivityRecreate || (mRecordState == FmRecorder.STATE_INVALID && mService.getRecorderState() != FmRecorder.STATE_IDLE)) {
            mRecordState = mService.getRecorderState();
        }
        Log.d(TAG, "wangyannan onServiceConnected, mRecordState = " + mRecordState);
        // when get latest status , update ui 
        // 获取到当前录音的最新状态后更新界面信息
        updateUi();
        removeNotification();
        // wangyannan  end

        // 1. If have stopped recording, we need check whether need show save dialog again.
        // Because when stop recording in background, we need show it when switch to foreground.
        if (isStopRecording()) {
            if (!isSaveDialogShown()) {
                //弹出是否保存录音对话框
                showSaveDialog();
            }
            return;
        }
        // 2. If not start recording, start it directly, this case happen when start this
        // activity from main fm activity.
        if (!isStartRecording()) {
            // 开始录音
            mService.startRecordingAsync();
        }
        mPlayIndicator.startAnimation();
        mStopRecordButton.setEnabled(true);
        mHandler.removeMessages(FmListener.MSGID_REFRESH);
        // 发送MSGID_REFRESH消息更新录音界面的事件信息
        mHandler.sendEmptyMessage(FmListener.MSGID_REFRESH);
    };

    @Override
    public void onServiceDisconnected(android.content.ComponentName name) {
        mService = null;
    };
};

更新界面录音时间信息

 private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        Log.d(TAG, "handleMessage: msg_what = " + msg.what);
        switch (msg.what) {
            case FmListener.MSGID_REFRESH:
                if (mService != null) {
                    // 通过FmService得到录音的事件
                    long recordTimeInMillis = mService.getRecordTime();
                    long recordTimeInSec = recordTimeInMillis / 1000L;
                    // 计算分秒更新界面事件信息
                    mMintues.setText(addPaddingForString(recordTimeInSec / TIME_BASE));
                    mSeconds.setText(addPaddingForString(recordTimeInSec % TIME_BASE));
                    // 每1s刷新都需要检查当前空间和Fm播放的状态
                    checkStorageSpaceAndStop();
                }
                mHandler.sendEmptyMessageDelayed(FmListener.MSGID_REFRESH, 1000);
                break;
            .....

每1s更新界面显示时间,并检测Fm是否播放和空间是否够

private void checkStorageSpaceAndStop() {
    Log.d(TAG, "checkStorageSpaceAndStop");
    long recordTimeInMillis = mService.getRecordTime();
    long recordTimeInSec = recordTimeInMillis / 1000L;
    // Check storage free space
    String recordingSdcard = FmUtils.getDefaultStoragePath();
    // 通过FmUtils判断存储空间是否足够
    if (!FmUtils.hasEnoughSpace(recordingSdcard)) {
        // Need to record more than 1s.
        // Avoid calling MediaRecorder.stop() before native record starts.
        if (recordTimeInSec >= 1) {
            // Insufficient storage
            mService.stopRecordingAsync();
            Toast.makeText(FmRecordActivity.this,
                    R.string.toast_sdcard_insufficient_space,
                    Toast.LENGTH_SHORT).show();
        }
        // 判断Fm如果不在播放则停止录音
    } else if (mService.getPowerStatus() != FmService.POWER_UP) {
        // Need to record more than 1s.
        // Avoid calling MediaRecorder.stop() before native record starts.
        if (recordTimeInSec >= 1) {
            mService.stopRecordingAsync();
        }
    }
}

后台服务 FmService

FmService在FmMainActivity通过start的方式已经启动,只有停止Fm播放,并返回键退出时才会stopService,其他情况即使是移除FmRadio在任务栈中,该Service会通过通知设置为前台进程一直运行。

如果Service处于后台,我们只能通过发送广播等方式与其交互,只有通过BinderService的方式才可以通过FmService对象直接与其交互。

还有一点需要注意的是Service中也不能执行耗时操作,否则会引起ANR,我们可以通过子线程的方式执行耗时操作,如果需要和Service及交互,则主线程中保留子线程的Handler即可,通过消息机制可完美与子线程交互,在FmService中是通过HandlerThread的方式实现。

HandlerThread handlerThread = new HandlerThread("FmRadioServiceThread");
handlerThread.start();
mFmServiceHandler = new FmRadioServiceHandler(handlerThread.getLooper());

HandlerThread会开启新的线程,子线程的Handler就是FmRadioServiceHandler,因为FmRadioServiceHandler的Looper对象是子线程的Looper。

  • 开始录音 mService.startRecordingAsync();
/**
 * Start recording
 */
public void startRecordingAsync() {
        Log.d(TAG, "startRecordingAsync");
    mFmServiceHandler.removeMessages(FmListener.MSGID_STARTRECORDING_FINISHED);
    mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_STARTRECORDING_FINISHED);
}
 /**
 * The background handler
 */
class FmRadioServiceHandler extends Handler {
    public FmRadioServiceHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        Bundle bundle;
        boolean isPowerup = false;
        boolean isSwitch = true;
    Log.d(TAG, "handleMessage: " + msg.what);
        switch (msg.what) {
    ...
    /********** recording **********/
    case FmListener.MSGID_STARTRECORDING_FINISHED:
        startRecording();
        break;
    ...

startRecording在子线程中运行

private void startRecording() {
        Log.d(TAG, "startRecording");
    // 获取默认存储路径
    sRecordingSdcard = FmUtils.getDefaultStoragePath();
    if (sRecordingSdcard == null || sRecordingSdcard.isEmpty()) {
        Log.d(TAG, "startRecording, may be no sdcard");
        onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT);
        return;
    }

    if (mFmRecorder == null) {
        mFmRecorder = new FmRecorder();
        mFmRecorder.registerRecorderStateListener(FmService.this);
    }

    if (isSdcardReady(sRecordingSdcard)) {
        // 调用FmRecorder对音频录音的封装类进行录音
        mFmRecorder.startRecording(mContext);
    } else {
        onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT);
    }
}

停止录音调用了mService.stopRecordingAsync();,流程和开始录音流程相同。

  • 录音状态通知代码设计

除了分析录音流程,这里再补充下FmService是如何将自己的状态通知给其他需要获取状态的界面。这里使用的当然是回调,其是通过List集合维护了所有注册接口监听器的对象,待状态需要通知给关注者时调用监听器中的callback方法即可。该Listener设计如下
FmListener

/**
 * Activity connect FMRadio service should implements this interface to update
 * ui or status
 */
public interface FmListener {
    /**
     * directly call back from service to activity
     */
    // FM RDS station changed
    int LISTEN_RDSSTATION_CHANGED = 0x00100010;

    // FM PS information changed
    int LISTEN_PS_CHANGED = 0x00100011;

    // FM RT information changed
    int LISTEN_RT_CHANGED = 0x00100100;

    // FM Record state changed
    int LISTEN_RECORDSTATE_CHANGED = 0x00100101; // 1048833

    // FM record error occur
    int LISTEN_RECORDERROR = 0x00100110; // 1048848

    // FM record mode change
    int LISTEN_RECORDMODE_CHANGED = 0x00100111; // 4018849

    // FM Record state changed
    int LISTEN_SPEAKER_MODE_CHANGED = 0x00101000; // 1052672

    // Bundle keys
    String SWITCH_ANTENNA_VALUE = "switch_antenna_value";
    String CALLBACK_FLAG = "callback_flag";
    String KEY_IS_SWITCH_ANTENNA = "key_is_switch_antenna";
    String KEY_BT_STATE = "key_bt_state";
    String KEY_IS_TUNE = "key_is_tune";
    String KEY_TUNE_TO_STATION = "key_tune_to_station";
    String KEY_IS_SEEK = "key_is_seek";
    String KEY_SEEK_TO_STATION = "key_seek_to_station";
    String KEY_IS_SCAN = "key_is_scan";
    String KEY_RDS_STATION = "key_rds_station";
    String KEY_PS_INFO = "key_ps_info";
    String KEY_RT_INFO = "key_rt_info";
    String KEY_STATION_NUM = "key_station_num";

    // Audio focus related
    String KEY_AUDIOFOCUS_CHANGED = "key_audiofocus_changed";

    // Recording
    String KEY_RECORDING_STATE = "key_is_recording_state";
    String KEY_RECORDING_ERROR_TYPE = "key_recording_error_type";
    String KEY_IS_RECORDING_MODE = "key_is_recording_mode";
    String KEY_RECORDING_NAME = "recording_name";

    // For change speaker/earphone mode
    String KEY_IS_SPEAKER_MODE = "key_is_speaker_mode";

    /**
     * handle message: call back from service to activity
     */
    // Message to handle
    int MSGID_UPDATE_RDS = 1;
    int MSGID_UPDATE_CURRENT_STATION = 2;
    int MSGID_ANTENNA_UNAVAILABE = 3;
    int MSGID_SWITCH_ANTENNA = 4;
    int MSGID_SET_RDS_FINISHED = 5;
    int MSGID_SET_CHANNEL_FINISHED = 6;
    int MSGID_SET_MUTE_FINISHED = 7;
    // Fm main
    int MSGID_POWERUP_FINISHED = 9;
    int MSGID_POWERDOWN_FINISHED = 10;
    int MSGID_FM_EXIT = 11;
    int MSGID_SCAN_CANCELED = 12;
    int MSGID_SCAN_FINISHED = 13;
    int MSGID_AUDIOFOCUS_FAILED = 14;
    int MSGID_TUNE_FINISHED = 15;
    int MSGID_SEEK_FINISHED = 16;
    int MSGID_ACTIVE_AF_FINISHED = 18;
    // Recording
    int MSGID_RECORD_STATE_CHANGED = 19;
    int MSGID_RECORD_ERROR = 20;
    int MSGID_RECORD_MODE_CHANED = 21;
    int MSGID_STARTRECORDING_FINISHED = 22;
    int MSGID_STOPRECORDING_FINISHED = 23;
    int MSGID_STARTPLAYBACK_FINISHED = 24;
    int MSGID_STOPPLAYBACK_FINISHED = 25;
    int MSGID_SAVERECORDING_FINISHED = 26;
    // Audio focus related
    int MSGID_AUDIOFOCUS_CHANGED = 30;

    int NOT_AUDIO_FOCUS = 33;

    int MSGID_BT_STATE_CHANGED = 34;

    // For refresh time
    int MSGID_REFRESH = 101;

    int UPDATE_NOTIFICATION = 102;

    // For EM
    String KEY_IS_POWER_UP = "key_is_power_up";


    /**
     * Call back method to activity from service
     */
    void onCallBack(Bundle bundle);
}

该Listener包含了各种状态值,以及回调方法onCallBack

这里已通知录音出错为例

Bundle bundle = new Bundle(2);
bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RECORDERROR);
bundle.putInt(FmListener.KEY_RECORDING_ERROR_TYPE, mRecorderErrorType);
notifyActivityStateChanged(bundle);
private ArrayList<Record> mRecords = new ArrayList<Record>();
/**
 * Call back from service to activity
 *
 * @param bundle The message to activity
 */
private void notifyActivityStateChanged(Bundle bundle) {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "notifyActivityStateChanged: " + mRecords.size());
    }
    // 遍历mRecords集合
    if (!mRecords.isEmpty()) {
        synchronized (mRecords) {
            Iterator<Record> iterator = mRecords.iterator();
            while (iterator.hasNext()) {
                Record record = (Record) iterator.next();

                FmListener listener = record.mCallback;

                if (listener == null) {
                    iterator.remove();
                    return;
                }
                // 对注册该监听器的对象回调onCallBack方法
                listener.onCallBack(bundle);
            }
        }
    } else {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "notifyActivityStateChanged: " + mRecords.isEmpty());
        }
    }
}

Record只是对FmListener的简单封装,用来唯一标识注册的FmListener

/**
 * FM Radio listener record
 */
private static class Record {
    int mHashCode; // hash code
    FmListener mCallback; // call back
}

注册方法

/**
 * Register FM Radio listener, activity get service state should call this
 * method register FM Radio listener
 *
 * @param callback FM Radio listener
 */
public void registerFmRadioListener(FmListener callback) {
    synchronized (mRecords) {
        // register callback in AudioProfileService, if the callback is
        // exist, just replace the event.
        Record record = null;
        int hashCode = callback.hashCode();
        final int n = mRecords.size();
        for (int i = 0; i < n; i++) {
            record = mRecords.get(i);
            if (hashCode == record.mHashCode) {
                return;
            }
        }
        record = new Record();
        record.mHashCode = hashCode;
        record.mCallback = callback;
        mRecords.add(record);
    }
}

这里已FmRecordActivity注册该监听器为例,在服务连接上后会进行注册mService.registerFmRadioListener(mFmListener); ,mFmListener为下

// Service listener
private final FmListener mFmListener = new FmListener() {
    @Override
    public void onCallBack(Bundle bundle) {
        int flag = bundle.getInt(FmListener.CALLBACK_FLAG);
        if (flag == FmListener.MSGID_FM_EXIT) {
            mHandler.removeCallbacksAndMessages(null);
            finish();
        }

        // remove tag message first, avoid too many same messages in queue.
        Message msg = mHandler.obtainMessage(flag);
        msg.setData(bundle);
        mHandler.removeMessages(flag);
        mHandler.sendMessage(msg);
    }
};

FmRecordActivity将接收到信息,封装为消息发送给mHandler交由其处理. 上一步携带的消息为LISTEN_RECORDERROR,故会走下面case分支

case FmListener.LISTEN_RECORDERROR:
    Bundle bundle = msg.getData();
    int errorType = bundle.getInt(FmListener.KEY_RECORDING_ERROR_TYPE);
    handleRecordError(errorType);
    break;

对于其他FmMainActivity和FmFavoriteActivity界面处理机制是相同的。

录音实现 FmRecorder

  • 开始录音
    FmService中的调用FmRecorder的startRecording()会执行下面方法
/**
 * Start recording the voice of FM, also check the pre-conditions, if not
 * meet, will return an error message to the caller. if can start recording
 * success, will set FM record state to recording and notify to the caller
 */
public void startRecording(Context context) {
    Log.d(TAG, "startRecording");
    mRecordTime = 0;
    // 1. 检查存储是否挂载
    // Check external storage
    if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
        Log.e(TAG, "startRecording, no external storage available");
        setError(ERROR_SDCARD_NOT_PRESENT);
        return;
    }
    // 2. 检查存储大小是否足够
    String recordingSdcard = FmUtils.getDefaultStoragePath();
    // check whether have sufficient storage space, if not will notify
    // caller error message
    if (!FmUtils.hasEnoughSpace(recordingSdcard)) {
        setError(ERROR_SDCARD_INSUFFICIENT_SPACE);
        Log.e(TAG, "startRecording, SD card does not have sufficient space!!");
        return;
    }
    // 3. 创建录音目录
    // get external storage directory
    File sdDir = new File(recordingSdcard);
    File recordingDir = new File(sdDir, FM_RECORD_FOLDER);
    // exist a file named FM Recording, so can't create FM recording folder
    if (recordingDir.exists() && !recordingDir.isDirectory()) {
        Log.e(TAG, "startRecording, a file with name \"FM Recording\" already exists!!");
        setError(ERROR_SDCARD_WRITE_FAILED);
        return;
    } else if (!recordingDir.exists()) { // try to create recording folder
        boolean mkdirResult = recordingDir.mkdir();
        if (!mkdirResult) { // create recording file failed
            setError(ERROR_RECORDER_INTERNAL);
            return;
        }
    }
    // 4. 创建临时的录音文件,通过日期和后缀名生成临时录音文件的名字
    // create recording temporary file
    long curTime = System.currentTimeMillis();
    Date date = new Date(curTime);
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMddyyyy_HHmmss",
            Locale.ENGLISH);
    String time = simpleDateFormat.format(date);
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(time).append(RECORDING_FILE_EXTENSION);
    String name = stringBuilder.toString();
    mRecordFile = new File(recordingDir, name);
    try {
        if (mRecordFile.createNewFile()) {
            Log.d(TAG, "startRecording, createNewFile success with path "
                    + mRecordFile.getPath());
        }
    } catch (IOException e) {
        Log.e(TAG, "startRecording, IOException while createTempFile: " + e);
        e.printStackTrace();
        setError(ERROR_SDCARD_WRITE_FAILED);
        return;
    }
    // 5. 通过MediaRecorder进行录音
    // set record parameter and start recording
    try {
        mRecorder = new MediaRecorder();
        mRecorder.setOnErrorListener(this);
        mRecorder.setOnInfoListener(this);
        mRecorder.setAudioSource(MediaRecorder.AudioSource.RADIO_TUNER);
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        final int samplingRate = 44100;
        mRecorder.setAudioSamplingRate(samplingRate);
        final int bitRate = 128000;
        mRecorder.setAudioEncodingBitRate(bitRate);
        final int audiochannels = 2;
        mRecorder.setAudioChannels(audiochannels);
        mRecorder.setOutputFile(mRecordFile.getAbsolutePath()); // 设置录音文件的输出文件路径
        mRecorder.prepare();
        mRecordStartTime = SystemClock.elapsedRealtime();
        mRecorder.start(); // 开始录音
        mIsRecordingFileSaved = false;
    } catch (IllegalStateException e) {
        Log.e(TAG, "startRecording, IllegalStateException while starting recording!", e);
        setError(ERROR_RECORDER_INTERNAL);
        return;
    } catch (IOException e) {
        Log.e(TAG, "startRecording, IOException while starting recording!", e);
        setError(ERROR_RECORDER_INTERNAL);
        return;
    }
    // 6. 设置当前录音状态
    setState(STATE_RECORDING);
}

MediaRecorder具体使用在++MediaRecorder和AudioRecorder比较篇记录++

  • 停止录音
/**
 * Stop recording, compute recording time and update FM recorder state
 */
public void stopRecording() {
    Log.d(TAG, "stopRecording, mInternalState = " + mInternalState);
    if (STATE_RECORDING != mInternalState) {
        Log.w(TAG, "stopRecording, called in wrong state: state = " + mInternalState);
        return;
    }
    // 记录录音时间
    mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime;
    stopRecorder(); // 停止录音
    // 设置录音状态为IDLE
    setState(STATE_IDLE);
}
private void stopRecorder() {
    Log.d(TAG, "stopRecorder");
    synchronized (this) {
        if (mRecorder != null) {
            try {
                // MediaRecorder停止录音
                mRecorder.stop();
            } catch (IllegalStateException ex) {
                Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex);
                setError(ERROR_RECORDER_INTERNAL);
            } catch (RuntimeException e) {
                Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + e);
                setError(ERROR_RECORDER_INTERNAL);
            } finally {
                mRecorder.release();
                setState(STATE_IDLE);
                mRecorder = null;
            }
        }
    }
}
  • 保存录音
/**
 * Save recording file with the given name, and insert it's info to database
 *
 * @param context The context
 * @param newName The name to override default recording name
 */
public void saveRecording(Context context, String newName) {
    ...
    // 对录音临时文件按照设置的名称重命名
    File newRecordFile = new File(mRecordFile.getParent(), newName + RECORDING_FILE_EXTENSION);
    boolean succuss = mRecordFile.renameTo(newRecordFile);
    if (succuss) {
        mRecordFile = newRecordFile;
    }
    mIsRecordingFileSaved = true;
    // insert recording file info to database
    addRecordingToDatabase(context);
}

录音状态有以下4种

// FM Recorder state not recording and not playing
public static final int STATE_IDLE = 5;
// FM Recorder state recording
public static final int STATE_RECORDING = 6;
// FM Recorder state playing
public static final int STATE_PLAYBACK = 7;
// FM Recorder state invalid, need to check
public static final int STATE_INVALID = -1;

猜你喜欢

转载自blog.csdn.net/w1070216393/article/details/80510297