8

我使用 jLayer 来解码 MP3 数据,这个调用:

SampleBuffer output = (SampleBuffer) decoder.decodeFrame(frameHeader, bitstream);

这个返回解码数据的调用返回一个 short[] 数组。 output.getBuffer();

当我使用该方法调用 AudioTrack write() 时,它会在我循环浏览文件时正常播放:

at.write(output.getBuffer(), 0, output.getBuffer().length);

但是,当我使用此答案中的任何方法将 short[] 数组转换为 byte[] 数组时:https ://stackoverflow.com/a/12347176/1176436声音会失真和抖动:

at.write(output.getBuffer(), 0, output.getBuffer().length);

变成:

byte[] array = ShortToByte_Twiddle_Method(output.getBuffer());
at.write(array,  0,  array.length);

我做错了什么,我能做些什么来解决它?不幸的是,我需要将 pcm 数据放在我正在使用的另一个 3rd 方库的字节数组中。如果这很重要,文件是 22kHz,这就是实例化的方式:

at = new AudioTrack(AudioManager.STREAM_MUSIC, 22050, AudioFormat.CHANNEL_OUT_STEREO,
                AudioFormat.ENCODING_PCM_16BIT, 10000 /* 10 second buffer */,
                AudioTrack.MODE_STREAM);   

非常感谢你。

编辑:这就是我现在实例化 AudioTrack 变量的方式。因此,对于 44kHz 文件,发送的值为 44100,而对于 22kHz 文件,发送值为 22050。

at = new AudioTrack(AudioManager.STREAM_MUSIC, decoder.getOutputFrequency(), 
                                  decoder.getOutputChannels() > 1 ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO,
                                  AudioFormat.ENCODING_PCM_16BIT, 10000 /* 10 second buffer */,
                                  AudioTrack.MODE_STREAM);

这是解码方法:

public byte[] decode(InputStream inputStream, int startMs, int maxMs) throws IOException {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024);

        float totalMs = 0;
        boolean seeking = true;

        try {
            Bitstream bitstream = new Bitstream(inputStream);
            Decoder decoder = new Decoder();

            boolean done = false;
            while (!done) {
                Header frameHeader = bitstream.readFrame();
                if (frameHeader == null) {
                    done = true;
                } else {
                    totalMs += frameHeader.ms_per_frame();

                    if (totalMs >= startMs) {
                        seeking = false;
                    }

                    if (!seeking) {
                        // logger.debug("Handling header: " + frameHeader.layer_string());
                        SampleBuffer output = (SampleBuffer) decoder.decodeFrame(frameHeader, bitstream);                            

                        short[] pcm = output.getBuffer();
                        for (short s : pcm) {
                            outStream.write(s & 0xff);
                            outStream.write((s >> 8) & 0xff);
                        }
                    }

                    if (totalMs >= (startMs + maxMs)) {
                        done = true;
                    }
                }
                bitstream.closeFrame();
            }

            return outStream.toByteArray();
        } catch (BitstreamException e) {
            throw new IOException("Bitstream error: " + e);
        } catch (DecoderException e) {
            throw new IOException("Decoder error: " + e);
        }
    }

听起来是这样的(等待几秒钟):https ://vimeo.com/60951237 (这是实际文件:http ://www.tonycuffe.com/mp3/tail%20toddle.mp3 )

编辑:我很想分红,但我把赏金给了比尔,把接受的答案给了尼尔。两者都是巨大的帮助。对于那些想知道的人,我最终重写了 Sonic 原生代码,它帮助我完成了这个过程。

4

2 回答 2

4

正如@Bill Pringlemeir 所说,问题在于您的转换方法实际上并没有转换。short 是一个 16 位的数字;一个字节是一个 8 位数字。您选择的方法不会转换短裤的内容(即内容从 16 位变为 8 位),它改变了存储相同位集合的方式。正如你所说,你需要这样的东西:

SampleBuffer output = (SampleBuffer) decoder.decodeFrame(frameHeader, bitstream);
byte[] array = MyShortToByte(output.getBuffer());
at.write(array,  0,  array.length);

@Bill Pringlemeir 的方法相当于将所有短裤除以 256 以确保它们适合字节范围:

byte[] MyShortToByte(short[] buffer) {
    int N = buffer.length;
    ByteBuffer byteBuf = ByteBuffer.allocate(N);
    while (N >= i) {
        byte b = (byte)(buffer[i]/256);  /*convert to byte. */
        byteBuf.put(b);
        i++;
    }
    return byteBuf.array();
}

这会起作用,但可能会给你非常安静、前卫的音调。如果您负担得起处理时间,则两次通过的方法可能会产生更好的结果:

