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


/* Třída MidisLoader uchovává kolekci objektů typu MidiInfo v 
   hašovací tabulce typu HashMap, jejíž klíže tvoří jména těchto objektů.

   Název a jméno souboru midi-sekvence se načítá z konfiguračního souboru 
   při vytváření instance třídy MidisLoader. Předpokládáme, že se tento
   soubor nachází v adresáři Sounds/.

   Tato třída umožňuje přehrát, zastavit, opětovně spustit a cyklit specifikovanou
   midi-sekvenci. K sekvenceru (ne sekvenci) lze připojit implementaci 
   rozhraní SoundsWatcher.

   Třída MidisLoader se od třídy ClipsLoader liší v tom, že obsahuje objekt typu 
   Sequencer pro přehrávání sekvencí. Z toho plyne, že najednou může hrát pouze jediná
   sekvence, zatímco klipů může hrát současně větší počet.

   Odkaz na sekvencer se předává každému objektu typu MidiInfo, 
   který je odpovědný za přehrání, zastavení, opětovné spuštění a cyklické
   přehrávání příslušných sekvencí.
*/



import java.io.*;
import javax.sound.midi.*;
import java.util.*;



public class MidisLoader implements MetaEventListener 
{
  // konstanta meta události midi pro oznámení konce stopy 
  private static final int END_OF_TRACK = 47;

  private final static String SOUND_DIR = "Sounds/";

  private Sequencer sequencer;

  private HashMap midisMap;
  private MidiInfo currentMidi = null;   
        // odkaz na právě hrající objekt typu MidiInfo

  private SoundsWatcher watcher = null;


  public MidisLoader() 
  {midisMap = new HashMap();
    initSequencer();
  }

  public MidisLoader(String soundsFnm) 
  { midisMap = new HashMap();
    initSequencer();
    loadSoundsFile(soundsFnm);
  }


  private void initSequencer() 
  /* Připraví MIDI-sekvencer a naslouchač meta-událostí sekvenceru. 
     Syntezátor nepoužíváme. */
  {
    try {
      sequencer = MidiSystem.getSequencer();
      if (sequencer == null) {
        System.out.println("Nemohu získat sekvencer");
        return;
      }

      sequencer.open();
      sequencer.addMetaEventListener(this);

      // sekvencer nemusí být totožný se syntezátorem,
      // takže propoj sekvencer --> syntezáror (to je nutné v J2SE 1.5)
      if (!(sequencer instanceof Synthesizer)) {
        System.out.println("Linking the MIDI sequencer and synthesizer");
        Synthesizer synthesizer = MidiSystem.getSynthesizer();
        Receiver synthReceiver = synthesizer.getReceiver();
        Transmitter seqTransmitter = sequencer.getTransmitter();
        seqTransmitter.setReceiver(synthReceiver);
      }
    }
    catch (MidiUnavailableException e){
      System.out.println("Není k dispozici žádný sekvencer");
      sequencer = null;
    }
  } // konec metody: initSequencer()



  private void loadSoundsFile(String soundsFnm)
  /* Formát vstupních řádku je následjící:
  /* The format of the input lines are:
        <name> <fnm>                     // jediný zvukový soubor
        nebo prázdné řádky 
        nebo řádky s komentáři.
  */
  { 
    String sndsFNm = SOUND_DIR + soundsFnm;
    System.out.println("Čtený soubor: " + sndsFNm);
    try {
      InputStream in = this.getClass().getResourceAsStream(sndsFNm);
      BufferedReader br = new BufferedReader( new InputStreamReader(in));
      // BufferedReader br = new BufferedReader( new FileReader(sndsFNm));
      StringTokenizer tokens;
      String line, name, fnm;
      while((line = br.readLine()) != null) {
        if (line.length() == 0)  // prázdný řádek
          continue;
        if (line.startsWith("//"))   // komentář
          continue;

        tokens = new StringTokenizer(line);
        if (tokens.countTokens() != 2)
          System.out.println("Nesprávný počet argumentů pro " + line);
        else {
          name = tokens.nextToken();
          fnm = tokens.nextToken();
          load(name, fnm);
        }
      }
      br.close();
    } 
    catch (IOException e) 
    { System.out.println("Chyba při čtení souboru: " + sndsFNm);
      System.exit(1);
    }
  }  // konec metody: loadSoundsFile()


