0

I've been working on a java project that makes calls from an usb modem. The application works pefrectly on my computer, but when I tryed to run it on a lower-specs one, the audio stream of the person calling from the pc goes out perfectly and it's perfectly heared on the phone called. But the audio that should be heared by the pc user gets delayed (3 to 5 secs), with white noise, and makes literally impossible to make a conversation.

Some things to take in mind:

  • My computer is an i3 4gb RAM notebook, and the low specs are Pentium 4 1gb RAM desktop.
  • I tested the CPU and RAM usages, the application consumes 20 - 25% of the cpu on my computer, almost 100% on the low-specs one, and about 30 - 40mb from RAM on both cases.
  • The application also has a call recording feature, and for some reason the output files are written perfectly (no delays or iterferences).

Any clue on what could be the problem or how can it be solved?

Class made to handle the audio after I start the new thread:(ingoing call audio)

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 )
        {
        }          
    }

Class made to handle the audio after I start the new thread:(outgoing call audio)

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 )
        {
        }            
    }

Thank you in advice

4

2 回答 2

1

您需要采取的步骤是:

  1. 分析/采样应用程序并找出真正花费时间的地方。VisualVM 功能强大且免费,是 JDK 的一部分。启动您的应用程序。启动 VisualVM。让 VisualVM 连接到您的应用程序。转到 Sampler 选项卡并开始对 CPU 使用情况进行采样。几分钟后拍摄快照。看它。如果您无法弄清楚,请在此处发布一些内容。
  2. 将音频缓冲区初始化移出循环。如果您的缓冲区为 20 毫秒,则正在分配字节数组并每秒收集 50 次垃圾。这很明显也很容易做到,但可能不会解决您的问题。
  3. 用 BufferedOutputStreams 包装您的 FileOutputStreams。像这样:OutputStream fos = new BufferedOutputStream( new FileOutputStream("Llamadas/" + timestamp + "-" + tel + "-OUT.raw")); 您将获得极大的性能提升。现在循环的每次迭代都等待缓冲区完成写入磁盘的方式。物理磁盘很慢,这增加了很多等待。
  4. 摆脱内部的while循环。实际填满缓冲区并不重要。当内部 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 中的改进不够好,您可以将写入移到后台。一旦您读取了第一条数据,您就可以在单独的线程中开始写入 - 但不要等待写入完成。立即开始读取下一条数据。获得下一位数据后,您将等待第一次写入完成,然后在后台开始第二次写入。
于 2013-09-24T16:36:02.423 回答
0

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

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

于 2013-09-18T20:08:18.517 回答