Search code examples
javamultithreadingswingmidiswingworker

Adding a repeat function to a simple MIDI player


I am trying to implement a repeat function on a custom MIDI player, but I am unable to implement a repeat function. Here are the classes I am using:

NotePlayer - Plays MIDI notes using Java's MIDI package.

GuitarTunerGUI

  • Interface to the NotePlayer class.
  • Provides six JButtons for each guitar string, a JComboBox for selecting the desired tuning, and a JCheckBox for toggling the repeat function.
  • Provides toggleRepeat() for toggling the repeatEnabled field, a private field of the GuitarTunerGUI class.

I created a SwingWorker that is responsible for playing a MIDI note in a separate thread. This solves the issue of keeping the GUI responsive while the note is being played. However, a problem arises when repeat is enabled and the user pushes more than one button.

When the user pushes one of the six JButtons the listener does the following:

public void actionPerformed(ActionEvent event) {
    // The note param is a private field of the listener object
    MusicianWorker clapton = new MusicianWorker(note);
    clapton.execute();
}

The execute method does the following:

protected Void doInBackground() throws Exception {
    do {
        NotePlayer.playNote(thisNote);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            System.out.println(ex.getMessage());
        }
    } while (repeatEnabled);

    return null;
}

An issue arises when the user pushes multiple buttons without toggling repeat. For example, when the 'A' button and the 'E' button are pushed sequentially, two threads are created and the 'A' and 'E' notes are both played repeatedly until repeatEnabled is toggled off. When the user pushes a JButton I need to first determine if any worker threads are currently executing and, if so kill those threads before playing the specified note. Thanks in advance for your time and feedback.


Solution

  • The code you have given is great, it just needs to be tweaked a little bit. When you create your SwingWorker, you should keep track of it in an instance variable (maybe in a List if you are going to be wanting to play multiple notes at some point?). Then, before playing a new note you check to see if the last note has finished, and if not, you cancel it.

    Whether or not cancellation will have any effect on your MusicianWorker is up to you. The worker thread will be interrupted, which would mean that your Thread.sleep method would prematurely terminate if it is running - you would have to check your docs to see what effect it would have on NotePlayer.

    Lastly, it seems that you don't actually need to be using the SwingWorker at all, since your background task is not interacting with the UI. You might want to investigate Executors.

    You could try something like this:

    public class AlbertHall {
      private final ExecutorService es = Executors.newSingleThreadExecutor();
      // No longer a local variable in the listener
      private Future<Void> clapton; // ... or maybe a Collection of Futures
    
      private class Listener implements ActionListener {
        private final Note note;
    
        public Listener(Note note) {
          this.note = note;
        }
    
        public void actionPerformed(ActionEvent event) {
          // Watch out, Clapton may finish after you have asked if he is done
          // but before you call cancel
          if (clapton != null && !clapton.isDone()) clapton.cancel(true);
    
          // You may need to have a wait loop here if Clapton takes a while 
          // to leave the stage
    
          // Next note
          clapton = es.submit(new MusicianWorker(note));
        }
      }
    
      static class MusicianWorker implements Runnable {
        private final Note note;
    
        public MusicianWorker(Note note) {
          this.note = note;
        }
    
        public void run() {
          boolean cancelRequested = false;
          do {
            NotePlayer.playNote(thisNote);
            try {
              Thread.sleep(3000);
            } catch (InterruptedException ex) {
              // Looks like we got cancelled
              cancelRequested = true;
            }
          } while (repeatEnabled && !cancelRequested);
        }
      }
    }