Внезапная задержка во время записи звука в течение длительных периодов времени в 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 Гц. по умолчанию, если не указано иное):
- Критическое количество времени, которое должно пройти, прежде чем появится задержка, кажется различным в зависимости от используемой машины. На моем настольном ПК с Windows 10 оно составляет от 6,5 до 7 часов. На моем ноутбуке (также использующем Windows 10), однако, для одного и того же аудиоформата это где-то между 4 и 5 часами.
- Количество используемых аудиоканалов, кажется, оказывает влияние. Если я изменю количество каналов со стерео на моно, время до появления задержки удваивается и составляет от 13 до 13,5 часов на моем рабочем столе.
- Уменьшение размера выборки с 16 до 8 бит также приводит к удвоению времени до появления задержки. Где-то между 13 и 13,5 часами на моем рабочем столе.
- Изменение порядка байтов от младшего к старшему не имеет никакого эффекта.
- Переключение со стереомикса на физический микрофон также не имеет никакого эффекта.
- Я попытался открыть строку, используя разные размеры буфера (1024, 2048 и 3072 кадра выборки), а также размер буфера по умолчанию. Это также ничего не изменило.
- Сброс TargetDataLine после начала задержки приводит к тому, что все байты равны нулю в течение приблизительно одной-двух секунд. После этого я снова получаю ненулевые значения. Задержка, однако, все еще там. Если я очищаю линию до критической точки, я не получаю эти нулевые байты.
- Остановка и перезапуск TargetDataLine после появления задержки также ничего не меняет.
- Однако закрытие и повторное открытие TargetDataLine избавляет от задержки, пока она не появится через несколько часов.
- Автоматическая очистка внутреннего буфера TargetDataLines каждые десять минут не помогает решить проблему. Следовательно, переполнение буфера во внутреннем буфере, похоже, не является причиной.
- Использование параллельного сборщика мусора во избежание зависаний приложения также не помогает.
- Используемая частота дискретизации представляется важной. Если я удвою частоту дискретизации до 88200 Гц, задержка начинает происходить где-то между 3 и 3,5 часами работы.
- Если я позволю ему работать под Linux, используя мой аудиоформат по умолчанию, он все равно будет работать нормально после 9 часов работы.
Выводы, которые я сделал:
Эти результаты позволяют мне прийти к выводу, что время, в течение которого я могу записывать звук до того, как эта проблема начинает возникать, зависит от компьютера, на котором запущено приложение, и от скорости передачи в байтах (т.е. размера кадра и частоты дискретизации) аудио формат. Похоже, что это верно (хотя я не могу полностью подтвердить это на данный момент), потому что, если я объединю изменения, сделанные в 2 и 3, я бы предположил, что я могу записывать аудио образцы в четыре раза дольше (что было бы где-то между 26 и 27 часов), как при использовании моего аудио формата "по умолчанию" до того, как начинает появляться задержка. Поскольку я не нашел времени, чтобы приложение могло работать так долго, я могу только сказать, что оно работало нормально в течение примерно 15 часов, прежде чем мне пришлось остановить его из-за нехватки времени на моей стороне. Таким образом, эту гипотезу еще предстоит подтвердить или опровергнуть.
Согласно результату пункта 13, кажется, что вся проблема возникает только при использовании Windows. Поэтому я думаю, что это может быть ошибкой в специфичных для платформы частях javax.sound.sampled API.
Хотя я думаю, что мог бы найти способ измениться, когда эта проблема начинает возникать, я не удовлетворен результатом. Я мог бы периодически закрывать и открывать линию, чтобы проблема вообще не появлялась. Однако выполнение этого приведет к небольшому произвольному промежутку времени, когда я не смогу захватывать аудиосэмплы. Кроме того, в Javadoc говорится, что некоторые строки вообще не могут быть открыты после закрытия. Поэтому это не очень хорошее решение в моем случае.
В идеале весь этот вопрос не должен происходить вообще. Есть ли что-то, чего я полностью упускаю, или я испытываю ограничения того, что возможно с API javax.sound.sampled? Как я могу избавиться от этой проблемы вообще?
Изменение: По предложению Xtreme Biker и Gidds я создал небольшой пример приложения. Вы можете найти его в этом хранилище Github.
Ответы
Ответ 1
У меня (довольно) огромный опыт взаимодействия с аудио Java. Вот несколько моментов, которые могут помочь вам найти правильное решение:
- Это не вопрос версии JVM - аудиосистема java едва ли была обновлена с Java 1.3 или 1.5
- Аудиосистема java является оберткой для любого аудиоинтерфейса API, который может предложить операционная система. В linux это библиотека Pulseaudio, для windows - API для прямого показа аудио (если я не ошибаюсь в последнем).
- Опять же, API аудиосистемы является своего рода устаревшим API - некоторые функции не работают или не реализованы, другие варианты поведения выглядят довольно странно, поскольку они зависят от устаревшего дизайна (я могу привести примеры, если требуется).
- Это не вопрос сбора мусора - если вы понимаете, что вы понимаете "задержку" (аудио-данные задерживаются на 1-2 секунды, то есть вы начинаете слышать материал через 1-2 секунды), ну, сборщик мусора не может привести к тому, что пустые данные будут волшебным образом захвачены целевой строкой данных, а затем добавляются данные как обычно с байтовым смещением в 2 секунды.
- Скорее всего, здесь происходит либо аппаратное обеспечение, либо драйвер, предоставляющий вам искаженные данные за 2 секунды в определенный момент, а затем, как обычно, потоковую передачу остальных данных, что приводит к "задержке", с которой вы столкнулись.
- Тот факт, что он отлично работает на Linux, означает, что это не аппаратная проблема, а проблема, связанная с драйвером.
- Чтобы подтвердить это подозрение, вы можете попробовать захватить аудио через FFmpeg в течение той же продолжительности и посмотреть, воспроизводится ли проблема.
- Если вы используете специализированное оборудование для захвата звука, лучше обратитесь к производителю оборудования и узнайте у него о проблеме, с которой вы сталкиваетесь в Windows.
- В любом случае, при написании приложения для захвата звука с нуля я настоятельно рекомендую по возможности держаться подальше от аудио-системы Java. Это хорошо для POC, но это устаревший API. JNA всегда является жизнеспособным вариантом (я использовал его в Linux с ALSA/Pulse-audio для управления атрибутами аппаратного обеспечения звука, которые аудиосистема Java не могла изменить), поэтому вы можете найти примеры захвата звука в C++ для окон и перевести их на Java. Это даст вам точный контроль над устройствами захвата звука, намного больше, чем то, что JVM предоставляет OOTB. Если вы хотите взглянуть на живой/дышащий пример использования JNA, посмотрите мой проект кодера JNA AAC.
- Опять же, если вы используете специальный захват Harwdare, есть большая вероятность, что производитель уже предоставляет свой собственный низкоуровневый C api для взаимодействия с аппаратным обеспечением, и вы должны рассмотреть его также.
- Если это не так, возможно, вам и вашей компании/клиенту стоит подумать об использовании специализированного оборудования для захвата (оно не должно быть таким дорогим).