在Java上延迟语音流

时间:2013-09-05 16:03:12

标签: java audio

我一直在研究一个从usb调制解调器调用的java项目。 该应用程序在我的计算机上运行正常,但是当我尝试在较低规格的应用程序上运行时,从PC呼叫的人的音频流完全熄灭,并且在电话上完全听到了。但是应该由电脑用户听到的音频被延迟(3到5秒),伴有白噪声,并且几乎不可能进行对话。

要记住的一些事情:

  • 我的电脑是i3 4gb RAM笔记本电脑,低规格是Pentium 4 1gb RAM桌面。
  • 我测试了CPU和RAM的使用情况,应用程序在我的计算机上消耗了20 - 25%的CPU,在低规格的CPU上消耗了近100%,在两种情况下消耗了大约30 - 40mb的内存。
  • 该应用程序还具有通话记录功能,并且由于某种原因输出文件写得很完美(没有延迟或不正确)。

关于可能出现什么问题或如何解决问题的任何线索?

在我开始新线程后处理音频的类:(进入呼叫音频)

public class SerialVoiceReader implements Runnable{

    /** The running. */
private volatile boolean running = true;

/** The in. */
DataInputStream in;

/** The af. */
AudioFormat af;

/** The samples per frame. */
private int samplesPerFrame = 160; 

/** The audio buffer size. */
private int audioBufferSize = samplesPerFrame * 2 ; //20ms delay

private String tel;

private String timestamp;

public SerialVoiceReader ( DataInputStream in,  AudioFormat af){
    this.in = in;
    this.af = af;
}

public void run (){
        try
        {
            Info infos = new Info(SourceDataLine.class, af);
            SourceDataLine dataLine  = (SourceDataLine) AudioSystem.getLine(infos);
            dataLine.open(dataLine.getFormat(),audioBufferSize *2);                     
            dataLine.start();   
// set the volume up
            if (dataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
                FloatControl volume = (FloatControl) dataLine.getControl(FloatControl.Type.MASTER_GAIN);
                volume.setValue(volume.getMaximum());
            }
// get a field from GUI to set as part of the file name
            tel = CallGUI.telField.getText();
            timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(Calendar.getInstance().getTime());

            // save the stream to a file to later set the header and make it .wav format
            FileOutputStream fos = new FileOutputStream("Llamadas/" + timestamp + "-" + tel + "-OUT.raw");
            // the audio buffer writing (this is the audio that goes out on the call)
            while (running){
                byte[] buffer = new byte[audioBufferSize];
                int offset = 0;
                int numRead = 0;
                while (running && (offset < buffer.length && (numRead = this.in.read(buffer, offset, buffer.length - offset)) >= 0)) 
                {
                    offset += numRead;
                }
                if(running && offset>=0){
                    dataLine.write(buffer, 0, offset);
                    fos.write(buffer);
                }
            }   
            dataLine.stop();
            dataLine.drain();
            dataLine.close();
            fos.close();

        }
        catch ( Exception e )
        {
        }          
    }

在我开始新线程后处理音频的类:(拨出呼叫音频)

public class SerialVoiceWriter implements Runnable{

    /** The running. */
    private volatile boolean running = true;

    /** The out. */
    DataOutputStream out;

    /** The af. */
    AudioFormat af;

    /** The samples per frame. */
    private int samplesPerFrame = 160; 

    /** The audio buffer size. */
    private int audioBufferSize = samplesPerFrame * 2; //20ms delay

    private String tel;

    private String timestamp;

    public SerialVoiceWriter ( DataOutputStream out, AudioFormat af, Boolean playMessage)
    {
        this.out = out;
        this.af = af;
    }

