/*
 * Copyright (C) 2013 Darth Affe <http://wyrez.org> and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * 
 * 
 * This Code is based on the source provided with a tutorial by
 * http://www.badlogicgames.com (as of 16.05.2013).
 * The original source can be found here:
 * http://code.google.com/p/audio-analysis/source/browse/trunk/src/com/badlogic/audio/io/WaveDecoder.java
 */
package org.wyrez.audio.decoder;

import java.io.BufferedInputStream;
import java.io.InputStream;
import org.wyrez.audio.util.EndianDataInputStream;

/**
 * A simple converter that can read in the PCM data from a Wav file, converting
 * the data to signed 32-bit floats in the range [-1,1]. This only supports
 * 16-bit Wav files with a maximum of 2 channels.
 *
 * @author Darth Affe
 */
public class WaveDecoder implements Decoder {

    private final float MAX_VALUE = 1.0f / Short.MAX_VALUE;//inverse max short value as float
    private final EndianDataInputStream in;
    private final int channels;
    private final float samplingRate;

    /**
     * Constructor, sets the input stream to read the Wav file from. Audio files
     * with more than 2 channels aren't supported!
     *
     * @param stream The input stream.
     * @throws Exception in case the input stream couldn't be read properly
     */
    public WaveDecoder(InputStream stream) throws Exception {
        if (stream == null) {
            throw new IllegalArgumentException("Input stream must not be null");
        }

        in = new EndianDataInputStream(new BufferedInputStream(stream, 1024 * 1024));
        if (!in.read4ByteString().equals("RIFF")) {
            throw new IllegalArgumentException("not a wav");
        }

        in.readIntLittleEndian();

        if (!in.read4ByteString().equals("WAVE")) {
            throw new IllegalArgumentException("expected WAVE tag");
        }

        if (!in.read4ByteString().equals("fmt ")) {
            throw new IllegalArgumentException("expected fmt tag");
        }

        if (in.readIntLittleEndian() != 16) {
            throw new IllegalArgumentException("expected wave chunk size to be 16");
        }

        if (in.readShortLittleEndian() != 1) {
            throw new IllegalArgumentException("expected format to be 1");
        }

        channels = in.readShortLittleEndian();
        if (channels > 2) {
            throw new IllegalArgumentException("more than 2 channels aren't supported");
        }
        samplingRate = in.readIntLittleEndian();

        in.readIntLittleEndian();
        in.readShortLittleEndian();
        int fmt = in.readShortLittleEndian();

        if (fmt != 16) {
            throw new IllegalArgumentException("Only 16-bit signed format supported");
        }

        if (!in.read4ByteString().equals("data")) {
            throw new RuntimeException("expected data tag");
        }

        in.readIntLittleEndian();
    }

    /**
     * Tries to read in samples.length stereo samples, converting non float
     * formats to float 32-bit. Returns the number of samples actually read.
     * Guarantees that samples.length samples are read in if there was enough
     * data in the stream.
     *
     * @param samples The samples array to write the samples to
     * @return The number of samples actually read
     */
    @Override
    public int readSamplesStereo(float[] samples) {
        switch (channels) {
            case 1:
                return readMonoAsStereo(samples);
            case 2:
                return readRealStereo(samples);
            default:
                return 0;
        }
    }

    private int readRealStereo(float[] samples) {
        int readSamples = 0;
        for (int i = 0; i < samples.length - 1; i += 2) {
            try {
                samples[i] = in.readShortLittleEndian() * MAX_VALUE;
                samples[i + 1] = in.readShortLittleEndian() * MAX_VALUE;
                readSamples += 2;
            } catch (Exception ex) {
                break;
            }
        }
        return readSamples;
    }

    private int readMonoAsStereo(float[] samples) {
        int readSamples = 0;
        for (int i = 0; i < samples.length - 1; i += 2) {
            try {
                float sample = in.readShortLittleEndian() * MAX_VALUE;
                samples[i] = sample;
                samples[i + 1] = sample;
                readSamples += 2;
            } catch (Exception ex) {
                break;
            }
        }
        return readSamples;
    }

    /**
     * Tries to read in samples.length samples, merging stereo to a mono channel
     * by averaging and converting non float formats to float 32-bit. Returns
     * the number of samples actually read. Guarantees that samples.length
     * samples are read in if there was enough data in the stream.
     *
     * @param samples The samples array to write the samples to
     * @return The number of samples actually read
     */
    @Override
    public int readSamplesMergedMono(float[] samples) {
        int readSamples = 0;
        for (int i = 0; i < samples.length; i++) {
            float sample = 0;
            try {
                for (int j = 0; j < channels; j++) {
                    int shortValue = in.readShortLittleEndian();
                    sample += (shortValue * MAX_VALUE);
                }
                sample /= channels;
                samples[i] = sample;
                readSamples++;
            } catch (Exception ex) {
                break;
            }
        }
        return readSamples;
    }

    /**
     * Returns the number of channels in the audio source. (This value can't be
     * greater than 2)
     * 
     * @returns The channel count
     */
    @Override
    public int getChannelCount() {
        return channels;
    }

    /**
     * Returns the sampling rate in Hz of the audio source.
     * 
     * @returns the sampling rate as floatF
     */
    @Override
    public float getSamplingRate() {
        return samplingRate;
    }
}
