【问题标题】:audio latency issues音频延迟问题
【发布时间】:2018-07-19 03:21:45
【问题描述】:

在我想要创建的应用程序中,我面临一些技术障碍。我在应用程序中有两个音乐曲目。例如,用户将音乐背景导入为第一首曲目。第二条路径是用户根据扬声器设备(或耳机)播放的第一首曲目的节奏录制的语音。此时我们面临延迟。在应用程序中录制和播放后,用户会听到音轨之间失去同步,这是由于麦克风和扬声器延迟造成的。

首先,我尝试通过过滤输入声音来检测延迟。我使用android的AudioRecord类,方法read()。此方法用音频数据填充我的短数组。 我发现这个数组的初始值是零,所以我决定在开始将它们写入输出流之前将它们剪掉。 所以我认为这些零是麦克风的“预热”延迟。这种方法正确吗?这个操作给出了一些结果,但并没有解决问题,而且在这个阶段,我离那个还很远。

但更糟糕的情况是启动扬声器和播放音乐之间的延迟。我无法过滤或检测到这种延迟。我试图创建一些计算延迟的校准功能。我通过扬声器播放“哔”声,当我开始播放时,我也开始测量时间。然后,我开始录制并聆听麦克风检测到的声音。当我在应用程序中识别出这种声音时,我会停止测量时间。我多次重复这个过程,最终值是这些结果的平均值。这就是我尝试测量设备延迟的方式。现在,当我有这个值时,我可以简单地向后移动第二个音轨以实现两条记录的同步(我会丢失一些初始毫秒的录音,但我跳过了这种情况,现在,有一些可能修复它) . 我以为这种方法可以解决问题,但事实证明这并不像我想象的那么简单。我在这里发现了两个问题: 1.同时播放两首曲目时有延迟 2. 设备音频延迟随机。

第一个:我使用 AudioTrack 类播放两个曲目,然后像这样运行方法play():

val firstTrack = //creating a track
val secondTrack = //creating a track

firstTrack.play()
secondTrack.play()

此代码会导致播放曲目阶段出现延迟。现在,我什至不必考虑录制时的延迟;我无法毫无延迟地同时播放两首曲目。我使用一些外部音频文件(未记录在我的应用程序中)对此进行了测试 - 我正在使用上面的代码启动相同的音频文件,我可以看到延迟。我也用 MediaPlayer 类尝试过,我得到了相同的结果。在这种情况下,我什至尝试在回调 OnPreparedListener 调用时播放曲目:

val firstTrack = //AudioPlayer
val secondTrack = //AudioPlayer

second.setOnPreparedListener {
  first.start()
  second.start()
}

这并没有帮助。 我知道 Android 还提供了一个名为 SoundPool 的类。根据文档,同时播放曲目会更好,但我不能使用它,因为它只支持小音频文件,这不能限制我。 我该如何解决这个问题?如何才能同时开始精确播放两首曲目?

第二个:音频延迟不是确定性的 - 有时它更小,有时它很大,而且我无法控制。因此,测量设备延迟可以再次提供帮助 - 它无法解决问题。

总结一下:是否有任何解决方案可以为我提供每个设备(或应用会话?)的准确延迟或检测实际延迟的其他触发器,以便在同时播放两个曲目时提供最佳同步?

提前谢谢你!

【问题讨论】:

  • 我的应用和你的类似:一首曲目是音乐,另一首是用户在该曲目上录制的歌声。当面临同样的问题时,我能找到的唯一可靠的解决方案是提供一个滑块,用户可以设置该滑块来更改两次播放之间的延迟。对于同一设备,此延迟通常是恒定的,但如果输出方法不同(例如蓝牙耳机),则可能会发生变化。所以你应该准备好检测这些情况并存储不同的延迟值。

标签: android audio microphone latency


【解决方案1】:

为卡拉 OK 应用同步音频非常困难。您似乎面临的主要问题是输出流中的可变延迟。

