Groove.java

package com.github.dakusui.symfonion.song;

import com.github.dakusui.json.JsonFormatException;
import com.github.dakusui.json.JsonInvalidPathException;
import com.github.dakusui.json.JsonTypeMismatchException;
import com.github.dakusui.json.JsonUtils;
import com.github.dakusui.symfonion.utils.Fraction;
import com.github.dakusui.symfonion.exceptions.SymfonionException;
import com.github.dakusui.symfonion.utils.Utils;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import java.util.LinkedList;
import java.util.List;

import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.*;
import static com.github.dakusui.symfonion.exceptions.SymfonionIllegalFormatException.NOTE_LENGTH_EXAMPLE;
import static com.github.dakusui.symfonion.exceptions.SymfonionTypeMismatchException.OBJECT;

public class Groove {
  public static final Groove DEFAULT_INSTANCE = new Groove();

  record Beat(Fraction length, long ticks, int accent) {
  }

  public record Unit(long pos, int accentDelta) {

    public int accent() {
        return this.accentDelta;
      }
    }
  
  List<Beat> beats = new LinkedList<>();
  
  private final int resolution;
  
  public Groove() {
    this(384);
  }
  
  public Groove(int resolution) {
    this.resolution = resolution;
  }
  
  public Unit resolve(Fraction offset) {
    if (offset == null) {
      String msg = "offset cannot be null. (Groove#resolve)";
      throw runtimeException(msg, null);
    }
    if (Fraction.compare(offset, Fraction.zero) < 0) {
      String msg = "offset cannot be negative. (Groove#resolve)";
      throw runtimeException(msg, null);
    }
    long pos = 0;
    
    Fraction rest = offset.clone();
    int i = 0;
    while (Fraction.compare(rest, Fraction.zero) > 0) {
      if (i >= this.beats.size()) {
        break;
      }
      Beat beat = this.beats.get(i);
      rest = Fraction.subtract(rest, beat.length);
      pos += beat.ticks;
      i++;
    }
    long p;
    int d = 0;
    if (Fraction.compare(rest, Fraction.zero) < 0) {
      Beat beat = this.beats.get(i - 1);
      p = (long) (pos + Fraction.div(rest, beat.length).doubleValue() * beat.ticks);
    } else if (Fraction.compare(rest, Fraction.zero) == 0) {
      if (i < this.beats.size()) {
        d = this.beats.get(i).accent;
      }
      p = pos;
    } else {
      p = (pos + (long) (rest.doubleValue() * this.resolution));
    }
    return new Unit(p, d);
  }
  
  public void add(Fraction length, long ticks, int accent) {
    if (this == DEFAULT_INSTANCE) {
      throw runtimeException("Groove.DEFAULT_INSTANCE is immutable.", null);
    }
    beats.add(new Beat(length, ticks, accent));
  }
  
  public static Groove createGroove(JsonArray grooveDef) throws SymfonionException, JsonTypeMismatchException, JsonInvalidPathException, JsonFormatException {
    Groove ret = new Groove();
    for (JsonElement elem : grooveDef) {
      if (!elem.isJsonObject()) {
        throw typeMismatchException(elem, OBJECT);
      }
      JsonObject cur = elem.getAsJsonObject();
      String len = JsonUtils.asString(cur, Keyword.$length);
      long ticks = JsonUtils.asLong(cur, Keyword.$ticks);
      int accent = JsonUtils.asInt(cur, Keyword.$accent);
      
      Fraction f = Utils.parseNoteLength(len);
      if (f == null) {
        throw illegalFormatException(JsonUtils.asJsonElement(cur, Keyword.$length), NOTE_LENGTH_EXAMPLE);
      }
      ret.add(f, ticks, accent);
    }
    return ret;
  }
}