0

A friend recently asked me to make a simple Buzzer program and has found a strange "bug" in what I wrote for him.

If a key is pushed and the buzzer is reset rapidly over the course of a few seconds he observes a 2 to 3 second freeze of the program that occurs between the the first key press after the reset and the "buzz" indication. He has the following Java installation:

build 1.7.0_25-b16

However, I do not experience this problem on my computer, with the following Java installation:

$ java -version
java version "1.6.0_51"
Java(TM) SE Runtime Environment (build 1.6.0_51-b11-457-10M4509)
Java HotSpot(TM) 64-Bit Server VM (build 20.51-b01-457, mixed mode)

Upon unfreeze, the program returns the appropriate key (that is, not the last key pressed, but the first one pushed after the reset and before the freeze). This suggests that the problem is not with the listener but with the reaction to the listener.

Any thoughts on what might be causing this phenomenon? Thanks in advance for your help.

Source code:

/**
 * Buzzer.java
 *
 * Buzzer
 */
package org.lexingtonma.lhs.nhb;

import java.awt.Color;
import java.awt.DefaultKeyboardFocusManager;
import java.awt.Font;
import java.awt.KeyEventDispatcher;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;


/**
 * @author Arman D. Bilge
 *
 */
public class Buzzer extends JFrame implements KeyListener {

    private static final long serialVersionUID = 7492374642744742658L;
    private static final String BUZZ_A = "BuzzA.wav";
    private static final String BUZZ_B = "BuzzB.wav";
    private Clip buzz = null;
    private boolean listening = true;
    private final JTextField display = new JTextField(3);
    private final JButton reset = new JButton("Reset");

    {

        DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager()
            .addKeyEventDispatcher(new KeyEventDispatcher() {
                public boolean dispatchKeyEvent(KeyEvent e) {
                    keyTyped(e);
                    return false;
                }
            });
        setTitle("Buzzer");
        final JPanel panel = new JPanel();
        setSize(256, 162);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        display.setFont(new Font("Helvetica", Font.BOLD, 64));
        display.setForeground(Color.WHITE);
        display.setVisible(true);
        display.setEditable(false);
        display.setHorizontalAlignment(JTextField.CENTER);
        panel.add(display);
        reset.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                display.setText("");
                display.setBackground(Color.WHITE);
                listening = true;
                reset.setEnabled(false);
            }
        });
        reset.setEnabled(false);
        panel.add(reset);
        add(panel);
        try {
            buzz = AudioSystem.getClip();
        } catch (LineUnavailableException e) {
            JOptionPane.showMessageDialog(this, e.getLocalizedMessage(), "FATAL ERROR", JOptionPane.ERROR_MESSAGE);
        }
    }

    public static final void main(String args[]) {
        Buzzer b = new Buzzer();
        b.setVisible(true);
    }

    public void keyPressed(KeyEvent e) {
        // Do nothing        
    }

    public void keyReleased(KeyEvent e) {
        // Do nothing        
    }

    public void keyTyped(KeyEvent e) {
        final char c = e.getKeyChar();
        if (listening && Character.isLetterOrDigit(c)) {
            buzz.close();
            listening = false;
            if (Character.isDigit(c)) {
                display.setBackground(Color.RED);
                try {
                    buzz.open(AudioSystem.getAudioInputStream(getClass().getResource(BUZZ_A)));
                    buzz.start();
                } catch (Exception ex) {
                    JOptionPane.showMessageDialog(this, ex.getLocalizedMessage(), "FATAL ERROR", JOptionPane.ERROR_MESSAGE);
                    System.exit(1);
                }
            } else {
                display.setBackground(Color.BLUE);
                try {
                    buzz.open(AudioSystem.getAudioInputStream(getClass().getResource(BUZZ_B)));
                    buzz.start();
                } catch (Exception ex) {
                    JOptionPane.showMessageDialog(this, ex.getLocalizedMessage(), "FATAL ERROR", JOptionPane.ERROR_MESSAGE);
                    System.exit(1);
                }
            }
            display.setText("" + c);
            reset.setEnabled(true);
        }

    }

}

Jar: https://www.dropbox.com/s/62fl2i97m9hrx9m/Buzzer.jar

Matt Ball
  • 354,903
  • 100
  • 647
  • 710
