/* * Licensed Materials - Property of Perforce Software, Inc. * © Copyright Perforce Software, Inc. 2014, 2021 * © Copyright IBM Corp. 2009, 2014 * © Copyright ILOG 1996, 2009 * All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: * The Software and Documentation were developed at private expense and * are "Commercial Items" as that term is defined at 48 CFR 2.101, * consisting of "Commercial Computer Software" and * "Commercial Computer Software Documentation", as such terms are * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4, * as applicable. */ package sound; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; import javax.sound.sampled.SourceDataLine; import javax.swing.ImageIcon; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ilog.views.chart.IlvAxis; import ilog.views.chart.IlvChart; import ilog.views.chart.IlvChartRenderer; import ilog.views.chart.IlvColor; import ilog.views.chart.IlvStyle; import ilog.views.chart.action.IlvChartAction; import ilog.views.chart.data.IlvCyclicDataSet; import ilog.views.chart.data.IlvDefaultDataSet; import ilog.views.chart.renderer.IlvSinglePolylineRenderer; import ilog.views.util.IlvImageUtil; import shared.AbstractChartExample; /** * A simple PCM sound player. This example shows how to display real-time data * using the JViews Chart library. It uses the Java Sound technology to play PCM * audio files while displaying the audio data in two <code>IlvChart</code>s. * <P> * Since this example relies on the Java Sound API to read audio files, the * supported audio formats depends on the Java Sound API implementation used. * <BR> * By default, the Java Sound Sun implementation supports the following PCM * audio file formats: AIFF, AU, WAV (only PCM, only 16 bit). * <P> * If you are interested in reading MP3 files, you can either: * <UL> * <LI>Use the Tritonus Java Sound implementation (be sure to check the * supported platforms first) that natively supports MP3 audio files (see * <a href="http://tritonus.org">The Tritonus Project</a> for more information). * <LI>Plug your own MP3 decoder using the new Service Provider Interface. This * technique is detailed in the following <a href= * "http://www.javaworld.com/javaworld/jw-11-2000/jw-1103-mp3.html">JavaWorld * article</a>. A free MP3 decoder that can be plugged-in this way can be found * on the Tritonus Project home page (section 'Plug-in'). Note that this plug-in * is distributed under the GPL license hence cannot be provided with this * example. * </UL> */ public class SoundPlayer extends AbstractChartExample implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(SoundPlayer.class); // The default buffer size. private static final int DS_BUFFER_SIZE = 2048; // The available update rates. static final int[] UPDATE_RATE = { 1, 5, 10, 20 }; static final String[] UPDATE_RATE_STR = { "1s", "1/5s", "1/10s", "1/20s" }; // The player state. boolean state = false; // The charts update rate. int updateRate; // The file to be played. File audioFile = null; // The thread in which sound decoding is performed. Thread playerThread; // The left channel chart. IlvChart leftChart; // The right channel chart. IlvChart rightChart; // The left channel data set. IlvDefaultDataSet leftChannel; // The right channel data set. IlvDefaultDataSet rightChannel; // The audio input stream where the data is read. AudioInputStream audioInputStream; // The audio format of the audio file. AudioFormat audioFormat; // The DataLine on which data is written. SourceDataLine line; // The gain line control. FloatControl gain; // The gain controller. JSlider gainCtrl; // The header. JLabel title; // The play start action and button. IlvChartAction playAction; // The stop action and button. IlvChartAction stopAction; // The update rate controller. JComboBox<String> updateRateCtrl; // The file chooser. JFileChooser fileChooser; /** * Initializes a new <code>TableModelDemo</code> object. **/ Override public void init(Container container) { super.init(container); // ======================= UI Initialization ========================== container.setLayout(new BorderLayout()); // Left channel chart initialization. leftChart = createChart(); leftChart.setHeaderText("Left Channel"); leftChart.getHeader().setFont(leftChart.getFont()); leftChart.getHeader().setForeground(leftChart.getForeground()); // Right channel chart initialization. rightChart = createChart(); rightChart.setFooterText("Right Channel"); rightChart.getFooter().setFont(rightChart.getFont()); rightChart.getFooter().setForeground(rightChart.getForeground()); // Synchronize both charts. This methods allows you to share the same // IlvAxis between two IlvCharts (in our case the x-axis) and to // synchronize their plot area as well. rightChart.synchronizeAxis(leftChart, IlvAxis.X_AXIS, true); // Data set initialization. In order to have the best performance // coupled with a low memory usage, data sets are instances of the // IlvCyclicDataSet class. This class stores data in a fixed-size // buffer, the oldest values being removed when new values are added, // limiting the memory usage to the buffer size and avoiding // costly memory allocation. LINEAR_MODE avoids expensive data range // recomputations. leftChannel = new IlvCyclicDataSet("Left", DS_BUFFER_SIZE, IlvCyclicDataSet.LINEAR_MODE, false); rightChannel = new IlvCyclicDataSet("Right", DS_BUFFER_SIZE, IlvCyclicDataSet.LINEAR_MODE, false); // Renderer initialization. IlvChartRenderer r = new IlvSinglePolylineRenderer(); leftChart.addRenderer(r, leftChannel); r.setStyles(new IlvStyle[] { IlvStyle.createStroked(Color.green) }); r = new IlvSinglePolylineRenderer(); rightChart.addRenderer(r, rightChannel); r.setStyles(new IlvStyle[] { IlvStyle.createStroked(Color.green) }); // Add a toolbar and a header. JPanel panel = new JPanel(new GridLayout(0, 1)); panel.add(createToolBar()); ImageIcon icon = null; try { icon = new ImageIcon(IlvImageUtil.loadImageFromFile(getClass(), "volume24.gif")); } catch (IOException e) { System.out.println("Cannot load volume24.gif"); } title = new JLabel(icon); title.setHorizontalAlignment(JLabel.LEFT); title.setOpaque(true); title.setBackground(Color.gray); title.setForeground(Color.white); title.setFont(new Font("Dialog", Font.BOLD, 16)); panel.add(title); container.add(panel, BorderLayout.NORTH); panel = new JPanel(new GridLayout(2, 1)); panel.add(leftChart); panel.add(rightChart); container.add(panel, BorderLayout.CENTER); } /** * Returns an <code>IlvChart</code> instance properly configured. */ protected IlvChart createChart() { IlvChart chart = new IlvChart(); chart.setForeground(Color.black); chart.setFont(new Font("Dialog", Font.PLAIN, 10)); // Explicitly set the chart area margins. Calling setMargins // disables the auto-computation mode of the margins. chart.getChartArea().setMargins(new Insets(5, 5, 5, 5)); // Enable the automatic scrolling mode. chart.setShiftScroll(true); // Set the plot area style. chart.getChartArea().setPlotStyle(new IlvStyle(Color.black)); // Disable the autoDataRange mode. chart.getXAxis().setAutoDataRange(false); // No x-scale is needed. chart.setXScale(null); // Hide the y-scale. Note that setting it to null will prevent the // y-grid from being displayed. Indeed, the default behavior of the IlvGrid // class is to compute the grid values using the corresponding IlvScale // instance. Hence, if no scales are set, no grid is drawn by default. // For an example of a custom grid, see the monitor and the stock // examples. chart.getYScale(0).setVisible(false); // Set the color of the major grid lines. chart.getYGrid(0).setMajorPaint(IlvColor.darker(Color.gray)); return chart; } /** * Populates the toolbar. */ Override protected void populateToolBar(JToolBar toolbar) { // Creates the "Open" action. fileChooser = new JFileChooser(); URL soundsDirectory = getResourceURL("data/sounds"); if ("file".equals(soundsDirectory.getProtocol())) { try { // do not use URL.getPath() because of escaped URL fileChooser.setCurrentDirectory(new File(soundsDirectory.toURI())); } catch (URISyntaxException e) { LOGGER.trace("Ignoring : Cannot setCurrentDirectory to fileChooser because of bad URI Syntax"); } } IlvChartAction action = new IlvChartAction("Open...", null, null, "Open a file", null) { Override public void actionPerformed(ActionEvent evt) { if (fileChooser.showOpenDialog(SoundPlayer.this) == JFileChooser.APPROVE_OPTION) { audioFile = fileChooser.getSelectedFile(); title.setText("Ready to play " + audioFile.getName()); playAction.setEnabled(true); } } }; action.setIcon(getClass(), "open24.gif"); action.setEnabled(true); addAction(toolbar, action); // Creates the "Play" action. playAction = new IlvChartAction("Play", null, null, "Play", null) { Override public void actionPerformed(ActionEvent evt) { if (audioFile != null) play(); } }; toolbar.addSeparator(); playAction.setIcon(getClass(), "play24.gif"); playAction.setEnabled(false); addAction(toolbar, playAction); // Creates the "Stop" action. stopAction = new IlvChartAction("Stop", null, null, "Stop", null) { Override public void actionPerformed(ActionEvent evt) { if (playerThread != null) { setPlaying(false); title.setText("Ready to play " + audioFile.getName()); stopAction.setEnabled(false); } } }; stopAction.setIcon(getClass(), "stop24.gif"); stopAction.setEnabled(false); addAction(toolbar, stopAction); // Creates the "Update Rate" controller. updateRateCtrl = new JComboBox<String>(UPDATE_RATE_STR); updateRateCtrl.setToolTipText("Update Rate"); updateRateCtrl.setSelectedIndex(2); updateRate = UPDATE_RATE[updateRateCtrl.getSelectedIndex()]; updateRateCtrl.setMaximumSize(updateRateCtrl.getPreferredSize()); updateRateCtrl.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent evt) { updateRate = UPDATE_RATE[updateRateCtrl.getSelectedIndex()]; } }); toolbar.addSeparator(); toolbar.add(updateRateCtrl); // Creates the "Gain" controller. gainCtrl = new JSlider(JSlider.VERTICAL); gainCtrl.setToolTipText("Gain level"); gainCtrl.setPreferredSize(gainCtrl.getMinimumSize()); gainCtrl.addChangeListener(new ChangeListener() { Override public void stateChanged(ChangeEvent evt) { if (gain != null) gain.setValue(gainCtrl.getValue()); } }); toolbar.addSeparator(); toolbar.add(gainCtrl); } /** * Sets the state of the player thread. */ protected synchronized void setPlaying(boolean state) { this.state = state; } /** * Returns the player thread state. */ protected synchronized boolean isPlaying() { return state; } /** * Plays the selected file. This method is called by the 'Play' action. */ protected void play() { if (audioFile == null) return; // Stop the player thread. if (playerThread != null && playerThread.isAlive()) { setPlaying(false); while (playerThread.isAlive()) ; } // Update the GUI. stopAction.setEnabled(true); // Reset the data sets. leftChannel.setData(null, null, 0); rightChannel.setData(null, null, 0); // Reset the axis visible range. leftChart.getXAxis().setVisibleRange(0, (DS_BUFFER_SIZE - 1) / 2); rightChart.getXAxis().setVisibleRange(0, (DS_BUFFER_SIZE - 1) / 2); // Reset the axis data range. leftChart.getYAxis(0).setDataRange(-30000., 30000.); rightChart.getYAxis(0).setDataRange(-30000., 30000.); // Start a new player thread. try { playerThread = new Thread(this); playerThread.start(); } catch (Exception e) { e.printStackTrace(); } } /** * Called when a player thread is started. This method performs the audio data * decoding and charts update. */ Override public void run() { try { // Update the header label. SwingUtilities.invokeLater(new Runnable() { Override public void run() { title.setText("Opening ".concat(audioFile.getName()).concat("...")); } }); setPlaying(true); // Java Sound code. Get an AudioInputStream from the specified file // and convert it to a 16bits/PCM input stream if needed. Note that // an UnsupportedAudioFileException is thrown if the audio format // is unsupported or cannot be converted. File fileIn = audioFile; audioInputStream = AudioSystem.getAudioInputStream(fileIn); audioFormat = audioInputStream.getFormat(); // Convert the audio format if needed. if (audioFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) { AudioFormat newFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, audioFormat.getSampleRate(), 16, audioFormat.getChannels(), audioFormat.getChannels() * 2, audioFormat.getSampleRate(), false); AudioInputStream newStream = AudioSystem.getAudioInputStream(newFormat, audioInputStream); audioFormat = newFormat; audioInputStream = newStream; } int bytesPerFrame = audioFormat.getFrameSize(); // int sampleSize = bytesPerFrame / audioFormat.getChannels(); int nBufferSize = (int) (audioFormat.getSampleRate() * bytesPerFrame) / 10; int nBytesRead = 0; byte[] abData = new byte[nBufferSize]; int nChannels = audioFormat.getChannels(); int nFramesAdded = 0; DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, nBufferSize); if (!AudioSystem.isLineSupported(info)) { System.err.println("Play.playAudioStream does not handle this type of audio on this system."); return; } line = (SourceDataLine) AudioSystem.getLine(info); // Open the data line. line.open(audioFormat, nBufferSize); // Adjust the gain level on the output line. if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) { gain = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN); gainCtrl.getModel().setRangeProperties((int) gain.getValue(), gainCtrl.getExtent(), (int) gain.getMinimum(), (int) gain.getMaximum(), false); } SwingUtilities.invokeLater(new Runnable() { Override public void run() { title.setText("Playing ".concat(audioFile.getName())); } }); line.start(); // Start to batch notification events to avoid a time-consuming // notification/update each time a data point is added. Events will // be sent according to the charts update rate value. synchronized (leftChart.getLock()) { leftChannel.startBatch(); } synchronized (rightChart.getLock()) { rightChannel.startBatch(); } // Read data from the audio input stream. while (nBytesRead != -1) { try { nBytesRead = audioInputStream.read(abData); } catch (IOException e) { e.printStackTrace(); } if (nBytesRead >= 0) { int numFramesRead = nBytesRead / bytesPerFrame; // Write audio data to the data line. line.write(abData, 0, nBytesRead); // Add the data points. for (int i = 0; i < numFramesRead; i++) { if (!isPlaying()) return; ++nFramesAdded; // Determine whether to terminate and restart the // batching of notification events, according to the // selected update rate. boolean doUpdate = ((nFramesAdded % (audioFormat.getSampleRate() / updateRate)) == 0); // Decode the value (see the PCM file format for more information). float cValue = (float) ((abData[bytesPerFrame * i + 1] << 8) | (abData[bytesPerFrame * i + 1] & 0xff)); // Need to synchronize, so as not to interfere with the // dataset accesses that the chart makes during drawing. synchronized (leftChart.getLock()) { leftChannel.addData(-1, cValue); if (doUpdate) { leftChannel.endBatch(); rightChannel.endBatch(); } } // Likewise for the second channel, if present. if (nChannels == 2) { cValue = (float) ((abData[bytesPerFrame * i + 3] << 8) | (abData[bytesPerFrame * i + 2] & 0xff)); } synchronized (rightChart.getLock()) { rightChannel.addData(-1, cValue); if (doUpdate) { leftChannel.startBatch(); rightChannel.startBatch(); } } } } } SwingUtilities.invokeLater(new Runnable() { Override public void run() { title.setText("Ready to play " + audioFile.getName()); } }); } catch (Exception e) { e.printStackTrace(); SwingUtilities.invokeLater(new Runnable() { Override public void run() { title.setText("Unsupported file format :".concat(audioFile.getName())); } }); } finally { synchronized (leftChart.getLock()) { leftChannel.endBatch(); } synchronized (rightChart.getLock()) { rightChannel.endBatch(); } if (audioInputStream != null) try { audioInputStream.close(); } catch (IOException e) { } if (line != null) { line.drain(); line.close(); line = null; } setPlaying(false); } } /** * Called when the application is about to be closed. */ Override protected void quit() { if (playerThread != null && playerThread.isAlive()) { setPlaying(false); while (playerThread.isAlive()) ; } } /** * Main entry point. */ public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { Override public void run() { JFrame frame = new JFrame("Sound Player Example"); SoundPlayer sample = new SoundPlayer(); sample.init(frame.getContentPane()); sample.setFrameGeometry(550, 350, true); frame.setVisible(true); } }); } }