当我在另一个应用程序中输入时,我的 Java 程序没有发出声音

My Java program doesn't produce an audible sound when I'm typing in another application

我用 Swing 编写了一个相当简单的时钟和计时器程序。计时器似乎工作正常。它以正确的速率倒计时秒、分钟和可能的小时(尽管我还没有测试过那个),当时间到期时,它会播放一个 *.wav 文件来通知您。问题是如果程序在后台 运行,则声音并不总是能听到。我刚才做了一些测试,如果我只是在 Firefox 中阅读网页,我可以正常听到声音,但是当我输入内容时,例如 URL,然后声音就没有了好像根本没戏

这是我的 Java 代码的问题吗——我这个周末才在 Java 开始处理声音问题——还是操作系统的问题?我正在使用 OpenJDK 11 和 Ubuntu 18.04.

这里是(我希望是)相关的代码位。

class ClipPlayer implements LineListener {

    boolean playable;
    boolean done;
    AudioInputStream stream;
    Clip clip;

    public ClipPlayer(String fileName) {

        playable = false;
        done = false;
        stream = null;
        clip = null;
        File source = new File(fileName);
        boolean proceed = source.exists();  
        if (proceed) {
            try {
                stream = AudioSystem.getAudioInputStream(source);
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                proceed = false;
            }
        }       
        if (proceed) {
            AudioFormat format = stream.getFormat();
            DataLine.Info info = new DataLine.Info(Clip.class,format);
            try {
                clip = (Clip)AudioSystem.getLine(info);
                clip.addLineListener(this);
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                proceed = false;
            }
        }
        playable = proceed;

    }

    public void close() {

        try {
            clip.close();
            stream.close();
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
            System.exit(1);
        }

    }

    public void open() {

        if (playable) {
            try {
                clip.open(stream);
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                System.exit(1);
            }
        } else {
            System.out.println("The file cannot be played.");
        }

    }

    public void play() {

        if (playable && !done) {            
            try {
                clip.start();
                while (!done) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        System.out.println("Exception: " + e.getMessage());
                        System.exit(1);
                    }
                }
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                System.exit(1);
            }
        } else if (!playable) {
            System.out.println("The file cannot be played.");
        } else if (done) {
            System.out.println("The player must be reset before it can be played again.");
        }

    }

    public void repeat(int n) {

        for (int i = 0; i < n; i++) {
            play();
            reset();
        }

    }

    public void reset() {

        done = false;
        clip.setFramePosition(0);

    }

    @Override
    public void update(LineEvent event) {
        if (event.getType() == LineEvent.Type.STOP) {
            done = true;
        }
    }

}


class Timer extends JPanel implements ActionListener {

    private static final int MILLISECONDS_PER_SECOND = 1000;
    private static final int MILLISECONDS_PER_MINUTE = 60*MILLISECONDS_PER_SECOND;
    private static final int MILLISECONDS_PER_HOUR   = 60*MILLISECONDS_PER_MINUTE;

    private static final int HOURS   = 0;
    private static final int MINUTES = 1;
    private static final int SECONDS = 2;
    private static final int RESET   = 0;
    private static final int PAUSE   = 1;

    long       timeRemaining;
    boolean    finished;
    boolean    paused;
    JSpinner[] spinners;
    JButton[]  buttons;
    JLabel     remainderLabel;
    ClipPlayer player;

    public Timer() {

        timeRemaining = 0;
        finished = true;
        paused = true;
        player = new ClipPlayer("/example/path/file.wav");
        spinners = new JSpinner[3];
        spinners[HOURS] = new JSpinner(new SpinnerNumberModel(0,0,99,1));
        spinners[MINUTES] = new JSpinner(new SpinnerNumberModel(0,0,60,1));
        spinners[SECONDS] = new JSpinner(new SpinnerNumberModel(0,0,60,1));
        buttons = new JButton[2];
        buttons[RESET] = new JButton("Begin");
        buttons[PAUSE] = new JButton("Pause");
        buttons[PAUSE].setEnabled(false);
        for (int i = 0; i < 2; i++) {
            buttons[i].addActionListener(this);
            buttons[i].setActionCommand("Button_" + i);
        }
        remainderLabel = new JLabel();

        // Layout code omitted for brevity

    }

    // actionPerformed method omitted for brevity

    public void initialize() {

        Object h = spinners[HOURS].getValue();
        Object m = spinners[MINUTES].getValue();
        Object s = spinners[SECONDS].getValue();
        timeRemaining = ((Integer)h)*MILLISECONDS_PER_HOUR;
        timeRemaining += ((Integer)m)*MILLISECONDS_PER_MINUTE;
        timeRemaining += ((Integer)s)*MILLISECONDS_PER_SECOND;
        if (timeRemaining > 0) {
            finished = false;
            paused = false;
            buttons[RESET].setText("Reset");
            buttons[PAUSE].setEnabled(true);
        } else {
            finished = true;
            paused = false;
        }

    }

