Android AudioManager处理两个播放器同时有声音,停止其中一个播放的问题

本文介绍Android 2.2及以上版本中音频焦点管理机制,通过AudioManager实现应用间音频输出协调,避免多应用同时播放造成用户体验下降。文章详细阐述了如何请求、监听及放弃音频焦点,并解释了不同焦点变化的含义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     尽管某个时刻只有一个activity可以运行,Android却是一个多任务环境.这对使用音频的应用带来了特殊的挑战,因为只有一个音频输出而可能多个媒体都想用它.在Android2.2之前,没有内建的机制来处理这个问题,所以可能在某些情况下导致坏的用户体验.例如,当一个用户正在听音乐而另一个应用需要通知用户一些重要的事情时,用户可能由于音乐声音大而不能听的通知.从Android2.2开始,平台为应用提供了一个协商它们如何使用设备音频输出的途径,这个机制叫做音频焦点,AudioManager。

    当你的应用需要输出像乐音和通知之类的音频时,你应该总是请求音频焦点.一旦应用具有了焦点,它就可以自由的使用音频输出.但它总是应该监听焦点的变化.如果被通知丢失焦点,它应该立即杀死声音或降低到静音水平(有一个标志表明应选择哪一个)并且仅当重新获得焦点后才恢复大声播放。

  首先,注册一个焦点监听器OnAudioFocusChangeListener 。

private AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener = null;
		//Build.VERSION.SDK_INT表示当前SDK的版本,Build.VERSION_CODES.ECLAIR_MR1为SDK 7版本 ,
		  //因为AudioManager.OnAudioFocusChangeListener在SDK8版本开始才有。
		if(Build.VERSION.SDK_INT > Build.VERSION_CODES.ECLAIR_MR1){
			mAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
				@Override
				public void onAudioFocusChange(int focusChange) {
					if(focusChange == AudioManager.AUDIOFOCUS_LOSS){
						//失去焦点之后的操作
						if(isPlaying()){
							pause();
						}
					}else if(focusChange == AudioManager.AUDIOFOCUS_GAIN){
						//获得焦点之后的操作
					}
				}
			};
		}

要请求音频焦点,你必须从AudioManager mAudioMgr调用requestAudioFocus(),如下所示:

private void requestAudioFocus() {
		if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.ECLAIR_MR1){
			return;
		}
		if (mAudioMgr == null)
			mAudioMgr = (AudioManager) ApplicationEx.app
					.getSystemService(Context.AUDIO_SERVICE);
		if (mAudioMgr != null) {
			DLog.i(TAG, "Request audio focus");
			int ret = mAudioMgr.requestAudioFocus(mAudioFocusChangeListener,
					AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
			if (ret != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
				DLog.i(TAG, "request audio focus fail. " + ret);
			}
		}

	}


放弃焦点

private void abandonAudioFocus() {
		if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.ECLAIR_MR1){
			return;
		}
		if (mAudioMgr != null) {

			DLog.i(TAG, "Abandon audio focus");

			mAudioMgr.abandonAudioFocus(mAudioFocusChangeListener);

			mAudioMgr = null;
		}
	}

参数focusChange告诉你音频焦点如何发生了变化,它可以是以上几种值(它们都是定义在AudioManager中的常量):

  • AUDIOFOCUS_GAIN:你已获得了音频焦点.

  • AUDIOFOCUS_LOSS:你已经丢失了音频焦点比较长的时间了.你必须停止所有的音频播放.因为预料到你可能很长时间也不能再获音频焦点,所以这里是清理你的资源的好地方.比如,你必须释放MediaPlayer.

  • AUDIOFOCUS_LOSS_TRANSIENT:你临时性的丢掉了音频焦点,很快就会重新获得.你必须停止所有的音频播放,但是可以保留你的资源,因为你可能很快就能重新获得焦点.

  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你临时性的丢掉了音频焦点,但是你被允许继续以低音量播放,而不是完全停止.

当第三方播放器也使用了这个机制的话,当你的应用获取声音焦点之后,第三方播放器失去焦点,做了暂停处理,即会停止播放。这样就不会出现两个播放器同时播放音乐的情况了。而如果第三方播放器没有经过处理,不管有没有焦点都一直播放,那就没有办法了。 =。=||



 

