【问题标题】:Sudden delay while recording audio over long time periods inside the JVM在 JVM 内长时间录制音频时突然延迟
【发布时间】:2019-08-24 05:43:43
【问题描述】:

我正在使用 JDK 版本 8 更新 201 实现一个实时(或至少尽可能接近实时)记录和分析音频的应用程序。同时执行模拟应用程序典型用例的测试,我注意到在连续录制音频几个小时后,突然出现了一到两秒的延迟。到目前为止,没有明显的延迟。直到在这个关键点录制了几个小时之后,才开始出现这种延迟。

到目前为止我已经尝试过什么

为了检查我的音频样本录制计时代码是否错误,我注释掉了与计时相关的所有内容。这基本上让我有了这个更新循环,它在音频样本准备好后立即获取它们(注意: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)
}

但是,即使我没有任何时间,问题也没有解决。因此,我继续进行了一些实验,让应用程序使用不同的音频格式运行,这导致了非常混乱的结果(我将使用 PCM 签名的 16 位立体声音频格式,小端序和 44100.0 Hz 的采样率默认情况下,除非另有说明):

  1. 延迟出现之前必须经过的关键时间量似乎因所使用的机器而异。在我的 Windows 10 台式电脑上,它大约需要 6.5 到 7 个小时。但是,在我的笔记本电脑(也使用 Windows 10)上,相同的音频格式需要 4 到 5 个小时。
  2. 使用的音频通道数量似乎有影响。如果我将声道数量从立体声更改为单声道,延迟开始出现之前的时间会加倍,在我的桌面上大约为 13 到 13.5 小时。
  3. 将样本大小从 16 位减少到 8 位也会导致延迟开始出现之前的时间加倍。在我的桌面上使用 13 到 13.5 小时。
  4. 将字节顺序从 little endian 更改为 big endian 无效。
  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 声明某些行在关闭后根本无法重新打开。因此,就我而言,这不是一个好的解决方案。

理想情况下,这整个问题根本不应该发生。是否有一些我完全遗漏的东西,或者我是否遇到了 javax.sound.sampled API 的限制?我怎样才能完全摆脱这个问题?

编辑:根据 Xtreme Biker 和 gidds 的建议,我创建了一个小型示例应用程序。您可以在 Github repository 中找到它。

【问题讨论】:

标签: java kotlin audio-recording javax.sound.sampled


【解决方案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 音频系统无法更改的音频硬件属性),因此您可以在 Windows 的 C++ 中寻找音频捕获示例并将它们转换为爪哇。它将为您提供对音频捕获设备的精细控制,远远超过 JVM 提供的 OOTB。如果您想查看一个活生生的可用 JNA 示例,请查看我的 JNA AAC encoder 项目。
  10. 同样,如果您使用特殊的捕获硬件,制造商很有可能已经提供了自己的低级 C api 用于与硬件接口,您也应该考虑看看它。
  11. 如果不是这样,也许您和您的公司/客户应该 考虑使用专门的捕获硬件(不必 这么贵)。

【讨论】:

  • AudioSystem 工作正常,让你降级我不知道你从哪里来!!
  • @gpasch 好吧,它缺少音量控制(以及其他混音器控制功能,据记载至少存在)、通道解复用和动态设备检测(音频接口不能在 JVM 类加载期间始终存在)。我只是认为开发人员必须使用 JNA 并深入研究 ALSA 头文件以执行诸如“在第一个通道对上获得增益”之类的事情是不合理的(AudioSystem 无法做到这一点) ,即使遵循文档)。我并不是有意忽视 Sun 开发人员在这个 API 上所做的所有努力。
猜你喜欢
  • 2020-05-25
  • 1970-01-01
  • 2013-06-12
  • 2017-09-05
  • 2019-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-08
相关资源
最近更新 更多