/* * JFugue, an Application Programming Interface (API) for Music Programming * http://www.jfugue.org * * Copyright (C) 2003-2014 David Koelle * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jfugue.rhythm; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jfugue.midi.MidiDefaults; import org.jfugue.pattern.Pattern; import org.jfugue.pattern.PatternProducer; import org.staccato.StaccatoUtil; public class Rhythm implements PatternProducer { private List<String> layers; private Map<Integer, List<AltLayer>> altLayers; private Map<Character, String> rhythmKit; private int length = 1; public Rhythm() { this(DEFAULT_RHYTHM_KIT); } public Rhythm(String... layers) { this(DEFAULT_RHYTHM_KIT, layers); } public Rhythm(Map<Character, String> rhythmKit) { layers = new ArrayList<String>(); altLayers = new HashMap<Integer, List<AltLayer>>(); setRhythmKit(rhythmKit); } public Rhythm(Map<Character, String> rhythmKit, String... layers) { this(rhythmKit); for (String layer : layers) { this.addLayer(layer); } } public Rhythm setRhythmKit(Map<Character, String> rhythmKit) { this.rhythmKit = rhythmKit; return this; } public Map<Character, String> getRhythmKit() { return this.rhythmKit; } /** * Adds a layer to this Rhythm, but fails silently if * the rhythm already has MAX_LAYERS layers. * @param layer * @return */ public Rhythm addLayer(String layer) { if (this.layers.size() < MidiDefaults.LAYERS) { this.layers.add(layer); } return this; } public String getLayer(int layer) { return this.layers.get(layer); } /** * Returns all layers that have been added with the traditional addLayer() method - but to * truly find out what the layer will sound like at a given segment, use getLayersForSegment(), * which takes alt layers into account. * @see getLayersForSegment */ public List<String> getLayers() { return this.layers; } /** * Sets all of the layers */ public Rhythm setLayers(List<String> layers) { if (layers.size() > MidiDefaults.LAYERS) { throw new RuntimeException("Size of the List<String> provided to Rhythm.setLayers() is greater than "+MidiDefaults.LAYERS); } this.layers = layers; return this; } /** * Returns all layers, including altLayers, for the given segment * @see getLayers */ public String[] getLayersForSegment(int segment) { String[] retVal = new String[layers.size()]; for (int layer = 0; layer < layers.size(); layer++) { List<AltLayer> altLayers = getSortedAltLayersForLayer(layer); // Start with the base layer retVal[layer] = getLayer(layer); // See if the base layer should be replaced by any of the alt layers for (AltLayer altLayer : altLayers) { if (altLayer.shouldProvideAltLayer(segment)) { // Remember that RhythmAltLayerProvider is allowed to return null if there is nothing to add String rhythmOrNull = altLayer.getAltLayer(segment); if (rhythmOrNull != null) { retVal[layer] = rhythmOrNull; } } } } return retVal; } /** * Returns true if the number of layers is less than MAX_LAYERS, which is limited to * 16 by the MIDI Specification * @return */ public boolean canAddLayer() { return (this.layers.size() < MidiDefaults.LAYERS); } public Rhythm clone() { return new Rhythm(this.rhythmKit, this.getLayers().toArray(new String[0])); } /** * Returns all AltLayers for the given layer; the resulting list is unsorted by z-order * @see getSortedAltLatersForLayer */ public List<AltLayer> getAltLayersForLayer(int layer) { if (altLayers.get(layer) == null) { altLayers.put(layer, new ArrayList<AltLayer>()); } return altLayers.get(layer); } /** * Returns all AltLayers for the given layer sorted by each AltLayer's z-order */ public List<AltLayer> getSortedAltLayersForLayer(int layer) { List<AltLayer> retVal = getAltLayersForLayer(layer); Collections.sort(retVal, new Comparator<AltLayer>() { @Override public int compare(AltLayer altLayer1, AltLayer altLayer2) { if (altLayer1.zOrder < altLayer2.zOrder) return -1; if (altLayer1.zOrder > altLayer2.zOrder) return 1; return 0; } }); return retVal; } /** * Sets an alt layer that will recur every recurrence times *after* the start index is reached. * If the start index is 2 and the recurrence is 5, this alt layer will be used every time * the segment % recurrence == start. By default, this has a Z-Order of 1. */ public Rhythm addRecurringAltLayer(int layer, int start, int end, int recurrence, String rhythmString) { return addRecurringAltLayer(layer, start, end, recurrence, rhythmString, 1); } /** * Sets an alt layer that will recur every recurrence times *after* the start index is reached. * If the start index is 2 and the recurrence is 5, this alt layer will be used every time * the segment % recurrence == start */ public Rhythm addRecurringAltLayer(int layer, int start, int end, int recurrence, String rhythmString, int zOrder) { getAltLayersForLayer(layer).add(new AltLayer(start, end, recurrence, rhythmString, null, zOrder)); return this; } /** * Sets an alt layer that will play between and including the start and end indices. * By default, this has a Z-Order of 2. */ public Rhythm addRangedAltLayer(int layer, int start, int end, String rhythmString) { return addRangedAltLayer(layer, start, end, rhythmString, 2); } /** * Sets an alt layer that will play between and including the start and end indices. */ public Rhythm addRangedAltLayer(int layer, int start, int end, String rhythmString, int zOrder) { getAltLayersForLayer(layer).add(new AltLayer(start, end, -1, rhythmString, null, zOrder)); return this; } /** * Sets an alt layer that will play one time, at the given segment. * By default, this has a Z-Order of 3. */ public Rhythm addOneTimeAltLayer(int layer, int oneTime, String rhythmString) { return addOneTimeAltLayer(layer, oneTime, rhythmString, 3); } /** * Sets an alt layer that will play one time, at the given segment. */ public Rhythm addOneTimeAltLayer(int layer, int oneTime, String rhythmString, int zOrder) { getAltLayersForLayer(layer).add(new AltLayer(oneTime, oneTime, -1, rhythmString, null, zOrder)); return this; } /** * Gives a RhythmAltLayerProvider, which will make its own determination about what type of * alt layer to play, and when to play it. * By default, this has a Z-Order of 4. * @see RhythmAltLayerProvider */ public Rhythm addAltLayerProvider(int layer, RhythmAltLayerProvider altLayerProvider) { return addAltLayerProvider(layer, altLayerProvider, 4); } /** * Gives a RhythmAltLayerProvider, which will make its own determination about what type of * alt layer to play, and when to play it. * @see RhythmAltLayerProvider */ public Rhythm addAltLayerProvider(int layer, RhythmAltLayerProvider altLayerProvider, int zOrder) { getAltLayersForLayer(layer).add(new AltLayer(0, getLength(), -1, null, altLayerProvider, zOrder)); return this; } /** * Combines rhythms into multiple layers. If there are * more than MAX_LAYERS layers in the provided rhythms, * only the first MAX_LAYERS are used (for example, if you * pass five rhythms that each have four layers, the combined * rhythm will only contain the layers from the first four rhythms). * This method also ensures that the Rhythm Kit for each of the * provided Rhythms is added to the return value's Rhythm Kit. * @param rhythms the rhythms to combine * @return the combined rhythm */ public static Rhythm combine(Rhythm... rhythms) { Rhythm retVal = new Rhythm(); for (Rhythm rhythm : rhythms) { // Add the rhythm's Rhythm Kit to the return value's rhythm kit retVal.getRhythmKit().putAll(rhythm.getRhythmKit()); // Add the rhythm data for (String layer : rhythm.getLayers()) { if (retVal.canAddLayer()) { retVal.addLayer(layer); } else { return retVal; } } // Add the alt layer into for (int key : rhythm.altLayers.keySet()) { retVal.getAltLayersForLayer(key).addAll(rhythm.getAltLayersForLayer(key)); } // Figure out the length of the new rhythm if (retVal.getLength() < rhythm.getLength()) { retVal.setLength(rhythm.getLength()); } } return retVal; } /** * Sets the length of the rhythm, which is the number of times that a single * pattern is repeated. For example, creating a layer of "S...S...S...O..." and * a length of 3 would result in a Rhythm pattern of "S...S...S...O...S...S...S...O...S...S...S...O..." */ public Rhythm setLength(int length) { this.length = length; return this; } public int getLength() { return this.length; } /** * Uses the RhythmKit to translate the given rhythm into a Staccato music string. * @see getPattern */ public String getStaccatoStringForRhythm(String rhythm) { StringBuilder buddy = new StringBuilder(); for (char ch : rhythm.toCharArray()) { if (rhythmKit.get(ch) != null) { buddy.append(rhythmKit.get(ch)); buddy.append(" "); } else { throw new RuntimeException("The character '"+ch+"' used in the rhythm layer \""+rhythm+"\" is not associated with a Staccato music string in the RhythmKit "+rhythmKit); } } return buddy.toString().trim(); } @Override public Pattern getPattern() { StringBuilder buddy = new StringBuilder(); buddy.append(StaccatoUtil.createTrackElement((byte)9)); buddy.append(" "); for (int segment=0; segment < getLength(); segment++) { byte layerCounter = 0; for (String layer : getLayersForSegment(segment)) { buddy.append(StaccatoUtil.createLayerElement(layerCounter)); buddy.append(" "); layerCounter++; buddy.append(getStaccatoStringForRhythm(layer)); buddy.append(" "); } } return new Pattern(buddy.toString().trim()); } /** * Returns the full rhythm, including alt layers, but not translated into Staccato music strings by looking up rhythm entries into the RhythmKit * @return */ public String[] getRhythm() { // Create the full rhythm for each layer and each segment StringBuilder[] builders = new StringBuilder[this.layers.size()]; for (int i=0; i < layers.size(); i++) { builders[i] = new StringBuilder(); for (int segment=0; segment < getLength(); segment++) { builders[i].append(getLayersForSegment(segment)[i]); } } // Get strings from the builders String[] retVal = new String[this.layers.size()]; for (int i=0; i < layers.size(); i++) { retVal[i] = builders[i].toString(); } return retVal; } class AltLayer { public String rhythmString; public RhythmAltLayerProvider altLayerProvider; public int startIndex; public int endIndex; public int recurrence; public int zOrder; public AltLayer(int start, int end, int recurrence, String rhythmString, RhythmAltLayerProvider altLayerProvider, int zOrder) { this.startIndex = start; this.endIndex = end; this.recurrence = recurrence; this.rhythmString = rhythmString; this.altLayerProvider = altLayerProvider; this.zOrder = zOrder; } /** * Indicates whether this alt layer should be provided for the given segment */ public boolean shouldProvideAltLayer(int segment) { // ALways return true if there is an AltLayerProvider if (altLayerProvider != null) { return true; } // Check if we're in the right range of start and end indexes, and check the recurrence if ((segment >= startIndex) && (segment <= endIndex)) { if (recurrence == -1) return true; if ((recurrence != -1) && (segment % (recurrence) == startIndex)) return true; } return false; } /** * Returns this alt layer, assuming that shouldProvideAltLayer is true */ public String getAltLayer(int segment) { if (altLayerProvider != null) { return altLayerProvider.provideAltLayer(segment); } else { return this.rhythmString; } } } public static final Map<Character, String> DEFAULT_RHYTHM_KIT = new HashMap<Character, String>() {{ put('.', "Ri"); put('O', "[BASS_DRUM]i"); put('o', "Rs [BASS_DRUM]s"); put('S', "[ACOUSTIC_SNARE]i"); put('s', "Rs [ACOUSTIC_SNARE]s"); put('^', "[PEDAL_HI_HAT]i"); put('`', "[PEDAL_HI_HAT]s Rs"); put('*', "[CRASH_CYMBAL_1]i"); put('+', "[CRASH_CYMBAL_1]s Rs"); put('X', "[HAND_CLAP]i"); put('x', "Rs [HAND_CLAP]s"); }}; }