8

I'm using jLayer to decode MP3 data, with this call:

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

This call which returns the decoded data, returns an array of short[]. output.getBuffer();

When I call AudioTrack write() with that method, it plays fine as I loop through the file:

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

However, when I convert the short[] array to byte[] array using any of the methods in this answer: https://stackoverflow.com/a/12347176/1176436 the sound gets distorted and jittery:

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

becomes:

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

Am I doing anything wrong and what can I do to fix it? Unfortunately I need the pcm data to be in a byte array for another 3rd party library I'm using. The file is 22kHz if that matters and this is how at is being instantiated:

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

Thank you so much in advance.

Edit: This is how I'm instantiating the AudioTrack variable now. So for 44kHz files, the value that is getting sent is 44100, while for 22kHz files, the value is 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);

This is decode method:

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);
        }
    }

This is how it sounds (wait a few seconds): https://vimeo.com/60951237 (and this is the actual file: http://www.tonycuffe.com/mp3/tail%20toddle.mp3)

Edit: I would have loved to have split the bounty, but instead I have given the bounty to Bill and the accepted answer to Neil. Both were a tremendous help. For those wondering, I ended up rewriting the Sonic native code which helped me move along the process.

12
  • It appears that the decoding works when the file is 44kHz sample rate, but for 22kHz, it becomes completely choppy. Commented Mar 3, 2013 at 9:14
  • 1
    So once I get a suitable amount of byte[]s back, I can send it to sonic and it spits back modified byte[]s, which I pass to AudioTrack. With 44kHz files it works beautifully (even with playback rate modification). Even if I take out the sonic conversion for 22 khz files, it sounds pretty bad. Commented Mar 3, 2013 at 18:11
  • 1
    Yes :). I don't get anything near the sound that I'd be expecting, just a couple of "Thuds" (like someone hitting a microphone). Commented Mar 3, 2013 at 18:31
  • 1
    I tried the Java version, but it keeps running into arrayOutOfBoundExceptions. Commented Mar 3, 2013 at 19:57
  • 1
    This question got closed, but 5 people voted it up. Commented Sep 16, 2013 at 19:15

2 Answers 2

4

As @Bill Pringlemeir says, the problem is that your conversion method doesn't actually convert. A short is a 16 bit number; a byte is an 8 bit number. The method you have chosen doesn't convert the contents of the shorts (ie go from 16 bits to 8 bits for the contents), it changes the way in which the same collection of bits is stored. As you say, you need something like this:

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

@Bill Pringlemeir's approach is equivalent to dividing all the shorts by 256 to ensure they fit in the byte range:

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();
}

This will work, but will probably give you very quiet, edgy tones. If you can afford the processing time, a two pass approach will probably give better results:

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();
}

Again, beware signed / unsigned issue. The above works signed-> signed and unsigned->unsigned; but not between the two. It may be that you are reading signed shorts (-32768-32767), but need to output unsigned bytes (0-255), ...

If you can afford the processing time, a more precise (smoother) approach would be to go via floats (this also gets round the signed/unsigned issue):

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();
}
Sign up to request clarification or add additional context in comments.

14 Comments

I tried your conversion, it still sounds choppy. Maybe I should be padding the byte array with 0s at each conversion?
A little bit, still choppy however.
If you can bear the processing cost, or at least if you can fro an experiment, try the alternative I'll put above in the next couple of minutes. If it's still choppy, then it is very unlikely that this conversion is the cause, and more likely that something else is the issue
The float alternative is now up to try, but I can't edit the previous comment to reflect this ...
I tried both, still choppy. You can see the editted question at the end for the intended sound as well as the outputted sound.
|
3
+50

The issue is with your short to byte conversion. The byte conversion link preserves all information including the high and low byte portions. When you are converting from 16bit to 8bit PCM samples, you must discard the lower byte. My Java skills are weak, so the following may not work verbatim. See also: 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++;
}

That is the following conversion,

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

A is a bit that is kept. B is a discarded bit and S is a bit that you may wish to use for rounding. The rounding is not needed, but it may sound a little better. Basically, 16 bit PCM is higher resolution than 8 bit PCM. You lose those bits when the conversion is done. The short to byte routine tries to preserve all information.

Of course, you must tell the sound library that you are using 8-bit PCM. My guess,

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

If you can only use 16bit PCM to play audio, then you have to do the inverse and convert the 8bit PCM from the library to 16bit PCM for playback. Also note, that typically, 8bit samples are often NOT straight PCM but u-law or a-law encoded. If the 3rd party library uses these formats, the conversion is different but you should be able to code it from the wikipedia links.

NOTE: I have not included the rounding code as overflow and sign handling will complicate the answer. You must check for overflow (Ie, 0x8f + 1 gives 0xff or 255 + 1 giving -1). However, I suspect the library is not straight 8bit PCM.

See Also: Alsa PCM overview, Multi-media wiki entry on PCM - Ultimately Android uses ALSA for sound.

Other factors that must be correct for a PCM raw buffer are sample rate, number of channels (stereo/mono), PCM format including bits, companding, little/big endian and sample interleaving.

EDIT: After some investigation, the JLayer decoder typically returns big endian 16bit values. The Sonic filter, takes a byte but threats them as 16bit little endian underneath. Finally, the AudioTrack class expects 16 bit little endian underneath. I believe that for some reason the JLayer mp3 decoder will return 16bit little endian values. The decode() method in the question does a byte swap of the 16 bit values. Also, the posted audio sounds as if the bytes are swapped.

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);
                        }
                    }
...

For 44k mp3s, you call the routine with swap = true;. For the 22k mp3 swap = false. This explains all the reported phenomena. I don't know why the JLayer mp3 decoder would sometimes output big endian and other times little endian. I imagine it depends on the source mp3 and not the sample rate.

5 Comments

Thanks for your help so far, I'm in the process of trying it. I think I understand what you're saying, but why would a 44kHz file converted in the original method and set as 16 bit pcm work?
Sorry if I wasn't clear, when I switch to the 22 kHz file, I switch the AudioTrack instantiation to be at 22050. In fact, it's now handled automatically. Please see the edit to see the new instantiation.
0 [Tuna ]: OMAP4 - Tuna TI OMAP4 Board 1 [OMAP4HDMI ]: OMAP4HDMI - OMAP4HDMI OMAP4HDMI
How would that be possible? The problem is, the act of converting the short[] to byte[] is causing the audio corruption, which happens before Sonic gets the data. If I send to the audiotrack the array as shorts, it sounds perfectly fine. If I send it as a byte[] array, for 44 kHz files it works just as well, for 22 kHz, well: vimeo.com/60951237
Ok. I didn't know that 22k works with 16bit. It still seems like the problem is the conversion OR specifying the PCM playback. It is NOT the driver. The 16bit sample maybe big-endian or little-endian. Please look at my edit and try that without Sonic. It appears the buffers take a byte, but the are converted to short by the sonic JNI methods.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.