  public void close()
  /* Uzavře sekvencer (a jakoukoli přehrávanou sekvenci). 
     Od metody close() ve třídě ClipsLoader se liší v tom, že 
     se pracuje především se sekvencerem než-li s konkrétní midi-sekvenci.
  */
  { stop();    // zastav přehrávání sekvence
    if (sequencer != null) {
      if (sequencer.isRunning())
        sequencer.stop();
       
      sequencer.removeMetaEventListener(this);
      sequencer.close();
      sequencer = null;
    }
  }  // konec metody: close()


  public void setWatcher(SoundsWatcher sw)
  {  watcher = sw;  }



  // ----------- manipulace s konkrétní midi-sekvencí --------


  public void load(String name, String fnm)
  // vytvoří objekt typu MidiInfo a uloží jej pod názvem "name"
  {
    if (midisMap.containsKey(name))
      System.out.println( "Chyba: " + name + "already stored");
    else if (sequencer == null)
      System.out.println( "Pro dané jméno neexistuje sekvence: " + name);
    else {
      midisMap.put(name, new MidiInfo(name, fnm, sequencer) );
      System.out.println("-- " + name + "/" + fnm);
    }
  }  // konec metody: load()



  public void play(String name, boolean toLoop)
  // přehraj (možná i cyklicky) danou sekvenci
  { 
     MidiInfo mi = (MidiInfo) midisMap.get(name);
     if (mi == null)
       System.out.println( "Chyba: " + name + "not stored");
     else {
       if (currentMidi != null)
         System.out.println("Je nám líto, ale " + currentMidi.getName() +
                              " již hraje");
       else {
        currentMidi = mi;   // ulož odkaz na právě hrající sekvenci midi
        mi.play(toLoop);
       }
     }
  } // konec metody: play()



  public void stop()
  { if (currentMidi != null)
      currentMidi.stop();    // aktivuj meta-událost 'end-of-track'
    else                     // takže dojde k volání metody meta()
      System.out.println("Nehraje žádná muzika");
  } // konec metody: stop()



  public void pause()
  { if (currentMidi != null)
      currentMidi.pause();
    else
      System.out.println( "Není co pozastavit");
  } // konec metody: pause()


  public void resume()
  { if (currentMidi != null)
      currentMidi.resume();
    else
      System.out.println("Není co opětovně spustit");
  } // konec metody: resume()


  // ---------------------------------------------------


  public void meta(MetaMessage meta)
  /* volá se při vyvolání meta události během přehrávání sekvence. 
     Kód pouze řeší událost end-of-track, kterou může spustit objekt typu
     MidisInfo dohraje-li sekvence na konec nebo je-li zastavena. Nicméně
     na svém konci může cyklit, což prověříme voláním metody tryLooping()
     objektu typu MidiInfo.

     Je-li připojen objekt pro sledování událostí, informujeme jej o 
     aktuálním stavu.
  */
  {
    if (meta.getType() == END_OF_TRACK) {
      String name = currentMidi.getName();
      // System.out.println("  Událost END_OF_TRACK pro " + name);
      boolean hasLooped = currentMidi.tryLooping();  // muzika stále cyklí?
      if (!hasLooped)   // dosud neskončila
        currentMidi = null;

      if (watcher != null) {   // oznam dozorci
        if (hasLooped)      // muzika hraje znovu od začátku
          watcher.atSequenceEnd(name, SoundsWatcher.REPLAYED);
        else   // muzika dohrála
          watcher.atSequenceEnd(name, SoundsWatcher.STOPPED);
      }
    }
  } // konec metody: meta()


} // konec třídy: MidisLoader
