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

/* Sestaví sekvenci objektů typu MidiEvent obsahujících zprávy
   NOTE_ON/NOTE_OFF pro přehrávní tónů a zprávy PROGRAM_CHANGE, 
   CONTROL_CHANGE pro změnu nástrojů. Rychlost přehrávání lze
   také specifikovat.

   Tóny lze vyjádřit jako celá čísla pomocí názvu not (např. F4#).
   Více se dozvíte na adrese http://www.phys.unsw.edu.au/~jw/notes.html.

   Tato ukázková aplikace demonstruje způsobu generování MIDI-sekvencí
   za běhu programu přímo z kódu. Dále ukazuje, jak lze sekvence
   konstruovat pomocí jednoduchého notového záznamu.

   Změny 6. sprna 2004
     - do metody createSequencer() přidáno volání synthesizer.open()
*/


import javax.sound.midi.*;


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

  // kanál využívaný sekvencí
  private static final int CHANNEL = 0;

  // název ovládacího prvku kanálu pro změnu nástrojové banky
  private static final int BANK_CONTROLLER = 0;

  private static final int VOLUME = 90;  // noty mají pevně danou hlasitost

  /* Pro posun not používáje stupnici C-Dur, která
     vypadá takto: "C D E F G A B (C)", ale pro uložení
     jsme zvolili jiné pořadí pro zjednodušení vyhledávání:
     "A B C D E F G". */
  private static final int[] cOffsets =  {9, 11, 0, 2, 4, 5, 7};
                                        // A  B  C  D  E  F  G
  private static final int C4_KEY = 60;    
        // C4 je nota "C" ve čtvrté oktávě klaviatury 
  private static final int OCTAVE = 12;    // velikost oktávy v notách


  private Sequencer sequencer;
  private Synthesizer synthesizer;

  // sestaví sekvenci s jedinou stopou zpráv typu MidiEvent
  private Sequence sequence;
  private Track track;

  private int tickPos = 0;   // pro časové značky zpráv typu MidiEvent


  public SeqSynth()
  {
    createSequencer();
    // listInstruments();
    createTrack(4);       // PPQ rozlišení = 4

    // makeScale(21);     // klíčem je "A0"
    makeSong();

    startSequencer(60);   // tempo: 60 úderů za minutu (BPM)

    // počkej, až sekvence dohraje
    try { 
      Thread.sleep(600000);   // 10 minut v milisekundách
    } 
    catch(InterruptedException e) 
    { System.out.println("Spánek byl přerušen"); }
    System.exit(0);
  } // konec metody: SeqSynth()


  private void createSequencer()
  // vytvoří sekvence a vytáhne si jeho syntezátor
  {
    try {
      sequencer = MidiSystem.getSequencer();
      sequencer.open();

      // sekvencer možná není totožný se syntezátorem
      // takže napoj sekvencer --> syntezátor (k tomu je třeba J2SE 1.5)
      if (!(sequencer instanceof Synthesizer)) {
        System.out.println("Linking the MIDI sequencer and synthesizer");
        synthesizer = MidiSystem.getSynthesizer();
	    synthesizer.open();    // nový
        Receiver synthReceiver = synthesizer.getReceiver();
        Transmitter seqTransmitter = sequencer.getTransmitter();
        seqTransmitter.setReceiver(synthReceiver);
      }
      else 
        synthesizer = (Synthesizer) sequencer;
    }
    catch (MidiUnavailableException e) {
      e.printStackTrace();
    }
  }  // konec metody: createSequencer()


  private void listInstruments()
  /* Pomocná metoda pro zobrazení, jaké nástroje jsou právě k dispozici.
     Každý nástroj je indexován číslem banky a programu. */
  {
    Instrument[] instrument = synthesizer.getAvailableInstruments();
    System.out.println("Počet nástrojů: " + instrument.length);
    for (int i=0; i < instrument.length; i++) {
      Patch p = instrument[i].getPatch();
      System.out.print("(" + instrument[i].getName() + 
                     " <" + p.getBank() + "," + p.getProgram() + ">) ");
      if (i%3 ==0)
        System.out.println();
    }
    System.out.println();
  } // konec metody: listInstruments()



  private void createTrack(int resolution)
  /* Sekvence používá PPQ (pulsů za čtvrtinu - tiků za úder)
     Skutečný počet pulsů je definován pomocí rozlišení.
     Čím vyšší je rozlišení, tím rychleji hraje hudba (vyšší tempo).

     K sekvenci přidá prázdou stopu.
  */
  { try {
      sequence = new Sequence(Sequence.PPQ, resolution);
    }
    catch (InvalidMidiDataException e) {
      e.printStackTrace();
    }
    track = sequence.createTrack();
  } // konec metody: createTrack()



  // ----------------- příklady tvorby sekvencí ----------------


  private void makeScale(int baseNote)
  /* Vytvoří sekvenci tvořenou vzrůstající stupnicí následovanou
     klesající. */
  {
    for (int i=0; i < 13; i++) {   // jedna oktáva nahoru
      add(baseNote);
      baseNote++;
    }
    for (int i=0; i < 13; i++) {    // jedna oktáva dolů
      add(baseNote);
      baseNote--;
    }
  } // konec metody: makeScale()



  private void makeSong() 
  /* Několik prvních tónů skladby "As Time Goes By" z filmu 'Casablanca'.     
     Původní MIDI-sekvenci vytvořil Heinz M. Kabutz.
     viz http://www.javaspecialists.co.za/archive/Issue076.html
  */
  { changeInstrument(0,33);    // nastav banku a program (basa)
    addRest(7);
    add("F4"); add("F4#"); add("F4"); add("D4#"); 
    add("C4#"); add("D4#", 3);  add("F4"); add("G4#"); 
    add("F4#"); add("F4"); add("D4#"); add("F4#", 3); 
    add("G4#"); add("C5#"); add("C5"); add("A4#"); 
    add("G4#"); add("A4#", 4); add("G4", 4); add("G4#", 2);
    changeInstrument(0,15);   // cimbál
    addRest(1);
    add("C5"); add("D5#"); add("C5#"); add("C5"); add("A4#");
    add("C5", 2); add("C5#", 2); add("G4#", 2); add("G4#", 2); 
    add("C4#", 2); add("D4#", 2); add("C4#", 2); 
    addRest(1);
  } // konec metody: makeSong()



  // ------------ metody add ----------------------

  private void add(String noteStr)
  {  add(noteStr, 1);  }

  private void add(int note)
  { add(note, 1);  }


  private void add(String noteStr, int period)
  // převeď řetězec s názvem noty na odpovídající číslo noty, 
