
// EchoSamplesPlayer.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th


/* Celý zvokový soubor se načte jako pole bajtů samples[] přes
   objekt typu AudioInputStream. Poté se aplikuje efekt ozvěny vytvořením
   nového pole bajtů. Předává se po malých kouskách objektu typu
   SourceDataLine jdoucí do směšovače.

   Efekt ozvěny zabírá 4 kopie původního zvuku doplněné za jeho konec.
   Hlasitost každé z nich se postupně snižuje o 0,5 oproti svému předchůdci.

   Pro jednodušší programování se efekt aplikujeme pouze na 8-bitové
   znaménkové/bezznaménkové PCM-audio.

   Kód je ve stylu jazyka C, tedy jako série statických metod
   a statických globálních proměnných. Se třídami/objekty by
   vše fungovalo úplně stejně.   
*/


import java.io.*;
import javax.sound.sampled.*;


public class EchoSamplesPlayer  
{
  private static final int ECHO_NUMBER = 4;   // kolek ozvěn se má přidat
  private static final double DECAY = 0.5;    // slábnutí každé ozvěny

  private static AudioInputStream stream;
  private static AudioFormat format = null;
  private static SourceDataLine line = null;


  public static void main(String[] args) 
  {
    if (args.length != 1) {
      System.out.println("Použití: java EchoSamplesPlayer <clip file>");
      System.exit(0);
    }

    createInput("Sounds/" + args[0]);

    if (!isRequiredFormat()) {    // nebylo v programu SamplesPlayer
      System.out.println("Format unsuitable for echoing");
      System.exit(0);
    }

    createOutput();

    int numBytes = (int)(stream.getFrameLength() * format.getFrameSize());
    System.out.println("Size v bajtech: " + numBytes);

    byte[] samples = getSamples(numBytes);
    play(samples);

    System.exit(0);   // v J2SE 1.4.2 nebo dříve nutné
  } // konec metody: main()


