Provide Audio Controls During Playback





Provide Audio Controls During Playback

Let your users take control of JavaSound playback.

To complete this set of JavaSound-related hacks, why not give the user the opportunity to control the sound as it plays? JavaSound provides a very dynamic means of getting at controls like gain and pan (more commonly thought of as volume and balance) through a discovery mechanism that you can use to support any kind of control that might exist, even a control you know nothing about.

On the other hand, JavaSound presents a control not as a GUI widget, but just as an object that can affect the behavior of a Line. This hack will help you provide the GUI side.

The Control class simply defines a getType() and toString() method. What's more interesting is its subclasses, each of which defines a different kind of control:


BooleanControl

Controls a value that can be either true or false


EnumControl

Controls a value that can be one of n known values


FloatControl

Controls a value that is expressed as a floating-point number


CompoundControl

Controls multiple properties, and itself contains multiple controls

You can get the Controls supported by your Line simply by calling Line. getControls(), which returns an array of Controls. You can also ask for a specific control by using a constant of the Control.Type subclass, such as BooleanControl.Type.MUTE or FloatControl.Type.MASTER_GAIN. Pass this constant to Line.isControlSupported() to see if the control is available for the given line, and then get the control object with Line.getControl().

If you look at the subclasses of Control, you'll see that each provides getter and setter methods appropriate to its data type. BooleanControl, for example, has a getValue() that returns a boolean and a setValue() that takes a boolean. FloatControl has similar methods that work with floats. Each also provides a number of what might be called "verbiage methods" that provide text for building a GUI. For example, FloatControl offers label names for its minimum, maximum, and middle values, and another that describes the units represented by the control (like "dB" for a gain-related control, or "frames per second" for one that is timing-related).

Anyways, you've probably figured out that you can build a GUI by using Swing to provide a view to these Controls. In fact, it's fairly straightforward to take a factory approach to handing out JComponents based on Controls, as shown in Figure

Controls and their Swing representation

Control type

Swing representation

BooleanControl

JCheckBox

EnumControl

JComboBox

FloatControl

JSlider


The CompoundControl can't be handled as easily because you can't know how far you'll have to recursively dig into its hierarchy of controls, nor how to represent them in relation to one another. For what it's worth, JavaSound does not define any CompoundControl.Type constants, and it's not clear that any CompoundControls exist in J2SE; the Javadoc only speaks in theory of supporting something like a graphic equalizer, which would need to be a CompoundControl.

Theory to Practice

To handle the different types of controls that can be encountered, one nice approach would be to build a factory: you hand it a Control, and it gives you back a JComponent that you can add to your GUI. Figure shows a simple implementation of this.

Factory to generate Swing widgets for JavaSound controls
import javax.sound.sampled.*;

public class ControlComponentFactory {

	private ControlComponentFactory() {super();}

	public static JComponent getComponentFor (Control control) {
		System.out.println (control.getType().getClass());
		if (control instanceof BooleanControl)
			return new BooleanControlComponent ((BooleanControl) control); 
		else if (control instanceof FloatControl) 
			return new FloatControlComponent ((FloatControl) control);
		return new JLabel ("unsupported"); 
	} 
}

You might notice that I haven't written a GUI class for EnumControl. That's because in my testing, the array of Controls returned by getControl() has never contained an EnumControl, so I don't have a good way to test it. After looking at the implementation of the boolean and float cases, I'll discuss how an EnumControlComponent would work.

The easier case is the BooleanControl, used for controls like Mute, which turns off the sound (but remembers the previous volume, so when unmuted, the sound is as loud as it was before). Figure shows the implementation of the BooleanControlComponent.

Swing widget for a JavaSound BooleanControl
import javax.sound.sampled.*;

public class BooleanControlComponent extends JPanel
	implements ActionListener {
	BooleanControl control;
	JCheckBox box;
	public BooleanControlComponent (BooleanControl c) {
		control = c;
		box = new JCheckBox ();
		box.setSelected (control.getValue());
		add (box);
		box.addActionListener (this);
	}
	public void actionPerformed (ActionEvent ae) {        
		control.setValue (box.isSelected()); 
	} 
}

As you can see, this is practically trivial. The GUI contains a single JCheckBox, whose value you set to the value of the control so that it is in a proper state when first shown. Then, when the user clicks it, you just call the BooleanControl's setValue() method.

Supporting a FloatControl is a lot harder. As you might expect, the way to represent a range of floating-point values is with a JSlider; the user can slide left to reduce the value and right to increase it. To ensure the GUI's usefulness, you can put JLabels on the left and right of the slider to show what the minimum and maximum values mean. For example, on a Pan control, which adjusts placement of stereo sound, the minimum value of -1.0 is Left and the maximum value of 1.0 is Right.

What makes it hard is handling a mapping of arbitrary floating-point values to a JSlider's int-based range. Complicating things is the fact that FloatControls can and do use very different ranges for their values, often spanning negative and positive values, and operating in both small and vast ranges of possible values. For example, if you decide to have your JSlider values range from 0 to 1000, it may have to accommodate ranges as disparate as:

  • -1.0 to 1.0

  • 0.0 to 48000.0

  • -80.0 to 13.9794

