You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android:从立体声MP3提取单声道并转为双声道AudioStream播放

解决MP3左声道提取并转为双声道播放的问题

你这是踩了个典型的坑——直接读取MP3的原始字节根本没法直接处理声道!因为MP3是压缩编码的音频格式,你读出来的soundBytes1是压缩后的编码数据,不是能直接操作的原始PCM音频采样,自然没法被识别成有效的音频流。

下面给你一套可行的实现方案,核心分为三个步骤:解码MP3到PCM原始数据、提取左声道并生成双声道PCM、将处理后的PCM转为可播放的音频流(或直接播放)。

步骤1:解码MP3为PCM原始数据

我们用Android原生的MediaExtractor和MediaCodec来解码MP3,拿到未经压缩的PCM数据:

private byte[] decodeMP3ToPCM(int rawResId) throws IOException {
    MediaExtractor extractor = new MediaExtractor();
    extractor.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/" + rawResId));
    
    // 找到音频轨道
    int audioTrackIndex = -1;
    for (int i = 0; i < extractor.getTrackCount(); i++) {
        MediaFormat format = extractor.getTrackFormat(i);
        String mime = format.getString(MediaFormat.KEY_MIME);
        if (mime.startsWith("audio/")) {
            audioTrackIndex = i;
            break;
        }
    }
    if (audioTrackIndex == -1) {
        throw new IOException("未找到音频轨道");
    }

    extractor.selectTrack(audioTrackIndex);
    MediaFormat format = extractor.getTrackFormat(audioTrackIndex);
    MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
    codec.configure(format, null, null, 0);
    codec.start();

    ByteBuffer[] inputBuffers = codec.getInputBuffers();
    ByteBuffer[] outputBuffers = codec.getOutputBuffers();
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    ByteArrayOutputStream pcmOutputStream = new ByteArrayOutputStream();

    boolean decodeDone = false;
    while (!decodeDone) {
        // 喂入解码数据
        int inputBufferIndex = codec.dequeueInputBuffer(10000);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            int sampleSize = extractor.readSampleData(inputBuffer, 0);
            if (sampleSize < 0) {
                codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                decodeDone = true;
            } else {
                codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0);
                extractor.advance();
            }
        }

        // 读取解码后的PCM数据
        int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 10000);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
            byte[] chunk = new byte[bufferInfo.size];
            outputBuffer.get(chunk);
            outputBuffer.clear();
            pcmOutputStream.write(chunk);
            codec.releaseOutputBuffer(outputBufferIndex, false);
            
            outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                decodeDone = true;
            }
        }
    }

    codec.stop();
    codec.release();
    extractor.release();
    return pcmOutputStream.toByteArray();
}

步骤2:提取左声道并生成双声道PCM

假设解码后的PCM是16位深度、立体声、小端字节序(MP3解码后的常见格式),每个采样占2字节,声道顺序是左、右交替存储。我们把左声道的每个采样复制一份,组成新的双声道数据:

private byte[] convertLeftToStereo(byte[] pcmData, int sampleRate, int bitDepth) {
    int bytesPerSample = bitDepth / 8;
    int bytesPerFrame = bytesPerSample * 2; // 立体声每帧占4字节(左+右)
    int frameCount = pcmData.length / bytesPerFrame;

    ByteArrayOutputStream stereoOutputStream = new ByteArrayOutputStream();
    ByteBuffer buffer = ByteBuffer.wrap(pcmData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);

    for (int i = 0; i < frameCount; i++) {
        // 读取左声道采样
        short leftSample = buffer.getShort();
        // 将左声道采样写入两次(左+右声道都用左声道数据)
        byte[] sampleBytes = new byte[]{(byte)(leftSample & 0xFF), (byte)((leftSample >> 8) & 0xFF)};
        stereoOutputStream.write(sampleBytes);
        stereoOutputStream.write(sampleBytes);
    }
    return stereoOutputStream.toByteArray();
}

步骤3:生成可识别的AudioStream或直接播放

如果需要生成能被播放器识别的音频流,我们可以把处理后的PCM封装成WAV格式(WAV是带头部的PCM,播放器可直接识别):

