//----------------------------------------------------------------------------//
// //
// C h o r d I n f o //
// //
//----------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr"> //
// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
// This software is released under the GNU General Public License. //
// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
//----------------------------------------------------------------------------//
// </editor-fold>
package omr.score.entity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import omr.score.entity.ChordInfo.Degree.DegreeType;
import static omr.score.entity.ChordInfo.Kind.Type.*;
import omr.score.entity.Note.Step;
import static omr.util.RegexUtil.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class {@code ChordInfo} is a formatted piece of text that
* describes a chord symbol such as F#m7, A(9) or BMaj7/D#.
* This class is organized according to the target MusicXML harmony element.
*
* <p>TODO: Add support for degree subtract (besides add and alter) </p>
*
* <p>TODO: Add support for classical functions (besides root)</p>
*
* <p>TODO: Add support for French steps: Do, Ré, Mi, etc.</p>
*
* @author Hervé Bitteur
*/
public class ChordInfo
{
//~ Static fields/initializers ---------------------------------------------
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(ChordInfo.class);
/** Unicode value for <b>flat</b> sign: {@value}. */
public static final String FLAT = "\u266D";
/** Unicode value for <b>natural</b> sign: {@value}. (not used) */
public static final String NATURAL = "\u266E";
/** Unicode value for <b>sharp</b> sign: {@value}. */
public static final String SHARP = "\u266F";
/** Unicode value for <b>triangle</b> sign: {@value}. */
public static final String DELTA = "\u25B3";
// String constants to be used by their names to avoid any typo.
// Their values are not relevant, but must be unique.
private static final String ROOT_STEP = "rootStep";
private static final String ROOT_ALTER = "rootAlter";
private static final String BASS_STEP = "bassStep";
private static final String BASS_ALTER = "bassAlter";
private static final String MAJ = "maj";
private static final String MIN = "min";
private static final String AUG = "aug";
private static final String DIM = "dim";
private static final String HDIM = "hdim";
private static final String DEGS = "degs";
private static final String SUS = "sus";
private static final String KIND = "kind";
private static final String PARS = "pars";
private static final String PMAJ7 = "pmaj7";
private static final String DEG_VALUE = "degValue";
private static final String DEG_ALTER = "degAlter";
/** Pattern for any step. */
private static final String STEP_CLASS = "[A-G]";
/** Pattern for root value. A, A# or Ab */
private static final String rootPat = group(ROOT_STEP, STEP_CLASS)
+ group(ROOT_ALTER, Alter.CLASS) + "?";
/** Pattern for bass value, if any. /A, /A# or /Ab */
private static final String bassPat = "(/"
+ group(BASS_STEP, STEP_CLASS)
+ group(BASS_ALTER, Alter.CLASS) + "?"
+ ")";
/** Pattern for major indication. M, maj or DELTA */
private static final String majPat = group(
MAJ,
"(M|[Mm][Aa][Jj]|" + DELTA + ")");
/** Pattern for minor indication. min, m or - */
private static final String minPat = group(MIN, "(m|[Mm][Ii][Nn]|-)");
/** Pattern for augmented indication. aug or + */
private static final String augPat = group(AUG, "([Aa][Uu][Gg]|\\+)");
/** Pattern for diminished indication. dim or ° */
private static final String dimPat = group(DIM, "([Dd][Ii][Mm]|°)");
/** Pattern for half-diminished indication. o with a slash */
private static final String hdimPat = group(HDIM, "\u00F8");
/** Pattern for any of the indication alternatives. (except sus) */
private static final String modePat = "(" + majPat + "|" + minPat + "|"
+ augPat + "|" + dimPat + "|"
+ hdimPat + ")";
/** Pattern for (maj7) in min(maj7) = MAJOR_MINOR. */
private static final String parMajPat = "(\\("
+ group(
PMAJ7,
"(M|[Mm][Aa][Jj]|" + DELTA + ")7")
+ "\\))";
/** Pattern for any degree value. 5, 6, 7, 9, 11 or 13 */
private static final String DEG_CLASS = "(5|6|7|9|11|13)";
/** Pattern for a sequence of degrees. */
private static final String degsPat = group(
DEGS,
DEG_CLASS + "(" + Alter.CLASS + DEG_CLASS + ")?");
/** Pattern for a suspended indication. sus2 or sus4 */
private static final String susPat = group(SUS, "([Ss][Uu][Ss][24])");
/** Pattern for the whole kind value. */
private static final String kindPat = group(
KIND,
modePat + "?"
+ parMajPat + "?"
+ degsPat + "?"
+ susPat + "?");
/** Pattern for parenthesized degrees if any. (6), (#9), (#11b13) */
private static final String parPat = "(\\("
+ group(
PARS,
Alter.CLASS + "?" + DEG_CLASS + "(" + Alter.CLASS + DEG_CLASS + ")*")
+ "\\))";
/** Uncompiled patterns for whole chord symbol. */
private static final String[] raws = new String[]{
rootPat
+ kindPat + "?"
+ parPat + "?"
+ bassPat + "?"
// TODO: add a pattern for functions
};
/** Compiled patterns for whole chord symbol. */
private static List<Pattern> patterns = null;
/** Pattern for one degree. (in a sequence of degrees) */
private static final String degPat = group(DEG_ALTER, Alter.CLASS) + "?"
+ group(DEG_VALUE, DEG_CLASS);
/** Compiled pattern for one degree. */
private static final Pattern degPattern = Pattern.compile(degPat);
//~ Instance fields --------------------------------------------------------
//
/** The whole underlying chord text. (meant to ease debugging) */
private final String content;
/** Root. */
private final Pitch root;
/** Kind. */
private final Kind kind;
/** Bass, if any. */
private final Pitch bass;
/** Degrees, if any. */
private final List<Degree> degrees;
//~ Constructors -----------------------------------------------------------
//-----------//
// ChordInfo //
//-----------//
/**
* Creates a new ChordInfo object, with all parameters.
*
* @param content the full underlying text
* @param root root of the chord
* @param kind type of the chord
* @param bass bass of the chord, or null
* @param degrees additions / subtractions / alterations if any
*/
public ChordInfo (String content,
Pitch root,
Kind kind,
Pitch bass,
List<Degree> degrees)
{
this.content = content;
this.root = root;
this.kind = kind;
this.bass = bass;
this.degrees = degrees;
}
//-----------//
// ChordInfo //
//-----------//
/**
* Creates a new ChordInfo object, with all parameters.
*
* @param text the full underlying text
* @param root root of the chord
* @param kind type of the chord
* @param bass bass of the chord, or null
* @param degrees additions / subtractions / alterations if any
*/
public ChordInfo (String text,
Pitch root,
Kind kind,
Pitch bass,
Degree... degrees)
{
this(text, root, kind, bass, Arrays.asList(degrees));
}
//-----------//
// ChordInfo //
//-----------//
/**
* Convenient constructor that creates a new ChordInfo object with
* no bass information.
*
* @param text the full underlying text
* @param root root of the chord
* @param kind type of the chord
* @param degrees additions / subtractions / alterations if any
*/
public ChordInfo (String text,
Pitch root,
Kind kind,
Degree... degrees)
{
this(text, root, kind, null, Arrays.asList(degrees));
}
//~ Methods ----------------------------------------------------------------
//
//--------//
// create //
//--------//
/**
* Convenient method to try to build a ChordInfo instance from a
* provided piece of text.
*
* @param text the precise text of the chord symbol
* @return a populated ChordInfo instance if successful, null otherwise
*/
public static ChordInfo create (String text)
{
for (Pattern pattern : getPatterns()) {
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
// Root
Pitch root = Pitch.create(
getGroup(matcher, ROOT_STEP),
getGroup(matcher, ROOT_ALTER));
// Degrees
String degStr = getGroup(matcher, DEGS);
List<Degree> degrees = Degree.createList(degStr, null);
Degree firstDeg = !degrees.isEmpty() ? degrees.get(0) : null;
String firstDegStr = firstDeg != null
? Integer.toString(degrees.get(0).value) : "";
// (maj7) special stuff
String pmaj7 = standard(matcher, PMAJ7);
// Kind
Kind kind = Kind.create(matcher, firstDegStr + pmaj7);
// Bass
Pitch bass = Pitch.create(
getGroup(matcher, BASS_STEP),
getGroup(matcher, BASS_ALTER));
if (firstDeg != null
&& (kind.type != SUSPENDED_FOURTH)
&& (kind.type != SUSPENDED_SECOND)) {
// Remove first degree
degrees.remove(firstDeg);
}
// Degrees in parentheses
String parStr = getGroup(matcher, PARS);
degrees.addAll(Degree.createList(parStr, firstDeg));
return new ChordInfo(text, root, kind, bass, degrees);
}
}
logger.debug("No pattern match for chord text {}", text);
return null;
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("{");
sb.append(getClass().getSimpleName());
sb.append(" '")
.append(content)
.append("'");
sb.append(" root:")
.append(root);
sb.append(" kind:")
.append(kind);
if (bass != null) {
sb.append(" bass:")
.append(bass);
}
for (Degree degree : degrees) {
sb.append(" deg:")
.append(degree);
}
sb.append("}");
return sb.toString();
}
//-------------//
// getPatterns //
//-------------//
/**
* Compile if needed, and provide the patterns ready to use.
*
* @return the compiled patterns
*/
private static List<Pattern> getPatterns ()
{
if (patterns == null) {
patterns = new ArrayList<>();
for (String raw : raws) {
patterns.add(Pattern.compile(raw));
}
}
return patterns;
}
//----------//
// standard //
//----------//
/**
* Standardize an input sequence, by returning the standard
* value when the token is valid.
*
* @param matcher the matcher
* @param name the group name which is also the standard value to return
* if the token is valid
* @return standard value, or empty string
*/
private static String standard (Matcher matcher,
String name)
{
String token = getGroup(matcher, name);
return token.isEmpty() ? "" : name;
}
/**
* @return the content
*/
public String getContent ()
{
return content;
}
/**
* @return the root
*/
public Pitch getRoot ()
{
return root;
}
/**
* @return the kind
*/
public Kind getKind ()
{
return kind;
}
/**
* @return the bass
*/
public Pitch getBass ()
{
return bass;
}
/**
* @return the degrees
*/
public List<Degree> getDegrees ()
{
return degrees;
}
//~ Inner Classes ----------------------------------------------------------
//
//-------//
// Alter //
//-------//
/**
* Handling of alteration indication (flat, sharp or nothing).
* The class accepts both number (#) and real sharp sign, as well as both
* (b) and real flat sign.
*/
public static class Alter
{
//~ Static fields/initializers -----------------------------------------
/** Alter class. */
private static final String CLASS = "["
+ FLAT
+ "b"
+ SHARP
+ "#"
+ "]";
//~ Methods ------------------------------------------------------------
/**
* Convert sharp/flat/empty sign to Integer.
*
* @param str the alteration sign
* @return the Integer value, null if input string has unexpected value
*/
private static Integer toAlter (String str)
{
switch (str) {
case SHARP:
case "#":
return 1;
case FLAT:
case "b":
return -1;
case "":
return 0;
default:
return null;
}
}
/**
* Convert an alteration Integer value to the corresponding sign.
*
* @param alter Integer value, perhaps null
* @return the sign string, perhaps empty but not null
*/
private static String toString (Integer alter)
{
if (alter == null) {
return "";
} else {
return (alter == 1) ? "#" : ((alter == -1) ? "b" : "");
}
}
}
//--------//
// Degree //
//--------//
/**
* Handling of degree information.
* <p>TODO: subtraction is not yet handled
*/
public static class Degree
{
//~ Enumerations -------------------------------------------------------
public static enum DegreeType
{
//~ Enumeration constant initializers ------------------------------
ADD, ALTER, SUBTRACT;
}
//~ Instance fields ----------------------------------------------------
//
/** nth value of the degree, wrt the chord root. */
public final int value;
/** Alteration, if any. */
public final int alter;
/** Which operation is performed. */
public final DegreeType type;
/** Specific text display for degree operation, if any. */
public final String text;
//~ Constructors -------------------------------------------------------
//
public Degree (int value,
int alter,
DegreeType type)
{
this(value, alter, type, "");
}
public Degree (int value,
int alter,
DegreeType type,
String text)
{
this.value = value;
this.alter = alter;
this.type = type;
this.text = text;
}
//~ Methods ------------------------------------------------------------
//
/**
* Build a sequence of Degree instances from the provided string
*
* @param str the provided string, without parentheses, such as
* 7b13
* @param dominant the chord dominant degree, if any, otherwise null
* @return the list of degrees
*/
public static List<Degree> createList (String str,
Degree dominant)
{
List<Degree> degrees = new ArrayList<>();
if (str == null || str.isEmpty()) {
return degrees;
}
// Loop on occurrences of the one-degree pattern
Matcher matcher = degPattern.matcher(str);
while (matcher.find()) {
// Deg value
String degStr = getGroup(matcher, DEG_VALUE);
final int deg = Integer.decode(degStr);
// Deg type: 'add' or 'alter'
// TODO: handle 'subtract' as well
final DegreeType type;
if (dominant != null && dominant.value > deg) {
type = DegreeType.ALTER;
} else if (deg <= 5) {
type = DegreeType.ALTER;
} else {
type = DegreeType.ADD;
}
// Deg alter
String altStr = getGroup(matcher, DEG_ALTER);
final int alter = Alter.toAlter(altStr);
degrees.add(new Degree(deg, alter, type, ""));
}
return degrees;
}
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("(");
sb.append(value);
sb.append(Alter.toString(alter));
sb.append(" ")
.append(type);
if (!text.isEmpty()) {
sb.append(" '")
.append(text)
.append("'");
}
sb.append(")");
return sb.toString();
}
}
//------//
// Kind //
//------//
/**
* Handling of kind (aka quality) chord information.
*/
public static class Kind
{
//~ Enumerations -------------------------------------------------------
public static enum Type
{
//~ Enumeration constant initializers ------------------------------
MAJOR,
MINOR,
AUGMENTED,
DIMINISHED,
DOMINANT,
MAJOR_SEVENTH,
MINOR_SEVENTH,
DIMINISHED_SEVENTH,
AUGMENTED_SEVENTH,
HALF_DIMINISHED,
MAJOR_MINOR, // min(maj7) = minor 3rd + perfect 5th + major 7th
MAJOR_SIXTH,
MINOR_SIXTH,
DOMINANT_NINTH,
MAJOR_NINTH,
MINOR_NINTH,
DOMINANT_11_TH,
MAJOR_11_TH,
MINOR_11_TH,
DOMINANT_13_TH,
MAJOR_13_TH,
MINOR_13_TH,
SUSPENDED_SECOND,
SUSPENDED_FOURTH,
// NEAPOLITAN,
// ITALIAN,
// FRENCH,
// GERMAN,
// PEDAL,
// POWER,
// TRISTAN,
OTHER,
NONE;
}
//~ Instance fields ----------------------------------------------------
//
/** Precise type of kind. (subset of the 33 Music XML values) */
public final Type type;
/** Flag to signal parenthesis, if any. */
public final boolean paren;
/** Flag to signal use of symbol, if any. */
public final boolean symbol;
/** Exact display text for the chord kind. (For example min vs m) */
public final String text;
//~ Constructors -------------------------------------------------------
public Kind (Type type)
{
this(type, "", false, false);
}
public Kind (Type type,
String text)
{
this(type, text, false, false);
}
public Kind (Type type,
String text,
boolean symbol)
{
this(type, text, symbol, false);
}
public Kind (Type type,
String text,
boolean symbol,
boolean paren)
{
this.type = type;
this.paren = paren;
this.text = text;
this.symbol = symbol;
}
//~ Methods ------------------------------------------------------------
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("{");
sb.append(type);
if (!text.isEmpty()) {
sb.append(" '")
.append(text)
.append("'");
}
if (paren) {
sb.append(" parens");
}
if (symbol) {
sb.append(" symbol");
}
sb.append("}");
return sb.toString();
}
/**
* Create proper Kind object from a provided matcher, augmented
* by dominant string if any.
*
* @param matcher matcher on input string
* @param dominant dominant information if any, empty string otherwise
* @return Kind instance, or null if failed
*/
private static Kind create (Matcher matcher,
String dominant)
{
String kindStr = getGroup(matcher, KIND);
String parStr = getGroup(matcher, PARS);
boolean paren = !parStr.isEmpty();
// Check for suspended first
String susStr = getGroup(matcher, SUS);
switch (susStr.toLowerCase()) {
case "sus2":
return new Kind(SUSPENDED_SECOND, kindStr, false, paren);
case "sus4":
return new Kind(SUSPENDED_FOURTH, kindStr, false, paren);
case "": // Fall through
}
// Then check for other combinations
final String str = standard(matcher, MIN)
+ standard(matcher, MAJ)
+ standard(matcher, AUG)
+ standard(matcher, DIM)
+ standard(matcher, HDIM)
+ dominant;
Type type = typeOf(str);
// Special case for Triangle sign => maj7 rather than major
if (type == MAJOR && getGroup(matcher, MAJ).equals(DELTA)) {
type = MAJOR_SEVENTH;
}
// Use of symbol?
boolean symbol = getGroup(matcher, MAJ).equals(DELTA)
|| getGroup(matcher, MIN).equals("-")
|| getGroup(matcher, AUG).equals("+");
return (type != null) ? new Kind(type, kindStr, symbol, paren) : null;
}
/**
* Convert a provided string to proper Type value.
*
* @param str provided string, assumed to contain only 'standard' values
* @return the corresponding type, or null if none found
*/
private static Type typeOf (String str)
{
switch (str) {
case "":
case MAJ:
return MAJOR;
case MIN:
return MINOR;
case AUG:
return AUGMENTED;
case DIM:
return DIMINISHED;
case "7":
return DOMINANT;
case MAJ + "7":
return MAJOR_SEVENTH;
case MIN + "7":
return MINOR_SEVENTH;
case DIM + "7":
return DIMINISHED_SEVENTH;
case AUG + "7":
return AUGMENTED_SEVENTH;
case HDIM:
return HALF_DIMINISHED;
case MIN + PMAJ7:
return MAJOR_MINOR;
case MAJ + "6":
case "6":
return MAJOR_SIXTH;
case MIN + "6":
return MINOR_SIXTH;
case "9":
return DOMINANT_NINTH;
case MAJ + "9":
return MAJOR_NINTH;
case MIN + "9":
return MINOR_NINTH;
case "11":
return DOMINANT_11_TH;
case MAJ + "11":
return MAJOR_11_TH;
case MIN + "11":
return MINOR_11_TH;
case "13":
return DOMINANT_13_TH;
case MAJ + "13":
return MAJOR_13_TH;
case MIN + "13":
return MINOR_13_TH;
default:
// Nota: Thanks to regexp match, this should not happen
logger.warn("No kind type for {}", str);
return null;
}
}
}
//-------//
// Pitch //
//-------//
/**
* General handling of pitch information, used by root and bass.
*/
public static class Pitch
{
//~ Instance fields ----------------------------------------------------
/** Related step. */
public final Note.Step step;
/** Alteration, if any. */
public final Integer alter;
//~ Constructors -------------------------------------------------------
public Pitch (Step step,
Integer alter)
{
this.step = step;
this.alter = alter;
}
public Pitch (Step step)
{
this(step, 0);
}
//~ Methods ------------------------------------------------------------
/**
* Create a Pitch object from provided step and alter strings
*
* @param stepStr provided step string
* @param alterStr provided alteration string
* @return Pitch instance, or null if failed
*/
public static Pitch create (String stepStr,
String alterStr)
{
stepStr = stepStr.trim();
alterStr = alterStr.trim();
if (!stepStr.isEmpty()) {
return new Pitch(
Note.Step.valueOf(stepStr),
Alter.toAlter(alterStr));
} else {
return null;
}
}
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder();
sb.append(step);
sb.append(Alter.toString(alter));
return sb.toString();
}
}
/*
* major : X|maj ................ : 1 - 3 - 5
* minor : m|min ................ : 1 - b3 - 5
* augmented : +|aug............. : 1 - 3 - #5
* diminished : °|dim............ : 1 - b3 - b5
* dominant : 7 ................. : 1 - 3 - 5 - b7
* major-seventh : M7|maj7....... : 1 - 3 - 5 - 7
* minor-seventh : m7|min7....... : 1 - b3 - 5 - b7
* diminished-seventh : dim7..... : 1 - b3 - b5 -bb7
* augmented-seventh :+7|7+5|aug7 : 1 - 3 - #5 - b7
* half-diminished :-7b5|m7b5.... : 1 - b3 - b5 - b7
* major-minor : min(maj7)....... : 1 - b3 - 5 - 7
* major-sixth : 6 .............. : 1 - 3 - 5 - 6
* minor-sixth : m6|min6......... : 1 - b3 - 5 - 6
* dominant-ninth : 9............ : 1 - 3 - 5 - b7 - 9
* major-ninth : M9|maj9......... : 1 - 3 - 5 - 7 - 9
* minor-ninth : m9|min9......... : 1 - b3 - 5 - b7 - 9
* dominant-11th : 11............ : 1 - 3 - 5 - b7 - 9 - 11
* major-11th : M11|maj11........ : 1 - 3 - 5 - 7 - 9 - 11
* minor-11th : m11|min11........ : 1 - b3 - 5 - b7 - 9 - 11
* dominant-13th : 13............ : 1 - 3 - 5 - b7 - 9 - 11 - 13
* major-13th : M13|maj13........ : 1 - 3 - 5 - 7 - 9 - 11 - 13
* minor-13th : m13|min13........ : 1 - b3 - 5 - b7 - 9 - 11 - 13
* suspended-second : sus2....... : 1 - 2 - 5
* suspended-fourth : sus4....... : 1 - 4 - 5
* suspended-ninth : sus9........ : 1 - 4 - 5 - b7 - 9
* Neapolitan : ................. :
* Italian : .................... :
* French : ..................... :
* German : ..................... :
* pedal : ...................... :
* power : ...................... :
* Tristan : .................... :
* other : ...................... :
* none : ....................... :
*/
}