这几乎肯定是由“预热”延迟引起的:从在背景音轨上点击“播放”到音频设备(例如耳机)呈现第一帧音频数据所需的时间。这可能有很大的差异并且难以测量。

尝试的第一件事(也是最简单的)是在构造AudioTrack 时使用MODE_STREAM,并在调用play (more here) 之前使用bufferSizeInBytes 数据填充它。这应该会导致更低、更一致的“预热”延迟。

更好的方法是使用Android NDK 让音频流持续运行,在您点击播放之前一直输出静音,然后立即开始发送音频帧。您在这里唯一的延迟是连续输出延迟。

如果您决定走这条路,我建议您查看Oboe library(完整披露:我是作者之一)。

要回答您的具体问题之一......

有没有办法以编程方式计算音频输出流的延迟?

是的。解释这一点的最简单方法是使用code sample(这是 AAudio API 的 C++,但使用 Java AudioTrack 的原理相同):

// Get the index and time that a known audio frame was presented for playing
int64_t existingFrameIndex;
int64_t existingFramePresentationTime;
AAudioStream_getTimestamp(stream, CLOCK_MONOTONIC, &existingFrameIndex, &existingFramePresentationTime);

// Get the write index for the next audio frame
int64_t writeIndex = AAudioStream_getFramesWritten(stream);

// Calculate the number of frames between our known frame and the write index
int64_t frameIndexDelta = writeIndex - existingFrameIndex;

// Calculate the time which the next frame will be presented
int64_t frameTimeDelta = (frameIndexDelta * NANOS_PER_SECOND) / sampleRate_;
int64_t nextFramePresentationTime = existingFramePresentationTime + frameTimeDelta;

// Assume that the next frame will be written into the stream at the current time
int64_t nextFrameWriteTime = get_time_nanoseconds(CLOCK_MONOTONIC);

// Calculate the latency
*latencyMillis = (double) (nextFramePresentationTime - nextFrameWriteTime) / NANOS_PER_MILLISECOND;

警告:此方法依赖于音频硬件报告的准确时间戳。我知道这适用于 Google Pixel 设备,但听说它在其他设备上不太准确,所以 YMMV。

【讨论】:

  • 感谢您的回答。我曾考虑使用双簧管库和 AAudio API 以获得更好的性能,但这个解决方案的问题是 AAudio 是在 Android 8.1 中引入的。我想找到更多版本的解决方案,比如说 API 21。这些延迟计算看起来非常好,但我认为它并不能解决每个设备上的问题。但这很有趣,谢谢。
  • Oboe 向后兼容 API 16。通过 AudioTrack.getTimestamp 计算延迟可从 API 23 开始(不是通过 Oboe)。
  • @donturner 即使使用双簧管,我也无法将录制的音频与背景音轨正确同步。你愿意花点时间看看我做错了什么吗?
  • 我无法提供 1:1 支持,但如果您在 SO 上发布带有双簧管标签的问题,我会尝试看看。
【解决方案2】:

按照donturner的回答,这里有一个Java版本(根据SDK版本也使用其他方法)

/** The audio latency has not been estimated yet */
private static long AUDIO_LATENCY_NOT_ESTIMATED = Long.MIN_VALUE+1;

/** The audio latency default value if we cannot estimate it */
private static long DEFAULT_AUDIO_LATENCY = 100L * 1000L * 1000L; // 100ms

/**
 * Estimate the audio latency
 *
 * Not accurate at all, depends on SDK version, etc. But that's the best
 * we can do.
 */

