linux上的java声音:如何足够快地从TargetDataLine捕获以跟上?

Posted

技术标签:

【中文标题】linux上的java声音:如何足够快地从TargetDataLine捕获以跟上?【英文标题】:java sound on linux: how to capture from TargetDataLine quickly enough to keep up? 【发布时间】:2014-05-02 08:39:57 【问题描述】:

我正在使用 Java 声音 API 和 Java 1.7。当我在 Linux 上运行我的应用程序(java 版本“1.7.0_51”,Java(TM) SE 运行时环境(内部版本 1.7.0_51-b13))时,我很难从 TargetDataLine 快速读取以跟上正在记录的内容, Java HotSpot(TM) 64 位服务器 VM(内部版本 24.51-b03,混合模式),Red Hat Enterprise Linux 5)。在我的 Windows 7 笔记本电脑上运行相同的程序时,我没有这个问题。我有点难过。

为了隔离问题,我编写了一个程序,该程序从 TargetDataLine 捕获一段时间(以交互方式确定)并记录每次阻塞读取固定字节数所花费的时间量,然后将它们打印出来以及平均阅读时间、经过的总时间和捕获的音频的时间价值。

我的测试程序如下:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.Audiosystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.TargetDataLine;

/**
 * This is a test of mic capture delay for given buffer and fetch settings.
 */
public class MicCaptureDelayTest 

   /**
    * the audio format used for capturing and transmitting
    */
   private static final AudioFormat format =
         new AudioFormat(8000, 16, 1, true, true);

   /**
    * This is the target data line buffer size to request, in bytes.
    */
   private static final int MIC_BUFFER_SIZE = 1000;

   /**
    * This is the number of bytes to try to fetch from the target data line at a
    * time.
    */
   private static final int MIC_FETCH_SIZE = 480;

   /**
    * Searches for available mixers on the system that have a microphone.
    * @return a list of matching mixers
    */
   private static List<Mixer.Info> findMicrophoneMixers() 
      Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
      List<Mixer.Info> matches = new ArrayList<>();
      for (Mixer.Info mixerInfo : mixerInfos) 
         Mixer mixer = AudioSystem.getMixer(mixerInfo);
         DataLine.Info lineInfo = new DataLine.Info(TargetDataLine.class,
               format);
         boolean isSupported = mixer.isLineSupported(lineInfo);

         if (isSupported) 
            matches.add(mixerInfo);
         
      

      return matches;
   

   /**
    * This is the test recording thread.
    */
   private static class MicFetcher extends Thread 

      /**
       * This is the requested recording state.
       */
      private boolean shouldRecord = false;

      /**
       * This is the current processed recording state of the thread.
       */
      private boolean isRecording = false;

      /**
       * This is the Java audio interface line microphone data is captured from.
       */
      private TargetDataLine lineFromMic;

      /**
       * Runs the test mic capture thread body.
       */
      @Override
      public void run() 

         List<Mixer.Info> matchingMixerInfo = findMicrophoneMixers();

         // Use the first matching mixer.
         Mixer mixerToUse = AudioSystem.getMixer(matchingMixerInfo.get(0));

         DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);

         try 
            lineFromMic = (TargetDataLine) mixerToUse.getLine(info);
            lineFromMic.open(format, MIC_BUFFER_SIZE);
          catch (LineUnavailableException e) 
            e.printStackTrace();
            return;
         

         byte[] transferBuffer = new byte[MIC_FETCH_SIZE];
         List<Long> readTimesNanos = new LinkedList<>();
         int numFramesCaptured = 0;
         long startTimeNanos = 0;

         while (true) 
            boolean currentShouldRecord;
            synchronized(this) 
               currentShouldRecord = shouldRecord;
            

            if (!isRecording && currentShouldRecord) 
               // Start recording.

               System.out.println("Starting.");
               lineFromMic.start();
               isRecording = true;
               startTimeNanos = System.nanoTime();

             else if (isRecording && !currentShouldRecord) 
               // Stop recording.
               System.out.println("Stopping.");
               lineFromMic.stop();
               lineFromMic.flush();

               System.out.print("read times (ms): ");
               long sumReadTimesNanos = 0;
               int i = 0;
               for (Long sampleTimeNanos : readTimesNanos) 
                  if (i % 5 == 0) 
                     System.out.println();
                  
                  System.out.printf("%.2f  ", sampleTimeNanos / 1.0e6);
                  sumReadTimesNanos += sampleTimeNanos;
                  ++i;
               
               System.out.println();
               System.out.println(
                     "Mean read time (ms): "
                           + (sumReadTimesNanos / 1.0e6
                                 / readTimesNanos.size()));

               long stopTimeNanos = System.nanoTime();
               System.out.println("Time captured (s): "
                     + (numFramesCaptured / format.getFrameRate()));
               System.out.println("Time elapsed (s): "
                     + (stopTimeNanos - startTimeNanos) / 1.0e9);

               readTimesNanos.clear();
               numFramesCaptured = 0;
               isRecording = false;

             else if (isRecording) 
               // Continue recording.

               long beforeTimeNanos = System.nanoTime();

               // Retrieve data from the line.  This blocks.
               int numBytesRead = lineFromMic.read(
                     transferBuffer, 0, MIC_FETCH_SIZE);
               numFramesCaptured += numBytesRead / format.getFrameSize();

               long afterTimeNanos = System.nanoTime();
               long timeElapsedNanos = afterTimeNanos - beforeTimeNanos;
               readTimesNanos.add(timeElapsedNanos);
            
         
      

      /**
       * Requests to toggle the recording state of the test recording thread.
       */
      public synchronized void toggleState() 
         shouldRecord = ! shouldRecord;
      
   

   /**
    * Runs the test program.  Newline toggles state.
    * @param args command line args-- none needed
    * @throws IOException if thrown when trying to get console input
    */
   public static void main(String[] args) throws IOException 
      BufferedReader inputReader =
            new BufferedReader(new InputStreamReader(System.in));

      MicFetcher fetcher = new MicFetcher();
      fetcher.start();

      while (true) 
         // Toggle state for each line of input (ie, press enter to toggle).
         inputReader.readLine();
         fetcher.toggleState();
      
   

