AudioTrack - короткое искажение массива в байтовый массив с использованием jlayer(java mp3 decoder)

Я использую jLayer для декодирования данных MP3, с помощью этого вызова:

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

Этот вызов, который возвращает декодированные данные, возвращает массив short[]. output.getBuffer();

Когда я вызываю AudioTrack write() с этим методом, он проигрывается, когда я перебираю файл:

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

Однако, когда я преобразую массив short [] в массив byte[], используя любой из методов в этом ответе: /questions/23038999/kak-preobrazovat-korotkij-massiv-v-bajtovyij-massiv/23039011#23039011 звук искажается и дрожит:

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

будет выглядеть так:

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

Я делаю что-то не так и что я могу сделать, чтобы это исправить? К сожалению, мне нужно, чтобы данные pcm были в байтовом массиве для другой сторонней библиотеки, которую я использую. Файл имеет значение 22 кГц, если это имеет значение, и вот как создается экземпляр:

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

Огромное спасибо заранее.

Изменить: Вот как я сейчас создаю экземпляр переменной AudioTrack. Таким образом, для файлов с частотой 44 кГц отправляемое значение равно 44100, а для файлов с частотой 22 кГц - 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, который помог мне продвинуться в этом процессе.

2 ответа

Решение

Как говорит @Bill Pringlemeir, проблема в том, что ваш метод конвертации не конвертирует. Коротким является 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();
}

Проблема с вашим short в byte преобразование. Ссылка преобразования байтов сохраняет всю информацию, включая верхнюю и нижнюю byte части. Когда вы конвертируете из 16-битных в 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. Вы теряете эти биты, когда преобразование сделано. short в byte рутина пытается сохранить всю информацию.

Конечно, вы должны сообщить звуковой библиотеке, что вы используете 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-law или a-law. Если сторонняя библиотека использует эти форматы, преобразование отличается, но вы должны быть в состоянии закодировать его по ссылкам в Википедии.

ПРИМЕЧАНИЕ: я не включил код округления как overflow а также sign обработка усложнит ответ. Вы должны проверить overflow (То есть 0x8f + 1 дает 0xff или 255 + 1 дает -1). Тем не менее, я подозреваю, что библиотека не является прямой 8bit PCM,

См. Также: обзор Alsa PCM, мультимедийная вики-статья о PCM - в конечном итоге Android использует ALSA для звука.

Другими факторами, которые должны быть корректными для необработанного буфера PCM, являются частота дискретизации, количество каналов (стерео / моно), формат PCM, включая биты, компандирование, байтовый / младший порядок и чередование выборок.

РЕДАКТИРОВАТЬ: после некоторого расследования, декодер JLayer обычно возвращает big endian 16-битные значения. Соник фильтр, занимает 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, а не от частоты дискретизации.

Другие вопросы по тегам