Ver código fonte

feat(tempo): Add Graphical metronome mark (#468)

update skyline to prevent lyricist collision
EngravingRule for metronome mark x and y shift
Simon 6 anos atrás
pai
commit
6ff4fcbef2

+ 8 - 0
external/vexflow/vexflow.d.ts

@@ -212,6 +212,14 @@ declare namespace Vex {
             public addTimeSignature(sig: string): void;
 
             public setVoltaType(type: number, number_t: number, y: number): void;
+
+            public setTempo(tempo: Object, y: number): Stave;
+
+            public setShiftX(x: number): Stave;
+        }
+
+        export class StaveTempo extends StaveModifier { // needs Vexflow PR to be exported/usable
+            constructor(tempo: Object, x: number, shift_y: number);
         }
 
         export class Volta extends StaveModifier {

+ 1 - 1
src/MusicalScore/Graphical/BoundingBox.ts

@@ -38,7 +38,7 @@ export class BoundingBox {
      * Create a bounding box
      * @param dataObject Graphical object where the bounding box will be attached
      * @param parent Parent bounding box of an object in a higher hierarchy position
-     * @param isSymbol Defines the bounding box to be symbol thus not calculating it's boundaries by itself. NOTE: Borders need to be set!
+     * @param isSymbol Defines the bounding box to be symbol thus not calculating its boundaries by itself. NOTE: Borders need to be set!
      */
     constructor(dataObject: Object = undefined, parent: BoundingBox = undefined, isSymbol: boolean = false) {
         this.parent = parent;

+ 16 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -168,6 +168,8 @@ export class EngravingRules {
     private subMeasureXSpacingThreshold: number;
     private measureDynamicsMaxScalingFactor: number;
     private wholeRestXShiftVexflow: number;
+    private metronomeMarkXShift: number;
+    private metronomeMarkYShift: number;
     private maxInstructionsConstValue: number;
     private noteDistances: number[] = [1.0, 1.0, 1.3, 1.6, 2.0, 2.5, 3.0, 4.0];
     private noteDistancesScalingFactors: number[] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0];
@@ -384,6 +386,8 @@ export class EngravingRules {
         this.subMeasureXSpacingThreshold = 35;
         this.measureDynamicsMaxScalingFactor = 2.5;
         this.wholeRestXShiftVexflow = -2.5; // VexFlow draws rest notes too far to the right
+        this.metronomeMarkXShift = -6; // our unit, is taken * unitInPixels
+        this.metronomeMarkYShift = -0.5;
 
         // Render options (whether to render specific or invisible elements)
         this.coloringEnabled = true;
@@ -1290,6 +1294,18 @@ export class EngravingRules {
     public set WholeRestXShiftVexflow(value: number) {
         this.wholeRestXShiftVexflow = value;
     }
+    public get MetronomeMarkXShift(): number {
+        return this.metronomeMarkXShift;
+    }
+    public set MetronomeMarkXShift(value: number) {
+        this.metronomeMarkXShift = value;
+    }
+    public get MetronomeMarkYShift(): number {
+        return this.metronomeMarkYShift;
+    }
+    public set MetronomeMarkYShift(value: number) {
+        this.metronomeMarkYShift = value;
+    }
     public get MaxInstructionsConstValue(): number {
         return this.maxInstructionsConstValue;
     }

+ 11 - 10
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1419,29 +1419,26 @@ export abstract class MusicSheetCalculator {
                                                                        textAlignment);
 
                 if (entry.Expression instanceof InstantaneousTempoExpression) {
-                    let alreadyAdded: boolean = false;
+                    //already added?
                     for (const expr of staffLine.AbstractExpressions) {
                         if (expr instanceof GraphicalInstantaneousTempoExpression &&
                             (expr.SourceExpression as AbstractTempoExpression).Label === entry.Expression.Label) {
-                            alreadyAdded = true;
+                            //already added
+                            continue;
                         }
                     }
 
-                    if (alreadyAdded) {
-                        continue;
-                    }
-
                     const graphicalTempoExpr: GraphicalInstantaneousTempoExpression = new GraphicalInstantaneousTempoExpression(entry.Expression, graphLabel);
                     if (graphicalTempoExpr.ParentStaffLine === undefined) {
                         log.warn("Adding staffline didn't work");
-                        // I am actually fooling the linter her and use the created object. This method needs refactoring,
-                        // all graphical expression creations should be in one place and ahve basic stuff like labels, lines, ...
+                        // I am actually fooling the linter here and use the created object. This method needs refactoring,
+                        // all graphical expression creations should be in one place and have basic stuff like labels, lines, ...
                         // in their constructor
                     }
                     // in case of metronome mark:
                     if ((entry.Expression as InstantaneousTempoExpression).Enum === TempoEnum.metronomeMark) {
-                        // use smaller font:
-                        graphLabel.Label.fontHeight = 1.2;
+                        this.createMetronomeMark((entry.Expression as InstantaneousTempoExpression));
+                        continue;
                     }
                 } else if (entry.Expression instanceof ContinuousTempoExpression) {
                     // FIXME: Not yet implemented
@@ -1463,6 +1460,10 @@ export abstract class MusicSheetCalculator {
         }
     }
 
+    protected createMetronomeMark(metronomeExpression: InstantaneousTempoExpression): void {
+        throw new Error("abstract, not implemented");
+    }
+
     protected graphicalMeasureCreatedCalculations(measure: GraphicalMeasure): void {
         return;
     }

+ 1 - 1
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -228,7 +228,7 @@ export class SkyBottomLineCalculator {
 
     /**
      * This method updates the SkyLine for a given range with a given value
-     * @param  to update the SkyLine for
+     * //param  to update the SkyLine for
      * @param start Start index of the range
      * @param end End index of the range
      * @param value ??

+ 24 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -45,6 +45,7 @@ import { GraphicalSlur } from "../GraphicalSlur";
 import { BoundingBox } from "../BoundingBox";
 import { ContinuousDynamicExpression } from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
 import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
+import { InstantaneousTempoExpression } from "../../VoiceData/Expressions";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -470,6 +471,29 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
   }
 
+  protected createMetronomeMark(metronomeExpression: InstantaneousTempoExpression): void {
+    const vfStave: Vex.Flow.Stave = (this.graphicalMusicSheet.MeasureList[0][0] as VexFlowMeasure).getVFStave();
+    //vfStave.addModifier(new Vex.Flow.StaveTempo( // needs Vexflow PR
+    vfStave.setTempo(
+      {
+          bpm: metronomeExpression.TempoInBpm,
+          dots: metronomeExpression.dotted,
+          //duration: metronomeExpression.beatUnit
+          duration: "q"
+      },
+      EngravingRules.Rules.MetronomeMarkYShift * unitInPixels);
+       // -50, -30), 0); //needs Vexflow PR
+       //.setShiftX(-50);
+
+    (<any>vfStave.getModifiers()[vfStave.getModifiers().length - 1]).setShiftX(
+      EngravingRules.Rules.MetronomeMarkXShift * unitInPixels
+    );
+    // TODO calculate bounding box of metronome mark instead of hacking skyline to fix lyricist collision
+    const skyline: number[] = this.graphicalMusicSheet.MeasureList[0][0].ParentStaffLine.SkyLine;
+    skyline[0] = Math.min(skyline[0], -4.5 + EngravingRules.Rules.MetronomeMarkYShift);
+    // somehow this is called repeatedly in Clementi, so skyline[0] = Math.min instead of -=
+  }
+
   /**
    * Calculate a single OctaveShift for a [[MultiExpression]].
    * @param sourceMeasure

+ 6 - 7
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -157,28 +157,27 @@ export class ExpressionReader {
         let dirContentNode: IXmlElement = dirNode.element("metronome");
         if (dirContentNode !== undefined) {
             const beatUnit: IXmlElement = dirContentNode.element("beat-unit");
-            const hasDot: boolean = dirContentNode.element("beat-unit-dot") !== undefined;
+            const dotted: boolean = dirContentNode.element("beat-unit-dot") !== undefined;
             const bpm: IXmlElement = dirContentNode.element("per-minute");
+            // TODO check print-object = false -> don't render invisible metronome mark
             if (beatUnit !== undefined && bpm !== undefined) {
                 const useCurrentFractionForPositioning: boolean = (dirContentNode.hasAttributes && dirContentNode.attribute("default-x") !== undefined);
                 if (useCurrentFractionForPositioning) {
                     this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
                 }
-                let text: string = beatUnit.value + " = " + bpm.value;
-                if (hasDot) {
-                    text = "dotted " + text;
-                }
                 const bpmNumber: number = parseInt(bpm.value, 10);
                 this.createNewTempoExpressionIfNeeded(currentMeasure);
                 const instantaneousTempoExpression: InstantaneousTempoExpression =
-                    new InstantaneousTempoExpression(text,
+                    new InstantaneousTempoExpression(undefined,
                                                      this.placement,
                                                      this.staffNumber,
                                                      bpmNumber,
                                                      this.currentMultiTempoExpression,
                                                      true);
+                instantaneousTempoExpression.dotted = dotted;
+                instantaneousTempoExpression.beatUnit = beatUnit.value;
                 this.currentMultiTempoExpression.addExpression(instantaneousTempoExpression, "");
-                this.currentMultiTempoExpression.CombinedExpressionsText = text;
+                this.currentMultiTempoExpression.CombinedExpressionsText = "test";
             }
             return;
         }

+ 11 - 0
src/MusicalScore/VoiceData/Expressions/InstantaneousTempoExpression.ts

@@ -7,9 +7,15 @@ import {MultiTempoExpression} from "./MultiTempoExpression";
 export class InstantaneousTempoExpression extends AbstractTempoExpression {
     constructor(label: string, placement: PlacementEnum, staffNumber: number,
                 soundTempo: number, parentMultiTempoExpression: MultiTempoExpression, isMetronomeMark: boolean = false) {
+        /*if (isMetronomeMark) {
+            label = " = " + soundTempo;
+        }*/
         super(label, placement, staffNumber, parentMultiTempoExpression);
         this.setTempoAndTempoType(soundTempo);
     }
+
+    public dotted: boolean;
+    public beatUnit: string;
     private static listInstantaneousTempoLarghissimo: string[] = ["Larghissimo", "Sehr breit", "very, very slow"]; // }), TempoEnum.larghissimo);
     private static listInstantaneousTempoGrave: string[] = ["Grave", "Schwer", "slow and solemn"]; //  }), TempoEnum.grave);
     private static listInstantaneousTempoLento: string[] = ["Lento", "Lent", "Langsam", "slowly"]; //  }), TempoEnum.lento);
@@ -261,6 +267,11 @@ export class InstantaneousTempoExpression extends AbstractTempoExpression {
         return Fraction.plus(this.ParentMultiTempoExpression.SourceMeasureParent.AbsoluteTimestamp, this.ParentMultiTempoExpression.Timestamp).RealValue;
     }
     private setTempoAndTempoType(soundTempo: number): void {
+        if (!this.label) {
+            this.tempoInBpm = soundTempo;
+            this.tempoEnum = TempoEnum.metronomeMark;
+            return;
+        }
         if (InstantaneousTempoExpression.isStringInStringList(InstantaneousTempoExpression.listInstantaneousTempoLarghissimo, this.label)) {
             if (soundTempo === 0) {
                 soundTempo = InstantaneousTempoExpression.getDefaultValueForTempoType(TempoEnum.larghissimo);