  private static void createInput(String fnm)
  // Připraví vstupní zvukový proud z audio souboru
  {
    try {
      // připoj zvukový proud k vzorkovanému audio-souboru
      stream = AudioSystem.getAudioInputStream( new File(fnm) );
      format = stream.getFormat();
      System.out.println("Audio format: " + format);

      // převeď data ve formátu ULAW/ALAW do formátu PCM
      if ( (format.getEncoding() == AudioFormat.Encoding.ULAW) ||
           (format.getEncoding() == AudioFormat.Encoding.ALAW) ) {
        AudioFormat newFormat = 
           new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                                format.getSampleRate(),
                                format.getSampleSizeInBits()*2,
                                format.getChannels(),
                                format.getFrameSize()*2,
                                format.getFrameRate(), true);  // způsob uložení dat: big endian
        // aktualizuj podrobné údaje ohledně datového proudu a formátu
        stream = AudioSystem.getAudioInputStream(newFormat, stream);
        System.out.println("Převedený audio formát" + newFormat);
        format = newFormat;
      }
    }
    catch (UnsupportedAudioFileException e) 
    {  System.out.println( e.getMessage());  
       System.exit(0);
    }
    catch (IOException e) 
    {  System.out.println( e.getMessage());  
       System.exit(0);
    }
  }  // konec metody: createInput()


  private static boolean isRequiredFormat()
  // ozvěnu aplikujeme pouze na 8bitová PCM data s/bez znaménka
  {
    if ( ((format.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) ||
          (format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED)) &&
         (format.getSampleSizeInBits() == 8))
      return true;
    else
      return false;
  }  // konec metody: isRequiredFormat()


  private static void createOutput()
  // připraví objekt typu SourceDataLine směřující do JVM-směšovače
  {
    try {
      DataLine.Info info =
            new DataLine.Info(SourceDataLine.class, format);
      if (!AudioSystem.isLineSupported(info)) {
        System.out.println("Linka nepodporuje: + format);
        System.exit(0);
      }
      // získej linku požadovaného formátu
      line = (SourceDataLine) AudioSystem.getLine(info);
      line.open(format);
    }
    catch (Exception e)
    {  System.out.println( e.getMessage());  
       System.exit(0);
    }
  }  // konec metody: createOutput()



  private static byte[] getSamples(int numBytes) 
  /* Načte všechny vzorky z objektu typu AudioInputStream jako
     jedno pole bajtů. Vrátí jej upravené, s přidaným efektem
     ozvěny. */
  {
     // načti celý datový proud do pole samples[]
     byte[] samples = new byte[numBytes];
     DataInputStream dis = new DataInputStream(stream);
     try {
       dis.readFully(samples);
     }
     catch (IOException e) 
     { System.out.println( e.getMessage());  
       System.exit(0);
     }

     /* Vytvoř pole bajtů aplikací audio-efektu.
        Tento řádek tvoří hlavní odlišnost oproti 
        aplikaci SamplesPlayer, kde se načtené vzorky vrací
        beze změn. */
     return echoSamples(samples, numBytes);   
  } // konec metody: getSamples() 


  private static byte[] echoSamples(byte[] samples, int numBytes)
  /* Efelt ozvěny tvoří ECHO_NUMBER (4) kopií původního zvuku
     přidané na jeho konec.

     Hlasitost každé z nich je oproti původnímu snížena o DECAY (0,5).

     Změnu pole bajtů provádní metoda echoSample()
  */
  {  int numTimes = ECHO_NUMBER + 1;
     double currDecay = 1.0;
     short sample, newSample;
     byte[] newSamples = new byte[numBytes*numTimes];  

     for (int j=0; j < numTimes; j++) {
       for (int i=0; i < numBytes; i++)
         newSamples[i + (numBytes*j)] = echoSample(samples[i], currDecay);
       currDecay *= DECAY;
     }
     return newSamples;
  }  // konec metody: echoSamples()


  private static byte echoSample(byte sampleByte, double currDecay)
  /* Poněvadž je tento efekt omezen pouze na 8-bitové vzorky ,
     nemusíme se starat rozlišení velký/malý endian nebo nezvyklé
     formáty bajtů. Jeden bajt reprezentuje amplitudu (hlasitost) 
     vzorku.
 
     Byte se převede na short, podělí útlumem a vrátí jako byte.

     Bezznaménkový bajt potřebuje při konverzi maskování, poněvadž
     Java uchovává typ short ve formě se znaménkem, takže ořízneme
     jakékoliv nadbytečné bity ještě před tím, než provedeme konverzi
     (převod na short, který používá 16 bitů).

  */
  {
    short sample, newSample;
    if (format.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) {
      sample = (short)(sampleByte & 0xff);  // unsigned 8 bit --> short
      newSample = (short)(sample * currDecay);
      return (byte) newSample;
    }
    else if (format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED) {
      sample = (short)sampleByte;  // signed 8 bit --> short
      newSample = (short)(sample * currDecay);
      return (byte) newSample;
    }
    else
      return sampleByte;    // beze změmy
  } // konec metody: echoSample


  private static void play(byte[] samples)
  /* Pole bajtů samples[] připojí k proudu, načte kousky dat
     a předá je objektu typu SourceDataLine. */
  {
    // pole bajtů --> datový proud
    InputStream source = new ByteArrayInputStream(samples);

    int numRead = 0;
    byte[] buf = new byte[line.getBufferSize()];

    line.start();
    // načítej a přehrávej dávky zvukových dat
    try {
      while ((numRead = source.read(buf, 0, buf.length)) >= 0) {
        int offset = 0;
        while (offset < numRead)
          offset += line.write(buf, offset, numRead-offset);
      }
    }
    catch (IOException e) 
    {  System.out.println( e.getMessage()); }

    // posečkej, dokud se nepřehrají všechna data a pak linku uzavři
    line.drain();
    line.stop();
    line.close();
  }  // konec metody: play()


} // konec třídy: EchoSamplesPlayer