nimble agar
  • 417
  • 8
  • 15
  • What operating system? – sage88 Oct 06 '13 at 02:17
  • He's on Windows 7; I'm on Mac OS X 10.6. The problem also appears in JVM7 installations on Mac OS X. – nimble agar Oct 06 '13 at 02:19
  • So the bug is present for 1.7 in both OSX and Win 7 then? (I've had some issues with keybinding and 1.7 and OSX, but everything worked fine in Win 7 so I figured I'd ask) – sage88 Oct 06 '13 at 02:20
  • btw you might consider using KeyAdapter if you're not going to use some of the methods: http://docs.oracle.com/javase/7/docs/api/java/awt/event/KeyAdapter.html – sage88 Oct 06 '13 at 02:23
  • Yes, it appears in 1.7 on both OSX and Windows 7. – nimble agar Oct 06 '13 at 02:23
  • @sage88 Thanks for suggesting KeyAdapter; good to know about. Actually it turns out that I didn't need to use the KeyListener interface anyway, and unfortunately I'm already extending JFrame. – nimble agar Oct 06 '13 at 02:27
  • @sage88 Might as well use the key bindings API as it doesn't suffer from the focus issues that key listener does... – MadProgrammer Oct 06 '13 at 02:27
  • There seems to be a change in the threading model between 6 & 7 which can cause a dead lock in the EDT if your use of threads isn't don't right. Try using a separate thread to load and start the buzz in... – MadProgrammer Oct 06 '13 at 02:29
  • @MadProgrammer I agree that this would be better done with key bindings, was just making a suggestion for the way he'd done it. – sage88 Oct 06 '13 at 02:35
  • @MadProgrammer I tried launching new threads for the buzzing but unfortunately to no avail. Maybe I will switch to key bindings then. – nimble agar Oct 06 '13 at 02:38

2 Answers2

2

It's safer to create a new Clip object every time and discard the old one. Try this method

public static void play(String name) {
   try{
     AudioInputStream sounds = AudioSystem.getAudioInputStream(Buzzer.class.getResource(name));
     final Clip clip = AudioSystem.getClip();
     clip.addLineListener(new LineListener() {
         public void update(LineEvent e) {
             LineEvent.Type type = e.getType();
             if(type == type.STOP) clip.close();
         }
     });
     clip.open(sounds);
     clip.start();
   } catch(Exception e){
       e.printStackTrace();
   }

}

Tesseract
  • 8,049
  • 2
  • 20
  • 37
  • btw. did you read the API documentation for Clip.open? It says there "Invoking this method on a line which is already open is illegal and may result in an IllegalStateException" – Tesseract Oct 06 '13 at 03:00
  • Thanks, this worked! No, I did not look at the API very carefully, but I thought using `close()` before every `open()` would prevent this? – nimble agar Oct 06 '13 at 03:03
  • The open and close methods are both non blocking. i.e. calling close() returns immediately without waiting for the clip to actually close. So when you call open the clip may not be closed yet. – Tesseract Oct 06 '13 at 03:05
1

As was suggested, you could give keybinding a try to see if it helps. This just shows how to do it with the Enter key.

public class Buzzer extends JFrame {
private static final String enter = "ENTER";
    public Buzzer() {
        // Key bound AbstractAction item
        enterAction = new EnterAction();
        // Gets the JFrame InputMap and pairs the key to the action
        this.getInputMap().put(KeyStroke.getKeyStroke(enter), "doEnterAction");
        // This line pairs the AbstractAction enterAction to the action "doEnterAction"
        this.getActionMap().put("doEnterAction", enterAction);
    }

    private class EnterAction extends AbstractAction {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("Enter was pressed.");
        }
    }
}

Actually I can't remember if this will work when applied to a JFrame, you might need to make a JPanel first to apply these to. Just replace 'this' with the panel name.

sage88
  • 4,104
  • 4
  • 31
  • 41
  • Do I need to make a separate rule for every KeyStroke? Or is there an easy way to capture all key presses and then perform logic on them? This answer seems to suggest not: http://stackoverflow.com/a/15422641/1642693 – nimble agar Oct 06 '13 at 02:49
  • If you want to map several keys to the same function then you can just bind them all to the same action when you do the getActionMap.put(), but there's no way to do a range of keys, key bindings are intended to map a single key event to an action. – sage88 Oct 06 '13 at 02:56