    public void update(long dt) {

        if (!finished && !paused) {
            timeRemaining -= dt;
            if (timeRemaining > 0) {
                long hours = timeRemaining/MILLISECONDS_PER_HOUR;
                long r = timeRemaining - hours*MILLISECONDS_PER_HOUR;
                long minutes = r/MILLISECONDS_PER_MINUTE;
                r -= minutes*MILLISECONDS_PER_MINUTE;
                long seconds = r/MILLISECONDS_PER_SECOND;
                StringBuilder sb = new StringBuilder();
                sb.append(hours);
                sb.append(":");
                if (minutes < 10) sb.append("0");
                sb.append(minutes);
                sb.append(":");
                if (seconds < 10) sb.append("0");
                sb.append(seconds);
                remainderLabel.setText(sb.toString());
            } else {
                finished = true;
                buttons[RESET].setText("Begin");
                buttons[PAUSE].setEnabled(false);
                remainderLabel.setText("0:00:00");
                System.out.println("Time's up!");
                player.open();
                player.repeat(3);
                player.close();
            }
        }

    }

}

public class Time implements Runnable {

    JFrame frame;
    Clock  clock;
    Timer timer;

    // Constructor and main method omitted for brevity

    public void run() {

        long t0 = System.currentTimeMillis();
        boolean running = true;
        while (running) {
            long t1 = System.currentTimeMillis();
            clock.update(t1);
            timer.update(t1-t0);
            t0 = t1;
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
                System.out.println("Interrupted Exception: " + ie.getMessage());
                System.exit(1);
            }
        }       
        System.exit(0);

    }

}


正如我所说,该程序似乎在大多数时间都能正常运行。问题是当程序在后台 运行 时,声音并不总是能听到。我应该注意到,即使在后台 运行 时,计时器似乎也会正确倒计时。当我意识到我错过了返回 Java 程序的声音和 alt-tab 时,计时器按预期显示 0:00:00。

代码间歇性工作的事实加上问题代码中缺少 thread-safety 让我相信你有竞争条件。基本上,发生的事情是播放声音的线程不是更改 playabledone 等字段的线程。因此,一个线程对这些字段所做的更改不会被另一个线程观察到。因此,我将上面的代码重构为 thread-safe.

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.swing.*;
import java.awt.event.ActionListener;
import java.io.File;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

class ClipPlayer implements LineListener {

    private final AtomicBoolean playable;
    private final AtomicBoolean done;
    private final Optional<AudioInputStream> stream;
    private final Optional<Clip> clip;

    public ClipPlayer(String fileName) {

        playable = new AtomicBoolean(false);
        done = new AtomicBoolean(false);
        File source = new File(fileName);
        boolean proceed = source.exists();
        stream = createAudioInputStream(source, proceed);
        if (stream == Optional.<AudioInputStream>empty()) {
            proceed = false;
        }
        clip = createClip(stream.get(), proceed);
        if (clip == Optional.<Clip>empty()) {
            proceed = false;
        }

        playable.set(proceed);
    }

    private Optional<Clip> createClip(AudioInputStream stream, boolean proceed) {
        if (!proceed) {
            return Optional.empty();
        }
        AudioFormat format = stream.getFormat();
        DataLine.Info info = new DataLine.Info(Clip.class,format);
        try {
            Clip clip = (Clip)AudioSystem.getLine(info);
            clip.addLineListener(this);
            return Optional.of(clip);
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
            return Optional.empty();
        }
    }

    private static Optional<AudioInputStream> createAudioInputStream(File source, boolean proceed) {
        if (!proceed) {
            return Optional.empty();
        }
        try {
            AudioInputStream stream = AudioSystem.getAudioInputStream(source);
            Optional<AudioInputStream> optionalStream = Optional.ofNullable(stream);
            return optionalStream;
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
            return Optional.empty();
        }
    }

    public void close() {

        try {
            if (clip.isPresent()) {
                clip.get().close();
            }
            if (stream.isPresent()) {
                stream.get().close();
            }
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
            System.exit(1);
        }

    }

    public void open() {

        if (playable.get()) {
            try {
                clip.get().open(stream.get());
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                System.exit(1);
            }
        } else {
            System.out.println("The file cannot be played.");
        }

    }

    public void play() {

        if (playable.get() && !done.get()) {
            try {
                clip.start();
                while (!done.get()) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        System.out.println("Exception: " + e.getMessage());
                        System.exit(1);
                    }
                }
            } catch (Exception e) {
                System.out.println("Exception: " + e.getMessage());
                System.exit(1);
            }
        } else if (!playable.get()) {
            System.out.println("The file cannot be played.");
        } else if (done.get()) {
            System.out.println("The player must be reset before it can be played again.");
        }

    }

    public void repeat(int n) {

        for (int i = 0; i < n; i++) {
            play();
            reset();
        }

    }

    public void reset() {

        done.set(false);
        if (clip.isPresent()) {
            clip.get().setFramePosition(0);
        }

    }

    @Override
    public void update(LineEvent event) {
        if (event.getType() == LineEvent.Type.STOP) {
            done.set(true);
        }
    }

}


