package lejos.nxt;

import java.io.*;


/**
 * NXT sound routines.
 *
 */
public class Sound
{

    private static final int RIFF_HDR_SIZE = 44;
    private static final int RIFF_RIFF_SIG = 0x52494646;
    private static final int RIFF_WAVE_SIG = 0x57415645;
    private static final int RIFF_FMT_SIG = 0x666d7420;
    private static final short RIFF_FMT_PCM = 0x0100;
    private static final short RIFF_FMT_1CHAN = 0x0100;
    private static final short RIFF_FMT_8BITS = 0x0800;
    private static final int RIFF_DATA_SIG = 0x64617461;
    
    public static final int VOL_MAX = 100;
    public static final String VOL_SETTING = "lejos.volume";
    // Instruments (yes I know they don't sound anything like the names!)
    public final static int[] PIANO = new int[]{4, 25, 500, 7000, 5};
    public final static int[] FLUTE = new int[]{10, 25, 2000, 1000, 25};
    public final static int[] XYLOPHONE = new int[]{1, 8, 3000, 5000, 5};
    
    private static int masterVolume = 0;
    
    /**
     * Static contstructor to force loading of system settings
     */
    static {
        loadSettings();
    }

    private Sound()
    {
    }
    
    /**
     * Play a system sound.
     * <TABLE BORDER=1>
     * <TR><TH>aCode</TH><TH>Resulting Sound</TH></TR>
     * <TR><TD>0</TD><TD>short beep</TD></TR>
     * <TR><TD>1</TD><TD>double beep</TD></TR>
     * <TR><TD>2</TD><TD>descending arpeggio</TD></TR>
     * <TR><TD>3</TD><TD>ascending  arpeggio</TD></TR>
     * <TR><TD>4</TD><TD>long, low buzz</TD></TR>
     * </TABLE>
     */
    public static int C2 = 523;

    public static void systemSound(boolean aQueued, int aCode)
    {
        if (aCode == 0)
            playTone(600, 200);
        else if (aCode == 1)
        {
            playTone(600, 150);
            pause(200);
            playTone(600, 150);
            pause(150);
        }
        else if (aCode == 2)// C major arpeggio
            for (int i = 4; i < 8; i++)
            {
                playTone(C2 * i / 4, 100);
                pause(100);
            }
        else if (aCode == 3)
            for (int i = 7; i > 3; i--)
            {
                playTone(C2 * i / 4, 100);
                pause(100);
            }
        else if (aCode == 4)
        {
            playTone(100, 500);
            pause(500);
        }
    }

    /**
     * Beeps once.
     */
    public static void beep()
    {
        systemSound(true, 0);
    }

    /**
     * Beeps twice.
     */
    public static void twoBeeps()
    {
        systemSound(true, 1);
    }

    /**
     * Downward tones.
     */
    public static void beepSequence()
    {
        systemSound(true, 3);
    }

    /**
     * Upward tones.
     */
    public static void beepSequenceUp()
    {
        systemSound(true, 2);
    }

    /**
     * Low buzz 
     */
    public static void buzz()
    {
        systemSound(true, 4);
    }

    public static void pause(int t)
    {
        try
        {
            Thread.sleep(t);
        }
        catch (InterruptedException e)
        {
        }
    }

    /**
     * Returns the number of milliseconds remaining of the current tone or sample.
     * @return milliseconds remaining
     */
    public static native int getTime();
    
    /**
     * Plays a tone, given its frequency and duration. 
     * @param aFrequency The frequency of the tone in Hertz (Hz).
     * @param aDuration The duration of the tone, in milliseconds.
     * @param aVolume The volume of the playback 100 corresponds to 100%
     */
    static native void playFreq(int aFrequency, int aDuration, int aVolume);


    /**
     * Plays a tone, given its frequency and duration. 
     * @param aFrequency The frequency of the tone in Hertz (Hz).
     * @param aDuration The duration of the tone, in milliseconds.
     * @param aVolume The volume of the playback 100 corresponds to 100%
     */
    public static void playTone(int aFrequency, int aDuration, int aVolume)
    {
        if (aVolume >= 0)
            aVolume = (aVolume*masterVolume)/100;
        else
            aVolume = -aVolume;
        playFreq(aFrequency, aDuration, aVolume);
    }
    

    public static void playTone(int freq, int duration)
    {
        playTone(freq, duration, masterVolume);
    }

    /**
     * Internal method used to play sound sample from a file
     * @param page the start page of the file
     * @param offset the start of the samples in the file
     * @param len the length of the sample to play
     * @param freq the sampling frequency 
     * @param vol the volume 100 corresponds to 100%
     */
    static native void playSample(int page, int offset, int len, int freq, int vol);

