/* * JFugue - API for Music Programming * Copyright (C) 2003-2008 David Koelle * * http://www.jfugue.org * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.jfugue; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.EventListener; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MidiSystem; import javax.swing.event.EventListenerList; /** * This class represents a segment of music. By representing segments of music * as patterns, JFugue gives users the opportunity to play around with pieces of * music in new and interesting ways. Patterns may be added together, * transformed, or otherwise manipulated to expand the possibilities of creative * music. * * @author David Koelle * @version 2.0 * @version 4.0 - Added Pattern Properties * @version 4.0.3 - Now implements Serializable */ public class Pattern { private StringBuilder musicString; private Map<String, String> properties; /** * Instantiates a new pattern */ public Pattern() { this(""); } /** * Instantiates a new pattern using the given music string * * @param s * the music string */ public Pattern(String musicString) { setMusicString(musicString); properties = new HashMap<String, String>(); } /** Copy constructor */ public Pattern(Pattern pattern) { this(pattern.getMusicString()); Iterator<String> iter = pattern.getProperties().keySet().iterator(); while (iter.hasNext()) { String key = iter.next(); String value = pattern.getProperty(key); setProperty(key, value); } } /** * This constructor creates a new Pattern that contains each of the given * patterns * * @version 4.0 */ public Pattern(Pattern... patterns) { this(); for (Pattern p : patterns) { this.add(p); } } /** * Creates a Pattern given a MIDI file - do not use. Note the Package scope, * limiting this method to be called only by JFugue. If you want to load * MIDI, use Player.loadMidi, which sets the sequence timing correctly. * * @param file * @throws IOException * @throws InvalidMidiDataException */ static Pattern loadMidi(File file) throws IOException, InvalidMidiDataException { MidiParser parser = new MidiParser(); MusicStringRenderer renderer = new MusicStringRenderer(); parser.addParserListener(renderer); parser.parse(MidiSystem.getSequence(file)); Pattern pattern = new Pattern(renderer.getPattern().getMusicString()); return pattern; } /** * Sets the music string kept by this pattern. * * @param s * the music string */ public void setMusicString(String musicString) { this.musicString = new StringBuilder(); this.musicString.append(musicString); } /** * Adds to the music string kept by this pattern. * * @param s * the music string to add */ private void appendMusicString(String appendString) { this.musicString.append(appendString); } /** * Returns the music string kept in this pattern * * @return the music string */ public String getMusicString() { return this.musicString.toString(); } /** * Inserts a MusicString before this music string. NOTE - this does not call * fragmentAdded! * * @param musicString * the string to insert */ public void insert(String musicString) { this.musicString.insert(0, " "); this.musicString.insert(0, musicString); } /** * Adds an additional pattern to the end of this pattern. * * @param pattern * the pattern to add */ public void add(Pattern pattern) { fireFragmentAdded(pattern); appendMusicString(" "); appendMusicString(pattern.getMusicString()); } /** * Adds a music string to the end of this pattern. * * @param musicString * the music string to add */ public void add(String musicString) { add(new Pattern(musicString)); } /** * Adds an additional pattern to the end of this pattern. * * @param pattern * the pattern to add */ public void add(Pattern pattern, int numTimes) { for (int i = 0; i < numTimes; i++) { fireFragmentAdded(pattern); appendMusicString(" "); appendMusicString(pattern.getMusicString()); } } /** * Adds a music string to the end of this pattern. * * @param musicString * the music string to add */ public void add(String musicString, int numTimes) { add(new Pattern(musicString), numTimes); } /** * Adds a number of patterns sequentially * * @param musicString * the music string to add * @version 4.0 */ public void add(Pattern... patterns) { for (Pattern pattern : patterns) { add(pattern); } } /** * Adds a number of patterns sequentially * * @param musicString * the music string to add * @version 4.0 */ public void add(String... musicStrings) { for (String string : musicStrings) { add(string); } } /** * Adds an individual element to the pattern. This takes into account the * possibility that the element may be a sequential or parallel note, in * which case no space is placed before it. * * @param element * the element to add */ public void addElement(JFugueElement element) { String elementMusicString = element.getMusicString(); // Don't automatically add a space if this is a continuing note event if ((elementMusicString.charAt(0) == '+') || (elementMusicString.charAt(0) == '_')) { appendMusicString(elementMusicString); } else { appendMusicString(" "); appendMusicString(elementMusicString); fireFragmentAdded(new Pattern(elementMusicString)); } } /** * Sets the title for this Pattern. As of JFugue 4.0, the title is set as a * property with the key Pattern.TITLE * * @param title * the title for this Pattern */ public void setTitle(String title) { setProperty(TITLE, title); } /** * Returns the title of this Pattern As of JFugue 4.0, the title is set as a * property with the key Pattern.TITLE * * @return the title of this Pattern */ public String getTitle() { return getProperty(TITLE); } /** * Get a property on this pattern, such as "author" or "date". * * @version 4.0 */ public String getProperty(String key) { return properties.get(key); } /** * Set a property on this pattern, such as "author" or "date". * * @version 4.0 */ public void setProperty(String key, String value) { properties.put(key, value); } /** * Get all properties set on this pattern, such as "author" or "date". * * @version 4.0 */ public Map<String, String> getProperties() { return properties; } /** * Repeats the music string in this pattern by the given number of times. * Example: If the pattern is "A B", calling <code>repeat(4)</code> will * make the pattern "A B A B A B A B". * * @version 3.0 */ public void repeat(int times) { repeat(null, getMusicString(), times, null); } /** * Only repeats the portion of this music string that starts at the string * index provided. This allows some initial header information to only be * specified once in a repeated pattern. Example: If the pattern is "T0 A B" * , calling <code>repeat(4, 3)</code> will make the pattern * "T0 A B A B A B A B". * * @version 3.0 */ public void repeat(int times, int beginIndex) { String string = getMusicString(); repeat(string.substring(0, beginIndex), string.substring(beginIndex), times, null); } /** * Only repeats the portion of this music string that starts and ends at the * string indices provided. This allows some initial header information and * trailing information to only be specified once in a repeated pattern. * Example: If the pattern is "T0 A B C", calling * <code>repeat(4, 3, 5)</code> will make the pattern "T0 A B A B A B A B C" * . * * @version 3.0 */ public void repeat(int times, int beginIndex, int endIndex) { String string = getMusicString(); repeat(string.substring(0, beginIndex), string.substring(beginIndex, endIndex), times, string.substring(endIndex)); } private void repeat(String header, String repeater, int times, String trailer) { StringBuffer buffy = new StringBuffer(); // Add the header, if it exists if (header != null) { buffy.append(header); } // Repeat and add the repeater for (int i = 0; i < times; i++) { buffy.append(repeater); if (i < times - 1) { buffy.append(" "); } } // Add the trailer, if it exists if (trailer != null) { buffy.append(trailer); } // Create the new Pattern and return it this.setMusicString(buffy.toString()); } /** * Returns a new Pattern that is a subpattern of this pattern. * * @return subpattern of this pattern * @version 3.0 */ public Pattern subPattern(int beginIndex) { return new Pattern(substring(beginIndex)); } /** * Returns a new Pattern that is a subpattern of this pattern. * * @return subpattern of this pattern * @version 3.0 */ public Pattern subPattern(int beginIndex, int endIndex) { return new Pattern(substring(beginIndex, endIndex)); } protected String substring(int beginIndex) { return getMusicString().substring(beginIndex); } protected String substring(int beginIndex, int endIndex) { return getMusicString().substring(beginIndex, endIndex); } public static Pattern loadPattern(File file) throws IOException { StringBuffer buffy = new StringBuffer(); Pattern pattern = new Pattern(); BufferedReader bread = new BufferedReader( new InputStreamReader(new FileInputStream(file))); while (bread.ready()) { String s = bread.readLine(); if ((s != null) && (s.length() > 1)) { if (s.charAt(0) != '#') { buffy.append(" "); buffy.append(s); } else { String key = s.substring(1, s.indexOf(':')).trim(); String value = s.substring(s.indexOf(':') + 1, s.length()) .trim(); if (key.equalsIgnoreCase(TITLE)) { pattern.setTitle(value); } else { pattern.setProperty(key, value); } } } } bread.close(); pattern.setMusicString(buffy.toString()); return pattern; } /** * Saves the pattern as a text file * * @param filename * the filename to save under */ public void savePattern(File file) throws IOException { BufferedWriter out = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); if ((getProperties().size() > 0) || (getTitle() != null)) { out.write("#\n"); if (getTitle() != null) { out.write("# "); out.write("Title: "); out.write(getTitle()); out.write("\n"); } Iterator<String> iter = getProperties().keySet().iterator(); while (iter.hasNext()) { String key = iter.next(); if (!key.equals(TITLE)) { String value = getProperty(key); out.write("# "); out.write(key); out.write(": "); out.write(value); out.write("\n"); } } out.write("#\n"); out.write("\n"); } String musicString = getMusicString(); while (musicString.length() > 0) { if ((musicString.length() > 80) && (musicString.indexOf(' ', 80) > -1)) { int indexOf80ColumnSpace = musicString.indexOf(' ', 80); out.write(musicString.substring(0, indexOf80ColumnSpace)); out.newLine(); musicString = musicString.substring(indexOf80ColumnSpace, musicString.length()); } else { out.write(musicString); musicString = ""; } } out.close(); } /** * Returns a String containing key-value pairs stored in this object's * properties, separated by semicolons and spaces. Values are returned in * the following form: key1: value1; key2: value2; key3: value3 * * @return a String containing key-value pairs stored in this object's * properties, separated by semicolons and spaces */ public String getPropertiesAsSentence() { StringBuilder buddy = new StringBuilder(); Iterator<String> iter = getProperties().keySet().iterator(); while (iter.hasNext()) { String key = iter.next(); String value = getProperty(key); buddy.append(key); buddy.append(": "); buddy.append(value); buddy.append("; "); } String result = buddy.toString(); return result.substring(0, result.length() - 2); // Take off the last // semicolon-space } /** * Returns a String containing key-value pairs stored in this object's * properties, separated by newline characters. * * Values are returned in the following form: key1: value1\n key2: value2\n * key3: value3\n * * @return a String containing key-value pairs stored in this object's * properties, separated by newline characters */ public String getPropertiesAsParagraph() { StringBuilder buddy = new StringBuilder(); Iterator<String> iter = getProperties().keySet().iterator(); while (iter.hasNext()) { String key = iter.next(); String value = getProperty(key); buddy.append(key); buddy.append(": "); buddy.append(value); buddy.append("\n"); } String result = buddy.toString(); return result.substring(0, result.length()); } /** * Changes all timestamp values by the offsetTime passed in. NOTE: This * method is only useful for patterns that have been converted from a MIDI * file. * * @param offsetTime */ public void offset(long offsetTime) { StringBuffer buffy = new StringBuffer(); String[] tokens = getMusicString().split(" "); for (int i = 0; i < tokens.length; i++) { if ((tokens[i].length() > 0) && (tokens[i].charAt(0) == '@')) { String timeNumberString = tokens[i].substring(1, tokens[i].length()); if (timeNumberString.indexOf("[") == -1) { long timeNumber = Long.parseLong(timeNumberString); long newTime = timeNumber + offsetTime; if (newTime < 0) { newTime = 0; } buffy.append("@" + newTime); } else { buffy.append(tokens[i]); } } else { buffy.append(tokens[i]); } buffy.append(" "); } setMusicString(buffy.toString()); } /** * Returns an array of strings representing each token in the Pattern. * * @return */ public String[] getTokens() { StringTokenizer strtok = new StringTokenizer(musicString.toString(), " \n\t"); List<String> list = new ArrayList<String>(); while (strtok.hasMoreTokens()) { String token = strtok.nextToken(); if (token != null) { list.add(token); } } String[] retVal = new String[list.size()]; list.toArray(retVal); return retVal; } /** * Indicates whether the provided musicString is composed of valid elements * that can be parsed by the Parser. * * @param musicString * the musicString to test * @return whether the musicString is valid * @version 3.0 */ // public static boolean isValidMusicString(String musicString) // { // try { // Parser parser = new Parser(); // parser.parse(musicString); // } catch (JFugueException e) // { // return false; // } // return true; // } // // Listeners // /** List of ParserListeners */ protected EventListenerList listenerList = new EventListenerList(); /** * Adds a <code>PatternListener</code>. The listener will receive events * when new parts are added to the pattern. * * @param listener * the listener that is to be notified when new parts are added * to the pattern */ public void addPatternListener(PatternListener l) { listenerList.add(PatternListener.class, l); } /** * Removes a <code>PatternListener</code>. * * @param listener * the listener to remove */ public void removePatternListener(PatternListener l) { listenerList.remove(PatternListener.class, l); } protected void clearPatternListeners() { EventListener[] l = listenerList.getListeners(PatternListener.class); int numListeners = l.length; for (int i = 0; i < numListeners; i++) { listenerList.remove(PatternListener.class, (PatternListener) l[i]); } } /** Tells all PatternListener interfaces that a fragment has been added. */ private void fireFragmentAdded(Pattern fragment) { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == PatternListener.class) { ((PatternListener) listeners[i + 1]).fragmentAdded(fragment); } } } /** * @version 3.0 */ @Override public String toString() { return getMusicString(); } public static final String TITLE = "Title"; }