package com.xenoage.zong.musiclayout.stamper; import com.xenoage.utils.color.Color; import com.xenoage.utils.math.VSide; import com.xenoage.utils.math.geom.Rectangle2f; import com.xenoage.zong.core.music.WaypointPosition; import com.xenoage.zong.core.music.beam.Beam; import com.xenoage.zong.core.music.chord.Chord; import com.xenoage.zong.core.music.chord.StemDirection; import com.xenoage.zong.core.music.format.SP; import com.xenoage.zong.core.music.lyric.Lyric; import com.xenoage.zong.core.music.lyric.SyllableType; import com.xenoage.zong.core.music.slur.Slur; import com.xenoage.zong.core.music.slur.SlurWaypoint; import com.xenoage.zong.core.music.tuplet.Tuplet; import com.xenoage.zong.core.music.util.DurationInfo; import com.xenoage.zong.core.text.FormattedTextStyle; import com.xenoage.zong.musiclayout.layouter.cache.OpenLyricsCache; import com.xenoage.zong.musiclayout.layouter.cache.OpenSlursCache; import com.xenoage.zong.musiclayout.layouter.cache.OpenTupletsCache; import com.xenoage.zong.musiclayout.layouter.scoreframelayout.util.ChordStampings; import com.xenoage.zong.musiclayout.layouter.scoreframelayout.util.LastLyrics; import com.xenoage.zong.musiclayout.layouter.scoreframelayout.util.StaffStampings; import com.xenoage.zong.musiclayout.notation.ChordNotation; import com.xenoage.zong.musiclayout.notation.chord.*; import com.xenoage.zong.musiclayout.settings.ChordWidths; import com.xenoage.zong.musiclayout.settings.LayoutSettings; import com.xenoage.zong.musiclayout.spacing.BeamSpacing; import com.xenoage.zong.musiclayout.stampings.*; import com.xenoage.zong.symbols.Symbol; import com.xenoage.zong.symbols.common.CommonSymbol; import lombok.val; import java.util.List; import static com.xenoage.utils.NullUtils.notNull; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.utils.kernel.Range.range; import static com.xenoage.zong.core.music.format.SP.sp; import static com.xenoage.zong.musiclayout.layouter.scoreframelayout.LyricStamper.lyricStamper; import static com.xenoage.zong.musiclayout.stamper.SlurStamper.slurStamper; import static com.xenoage.zong.musiclayout.stamper.BeamStamper.beamStamper; import static com.xenoage.zong.musiclayout.stamper.DirectionStamper.directionStamper; import static com.xenoage.zong.musiclayout.stamper.LegerLinesStamper.legerLinesStamper; /** * Creates the {@link Stamping}s for a chord. * * TODO (ZONG-91): clean up by using a ChordSpacer/ChordSpacing * * @author Andreas Wenger */ public class ChordStamper { public static final ChordStamper chordStamper = new ChordStamper(); /** * Returns all the stampings for the given {@link Chord}, including beams, * tuplets, slurs and other attachments. * * The given {@link OpenSlursCache}, * {@link OpenLyricsCache}, {@link LastLyrics} and {@link OpenTupletsCache} may be modified. */ public List<Stamping> stampAll(ChordNotation chord, float xMm, BeamSpacing beam, StaffStampings staffStampings, StamperContext context, FormattedTextStyle defaultLyricStyle, OpenSlursCache openSlursCache, OpenLyricsCache openLyricsCache, LastLyrics lastLyrics, OpenTupletsCache openTupletsCache) { List<Stamping> ret = alist(); Chord element = chord.getElement(); int staffIndex = context.staffIndex; int systemIndex = context.systemIndex; //noteheads, leger lines, dots, accidentals, stem, flags, articulations ChordStampings chordSt = stampCore(chord, xMm, context); chordSt.addAllTo(ret); //beam if (beam != null) { //stamp the whole beam (when we find the beginning of the beam) //TIDY: create/store beam stampings elsewhere? Beam beamElement = beam.notation.element; int chordIndex = beamElement.getWaypointIndex(element); if (chordIndex == 0) { ret.addAll(beamStamper.stamp(beam, context.getCurrentStaffStamping())); } } //ties and slurs for (Slur slur : element.getSlurs()) { SlurWaypoint wp = slur.getWaypoint(element); WaypointPosition pos = slur.getWaypointPosition(element); int noteIndex = notNull(wp.getNoteIndex(), 0); //TODO: choose top/bottom NoteheadStamping notehead = chordSt.noteheads[noteIndex]; //define the placement: above or below (TODO: better strategy) VSide side = slurStamper.getSide(slur); //compute position val staff = staffStampings.get(systemIndex, notehead.parentStaff.staffIndex); val slurCache = openSlursCache.getOrCreate(slur); float distanceIs = slurStamper.getAdditionalDistanceIs(chord, slur.getSide()); SP defaultSp = sp(notehead.position.xMm, notehead.position.lp + side.getDir() * distanceIs * 2); if (pos == WaypointPosition.Start) slurCache.setStart(defaultSp, staff, systemIndex); else slurCache.setStop(defaultSp, staff, systemIndex); } //lyric List<Lyric> lyrics = element.getLyrics(); if (lyrics.size() > 0) { float baseLine = -10; for (Lyric lyric : lyrics) { if (lyric != null) { SyllableType lyricType = lyric.getSyllableType(); StaffTextStamping lastLyric = lastLyrics.get(staffIndex, lyric.getVerse()); if (lyricType == SyllableType.Extend) { //extend if (lastLyric != null) //TODO: frame breaks... { //remember it openLyricsCache.setUnderscore((Lyric) lastLyric.getElement(), lastLyric, chordSt.noteheads[0]/* TODO*/, staffIndex); } } else { //normal lyric //create text stamping StaffTextStamping sts = lyricStamper.createSyllableStamping(lyric, defaultLyricStyle, context.getCurrentStaffStamping(), chordSt.noteheads[0]/* TODO*/.position.xMm, baseLine); ret.add(sts); //when middle or end syllable, add a hypen between the preceding syllable and this syllable if (lastLyric != null) //TODO: frame breaks... { if (lyricType == SyllableType.Middle || lyricType == SyllableType.End) { StaffTextStamping hyphenStamping = lyricStamper.createHyphenStamping( lastLyric, sts, defaultLyricStyle); ret.add(hyphenStamping); } } //remember this lyric as the currently last one in the current staff and verse lastLyrics.set(staffIndex, lyric.getVerse(), sts); } } baseLine += -5; } } //directions ret.addAll(directionStamper.stampForChord(chordSt, context.layouter.symbols)); //tuplet Tuplet tuplet = element.getTuplet(); if (tuplet != null) { openTupletsCache.addChord(element, tuplet, chordSt); } return ret; } /** * Draws the given chord, including noteheads, stem, flags, accidentals, dots, * articulations and leger lines. */ public ChordStampings stampCore(ChordNotation chord, float chordXMm, StamperContext context) { val staff = context.getCurrentStaffStamping(); Chord element = chord.element; boolean grace = element.isGrace(); LayoutSettings settings = context.getSettings(); float scaling = (grace ? settings.scalingGrace : 1); ChordWidths chordWidths = (grace ? settings.graceChordWidths : settings.chordWidths); float leftNoteXMm = getLeftNoteXMm(chordXMm, chord.notes, staff.is); //stem StemStamping stem = stampStem(chord, leftNoteXMm, context); //type of notehead CommonSymbol noteheadSymbol = CommonSymbol.NoteWhole; DurationInfo.Type symbolType = DurationInfo.getNoteheadSymbolType(element.getDisplayedDuration()); if (symbolType == DurationInfo.Type.Half) noteheadSymbol = CommonSymbol.NoteHalf; else if (symbolType == DurationInfo.Type.Quarter) noteheadSymbol = CommonSymbol.NoteQuarter; //noteheads NotesNotation notes = chord.notes; NoteheadStamping[] noteheads = new NoteheadStamping[notes.getNotesCount()]; for (int iNote : range(noteheads)) { NoteDisplacement note = notes.getNote(iNote); Symbol noteSymbol = context.getSymbol(noteheadSymbol); float noteXMm = getNoteheadXMm(leftNoteXMm + note.xIs * staff.is, scaling, staff, noteSymbol); NoteheadStamping noteSt = new NoteheadStamping(chord, iNote, noteSymbol, Color.black, staff, sp(noteXMm, note.lp), scaling); noteheads[iNote] = noteSt; } //flags (only drawn if there is no beam) int flagsCount = DurationInfo.getFlagsCount(element.getDisplayedDuration()); Beam beam = element.getBeam(); StemDirection stemDir = chord.stemDirection; FlagsStamping flags = null; if (beam == null && flagsCount > 0 && chord.stem != null /* can happen when no stem is used */) { FlagsStamping.FlagsDirection flag = (stemDir == StemDirection.Up ? FlagsStamping.FlagsDirection.Down : FlagsStamping.FlagsDirection.Up); Symbol flagSymbol = context.getSymbol(CommonSymbol.NoteFlag); flags = new FlagsStamping(chord, staff, flag, flagsCount, flagSymbol, scaling, sp(leftNoteXMm + notes.stemOffsetIs * staff.is, chord.stem.endSlp.lp)); } //accidentals AccidentalsNotation accs = chord.accidentals; AccidentalStamping[] accsSt = new AccidentalStamping[0]; if (accs != null) { accsSt = new AccidentalStamping[accs.accidentals.length]; for (int iAcc : range(accsSt)) { AccidentalDisplacement acc = accs.accidentals[iAcc]; AccidentalStamping accSt = new AccidentalStamping(chord, iAcc, staff, sp(chordXMm + (acc.xIs - chord.width.frontGap + 0.5f /* 0.5f: half accidental width - TODO */) * staff.is, acc.yLp), 1, context.getSymbol(CommonSymbol.getAccidental(acc.accidental))); accsSt[iAcc] = accSt; } } //dots int[] dotPositions = notes.dotsLp; int dotsPerNote = notes.getDotsPerNoteCount(); ProlongationDotStamping[] dots = new ProlongationDotStamping[dotPositions.length * dotsPerNote]; Symbol dotSymbol = context.getSymbol(CommonSymbol.NoteDot); for (int iNote : range(dotPositions)) { for (int iDot : range(dotsPerNote)) { ProlongationDotStamping dotSt = new ProlongationDotStamping(chord, staff, dotSymbol, sp(leftNoteXMm + notes.getDotsOffsetIs(iDot) * staff.is, dotPositions[iNote])); dots[iNote * dotsPerNote + iDot] = dotSt; } } //articulations ArticulationsNotation arts = chord.articulations; ArticulationStamping[] artsSt = new ArticulationStamping[0]; if (arts != null) { artsSt = new ArticulationStamping[arts.articulations.length]; float noteheadWidth = chordWidths.get(element.getDuration()); for (int iArt : range(artsSt)) { ArticulationDisplacement art = arts.articulations[iArt]; ArticulationStamping artSt = new ArticulationStamping(chord, iArt, staff, sp(leftNoteXMm + (art.xIs + (noteheadWidth / 2)) * staff.is, art.yLp), 1, context.getSymbol(CommonSymbol.getArticulation(art.articulation))); artsSt[iArt] = artSt; } } //leger lines LegerLineStamping[] legerLines = legerLinesStamper.stamp(chord, chordXMm, staff); return new ChordStampings(element, chordXMm, staff, noteheads, dots, accsSt, legerLines, artsSt, flags, stem); } float getLeftNoteXMm(float chordXMm, NotesNotation notesAlignment, float staffIs) { //left-suspended chord? then move chord to the left by the width of a notehead float leftNoteXMm = chordXMm; if (notesAlignment.leftSuspended) leftNoteXMm -= notesAlignment.noteheadWidthIs * staffIs; return leftNoteXMm; } StemStamping stampStem(ChordNotation chordNotation, float leftNoteXMm, StamperContext context) { StemNotation stem = chordNotation.stem; if (stem == null) return null; val noteStaff = context.getStaffStamping(stem.startSlp.staff); val endStaff = context.getStaffStamping(stem.endSlp.staff); float stemXMm = leftNoteXMm + chordNotation.notes.stemOffsetIs * noteStaff.is; return new StemStamping(chordNotation, stemXMm, stem.startSlp.lp, noteStaff, stem.endSlp.lp, endStaff, chordNotation.stemDirection); } //TIDY private float getNoteheadXMm(float xMm, float scaling, StaffStamping staff, Symbol symbol) { float ret = xMm; Rectangle2f bounds = symbol.getBoundingRect().scale(scaling); float interlineSpace = staff.is; float lineWidth = staff.getLineWidthMm(); ret += (bounds.size.width / 2) * interlineSpace - lineWidth / 2; return ret; } }