class Timer extends JPanel implements ActionListener {

    private static final int MILLISECONDS_PER_SECOND = 1000;
    private static final int MILLISECONDS_PER_MINUTE = 60*MILLISECONDS_PER_SECOND;
    private static final int MILLISECONDS_PER_HOUR   = 60*MILLISECONDS_PER_MINUTE;

    private static final int HOURS   = 0;
    private static final int MINUTES = 1;
    private static final int SECONDS = 2;
    private static final int RESET   = 0;
    private static final int PAUSE   = 1;

    private final AtomicLong    timeRemaining;
    private final AtomicBoolean finished;
    private final AtomicBoolean paused;
    private final JSpinner[]    spinners;
    private final JButton[]     buttons;
    private final JLabel        remainderLabel;
    private final ClipPlayer    player;

    public Timer() {

        this.timeRemaining = new AtomicLong(0L);
        this.finished = new AtomicBoolean(true);
        paused = new AtomicBoolean(true);
        player = new ClipPlayer("/example/path/file.wav");
        spinners = new JSpinner[3];
        spinners[HOURS] = new JSpinner(new SpinnerNumberModel(0,0,99,1));
        spinners[MINUTES] = new JSpinner(new SpinnerNumberModel(0,0,60,1));
        spinners[SECONDS] = new JSpinner(new SpinnerNumberModel(0,0,60,1));
        buttons = new JButton[2];
        buttons[RESET] = new JButton("Begin");
        buttons[PAUSE] = new JButton("Pause");
        buttons[PAUSE].setEnabled(false);
        for (int i = 0; i < 2; i++) {
            buttons[i].addActionListener(this);
            buttons[i].setActionCommand("Button_" + i);
        }
        remainderLabel = new JLabel();

        // Layout code omitted for brevity

    }

    // actionPerformed method omitted for brevity

    public void initialize() {

        Object h = spinners[HOURS].getValue();
        Object m = spinners[MINUTES].getValue();
        Object s = spinners[SECONDS].getValue();
        timeRemaining.set(((Integer)h)*MILLISECONDS_PER_HOUR);
        timeRemaining.addAndGet(((Integer)m)*MILLISECONDS_PER_MINUTE);
        timeRemaining.addAndGet(((Integer)s)*MILLISECONDS_PER_SECOND);
        if (timeRemaining.get() > 0) {
            this.finished.set(false);
            paused.set(false);
            buttons[RESET].setText("Reset");
            buttons[PAUSE].setEnabled(true);
        } else {
            this.finished.set(true);
            paused.set(false);
        }

    }

    public void update(long dt) {

        if (!finished.get() && !paused.get()) {
            timeRemaining.addAndGet(-1 * dt);
            if (timeRemaining.get() > 0) {
                long hours = timeRemaining.get()/MILLISECONDS_PER_HOUR;
                long r = timeRemaining.get() - hours*MILLISECONDS_PER_HOUR;
                long minutes = r/MILLISECONDS_PER_MINUTE;
                r -= minutes*MILLISECONDS_PER_MINUTE;
                long seconds = r/MILLISECONDS_PER_SECOND;
                StringBuilder sb = new StringBuilder();
                sb.append(hours);
                sb.append(":");
                if (minutes < 10) sb.append("0");
                sb.append(minutes);
                sb.append(":");
                if (seconds < 10) sb.append("0");
                sb.append(seconds);
                remainderLabel.setText(sb.toString());
            } else {
                finished.set(true);
                buttons[RESET].setText("Begin");
                buttons[PAUSE].setEnabled(false);
                remainderLabel.setText("0:00:00");
                System.out.println("Time's up!");
                player.open();
                player.repeat(3);
                player.close();
            }
        }

    }

}

public class Time implements Runnable {

    private final JFrame frame;
    private final Clock clock;
    private final Timer timer;

    // Constructor and main method omitted for brevity

    public void run() {

        long t0 = System.currentTimeMillis();
        boolean running = true;
        while (running) {
            long t1 = System.currentTimeMillis();
            clock.update(t1);
            timer.update(t1-t0);
            t0 = t1;
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
                System.out.println("Interrupted Exception: " + ie.getMessage());
                System.exit(1);
            }
        }
        System.exit(0);

    }

}

请注意,我已使用 class 等 AtomicBoolean 来确保 thread-safety 字段 playabledone 以便更改一个线程对其他线程是可见的。

Thread-safety 问题也可能出现,因为由一个线程创建的 clipstream 的实例可能无法被其他线程观察到。为了解决这个问题,我将这些字段设置为 final 以保证它们只有一个引用。 Java 的 Optional class 也用于将 clipstream 设置为比 null.

更安全的值

希望对您有所帮助。有问题可以评论。