Внезапная задержка во время записи звука в течение длительных периодов времени в JVM

Я внедряю приложение, которое записывает и анализирует аудио в режиме реального времени (или, по крайней мере, настолько близко к реальному времени, насколько это возможно), используя обновление 201 JDK версии 8. Во время выполнения теста, имитирующего типичные случаи использования приложения, я заметил что после нескольких часов непрерывной записи звука была введена внезапная задержка где-то между одной и двумя секундами. До этого момента не было заметной задержки. Только после этой критической точки записи в течение нескольких часов эта задержка начала происходить.

Что я пробовал до сих пор

Чтобы проверить, неверен ли мой код для синхронизации записи аудиосэмплов, я прокомментировал все, что касается синхронизации. Это оставило меня по существу с этим циклом обновления, который выбирает аудиосэмплы, как только они будут готовы (Примечание: код Kotlin):

while (!isInterrupted) {
    val audioData = read(sampleSize, false)
    listener.audioFrameCaptured(audioData)
}

Это мой метод чтения:

fun read(samples: Int, buffered: Boolean = true): AudioData {
    //Allocate a byte array in which the read audio samples will be stored.
    val bytesToRead = samples * format.frameSize
    val data = ByteArray(bytesToRead)

    //Calculate the maximum amount of bytes to read during each iteration.
    val bufferSize = (line.bufferSize / BUFFER_SIZE_DIVIDEND / format.frameSize).roundToInt() * format.frameSize
    val maxBytesPerCycle = if (buffered) bufferSize else bytesToRead

    //Read the audio data in one or multiple iterations.
    var bytesRead = 0
    while (bytesRead < bytesToRead) {
        bytesRead += (line as TargetDataLine).read(data, bytesRead, min(maxBytesPerCycle, bytesToRead - bytesRead))
    }

    return AudioData(data, format)
}

Тем не менее, даже без каких-либо сроков с моей стороны проблема не была решена. Поэтому я немного поэкспериментировал и позволил приложению работать в разных аудиоформатах, что приводит к очень запутанным результатам (я собираюсь использовать 16-битный стереофонический аудиоформат PCM со слабым порядком байтов и частотой дискретизации 44100,0 Гц. по умолчанию, если не указано иное):

  1. Критическое количество времени, которое должно пройти, прежде чем появится задержка, кажется различным в зависимости от используемой машины. На моем настольном ПК с Windows 10 оно составляет от 6,5 до 7 часов. На моем ноутбуке (также использующем Windows 10), однако, для одного и того же аудиоформата это где-то между 4 и 5 часами.
  2. Количество используемых аудиоканалов, кажется, оказывает влияние. Если я изменю количество каналов со стерео на моно, время до появления задержки удваивается и составляет от 13 до 13,5 часов на моем рабочем столе.
  3. Уменьшение размера выборки с 16 до 8 бит также приводит к удвоению времени до появления задержки. Где-то между 13 и 13,5 часами на моем рабочем столе.
  4. Изменение порядка байтов от младшего к старшему не имеет никакого эффекта.
  5. Переключение со стереомикса на физический микрофон также не имеет никакого эффекта.
  6. Я попытался открыть строку, используя разные размеры буфера (1024, 2048 и 3072 кадра выборки), а также размер буфера по умолчанию. Это также ничего не изменило.
  7. Сброс TargetDataLine после начала задержки приводит к тому, что все байты равны нулю в течение приблизительно одной-двух секунд. После этого я снова получаю ненулевые значения. Задержка, однако, все еще там. Если я очищаю линию до критической точки, я не получаю эти нулевые байты.
  8. Остановка и перезапуск TargetDataLine после появления задержки также ничего не меняет.
  9. Однако закрытие и повторное открытие TargetDataLine избавляет от задержки, пока она не появится через несколько часов.
  10. Автоматическая очистка внутреннего буфера TargetDataLines каждые десять минут не помогает решить проблему. Следовательно, переполнение буфера во внутреннем буфере, похоже, не является причиной.
  11. Использование параллельного сборщика мусора во избежание зависаний приложения также не помогает.
  12. Используемая частота дискретизации представляется важной. Если я удвою частоту дискретизации до 88200 Гц, задержка начинает происходить где-то между 3 и 3,5 часами работы.
  13. Если я позволю ему работать под Linux, используя мой аудиоформат по умолчанию, он все равно будет работать нормально после 9 часов работы.

Выводы, которые я сделал:

