将 SourceDataLine 与 Java 中的显示同步

Synchronizing SourceDataLine with display in Java

我正在 Java 中创建声音可视化工具,但找不到同步播放声音和显示声波的正确方法。

注意:下面的片段只是草图,但我实际上都试过了

我尝试过的方法:

  1. 利用 SourceDataLine.write() 阻塞直到数据行缓冲区中有 space 的事实。这导致缓冲区总是几乎满了,并且在刷新整个缓冲区后实际上正在播放每个样本。这意味着半秒的延迟。

    try (SourceDataLine sdl = AudioSystem.getSourceDataLine(someFormat)) {
                sdl.open(someFormat);
                sdl.start();
    
        while(true) {
            byte[] samples = getSamplesSomehow();
            sdl.write(samples, 0, samples.length); // <-- blocks
            displaySamples(samples);
        }
    }
    
  2. 将要绘制的数据排入队列并在刷新整个缓冲区后将其出列(帧位置增加缓冲区大小或更多)。这只是行不通,我不确定为什么。

    Queue<Data> queue = new LinkedList<Data>();
    try (SourceDataLine sdl = AudioSystem.getSourceDataLine(someFormat)) {
                sdl.open(someFormat);
                sdl.start();
    
        while(true) {
            int position = sdl.getFramePosition();
            byte[] samples = getSamplesSomehow();
            Data data = new Data(position, samples); // <-- java bean
    
            // display
            if (queue.size > 0 && queue.peek().getPosition() < position - sdl.getBufferSize()) {
                byte[] samplesToDisplay = queue.remove().getSamples();
                displaySamples(samplesToDisplay);
            }
    
            // push new data
            queue.add(data);
            sdl.write(samples, 0, samples.length);
        }
    }
    
  3. 等待缓冲区几乎为空。这非常有效,但引入了忙等待,CPU 使用量增加了 5 倍。

    try (SourceDataLine sdl = AudioSystem.getSourceDataLine(someFormat)) {
                sdl.open(someFormat);
                sdl.start();
    
        while(true) {
            byte[] samples = getSamplesSomehow();
            while(sdl.getBufferSize() - sdl.available() >= data.length); // <-- busy waiting
            sdl.write(samples, 0, samples.length); // <-- doesn't get a chance to block
            displaySamples(samples);
        }
    }
    

解决这个问题的常用方法是什么?

在方法 #1 中,在 SDL 上输出之前将数据发送到可视化工具。

可视化工具和音频播放可能都应该在各自的线程中。因此,对数据切换使用线程安全的松散耦合。也许给您的 Visualizer 一个 ConcurrentLinkedQueue,以 FIFO 方式使用,并让您的声音线程调用它。

半秒延迟表示SDL缓冲区相当大。您可能应该在 SDL 打开方法中指定较小的缓冲区大小。我经常使用 8K 给定或取 2 的因数。在 44100 fps 16 位立体声(每帧四个字节)下,延迟应该只有 0.05 秒左右。如果小缓冲区大小导致丢失,请尝试将其加倍。例如,class AudioCue 使用默认缓冲区大小仅为 1K 进行剪辑等效播放,但混合工具(混合多个剪辑等效)默认设置为更安全的 8K。

根据您获取样本的方式,如果输入的缓冲区大小与您的 SDL 缓冲区大小不同,则可能需要一些 assembly/disassembly。但这在声音线程的 while 循环中应该很容易处理。