在JVM内长时间录制音频时突然延迟

问题描述 投票:16回答:1

我正在实现一个应用程序,它使用JDK Version 8 Update 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台式PC上,它介于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个回答
6
投票

我有一个(相当)丰富的Java音频接口经验。以下几点可能有助于指导您找到合适的解决方案:

  1. 这不是JVM版本的问题 - 自Java 1.3或1.5以来,Java音频系统几乎没有升级过
  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 ++中查找用于Windows的音频捕获示例并将其转换为Java的。它将为您提供对音频捕获设备的精细控制,远远超过JVM提供的OOTB。如果你想看看生活/呼吸可用的JNA示例,请查看我的JNA AAC encoder项目。
  10. 同样,如果您使用特殊捕获harwdare,制造商很有可能已经提供了自己的低级C api来与硬件接口,您也应该考虑看一下它。
  11. 如果情况并非如此,也许您和您的公司/客户应该考虑使用专门的捕获硬件(不一定非常昂贵)。
© www.soinside.com 2019 - 2024. All rights reserved.