    public void run ()
    {   
        try
        {   
                Info infos = new Info(TargetDataLine.class, af);
                TargetDataLine dataLine  = (TargetDataLine) AudioSystem.getLine(infos);
                dataLine.open(dataLine.getFormat(),audioBufferSize*2 );
                dataLine.start();

                tel = CallGUI.telField.getText();
                timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(Calendar.getInstance().getTime());

                FileOutputStream fis = new FileOutputStream("Llamadas/" + timestamp + "-" + tel + "-IN.raw");
                while (running){
                    byte[] audioBuffer = new byte[audioBufferSize];
                    int offset = 0;
                    int numRead = 0;
                    while (running && (offset < audioBuffer.length && (numRead = dataLine.read(audioBuffer, offset, audioBuffer.length - offset)) > 0)) 
                    {
                        offset += numRead;
                    }
                    if(running && offset>=0){
                        this.out.write(audioBuffer);
                        fis.write(audioBuffer);
                    }
                }               
                    dataLine.flush();   
                    dataLine.stop();
                    dataLine.close();
                    fis.close();
                    dataLine = null;                

        }
        catch (Exception e )
        {
        }            
    }

谢谢你的建议

2 个答案:

答案 0 :(得分:1)

您需要采取的步骤是:

  1. 对应用程序进行配置文件/示例,找出实际花费的时间。 VisualVM功能强大且免费,是JDK的一部分。开始申请。启动VisualVM。让VisualVM连接到您的应用程序。转到Sampler选项卡并开始采样CPU使用情况。几分钟后拍摄快照。看它。如果你无法搞清楚,可以在这里发布一些内容。
  2. 将音频缓冲区初始化移出循环。如果您的缓冲区是20ms,则正在分配字节数组,并且垃圾收集每秒50x。这显而易见,但可能无法解决您的问题。
  3. 使用BufferedOutputStreams 包装FileOutputStreams。像这样:OutputStream fos = new BufferedOutputStream( new FileOutputStream("Llamadas/" + timestamp + "-" + tel + "-OUT.raw"));你将获得极大改善的表现。现在的方式是循环的每次迭代都等待缓冲区完成写入磁盘。物理磁盘很慢,而且很多等待。
  4. 摆脱内在的循环。实际填充缓冲区并不重要。当内部while循环填满那个缓冲区时,你就会失去同步。您要做的是尝试从输入流中读取一次,如果读取了某些内容,则将读取的内容写入输出流。而不是调用write(byte [])调用DataOutputStream write(byte [],off,len)
  5. 这将需要更多的工作:而不是写入dataLine,然后按顺序写入fos,并行写入它们。它们每个都花费一定的时间将数据写入各自的目的地。如果fos需要X微秒而dataLine需要Y,那​​么当前代码需要X + Y微秒。如果你并行执行,最终只能等待max(X,Y)。 `

    ExecutorService es = Executors.newFixedThreadPool(2);
    Callable<Void>[] calls = new Callable[2];
    //... your other code here...
    if (running && offset >= 0) {   
      final int finalOffset = offset;
      Callable<Void> call1 = new Callable<Void>()
      {
        @Override
        public Void call() throws Exception
        {
          dataLine.write(buffer, 0, finalOffset);
          return null;
        }
      };
    
      Callable<Void> call2 = new Callable<Void>()
      {
        @Override
        public Void call() throws Exception
        {
           fos.write(buffer);  // or however you need to write.
           return null;
        }
       };
    
       calls[0] = call1;
       calls[1] = call2;
       List<Callable<Void>> asList = Arrays.asList(calls);
       es.invokeAll(asList);  // invokeAll will block until both callables have completed.
    }
    

    `

  6. 如果#5的改进不够好,您可以将写入移到后台。一旦读完第一段数据,就可以在不同的线程中启动写入 - 但不要等待写入完成。立即开始阅读下一条数据。获得下一位数据后,等待第一次写入完成,然后在后台开始第二次写入。

答案 1 :(得分:0)

我认为无论造成接近100%CPU的原因都是罪魁祸首。但这并没有真正告诉你任何具体的东西。首先,由于问题在于低端PC上的播放,您可能需要检查该设备上的音频驱动程序是否是最新的。之后,我会考虑优化处理音频接收的代码部分。虽然较旧的PC较低,但我认为它不应该对您要实现的内容有任何问题。我建议您在应用程序运行时运行性能分析器,以查看需要很长时间的内容。

更新:您可以尝试增加audioBufferSize以查看它是否有任何效果,20ms似乎很低。提供的代码仅适用于从PC发送的音频。从手机收到的音频怎么样?