private byte[] pcmToWav(byte[] pcmData, int sampleRate, int bitDepth, int channels) {
    ByteArrayOutputStream wavOutputStream = new ByteArrayOutputStream();
    // WAV文件头(固定44字节)
    byte[] header = new byte[44];
    int totalAudioLen = pcmData.length;
    int totalDataLen = totalAudioLen + 36;
    long byteRate = sampleRate * channels * bitDepth / 8;

    // 填充RIFF标识
    header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
    // 文件总大小
    header[4] = (byte) (totalDataLen & 0xff);
    header[5] = (byte) ((totalDataLen >> 8) & 0xff);
    header[6] = (byte) ((totalDataLen >> 16) & 0xff);
    header[7] = (byte) ((totalDataLen >> 24) & 0xff);
    // WAVE标识
    header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
    // fmt子块标识
    header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
    // fmt子块大小(PCM为16)
    header[16] = 16; header[17] = 0;
    // 音频格式(PCM为1)
    header[18] = 1; header[19] = 0;
    // 声道数
    header[20] = (byte) channels; header[21] = 0;
    // 采样率
    header[22] = (byte) (sampleRate & 0xff);
    header[23] = (byte) ((sampleRate >> 8) & 0xff);
    header[24] = (byte) ((sampleRate >> 16) & 0xff);
    header[25] = (byte) ((sampleRate >> 24) & 0xff);
    // 字节率
    header[26] = (byte) (byteRate & 0xff);
    header[27] = (byte) ((byteRate >> 8) & 0xff);
    header[28] = (byte) ((byteRate >> 16) & 0xff);
    header[29] = (byte) ((byteRate >> 24) & 0xff);
    // 块对齐
    header[30] = (byte) (channels * bitDepth / 8); header[31] = 0;
    // 位深度
    header[32] = (byte) bitDepth; header[33] = 0;
    // data子块标识
    header[34] = 'd'; header[35] = 'a'; header[36] = 't'; header[37] = 'a';
    // 音频数据大小
    header[38] = (byte) (totalAudioLen & 0xff);
    header[39] = (byte) ((totalAudioLen >> 8) & 0xff);
    header[40] = (byte) ((totalAudioLen >> 16) & 0xff);
    header[41] = (byte) ((totalAudioLen >> 24) & 0xff);

    try {
        wavOutputStream.write(header);
        wavOutputStream.write(pcmData);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return wavOutputStream.toByteArray();
}

最终调用示例

把这些方法串起来使用:

try {
    // 1. 解码MP3为PCM
    byte[] pcmData = decodeMP3ToPCM(R.raw.flume);
    // 从MediaFormat中获取真实参数(避免硬编码)
    MediaExtractor tempExtractor = new MediaExtractor();
    tempExtractor.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.flume));
    MediaFormat format = tempExtractor.getTrackFormat(0);
    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    int bitDepth = 16; // MP3解码后通常是16位
    tempExtractor.release();
    
    // 2. 左声道转双声道
    byte[] stereoPcm = convertLeftToStereo(pcmData, sampleRate, bitDepth);
    // 3. 转为WAV字节数组(可用于创建AudioStream或保存)
    byte[] wavData = pcmToWav(stereoPcm, sampleRate, bitDepth, 2);

    // 也可以直接用AudioTrack播放
    AudioTrack audioTrack = new AudioTrack(
            AudioManager.STREAM_MUSIC,
            sampleRate,
            AudioFormat.CHANNEL_OUT_STEREO,
            AudioFormat.ENCODING_PCM_16BIT,
            stereoPcm.length,
            AudioTrack.MODE_STATIC
    );
    audioTrack.write(stereoPcm, 0, stereoPcm.length);
    audioTrack.play();
} catch (IOException e) {
    e.printStackTrace();
}

关键注意点

  • 解码时尽量从MediaFormat中读取真实的采样率、声道数等参数,不要硬编码,避免适配问题。
  • 如果你的MP3是其他位深度或声道格式,需要调整convertLeftToStereo中的处理逻辑。
  • 操作PCM时要注意字节序(一般是小端),否则会出现声音失真的情况。

内容的提问来源于stack exchange,提问作者Neil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:32:25