byte[] MyShortToByte(short[] buffer) {
    int N = buffer.length;
    short min = 0;
    short max = 0;
    for (int i=0; i<N; i++) {
         if (buffer[i] > max) max = buffer[i];
         if (buffer[i] < min) min = buffer[i];
         }
    short scaling = 1+(max-min)/256; // 1+ ensures we stay within range and guarantee no divide by zero if sequence is pure silence ...

    ByteBuffer byteBuf = ByteBuffer.allocate(N);
    for (int i=0; i<N; i++) {
        byte b = (byte)(buffer[i]/scaling);  /*convert to byte. */
        byteBuf.put(b);
    }
    return byteBuf.array();
}

同样,请注意已签名/未签名的问题。以上作品有符号->有符号和无符号->无符号;但不在两者之间。可能是您正在阅读带符号的短裤(-32768-32767),但需要输出无符号字节(0-255),...

如果您负担得起处理时间,更精确(更顺畅)的方法是通过浮点数(这也解决了签名/未签名问题):

byte[] MyShortToByte(short[] buffer) {
    int N = buffer.length;
    float f[] = new float[N];
    float min = 0.0f;
    float max = 0.0f;
    for (int i=0; i<N; i++) {
         f[i] = (float)(buffer[i]);
         if (f[i] > max) max = f[i];
         if (f[i] < min) min = f[i];
         }
    float scaling = 1.0f+(max-min)/256.0f; // +1 ensures we stay within range and guarantee no divide by zero if sequence is pure silence ...

    ByteBuffer byteBuf = ByteBuffer.allocate(N);
    for (int i=0; i<N; i++) {
        byte b = (byte)(f[i]/scaling);  /*convert to byte. */
        byteBuf.put(b);
    }
    return byteBuf.array();
}
于 2013-03-03T17:05:04.227 回答
3

问题在于您shortbyte转换。字节转换链接保留所有信息,包括高位和低位部分byte。当您将 16 位 PCM 样本转换为 8 位PCM样本时,您必须丢弃低字节。我的 Java 技能很弱,所以以下内容可能无法逐字执行。另请参阅:短字节转换。

ByteBuffer byteBuf = ByteBuffer.allocate(N);
while (N >= i) {
  /* byte b = (byte)((buffer[i]>>8)&0xff);  convert to byte. native endian */
 byte b = (byte)(buffer[i]&0xff);  /*convert to byte; swapped endian. */
 byteBuf.put(b);
  i++;
}

也就是下面的转换,

  AAAA AAAA SBBB BBBB  -> AAAA AAAA, +1 if S==1 and positive else -1 if S==1

A是有点保留。 B是一个被丢弃S的位,是您可能希望用于舍入的位。不需要四舍五入,但听起来可能会好一些。基本上,16 位 PCM 的分辨率高于 8 位 PCM。转换完成后,您会丢失这些位。shorttobyte例程试图保留所有信息。

当然,您必须告诉声音库您正在使用8-bit PCM. 我猜,

at = new AudioTrack(AudioManager.STREAM_MUSIC, 22050, AudioFormat.CHANNEL_OUT_STEREO,
            AudioFormat.ENCODING_PCM_8BIT, 10000 /* 10 second buffer */,
            AudioTrack.MODE_STREAM);

如果你只能16bit PCM用来播放音频,那么你必须做相反的事情并将8bit PCM库中的转换16bit PCM为播放。另请注意,通常,8bit样本通常不是直接 PCM,而是u-lawa-law编码。如果 3 rd方库使用这些格式,则转换会有所不同,但您应该能够从 wikipedia 链接对其进行编码。

注意:我没有包含舍入代码,因为overflow处理sign会使答案复杂化。您必须检查overflow(即,0x8f + 1 给出 0xff 或 255 + 1 给出 -1)。但是,我怀疑图书馆不是直的8bit PCM

另请参阅:Alsa PCM 概述, PCM上的多媒体 wiki 条目- 最终,Android 使用ALSA来获取声音。

PCM 原始缓冲区必须正确的其他因素是采样率、通道数(立体声/单声道)、PCM 格式(包括位、压扩、小/大端和样本交错)。

编辑:经过一番调查,JLayer 解码器通常返回big endian16 位值。Sonic 过滤器,但在 16 位以下byte威胁它们little endian。最后,AudioTrack该类期望little endian下面有 16 位。我相信出于某种原因,JLayermp3 解码器将返回 16 位little endian值。问题中的decode()方法对 16 位值进行字节交换。此外,发布的音频听起来好像字节被交换了。

public byte[] decode(InputStream inputStream, int startMs, int maxMs, bool swap) throws IOException {
...
                    short[] pcm = output.getBuffer();
                    for (short s : pcm) {
                        if(swap) {
                          outStream.write(s & 0xff);
                          outStream.write((s >> 8) & 0xff);
                        } else {
                          outStream.write((s >> 8) & 0xff);
                          outStream.write(s & 0xff);
                        }
                    }
...

对于 44k mp3,您可以使用swap = true;. 对于 22k mp3 swap = false。这解释了所有报道的现象。我不知道为什么JLayermp3 解码器有时会输出big endian,有时会输出little endian。我想这取决于源 mp3 而不是采样率。

于 2013-03-03T16:19:15.880 回答