In fact, those are the ranges of default controls for pan, sampling rate, and master gain, respectively.

And to make things more fun, you have to be able to translate both ways: from control value to slider value (when first creating the widget so its initial onscreen representation is accurate), and from slider value to control value (when the user moves the slider).

But, in the end, it's just math. You can create a setSliderFromControl() method to calculate what percent of the control's maximum value is represented by the current value, apply that percent to the range of the JSlider's possible values, and set the JSlider to that. A setControlFromSlider() would use the exact same approach, except that it figures out the percentage-of-maximum of the JSlider, and applies that to the control's range. The resulting FloatControlComponent class is shown in Figure.

Swing widget for a JavaSound FloatControl
import javax.sound.sampled.*;

public class FloatControlComponent extends JPanel 
	implements ChangeListener {

	FloatControl control;
	JSlider slider;
	float min, max, range;
	final static int SLIDER_MIN = 0;
	final static int SLIDER_MAX = 1000;
	final static float SLIDER_RANGE = SLIDER_MAX - SLIDER_MIN;

public FloatControlComponent (FloatControl c) {
	control = c;
	min = c.getMinimum();
	max = c.getMaximum();
	range = max - min;
	add (new JLabel (control.getMinLabel()));
	slider = new JSlider (SLIDER_MIN, SLIDER_MAX);
	slider.addChangeListener (this);
	setSliderFromControl();
	add (slider);
	add (new JLabel (control.getMaxLabel()));
}
	private void setSliderFromControl() {
		// figure out value as percent of range
		float offsetValue = control.getValue() - min;
		float percent = 0.0f;
		if (range != 0.0)
		    percent = offsetValue / range;
        // apply that to SLIDER_RANGE
		int sliderValue = (int) (percent * SLIDER_RANGE);
		slider.setValue (sliderValue);
	}
    private void setControlFromSlider() {
		// figure out slider percentage
		float sliderPercentage =
			(float) slider.getValue() / SLIDER_RANGE;
		// figure out value for that percentage of range
		float rangeOffset = sliderPercentage * range;
		float newValue = rangeOffset + min;
		control.setValue (newValue);
	}
	// ChangeListener implementation 
	public void stateChanged (ChangeEvent e) {        
		setControlFromSlider(); 
	} 
}

Having provided these two implementations, it should be clear how you would create an EnumControlComponent. The EnumControl.getValues() method returns a String array that you would use as the model of an uneditable JComboBox. You'd use getValue() to set one of these as the initial value, and then on user events, you could pull out the JComboBox's selection and set the EnumControl with setValue().

Check It Out!

The DataLineControlGUI shown in Figure is a simple JPanel that contains a JLabel with the name of the file to be played on the first line of a GridBagLayout, and then control-name JLabels and factory-generated control widgets on each successive line. The sound is played by the PCMFilePlayer, which was introduced as part of playing uncompressed AIFFs and WAVs of arbitrary lengths [Hack #76].

Creating an audio player with GUI controls
import javax.sound.sampled.*;

public class DataLineControlGUI extends JPanel {

	PCMFilePlayer player;
	JButton startButton;

	public DataLineControlGUI (File f) {
		super();
		try {
			player = new PCMFilePlayer (f);
		} catch (Exception ioe) {
			add (new JLabel ("Error: " +
				 ioe.getMessage()));
			return; 
		}        
		DataLine line = player.getLine(); 
		// layout 
		// line 0: name        
		setLayout (new GridBagLayout());        
		GridBagConstraints gbc = new GridBagConstraints(); 
		gbc.gridy = 0; gbc.fill = GridBagConstraints.HORIZONTAL; 
		gbc.gridwidth = 2; gbc.anchor = GridBagConstraints.SOUTH; 
		add (new JLabel ("File: " +
                         player.getFile().getName()), gbc); 
    	 // subsequent lines: controls 
		 gbc.gridwidth = 1;        
		 Control[] controls = line.getControls(); 
		 for (int i=0; i<controls.length; i++) {
			gbc.gridx = 0;
			gbc.gridy++;
			gbc.anchor = GridBagConstraints.EAST;
			add (new JLabel(controls[i].getType().toString()), gbc);
			JComponent controlComp =
			   ControlComponentFactory.getComponentFor (controls[i]);
			gbc.gridx = 1; 
			gbc.anchor = GridBagConstraints.WEST; 
			add (controlComp, gbc);
		}
		// now start playing
		player.start();
	}
	public static void main (String[] args) {
		JFileChooser chooser = new JFileChooser();
		chooser.showOpenDialog(null);
		File file = chooser.getSelectedFile();
		DataLineControlGUI demo =
			new DataLineControlGUI (file);

		JFrame f = new JFrame ("JavaSound control");
		f.getContentPane().add (demo);
		f.pack();
		f.setVisible(true);
	} 
}

When run with a suitable audio file, the resulting GUI looks like Figure. Note that it's possible you might have other control widgets (or an unsupported label for EnumControls and CompoundControls) if JavaSound gives you different controls than it did when I wrote and ran this on Java 1.4.2.

JavaSound audio player with GUI controls



     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows