使用SourceDataLine的低帧速率

时间:2014-02-02 06:08:14

标签: java swing repaint javasound frame-rate

我用简单的Swing图形编写了一段时间的声音,但由于某些原因我的帧速率不稳定。

通常我在后台线程上执行以下操作:

for(;;) {
    // do some drawing
    aPanel.updateABufferedImage();
    // ask for asynchronous repaint
    aPanel.repaint();

    // write the sound
    aSourceDataLine.write(bytes, 0, bytes.length);
}

通过调试,我认为我已经将问题追溯到SourceDataLine#write的阻止行为。其文件陈述如下:

  

如果调用者尝试写入的数据多于当前编写的数据[...],则此方法将一直阻塞,直到写入所请求的数据量为止。

所以,这似乎意味着SourceDataLine实际上有自己的缓冲区,当我们将我们的缓冲区传递给write时它正在填充。它只在其自己的缓冲区已满时才会阻塞。这似乎是持久性:让它以可预测的方式阻止。

为了证明这个问题,这是一个最小的例子:

  • 将0写入SourceDataLine(没有声音)并计时。
  • 绘制任意图形(翻转每个像素颜色)并重复重绘周期。

example screenshot

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.sound.sampled.*;

class FrameRateWithSound implements Runnable {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new FrameRateWithSound());
    }

    volatile boolean soundOn = true;
    PaintPanel panel;

    @Override
    public void run() {
        JFrame frame = new JFrame();
        JPanel content = new JPanel(new BorderLayout());

        final JCheckBox soundCheck = new JCheckBox("Sound", soundOn);
        soundCheck.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                soundOn = soundCheck.isSelected();
            }
        });

        panel = new PaintPanel();

        content.add(soundCheck, BorderLayout.NORTH);
        content.add(panel, BorderLayout.CENTER);

        frame.setContentPane(content);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);

        new Thread(new Worker()).start();
    }

    class Worker implements Runnable {
        @Override
        public void run() {
            AudioFormat fmt = new AudioFormat(
                AudioFormat.Encoding.PCM_SIGNED,
                44100f, 8, 1, 1, 44100f, true
            );

            // just 0's
            byte[] buffer = new byte[1000];

            SourceDataLine line = null;
            try {
                line = AudioSystem.getSourceDataLine(fmt);
                line.open(fmt);
                line.start();

                for(;;) {
                    panel.drawNextPixel();
                    panel.repaint();

                    if(soundOn) {
                        // time the write
                        long t = System.currentTimeMillis();

                        line.write(buffer, 0, buffer.length);

                        t = ( System.currentTimeMillis() - t );
                        System.out.println("sound:\t" + t);
                    }

                    // just so it doesn't fly off the handle
                    Thread.sleep(2);
                }
            } catch(Exception e) {
                // lazy...
                throw new RuntimeException(e);
            } finally {
                if(line != null) {
                    line.close();
                }
            }
        }
    }

    class PaintPanel extends JPanel {
        Dimension size = new Dimension(200, 100);

        BufferedImage img = new BufferedImage(
            size.width, size.height, BufferedImage.TYPE_INT_RGB);

        int x, y;

        int repaints;
        long begin, prev;
        String fps = "0";

        PaintPanel() {
            setPreferredSize(size);
            setOpaque(false);

            Graphics2D g = img.createGraphics();
            g.setColor(Color.LIGHT_GRAY);
            g.fillRect(0, 0, size.width, size.height);
            g.dispose();
        }

        synchronized void drawNextPixel() {
            img.setRGB(x, y, img.getRGB(x, y) ^ 0xFFFFFF); // flip

            if( ( ++x ) == size.width ) {
                x = 0;
                if( ( ++y ) == size.height ) {
                    y = 0;
                }
            }
        }

        @Override
        protected synchronized void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(img, 0, 0, size.width, size.height, null);

            long curr = System.currentTimeMillis();
            // time this cycle
            long cycle = ( curr - prev );
            System.out.println("paint:\t" + cycle);

            ++repaints;
            // time FPS every 1 second
            if(curr - begin >= 1000) {
                begin = curr;
                fps = String.valueOf(repaints);
                repaints = 0;
            }

            prev = curr;

            g.setColor(Color.RED);
            g.drawString(fps, 12, size.height - 12);
        }
    }
}

如果您对此感到好奇,我建议您实际运行该示例。

“播放”期间的典型System.out Feed类似于以下内容:

sound:  0
paint:  2
sound:  0
paint:  2
sound:  0
paint:  3
sound:  0
paint:  2
paint:  2
sound:  325 // <- 'write' seems to be blocking here
sound:  0
paint:  328
sound:  0
paint:  2

这显示了write的行为非常清楚:它在大部分时间内旋转,然后在很长一段时间内阻挡,此时重新突出显示。 FPS仪表通常在播放期间显示~45,但动画显然不稳定。

当声音关闭时,FPS爬升,动画流畅。

那么有办法解决它吗?我究竟做错了什么?如何让write定期阻止?

这种行为在Windows和OSX环境中都很明显。

我尝试过的一件事就是使用Thread.sleep来规范它,但这并不是很好。它仍然不稳定。

2 个答案:

答案 0 :(得分:2)

解决方案似乎是使用open(AudioFormat, int)打开具有指定缓冲区大小的行。

line.open(fmt, buffer.length);

再次对其进行定时,我们可以看到write阻止更加一致:

sound:  22
paint:  24
sound:  21
paint:  24
sound:  20
paint:  22
sound:  21
paint:  23
sound:  20
paint:  23

动画很流畅。

答案 1 :(得分:0)

我严重怀疑声音回放是罪魁祸首。请参阅我对主要问题的评论。音频write()方法中发生的阻塞与音频呈现给回放系统的速率有关。由于音频处理通常比音频系统可以播放的速度快一个数量级(限制为44100 fps),因此大部分时间用于BOTH SourceDataLine和Clip。在这种阻塞形式中,CPU可以自由地执行其他操作。它没有挂起。

我更担心你对图像使用同步,以及对图像进行编辑。我非常确定在某个级别上进行编辑会消除该图像的默认图形加速。

您可以在Java-Gaming.org上查看有关Graphics2D优化的链接 http://www.java-gaming.org/topics/java2d-clearing-up-the-air/27506/msg/0/view/topicseen.html#new

我发现它对优化我的2D图形非常有帮助。

我不确定为什么你会在你的特定情况下得到融合。对我来说几次问题就是当帧和组件的循环代码在同一个类中时。通过将“游戏循环”代码和组件放在不同的类中,问题总是会消失,所以我从不打扰进一步思考它。因此,我没有清楚地了解为什么会起作用,或者这个行动是否是一个因素。

[编辑:只是仔细查看您的音频代码。我认为还有优化空间。有计算被不必要地重做并且可能正在消耗cpu。例如,由于内循环中有最终值,为什么每次迭代都要重新计算该部分?取常数部分并将其计算为一次值,并仅计算内循环中的未知数。我建议重构以避免所有同步和优化音频数据的数据生成,然后查看是否仍有问题。]