当我在我的 Linux 环境中运行它时,大约 10 秒的记录,输出如下所示:

Starting.

Stopping.
read times (ms): 
54.00  18.10  36.62  36.32  35.99  
18.10  18.25  54.26  18.30  35.56  
18.12  35.51  36.74  17.22  36.70  
35.29  18.33  35.60  18.23  54.72  
19.00  37.99  18.14  18.37  53.91  
18.37  35.34  36.00  18.00  36.00  
18.00  54.71  17.22  18.12  36.18  
36.64  36.08  18.00  54.34  18.26  
18.27  35.44  18.30  54.77  18.33  
18.24  36.51  35.47  36.52  18.35  
17.14  54.96  18.13  36.73  17.21  
54.95  18.28  18.37  36.54  36.72  
35.56  18.37  17.23  54.46  18.36  
35.53  18.08  36.00  36.00  17.99  
54.30  18.06  35.22  18.00  18.00  
53.93  18.32  35.63  36.64  18.16  
35.21  18.30  55.65  18.23  18.35  
35.55  36.32  35.60  18.30  36.33  
36.21  17.22  36.54  18.32  54.96  
17.19  18.36  35.62  36.67  35.25  
18.29  18.37  54.63  18.37  36.54  
18.35  53.91  18.37  17.23  36.70  
36.09  36.01  17.19  18.33  53.91  
18.37  36.56  18.36  35.53  36.58  
18.16  53.84  18.26  36.03  18.08
18.12  54.24  18.08  36.14  36.19
18.12  36.08  18.11  53.80  18.28
18.37  36.55  18.13  53.99  18.00
36.12  35.54  18.28  36.56  17.20
53.96  18.00  18.01  36.67  36.53
36.71  17.19  18.37  54.37  18.02
35.97  18.00  54.00  18.00  18.00
36.00  35.99  36.34  18.37  18.35
53.93  18.13  36.63  18.33  36.33
36.34  18.33  36.55  35.51  36.66
18.29  18.06  54.00  17.99  36.08
18.25  36.64  36.38  18.37  35.55
36.66  18.21  36.73  17.19  54.27
18.13  35.55  18.18  36.31  35.56
18.34  53.90  18.36  18.09  36.15
18.22  53.90  18.32  18.37  53.89
18.19  36.04  17.20  53.94  18.31
18.37  36.55  36.70  36.61  18.35
17.18  53.97  18.32  36.55  19.01
18.99  57.00  18.99  38.01  18.98
38.00  18.99  36.99  36.35  18.37
36.55  36.70  18.04  38.00  19.00
38.00  37.99  18.99  37.99  19.00
37.06  36.43  36.03  18.00  18.00
54.47  18.25  36.70  18.22  18.37
53.55  18.33  35.59  36.59  18.29
35.36  18.37  54.89  18.24  36.44
18.33  18.36  53.52  18.13  36.36
35.57  18.20  35.52  18.20  53.78
18.18  18.16  35.49  36.67  36.54
18.37  36.53  36.67  17.19  36.65
18.29  54.87  17.14  18.24  36.68
35.49  35.61  18.27  18.36  53.77
18.24  35.43  18.35  53.90  18.37
18.24  38.00  38.00  37.99  18.99
19.01  37.98  19.00  57.00  18.99
19.00  38.00  18.99  55.01  18.98
35.99  18.00  18.01  54.98  18.00
37.00  17.99  36.00  36.00  17.99
54.01  18.98  18.00  36.02  18.98
53.16  18.34  35.59  36.20  17.98
36.00  18.00  54.00  17.99  18.00
36.00  35.99  36.01  17.99  18.00
54.00  17.98  35.99  18.00  54.28
Mean read time (ms): 30.210176811594206
Time captured (s): 10.35
Time elapsed (s): 10.466399