Эти результаты позволяют мне прийти к выводу, что время, в течение которого я могу записывать звук до того, как эта проблема начинает возникать, зависит от компьютера, на котором запущено приложение, и от скорости передачи в байтах (т.е. размера кадра и частоты дискретизации) аудио формат. Похоже, что это верно (хотя я не могу полностью подтвердить это на данный момент), потому что, если я объединю изменения, сделанные в 2 и 3, я бы предположил, что я могу записывать аудио образцы в четыре раза дольше (что было бы где-то между 26 и 27 часов), как при использовании моего аудио формата "по умолчанию" до того, как начинает появляться задержка. Поскольку я не нашел времени, чтобы приложение могло работать так долго, я могу только сказать, что оно работало нормально в течение примерно 15 часов, прежде чем мне пришлось остановить его из-за нехватки времени на моей стороне. Таким образом, эту гипотезу еще предстоит подтвердить или опровергнуть.

Согласно результату пункта 13, кажется, что вся проблема возникает только при использовании Windows. Поэтому я думаю, что это может быть ошибкой в специфичных для платформы частях javax.sound.sampled API.

Хотя я думаю, что мог бы найти способ измениться, когда эта проблема начинает возникать, я не удовлетворен результатом. Я мог бы периодически закрывать и открывать линию, чтобы проблема вообще не появлялась. Однако выполнение этого приведет к небольшому произвольному промежутку времени, когда я не смогу захватывать аудиосэмплы. Кроме того, в Javadoc говорится, что некоторые строки вообще не могут быть открыты после закрытия. Поэтому это не очень хорошее решение в моем случае.

В идеале весь этот вопрос не должен происходить вообще. Есть ли что-то, чего я полностью упускаю, или я испытываю ограничения того, что возможно с API javax.sound.sampled? Как я могу избавиться от этой проблемы вообще?

Изменение: По предложению Xtreme Biker и Gidds я создал небольшой пример приложения. Вы можете найти его в этом хранилище Github.

Ответы

Ответ 1

У меня (довольно) огромный опыт взаимодействия с аудио Java. Вот несколько моментов, которые могут помочь вам найти правильное решение:

  1. Это не вопрос версии JVM - аудиосистема java едва ли была обновлена с Java 1.3 или 1.5
  2. Аудиосистема java является оберткой для любого аудиоинтерфейса API, который может предложить операционная система. В linux это библиотека Pulseaudio, для windows - API для прямого показа аудио (если я не ошибаюсь в последнем).
  3. Опять же, API аудиосистемы является своего рода устаревшим API - некоторые функции не работают или не реализованы, другие варианты поведения выглядят довольно странно, поскольку они зависят от устаревшего дизайна (я могу привести примеры, если требуется).
  4. Это не вопрос сбора мусора - если вы понимаете, что вы понимаете "задержку" (аудио-данные задерживаются на 1-2 секунды, то есть вы начинаете слышать материал через 1-2 секунды), ну, сборщик мусора не может привести к тому, что пустые данные будут волшебным образом захвачены целевой строкой данных, а затем добавляются данные как обычно с байтовым смещением в 2 секунды.
  5. Скорее всего, здесь происходит либо аппаратное обеспечение, либо драйвер, предоставляющий вам искаженные данные за 2 секунды в определенный момент, а затем, как обычно, потоковую передачу остальных данных, что приводит к "задержке", с которой вы столкнулись.
  6. Тот факт, что он отлично работает на Linux, означает, что это не аппаратная проблема, а проблема, связанная с драйвером.
  7. Чтобы подтвердить это подозрение, вы можете попробовать захватить аудио через FFmpeg в течение той же продолжительности и посмотреть, воспроизводится ли проблема.
  8. Если вы используете специализированное оборудование для захвата звука, лучше обратитесь к производителю оборудования и узнайте у него о проблеме, с которой вы сталкиваетесь в Windows.
  9. В любом случае, при написании приложения для захвата звука с нуля я настоятельно рекомендую по возможности держаться подальше от аудио-системы Java. Это хорошо для POC, но это устаревший API. JNA всегда является жизнеспособным вариантом (я использовал его в Linux с ALSA/Pulse-audio для управления атрибутами аппаратного обеспечения звука, которые аудиосистема Java не могла изменить), поэтому вы можете найти примеры захвата звука в C++ для окон и перевести их на Java. Это даст вам точный контроль над устройствами захвата звука, намного больше, чем то, что JVM предоставляет OOTB. Если вы хотите взглянуть на живой/дышащий пример использования JNA, посмотрите мой проект кодера JNA AAC.
  10. Опять же, если вы используете специальный захват Harwdare, есть большая вероятность, что производитель уже предоставляет свой собственный низкоуровневый C api для взаимодействия с аппаратным обеспечением, и вы должны рассмотреть его также.
  11. Если это не так, возможно, вам и вашей компании/клиенту стоит подумать об использовании специализированного оборудования для захвата (оно не должно быть таким дорогим).