code: /*
   * Computoser is a music-composition algorithm and a website to present the results
   * Copyright (C) 2012-2014  Bozhidar Bozhanov
   *
   * Computoser is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Affero General Public License as
   * published by the Free Software Foundation, either version 3 of the
   * License, or (at your option) any later version.
   *
   * Computoser is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Affero General Public License for more details.
   *
   * You should have received a copy of the GNU Affero General Public License
   * along with Computoser.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.music.tools;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Random;
  import java.util.Set;
  
  import javax.sound.sampled.AudioFormat;
  import javax.sound.sampled.AudioSystem;
  import javax.sound.sampled.SourceDataLine;
  
  import org.apache.commons.lang.ArrayUtils;
  
  import jm.JMC;
  
  import com.music.MainPartGenerator.MainPartContext;
  import com.music.ScoreContext;
  import com.music.util.music.Chance;
  import com.music.util.music.ToneResolver;
  
  public class ScaleTester {
      private static final int SCALE_SIZE = 7;
      private static final boolean USE_ET = true;
      private static final int CHROMATIC_SCALE_SILZE = 12;
      private static final double FUNDAMENTAL_FREQUENCY = 262.626;
      // chromatic-to-scale ratio: (12/7) 1,7142857142857142857142857142857
  
      private static Random random = new Random();
      private static int sampleRate = 8000;
      private static Map<Double, long[]> fractionCache = new HashMap<>();
      private static double fundamentalFreq = 0;
  
      public static void main(String[] args) {
          System.out
                  .println("Usage: java ScaleTester <fundamental frequency> <chromatic scale size> <scale size> <use ET>");
          final AudioFormat af = new AudioFormat(sampleRate, 16, 1, true, true);
          try {
              fundamentalFreq = getArgument(args, 0, FUNDAMENTAL_FREQUENCY, Double.class);
              int pitchesInChromaticScale = getArgument(args, 1, CHROMATIC_SCALE_SILZE, Integer.class);
  
              List<Double> harmonicFrequencies = new ArrayList<>();
              List<String> ratios = new ArrayList<>();
              Set<Double> frequencies = new HashSet<Double>();
              frequencies.add(fundamentalFreq);
              int octaveMultiplier = 2;
              for (int i = 2; i < 100; i++) {
                  // Exclude the 7th harmonic TODO exclude the 11th as well?
                  // http://www.phy.mtu.edu/~suits/badnote.html
                  if (i % 7 == 0) {
                      continue;
                  }
                  double actualFreq = fundamentalFreq * i;
                  double closestTonicRatio = actualFreq / (fundamentalFreq * octaveMultiplier);
                  if (closestTonicRatio < 1 || closestTonicRatio > 2) {
                      octaveMultiplier *= 2;
                  }
                  double closestTonic = actualFreq - actualFreq % (fundamentalFreq * octaveMultiplier);
                  double normalizedFreq = fundamentalFreq * (actualFreq / closestTonic);
  
                  harmonicFrequencies.add(actualFreq);
                  frequencies.add(normalizedFreq);
                  if (frequencies.size() == pitchesInChromaticScale) {
                      break;
                  }
              }
  
              System.out.println("Harmonic (overtone) frequencies: " + harmonicFrequencies);
              System.out.println("Transposed harmonic frequencies: " + frequencies);
  
              List<Double> chromaticScale = new ArrayList<>(frequencies);
              Collections.sort(chromaticScale);
  
              // find the "perfect" interval (e.g. perfect fifth)
              int perfectIntervalIndex = 0;
              int idx = 0;
              for (Iterator<Double> it = chromaticScale.iterator(); it.hasNext();) {
                  Double noteFreq = it.next();
                  long[] fraction = findCommonFraction(noteFreq / fundamentalFreq);
                  fractionCache.put(noteFreq, fraction);
                  if (fraction[0] == 3 && fraction[1] == 2) {
                      perfectIntervalIndex = idx;
                      System.out.println("Perfect interval (3/2) idx: " + perfectIntervalIndex);
                  }
                  idx++;
                  ratios.add(Arrays.toString(fraction));
              }
              System.out.println("Ratios to fundemental frequency: " + ratios);
  
              if (getBooleanArgument(args, 4, USE_ET)) {
                  chromaticScale = temper(chromaticScale);
              }
  
              System.out.println();
              System.out.println("Chromatic scale: " + chromaticScale);
  
              Set<Double> scaleSet = new HashSet<Double>();
              scaleSet.add(chromaticScale.get(0));
              idx = 0;
              List<Double> orderedInCircle = new ArrayList<>();
              // now go around the circle of perfect intervals and put the notes
              // in order
              while (orderedInCircle.size() < chromaticScale.size()) {
                  orderedInCircle.add(chromaticScale.get(idx));
                  idx += perfectIntervalIndex;
                  idx = idx % chromaticScale.size();
              }
              System.out.println("Pitches Ordered in circle of perfect intervals: " + orderedInCircle);
  
              List<Double> scale = new ArrayList<Double>(scaleSet);
              int currentIdxInCircle = orderedInCircle.size() - 1; // start with
                                                                   // the last
                                                                   // note in the
                                                                   // circle
              int scaleSize = getArgument(args, 3, SCALE_SIZE, Integer.class);
              while (scale.size() < scaleSize) {
                  double pitch = orderedInCircle.get(currentIdxInCircle % orderedInCircle.size());
                  if (!scale.contains(pitch)) {
                      scale.add(pitch);
                  }
                  currentIdxInCircle++;
              }
              Collections.sort(scale);
  
              System.out.println("Scale: " + scale);
  
              SourceDataLine line = AudioSystem.getSourceDataLine(af);
              line.open(af);
              line.start();
  
              Double[] scaleFrequencies = scale.toArray(new Double[scale.size()]);
  
              // first play the whole scale
              WaveMelodyGenerator.playScale(line, scaleFrequencies);
              // then generate a random melody in the scale
              WaveMelodyGenerator.playMelody(line, scaleFrequencies);
  
              line.drain();
              line.close();
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  
      private static boolean getBooleanArgument(String[] args, int i, boolean defaultValue) {
          if (args.length > i) {
              return Boolean.parseBoolean(args[i]);
          } else {
              return defaultValue;
          }
      }
  
      private static <T extends Number> T getArgument(String[] args, int i, T defaultValue, Class<T> resultClass) {
          if (args.length > i) {
              return resultClass.cast(Double.parseDouble(args[i]));
          } else {
              return defaultValue;
          }
      }
  
      private static List<Double> temper(List<Double> chromaticScale) {
          System.out.println("Before temper: " + chromaticScale);
          Double currentNote = chromaticScale.get(0);
          List<Double> result = new ArrayList<Double>();
          result.add(currentNote);
          double ratio = Math.pow(2, 1d / chromaticScale.size());
          for (int i = 1; i < chromaticScale.size(); i++) {
              currentNote = currentNote * ratio;
              currentNote = ((int) (currentNote * 1000)) / 1000d;
              result.add(currentNote);
              // Fill the fractions cache with the new values:
              long[] fraction = findCommonFraction(currentNote / fundamentalFreq);
              fractionCache.put(currentNote, fraction);
          }
          return result;
      }
  
      public static long[] findCommonFraction(double decimal) {
          long multiplier = 100000000l;
          long numerator = (int) (decimal * multiplier);
          long denominator = multiplier;
  
          long[] result = simplify(numerator, denominator);
          return result;
      }
  
      private static long[] simplify(long numerator, long denominator) {
          int divisor = 2;
          long maxDivisor = Math.min(numerator, denominator) / 2;
          while (divisor < maxDivisor) {
              if (numerator % divisor == 0 && denominator % divisor == 0) {
                  numerator = numerator / divisor;
                  denominator = denominator / divisor;
              } else {
                  divisor++;
              }
          }
  
          return new long[] { numerator, denominator };
      }
  
      /**
       * Low-level sound wave handling
       *
       */
      public static class WavePlayer {
  
          public static void playNotes(SourceDataLine line, double[] frequencies) {
              for (int i = 0; i < frequencies.length; i++) {
                  playNote(line, frequencies[i]);
              }
          }
  
          public static void playNotes(SourceDataLine line, Double[] frequencies) {
              playNotes(line, ArrayUtils.toPrimitive(frequencies));
          }
  
          public static void playNote(SourceDataLine line, double frequency) {
              play(line, generateSineWavefreq(frequency, 1));
          }
  
          private static void play(SourceDataLine line, byte[] array) {
              int length = sampleRate * array.length / 1000;
              line.write(array, 0, array.length);
          }
  
          private static byte[] generateSineWavefreq(double frequencyOfSignal, double seconds) {
              byte[] sin = new byte[(int) (seconds * sampleRate)];
              double samplingInterval = (double) (sampleRate / frequencyOfSignal);
              for (int i = 0; i < sin.length; i++) {
                  double angle = (2.0 * Math.PI * i) / samplingInterval;
                  sin[i] = (byte) (Math.sin(angle) * 127);
              }
              return sin;
          }
      }
  
      /**
       * Simple class that generates and plays a melody in a given scale
       *
       */
      public static class WaveMelodyGenerator {
  
          private static void playMelody(SourceDataLine line, Double[] scaleFrequencies) {
              int position;
              MainPartContext lCtx = new MainPartContext();
              lCtx.setDirectionUp(true);
              ScoreContext ctx = new ScoreContext();
              double[] melody = new double[30];
              double[] lengths = new double[30];
              for (int i = 0; i < 30; i++) {
                  position = getNextNotePitchIndex(ctx, lCtx, scaleFrequencies);
                  double freq = scaleFrequencies[position];
                  double length = getNoteLength(lCtx);
                  melody[i] = freq;
                  lengths[i] = length;
              }
  
              WavePlayer.playNotes(line, melody);
          }
  
          private static void playScale(SourceDataLine line, Double[] scaleFrequencies) {
              WavePlayer.playNotes(line, scaleFrequencies);
              WavePlayer.playNote(line, scaleFrequencies[0] * 2);
          }
  
  
          /**
           * Pieces copied from MainPartGenerator
           */
          private static final int[] PROGRESS_TYPE_PERCENTAGES = new int[] { 25, 48, 25, 2 };
          private static final int[] NOTE_LENGTH_PERCENTAGES = new int[] { 10, 31, 40, 7, 9, 3 };
  
          public static double getNoteLength(MainPartContext lCtx) {
              double length = 0;
              int lengthSpec = Chance.choose(NOTE_LENGTH_PERCENTAGES);
  
              // don't allow drastic changes in note length
              if (lCtx.getPreviousLength() != 0 && lCtx.getPreviousLength() < 1 && lengthSpec == 5) {
                  length = 4;
              } else if (lCtx.getPreviousLength() != 0 && lCtx.getPreviousLength() >= 2 && lengthSpec == 0) {
                  lengthSpec = 1;
              }
  
              if (lengthSpec == 0
                      && (lCtx.getSameLengthNoteSequenceCount() == 0 || lCtx.getSameLengthNoteType() == JMC.SIXTEENTH_NOTE)) {
                  length = JMC.SIXTEENTH_NOTE;
              } else if (lengthSpec == 1
                      && (lCtx.getSameLengthNoteSequenceCount() == 0 || lCtx.getSameLengthNoteType() == JMC.EIGHTH_NOTE)) {
                  length = JMC.EIGHTH_NOTE;
              } else if (lengthSpec == 2
                      && (lCtx.getSameLengthNoteSequenceCount() == 0 || lCtx.getSameLengthNoteType() == JMC.QUARTER_NOTE)) {
                  length = JMC.QUARTER_NOTE;
              } else if (lengthSpec == 3
                      && (lCtx.getSameLengthNoteSequenceCount() == 0 || lCtx.getSameLengthNoteType() == JMC.DOTTED_QUARTER_NOTE)) {
                  length = JMC.DOTTED_QUARTER_NOTE;
              } else if (lengthSpec == 4) {
                  length = JMC.HALF_NOTE;
              } else if (lengthSpec == 5) {
                  length = JMC.WHOLE_NOTE;
              }
  
              // handle sequences of notes with the same length
              if (lCtx.getSameLengthNoteSequenceCount() == 0 && Chance.test(17)
                      && length <= JMC.DOTTED_QUARTER_NOTE) {
                  lCtx.setSameLengthNoteSequenceCount(3 + random.nextInt(7));
                  lCtx.setSameLengthNoteType(length);
              }
              if (lCtx.getSameLengthNoteSequenceCount() > 0) {
                  lCtx.setSameLengthNoteSequenceCount(lCtx.getSameLengthNoteSequenceCount() - 1);
              }
  
              return length;
          }
  
          private static int getNextNotePitchIndex(ScoreContext ctx, MainPartContext lCtx, Double[] frequencies) {
              int notePitchIndex;
              if (lCtx.getPitches().isEmpty()) {
                  // avoid excessively high and low notes.
                  notePitchIndex = 0;
                  lCtx.getPitchRange()[0] = 0;
                  lCtx.getPitchRange()[1] = frequencies.length;
              } else {
                  int previousNotePitch = lCtx.getPitches().get(lCtx.getPitches().size() - 1);
                  boolean shouldResolveToStableTone = shouldResolveToStableTone(lCtx.getPitches(), frequencies);
  
                  if (!lCtx.getCurrentChordInMelody().isEmpty()) {
                      notePitchIndex = lCtx.getCurrentChordInMelody().get(0);
                      lCtx.getCurrentChordInMelody().remove(0);
                  } else if (shouldResolveToStableTone) {
                      notePitchIndex = resolve(previousNotePitch, frequencies);
                      if (lCtx.getPitches().size() > 1 && notePitchIndex == previousNotePitch) {
                          // in that case, make a step to break the repetition
                          // pattern
                          int pitchChange = getStepPitchChange(frequencies, lCtx.isDirectionUp(),
                                  previousNotePitch);
                          notePitchIndex = previousNotePitch + pitchChange;
                      }
                  } else {
                      // try getting a pitch. if the pitch range is exceeded, get
                      // a
                      // new consonant tone, in the opposite direction, different
                      // progress type and different interval
                      int attempt = 0;
  
                      // use a separate variable in order to allow change only for
                      // this particular note, and not for the direction of the
                      // melody
                      boolean directionUp = lCtx.isDirectionUp();
                      do {
                          int progressType = Chance.choose(PROGRESS_TYPE_PERCENTAGES);
                          // in some cases change the predefined direction (for
                          // this pitch only), for a more interesting melody
                          if ((progressType == 1 || progressType == 2) && Chance.test(15)) {
                              directionUp = !directionUp;
                          }
  
                          // always follow big jumps with a step back
                          int needsStepBack = needsStepBack(lCtx.getPitches());
                          if (needsStepBack != 0) {
                              progressType = 1;
                              directionUp = needsStepBack == 1;
                          }
                          if (progressType == 1) { // step
                              int pitchChange = getStepPitchChange(frequencies, directionUp, previousNotePitch);
                              notePitchIndex = previousNotePitch + pitchChange;
                          } else if (progressType == 0) { // unison
                              notePitchIndex = previousNotePitch;
                          } else { // 2 - intervals
                              // for a melodic sequence, use only a "jump" of up
                              // to 6 pitches in current direction
                              int change = 2 + random.nextInt(frequencies.length - 2);
                              notePitchIndex = (previousNotePitch + change) % frequencies.length;
                          }
  
                          if (attempt > 0) {
                              directionUp = !directionUp;
                          }
                          // if there are more than 3 failed attempts, simply
                          // assign a random in-scale, in-range pitch
                          if (attempt > 3) {
                              int start = lCtx.getPitchRange()[1] - random.nextInt(6);
                              for (int i = start; i > lCtx.getPitchRange()[0]; i--) {
                                  if (Arrays.binarySearch(lCtx.getCurrentScale().getDefinition(), i % 12) > -1) {
                                      notePitchIndex = i;
                                  }
                              }
                          }
                          attempt++;
                      } while (!ToneResolver.isInRange(notePitchIndex, lCtx.getPitchRange()));
                  }
              }
              lCtx.getPitches().add(notePitchIndex);
              return notePitchIndex;
          }
  
          private static int resolve(int previousNotePitch, Double[] frequencies) {
              int idx = previousNotePitch + 1;
              int step = 1;
              while (idx >= 0 && idx < frequencies.length) {
                  if (fractionCache.get(frequencies[idx])[0] <= 9) {
                      return idx;
                  }
                  if (step > 0) {
                      step = -step;
                  } else {
                      step = -step;
                      step++;
                  }
                  idx += step;
                  idx = idx % frequencies.length;
              }
              return 0;
          }
  
          private static int needsStepBack(List<Integer> pitches) {
              if (pitches.size() < 2) {
                  return 0;
              }
              int previous = pitches.get(pitches.size() - 1);
              int prePrevious = pitches.get(pitches.size() - 2);
  
              int diff = previous - prePrevious;
              if (Math.abs(diff) > 6) {
                  return (int) -Math.signum(diff); // the opposite direction of
                                                   // the previous interval
              }
              return 0;
          }
  
          private static int getStepPitchChange(Double[] frequencies, boolean directionUp, int previousNotePitch) {
              int pitchChange = 0;
  
              int[] steps = new int[] { -1, 1 };
              if (directionUp) {
                  steps = new int[] { 1, -1, };
              }
              for (int i : steps) {
                  // if the pitch is in the predefined direction and it is within
                  // the scale - use it.
                  if (previousNotePitch + i < frequencies.length && previousNotePitch + i > 0) {
                      pitchChange = i;
                  }
                  // in case no other matching tone is found that is common, the
                  // last appropriate one will be retained in "pitchChange"
              }
              return pitchChange;
          }
  
          private static boolean shouldResolveToStableTone(List<Integer> pitches, Double[] frequencies) {
              // if the previous two pitches are unstable
              int previousNotePitch = pitches.get(pitches.size() - 1);
              int prePreviousNotePitch = 0;
              if (pitches.size() >= 2) {
                  prePreviousNotePitch = pitches.get(pitches.size() - 2);
              }
              long[] previousRatio = fractionCache.get(frequencies[previousNotePitch]);
              long[] prePreviousRatio = fractionCache.get(frequencies[prePreviousNotePitch]);
              if (prePreviousNotePitch != 0 && previousRatio[0] > 9 && prePreviousRatio[0] > 9) {
                  return true;
              }
              return false;
          }
      }
  }