/*
 * @(#)EightSVXDecoder.java  1.0  1999-10-19
 *
 * Copyright (c) 1999 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.eightsvx;

import org.monte.media.AbortException;
import org.monte.media.ParseException;
import org.monte.media.iff.*;
import java.util.Vector;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.applet.AudioClip;

/**
 * Creates a collection of EightSVXAudioClip objects by
 * reading an IFF 8SVX file.
 *
 * <p><b>8SVX Type Definitions</b>
 * <pre>
 * #define ID_8SVX MakeID('8', 'S', 'V', 'X')
 * #define ID_VHDR MakeID('V', 'H', 'D', 'R')
 *
 * typedef LONG Fixed;     // A Fixed-point value, 16 bits to the left of
 * // the point and 16 to the right. A Fixed is a number
 * // of 2^16ths, i.e., 65536ths.
 * #define Unity 0x10000L  // Unity = Fixed 1.0 = maximum volume
 *
 * // sCompression: Choice of compression algorithm applied to the samples.
 * #define sCmpNone   0  // not compressed
 * #define sCmpFibDelta 1  // Fibonacci-delta encoding.
 * // Can be more kinds in the future.
 *
 * typedef struct {
 * ULONG oneShotHiSamples,   // # samples in the high octave 1-shot part
 * repeatHiSamples,   // # samples in the high octave repeat part
 * samplesPerHiCycle; // # samples/cycle in high octave, else 0
 * UWORD samplesPerSec;     // data sampling rate
 * UBYTE ctOctave,          // # octaves of waveform
 * sCompression;      // data compression technique used
 * Fixed volume;            // playback volume form 0 to Unity (full
 * // volume). Map this value into the output
 * // hardware's dynamic range.
 * } Voice8Header;
 *
 * #define ID_NAME MakeID('N', 'A', 'M', 'E')
 * // NAME chunk contains a CHAR[], the voice's name.
 *
 * #define ID_Copyright MakeID('(', 'c', ')', ' ')
 * // "(c) " chunk contains a CHAR[], the FORM's copyright notice.
 *
 * #define ID_AUTH MakeID('A', 'U', 'T', 'H')
 * // AUTH chunk contains a CHAR[], the author's name.
 *
 * #define ID_ANNO MakeID('A', 'N', 'N', 'O')
 * // ANNO chunk contains a CHAR[], author's text annotations.
 *
 * #define ID_ATAK MakeID('A', 'T', 'A', 'K')
 * #define ID_RLSE MakeID('R', 'L', 'S', 'E')
 *
 * typedef struct {
 * UWORD duration; // segment duration in milliseconds, > 0
 * Fixed dest;     // destination volume factor
 * } EGPoint;
 *
 * // ATAK and RLSE chunks contain an EGPoint[], piecewise-linear envelope.
 * // The envelope defines a function of time returning Fixed values. It's
 * // used to scale the nominal volume specified in the Voice8Header.
 *
 * #define RIGHT    4L
 * #define LEFT     2L
 * #define STEREO   6L
 *
 * #define ID_CHAN MakeID('C', 'H', 'A', 'N')
 * typedef sampletype LONG;
 *
 * #define ID_PAN MakeID('P', 'A', 'N', ' ')
 * typedef sposition Fixed; // 0 <= sposition <= Unity
 * // Unity refers to the maximum possible volume.
 *
 *
 * #define ID_BODY MakeID('B', 'O', 'D', 'Y')
 * typedef character BYTE; // 8 bit signed number, -128 thru 127.
 * // BODY chunk contains a BYTE[], array of audio data samples
 * </pre>
 *
 * <p><b>8SVX Regular Expression</b>
 * <pre>
 * 8SVX       ::= "FORM" #{ "8SVX" VHDR [NAME] [Copyright] [AUTH] ANNO* [ATAK] [RLSE] [CHAN] [PAN] BODY }
 *
 * VHDR       ::= "VHDR" #{ Voice8Header }
 * NAME       ::= "NAME" #{ CHAR* } [0]
 * Copyright  ::= "(c) " #{ CHAR* } [0]
 * AUTH       ::= "AUTH" #{ CHAR* } [0]
 * ANNO       ::= "ANNO" #{ CHAR* } [0]
 *
 * ATAK       ::= "ATAK" #{ EGPoint* }
 * RLSE       ::= "RLSE" #{ EGPoint* }
 * CHAN       ::= "CHAN" #{ sampletype }
 * PAN        ::= "PAN " #{ sposition }
 * BODY       ::= "BODY" #{ BYTE* } [0]
 * </pre>
 * The token "#" represents a ckSize LONG count of the following {braced} data bytes.
 * E.g., a VHDR's "#" should equal sizeof(Voicd8Header). Literal items are shown in
 * "quotes", [square bracket items] are optional, and "*" means 0 ore more replications.
 * A sometimes-needed pad byte is shown als "[0]".
 *
 * @author  Werner Randelshofer, Hausmatt 10, CH-6405 Immensee, Switzerland
 * @version  1.0  1999-10-19
 */
