Play.java

package com.github.dakusui.symfonion.cli.subcommands;

import com.github.dakusui.symfonion.cli.Cli;
import com.github.dakusui.symfonion.cli.Subcommand;
import com.github.dakusui.symfonion.core.Symfonion;
import com.github.dakusui.symfonion.exceptions.SymfonionException;
import com.github.dakusui.symfonion.song.Song;
import com.github.dakusui.symfonion.utils.midi.MidiDeviceScanner;
import com.github.dakusui.symfonion.utils.midi.MidiUtils;

import javax.sound.midi.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;

import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.*;
import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.ContextKey.SOURCE_FILE;

public class Play implements Subcommand {

  @Override
  public void invoke(Cli cli, PrintStream ps, InputStream inputStream) throws IOException {
    try (Context ignored = context($(SOURCE_FILE, cli.source()))) {
      Symfonion symfonion = cli.symfonion();

      Song song = symfonion.load(cli.source().getAbsolutePath());
      Map<String, Sequence> sequences = symfonion.compile(song);
      ps.println();
      Map<String, MidiDevice> midiOutDevices = prepareMidiOutDevices(ps, cli.midiOutRegexPatterns());
      ps.println();
      play(midiOutDevices, sequences);
    }
  }

  public static Map<String, MidiDevice> prepareMidiOutDevices(PrintStream ps, Map<String, Pattern> portDefinitions) {
    Map<String, MidiDevice> devices = new HashMap<>();
    for (String portName : portDefinitions.keySet()) {
      Pattern regex = portDefinitions.get(portName);
      ////
      // BEGIN: Trying to find an output device whose name matches the given regex
      MidiDeviceScanner scanner = MidiUtils.chooseOutputDevices(ps, regex);
      scanner.scan();
      MidiDevice.Info[] matchedInfos = MidiUtils.getInfos(portName, scanner, regex);
      // END
      ////
      try {
        devices.put(portName, MidiSystem.getMidiDevice(matchedInfos[0]));
      } catch (MidiUnavailableException e) {
        throw failedToAccessMidiDevice("out", e, matchedInfos);
      }
    }
    return devices;
  }

  private static Map<String, Sequencer> prepareSequencers(List<String> portNames, Map<String, MidiDevice> midiOutDevices, Map<String, Sequence> sequences) throws MidiUnavailableException, InvalidMidiDataException {
    Map<String, Sequencer> ret = new HashMap<>();
    final List<Sequencer> playingSequencers = new LinkedList<>();
    for (String portName : portNames) {
      MidiDevice midiOutDevice = midiOutDevices.get(portName);
      Sequence sequence = sequences.get(portName);
      final Sequencer sequencer = prepareSequencer(midiOutDevice, sequence, seq -> createMetaEventListener(playingSequencers, seq));
      playingSequencers.add(sequencer);
      ret.put(portName, sequencer);
    }
    return ret;
  }

  private static Sequencer prepareSequencer(MidiDevice midiOutDevice, Sequence sequence, Function<Sequencer, MetaEventListener> metaEventListenerFactory) throws MidiUnavailableException, InvalidMidiDataException {
    final Sequencer sequencer = MidiSystem.getSequencer();
    sequencer.open();
    connectMidiDeviceToSequencer(midiOutDevice, sequencer);
    sequencer.setSequence(sequence);
    sequencer.addMetaEventListener(metaEventListenerFactory.apply(sequencer));
    return sequencer;
  }

  private static void connectMidiDeviceToSequencer(MidiDevice midiOutDevice, Sequencer sequencer) throws MidiUnavailableException {
    if (midiOutDevice != null) {
      midiOutDevice.open();
      assignDeviceReceiverToSequencer(sequencer, midiOutDevice);
    }
  }

  private static void assignDeviceReceiverToSequencer(Sequencer sequencer, MidiDevice dev) throws MidiUnavailableException {
    for (Transmitter tr : sequencer.getTransmitters()) {
      tr.setReceiver(null);
    }
    sequencer.getTransmitter().setReceiver(dev.getReceiver());
  }

  private static MetaEventListener createMetaEventListener(List<Sequencer> playingSequencers, Sequencer sequencer) {
    return new MetaEventListener() {
      final Sequencer seq = sequencer;

      @Override
      public void meta(MetaMessage meta) {
        if (meta.getType() == 0x2f) {
          synchronized (Play.class) {
            try {
              Thread.sleep(1000);
            } catch (InterruptedException e) {
              throw interrupted(e);
            }
            playingSequencers.remove(this.seq);
            if (playingSequencers.isEmpty()) {
              Play.class.notifyAll();
            }
          }
        }
      }
    };
  }

  private static void startSequencers(List<String> portNames, Map<String, Sequencer> sequencers) {
    for (String portName : portNames) {
      System.out.println("Start playing on " + portName + "(" + System.currentTimeMillis() + ")");
      sequencers.get(portName).start();
    }
  }

  private static void cleanUpSequencers(List<String> portNames, Map<String, MidiDevice> midiOutDevices, Map<String, Sequencer> sequencers) {
    List<String> tmp = new LinkedList<>(portNames);
    Collections.reverse(portNames);
    for (String portName : tmp) {
      MidiDevice dev = midiOutDevices.get(portName);
      if (dev != null) {
        dev.close();
      }
      Sequencer sequencer = sequencers.get(portName);
      if (sequencer != null) {
        sequencer.close();
      }
    }
  }

  private static synchronized void play(Map<String, MidiDevice> midiOutDevices, Map<String, Sequence> sequences) throws SymfonionException {
    List<String> portNames = new LinkedList<>(sequences.keySet());
    Map<String, Sequencer> sequencers;
    try {
      sequencers = prepareSequencers(portNames, midiOutDevices, sequences);
      try {
        startSequencers(portNames, sequencers);
        Play.class.wait();
      } finally {
        System.out.println("Finished playing.");
        cleanUpSequencers(portNames, midiOutDevices, sequencers);
      }
    } catch (MidiUnavailableException e) {
      throw deviceException("Midi device was not available.", e);
    } catch (InvalidMidiDataException e) {
      throw deviceException("Data was invalid.", e);
    } catch (InterruptedException e) {
      throw deviceException("Operation was interrupted.", e);
    }
  }
}