Media3 ExoPlayer扩展切换声道能力

ExoPlayer并没有提供切换左右声道的接口,对于单音轨歌曲切换原伴唱就需要自己实现切换左右声道的能力了。

1.音频存储

PCM (脉冲编码调制)是音频的原始数字表示形式,对于立体声(双声道),其数据通常是交错存储的。

一个 16 位(2 字节)、44.1kHz 的立体声 PCM 数据流,它的排列会是这样的:

css 复制代码
[左声道样本1字节1][左声道样本1字节2][右声道样本1字节1][右声道样本1字节2][左声道样本2字节1][左声道样本2字节2][右声道样本2字节1][右声道样本2字节2]...

可以简化为: L1, R1, L2, R2, L3, R3, ...

2.实现方式

基于上述格式,我们可以通过不同的算法来实现 "切换左声道" 的效果:

以右声道为例:让左声道没有声音,原始:L1, R1, L2, R2, ... 处理后:0, R1, 0, R2, ...

或者左右声道都填充左声道,保持立体声和简化计算,原始:L1, R1, L2, R2, ... 处理后:L1, L1, L2, L2, ...

3.音频处理流程

AudioProcessor 采用处理器链(AudioProcessorChain)设计模式。它可以将解码后的音频数据通过一系列处理器转换后输出到 AudioTrack。开发者可以自定义 AudioProcessor 插入处理链,以实现对音频数据的特定处理,如切换左右声道。

4.AudioProcessor

AudioProcessor 是 ExoPlayer 中负责音频数据处理的核心组件之一,核心方法包括:

  • configure():初始化处理器(传入音频的采样率、声道数、编码格式)
  • isActive():返回处理器是否处于激活状态(如 "仅立体声时才处理声道切换")
  • queueInput(ByteBuffer):接收输入的音频数据,在这里实现自定义处理逻辑(比如修改声道数据)
  • getOutput():返回处理后的音频数据。
  • flush()/reset():用于播放器状态切换(如暂停、停止)时清空缓冲区、重置状态。

当然ExoPlayer还提供了音频处理器的基类BaseAudioProcessor , 会把 AudioProcessor 接口的复杂逻辑封装,只暴露几个核心抽象方法。

5.代码实现

因为ExoPlayer默认走会ToInt16PcmAudioProcessor , 将不同格式的PCM音频编码转换为16位PCM格式,只处理16位PCM立体声音频数据。

Java 复制代码
@UnstableApi
public class SwitchChannelAudioProcessor extends BaseAudioProcessor {

    private static final String TAG = "SwitchChannelProcessor";

    /**
     * 表示每个声道的字节数
     */
    private int bytesPerChannel;

    /**
     * 当前声道模式
     */
    private int channelMode = C.CHANNEL_MODE_NONE;

    /**
     * 设置声道模式
     *
     * @param mode 声道模式
     */
    public void setAudioChannelMode(int mode) {
        this.channelMode = mode;
    }

    @Override
    protected AudioFormat onConfigure(AudioFormat inputAudioFormat)
            throws UnhandledAudioFormatException {
        Log.d(TAG, "onConfigure inputAudioFormat:" + inputAudioFormat);
        // 只处理16位PCM立体声音频数据 
        if (inputAudioFormat.channelCount != 2 || inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
            return AudioFormat.NOT_SET;
        }
        this.bytesPerChannel = Util.getPcmFrameSize(inputAudioFormat.encoding, 1);
        return inputAudioFormat;
    }

    @Override
    public void queueInput(@NonNull ByteBuffer inputBuffer) {
        // 判断输入缓冲区是否为空
        int remaining = inputBuffer.remaining();
        if (remaining == 0) {
            return;
        }
        // 获取输入缓冲区的剩余字节数
        int position = inputBuffer.position();
        int limit = inputBuffer.limit();
        int size = limit - position;
        // 创建输出缓冲区
        ByteBuffer outputBuffer = replaceOutputBuffer(size);
        try {
            // 立体声模式(包括默认情况)
            if (channelMode == C.CHANNEL_MODE_NONE || channelMode == C.CHANNEL_MODE_STEREO) {
                // 直接复制输入数据到输出缓冲区
                outputBuffer.put(inputBuffer);
            } else {
                // 处理PCM数据
                while (position <= limit - bytesPerChannel * 2) {
                    // 读取左声道数据(前x字节)
                    short leftSample = inputBuffer.getShort(position);
                    position += bytesPerChannel;

                    // 读取右声道数据(后x字节)
                    short rightSample = inputBuffer.getShort(position);
                    position += bytesPerChannel;

                    // 根据声道模式处理数据
                    switch (channelMode) {
                        case C.CHANNEL_MODE_LEFT_ONLY:
                            // 仅左声道
                            outputBuffer.putShort(leftSample);
                            outputBuffer.putShort(leftSample);
                            break;
                        case C.CHANNEL_MODE_RIGHT_ONLY:
                            // 仅右声道
                            outputBuffer.putShort(rightSample);
                            outputBuffer.putShort(rightSample);
                            break;
                        case C.CHANNEL_MODE_SWAP:
                            // 声道反转:左右声道互换
                            outputBuffer.putShort(rightSample);
                            outputBuffer.putShort(leftSample);
                            break;
                    }
                }
                //更新输入缓冲区位置
                inputBuffer.position(position);
            }
        } catch (Exception e) {
            Log.e(TAG, "Error processing audio data", e);
            inputBuffer.position(limit);
        }
        // 输出缓冲区准备读取
        outputBuffer.flip();
    }
}
相关推荐
字节跳动视频云技术团队1 天前
把 AI 视频的钱花在刀刃上,不是每一刀上
人工智能·音视频开发
大辉狼_音频架构4 天前
第 02 篇:时域的语言 —— 卷积、LTI 系统与冲激响应
算法·音视频开发
大辉狼_音频架构4 天前
第 01 篇:音频采样与量化 —— 从 I2S 帧到奈奎斯特定理
嵌入式·音视频开发
qingmiaozhuan6 天前
区域录屏窗口位移录制框固定?GDI+win32gui动态坐标追踪4种代码方案
音视频开发·录屏工具·录屏技术·电脑录屏
KnowFun9 天前
互动视频的前端实现:有向图建模、分支预加载与尾帧管线
音视频开发
大辉狼_音频架构14 天前
AudioReach Plugin 机制问题解答
嵌入式·音视频开发
大辉狼_音频架构15 天前
AudioReach:Tinyalsa PCM Plugin 机制
嵌入式·音视频开发
老孙讲技术15 天前
凌晨三点报警全乱了:一个 callback 地址,如何把设备托管消息拆成多客户流水线?
物联网·音视频开发
ltlovezh19 天前
点播 Seek 性能优化:丢帧的原理、判定与硬解落地
音视频开发
音视频牛哥20 天前
把Android设备变成RTSP网络摄像机:SmartMediaKit 后台采集、编码与轻量级RTSP服务实践
音视频开发·视频编码·直播