/*
* 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.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
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 implements Serializable
{
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(new String(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 FileWriter(file));
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 = new Long(timeNumberString).longValue();
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
*/
public String toString()
{
return getMusicString();
}
public static final String TITLE = "Title";
}