Android实现左右声道播放不同音乐(附带源码)

一、项目介绍

1. 背景与作用

在多媒体应用、游戏或音乐播放器中,有时需要将不同的音轨分别输出到左声道和右声道,例如:

  • 立体声演示:一首歌左耳鼓点,右耳主旋律

  • 游戏音效:主角声音在左,环境音在右

  • 听觉训练:语音教材左右声道切换不同练习内容

Android 原生音频 API 支持对音频进行左右声道的平衡(pan)控制,本项目将演示如何同时播放两路音源,并分别将其 panning 到左右声道,实现“左声道播放 A 音乐,右声道播放 B 音乐”的效果。

2. 功能目标

  • 在应用启动后,加载本地或网络音频资源(MP3/OGG)

  • 使用 AudioTrackMediaPlayer + AudioAttributes 播放音频

  • 分别对两个音源设置左/右声道输出

  • 同步控制播放、暂停、停止、音量

  • 封装为复用组件 StereoPlayer,暴露简单 API

  • 兼容 Android 5.0+,处理不同采样率、通道数的音频


二、相关知识

  1. 音频通道基础

    • 单声道(Mono):只有一个通道,播放时同音量输出到左右两边

    • 立体声(Stereo):两个通道,左右声道可分别控制音量

    • 在 Android 中,可通过 setStereoVolume(leftVol, rightVol)setVolume(…) + pan 设置平衡

  2. MediaPlayer vs AudioTrack

    • MediaPlayer:高层封装,易用,支持多种格式,但通道控制能力有限

    • AudioTrack:低层 API,可精细控制 PCM 数据及通道,适用于自定义音频处理

  3. AudioAttributes & AudioFormat

    • Android 5.0+ 推荐使用 AudioAttributes 指定用途(音乐、游戏、语音等)

    • AudioFormat 用于 AudioTrack,指定采样率、位宽、通道数

  4. 线程与同步

    • 音频播放在后台线程执行,确保 UI 交互流畅

    • 同步启动两个音源,需在同一时刻开始播放

  5. 资源管理

    • 播放完成后释放资源:release()stop(),避免内存或句柄泄露

    • 处理应用生命周期:后台暂停、退出时停止播放


三、实现思路

  1. 封装组件

    • StereoPlayer:内部持有两个 MediaPlayer 或两个 AudioTrack 实例

    • API:init(leftRes, rightRes), play(), pause(), stop(), setVolume(l, r), release()

  2. MediaPlayer 方案

    • 使用两个 MediaPlayer 分别加载左右音源

    • 启动时:先调用 prepareAsync(),在 onPrepared 中调用 start()

    • 设置左右声道音量:player.setVolume(1f, 0f) for left, 0f,1f for right

  3. AudioTrack 方案(进阶)

    • 手动读取 PCM 数据并通过双声道格式交叉写入

    • 构造 AudioTrack 时指定 CHANNEL_OUT_STEREO,然后分别写入左右的 PCM buffer

  4. 同步启动

    • 对于 MediaPlayer,可在两个都准备好后同时 start()

    • 对于 AudioTrack,可在同一线程中依次 play()

  5. UI 控制

    • 在布局中提供“播放”、“暂停”、“停止”按钮,及左右音量滑条

    • 绑定到 StereoPlayer API,实时控制


四、环境与依赖

// app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
  compileSdkVersion 34
  defaultConfig {
    applicationId "com.example.stereoplayer"
    minSdkVersion 21
    targetSdkVersion 34
  }
  buildFeatures { viewBinding true }
  kotlinOptions { jvmTarget = "1.8" }
}

dependencies {
  implementation 'androidx.appcompat:appcompat:1.6.1'
  implementation 'androidx.core:core-ktx:1.10.1'
}

五、整合代码

