AudioTrack - 将 short 数组转换为 byte 数组时使用 jlayer(Java MP3 解码器)出现失真问题

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://dev59.com/3Wgv5IYBdhLWcg3wKtlB#12347176声音会变得失真和抖动:

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

becomes:

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

我是否做错了什么,我该怎么解决?不幸的是,我需要将pcm数据转换为字节数组以供我使用的第三方库。如果有影响,文件的采样率为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
编辑:我很想把赏金分开,但我最终将赏金给了Bill,接受了Neil的答案。两位都非常有帮助。对于那些好奇的人,我最终重新编写了Sonic本地代码,这帮助我推进了进程。

1
所以一旦我得到足够数量的byte[],我可以将其发送给Sonic,它会返回修改后的byte[],然后我将其传递给AudioTrack。对于44kHz文件,它运行得非常好(即使进行播放速率修改)。即使我删除22 kHz文件的Sonic转换,声音也听起来相当糟糕。 - StackOverflowed
1
是的 :),我得到的声音跟我期望的完全不一样,只有几个“咚咚”的声音(像有人敲麦克风)。 - StackOverflowed
标准的NDK版本在这里,https://github.com/waywardgeek/sonic-ndk/blob/master/src/org/vinuxproject/sonic/Sonic.java。然而,这个库支持字节、无符号字节、短整型和浮点数吗?http://github.com/waywardgeek/sonic/blob/master/Sonic.java 此外,您可以自由地在自己的jar(或者是安卓应用程序包?)中不改变Sonic.class的情况下使用它。 - artless noise
1
我尝试了Java版本,但它一直遇到数组越界异常。 - StackOverflowed
1
这个问题已经被关闭,但有5个人投了赞成票。 - StackOverflowed
显示剩余7条评论
2个回答

4
正如@Bill Pringlemeir所说,问题在于你的转换方法实际上没有进行转换。short是一个16位的数字;byte是一个8位的数字。你选择的方法并没有转换shorts的内容(即将内容从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();
}

我尝试了你的转换,但仍然听起来很不流畅。也许我应该在每次转换时用0填充字节数组? - StackOverflowed
1
如果你能承担处理成本,或者至少可以进行一次实验,请尝试我接下来几分钟内提供的替代方案。如果仍然不流畅,那么很可能这种转换不是问题的原因,更有可能是其他问题。 - Neil Townsend
现在你可以尝试使用浮点型数据类型,但是我无法编辑之前的评论来反映这一点... - Neil Townsend
@Neil:我觉得这里有不止一件事情需要注意。请查看 https://github.com/waywardgeek/sonic-ndk/tree/master/jni 的 putBytesNative。如果您跟进代码,会发现即使顶级 Java 接口是 putBytes(),它实际上将 byte 缓冲区视为 shorts。此外,mp3 解码器的字节序也不清楚。一旦我们尝试去干涉 PCM,就会出现问题(除了在44K?为什么?)。正确设置 AudioTrack/AudioFormat 的字节序和 PCM 大小也很重要,以获得良好的声音效果。 - artless noise
另一个人的问题:“非常棒的博客,我只是想问一下29-32行的检查。我遇到了一个情况,我的生成的mp3是22khz单声道。我尝试删除检查并在音频轨道上运行转换,结果得到了一个外星声音。如果我将代码作为桌面应用程序运行并将结果保存在wav文件中,没有编辑器可以识别它。提前致谢!” - StackOverflowed
显示剩余9条评论

3

问题出在你的short转换为byte上。字节转换链接保留所有信息,包括高位和低位byte。当你将16位转换为8位PCM样本时,必须舍弃低位。我的Java技能较弱,因此以下内容可能不完全正确。另请参见:short to byte conversion.

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具有更高的分辨率。当转换完成时,你将失去这些位。将short转换为byte的程序尝试保留所有信息。

当然,你必须告诉声音库你正在使用8位PCM。我的猜测是,

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

如果您只能使用16位PCM来播放音频,则必须反过来将库中的8位PCM转换为16位PCM进行播放。还要注意,通常情况下,8位样本通常不是直接的PCM,而是μ律a律编码。如果第三方库使用这些格式,则转换方式不同,但您应该能够从维基百科链接中编写代码。
注意:我没有包含舍入代码,因为溢出符号处理会使答案复杂化。您必须检查是否存在溢出(即,0x8f + 1给出0xff或255 + 1给出-1)。但是,我怀疑该库不是直接的8位PCM

相关阅读:Alsa PCM概述, PCM的多媒体维基页面 - 最终Android使用 ALSA 来实现声音。

PCM原始缓冲区必须正确设置以下因素:采样率、通道数(立体声/单声道)、PCM格式(包括位数、压缩、小端/大端)和采样交错。

编辑: 经过一些调查,JLayer解码器通常会返回big endian 16位值。 Sonic过滤器接受一个byte,但在下面将它们视为16位little endian。 最后,AudioTrack类期望16位little endian。 我相信出于某种原因,JLayer mp3解码器将返回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。这解释了所有报告的现象。我不知道为什么JLayer mp3解码器有时会输出big endian,而其他时候则是little endian。我想这取决于源mp3而不是采样率。


谢谢你迄今为止的帮助,我正在尝试中。我想我明白你的意思,但是为什么44kHz文件在原始方法转换并设置为16位pcm会起作用呢? - StackOverflowed
抱歉如果我表达不清楚,当我切换到22 kHz文件时,我将AudioTrack实例化切换为22050。事实上,现在它已经自动处理了。请查看编辑以查看新的实例化。 - StackOverflowed
1
0 [Tuna]: OMAP4 - Tuna TI OMAP4板 1 [OMAP4HDMI]: OMAP4HDMI - OMAP4HDMI OMAP4HDMI - StackOverflowed
这怎么可能呢?问题在于,将short[]转换为byte[]的操作会导致音频损坏,在Sonic获取数据之前就已经发生了。如果我将数组作为shorts发送到audiotrack,则听起来完美无缺。如果我将其作为byte[]数组发送,对于44 kHz文件,它同样有效,但对于22 kHz文件,情况就不一样了:https://vimeo.com/60951237 - StackOverflowed
好的。我不知道22k可以使用16位。问题似乎仍然是转换或指定PCM播放。这不是驱动程序的问题。16位样本可能是大端小端。请查看我的编辑并尝试不使用Sonic。缓冲区似乎采用字节,但是它们由sonic JNI方法转换为short - artless noise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接