Java tutorial
/* * 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; } } }