MidiCompiler.java

package com.github.dakusui.symfonion.core;

import com.github.dakusui.logias.Logias;
import com.github.dakusui.logias.lisp.Context;
import com.github.dakusui.logias.lisp.s.Literal;
import com.github.dakusui.logias.lisp.s.Sexp;
import com.github.dakusui.symfonion.exceptions.ExceptionThrower;
import com.github.dakusui.symfonion.utils.Fraction;
import com.github.dakusui.symfonion.exceptions.SymfonionException;
import com.github.dakusui.symfonion.utils.Utils;
import com.github.dakusui.symfonion.song.*;
import com.github.dakusui.symfonion.song.Pattern.Parameters;
import com.google.gson.JsonArray;

import javax.sound.midi.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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

public class MidiCompiler {

  private final Context logiasContext;

  public MidiCompiler(Context logiasContext) {
    this.logiasContext = (logiasContext);
  }

  /**
   * Compiles a {@link Song} object into a map from a port name to {@link Sequence} object.
   *
   * @param song An object that holds user-provided music data.
   * @return A map from a port name to {@code Sequence} object.
   * @throws InvalidMidiDataException Won't be thrown.
   * @throws SymfonionException       Undefined part name is referenced by a bar.
   */
  public Map<String, Sequence> compile(Song song) throws InvalidMidiDataException, SymfonionException {
    System.err.println("Now compiling...");
    int resolution = 384;
    Map<String, Sequence> ret = new HashMap<>();
    Map<String, Track> tracks;
    tracks = new HashMap<>();
    for (String partName : song.partNames()) {
      Part part = song.part(partName);
      String portName = part.portName();
      Sequence sequence = ret.get(portName);
      if (sequence == null) {
        sequence = new Sequence(Sequence.PPQ, resolution / 4);
        ret.put(portName, sequence);
      }
      tracks.put(partName, sequence.createTrack());
    }

    ////
    // position is the offset of a bar from the beginning of the sequence.
    // Giving the sequencer a grace period to initialize its internal state.
    long barPositionInTicks = 0; //= resolution / 4;
    int barid = 0;
    for (Bar bar : song.bars()) {
      try (var ignored = context($(JSON_ELEMENT_ROOT, bar.rootJsonObject()))) {
        barStarted(barid);
        Groove groove = bar.groove();
        for (String partName : bar.partNames()) {
          partStarted(partName);
          Track track = tracks.get(partName);
          if (track == null) {
            aborted();
            throw partNotFound(bar.lookUpJsonNode(partName), partName);
          }
          int channel = song.part(partName).channel();
          for (List<Pattern> patterns : bar.part(partName)) {
            ////
            // relativePosition is a relative position from the beginning
            // of the bar the pattern belongs to.
            Fraction relPosInBar = Fraction.zero;
            for (Pattern each : patterns) {
              Parameters params = each.parameters();
              patternStarted();
              for (Stroke stroke : each.strokes()) {
                try {
                  Fraction endingPos = Fraction.add(relPosInBar, stroke.length());

                  stroke.compile(
                      this,
                      new MidiCompilerContext(
                          track,
                          channel,
                          params,
                          relPosInBar,
                          barPositionInTicks,
                          groove
                      )
                  );

                  relPosInBar = endingPos;
                  ////
                  // Breaks if relative position goes over the length of the bar.
                  if (Fraction.compare(relPosInBar, bar.beats()) >= 0) {
                    break;
                  }
                } finally {
                  strokeEnded();
                }
              }
              patternEnded();
            }
          }
          partEnded();
        }
        barEnded();
        barid++;
        barPositionInTicks += (long) (bar.beats().doubleValue() * resolution);
      }
    }
    System.err.println("Compilation finished.");
    return ret;
  }

  public MidiEvent createNoteOnEvent(int ch, int nKey, int velocity, long lTick) throws InvalidMidiDataException {
    return createNoteEvent(ShortMessage.NOTE_ON,
        ch,
        nKey,
        velocity,
        lTick);
  }

  public MidiEvent createNoteOffEvent(int ch, int nKey, long lTick) throws InvalidMidiDataException {
    return createNoteEvent(ShortMessage.NOTE_OFF,
        ch,
        nKey,
        0,
        lTick);
  }

  protected MidiEvent createNoteEvent(int nCommand,
                                      int ch,
                                      int nKey,
                                      int nVelocity,
                                      long lTick) throws InvalidMidiDataException {
    ShortMessage message = new ShortMessage();
    message.setMessage(nCommand,
        ch,
        nKey,
        nVelocity);
    return new MidiEvent(message,
        lTick);
  }

  public MidiEvent createProgramChangeEvent(int ch, int pgnum, long lTick) throws InvalidMidiDataException {
    ShortMessage message = new ShortMessage();
    message.setMessage(ShortMessage.PROGRAM_CHANGE, ch, pgnum, 0);

    return new MidiEvent(message, lTick);
  }

  public MidiEvent createSysexEvent(int ch, JsonArray arr, long lTick) throws InvalidMidiDataException {
    SysexMessage message = new SysexMessage();
    Context lctxt = this.logiasContext.createChild();
    Sexp channel = new Literal(ch);
    lctxt.bind("channel", channel);
    Logias logias = new Logias(lctxt);
    Sexp sysexsexp = logias.buildSexp(arr);
    Sexp sexp = logias.run(sysexsexp);
    if (Sexp.nil.equals(sexp)) {
      return null;
    }
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(SysexMessage.SYSTEM_EXCLUSIVE);    // status: SysEx start
    Iterator<Sexp> i = sexp.iterator().assumeList();
    while (i.hasNext()) {
      Sexp cur = i.next();
      baos.write((byte) cur.asAtom().longValue());
    }
    baos.write(SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE);    // End of exclusive
    try {
      baos.close();
    } catch (IOException e) {
      throw ExceptionThrower.runtimeException(e.getMessage(), e);
    }
    byte[] data = baos.toByteArray();
    message.setMessage(data, data.length);
    return new MidiEvent(message, lTick);
  }

  public MidiEvent createControlChangeEvent(int ch, int controllernum, int param, long lTick) throws InvalidMidiDataException {
    ShortMessage message = new ShortMessage();
    message.setMessage(ShortMessage.CONTROL_CHANGE, ch, controllernum, param);
    return new MidiEvent(message, lTick);
  }

  public MidiEvent createBankSelectMSBEvent(int ch, int bkmsb, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 0, bkmsb, lTick);
  }

  public MidiEvent createBankSelectLSBEvent(int ch, int bklsb, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 32, bklsb, lTick);
  }

  public MidiEvent createVolumeChangeEvent(int ch, int volume, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 7, volume, lTick);
  }

  public MidiEvent createPanChangeEvent(int ch, int pan, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 10, pan, lTick);
  }

  public MidiEvent createReverbEvent(int ch, int depth, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 91, depth, lTick);
  }

  public MidiEvent createChorusEvent(int ch, int depth, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 93, depth, lTick);
  }

  public MidiEvent createPitchBendEvent(int ch, int depth, long lTick) throws InvalidMidiDataException {
    ShortMessage message = new ShortMessage();
    message.setMessage(ShortMessage.PITCH_BEND, ch, 0, depth);
    return new MidiEvent(message, lTick);
  }

  public MidiEvent createModulationEvent(int ch, int depth, long lTick) throws InvalidMidiDataException {
    return createControlChangeEvent(ch, 1, depth, lTick);
  }

  public MidiEvent createAfterTouchChangeEvent(int ch, int v, long lTick) throws InvalidMidiDataException {
    ShortMessage message = new ShortMessage();
    message.setMessage(ShortMessage.CHANNEL_PRESSURE, ch, v, 0);
    return new MidiEvent(message, lTick);
  }

  public MidiEvent createTempoEvent(int tempo, long lTick) throws InvalidMidiDataException {
    int mpqn = 60000000 / tempo;
    MetaMessage mm = new MetaMessage();
    byte[] data = Utils.getIntBytes(mpqn);
    mm.setMessage(0x51, data, data.length);
    return new MidiEvent(mm, lTick);
  }

  public void noteProcessed() {
    System.out.print(".");
  }

  public void controlEventProcessed() {
    System.out.print("*");
  }

  public void sysexEventProcessed() {
    System.out.print("X");
  }

  public void barStarted(int barid) {
    System.out.println("bar[" + barid + "]");
  }

  public void patternStarted() {
    System.out.print("[");
  }

  public void patternEnded() {
    System.out.print("]");
  }

  public void barEnded() {
  }

  public void partStarted(String partName) {
    System.out.print("    " + partName + ":");
  }

  public void strokeEnded() {
    System.out.print("|");
  }

  public void partEnded() {
    System.out.println();
  }

  public void aborted() {
    System.out.println("aborted.");
  }

  public void noteSetProcessed() {
    System.out.print(";");
  }
}