/*
 * 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);
      }
    });
  }
}