import { MusicSheet } from "../../MusicSheet" ; import { Fraction } from "../../../Common/DataObjects/Fraction" ; import { MultiTempoExpression } from "../../VoiceData/Expressions/MultiTempoExpression" ; import { ContDynamicEnum , ContinuousDynamicExpression } from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression" ; import { ContinuousTempoExpression } from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousTempoExpression" ; import { InstantaneousDynamicExpression } from "../../VoiceData/Expressions/InstantaneousDynamicExpression" ; import { OctaveShift } from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift" ; import { Instrument } from "../../Instrument" ; import { MultiExpression } from "../../VoiceData/Expressions/MultiExpression" ; import { IXmlAttribute , IXmlElement } from "../../../Common/FileIO/Xml" ; import { SourceMeasure } from "../../VoiceData/SourceMeasure" ; import { InstantaneousTempoExpression } from "../../VoiceData/Expressions/InstantaneousTempoExpression" ; import { MoodExpression } from "../../VoiceData/Expressions/MoodExpression" ; import { UnknownExpression } from "../../VoiceData/Expressions/UnknownExpression" ; import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression" ; import { TextAlignmentEnum } from "../../../Common/Enums/TextAlignment" ; import { ITextTranslation } from "../../Interfaces/ITextTranslation" ; import log from "loglevel" ; import { FontStyles } from "../../../Common/Enums/FontStyles" ; import { RehearsalExpression } from "../../VoiceData/Expressions/RehearsalExpression" ; export class ExpressionReader { private musicSheet : MusicSheet ; private placement : PlacementEnum ; private soundTempo : number ; private soundDynamic : number ; private offsetDivisions : number ; private staffNumber : number ; private globalStaffIndex : number ; private directionTimestamp : Fraction ; private currentMultiTempoExpression : MultiTempoExpression ; private openContinuousDynamicExpression : ContinuousDynamicExpression ; private openContinuousTempoExpression : ContinuousTempoExpression ; private activeInstantaneousDynamic : InstantaneousDynamicExpression ; private openOctaveShift : OctaveShift ; constructor ( musicSheet , instrument , staffNumber ) { this . musicSheet = musicSheet ; this . staffNumber = staffNumber ; this . globalStaffIndex = musicSheet . getGlobalStaffIndexOfFirstStaff ( instrument ) + ( staffNumber - 1 ) ; this . initialize ( ) ; } public getMultiExpression : MultiExpression ; public readExpressionParameters ( xmlNode , currentInstrument , divisions , inSourceMeasureCurrentFraction , inSourceMeasureFormerFraction , currentMeasureIndex , ignoreDivisionsOffset ) { this . initialize ( ) ; const offsetNode = xmlNode . element ( "offset" ) ; if ( offsetNode !== undefined && ! ignoreDivisionsOffset ) { try { this . offsetDivisions = parseInt ( offsetNode . value , 10 ) ; } catch ( ex ) { const errorMsg = "ReaderErrorMessages/ExpressionOffsetError" + ", Invalid expression offset -> set to default." ; log . debug ( "ExpressionReader.readExpressionParameters" , errorMsg , ex ) ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; this . offsetDivisions = 0 ; } } this . directionTimestamp = Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ; let offsetFraction = new Fraction ( Math . abs ( this . offsetDivisions ) , divisions * 4 ) ; if ( this . offsetDivisions > 0 ) { if ( inSourceMeasureCurrentFraction . RealValue > 0 ) { offsetFraction = Fraction . multiply ( Fraction . minus ( inSourceMeasureCurrentFraction , inSourceMeasureFormerFraction ) , offsetFraction ) ; this . directionTimestamp = Fraction . plus ( offsetFraction , inSourceMeasureCurrentFraction ) ; } else { this . directionTimestamp = Fraction . createFromFraction ( offsetFraction ) ; } } else if ( this . offsetDivisions < 0 ) { if ( inSourceMeasureCurrentFraction . RealValue > 0 ) { offsetFraction = Fraction . multiply ( Fraction . minus ( inSourceMeasureCurrentFraction , inSourceMeasureFormerFraction ) , offsetFraction ) ; this . directionTimestamp = Fraction . minus ( inSourceMeasureCurrentFraction , offsetFraction ) ; } else { this . directionTimestamp = Fraction . createFromFraction ( offsetFraction ) ; } } const placeAttr = xmlNode . attribute ( "placement" ) ; if ( placeAttr ) { try { const placementString = placeAttr . value ; if ( placementString === "below" ) { this . placement = PlacementEnum . Below ; } else if ( placementString === "above" ) { this . placement = PlacementEnum . Above ; } } catch ( ex ) { const errorMsg = ITextTranslation . translateText ( "ReaderErrorMessages/ExpressionPlacementError" , "Invalid expression placement -> set to default." ) ; log . debug ( "ExpressionReader.readExpressionParameters" , errorMsg , ex ) ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; this . placement = PlacementEnum . Below ; } } if ( this . placement === PlacementEnum . NotYetDefined ) { try { const directionTypeNode = xmlNode . element ( "direction-type" ) ; if ( directionTypeNode ) { const dynamicsNode = directionTypeNode . element ( "dynamics" ) ; if ( dynamicsNode ) { const defAttr = dynamicsNode . attribute ( "default-y" ) ; if ( defAttr ) { this . readExpressionPlacement ( defAttr , "read dynamics y pos" ) ; } } const wedgeNode = directionTypeNode . element ( "wedge" ) ; if ( wedgeNode ) { const defAttr = wedgeNode . attribute ( "default-y" ) ; if ( defAttr ) { this . readExpressionPlacement ( defAttr , "read wedge y pos" ) ; } } const wordsNode = directionTypeNode . element ( "words" ) ; if ( wordsNode ) { const defAttr = wordsNode . attribute ( "default-y" ) ; if ( defAttr ) { this . readExpressionPlacement ( defAttr , "read words y pos" ) ; } } const rehearsalNode = directionTypeNode . element ( "rehearsal" ) ; if ( rehearsalNode ) { const defAttr = rehearsalNode . attribute ( "default-y" ) ; if ( defAttr ) { this . readExpressionPlacement ( defAttr , "read rehearsal pos" ) ; } } } } catch ( ex ) { const errorMsg = ITextTranslation . translateText ( "ReaderErrorMessages/ExpressionPlacementError" , "Invalid expression placement -> set to default." ) ; log . debug ( "ExpressionReader.readExpressionParameters" , errorMsg , ex ) ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; this . placement = PlacementEnum . Below ; } } if ( this . placement === PlacementEnum . NotYetDefined ) { if ( currentInstrument . Staves . length > 1 ) { this . placement = PlacementEnum . Below ; } else if ( currentInstrument . HasLyrics ) { this . placement = PlacementEnum . Above ; } else { this . placement = PlacementEnum . Below ; } } } public read ( directionNode , currentMeasure , inSourceMeasureCurrentFraction , inSourceMeasurePreviousFraction = undefined ) { let isTempoInstruction = false ; let isDynamicInstruction = false ; const n = directionNode . element ( "sound" ) ; if ( n ) { const tempoAttr = n . attribute ( "tempo" ) ; const dynAttr = n . attribute ( "dynamics" ) ; if ( tempoAttr ) { const match = tempoAttr . value . match ( /^(\d+)(\.\d+)?$/ ) ; if ( match ?. length > 0 ) { this . soundTempo = Math . round ( Number . parseFloat ( tempoAttr . value ) ) ; } else { log . info ( "invalid xml tempo: " + tempoAttr . value ) ; this . soundTempo = 100 ; } currentMeasure . TempoInBPM = this . soundTempo ; if ( this . musicSheet . DefaultStartTempoInBpm === 0 ) { this . musicSheet . DefaultStartTempoInBpm = this . soundTempo ; } this . musicSheet . HasBPMInfo = true ; isTempoInstruction = true ; } if ( dynAttr ) { const match = dynAttr . value . match ( /\d+/ ) ; this . soundDynamic = match !== undefined ? parseInt ( match [ 0 ] , 10 ) : 100 ; isDynamicInstruction = true ; } } const dirNode = directionNode . element ( "direction-type" ) ; if ( ! dirNode ) { return ; } let dirContentNode = dirNode . element ( "metronome" ) ; if ( dirContentNode ) { const beatUnit = dirContentNode . element ( "beat-unit" ) ; const dotted = dirContentNode . element ( "beat-unit-dot" ) !== undefined ; const bpm = dirContentNode . element ( "per-minute" ) ; if ( beatUnit !== undefined && bpm ) { const useCurrentFractionForPositioning = ( dirContentNode . hasAttributes && dirContentNode . attribute ( "default-x" ) !== undefined ) ; if ( useCurrentFractionForPositioning ) { this . directionTimestamp = Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ; } const bpmNumber = parseFloat ( bpm . value ) ; this . createNewTempoExpressionIfNeeded ( currentMeasure ) ; const instantaneousTempoExpression = new InstantaneousTempoExpression ( undefined , this . placement , this . staffNumber , bpmNumber , this . currentMultiTempoExpression , true ) ; instantaneousTempoExpression . parentMeasure = currentMeasure ; this . soundTempo = bpmNumber ; currentMeasure . TempoInBPM = this . soundTempo * ( dotted ? 1.5 : 1 ) ; if ( this . musicSheet . DefaultStartTempoInBpm === 0 ) { this . musicSheet . DefaultStartTempoInBpm = this . soundTempo ; } this . musicSheet . HasBPMInfo = true ; instantaneousTempoExpression . dotted = dotted ; instantaneousTempoExpression . beatUnit = beatUnit . value ; this . currentMultiTempoExpression . addExpression ( instantaneousTempoExpression , "" ) ; this . currentMultiTempoExpression . CombinedExpressionsText = "test" ; } return ; } dirContentNode = dirNode . element ( "dynamics" ) ; if ( dirContentNode ) { const fromNotation = directionNode . element ( "notations" ) !== undefined ; this . interpretInstantaneousDynamics ( dirContentNode , currentMeasure , inSourceMeasureCurrentFraction , fromNotation ) ; return ; } dirContentNode = dirNode . element ( "words" ) ; if ( dirContentNode ) { if ( isTempoInstruction ) { this . createNewTempoExpressionIfNeeded ( currentMeasure ) ; this . currentMultiTempoExpression . CombinedExpressionsText = dirContentNode . value ; const instantaneousTempoExpression = new InstantaneousTempoExpression ( dirContentNode . value , this . placement , this . staffNumber , this . soundTempo , this . currentMultiTempoExpression ) ; this . currentMultiTempoExpression . addExpression ( instantaneousTempoExpression , "" ) ; } else if ( ! isDynamicInstruction ) { this . interpretWords ( dirContentNode , currentMeasure , inSourceMeasureCurrentFraction ) ; } return ; } dirContentNode = dirNode . element ( "wedge" ) ; if ( dirContentNode ) { this . interpretWedge ( dirContentNode , currentMeasure , inSourceMeasurePreviousFraction , currentMeasure . MeasureNumber ) ; return ; } dirContentNode = dirNode . element ( "rehearsal" ) ; if ( dirContentNode ) { this . interpretRehearsalMark ( dirContentNode , currentMeasure , inSourceMeasureCurrentFraction , currentMeasure . MeasureNumber ) ; return ; } } public checkForOpenExpressions ( sourceMeasure , timestamp ) { if ( this . openContinuousDynamicExpression ) { this . createNewMultiExpressionIfNeeded ( sourceMeasure , timestamp ) ; this . closeOpenContinuousDynamic ( ) ; } if ( this . openContinuousTempoExpression ) { this . closeOpenContinuousTempo ( Fraction . plus ( sourceMeasure . AbsoluteTimestamp , timestamp ) ) ; } } public addOctaveShift ( directionNode , currentMeasure , endTimestamp ) { let octaveStaffNumber = 1 ; const staffNode = directionNode . element ( "staff" ) ; if ( staffNode ) { try { octaveStaffNumber = parseInt ( staffNode . value , 10 ) ; } catch ( ex ) { const errorMsg = ITextTranslation . translateText ( "ReaderErrorMessages/OctaveShiftStaffError" , "Invalid octave shift staff number -> set to default" ) ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; octaveStaffNumber = 1 ; log . debug ( "ExpressionReader.addOctaveShift" , errorMsg , ex ) ; } } const directionTypeNode = directionNode . element ( "direction-type" ) ; if ( directionTypeNode ) { const octaveShiftNode = directionTypeNode . element ( "octave-shift" ) ; if ( octaveShiftNode !== undefined && octaveShiftNode . hasAttributes ) { try { if ( octaveShiftNode . attribute ( "size" ) ) { const size = parseInt ( octaveShiftNode . attribute ( "size" ) . value , 10 ) ; let octave = 0 ; if ( size === 8 ) { octave = 1 ; } else if ( size === 15 ) { octave = 2 ; } if ( octaveShiftNode . attribute ( "type" ) ) { const type = octaveShiftNode . attribute ( "type" ) . value ; if ( type === "up" || type === "down" ) { const octaveShift = new OctaveShift ( type , octave ) ; octaveShift . StaffNumber = octaveStaffNumber ; this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; this . getMultiExpression . OctaveShiftStart = octaveShift ; octaveShift . ParentStartMultiExpression = this . getMultiExpression ; this . openOctaveShift = octaveShift ; } else if ( type === "stop" ) { if ( this . openOctaveShift ) { this . createNewMultiExpressionIfNeeded ( currentMeasure , endTimestamp ) ; this . getMultiExpression . OctaveShiftEnd = this . openOctaveShift ; this . openOctaveShift . ParentEndMultiExpression = this . getMultiExpression ; this . openOctaveShift = undefined ; } } } } } catch ( ex ) { const errorMsg = ITextTranslation . translateText ( "ReaderErrorMessages/OctaveShiftError" , "Error while reading octave shift." ) ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; log . debug ( "ExpressionReader.addOctaveShift" , errorMsg , ex ) ; } } } } private initialize ( ) { this . placement = PlacementEnum . NotYetDefined ; this . soundTempo = 0 ; this . soundDynamic = 0 ; this . offsetDivisions = 0 ; } private readExpressionPlacement ( defAttr , catchLogMessage ) { try { const y = parseInt ( defAttr . value , 10 ) ; if ( y < 0 ) { this . placement = PlacementEnum . Below ; } else if ( y > 0 ) { this . placement = PlacementEnum . Above ; } } catch ( ex ) { log . debug ( "ExpressionReader.readExpressionParameters" , catchLogMessage , ex ) ; } } private interpretInstantaneousDynamics ( dynamicsNode , currentMeasure , inSourceMeasureCurrentFraction , fromNotation ) { if ( dynamicsNode . hasElements ) { if ( dynamicsNode . hasAttributes && dynamicsNode . attribute ( "default-x" ) ) { this . directionTimestamp = Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ; } let expressionText = dynamicsNode . elements ( ) [ 0 ] . name ; if ( expressionText === "other-dynamics" ) { expressionText = dynamicsNode . elements ( ) [ 0 ] . value ; } if ( expressionText ) { if ( ! fromNotation ) { this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; } else { this . createNewMultiExpressionIfNeeded ( currentMeasure , Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ) ; } if ( this . openContinuousDynamicExpression !== undefined && this . openContinuousDynamicExpression . StartMultiExpression !== this . getMultiExpression ) { this . closeOpenContinuousDynamic ( ) ; } const instantaneousDynamicExpression = new InstantaneousDynamicExpression ( expressionText , this . soundDynamic , this . placement , this . staffNumber , currentMeasure ) ; this . getMultiExpression . addExpression ( instantaneousDynamicExpression , "" ) ; this . initialize ( ) ; if ( this . activeInstantaneousDynamic ) { this . activeInstantaneousDynamic . DynEnum = instantaneousDynamicExpression . DynEnum ; } else { this . activeInstantaneousDynamic = new InstantaneousDynamicExpression ( expressionText , 0 , PlacementEnum . NotYetDefined , 1 , currentMeasure ) ; } } } } private interpretWords ( wordsNode , currentMeasure , inSourceMeasureCurrentFraction ) { const text = wordsNode . value ; let fontStyle ; const fontStyleAttr = wordsNode . attribute ( "font-style" ) ; if ( fontStyleAttr ) { const fontStyleText = fontStyleAttr . value ; if ( fontStyleText === "italic" ) { fontStyle = FontStyles . Italic ; } } if ( text . length > 0 ) { if ( wordsNode . hasAttributes && wordsNode . attribute ( "default-x" ) ) { this . directionTimestamp = Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ; } if ( this . checkIfWordsNodeIsRepetitionInstruction ( text ) ) { return ; } this . fillMultiOrTempoExpression ( text , currentMeasure , fontStyle ) ; this . initialize ( ) ; } } private interpretWedge ( wedgeNode , currentMeasure , inSourceMeasureCurrentFraction , currentMeasureIndex ) { if ( wedgeNode !== undefined && wedgeNode . hasAttributes && wedgeNode . attribute ( "default-x" ) ) { this . directionTimestamp = Fraction . createFromFraction ( inSourceMeasureCurrentFraction ) ; } if ( wedgeNode ?. attribute ( "type" ) ?. value ?. toLowerCase ( ) === "stop" ) { this . createNewMultiExpressionIfNeeded ( currentMeasure , inSourceMeasureCurrentFraction ) ; } else { this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; } this . addWedge ( wedgeNode , currentMeasure ) ; this . initialize ( ) ; } private interpretRehearsalMark ( rehearsalNode , currentMeasure , inSourceMeasureCurrentFraction , currentMeasureIndex ) { currentMeasure . rehearsalExpression = new RehearsalExpression ( rehearsalNode . value , this . placement ) ; } private createNewMultiExpressionIfNeeded ( currentMeasure , timestamp = undefined ) { if ( ! timestamp ) { timestamp = this . directionTimestamp ; } if ( ! this . getMultiExpression || this . getMultiExpression && ( this . getMultiExpression . SourceMeasureParent !== currentMeasure || ( this . getMultiExpression . SourceMeasureParent === currentMeasure && this . getMultiExpression . Timestamp !== timestamp ) ) ) { this . getMultiExpression = new MultiExpression ( currentMeasure , Fraction . createFromFraction ( timestamp ) ) ; currentMeasure . StaffLinkedExpressions [ this . globalStaffIndex ] . push ( this . getMultiExpression ) ; } } private createNewTempoExpressionIfNeeded ( currentMeasure ) { if ( ! this . currentMultiTempoExpression || this . currentMultiTempoExpression . SourceMeasureParent !== currentMeasure || this . currentMultiTempoExpression . Timestamp !== this . directionTimestamp ) { this . currentMultiTempoExpression = new MultiTempoExpression ( currentMeasure , Fraction . createFromFraction ( this . directionTimestamp ) ) ; currentMeasure . TempoExpressions . push ( this . currentMultiTempoExpression ) ; } } private addWedge ( wedgeNode , currentMeasure ) { if ( wedgeNode !== undefined && wedgeNode . hasAttributes ) { const type = wedgeNode . attribute ( "type" ) . value . toLowerCase ( ) ; try { if ( type === "crescendo" || type === "diminuendo" ) { const continuousDynamicExpression = new ContinuousDynamicExpression ( ContDynamicEnum [ type ] , this . placement , this . staffNumber , currentMeasure ) ; if ( this . openContinuousDynamicExpression ) { this . closeOpenContinuousDynamic ( ) ; } this . openContinuousDynamicExpression = continuousDynamicExpression ; this . getMultiExpression . StartingContinuousDynamic = continuousDynamicExpression ; continuousDynamicExpression . StartMultiExpression = this . getMultiExpression ; if ( this . activeInstantaneousDynamic !== undefined && this . activeInstantaneousDynamic . StaffNumber === continuousDynamicExpression . StaffNumber ) { this . activeInstantaneousDynamic = undefined ; } } else if ( type === "stop" ) { if ( this . openContinuousDynamicExpression ) { this . closeOpenContinuousDynamic ( ) ; } } } catch ( ex ) { const errorMsg = "ReaderErrorMessages/WedgeError" + ", Error while reading Crescendo / Diminuendo." ; this . musicSheet . SheetErrors . pushMeasureError ( errorMsg ) ; log . debug ( "ExpressionReader.addWedge" , errorMsg , ex ) ; } } } private fillMultiOrTempoExpression ( inputString , currentMeasure , fontStyle ) { if ( ! inputString ) { return ; } const tmpInputString = inputString . trim ( ) ; this . createExpressionFromString ( "" , tmpInputString , currentMeasure , inputString , fontStyle ) ; } private createExpressionFromString ( prefix , stringTrimmed , currentMeasure , inputString , fontStyle ) { if ( InstantaneousTempoExpression . isInputStringInstantaneousTempo ( stringTrimmed ) || ContinuousTempoExpression . isInputStringContinuousTempo ( stringTrimmed ) ) { if ( currentMeasure . TempoExpressions . length > 0 ) { for ( let idx = 0 , len = currentMeasure . TempoExpressions . length ; idx < len ; ++ idx ) { const multiTempoExpression = currentMeasure . TempoExpressions [ idx ] ; if ( multiTempoExpression . Timestamp === this . directionTimestamp && multiTempoExpression . InstantaneousTempo !== undefined && multiTempoExpression . InstantaneousTempo . Label . indexOf ( stringTrimmed ) !== - 1 ) { return false ; } } } this . createNewTempoExpressionIfNeeded ( currentMeasure ) ; this . currentMultiTempoExpression . CombinedExpressionsText = inputString ; if ( InstantaneousTempoExpression . isInputStringInstantaneousTempo ( stringTrimmed ) ) { const instantaneousTempoExpression = new InstantaneousTempoExpression ( stringTrimmed , this . placement , this . staffNumber , this . soundTempo , this . currentMultiTempoExpression ) ; this . currentMultiTempoExpression . addExpression ( instantaneousTempoExpression , prefix ) ; return true ; } if ( ContinuousTempoExpression . isInputStringContinuousTempo ( stringTrimmed ) ) { const continuousTempoExpression = new ContinuousTempoExpression ( stringTrimmed , this . placement , this . staffNumber , this . currentMultiTempoExpression ) ; this . currentMultiTempoExpression . addExpression ( continuousTempoExpression , prefix ) ; return true ; } } if ( ContinuousDynamicExpression . isInputStringContinuousDynamic ( stringTrimmed ) ) { this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; const continuousDynamicExpression = new ContinuousDynamicExpression ( undefined , this . placement , this . staffNumber , currentMeasure , stringTrimmed ) ; if ( this . openContinuousDynamicExpression !== undefined && ! this . openContinuousDynamicExpression . EndMultiExpression ) { this . closeOpenContinuousDynamic ( ) ; } if ( this . activeInstantaneousDynamic !== undefined && this . activeInstantaneousDynamic . StaffNumber === continuousDynamicExpression . StaffNumber ) { this . activeInstantaneousDynamic = undefined ; } this . openContinuousDynamicExpression = continuousDynamicExpression ; continuousDynamicExpression . StartMultiExpression = this . getMultiExpression ; this . getMultiExpression . addExpression ( continuousDynamicExpression , prefix ) ; return true ; } if ( MoodExpression . isInputStringMood ( stringTrimmed ) ) { this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; currentMeasure . hasMoodExpressions = true ; const moodExpression = new MoodExpression ( stringTrimmed , this . placement , this . staffNumber ) ; moodExpression . fontStyle = fontStyle ; this . getMultiExpression . addExpression ( moodExpression , prefix ) ; return true ; } this . createNewMultiExpressionIfNeeded ( currentMeasure ) ; if ( currentMeasure . TempoExpressions . length > 0 ) { for ( let idx = 0 , len = currentMeasure . TempoExpressions . length ; idx < len ; ++ idx ) { const multiTempoExpression = currentMeasure . TempoExpressions [ idx ] ; if ( multiTempoExpression . Timestamp === this . directionTimestamp && multiTempoExpression . InstantaneousTempo !== undefined && multiTempoExpression . EntriesList . length > 0 && ! this . hasDigit ( stringTrimmed ) ) { if ( this . globalStaffIndex > 0 ) { if ( multiTempoExpression . EntriesList [ 0 ] . label . indexOf ( stringTrimmed ) >= 0 ) { return false ; } else { break ; } } } } } let textAlignment = TextAlignmentEnum . CenterBottom ; if ( this . musicSheet . Rules . CompactMode ) { textAlignment = TextAlignmentEnum . LeftBottom ; } const unknownExpression = new UnknownExpression ( stringTrimmed , this . placement , textAlignment , this . staffNumber ) ; unknownExpression . fontStyle = fontStyle ; this . getMultiExpression . addExpression ( unknownExpression , prefix ) ; return false ; } private closeOpenContinuousDynamic ( ) { this . openContinuousDynamicExpression . EndMultiExpression = this . getMultiExpression ; this . getMultiExpression . EndingContinuousDynamic = this . openContinuousDynamicExpression ; this . openContinuousDynamicExpression = undefined ; } private closeOpenContinuousTempo ( endTimestamp ) { this . openContinuousTempoExpression . AbsoluteEndTimestamp = endTimestamp ; this . openContinuousTempoExpression = undefined ; } private checkIfWordsNodeIsRepetitionInstruction ( inputString ) { inputString = inputString . trim ( ) . toLowerCase ( ) ; if ( inputString === "coda" || inputString === "tocoda" || inputString === "to coda" || inputString === "fine" || inputString === "d.c." || inputString === "dacapo" || inputString === "da capo" || inputString === "d.s." || inputString === "dalsegno" || inputString === "dal segno" || inputString === "d.c. al fine" || inputString === "d.s. al fine" || inputString === "d.c. al coda" || inputString === "d.s. al coda" ) { return true ; } return false ; } private hasDigit ( input ) { return /\d/ . test ( input ) ; } }