private static void estimateAudioLatency(AudioTrack track, long audioFramesWritten) {

    long estimatedAudioLatency = AUDIO_LATENCY_NOT_ESTIMATED;

    // First method. SDK >= 19.
    if (Build.VERSION.SDK_INT >= 19 && track != null) {

        AudioTimestamp audioTimestamp = new AudioTimestamp();
        if (track.getTimestamp(audioTimestamp)) {

            // Calculate the number of frames between our known frame and the write index
            long frameIndexDelta = audioFramesWritten - audioTimestamp.framePosition;

            // Calculate the time which the next frame will be presented
            long frameTimeDelta = _framesToNanoSeconds(frameIndexDelta);
            long nextFramePresentationTime = audioTimestamp.nanoTime + frameTimeDelta;

            // Assume that the next frame will be written at the current time
            long nextFrameWriteTime = System.nanoTime();

            // Calculate the latency
            estimatedAudioLatency = nextFramePresentationTime - nextFrameWriteTime;

        }
    }

    // Second method. SDK >= 18.
    if (estimatedAudioLatency == AUDIO_LATENCY_NOT_ESTIMATED && Build.VERSION.SDK_INT >= 18) {
        Method getLatencyMethod;
        try {
            getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class<?>[]) null);
            estimatedAudioLatency = (Integer) getLatencyMethod.invoke(track, (Object[]) null) * 1000000L;
        } catch (Exception ignored) {}
    }

    // If no method has successfully gave us a value, let's try a third method
    if (estimatedAudioLatency == AUDIO_LATENCY_NOT_ESTIMATED) {
        AudioManager audioManager = (AudioManager) CRT.getInstance().getSystemService(Context.AUDIO_SERVICE);
        try {
            Method getOutputLatencyMethod = audioManager.getClass().getMethod("getOutputLatency", int.class);
            estimatedAudioLatency = (Integer) getOutputLatencyMethod.invoke(audioManager, AudioManager.STREAM_MUSIC) * 1000000L;
        } catch (Exception ignored) {}
    }

    // No method gave us a value. Let's use a default value. Better than nothing.
    if (estimatedAudioLatency == AUDIO_LATENCY_NOT_ESTIMATED) {
        estimatedAudioLatency = DEFAULT_AUDIO_LATENCY;
    }

    return estimatedAudioLatency
}

private static long _framesToNanoSeconds(long frames) {
    return frames * 1000000000L / SAMPLE_RATE;
}

【讨论】:

  • 感谢分享!我想在我们的应用程序启动时添加“校准”功能,我可以使用空白轨道或生成轨道吗?如果你好奇,这是我正在开发的应用程序:github.com/hillelcoren/mudeo
  • 不确定是否理解问题。为什么反对空白轨道和生成轨道?你可以生成一个只有零的空白轨道是的:)
  • 对不起,另一个问题...对于一个空白音频文件,我将为 audioFramesWritten 设置什么。您可以在此处查看完整代码:github.com/hillelcoren/mudeo/blob/master/android/app/src/main/… 注意:我将采样率设置为 1600,因为未定义 SAMPLE_RATE。再次感谢您的帮助,您的代码正是我们想要的。
  • 我没有提到空白音频文件 :) 此代码用于 AudioTrack,您可以自己在其中写入字节(来自一个或多个音频源/文件)。所以 1/ 只需在字节流中写入零,不需要文件 2/ audioFramesWritten 是您已经写入正在播放的 AudioTrack 的音频帧数,并且您估计了延迟。只要跟踪它。如果您对术语不清楚:如果您使用单声道,则一帧等于一个样本,如果您使用立体声,则等于 2 个样本。样本可以是短的(= 2 字节)或浮点数,具体取决于您设置 AudioTrack 的方式。
  • 哦,顺便说一下,您的 AudioTrack 必须实际播放才能使其正常工作,并且仅在几秒钟后就会产生良好的效果。
【解决方案3】:

众所周知,android MediaPlayer 类开始播放音频的速度很慢,我在创建的应用程序中遇到了一个问题,即开始播放音频剪辑的延迟超过一秒。我通过切换到ExoPlayer 解决了这个问题,这导致播放在 100 毫秒内开始。我还读到ffmpeg 的启动音频启动时间比 ExoPlayer 还要快,但我没有使用它,所以我无法做出任何承诺。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-16
    • 1970-01-01
    • 1970-01-01
    • 2013-12-15
    • 2012-04-27
    • 2013-06-12
    • 1970-01-01
    相关资源
    最近更新 更多