public class EightSVXDecoder
implements IFFVisitor {
    /* Constants */
    public final static int EIGHT_SVX_ID = IFFParser.stringToID("8SVX");
    public final static int VHDR_ID = IFFParser.stringToID("VHDR");
    public final static int NAME_ID = IFFParser.stringToID("NAME");
    public final static int COPYRIGHT_ID = IFFParser.stringToID("(c) ");
    public final static int ANNO_ID = IFFParser.stringToID("ANNO");
    public final static int AUTH_ID = IFFParser.stringToID("AUTH");
    //public final static int ATAK_ID = IFFParser.stringToID("ATAK");
    //public final static int RLSE_ID = IFFParser.stringToID("RLSE");
    public final static int CHAN_ID = IFFParser.stringToID("CHAN");
    //public final static int PAN_ID = IFFParser.stringToID("PAN ");
    public final static int BODY_ID = IFFParser.stringToID("BODY");
    
    /* Instance variables */
    private Vector samples_ = new Vector();
    private boolean within8SVXGroup_ = false;
    
    /* Constructors  */
    /**
     * Creates a new Audio Source from the specified InputStream.
     *
     * Pre condition
     * InputStream must contain IFF 8SVX data.
     * Post condition
     * -
     * Obligation
     * -
     *
     * @param  in The input stream.
     */
    public EightSVXDecoder(InputStream in)
    throws IOException {
        try {
            IFFParser iff = new IFFParser();
            registerChunks(iff);
            iff.parse(in,this);
        }
        catch (ParseException e) {
            throw new IOException(e.toString());
        }
        catch (AbortException e) {
            throw new IOException(e.toString());
        }
        finally {
            in.close();
        }
    }
    
    public EightSVXDecoder() {
    }
    
    /* Accessors */
    public Vector getSamples() {
        return samples_;
    }
    
    /* Actions */
    public void registerChunks(IFFParser iff) {
        iff.declareGroupChunk(EIGHT_SVX_ID,IFFParser.ID_FORM);
        iff.declarePropertyChunk(EIGHT_SVX_ID,VHDR_ID);
        iff.declarePropertyChunk(EIGHT_SVX_ID,NAME_ID);
        iff.declarePropertyChunk(EIGHT_SVX_ID,COPYRIGHT_ID);
        iff.declareCollectionChunk(EIGHT_SVX_ID,ANNO_ID);
        iff.declarePropertyChunk(EIGHT_SVX_ID,AUTH_ID);
        iff.declarePropertyChunk(EIGHT_SVX_ID,CHAN_ID);
        iff.declareDataChunk(EIGHT_SVX_ID,BODY_ID);
    }
    
    /**
     * Visits the start of an IFF GroupChunkExpression.
     *
     * Altough this method is declared as public it may only
     * be called from an IFFParser that has been invoked
     * by this class.
     *
     * Pre condition
     * Vector <clips> must not be null.
     * This method espects only FORM groups of type 8SVX.
     * Post condition
     * -
     * Obligation
     * -
     *
     * @param  group Group Chunk to be visited.
     * @exception ParseException
     * When an error has been encountered.
     */
    public void enterGroup(IFFChunk group) {
        if (group.getType() == EIGHT_SVX_ID) { within8SVXGroup_ = true;}
    }
    public void leaveGroup(IFFChunk group) {
        if (group.getType() == EIGHT_SVX_ID) { within8SVXGroup_ = false;}
    }
    public void visitChunk(IFFChunk group, IFFChunk chunk)
    throws ParseException {
        if (within8SVXGroup_) {
            if (chunk.getID() == BODY_ID ) // && group.getID() == EIGHT_SVX_ID)
            {
                if (group.getPropertyChunk(VHDR_ID) == null) {
                    throw new ParseException("Sorry: Without 8SVX.VHDR-Chunk no sound possible");
                }
                EightSVXAudioClip newSample = new EightSVXAudioClip();
                decodeVHDR(newSample,group.getPropertyChunk(VHDR_ID));
                decodeCHAN(newSample,group.getPropertyChunk(CHAN_ID));
                decodeNAME(newSample,group.getPropertyChunk(NAME_ID));
                decodeCOPYRIGHT(newSample,group.getPropertyChunk(COPYRIGHT_ID));
                decodeAUTH(newSample,group.getPropertyChunk(COPYRIGHT_ID));
                decodeANNO(newSample,group.getCollectionChunks(ANNO_ID));
                decodeBODY(newSample,chunk);
                addAudioClip(newSample);
            }
        }
    }
    
    public void addAudioClip(AudioClip clip) {
        samples_.addElement(clip);
    }
    
    /**
     * The Voice 8 Header (VHDR) property chunk holds the playback parameters for the
     * sampled waveform.
     * <pre>
     * typedef LONG Fixed;     // A Fixed-point value, 16 bits to the left of
     * // the point and 16 to the right. A Fixed is a number
     * // of 2^16ths, i.e., 65536ths.
     * #define Unity 0x10000L  // Unity = Fixed 1.0 = maximum volume
     *
     * // sCompression: Choice of compression algorithm applied to the samples.
     * #define sCmpNone   0  // not compressed
     * #define sCmpFibDelta 1  // Fibonacci-delta encoding.
     * // Can be more kinds in the future.
     *
     * typedef struct {
     * ULONG oneShotHiSamples,   // # samples in the high octave 1-shot part
     * repeatHiSamples,   // # samples in the high octave repeat part
     * samplesPerHiCycle; // # samples/cycle in high octave, else 0
     * UWORD samplesPerSec;     // data sampling rate
     * UBYTE ctOctave,          // # octaves of waveform
     * sCompression;      // data compression technique used
     * Fixed volume;            // playback volume form 0 to Unity (full
     * // volume). Map this value into the output
     * // hardware's dynamic range.
     * } Voice8Header;
     * </pre>
     */
    protected void decodeVHDR(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        try {
            if (chunk != null) {
                MC68000InputStream in = new MC68000InputStream(new ByteArrayInputStream(chunk.getData()));
                sample.setOneShotHiSamples(in.readULONG());
                sample.setRepeatHiSamples(in.readULONG());
                sample.setSamplesPerHiCycle(in.readULONG());
                sample.setSampleRate(in.readUWORD());
                sample.setCtOctave(in.readUBYTE());
                sample.setSCompression(in.readUBYTE());
                sample.setVolume(in.readLONG());
            }
        }
        catch (IOException e) {
            throw new ParseException("Error parsing 8SVX VHDR:" +e.getMessage());
        }
    }
    
    protected void decodeCHAN(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        if (chunk != null) {
            sample.setSampleType(chunk.getData()[3]);
        }
    }
    
    protected void decodeNAME(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        if (chunk != null) {
            sample.setName(new String(chunk.getData()));
        }
    }
    
    protected void decodeCOPYRIGHT(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        if (chunk != null) {
            sample.setCopyright(new String(chunk.getData()));
        }
    }
    
    protected void decodeAUTH(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        if (chunk != null) {
            sample.setAuthor(new String(chunk.getData()));
        }
    }
    
    protected void decodeANNO(EightSVXAudioClip sample,IFFChunk[] chunks)
    throws ParseException {
        if (chunks != null) {
            for (int i=0; i < chunks.length; i++) {
                IFFChunk chunk = chunks[i];
                sample.setRemark(sample.getRemark() + new String(chunk.getData()));
            }
        }
    }
    
    protected void decodeBODY(EightSVXAudioClip sample,IFFChunk chunk)
    throws ParseException {
        if (chunk != null) {
            byte[] data = chunk.getData();
            sample.set8SVXBody(data);
        }
    }
    
}