// =======================================================
// 文件: res/layout/activity_main.xml
// 描述: 主界面布局:播放/暂停/停止 按钮 + 声道音量滑条
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:padding="16dp"
    android:layout_width="match_parent" android:layout_height="match_parent">

  <TextView android:text="左声道音量" />
  <SeekBar android:id="@+id/seekLeft" android:layout_width="match_parent"
      android:layout_height="wrap_content" android:max="100"/>

  <TextView android:layout_marginTop="8dp" android:text="右声道音量" />
  <SeekBar android:id="@+id/seekRight" android:layout_width="match_parent"
      android:layout_height="wrap_content" android:max="100"/>

  <LinearLayout android:orientation="horizontal"
      android:layout_marginTop="16dp" android:layout_width="match_parent"
      android:layout_height="wrap_content" android:gravity="center">

    <Button android:id="@+id/btnPlay"  android:text="播放"  android:layout_margin="8dp"
        android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <Button android:id="@+id/btnPause" android:text="暂停" android:layout_margin="8dp"
        android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <Button android:id="@+id/btnStop"  android:text="停止" android:layout_margin="8dp"
        android:layout_width="wrap_content" android:layout_height="wrap_content"/>
  </LinearLayout>
</LinearLayout>


// =======================================================
// 文件: StereoPlayer.kt
// 描述: 左右声道播放不同音源的封装组件(MediaPlayer 方案)
// =======================================================
package com.example.stereoplayer

import android.content.Context
import android.media.MediaPlayer
import android.net.Uri

class StereoPlayer(private val context: Context) {

  private var leftPlayer: MediaPlayer? = null
  private var rightPlayer: MediaPlayer? = null
  private var leftReady = false
  private var rightReady = false

  /** 初始化左右音源 */
  fun init(leftUri: Uri, rightUri: Uri) {
    release()
    leftPlayer = MediaPlayer().apply {
      setDataSource(context, leftUri)
      setVolume(1f, 0f)  // 左声道
      setOnPreparedListener { leftReady = true; tryStart() }
      prepareAsync()
    }
    rightPlayer = MediaPlayer().apply {
      setDataSource(context, rightUri)
      setVolume(0f, 1f)  // 右声道
      setOnPreparedListener { rightReady = true; tryStart() }
      prepareAsync()
    }
  }

  /** 播放,两端都准备好后同时 start */
  private fun tryStart() {
    if (leftReady && rightReady) {
      leftPlayer?.start()
      rightPlayer?.start()
    }
  }

  fun play() {
    leftPlayer?.start(); rightPlayer?.start()
  }
  fun pause() {
    if (leftPlayer?.isPlaying == true) leftPlayer?.pause()
    if (rightPlayer?.isPlaying == true) rightPlayer?.pause()
  }
  fun stop() {
    leftPlayer?.stop(); rightPlayer?.stop()
    leftReady = false; rightReady = false
  }

  /** 动态设置左右声道音量(0f~1f) */
  fun setVolumes(leftVol: Float, rightVol: Float) {
    leftPlayer?.setVolume(leftVol, 0f)
    rightPlayer?.setVolume(0f, rightVol)
  }

  fun release() {
    leftPlayer?.release(); rightPlayer?.release()
    leftPlayer = null; rightPlayer = null
  }
}


// =======================================================
// 文件: MainActivity.kt
// 描述: 示例 Activity:初始化 StereoPlayer 并绑定 UI
// =======================================================
package com.example.stereoplayer

import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.stereoplayer.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding
  private lateinit var player: StereoPlayer

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    player = StereoPlayer(this)
    // 初始化左右声道音源(raw 目录下)
    val leftUri = Uri.parse("android.resource://${packageName}/${R.raw.left_sound}")
    val rightUri= Uri.parse("android.resource://${packageName}/${R.raw.right_sound}")
    player.init(leftUri, rightUri)

    binding.seekLeft.setOnSeekBarChangeListener(object: SimpleSeekListener(){
      override fun onProgressChanged(sb: androidx.appcompat.widget.AppCompatSeekBar, p: Int, u: Boolean) {
        player.setVolumes(p/100f, binding.seekRight.progress/100f)
      }
    })
    binding.seekRight.setOnSeekBarChangeListener(object: SimpleSeekListener(){
      override fun onProgressChanged(sb: androidx.appcompat.widget.AppCompatSeekBar, p: Int, u: Boolean) {
        player.setVolumes(binding.seekLeft.progress/100f, p/100f)
      }
    })

    binding.btnPlay.setOnClickListener  { player.play()  }
    binding.btnPause.setOnClickListener { player.pause() }
    binding.btnStop.setOnClickListener  { player.stop()  }
  }

  override fun onDestroy() {
    super.onDestroy()
    player.release()
  }
}