    /**
     * Play a wav file
     * @param file the 8-bit PWM (WAV) sample file
     * @param vol the volume percentage 0 - 100
     * @return The number of milliseconds the sample will play for or < 0 if
     *         there is an error.
     */
    public static int playSample(File file, int vol)
    {
        // First check that we have a wave file. File must be at least 44 bytes
        // in size to contain a RIFF header.
        if (file.length() < RIFF_HDR_SIZE)
            return -1;
        // Now check for a RIFF header
        FileInputStream f = new FileInputStream(file);
        DataInputStream d = new DataInputStream(f);
        int sampleRate = 0;
        int dataLen = 0;
        try
        {
            if (d.readInt() != RIFF_RIFF_SIG)
                return -1;
            // Skip chunk size
            d.readInt();
            // Check we have a wave file
            if (d.readInt() != RIFF_WAVE_SIG)
                return -1;
            if (d.readInt() != RIFF_FMT_SIG)
                return -1;
            // Now check that the format is PCM, Mono 8 bits. Note that these
            // values are stored little endian.
            d.readInt(); // Skip chunk size
            if (d.readShort() != RIFF_FMT_PCM)
                return -1;
            if (d.readShort() != RIFF_FMT_1CHAN)
                return -1;
            sampleRate = d.readByte() & 0xff;
            sampleRate |= (d.readByte() & 0xff) << 8;
            sampleRate |= (d.readByte() & 0xff) << 16;
            sampleRate |= (d.readByte() & 0xff) << 24;
            d.readInt();
            d.readShort();
            if (d.readShort() != RIFF_FMT_8BITS)
                return -1;
            // Make sure we now have a data chunk
            if (d.readInt() != RIFF_DATA_SIG)
                return -1;
            dataLen = d.readByte() & 0xff;
            dataLen |= (d.readByte() & 0xff) << 8;
            dataLen |= (d.readByte() & 0xff) << 16;
            dataLen |= (d.readByte() & 0xff) << 24;
            d.close();
        }
        catch (IOException e)
        {
            return -1;
        }
        if (vol >= 0)
            vol = (vol*masterVolume)/100;
        else
            vol = -vol;
        playSample(file.getPage(), RIFF_HDR_SIZE, dataLen, sampleRate, vol);
        return getTime();
    }

    /**
     * Play a wav file
     * @param file the 8-bit PWM (WAV) sample file
     * @return The number of milliseconds the sample will play for or < 0 if
     *         there is an error.
     */
    public static int playSample(File file)
    {
        return playSample(file, masterVolume);
    }

    static int waitUntil(int t)
    {
        int t2;
        while ((t2 = (int) System.currentTimeMillis()) < t)
            Thread.yield();
        return t2;
    }

    /**
     * Play a note with attack, decay, sustain and release shape. This function
     * allows the playing of a more musical sounding note. It uses a set of
     * supplied "instrument" parameters to define the shape of the notes 
     * envelope.
     * @param inst Instrument definition
     * @param freq The note to play (in Hz)
     * @param len  The duration of the note (in ms)
     */
    public static void playNote(int[] inst, int freq, int len)
    {
        // Generate an enevlope controlled note. The instrument array contains
        // the shape of the envelope. The attack period, the decay period the
        // level to decay to and the level to decay to during the sustain part
        // of the note finally the length of the actual release period. All
        // periods are given in 2000th of a second units. This is because the
        // generation of a tone using the playTone function will often take
        // more than 1ms to execute. This means that the minimum tone segment
        // that we can reliably generate is 2ms and so we use this unit as the
        // basis of note generation.
        int segLen = inst[0];
        // All volume settings are scaled by 100.
        int step = 8000 / segLen;
        int vol = 2000;
        // We do not really have fine grained enough timing so try and keep
        // things aligned as closely as possible to a tick by waiting here
        // before we start for the next tick.
        int t = waitUntil((int) System.currentTimeMillis() + 1);
        // Generate the attack profile from 20 to full volume
        len /= 2;
        for (int i = 0; i < segLen; i++)
        {
            Sound.playTone(freq, 10, vol / 100);
            vol += step;
            t = waitUntil(t + 2);
        }
        len -= segLen;
        // Now do the decay
        segLen = inst[1];
        if (segLen > 0)
        {
            step = inst[2] / segLen;
            for (int i = 0; i < segLen; i++)
            {
                Sound.playTone(freq, 100, vol / 100);
                vol -= step;
                t = waitUntil(t + 2);
            }
            len -= segLen;
        }
        segLen = inst[4];
        len -= segLen;
        // adjust length of the sustain and possibly the release to match the
        // requested note length
        if (len > 0)
        {
            step = inst[3] / len;
            for (int i = 0; i < len; i++)
            {
                Sound.playTone(freq, 100, vol / 100);
                vol -= step;
                t = waitUntil(t + 2);
            }
        }
        else
            segLen += len;
        // Finally do the release
        if (segLen > 0)
        {
            step = (vol - 1000) / segLen;
            for (int i = 0; i < segLen; i++)
            {
                Sound.playTone(freq, 2, vol / 100);
                vol -= step;
                t = waitUntil(t + 2);
            }
        }
    }

    /**
     * Set the master volume level
     * @param vol 0-100
     */
    public static void setVolume(int vol)
    {
        if (vol > VOL_MAX) vol = VOL_MAX;
        if (vol < 0) vol = 0;
        masterVolume = vol;
    }

    /**
     * Get the current master volume level
     * @return the current master volume 0-100
     */
    public static int getVolume()
    {
        return masterVolume;
    }
    
    /**
     * Load the current system settings associated with this class. Called
     * automatically to initialize the class. May be called if it is required
     * to reload any settings.
     */
    public static void loadSettings()
    {
        masterVolume = SystemSettings.getIntSetting(VOL_SETTING, 80);
    }
}
