//----------------------------------------------------------------------------//
// //
// S l o t B u i l d e r //
// //
//----------------------------------------------------------------------------//
// <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;
import omr.constant.ConstantSet;
import omr.glyph.facets.Glyph;
import omr.math.Rational;
import omr.score.entity.Beam;
import omr.score.entity.BeamGroup;
import omr.score.entity.Chord;
import omr.score.entity.Measure;
import omr.score.entity.Note;
import omr.score.entity.ScoreSystem;
import omr.score.entity.Slot;
import omr.score.entity.Voice;
import static omr.score.SlotBuilder.Rel.*;
import omr.sheet.Scale;
import omr.util.TreeNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Class {@code SlotBuilder} handles the building of the sequence of
* slots for every measure within a given system.
*
* <p>The key point is to determine when two chords should belong or
* not to the same time slot:
* <ul>
* <li>Chords that share a common stem belong to the same slot.</li>
* <li>Chords that originate from the same physical glyph belong to the same
* slot. (for example a note head with one stem on left and one stem on right
* leads to two overlapping logical chords)</li>
* <li>Chords within the same beam group, but not on the same stem, cannot
* belong to the same slot.</li>
* <li>Similar abscissa is only an indication, it is not always reliable.</li>
* </ul>
* </p>
*
* @author Hervé Bitteur
*/
public class SlotBuilder
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(SlotBuilder.class);
//~ Instance fields --------------------------------------------------------
//
/** The containing system. */
private final ScoreSystem system;
/** Scale-dependent parameters. */
private final Parameters params;
//~ Constructors -----------------------------------------------------------
//
//-------------//
// SlotBuilder //
//-------------//
/**
* Creates a new SlotBuilder object.
*
* @param system the containing system
*/
public SlotBuilder (ScoreSystem system)
{
this.system = system;
params = new Parameters(system.getScale());
}
//~ Methods ----------------------------------------------------------------
//
//------------//
// buildSlots //
//------------//
/**
* Allocate the proper sequence of slots for the chords of the
* provided measure.
*
* @param measure the provided measure
*/
public void buildSlots (Measure measure)
{
MeasureChecker checker = new MeasureChecker(measure);
// We work on the population of chords, using inter-chords constraints
checker.buildRelationships();
checker.buildSlots();
checker.refineVoices();
}
//-----//
// Rel //
//-----//
/**
* Describes the oriented relationship between two chords.
*/
protected static enum Rel
{
/**
* Strongly before.
* Stem-located before in the same beam group.
* Abscissa-located before the vertically overlapping chord.
* Important abscissa difference in different staves.
*/
BEFORE("B"),
//
/** Strongly after.
* Stem-located after in the same beam group.
* Abscissa-located after the vertically overlapping chord.
* Important abscissa difference in different staves.
*/
AFTER("A"),
//
/**
* Strongly equal.
* Identical thanks to an originating glyph in common.
* Adjacency detected in same staff.
*/
EQUAL("="),
//
/**
* Weakly close.
* No important difference, use other separation criteria.
*/
CLOSE("?");
private final String mnemo;
Rel (String mnemo)
{
this.mnemo = mnemo;
}
@Override
public String toString ()
{
return mnemo;
}
}
//~ Inner Classes ----------------------------------------------------------
//
//----------------//
// MeasureChecker //
//----------------//
private class MeasureChecker
{
/** The measure at hand. */
private final Measure measure;
/** Inter-chord relationships for the current measure. */
private Rel[][] matrix;
/**
* A chord comparator based on inter-chord relationships
* and then on startTime when known.
*/
private Comparator<Chord> byRel = new Comparator<Chord>()
{
@Override
public int compare (Chord c1,
Chord c2)
{
Rel rel = getRel(c1, c2);
if (rel == null) {
return 0;
} else {
switch (rel) {
case BEFORE:
return -1;
case AFTER:
return 1;
default:
// Use start time difference when known
if (c1.getStartTime() != null
&& c2.getStartTime() != null) {
return c1.getStartTime().compareTo(c2.getStartTime());
} else {
return 0;
}
}
}
}
};
//----------------//
// MeasureChecker //
//----------------//
public MeasureChecker (Measure measure)
{
this.measure = measure;
}
//--------------------//
// buildRelationships //
//--------------------//
/**
* Compute the matrix of inter-chords relationships.
*/
public void buildRelationships ()
{
// Sort measure chords by abscissa
Collections.sort(measure.getChords(), Chord.byNodeAbscissa);
// Allocate matrix of inter-chord relartionships
int chordCount = measure.getChords().size();
matrix = new Rel[chordCount][chordCount];
// BeamGroup-based relationships
inspectBeams();
// Seed-based relationships
inspectSeeds();
// Location-based relationships
inspectLocations();
if (logger.isDebugEnabled()) {
dumpRelationships();
}
}
//--------------//
// inspectBeams //
//--------------//
/**
* Derive some inter-chord relationships from BeamGroup's.
*/
private void inspectBeams ()
{
// BeamGroup-based relationships
for (BeamGroup group : measure.getBeamGroups()) {
Set<Chord> chordSet = new HashSet<>();
for (Beam beam : group.getBeams()) {
chordSet.addAll(beam.getChords());
}
List<Chord> groupChords = new ArrayList<>(chordSet);
Collections.sort(groupChords, Chord.byAbscissa);
Chord prevChord = null;
for (Chord chord : groupChords) {
if (prevChord != null) {
setRel(prevChord, chord, BEFORE);
setRel(chord, prevChord, AFTER);
}
prevChord = chord;
}
}
}
//--------------//
// inspectSeeds //
//--------------//
/**
* Derive some inter-chords relationships from shared seeds.
*/
private void inspectSeeds ()
{
final int chordCount = measure.getChords().size();
int index = 0;
for (TreeNode pn : measure.getChords()) {
index++;
Chord ch1 = (Chord) pn;
if (ch1.isWholeDuration()) {
continue;
}
for (TreeNode n : measure.getChords().subList(index, chordCount)) {
Chord ch2 = (Chord) n;
if (ch2.isWholeDuration() || getRel(ch1, ch2) != null) {
continue;
}
// Check for common note glyph
if (haveCommonSeed(ch1, ch2)) {
setRel(ch1, ch2, EQUAL);
setRel(ch2, ch1, EQUAL);
}
}
}
}
//------------------//
// inspectLocations //
//------------------//
/**
* Derive the missing inter-chord relationships from chords
* relative locations.
*/
private void inspectLocations ()
{
final int chordCount = measure.getChords().size();
List<ChordPair> adjacencies = new ArrayList<>();
int index = 0;
for (TreeNode pn : measure.getChords()) {
index++;
Chord ch1 = (Chord) pn;
if (ch1.isWholeDuration()) {
continue;
}
Rectangle box1 = ch1.getBox();
for (TreeNode n : measure.getChords().subList(index, chordCount)) {
Chord ch2 = (Chord) n;
if (ch2.isWholeDuration() || getRel(ch1, ch2) != null) {
continue;
}
// Check y overlap
Rectangle box2 = ch2.getBox();
int yOverlap = Math.min(box1.y + box1.height, box2.y + box2.height)
- Math.max(box1.y, box2.y);
if (yOverlap > 0) {
// Boxes overlap vertically
if (areAdjacent(ch1, ch2)) {
setRel(ch1, ch2, EQUAL);
setRel(ch2, ch1, EQUAL);
adjacencies.add(new ChordPair(ch1, ch2));
} else {
if (ch1.getCenter().x <= ch2.getCenter().x) {
setRel(ch1, ch2, BEFORE);
setRel(ch2, ch1, AFTER);
} else {
setRel(ch2, ch1, BEFORE);
setRel(ch1, ch2, AFTER);
}
}
} else {
// Boxes do not overlap vertically
int dx = Math.abs(ch1.getCenter().x - ch2.getCenter().x);
if (dx <= params.maxSlotDx) {
setRel(ch1, ch2, CLOSE);
setRel(ch2, ch1, CLOSE);
} else {
if (ch1.getCenter().x <= ch2.getCenter().x) {
setRel(ch1, ch2, BEFORE);
setRel(ch2, ch1, AFTER);
} else {
setRel(ch2, ch1, BEFORE);
setRel(ch1, ch2, AFTER);
}
}
}
}
}
// Process detected adjacencies
if (!adjacencies.isEmpty()) {
logger.debug("Adjacencies: {}", adjacencies);
for (ChordPair pair : adjacencies) {
// Since ch1 ~ ch2, all neighbors of ch1 ~ neighbors of ch2
Set<Chord> n1 = getClosure(pair.one);
Set<Chord> n2 = getClosure(pair.two);
for (Chord ch1 : n1) {
for (Chord ch2 : n2) {
if (ch1 != ch2) {
if (getRel(ch1, ch2) != EQUAL) {
setRel(ch1, ch2, CLOSE);
}
if (getRel(ch2, ch1) != EQUAL) {
setRel(ch2, ch1, CLOSE);
}
}
}
}
}
}
}
//-------------//
// areAdjacent //
//-------------//
private boolean areAdjacent (Chord ch1,
Chord ch2)
{
final Rectangle box1 = ch1.getBox();
final Rectangle box2 = ch2.getBox();
// Check horizontal void gap
final int xGap = Math.max(box1.x, box2.x)
- Math.min(box1.x + box1.width, box2.x + box2.width);
if (xGap > params.maxAdjacencyGap) {
return false;
}
if (xGap < 0) {
return true;
}
if (ch1.getStem() != null
&& ch2.getStem() != null) {
// If they share the same stem -> true
if (ch1.getStem() == ch2.getStem()) {
return true;
}
// If stem directions are identical -> false
if (ch1.getStemDir() == ch2.getStemDir()) {
return false;
}
// If beam on each side -> false (different groups!)
if (!ch1.getBeams().isEmpty()
&& !ch2.getBeams().isEmpty()) {
return false;
}
// Check abscissa gap between stems
if (Math.abs(ch1.getHeadLocation().x - ch2.getHeadLocation().x)
> params.maxSlotDx) {
return false;
}
// All tests are OK
return true;
} else {
return false;
}
}
//-------------------//
// dumpRelationships //
//-------------------//
private void dumpRelationships ()
{
logger.info(measure.getContextString());
// List BeamGroups
for (BeamGroup group : measure.getBeamGroups()) {
logger.info(" {}", group);
}
// List chords relationships
StringBuilder sb = new StringBuilder(" ");
for (int ix = 0; ix < matrix.length; ix++) {
sb.append(String.format(" %2d", ix + 1));
}
for (int iy = 0; iy < matrix.length; iy++) {
Rel[] line = matrix[iy];
sb.append("\n");
sb.append(String.format("%2d", iy + 1));
for (int ix = 0; ix < matrix.length; ix++) {
sb.append(" ");
Rel rel = line[ix];
if (rel != null) {
sb.append(rel);
} else {
sb.append(".");
}
}
// Append chord description
sb.append(" ").append(getChord(iy + 1));
}
logger.info("\n{}", sb);
}
//------------//
// getClosure //
//------------//
private Set<Chord> getClosure (Chord chord)
{
Set<Chord> closes = new LinkedHashSet<>();
closes.add(chord);
for (TreeNode cn : measure.getChords()) {
Chord ch = (Chord) cn;
Rel rel1 = getRel(chord, ch);
Rel rel2 = getRel(ch, chord);
if (rel1 == CLOSE || rel1 == EQUAL
|| rel2 == CLOSE || rel2 == EQUAL) {
closes.add(ch);
}
}
return closes;
}
//----------//
// getChord //
//----------//
/**
* Retrieve a chord instance, knowing its id (in the measure).
*
* @param id the chord id
* @return the chord instance, or null
*/
private Chord getChord (int id)
{
for (TreeNode cn : measure.getChords()) {
Chord chord = (Chord) cn;
if (chord.getId() == id) {
return chord;
}
}
return null;
}
//----------------//
// haveCommonSeed //
//----------------//
/**
* Check whether the provided chords have a common seed glyph.
* If so, they must share the same time slot and must be in separate
* voices.
*
* @param ch1 first provided chord
* @param ch2 second provided chord
* @return true if there is a common seed
*/
private boolean haveCommonSeed (Chord ch1,
Chord ch2)
{
// use Chord -> Notes -> Glyph(s) and check for common glyph
Set<Glyph> seeds = new HashSet<>();
for (TreeNode nn : ch1.getNotes()) {
Note note = (Note) nn;
seeds.addAll(note.getGlyphs());
}
for (TreeNode nn : ch2.getNotes()) {
Note note = (Note) nn;
for (Glyph glyph : note.getGlyphs()) {
if (seeds.contains(glyph)) {
return true;
}
}
}
return false;
}
//--------//
// setRel //
//--------//
private void setRel (Chord from,
Chord to,
Rel rel)
{
matrix[from.getId() - 1][to.getId() - 1] = rel;
}
//--------//
// getRel //
//--------//
private Rel getRel (Chord from,
Chord to)
{
return matrix[from.getId() - 1][to.getId() - 1];
}
//------------//
// buildSlots //
//------------//
/**
* Build the measure time slots, using the inter-chord
* relationships and the chords durations.
*/
private void buildSlots ()
{
// The 'actives' collection gathers the chords that are "active"
// (not terminated) at the time slot being considered. Initially, it
// contains just the whole chords.
List<Chord> actives = new ArrayList<>(measure.getWholeChords());
Collections.sort(actives, Chord.byAbscissa);
// Create voices for whole chords
for (Chord chord : actives) {
chord.setStartTime(Rational.ZERO);
Voice.createWholeVoice(chord);
}
// List of chords assignable, but not yet assigned to a slot
List<Chord> pendings = new ArrayList<>();
for (TreeNode pn : measure.getChords()) {
Chord chord = (Chord) pn;
if (!chord.isWholeDuration()) {
pendings.add(chord);
}
}
// Assign chords to time slots
while (!pendings.isEmpty()) {
// Hosting time slot
Rational startTime = computeStartTime(actives);
dump("actives", actives);
// Chords ending here, with their voice available for the slot
List<Chord> freeEndings = new ArrayList<>();
// Chords ending here, with voice not available (beam group)
List<Chord> endings = new ArrayList<>();
for (Chord chord : actives) {
// Look for chord that finishes at the slot at hand
if (!chord.isWholeDuration()
&& (chord.getEndTime().compareTo(startTime) <= 0)) {
// Make sure voice is really available
BeamGroup group = chord.getBeamGroup();
if ((group != null) && (chord != group.getLastChord())) {
// Group continuation
endings.add(chord);
} else if (!chord.getFollowingTiedChords().isEmpty()) {
// Tie continuation
endings.add(chord);
} else {
freeEndings.add(chord);
}
}
}
dump("freeEndings", freeEndings);
dump("endings", endings);
// Which pending chords should join the slot?
dump("pendings", pendings);
List<Chord> incomings = retrieveIncomingChords(pendings);
dump("incomings", incomings);
Slot slot = new Slot(measure);
measure.getSlots().add(slot);
slot.setChords(incomings);
slot.setStartTime(startTime);
// Determine the voice of each chord of the slot
slot.buildVoices(endings);
// Update for next iteration
pendings.removeAll(incomings);
actives.removeAll(freeEndings);
actives.removeAll(endings);
actives.addAll(incomings);
}
}
//------//
// dump //
//------//
private void dump (String label,
Collection<Chord> chords)
{
if (logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
sb.append(label)
.append("[");
for (Chord chord : chords) {
sb.append("#").append(chord.getId());
}
sb.append("]");
logger.debug(sb.toString());
}
}
//------------------------//
// retrieveIncomingChords //
//------------------------//
/**
* Among the pendings chords, select the ones that would fit
* into the next time slot.
*
* @param pendings the sequence of noy yet assigned chords
* @return the collection of chords for next slot
*/
private List<Chord> retrieveIncomingChords (List<Chord> pendings)
{
Collections.sort(pendings, byRel);
List<Chord> incomings = new ArrayList<>();
Chord firstChord = pendings.get(0);
for (Chord chord : pendings) {
if (byRel.compare(firstChord, chord) == 0) {
incomings.add(chord);
} else {
break;
}
}
return incomings;
}
//------------------//
// computeStartTime //
//------------------//
/**
* Based on the provided active chords, determine the next
* expiration time.
*
* @param activeChords
*/
private Rational computeStartTime (Collection<Chord> activeChords)
{
Rational startTime = Rational.MAX_VALUE;
for (Chord chord : activeChords) {
if (!chord.isWholeDuration()) { // Skip the "whole" chords
Rational endTime = chord.getEndTime();
if (endTime.compareTo(startTime) < 0) {
startTime = endTime;
}
}
}
if (startTime.equals(Rational.MAX_VALUE)) {
startTime = Rational.ZERO;
}
logger.debug("startTime={}", startTime);
return startTime;
}
//--------------//
// refineVoices //
//--------------//
/**
* Slight improvements to voices in the current measure.
*/
private void refineVoices ()
{
// Preserve vertical voice order at beginning of measure
// Use chords from first time slot + whole chords
List<Chord> firsts = new ArrayList<>();
if (!measure.getSlots().isEmpty()) {
firsts.addAll(measure.getSlots().get(0).getChords());
}
if (measure.getWholeChords() != null) {
firsts.addAll(measure.getWholeChords());
}
Collections.sort(firsts, Chord.byOrdinate);
// Rename voices accordingly
for (int i = 0; i < firsts.size(); i++) {
Chord chord = firsts.get(i);
Voice voice = chord.getVoice();
measure.swapVoiceId(voice, i + 1);
}
}
}
//-----------//
// ChordPair //
//-----------//
private static class ChordPair
{
final Chord one;
final Chord two;
public ChordPair (Chord one,
Chord two)
{
this.one = one;
this.two = two;
logger.debug("Adjacent {}@{} & {}@{}",
one, one.getHeadLocation(), two, two.getHeadLocation());
}
@Override
public String toString ()
{
return "{ch#" + one.getId() + ",ch#" + two.getId() + "}";
}
}
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
Scale.Fraction maxSlotDx = new Scale.Fraction(
1,
"Maximum horizontal delta between a slot and a chord");
Scale.Fraction maxAdjacencyGap = new Scale.Fraction(
0.5,
"Maximum horizontal gap between adjacent chords bounds");
}
//------------//
// Parameters //
//------------//
private static class Parameters
{
//~ Instance fields ----------------------------------------------------
private final int maxSlotDx;
private final int maxAdjacencyGap;
//~ Constructors -------------------------------------------------------
public Parameters (Scale scale)
{
maxSlotDx = scale.toPixels(constants.maxSlotDx);
maxAdjacencyGap = scale.toPixels(constants.maxAdjacencyGap);
}
}
}