/*
 * [Play.java]
 *
 * Summary: Plays a sound using the javax.sound.sampled classes.
 *
 * Copyright: (c) 2008-2014 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.7+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.1 2008-09-01 initial version
 */
package com.mindprod.common17;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.net.URL;

import static java.lang.System.err;
import static java.lang.System.out;

/**
 * Plays a sound using the javax.sound.sampled classes.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 3.8 2009-03-31 change order of fields in defaults.csv.
 *          It plays the sound in the background. If you play another sound, before the
 *          first has finished, it won't play the sounds on top of each other. It will block,
 *          and not return until the first sound has finished at it has started the second.
 * @since 2008
 */
public class Play implements Runnable
    {
    /**
     * true if want debugging output
     */
    private static final boolean DEBUGGING = false;

    private static final String WHY_SOUND_FAILS = " or possible missing or malfunctioning sound card, " +
                                                  "or dirty contacts on your speaker cable.";

    /**
     * information about the format of the sound data.
     */
    private final AudioFormat af;

    /**
     * stream current background thread is playing.
     */
    private final AudioInputStream ais;

    /**
     * information about the output sound rendering device
     */
    private final DataLine.Info info;

    /**
     * size of buffer used to copy sound bytes across
     */
    private final int buffSize;

    /**
     * constructor to create a thread run object to play a sound in the background.
     *
     * @param ais      audion input stream to play
     * @param af       audio format of the stream
     * @param info     info about the line that renders the sound
     * @param buffSize size of buffer when copying bytes to rendering device.
     */
    private Play( AudioInputStream ais, AudioFormat af, DataLine.Info info, int buffSize )
        {
        this.ais = ais;
        this.af = af;
        this.info = info;
        this.buffSize = buffSize;
        }

    /**
     * play a sound file supported by javax.sound.sampled e.g. Usually au PCM signed 8-bit mono 48,000 Hz 384 kbps
     *
     * @param url usually a resource to play created with Class.getResource.
     *
     * @throws UnsupportedAudioFileException if you select a sound file type not supported on this platform.
     * @throws IOException                   if problem retrieving the URL
     */
    public static void play( final URL url ) throws UnsupportedAudioFileException, IOException
        {
        play( AudioSystem.getAudioInputStream( url ) );
        }

    /**
     * play a sound file supported by javax.sound.sampled e.g. a signed PCM 8-bit au resource.
     *
     * @param file a sound file to play
     *
     * @throws UnsupportedAudioFileException if you select a sound file type not supported on this platform.
     * @throws IOException                   if problem retrieving the file.
     */
    public static void play( final File file ) throws UnsupportedAudioFileException, IOException
        {
        play( AudioSystem.getAudioInputStream( file ) );
        }

    /**
     * play a sound file supported by javax.sound.sampled e.g. Usually au PCM signed 8-bit mono 48,000 Hz 384 kbps
     * <p/>
     * If has trouble logs on err and throws no exceptions.
     *
     * @param resourceOwner Class that owns the resources
     * @param resourceName  name of resource with embedded /s.
     */
    public static void play( final Class resourceOwner, final String resourceName )
        {
        try
            {
            final URL url = resourceOwner.getResource( resourceName );
            if ( url == null )
                {
                err.println( "Can't find sound effect resource " + resourceName );
                return;
                }
            play( url );
            }
        catch ( UnsupportedAudioFileException e )
            {
            err.println( "Unsupported Audio file format: possible missing or malfunctioning sound card" );
            }
        catch ( IOException e )
            {
            err.println( "Problems fetching sound effect data: possible missing or malfunctioning sound card." );
            }
        catch ( Exception e )
            {
            err.println(
                    "Problems fetching sound effect date to play a sound: "
                    + "possible missing or malfunctioning sound card."
            );
            e.printStackTrace( err );
            }
        }

    /**
     * play a sound file supported by javax.sound.sampled e.g. a signed PCM 8-bit au resource 48,000 Hz 384 kbps
     *
     * @param ais AudioInputStream to play
     */
    private static synchronized void play( final AudioInputStream ais )
        {
        // check out stream ahead of time so Exception will be fielded.
        final AudioFormat af = ais.getFormat();
        if ( DEBUGGING )
            {
            out.println( "audio format: " + af.toString() );
            }
        final DataLine.Info info = new DataLine.Info( SourceDataLine.class, af );
        if ( !AudioSystem.isLineSupported( info ) )
            {
            throw new IllegalArgumentException( "Cannot play an unsupported audio format" );
            }
        final int frameRate = ( int ) af.getFrameRate();
        final int frameSize = af.getFrameSize();
        final int buffSize = frameRate * frameSize / 10;
        if ( DEBUGGING )
            {
            out.println( "Frame Rate: " + frameRate );
            out.println( "Frame Size: " + frameSize );
            out.println( "Buffer Size: " + buffSize );
            }
        // We can't start another thread until previous one has terminated
        // because this method and run are both synchronised on the class object.
        // Start up a background thread to play the sound.
        new Thread( new Play( ais, af, info, buffSize ) ).start();
        }

    /**
     * background thread to feed bytes from stream to sound renderer
     */
    public void run()
        {
        synchronized ( getClass() )
            {
            try
                {
                final SourceDataLine line = ( SourceDataLine ) AudioSystem.getLine( info );
                line.open( af, buffSize );
                // start streaming in from the resource
                line.start();
                final byte[] data = new byte[ buffSize ];
                int bytesRead;
                while ( ( bytesRead = ais.read( data, 0, data.length ) ) != -1 )
                    {
                    line.write( data, 0, bytesRead );
                    }
                /* sound  data stream is finished, wait for sound to finish */
                line.drain();
                line.stop();
                line.close();
                }
            catch ( LineUnavailableException e )
                {
                // throwing an exception in a background thread probably won't be caught properly.
                err.println( "Line unavailable to play a sound" + WHY_SOUND_FAILS );
                }
            catch ( IOException e )
                {
                err.println( "Problems fetching data to play a sound" + WHY_SOUND_FAILS );
                }
            catch ( Exception e )
                {
                err.println( "Problems fetching data to play a sound" + WHY_SOUND_FAILS );
                e.printStackTrace( err );
                }
            }
        }
    }
