/*
 * 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/MP3Decoder.java
 */
package org.wyrez.audio.decoder;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
import org.tritonus.share.sampled.FloatSampleBuffer;

/**
 * A decoder for MP3 files with a maximum of 2 channels.
 *
 * @author Darth Affe
 */
public class MP3Decoder implements Decoder {

    private AudioInputStream in;
    private FloatSampleBuffer buffer;
    private byte[] bytes;
    private int channels;
    private float samplingRate;

    /**
     * Constructor, sets the input stream to read the MP3 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 MP3Decoder(InputStream stream) throws Exception {
        InputStream in = new BufferedInputStream(stream, 1024 * 1024);
        this.in = new MpegAudioFileReader().getAudioInputStream(in);
        AudioFormat baseFormat = this.in.getFormat();
        this.channels = baseFormat.getChannels();
        if (channels > 2) {
            throw new IllegalArgumentException("more than 2 channels aren't supported");
        }
        this.samplingRate = baseFormat.getSampleRate();
        AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                baseFormat.getSampleRate(), 16,
                baseFormat.getChannels(),
                baseFormat.getChannels() * 2,
                baseFormat.getSampleRate(), false);
        this.in = AudioSystem.getAudioInputStream(format, this.in);
    }

    /**
     * Tries to read in samples.length stereo samples. 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) {
        if (buffer == null || buffer.getSampleCount() < samples.length / 2) {
            buffer = new FloatSampleBuffer(in.getFormat().getChannels(), samples.length / 2, in.getFormat().getSampleRate());
            bytes = new byte[buffer.getByteArrayBufferSize(in.getFormat())];
        }

        int read = 0;
        int readBytes = 0;
        try {
            readBytes = in.read(bytes, read, bytes.length - read);
        } catch (IOException e) {
            return 0;
        }
        if (readBytes == -1) {
            return 0;
        }

        read += readBytes;
        while (readBytes != -1 && read != bytes.length) {
            try {
                readBytes = in.read(bytes, read, bytes.length - read);
            } catch (IOException e) {
                return 0;
            }
            read += readBytes;
        }

        int frameCount = bytes.length / in.getFormat().getFrameSize();
        buffer.setSamplesFromBytes(bytes, 0, in.getFormat(), 0, frameCount);

        for (int i = 0, j = 0; i < buffer.getSampleCount(); i++, j += 2) {
            samples[j] = buffer.getChannel(0)[i];
            samples[j + 1] = buffer.getChannel(buffer.getChannelCount() == 2 ? 1 : 0)[i];
        }
        return buffer.getSampleCount() * 2;
    }

    /**
     * Tries to read in samples.length samples, merging stereo to a mono channel
     * by averaging. 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) {
        if (buffer == null || buffer.getSampleCount() < samples.length) {
            buffer = new FloatSampleBuffer(in.getFormat().getChannels(), samples.length, in.getFormat().getSampleRate());
            bytes = new byte[buffer.getByteArrayBufferSize(in.getFormat())];
        }

        int read = 0;
        int readBytes = 0;
        try {
            readBytes = in.read(bytes, read, bytes.length - read);
        } catch (IOException e) {
            return 0;
        }
        if (readBytes == -1) {
            return 0;
        }

        read += readBytes;
        while (readBytes != -1 && read != bytes.length) {
            try {
                readBytes = in.read(bytes, read, bytes.length - read);
            } catch (IOException e) {
                return 0;
            }
            read += readBytes;
        }

        int frameCount = bytes.length / in.getFormat().getFrameSize();
        buffer.setSamplesFromBytes(bytes, 0, in.getFormat(), 0, frameCount);

        for (int i = 0; i < buffer.getSampleCount(); i++) {
            if (buffer.getChannelCount() == 2) {
                samples[i] = (buffer.getChannel(0)[i] + buffer.getChannel(1)[i]) / 2;
            } else {
                samples[i] = buffer.getChannel(0)[i];
            }
        }
        return buffer.getSampleCount();
    }

    /**
     * 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 float
     */
    @Override
    public float getSamplingRate() {
        return samplingRate;
    }
}
