All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.andrewthehan.etude.theory.Interval Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version

package com.github.andrewthehan.etude.theory;

import com.github.andrewthehan.etude.exception.EtudeException;

import java.util.Arrays;

public final class Interval{
  public enum Quality{
    PERFECT("P"), MAJOR("M"), MINOR("m"), DIMINISHED("d"), DOUBLY_DIMINISHED("dd"), AUGMENTED("A"), DOUBLY_AUGMENTED("AA");

    private final String symbol;

    private Quality(String symbol){
      this.symbol = symbol;
    }

    public static final Quality fromString(String qualityString){
      switch(qualityString){
        case "P": return PERFECT;
        case "M": return MAJOR;
        case "m": return MINOR;
        case "d": return DIMINISHED;
        case "dd": return DOUBLY_DIMINISHED;
        case "A": return AUGMENTED;
        case "AA": return DOUBLY_AUGMENTED;
        default: throw new EtudeException("Invalid quality string: " + qualityString);
      }
    }

    @Override
    public String toString(){
      return symbol;
    }
  }

  private final Quality quality;
  private final int number;

  public Interval(Quality quality, int number){
    if(number <= 0){
      throw new EtudeException("Invalid interval: " + quality + number + " (number must be a positive integer)");
    }
    switch(quality){
      case PERFECT:
        if(!Interval.isPerfect(number)){
          throw new EtudeException("Invalid interval: " + quality + number + " (number cannot have a perfect quality)");
        }
        break;
      case MAJOR: case MINOR:
        if(Interval.isPerfect(number)){
          throw new EtudeException("Invalid interval: " + quality + number + " (number cannot have major or minor quality)");
        }
        break;
      case DIMINISHED: case DOUBLY_DIMINISHED: case AUGMENTED: case DOUBLY_AUGMENTED:
        break;
    }
    this.quality = quality;
    this.number = number;
  }

  public static final Interval between(Pitch a, Pitch b){
    Letter letterA = a.getKey().getLetter();
    Letter letterB = b.getKey().getLetter();

    int letterCount = Letter.values().length;
    
    /**
     * 1 (because no distance == 1)
     * + letterDistance (subtracted 2 because C is the start of the octave)
     * + octaveDistance
     */
    int number = 1
      + (Math.floorMod(letterB.ordinal() - 2, letterCount) - Math.floorMod(letterA.ordinal() - 2, letterCount))
      + (b.getOctave() - a.getOctave()) * letterCount;

    if(number <= 0){
      throw new EtudeException("Cannot create interval with nonpositive number");
    }

    int offset = (b.getProgramNumber() - a.getProgramNumber()) % MusicConstants.KEYS_IN_OCTAVE;
    offset -= Arrays
      .stream(Scale.Quality.MAJOR.getStepPattern())
      .limit((number - 1) % letterCount)
      .sum();

    Quality quality;
    switch(offset){
      case -3:
        if(Interval.isPerfect(number)){
          throw new EtudeException("Cannot create interval for pitches: " + a + " -> " + b);
        }
        quality = Quality.DOUBLY_DIMINISHED;
        break;
      case -2:
        quality = Interval.isPerfect(number) ? Quality.DOUBLY_DIMINISHED : Quality.DIMINISHED;
        break;
      case -1:
        quality = Interval.isPerfect(number) ? Quality.DIMINISHED : Quality.MINOR;
        break;
      case 0:
        quality = Interval.isPerfect(number) ? Quality.PERFECT : Quality.MAJOR;
        break;
      case 1:
        quality = Quality.AUGMENTED;
        break;
      case 2:
        quality = Quality.DOUBLY_AUGMENTED;
        break;
      default:
        throw new EtudeException("Cannot create interval for pitches: " + a + " -> " + b);
    }

    return new Interval(quality, number);
  }

  public final int getOffset(){
    int letterCount = Letter.values().length;

    // initialize offset to take into account octave
    int offset = (number - 1) / letterCount * MusicConstants.KEYS_IN_OCTAVE;

    // take into account normalized number (within the range of an octave)
    offset += Arrays
      .stream(Scale.Quality.MAJOR.getStepPattern())
      .limit((number - 1) % letterCount)
      .sum();

    // take into account quality
    switch(quality){
      case PERFECT: case MAJOR:
        break;
      case MINOR:
        --offset;
        break;
      case DIMINISHED:
        offset -= Interval.isPerfect(number) ? 1 : 2;
        break;
      case DOUBLY_DIMINISHED:
        offset -= Interval.isPerfect(number) ? 2 : 3;
        break;
      case AUGMENTED:
        ++offset;
        break;
      case DOUBLY_AUGMENTED:
        offset += 2;
        break;
    }
    return offset;
  }

  public static final boolean isPerfect(int number){
    int normalized = number % 7;
    return normalized == 1 || normalized == 4 || normalized == 5;
  }

  public static final Interval fromString(String intervalString){
    if(intervalString == null){
      throw new EtudeException("Invalid interval string: " + intervalString);
    }
    else if(intervalString.trim().isEmpty()){
      throw new EtudeException("Invalid interval string: " + intervalString + " (blank)");
    }

    /*
     * has a non-digit / digit before it and a digit / non-digit after it;
     * a valid intervalString doesn't require this regex (only needs to detect
     * non-digit then digit) but in order to provide proper exception messages
     * for invalid intervalStrings, the current regex is required
     */
    String[] split = intervalString.split("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)");

    if(split.length < 2){
      throw new EtudeException("Invalid interval string: " + intervalString + " (missing information)");
    }
    else if(split.length > 2){
      throw new EtudeException("Invalid interval string: " + intervalString + " (contains extra information)");
    }

    Quality quality = Quality.fromString(split[0]);

    int number = Integer.parseInt(split[1]);

    return new Interval(quality, number);
  }

  @Override
  public String toString(){
    StringBuilder builder = new StringBuilder();
    builder.append(quality);
    builder.append(number);
    return builder.toString();
  }

  public final Quality getQuality(){
    return quality;
  }

  public final int getNumber(){
    return number;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy