• Android中几种常见的播放声音组件


    (1)概念

    (A)MediaPlayer

    MediaPlayer的功能很强大,下面附上一张该类封装音频的生命周期图:

    在这里插入图片描述
    适合在后台长时间播放本地音乐文件或者在线等流媒体文件,它的封装层次比较高,使用方式也比较简单。

    (B)SoundPool

    适合播放比较短的音频片段,比如游戏声音,按键声音,铃声片段等,并且可以同时播放多个音频。

    (C)AudioTrack

    AudioTrack属于更偏底层的音频播放,MediaPlayerService的内部就是使用了AudioTrack。

    AudioTrack用于单个音频播放和管理,相比于MediaPlayer具有:精炼、高效的优点。

    AudioTrack用于播放PCM(PCM无压缩的音频格式)音乐流的回放,如果要播需放其它格式音频,需要响应的解码器,这也是AudioTrack用的比较少的原因,需要自己解码音频。

    (D)Ringtone

    Ringtone为铃声、通知和其他类似声音提供快速播放的方法,这里还不得不提到一个管理类"RingtoneManager",提供系统铃声列表检索方法,并且,Ringtone实例需要从RingtoneManager获取。

    备注:

    这里要说到MediaPlayer和AudioTrack之间的联系,MediaPlayer在framework层也实例化了AudioTrack,其实质是MediaPlayer在framework层进行解码后,生成PCM流,然后代理委托给AudioTrack,最后AudioTrack传递给AudioFlinger进行混音,然后才传递给硬件播放。

    比较常见使用AudioTrack,CPU占用率低,内存消耗也比较少。因此如果是播放WAV音频文件,还是比较建议使用AudioTrack。

    总结:

    1. 对于延迟度要求不高,并且希望能够更全面的控制音乐的播放,MediaPlayer比较适合;
    2. 声音短小,延迟度小,并且需要几种声音同时播放的场景,适合使用SoundPool;
    3. 放大文件音乐,如WAV无损音频和PCM无压缩音频,可使用更底层的播放方式AudioTrack。它支持流式播放,可以读取(可来自本地和网络)音频流,却播放延迟较小;
    4. 对于系统类声音的播放和操作,Ringtone更适合;

    (2)简单使用

    (A)MediaPlayer

    MediaPlayer mMediaPlayer = new MediaPlayer(); // 创建MediaPlayer实例
    
    mMediaPlayer.setDataSource(dataSource); // 设置播放资源,可以是asset、sd卡路径,也可以是网络url
    
    mMediaPlayer.setLooping(false); // 不循环播放
    
    mMediaPlayer.prepare(); // 播放前准备,需要调用,create创建实例可以不用调用
    
    mMediaPlayer.start(); // 进行播放
    
    mMediaPlayer.stop(); // 停止播放
    
    mMediaPlayer.pause(); // 暂停播放
    
    mMediaPlayer.release(); // 释放播放资源
    
    mMediaPlayer.reset(); // 重置播放器状态
    
    mMediaPlayer.seekTo(); // 调整进度
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    (B)SoundPool

        //设置描述音频流信息的属性
        AudioAttributes abs = new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .build() ;
        SoundPool mSoundPoll =  new SoundPool.Builder()
                        .setMaxStreams(100)   //设置允许同时播放的流的最大值
                        .setAudioAttributes(abs)   //完全可以设置为null
                        .build() ;
    
    // 几个load方法和上文提到的MediaPlayer基本一致,这里的每个load都会返回一个SoundId值,这个值可以用来播放和卸载音乐。
    //------------------------------------------------------------
    
    int load(AssetFileDescriptor afd, int priority)
    
    int load(Context context, int resId, int priority)
    
    int load(String path, int priority)
    
    int load(FileDescriptor fd, long offset, long length, int priority)
    
    //-------------------------------------------------------------
    
    // 通过流id暂停播放
    final void pause(int streamID)
    
    // 播放声音,soundID:音频id(这个id来自load的返回值); left/rightVolume:左右声道(默认1,1);loop:循环次数(-1无限循环,0代表不循环);rate:播放速率(1为标准),该方法会返回一个streamID,如果StreamID为0表示播放失败,否则为播放成功
    final int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)
    
    //释放资源(很重要)
    final void release()
    
    //恢复播放
    final void resume(int streamID)
    
    //设置指定id的音频循环播放次数
    final void setLoop(int streamID, int loop)
    
    //设置加载监听(因为加载是异步的,需要监听加载,完成后再播放)
    void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener)
    
    //设置优先级(同时播放个数超过最大值时,优先级低的先被移除)
    final void setPriority(int streamID, int priority)
    
    //设置指定音频的播放速率,0.5~2.0(rate>1:加快播放,反之慢速播放)
    final void setRate(int streamID, float rate)
    
    //停止指定音频播放
    final void stop(int streamID)
    
    //卸载指定音频,soundID来自load()方法的返回值
    final boolean unload(int soundID)
    
    //暂停所有音频的播放
    final void autoPause()
    
    //恢复所有暂停的音频播放
    final void autoResume()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    (C)AudioTrack

    //第一个参数:声音的类型,有以下几种
            //STREAM_VOICE_CALL:电话声音
            //STREAM_SYSTEM:系统声音
            //STREAM_RING:铃声
            //STREAM_MUSIC:音乐声
            //STREAM_ALARM:警告声
            //STREAM_NOTIFICATION:通知声
            
    //第二个参数:采样频率,可选:8000,16000,22050,24000,32000,44100,48000等
    //第三个参数:声道数
    //第四个参数:采样格式,AudioFormat.ENCODING_PCM_16BIT,AudioFormat.ENCODING_PCM_8BIT
    //第五个参数:其配置AudioTrack内部的音频缓冲区大小,最好通过getMinBufferSize来计算
    //第六个参数:播放模式,MODE_STATIC需要一次性将所有的数据都写入播放缓冲区,简单高效,通常用于铃声,系统提示音的播放,MODE_STREAM需要按照一定的时间间隔不间断的写入音频数据,理论上可以用于任何音频场景,通常用来播放流媒体音频
    
    AudioTrack  mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 	//声音的类型
                    samplerate,	//设置音频数据的采样率  
                    AudioFormat.CHANNEL_CONFIGURATION_STEREO,	//设置输出声道为双声道立体声  
                    AudioFormat.ENCODING_PCM_16BIT,	//设置音频数据块是8位还是16位  
                    mAudioMinBufSize, AudioTrack.MODE_STREAM);	// 设置模式类型,在这里设置为流类型  
    
    mAudioTrack.play();  // 启动
    mAudioTrack.write();//数据写入audiotrack中
    
    // 停止与释放资源
    mAudioTrack.stop();
    mAudioTrack.release();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    (D)Ringtone

    //获取实例方法,均为RingtoneManager类提供
    
    //通过铃声uri获取
    static Ringtone getRingtone(Context context, Uri ringtoneUri)
    
    //通过铃声检索位置获取
    Ringtone getRingtone(int position)
    
    /**
     * 播放来电铃声的默认音乐
    */
    private void playRingtoneDefault(){
        Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) ;
        Ringtone mRingtone = RingtoneManager.getRingtone(this,uri);
        mRingtone.play();
        //mRingtone.stop();
    }
    
    /**
    * 随机播放一个Ringtone(有可能是提示音、铃声等)
    */
    private void ShufflePlayback(){
        RingtoneManager manager = new RingtoneManager(this) ;
        Cursor cursor = manager.getCursor();
        int count = cursor.getCount() ;
        int position = (int)(Math.random()*count) ;
        Ringtone mRingtone = manager.getRingtone(position) ;
        mRingtone.play();
        //mRingtone.stop();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    (3)一个内存泄露问题(充电提示音问题)

    (A)问题背景

    最近遇到一个项目上的问题,在Settings/Sound当中将充电提示音开关打开,发现刚开始插入充电后有提示音,测试到第40次后就再也没有提示音了,从Log看有如下报错:

    08-04 10:34:28.286627   417  1238 E AF::Track: Track(97): no more tracks available
    08-04 10:34:28.286714   417  1238 E AudioFlinger_Threads: createTrack_l() initCheck failed -12; no control block?
    
    08-04 10:34:28.294618   608  6382 E IAudioFlinger: createTrack returned error -12
    08-04 10:34:28.294696   608  6382 E AudioTrack: set(): createTrack_l fail! status = -12
    08-04 10:34:28.294719   608  6382 E AudioSink: Unable to create audio track
    08-04 10:34:28.294742   608  6382 D AudioTrack: ~AudioTrack(-1474187544): 0xa5724880
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    从代码中查看上面Error:

    //frameworks/av/services/audioflinger/Tracks.cpp
    
    //createTrack过程中会调用isTrackAllowed_l进行检查(最原始的createTrack起始于AudioFlinger.cpp)
    if (!thread->isTrackAllowed_l(channelMask, format, sessionId, uid)) {
            ALOGE("%s(%d): no more tracks available", __func__, mId);
            releaseCblk(); // this makes the track invalid.
            return;
        }
    
    //frameworks/av/services/audioflinger/Threads.h
    
    static constexpr uint32_t kMaxTracksPerUid = 40;
    
    virtual	bool	isTrackAllowed_l(
    	audio_channel_mask_t channelMask __unused,
    	audio_format_t format __unused,
    	audio_session_t sessionId __unused,
    	uid_t uid) const {
    		//通过trackCountForUid_l函数检查相同uid的最多不能创建超过40个(一直未释放的情况)
    		return trackCountForUid_l(uid) < PlaybackThread::kMaxTracksPerUid
    				&& mTracks.size() < PlaybackThread::kMaxTracks;
    	}
    
    //frameworks/av/services/audioflinger/Threads.cpp
    
    // trackCountForUid_l() must be called with ThreadBase::mLock held
    uint32_t AudioFlinger::PlaybackThread::trackCountForUid_l(uid_t uid) const
    {
        uint32_t trackCount = 0;
        for (size_t i = 0; i < mTracks.size() ; i++) {
        	//相同uid最多不能创建超过40个
            if (mTracks[i]->uid() == uid) {
                trackCount++;
            }
        }
        return trackCount;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    由于在创建提示音的过程中一直未释放资源,未执行析构函数 ~AudioTrack,从而导致40次之后再也无法createTrack。

    (B)插入/拔出充电触发的位置

    //frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
    
    private boolean isChargingFeedbackEnabled(@UserIdInt int userId) {
    		//CHARGING_SOUNDS_ENABLED——这个就是Settings/Sound下的开关按钮
            final boolean enabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.CHARGING_SOUNDS_ENABLED, 1, userId) != 0;
            final boolean dndOff = Settings.Global.getInt(mContext.getContentResolver(),
                    Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
                    == Settings.Global.ZEN_MODE_OFF;
            return enabled && dndOff;
        }
    
    private void updateIsPoweredLocked(int dirty) {
    	//...
    	if (mBootCompleted) {
                        if (mIsPowered && !BatteryManager.isPlugWired(oldPlugType)
                                && BatteryManager.isPlugWired(mPlugType)) {
                            //插入充电执行的操作
                            mNotifier.onWiredChargingStarted(mUserId);
                        } else if (dockedOnWirelessCharger) {
                            mNotifier.onWirelessChargingStarted(mBatteryLevel, mUserId);
                        } else if(!mIsPowered && isChargingFeedbackEnabled(mUserId)){
                        	//拔出充电执行的操作
                            mNotifier.onWiredChargingFinished(mUserId);
                        }
                    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (C)插入/拔出充电执行的步骤

    //frameworks/base/services/core/java/com/android/server/power/Notifier.java
    
    //插入充电的步骤
    public void onWiredChargingStarted(@UserIdInt int userId) {
            if (DEBUG) {
                Slog.d(TAG, "onWiredChargingStarted");
            }
    
            mSuspendBlocker.acquire();
            Message msg = mHandler.obtainMessage(MSG_WIRED_CHARGING_STARTED);
            msg.setAsynchronous(true);
            msg.arg1 = userId;
            mHandler.sendMessage(msg);
        }
    
    private void showWiredChargingStarted(@UserIdInt int userId) {
            playChargingStartedFeedback(userId, false /* wireless */);
            mSuspendBlocker.release();
        }
    
    private void playChargingStartedFeedback(@UserIdInt int userId, boolean wireless) {
            if (!isChargingFeedbackEnabled(userId)) {
                return;
            }
    
            // vibrate
            final boolean vibrate = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.CHARGING_VIBRATION_ENABLED, 1, userId) != 0;
            if (vibrate) {
                mVibrator.vibrate(CHARGING_VIBRATION_EFFECT, VIBRATION_ATTRIBUTES);
            }
    
            // play sound
            final String soundPath = Settings.Global.getString(mContext.getContentResolver(),
                    wireless ? Settings.Global.WIRELESS_CHARGING_STARTED_SOUND
                            : Settings.Global.CHARGING_STARTED_SOUND);
            final Uri soundUri = Uri.parse("file://" + soundPath);
            if (soundUri != null) {
                mRingtone = RingtoneManager.getRingtone(mContext, soundUri);
                if (mRingtone != null) {
                        mRingtone.setStreamType(AudioManager.STREAM_SYSTEM);
                        mRingtone.play();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    //frameworks/base/services/core/java/com/android/server/power/Notifier.java
    
    //拔出充电的步骤
    public void onWiredChargingFinished(@UserIdInt int userId) {
            if (DEBUG) {
                Slog.d(TAG, "onWiredChargingFinished");
            }
    
            mSuspendBlocker.acquire();
            Message msg = mHandler.obtainMessage(MSG_CHARGING_FINISHED);
            msg.setAsynchronous(true);
            msg.arg1 = userId;
            mHandler.sendMessage(msg);
        }
    
    private void showWiredChargingFinished(@UserIdInt int userId) {
            if (DEBUG) {
                Slog.d(TAG, "showWiredChargingFinished");
            }
            playChargingFinishedSound(userId);
            mSuspendBlocker.release();
        }
    
        private void playChargingFinishedSound(@UserIdInt int userId) {
            if (mRingtone != null) {
                if (DEBUG) {
                    Slog.d(TAG, "mRingtone stop");
                }
                mRingtone.stop();
            }
            mRingtone = null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    现在Google源码当中就是没有拔出充电进行release资源的stop()步骤,所以才导致40次之后不能在播放声音的问题,加上这段拔出充电的代码后,每次拔出充电的Log如下:

    08-16 15:56:16.558645   546  6043 D AudioTrack: stop(30): 0xa8d4e380 stop done
    08-16 15:56:16.590319   415   826 D AudioFlinger_Threads: removeTracks_l(62): removing track on session 169
    08-16 15:56:16.990597   546   693 D AudioTrack: ~AudioTrack(30): 0xa8d4e380
    
    • 1
    • 2
    • 3

    可以看到AudioTrack这边可以正常进行release动作了,自此问题解决。

  • 相关阅读:
    2022年全国最新消防设施操作员(中级消防设施操作员)题库及答案
    Redis 哨兵集群方案
    英语写作中“省略”、“忽略”、“忽视”omit、ignore、neglect 的用法
    上半年业绩韧性增强,两大核心业务成第二增长点,商汤用硬科技冲刺AI长跑
    C++模板
    Prometheus监控Linux主机(node-exporter)
    MVP的最佳架构:单体结构、SOA、微服务还是Serverless?
    工业镜头的重要参数之视场、放大倍率、芯片尺寸--51camera
    发布订阅机制和点对点机制
    【数据结构与算法】概论
  • 原文地址:https://blog.csdn.net/dongxianfei/article/details/126390445