// a poté ji přidej
  { int note = getKey(noteStr);
    add(note, period);
  }

  private void add(int note, int period)
  /* Přidání noty je ve skutečnosti přidáním dvou zpráv typu MidiEvent
     do stopy: zpráva NOTE_ON zahájí přehrávání tónu a zpráva NOTE_OFF
     jej zastaví. Délka hrají je stanovena časovou prodlevou (intervalem)
     mezi časovými značkami obou zpráv.
  */
  { setMessage(ShortMessage.NOTE_ON, note, tickPos);
    tickPos += period;
    setMessage(ShortMessage.NOTE_OFF, note, tickPos);
  }

  private void addRest(int period) 
  // pro přidání pomlky do stopy
  { tickPos += period; }

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


  private int getKey(String noteStr)
  /* Převede řětězec s notou (např. "C4", "B5#") na klíč. */
  {
    char[] letters = noteStr.toCharArray();

    if (letters.length < 2) {
      System.out.println("Nesprávna syntaxe noty; použije se C4");
      return C4_KEY;
    }

    // podívej se na písmeno noty: letters[0]
    int c_offset = 0;
    if ((letters[0] >= 'A') && (letters[0] <= 'G'))
      c_offset = cOffsets[letters[0] - 'A'];
    else
      System.out.println("Nesprávné písmeno: " + letters[0] + ", použiji C");

    // podívejme se na číslo oktávy: letters[1]
    int range = C4_KEY;
    if ((letters[1] >= '0') && (letters[1] <= '9'))
      range = OCTAVE * (letters[1] - '0' + 1);  // plus 1 pro midi
    else
      System.out.println("Nesprávné číslo: " + letters[1] + ", použiji 4");

    // podívejme se, je-li volitelný křížek přítomen: letters[2]
    int sharp = 0;
    if ((letters.length > 2) && (letters[2] == '#'))
      sharp = 1;    // křížek posune notu o jedničku výš 
                    // (na klaviatuře jde o černé klávesy)
    int key = range + c_offset + sharp;
    // System.out.println("note: " + noteStr + "; key: " + key);

    return key;
  }  // konec metody: getKey()


  private void setMessage(int onOrOff, int note, int tickPos)
  /* Zpráva typu MidiEvent je zabalena do zprávy typu ShortMessage
     před odesláním na stopu.
  */
  {
    if ((note < 0) || (note > 127)) {
      System.out.println("Čislo noty je mimo rozsah (0-127): " + note);
      return;
    }

    ShortMessage message = new ShortMessage();
    try {
      message.setMessage(onOrOff, CHANNEL, note, VOLUME);
      MidiEvent event = new MidiEvent(message, tickPos);
      track.add(event);
    }
    catch (InvalidMidiDataException e) {
      e.printStackTrace();
    }
  } // konec metody: setMessage()


  private void changeInstrument(int bank, int program)
  /* Nástroj je definován číslem svého banku a programu.
     Pokud zadaný bank a program odpovídají skutečnému
     nástroji, pak aktualizuje nastavení nástrojl syntezátoru.
  */
  {
    Instrument[] instrument = synthesizer.getAvailableInstruments();
    for (int i=0; i < instrument.length; i++) {
      Patch p = instrument[i].getPatch();
      if ((bank == p.getBank()) && (program == p.getProgram())) {
         // System.out.println("(" + instrument[i].getName() + 
         //            " <" + p.getBank() + "," + p.getProgram() + ">) ");
         programChange(program); 
         bankChange(bank);
         return;
      }
    }
    System.out.println("Nástroj tohoto typu neexistuje <" + bank +
                                            "," + program + ">");
  } // konec metody: changeInstrument()


  private void programChange(int program) 
  /* Změní nastavení programu přidáním zprávy PROGRAM_CHANGE do stopy.*/
  {
    ShortMessage message = new ShortMessage();
    try {
      message.setMessage(ShortMessage.PROGRAM_CHANGE, CHANNEL, program, 0);
                                  // druhý datový bajt (0) nepoužíváme
      MidiEvent event = new MidiEvent(message, tickPos);
      track.add(event);
    } 
    catch (InvalidMidiDataException e) {
       e.printStackTrace();
    }
  } // konec metody: programChange()


  private void bankChange(int bank) 
  /* Změní nastavení banky přidáním zprávy CONTROL_CHANGE do stopy. */
  {
    ShortMessage message = new ShortMessage();
    try {
      message.setMessage(ShortMessage.CONTROL_CHANGE, 
                                  CHANNEL, BANK_CONTROLLER, bank);
      MidiEvent event = new MidiEvent(message, tickPos);
      track.add(event);
    } 
    catch (InvalidMidiDataException e) {
       e.printStackTrace();
    }
  } // konec metody: bankChange()



  private void startSequencer(int tempo)
  /* Spustí sekvenci.
     Parametr tempo je v BPM (úderů za minutu),
     což pro stanovení rychlosti přehrávání kombinujeme 
     s rozlišením PPQ (tiků za úder). */
  {
    try {
      sequencer.setSequence(sequence);
    }
    catch (InvalidMidiDataException e) {
      e.printStackTrace();
    }
    sequencer.addMetaEventListener(this);
    sequencer.start();
    sequencer.setTempoInBPM(tempo);
  } // konec metody: startSequencer()


  public void meta(MetaMessage meta)
  // volá se při vyvolání meta události během přehrávání sekvence 
  {
    if (meta.getType() == END_OF_TRACK) {
      System.out.println("Konec stopy");
      System.exit(0);    // v J2SE 1.5 není třeba
    }
  } // konec metody: meta()


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

  public static void main(String[] args)
  {  new SeqSynth();  
     System.exit(0);
  }

} // konec třídy: SeqSynth