在我的 Windows 环境中类似的大约 10 秒录制的输出如下所示:

Starting.

Stopping.
read times (ms):
44.96  30.13  29.97  29.97  30.04
29.96  29.96  30.00  29.99  30.00
29.92  30.01  30.02  30.01  29.99
29.85  45.12  30.03  29.92  29.96
29.98  30.00  29.98  30.00  0.24
44.73  29.94  30.04  29.96  29.86
29.96  30.05  29.85  30.17  30.02
30.00  29.94  29.99  29.99  30.04
29.97  44.99  29.99  30.08  29.88
30.05  29.95  29.97  29.87  0.15
44.95  29.98  29.91  30.08  29.98
30.00  30.01  29.96  29.94  30.04
30.01  29.96  29.88  30.00  29.95
30.04  44.99  29.99  29.96  30.03
30.00  30.07  29.94  30.01  0.21
44.77  29.95  30.02  30.01  30.00
29.96  29.98  30.00  30.00  29.94
29.99  30.04  29.93  29.99  30.02
29.98  44.99  29.99  29.96  30.01
30.03  29.95  30.00  29.97  0.21
44.81  29.88  30.05  29.99  29.99
30.01  29.97  29.99  29.99  29.98
29.99  30.00  29.97  29.98  29.97
30.01  44.95  29.97  30.03  30.00
30.00  30.00  29.99  29.97  0.21
44.79  29.95  30.00  29.99  29.95
29.98  29.93  30.06  29.94  30.08
29.97  30.00  29.97  29.99  29.98
29.94  45.05  30.04  29.91  30.00
29.99  29.97  30.01  29.98  0.21
44.79  29.94  29.99  29.89  30.06
30.03  29.96  30.04  29.98  29.90
30.04  30.00  29.98  30.00  29.97
30.07  44.96  29.98  29.93  30.07
29.98  29.90  30.00  29.94  0.13
44.97  29.98  29.99  29.94  30.02
30.00  29.93  29.99  30.02  30.01
29.99  29.96  30.02  29.90  29.93
30.01  45.04  30.06  29.99  29.98
29.94  30.04  30.00  29.92  0.20
44.83  29.94  29.99  30.00  30.01
30.02  29.87  30.03  29.94  30.03
29.99  30.00  30.07  29.90  29.95
30.05  44.97  30.01  29.98  29.97
30.01  29.99  30.00  29.97  0.21
44.77  29.96  30.00  30.03  29.91
30.00  30.01  30.03  29.93  29.98
29.99  29.99  29.93  30.04  30.04
30.01  44.92  30.04  29.97  29.91
30.08  29.89  29.97  29.88  0.15
45.01  30.09  29.89  30.01  30.01
29.97  29.95  29.96  30.05  30.04
29.88  30.00  29.99  29.94  30.05
29.98  44.99  30.01  30.00  29.99
29.95  30.00  29.88  30.11  0.21
44.78  30.01  29.96  29.99  29.98
29.98  29.99  30.01  29.91  29.82
30.10  29.99  30.15  29.96  29.93
29.98  45.05  29.97  29.99  30.02
29.96  29.98  29.95  30.04  0.21
44.74  30.02  29.97  29.97  30.03
29.99  29.93  29.94  30.07  29.99
29.99  29.94  30.02  29.97  29.90
30.01  45.12  29.91  30.03  29.95
30.03  29.97  29.87  30.09  0.20
44.79  29.98  29.97  29.99  30.01
30.01  29.97  29.99  29.99  30.01
29.99  29.94  30.01  30.00  29.98
29.98  45.02  29.97  29.91  30.06
29.99  29.96  30.02  29.98
Mean read time (ms): 30.073811959885386
Time captured (s): 10.47
Time elapsed (s): 10.777957116

