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
相关产品推荐
相关产品推荐