// =======================================================
// 文件: SimpleSeekListener.kt
// 描述: 简易 SeekBar 监听器,省略不必要方法
// =======================================================
package com.example.stereoplayer

import android.widget.SeekBar

abstract class SimpleSeekListener: SeekBar.OnSeekBarChangeListener {
  override fun onStartTrackingTouch(p0: SeekBar?) {}
  override fun onStopTrackingTouch(p0: SeekBar?) {}
}

// =======================================================
// 文件: res/raw/left_sound.mp3, right_sound.mp3
// 描述: 左右声道示例音频文件,放置在 res/raw
// =======================================================

六、代码解读

  1. 布局

    • 两个 SeekBar 控制左右声道音量,三个按钮控制播放生命周期。

  2. StereoPlayer

    • 使用两个 MediaPlayer 同步播放,setVolume(1f,0f) 强制只输出到某侧

    • 在两端 onPrepared 回调后再 start(),保证同步开始

    • setVolumes 方法可在播放中动态调整声道平衡

  3. MainActivity

    • 通过 Uri.parse("android.resource://…") 加载 res/raw 下的音频

    • 绑定 SeekBar 与音量控制,btnPlay/btnPause/btnStop 分别调度对应方法

  4. 资源文件

    • res/raw 目录放置两个示例音频文件,命名为 left_sound.mp3right_sound.mp3


七、性能与优化

  1. 延迟加载与预加载

    • 对于大音频文件,可在界面加载时后台 prepare(),避免点击时卡顿

  2. AudioTrack 方案

    • 若需更精细的延迟和同步,可使用 AudioTrack 直接写 PCM 数据,并通过双声道 interleaving 控制左右

  3. 格式兼容

    • MediaPlayer 支持多种格式,若遇到兼容性问题可转 PCM 或 AAC

  4. 线程安全

    • 保证所有 MediaPlayer 操作在主线程或同一线程中进行,避免状态机冲突


八、项目总结与拓展

本文演示了在 Android 中如何使用两个 MediaPlayer 实现左右声道播放不同音频,并封装为 StereoPlayer 组件,提供初始化、播放、暂停、停止、声道音量调整等 API。

拓展思路

  1. 基于 AudioTrack:实现精确同步与音效处理

  2. 使用 ExoPlayer:更强大的流媒体支持与高级音频效果

  3. 网络音源:支持 HTTP URL,边播边缓存

  4. 环绕声:结合多声道输出(5.1/7.1)做更复杂的空间音频


九、FAQ

Q1:为什么两个 MediaPlayer 同步播放仍有延迟?
A1:MediaPlayer.prepareAsync() 各自独立,onPrepared` 回调可能存在数十毫秒差。可使用 AudioTrack PCM 方案或 ExoPlayer 的同步播放 API。

Q2:如何在后台播放?
A2:将 StereoPlayer 放入 ServiceForegroundService,并在通知中控制。

Q3:如何处理音频焦点?
A3:在播放前请求 AudioManager.requestAudioFocus(),并处理失去焦点时暂停或降低音量。

Q4:左右声道切换失效怎么办?
A4:检查音源是否为单声道文件;setVolume(1f,0f) 只能在立体声流上生效。可先转换音源为立体声。

Q5:如何添加淡入淡出效果?
A5:周期性调用 setVolume() 从 0 增到目标值(淡入),再从目标值到 0(淡出),可用 ValueAnimator 实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值