Linux 环境下大约 30 秒记录的汇总统计数据:

Mean read time (ms): 30.152922254616133
Time captured (s): 30.87
Time elapsed (s): 31.135111

Windows 环境下大约 30 秒录制的汇总统计数据:

Mean read time (ms): 30.020078674852652
Time captured (s): 30.54
Time elapsed (s): 30.901762071

我注意到经过时间和捕获时间之间的差异随着 Linux 端记录时间的增加而增加。在 Linux 端,单个获取时间似乎也不太规律。

我已经尝试调整缓冲区和提取大小,但我还没有找到一种组合可以足够快地从线路中提取。

什么可能导致获取缓慢?我如何确定合理的获取和缓冲区大小,以便具有低延迟但足够快的获取以跟上实时? Linux 上是否存在可能会影响这一点或我应该检查的声音配置问题?

谢谢!

【问题讨论】:

你匹配DataLine的比特率吗? DataLine getFormat() docs.oracle.com/javase/7/docs/api/javax/sound/sampled/… *edit 或者 AudioInputStream 的 getFormat() docs.oracle.com/javase/7/docs/api/javax/sound/sampled/… 我相信它们在 Windows 上同样不规则。但 Windows 倾向于以 15 毫秒为单位进行计时,而不是以毫秒为单位。 @j.con getFormat() 在目标数据行或以该行作为参数构造的 AudioInputStream 给我返回用于获取行的格式(8000 Hz,16 位、单声道、签名 PCM、大端)。 一些想法:字符串 I/O 可能会使问题有点混乱。也许改为写入数组,并在测试结束之前不要发布结果。其次,更喜欢 System.nanoTime() 的时间戳。 Window 的系统时钟(在 currentTimeMillis() 中使用)每 15.5 毫秒或类似的时间更新一次。 nanoTime() 改为使用高分辨率时间源。有兴趣看看你是否得到相同的结果。此外,还有其他库,例如 JAsioHost(我听说过但没有使用过)。 @PhilFreihofner 好的,我将测试程序更改为使用 nanoTime() 并重新运行测试。有趣的是,对于我使用的特定参数,给出的时间似乎接近 15 毫秒的倍数。但是,这也可能是在 windows 上实现 nanoTime 的产物吗?另外,我确实保留了各个时间的列表,并等到录音结束再打印出来。 【参考方案1】:
private static final int MIC_FETCH_SIZE = 480; // 0.12 seconds of data 

对于可靠的性能来说,缓冲区大小太小了。在 16 位单声道下,它仅代表 240 个声音样本。让它更像是 16000 个样本,或者:

private static final int MIC_FETCH_SIZE = 32000; // 2 seconds of data

注意:Java Sound 不保证读取的数量,而是返回实际读取的字节数。关键是,允许机会读取最多 2 秒的数据(如果可用)。

我认为这应该可以解决上述大部分问题。

【讨论】:

感谢您的回答。你如何从 480 字节到 30 个样本?每个 16 位样本不是 2 个字节(导致 240 个样本)吗? 哦,对不起,整个比特/字节的事情。 :P 不过,试着把它显着变大。 啊,好的。 :) 增加一次读取的数量以减少开销确实有意义。我对如此大的提取的担心是,至少会有与我正在处理的应用程序中提取时间一样多的延迟(用于通信)。我想我只需要在可靠性和延迟之间进行权衡。 那么,使用 Java 声音 API 是否不可能可靠地在麦克风的声音处理中获得 它有助于跳过,但我认为这意味着延迟至少与一个麦克风获取对应的时间量一样高。由于需要获取比这更大的块,是否不可能将延迟低至 30 毫秒?

以上是关于linux上的java声音:如何足够快地从TargetDataLine捕获以跟上?的主要内容,如果未能解决你的问题,请参考以下文章

无法足够快地分配内存?

如果 CoreMotion 的更新处理程序没有足够快地完成怎么办?

Capybara Webkit似乎没有足够快地提交表单

Lua没有足够快地实例化局部变量来打印它?嵌入式ESP8266

计算机组成原理基础的一些概念

Linux 上的 IDE 下没有声音(在 Jar 中可以)