<think>我们正在处理一个连续PCM数据的播放场景,数据源是ByteBuffer。在Android平台上,通常使用AudioTrack来播放PCM音频。由于数据是连续的,我们需要使用AudioTrack的流模式(STREAM模式)。关键点:1.初始化AudioTrack:需要指定采样率、声道配置、音频格式和缓冲区大小。2.数据写入:将ByteBuffer中的数据写入AudioTrack。由于ByteBuffer可能是直接的(Direct)或者非直接的(Heap),但AudioTrack的write方法可以接受ByteBuffer(从APIlevel3开始就有支持ByteBuffer的write方法,但更广泛支持是在APIlevel21的write(ByteBuffer,int,int)方法,不过我们通常使用write(ByteBuffer,int,int)或者更简单的write(ByteBuffer,int)方法。注意,在旧版本中,我们可能需要将ByteBuffer转换为byte数组,但为了效率,我们尽量直接使用ByteBuffer。但是,请注意:AudioTrack在写入数据时是阻塞的,直到数据被写入音频子系统。因此,我们需要在一个单独的线程中写入数据以避免阻塞主线程。步骤:1.配置AudioTrack参数。2.创建AudioTrack实例,使用STREAM模式。3.启动播放(调用play方法)。4.在一个循环中,从ByteBuffer中读取数据并写入AudioTrack,直到数据结束。5.停止和释放资源。注意:由于是连续数据,我们可能需要不断地从网络或文件读取数据,这里假设我们已经有一个不断提供数据的源(例如,通过回调或循环读取)。示例代码结构:我们将创建一个播放线程,该线程负责从ByteBuffer队列中取出数据并播放。但注意,用户的需求是直接从一个ByteBuffer播放连续数据。实际上,连续数据可能意味着多个ByteBuffer块。因此,我们通常会使用一个队列来管理这些数据块。不过,如果用户是一次性将整个PCM数据放入一个ByteBuffer(不太可能,因为PCM数据流很大),那么我们可以直接循环写入。但更常见的是分块。因此,我们设计一个简单的播放器,它有一个写入线程,不断从数据源获取ByteBuffer块并写入AudioTrack。由于问题要求示例代码,我们假设数据源可以不断地提供ByteBuffer(例如,每次提供4096字节)。我们使用一个阻塞队列来缓存这些ByteBuffer块。但注意:用户的问题中并没有提到数据源是如何提供数据的,所以我们将分两种情况:情况1:整个PCM数据在一个ByteBuffer中(但通常很大,所以需要分块写入)情况2:数据源会连续产生多个ByteBuffer块(更常见)由于问题描述是“连续PCM数据”,我们按照情况2来处理。设计:1.创建一个类:ByteBufferAudioPlayer。2.它包含一个AudioTrack实例和一个线程(或ExecutorService)用于播放。3.使用一个阻塞队列(例如LinkedBlockingQueue)来存放待播放的ByteBuffer块。4.提供写入方法:将ByteBuffer块放入队列。5.播放线程循环从队列中取出ByteBuffer块并写入AudioTrack,直到收到停止信号。但是,用户问题中并没有提到队列,而是直接要求从ByteBuffer播放。为了简单起见,我们先给出一个简单的例子:假设我们有一个大的ByteBuffer,我们将其分割成多个小块然后写入。但这样不够连续(因为数据源可能是实时的)。因此,我们采用队列的方式。然而,为了更贴近用户的问题,我们也可以提供另一种方式:用户不断调用write方法提供ByteBuffer块,然后我们在内部进行播放。这里我们提供两种方案:方案一:用户提供整个ByteBuffer(但很大,我们分块写入)方案二:用户分块提供ByteBuffer(通过一个方法不断调用)由于问题中说“连续”,我们选择方案二。示例代码:我们将创建一个播放器类,它包含一个播放线程和一个阻塞队列。我们提供开始、停止、写入方法。注意:写入方法是非阻塞的,它只是将数据放入队列。播放线程负责从队列中取数据并写入AudioTrack。但是,我们需要控制队列的大小,避免内存溢出。因此,当队列满时,写入方法可能会阻塞。参数选择:采样率(sampleRate):常见44100Hz声道(channelConfig):AudioFormat.CHANNEL_OUT_MONO或CHANNEL_OUT_STEREO音频格式(audioFormat):AudioFormat.ENCODING_PCM_16BIT(常见)缓冲区大小:通过AudioTrack.getMinBufferSize获取。代码示例:注意:我们使用AudioTrack.MODE_STREAM。步骤:1.初始化AudioTrack。2.开始播放线程。3.提供写入方法,将ByteBuffer块放入队列。4.播放线程循环从队列中取数据,写入AudioTrack。5.停止时,清空队列并停止AudioTrack。另外,我们还需要处理结束播放的情况,例如当写入一个结束标记时。由于ByteBuffer可能包含有效数据长度,我们假设每个ByteBuffer在放入队列时已经设置好limit(即数据长度)。我们在写入AudioTrack时使用:audioTrack.write(byteBuffer,byteBuffer.remaining())或者更精确地,我们使用一个指定长度的写入。但注意:AudioTrack的write(ByteBuffer,int)方法第二个参数是写入的长度(单位是字节)。所以我们可以这样写:示例代码:注意:我们使用一个标志位来通知播放线程停止,并且当队列为空时,播放线程需要等待。我们使用一个特殊的结束标记来通知播放线程没有更多数据了。我们定义:当需要停止时,我们向队列中放入一个特殊的ByteBuffer(例如一个0长度的ByteBuffer)作为结束标记。下面开始写代码:注意:需要处理AudioTrack的写入错误情况(例如写入失败,返回负值)。另外,由于播放是实时的,我们需要注意缓冲区大小和队列大小,以避免欠载(underflow)或溢出(overflow)。我们使用LinkedBlockingQueue,并设置一个最大容量(例如10个块),当队列满时,写入方法会阻塞,这样就不会无限制地消耗内存。但是,为了简单,我们先不设置队列容量(无界队列),但在实际应用中需要设置。现在,我们写一个简单的示例:类定义:</think>### Android播放ByteBuffer连续PCM数据的实现方案 在Android平台上播放来自ByteBuffer的连续PCM数据,核心是使用`AudioTrack`类。以下是完整实现方案: #### 1. 核心实现代码 ```java import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; public class ByteBufferPlayer { private AudioTrack audioTrack; private Thread playbackThread; private volatile boolean isPlaying = false; private final int sampleRate; // 如44100 private final int channelConfig; // 如AudioFormat.CHANNEL_OUT_MONO private final int audioFormat; // 如AudioFormat.ENCODING_PCM_16BIT public ByteBufferPlayer(int sampleRate, int channelConfig, int audioFormat) { this.sampleRate = sampleRate; this.channelConfig = channelConfig; this.audioFormat = audioFormat; } public void start() { if (isPlaying) return; int minBufferSize = AudioTrack.getMinBufferSize( sampleRate, channelConfig, audioFormat ); audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, minBufferSize, AudioTrack.MODE_STREAM ); audioTrack.play(); isPlaying = true; playbackThread = new Thread(this::playbackLoop); playbackThread.start(); } private void playbackLoop() { while (isPlaying) { // 从数据源获取ByteBuffer(需自定义实现) ByteBuffer pcmBuffer = getNextPcmBuffer(); if (pcmBuffer != null && pcmBuffer.remaining() > 0) { // 直接写入ByteBuffer int bytesWritten = audioTrack.write( pcmBuffer, pcmBuffer.remaining(), AudioTrack.WRITE_BLOCKING ); // 处理写入异常 if (bytesWritten < 0) { handleWriteError(bytesWritten); } } } } public void stop() { isPlaying = false; try { if (playbackThread != null) { playbackThread.join(500); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (audioTrack != null) { audioTrack.stop(); audioTrack.release(); } } } // 自定义数据源(示例) private native ByteBuffer getNextPcmBuffer(); } ``` #### 2. 关键技术点 1. **AudioTrack配置**: - **流模式**:使用`MODE_STREAM`支持连续数据流[^1] - **缓冲区计算**:`getMinBufferSize()`确定最小缓冲区 - **直接写入**:`write(ByteBuffer, int, int)`直接处理ByteBuffer 2. **性能优化**: ```java // 使用直接内存缓冲区减少拷贝 ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize); ``` 直接缓冲区(Direct ByteBuffer)避免JVM堆与本地堆间的数据拷贝[^1] 3. **实时性保障**: - `WRITE_BLOCKING`模式确保及时写入音频子系统 - 独立播放线程避免主线程阻塞 - 双缓冲机制防止音频断流 #### 3. 完整使用示例 ```java // 初始化播放器 ByteBufferPlayer player = new ByteBufferPlayer( 44100, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT ); // 开始播放 player.start(); // 在数据源线程填充数据 new Thread(() -> { while (hasMoreData) { ByteBuffer pcmData = generatePcmData(); // 生成PCM数据 player.enqueueBuffer(pcmData); // 添加自定义入队方法 } }).start(); // 停止播放 player.stop(); ``` #### 4. 音频参数参考值 | 参数 | 常用值 | 说明 | |------|--------|------| | 采样率 | 44100 Hz | CD音质标准 | | 声道配置 | `CHANNEL_OUT_MONO` | 单声道 | | | `CHANNEL_OUT_STEREO` | 立体声 | | 音频格式 | `ENCODING_PCM_16BIT` | 16位采样 | | | `ENCODING_PCM_FLOAT` | 32位浮点 | #### 5. 错误处理建议 ```java private void handleWriteError(int errorCode) { switch (errorCode) { case AudioTrack.ERROR_INVALID_OPERATION: // 错误处理逻辑 break; case AudioTrack.ERROR_BAD_VALUE: // 缓冲区大小异常 break; case AudioTrack.ERROR_DEAD_OBJECT: // 音频服务异常 recreateAudioTrack(); break; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值