Create Custom Shuffleboard Plugin: Make It Play Sounds

Having created a new empty custom plugin in the previous post, we are now ready to change it to make it play sounds when the boolean value it monitors goes from false to true. The plugin is available in this GitHub repo.

Adding some visual feedback

Before we add the sound support, we’re going to add some visual feedback so we can test the widget and assure it’s working. We’ll crib from the Boolean Box built-in widget to display grey squares (but since the main goal is sound, not visual, we won’t add the color customization options). Once that’s in, we’ll make the changes we need to wire up sound.

Note that we’ll be using the standard FRC install of vscode for these edits; I recommend it for Java because it will take care of some of the edits that can be automated (such as adding import statements to pull in the right classes) for you.

  • Edit SoundWidget.java
    • In initialize, we’ll add a data binding to bind the color to our current data value:
      root.backgroundProperty().bind(
        Bindings.createObjectBinding(
          () -> createSolidColorBackground(getColor()),
      dataProperty()));
      
    • Create the two functions that get the color and instantiate a Background object to bind to the background property of the root pane:
      private Background createSolidColorBackground(Color color) {
        return new Background(new BackgroundFill(color, null, null));
      }
      
      private Color getColor() {
        final Boolean data = getData();
        if (data == null) {
          return Color.BLACK;
        }
      
        if (data.booleanValue()) {
          return Color.LAWNGREEN;
        } else {
          return Color.DARKRED;
        }
      }
      
  • Build and test.

Wiring up the sound

We’ll start with a simple test sound, pre-loaded.

First things first: Shuffleboard doesn’t actually include the Media library of JavaFX as a build dependency. So we’ll need to pull that in so the jar can take advantage of it.

  • Modify the build.gradle file. Find the area labeled dependencies { and under it, add this row near the rows similar to it:

javaFxDeps wpilibTools.deps.javafx("media")

Now, we’ll put a WAV file in the home directory, /home/mtomczak/alert.wav. Later, we’ll parameterize this as a filename. Note: This is of course my home directory; change to the path to the file on your hard drive. Note that it will need to be the full absolute path from the top of the filesystem (/ on Linux, C:\ on Windows). Any wav format file should work; feel free to record your own using Audacity or some other sound tool.

To wire it all up, let’s edit SoundWidget.java again:

  • Add a private MediaPlayer player property
  • In initialize, add a listener on the Boolean value changing and wire up the media player to our sound file. Note that we’re using the absolute path to the file here; be sure to change it to what it is on your computer.
    dataProperty().addListener((newValue) -> checkSoundPlay());
    Media sound = new Media("file:///home/mtomczak/alert.wav");
    player = new MediaPlayer(sound);
    
  • add the checkSoundPlay function to see if sound should fire:
    private void checkSoundPlay() {
      final Boolean data = getData();
    
    if (data.booleanValue()) {
        // The stop here forces the player to reset, so that it starts the sound from the beginning
        // once it has already played through.
        player.stop();
        player.play();
      }
    }
    

And that’s all we need! Now whenever the Boolean value goes to true, the sound will play.

Adding sound as a configurable setting

Shuffleboard widgets have the ability to add configurable settings, which are saved between runs of the dashboard and let you customize your widget. We can use this to customize the audio file source to whatever we want. Currently, Shuffleboard itself does not support “file path” properties, but we can use strings instead.

So back in SoundWidget.java:

  • Add a couple new properties. One is a Property bean that should auto-wire as a property of our component. The other is a Text displayable node that we’ll need to report errors to the user (because sound files can be missing):
    /** Holder for an error panel; this gets attached to root if we need to display error text */
    private Text errorMessage;
    
    /** Path to the sound file to play */
    private final Property<String> soundPath = new SimpleStringProperty(this, "soundPath");
    
  • Let’s throw down a couple of functions to use the property and report errors if they occur. Notice how we’ve replaced the absolute path to the Media file with retrieving the path from our new soundPath property.
    private void loadSoundFile() {
      player = null;
      try {
        /* Checking file existence saves some time because the Media constructor is slow to resolve
         * the issue if the file is unloadable
         */
        File f = new File(soundPath.getValue());
        if (!f.exists()) {
          setErrorMessage("File " + soundPath.getValue() + " does not exist");
          return;
        }
        if (f.isDirectory()) {
          setErrorMessage(soundPath.getValue() + " is a directory");
          return;
        }
        Media sound = new Media("file://" + soundPath.getValue());
        player = new MediaPlayer(sound);
        setErrorMessage(null);
      } catch (MediaException e) {
        System.err.print(e);
        setErrorMessage(e.getMessage());
      }
    }
    
    private void setErrorMessage(String msg) {
      if (msg == null) {
        if (errorMessage != null) {
          root.getChildren().remove(errorMessage);
          errorMessage = null;
          return;
        }
      } else {
        if (errorMessage == null) {
          errorMessage = new Text();
          errorMessage.setStyle("color: red");
          root.getChildren().add(errorMessage);
        }
        errorMessage.setText(msg);
      }
    }
    
  • Change initialize to load the sound, replacing the previous sound-loading logic with just a call to loadSoundFile()
  • We also want to listen on changes to the soundPath property to know when to reload the sound. So in initialize, also do soundPath.addListener((newValue) -> loadSoundFile());

Dealing with properties

When a component has properties, they can be set and read. So we need to add some accessors to reach our properties. Shuffleboard will know what to do with these functions automatically, if they exist.

public String getSoundPath() {
  return soundPath.getValue();
}

public void setSoundPath(String path) {
  soundPath.setValue(path);
}

public Property<String> soundPathProperty() {
  return this.soundPath;
}

Finally, there’s a standard entrypoint to get the list of properties and some descriptions. We’ll override getSettings() to report what settings we can change to Shuffleboard.

@Override
public List<Group> getSettings() {
  return ImmutableList.of(
      Group.of("Sound",
          Setting.of("Source file", "Path to audio file to use", soundPath, String.class)));
}

Note that passing String.class is super important; if you omit it, it won’t cause any issues until you try and close Shuffleboard, and then you won’t be able to save your config.

With those changes complete, we can save and build (remember from the previous post to clean up any sound-* JARs you already have in ~/Shuffleboard/plugins/). Add the widget (by dragging a boolean NetworkTable entry from the list, right-clicking it, and selecting “Show As… -> Triggered Sound.” It should say File does not exist. Right-click again and select “Edit Properties,” then in the dialog that appears click to the right of “Source file” and type in the full path to a WAV file on your computer (such as /home/mtomczak/alert.wav). When the boolean goes from false to true, the sound should play!

The Shuffleboard, the properties panel, and the Glass dashboard running

Notes about use

A few parting thoughts on using this plugin:

  • The sound should play even when the tab containing the audio widgets isn’t visible.
  • Testing on Linux, I was only able to get .wav files working. It might be the case that on Windows, the JavaFX library will recognize MP3 or other formats, because under the hood I believe the library is relying on OS-supplied codecs to load audio. I haven’t been able to test this yet.
  • I recommend using audio sparingly. While it’s a great alternate channel to bring information to your driver and operator, it can quickly become overwhelming / distracting if over-used. I’d recommend considering very carefully the question “What happens if all the audio alerts fire at once” (both in terms of whether the listener can understand them and in terms of what they would want to do in response).

Comments