//----------------------------------------------------------------------------// // // // V o i c e // // // //----------------------------------------------------------------------------// // <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 omr.math.GCD; import omr.math.Rational; import omr.ui.symbol.Symbols; import omr.util.Navigable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * Class {@code Voice} gathers all informations related to a voice * within a measure. * * @author Hervé Bitteur */ public class Voice { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(Voice.class); //~ Enumerations ----------------------------------------------------------- public static enum Status { //~ Enumeration constant initializers ---------------------------------- /** A chord begins at this slot. */ BEGIN, /** A chord is still active * at this slot. */ CONTINUE; } //~ Instance fields -------------------------------------------------------- // /** Containing measure */ @Navigable(false) private final Measure measure; /** The voice id */ private int id; /** * Map (SlotId -> VoiceChord) to store chord information for each slot. * If a voice is assigned to a whole/multi rest, then this rest chord is * defined as the wholeChord of this voice, and the whole slot table is left * empty. * If the voice/slot combination is empty, the voice is free for this slot. * Otherwise, the active chord is referenced with a status flag to make a * difference between a slot where the chord starts, and the potential * following slots for which the chord is still active. */ private final SortedMap<Integer, VoiceChord> slotTable = new TreeMap<>(); /** * How the voice finishes (value = voiceEndTime - expectedMeasureEndTime) * - null: We can't tell * - negative: Voice is too short WRT expected measure duration * - zero: Voice equals the expected measure duration * - positive: Voice is too long WRT measure expected duration */ private Rational termination; /** Whole chord of the voice, if any */ private Chord wholeChord; /** Inferred time signature based on this voice content */ private TimeRational inferredTimeSig; //~ Constructors ----------------------------------------------------------- // //-------// // Voice // //-------// /** * Creates a new Voice object. * * @param chord the initial chord for this voice */ public Voice (Chord chord) { measure = chord.getMeasure(); id = measure.getVoicesNumber() + 1; measure.addVoice(this); chord.setVoice(this); logger.debug("Created voice#{}", id); } //~ Methods ---------------------------------------------------------------- //----------------// // getChordBefore // //----------------// /** * Retrieve the latest chord, if any, before the provided slot. * * @param slot the provided slot * @return the latest chord, in this voice, before this slot */ public Chord getChordBefore (Slot slot) { for (int sid = slot.getId() - 1; sid > 0; sid--) { VoiceChord info = slotTable.get(sid); if (info != null) { return info.getChord(); } } return null; } //---------------// // checkDuration // //---------------// /** * Check the duration of the voice, compared to measure expected * duration. */ public void checkDuration () { // Make all forward stuff explicit & visible try { if (isWhole()) { setTermination(null); // we can't tell anything } else { Rational timeCounter = Rational.ZERO; for (VoiceChord info : slotTable.values()) { if (info.getStatus() == Status.BEGIN) { Chord chord = info.getChord(); Slot slot = chord.getSlot(); // Need a forward before this chord ? if (timeCounter.compareTo(slot.getStartTime()) < 0) { insertForward( slot.getStartTime().minus(timeCounter), Mark.Position.BEFORE, chord); timeCounter = slot.getStartTime(); } timeCounter = timeCounter.plus(chord.getDuration()); } } // Need an ending forward ? Rational delta = timeCounter.minus( measure.getExpectedDuration()); setTermination(delta); if (delta.compareTo(Rational.ZERO) < 0) { // Insert a forward mark insertForward( delta.opposite(), Mark.Position.AFTER, getLastChord()); } else if (delta.compareTo(Rational.ZERO) > 0) { // Flag the measure as too long measure.addError( "Voice #" + getId() + " too long for " + delta); measure.setExcess(delta); } } } catch (Exception ex) { // User has been informed } } //------------------// // createWholeVoice // //------------------// /** * Factory method to create a voice made of just one whole/multi * rest. * * @param wholeChord the whole/multi rest chord * @return the created voice instance */ public static Voice createWholeVoice (Chord wholeChord) { Voice voice = new Voice(wholeChord); voice.wholeChord = wholeChord; return voice; } //-------// // setId // //-------// /** * Change the voice id (to rename voices) * * @param id the new id value */ public void setId (int id) { logger.debug("measure#{} {} renamed as {}", measure.getIdValue(), this, id); this.id = id; } //-------// // getId // //-------// /** * Report the voice id, starting from 1. * * @return the voice id */ public int getId () { return id; } //--------------------------// // getInferredTimeSignature // //--------------------------// /** * Report the time signature value that can be inferred from the * content of this voice. * * @return the "intrinsic" time signature rational value for this voice, * or null */ public TimeRational getInferredTimeSignature () { if (inferredTimeSig == null) { // TODO: update for the use of tuplets // Sequence of group (beamed or isolated chords) durations List<Rational> durations = new ArrayList<>(); // Current beam group, if any BeamGroup currentGroup = null; for (Map.Entry<Integer, VoiceChord> entry : slotTable.entrySet()) { VoiceChord info = entry.getValue(); if (info.getStatus() == Voice.Status.BEGIN) { Chord chord = info.getChord(); BeamGroup group = chord.getBeamGroup(); if (group == null) { // Isolated chord durations.add(chord.getDuration()); } else if (group != currentGroup) { // Starting a new group durations.add(group.getDuration()); } currentGroup = group; } } // Debug if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder("["); boolean started = false; Rational total = null; for (Rational dur : durations) { if (started) { sb.append(","); } started = true; if (dur == null) { sb.append("null"); } else { sb.append(dur); if (total == null) { total = dur; } else { total = total.plus(dur); } } } sb.append("] total:"); if (total != null) { sb.append(total); } else { sb.append("null"); } logger.debug("{}: {}", this, sb); } // Do we have a regular pattern? int count = 0; Rational common = null; for (Rational dur : durations) { if (common == null) { common = dur; } else if (!common.equals(dur)) { break; } count++; } if ((common != null) && (count == durations.size())) { // All the durations are equal inferredTimeSig = timeSigOf(count, common); } } return inferredTimeSig; } //--------------// // getLastChord // //--------------// /** * Report the last chord of this voice. * * @return the last chord, which may be a whole/multi */ public Chord getLastChord () { if (isWhole()) { return wholeChord; } else { for (int k = slotTable.lastKey(); k > 0; k--) { VoiceChord info = slotTable.get(k); if (info != null) { return info.getChord(); } } return null; } } //------------------// // getPreviousChord // //------------------// /** * Starting from a provided chord in this voice, report the * previous chord, if any, within that voice. * * @param chord the provided chord * @return the chord right before, or null */ public Chord getPreviousChord (Chord chord) { Chord prevChord = null; for (Map.Entry<Integer, VoiceChord> entry : slotTable.entrySet()) { VoiceChord info = entry.getValue(); if (info.getChord() == chord) { break; } prevChord = info.getChord(); } return prevChord; } //-------------// // getSlotInfo // //-------------// /** * Report the chord information for the specified slot. * * @param slot the specified slot * @return chordInfo the precise chord information, or null */ public VoiceChord getSlotInfo (Slot slot) { return slotTable.get(slot.getId()); } //----------------// // getTermination // //----------------// /** * Report how this voice finishes. * * @return 0=perfect, -n=too_short, +n=overlast, null=whole_rest/multi_rest */ public Rational getTermination () { return termination; } //---------------// // getWholeChord // //---------------// /** * Report the whole/multi rest chord which fills the voice, if any. * * @return the whole chord or null */ public Chord getWholeChord () { return wholeChord; } //--------// // isFree // //--------// /** * Report whether the voice is available at this slot. * * @param slot the specific slot for which we consider this voice * @return true if free */ public boolean isFree (Slot slot) { return ((getWholeChord() == null) && (slotTable.get(slot.getId()) == null)); } //---------// // isWhole // //---------// /** * Report whether this voice is made of a whole/multi rest. * * @return true if made of a whole/multi rest */ public boolean isWhole () { return wholeChord != null; } //-------------// // setSlotInfo // //-------------// /** * Define the chord information for the specified slot. * * @param slot the specified slot * @param chordInfo the precise chord information, or null to free the slot */ public void setSlotInfo (Slot slot, VoiceChord chordInfo) { if (isWhole()) { logger.error("You cannot insert a slot in a whole-only voice"); return; } slotTable.put(slot.getId(), chordInfo); updateSlotTable(); logger.debug("setSlotInfo slot#{} {}", slot.getId(), this); } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Voice#").append(id).append("}"); return sb.toString(); } //---------// // toStrip // //---------// /** * Return a string which represents the life of this voice. * * @return a strip-like graphic of the voice */ public String toStrip () { StringBuilder sb = new StringBuilder(); sb.append("{V").append(id).append(" "); // Whole/Multi if (wholeChord != null) { sb.append("|Ch#").append(String.format("%02d", wholeChord.getId())); for (int s = 1; s < measure.getSlots().size(); s++) { sb.append("======"); } sb.append("|W"); } else { for (Slot slot : measure.getSlots()) { VoiceChord info = getSlotInfo(slot); if (info != null) { // Active chord => busy if (info.getStatus() == Status.BEGIN) { sb.append("|Ch#").append( String.format("%02d", info.getChord().getId())); } else { // CONTINUE sb.append("======"); } } else { // No active chord => free sb.append("|....."); } } sb.append("|"); } sb.append("}"); return sb.toString(); } //-----------------// // updateSlotTable // //-----------------// /** * Update the slotTable. */ public void updateSlotTable () { Chord lastChord = null; for (Slot slot : measure.getSlots()) { if (slot.getStartTime() != null) { VoiceChord info = getSlotInfo(slot); if (info == null) { if ((lastChord != null) && (lastChord.getEndTime().compareTo(slot. getStartTime()) > 0)) { setSlotInfo( slot, new VoiceChord(lastChord, Status.CONTINUE)); } } else { lastChord = info.chord; } } } } //---------------// // insertForward // //---------------// private void insertForward (Rational duration, Mark.Position position, Chord chord) { Point point = new Point( chord.getHeadLocation().x, (chord.getHeadLocation().y + chord.getTailLocation().y) / 2); if (position == Mark.Position.AFTER) { point.x += 10; } else if (position == Mark.Position.BEFORE) { point.x -= 10; } Mark mark = new Mark( chord.getSystem(), point, position, Symbols.SYMBOL_MARK, duration); chord.addMark(mark); } //----------------// // setTermination // //----------------// private void setTermination (Rational termination) { this.termination = termination; } //-----------// // timeSigOf // //-----------// /** * Based on the number of common groups, derive the proper time * rational value. * * @param count the number of groups * @param common the common time duration of each group * @return the inferred time rational */ private TimeRational timeSigOf (int count, Rational common) { // Determine the time rational value of measure total duration TimeRational timeRational = new TimeRational( count * common.num, common.den); int gcd = GCD.gcd(count, timeRational.num); // Make sure num is a multiple of count timeRational = new TimeRational( (count / gcd) * timeRational.num, (count / gcd) * timeRational.den); // No 1 as num if (timeRational.num == 1) { timeRational = new TimeRational( 2 * timeRational.num, 2 * timeRational.den); } return timeRational; } //~ Inner Classes ---------------------------------------------------------- // //------------// // VoiceChord // //------------// public static class VoiceChord { //~ Instance fields ---------------------------------------------------- /** Related chord */ private final Chord chord; /** Current status */ private final Status status; //~ Constructors ------------------------------------------------------- public VoiceChord (Chord chord, Status status) { this.chord = chord; this.status = status; } //~ Methods ------------------------------------------------------------ public Chord getChord () { return chord; } public Status getStatus () { return status; } @Override public String toString () { StringBuilder sb = new StringBuilder(); sb.append("{Info"); sb.append(" Ch#").append(chord.getId()); sb.append(" ").append(status); sb.append("}"); return sb.toString(); } } }