Skip to content

Commit

Permalink
feat(autoBeam): add option to automatically beam notes
Browse files Browse the repository at this point in the history
add OSMDOptions.autoBeam and autoBeamOptions
add OSMDOptions.AutoBeamOptions interface
add autoBeam EngravingRules

vexflowMeasure: add autoBeamNotes()

demo: sort function tests alphabetically, add optional autoBeam settings
  • Loading branch information
sschmidTU committed Oct 18, 2018
1 parent cf2111e commit 09170a2
Show file tree
Hide file tree
Showing 7 changed files with 1,845 additions and 7 deletions.
15 changes: 12 additions & 3 deletions demo/index.js
Expand Up @@ -22,13 +22,14 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
"Mozart, W.A.- Clarinet Quintet (Excerpt)": "Mozart_Clarinet_Quintet_Excerpt.mxl",
"Mozart/Holzer - Land der Berge (national anthem of Austria)": "Land_der_Berge.musicxml",
"OSMD Function Test - All": "OSMD_function_test_all.xml",
"OSMD Function Test - Grace Notes": "OSMD_function_test_GraceNotes.xml",
"OSMD Function Test - Ornaments": "OSMD_function_test_Ornaments.xml",
"OSMD Function Test - Autobeam": "OSMD_function_test_autobeam.musicxml",
"OSMD Function Test - Accidentals": "OSMD_function_test_accidentals.musicxml",
"OSMD Function Test - Drumset": "OSMD_function_test_drumset.musicxml",
"OSMD Function Test - Expressions": "OSMD_function_test_expressions.musicxml",
"OSMD Function Test - Expressions Overlap": "OSMD_function_test_expressions_overlap.musicxml",
"OSMD Function Test - Grace Notes": "OSMD_function_test_GraceNotes.xml",
"OSMD Function Test - NoteHeadShapes": "OSMD_function_test_noteHeadShapes.musicxml",
"OSMD Function Test - Drumset": "OSMD_function_test_drumset.musicxml",
"OSMD Function Test - Ornaments": "OSMD_function_test_Ornaments.xml",
"Schubert, F. - An Die Musik": "Schubert_An_die_Musik.xml",
"Actor, L. - Prelude (Sample)": "ActorPreludeSample.xml",
"Anonymous - Saltarello": "Saltarello.mxl",
Expand Down Expand Up @@ -152,6 +153,14 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
// fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
setWantedStemDirectionByXml: true, // try false, which was previously the default behavior

autoBeam: false, // try true, OSMD Function Test AutoBeam sample
autoBeamOptions: {
beam_rests: false,
beam_middle_rests_only: false,
//groups: [[3,4], [1,1]],
maintain_stem_directions: false
},

// tupletsBracketed: true, // creates brackets for all tuplets except triplets, even when not set by xml
// tripletsBracketed: true,
// tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
Expand Down
6 changes: 5 additions & 1 deletion external/vexflow/vexflow.d.ts
Expand Up @@ -319,8 +319,12 @@ declare namespace Vex {
constructor(notes: StaveNote[], auto_stem: boolean);

public setContext(ctx: RenderContext): Beam;

public draw(): void;
public static generateBeams(notes: Vex.Flow.StemmableNote[], optionsObject?: any): Beam[];
}

export class Fraction { // Vex.Flow.Fraction, used for generateBeams
constructor(nominator: number, denominator: number);
}

export class Tuplet {
Expand Down
25 changes: 25 additions & 0 deletions src/MusicalScore/Graphical/EngravingRules.ts
Expand Up @@ -3,6 +3,7 @@ import { PagePlacementEnum } from "./GraphicalMusicPage";
import * as log from "loglevel";
import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";
import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
import { AutoBeamOptions } from "../../OpenSheetMusicDisplay/OSMDOptions";

export class EngravingRules {
private static rules: EngravingRules;
Expand Down Expand Up @@ -39,6 +40,10 @@ export class EngravingRules {
private betweenStaffDistance: number;
private staffHeight: number;
private betweenStaffLinesDistance: number;
/** Whether to automatically beam notes that don't already have beams in XML. */
private autoBeamNotes: boolean;
/** Options for autoBeaming like whether to beam over rests. See AutoBeamOptions interface. */
private autoBeamOptions: AutoBeamOptions;
private beamWidth: number;
private beamSpaceWidth: number;
private beamForwardLength: number;
Expand Down Expand Up @@ -219,6 +224,14 @@ export class EngravingRules {
this.minimumAllowedDistanceBetweenSystems = 3.0;
this.lastSystemMaxScalingFactor = 1.4;

// autoBeam options
this.autoBeamNotes = false;
this.autoBeamOptions = {
beam_middle_rests_only: false,
beam_rests: false,
maintain_stem_directions: false
};

// Beam Sizing Variables
this.beamWidth = EngravingRules.unit / 2.0;
this.beamSpaceWidth = EngravingRules.unit / 3.0;
Expand Down Expand Up @@ -558,6 +571,18 @@ export class EngravingRules {
public set BetweenStaffLinesDistance(value: number) {
this.betweenStaffLinesDistance = value;
}
public get AutoBeamNotes(): boolean {
return this.autoBeamNotes;
}
public set AutoBeamNotes(value: boolean) {
this.autoBeamNotes = value;
}
public get AutoBeamOptions(): AutoBeamOptions {
return this.autoBeamOptions;
}
public set AutoBeamOptions(value: AutoBeamOptions) {
this.autoBeamOptions = value;
}
public get BeamWidth(): number {
return this.beamWidth;
}
Expand Down
99 changes: 99 additions & 0 deletions src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts
Expand Up @@ -33,6 +33,7 @@ import {TechnicalInstruction} from "../../VoiceData/Instructions/TechnicalInstru
import {PlacementEnum} from "../../VoiceData/Expressions/AbstractExpression";
import {ArpeggioType} from "../../VoiceData/Arpeggio";
import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
import {AutoBeamOptions} from "../../../OpenSheetMusicDisplay/OSMDOptions";

export class VexFlowMeasure extends GraphicalMeasure {
constructor(staff: Staff, staffLine: StaffLine = undefined, sourceMeasure: SourceMeasure = undefined) {
Expand All @@ -57,6 +58,10 @@ export class VexFlowMeasure extends GraphicalMeasure {
private connectors: Vex.Flow.StaveConnector[] = [];
/** Intermediate object to construct beams */
private beams: { [voiceID: number]: [Beam, VexFlowVoiceEntry[]][]; } = {};
/** Beams created by (optional) autoBeam function. */
private autoVfBeams: Vex.Flow.Beam[];
/** Beams of tuplet notes created by (optional) autoBeam function. */
private autoTupletVfBeams: Vex.Flow.Beam[];
/** VexFlow Beams */
private vfbeams: { [voiceID: number]: Vex.Flow.Beam[]; };
/** Intermediate object to construct tuplets */
Expand Down Expand Up @@ -342,6 +347,17 @@ export class VexFlowMeasure extends GraphicalMeasure {
}
}
}
// Draw auto-generated beams from Beam.generateBeams()
if (this.autoVfBeams) {
for (const beam of this.autoVfBeams) {
beam.setContext(ctx).draw();
}
}
if (this.autoTupletVfBeams) {
for (const beam of this.autoTupletVfBeams) {
beam.setContext(ctx).draw();
}
}

// Draw tuplets
for (const voiceID in this.vftuplets) {
Expand Down Expand Up @@ -535,6 +551,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
// created them brand new. Is this needed? And more importantly,
// should the old beams be removed manually by the notes?
this.vfbeams = {};
const beamedNotes: StaveNote[] = []; // already beamed notes, will be ignored by this.autoBeamNotes()
for (const voiceID in this.beams) {
if (this.beams.hasOwnProperty(voiceID)) {
let vfbeams: Vex.Flow.Beam[] = this.vfbeams[voiceID];
Expand All @@ -558,6 +575,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
const note: Vex.Flow.StaveNote = ((<VexFlowVoiceEntry>entry).vfStaveNote as StaveNote);
if (note !== undefined) {
notes.push(note);
beamedNotes.push(note);
}
if (entry.parentVoiceEntry.IsGrace) {
isGraceBeam = true;
Expand All @@ -581,6 +599,87 @@ export class VexFlowMeasure extends GraphicalMeasure {
}
}
}
if (EngravingRules.Rules.AutoBeamNotes) {
this.autoBeamNotes(beamedNotes); // try to autobeam notes except those that are already beamed (beamedNotes).
}
}

/** Autobeams notes except beamedNotes, using Vexflow's Beam.generateBeams().
* Takes options from EngravingRules.Rules.AutoBeamOptions.
* @param beamedNotes notes that will not be autobeamed (because they are already beamed)
*/
private autoBeamNotes(beamedNotes: StemmableNote[]): void {
const notesToAutoBeam: StemmableNote[] = [];
let consecutiveBeamableNotes: StemmableNote[] = [];
let currentTuplet: Tuplet;
let tupletNotesToAutoBeam: StaveNote[] = [];
this.autoTupletVfBeams = [];
for (const staffEntry of this.staffEntries) {
for (const gve of staffEntry.graphicalVoiceEntries) {
const vfStaveNote: StaveNote = <StaveNote> (gve as VexFlowVoiceEntry).vfStaveNote;
if (gve.parentVoiceEntry.IsGrace || // don't beam grace notes
gve.notes[0].graphicalNoteLength.CompareTo(new Fraction(1, 4)) >= 0 || // don't beam quarter or longer notes
beamedNotes.contains(vfStaveNote)) { // don't beam already beamed notes
if (consecutiveBeamableNotes.length >= 2) { // don't beam notes surrounded by quarter notes etc.
for (const note of consecutiveBeamableNotes) {
notesToAutoBeam.push(note);
}
}
consecutiveBeamableNotes = [];
continue;
}

// create beams for tuplets separately
const noteTuplet: Tuplet = gve.notes[0].sourceNote.NoteTuplet;
if (noteTuplet) {
if (currentTuplet === undefined) {
currentTuplet = noteTuplet;
tupletNotesToAutoBeam.push(<StaveNote>(gve as VexFlowVoiceEntry).vfStaveNote);
} else {
if (currentTuplet === noteTuplet) {
tupletNotesToAutoBeam.push(<StaveNote>(gve as VexFlowVoiceEntry).vfStaveNote);
} else { // new tuplet, finish old one
if (tupletNotesToAutoBeam.length > 1) {
this.autoTupletVfBeams.push(new Vex.Flow.Beam(tupletNotesToAutoBeam, true));
}
tupletNotesToAutoBeam = [];
currentTuplet = noteTuplet;
tupletNotesToAutoBeam.push(<StaveNote>(gve as VexFlowVoiceEntry).vfStaveNote);
}
}
continue;
} else {
currentTuplet = undefined;
}

consecutiveBeamableNotes.push((gve as VexFlowVoiceEntry).vfStaveNote);
}
}
if (tupletNotesToAutoBeam.length >= 2) {
this.autoTupletVfBeams.push(new Vex.Flow.Beam(tupletNotesToAutoBeam, true));
}
if (consecutiveBeamableNotes.length >= 2) {
for (const note of consecutiveBeamableNotes) {
notesToAutoBeam.push(note);
}
}

// create options for generateBeams
const autoBeamOptions: AutoBeamOptions = EngravingRules.Rules.AutoBeamOptions;
const generateBeamOptions: any = {
beam_middle_only: autoBeamOptions.beam_middle_rests_only,
beam_rests: autoBeamOptions.beam_rests,
maintain_stem_directions: autoBeamOptions.maintain_stem_directions,
};
if (autoBeamOptions.groups && autoBeamOptions.groups.length) {
const groups: Vex.Flow.Fraction[] = [];
for (const fraction of autoBeamOptions.groups) {
groups.push(new Vex.Flow.Fraction(fraction[0], fraction[1]));
}
generateBeamOptions.groups = groups;
}

this.autoVfBeams = Vex.Flow.Beam.generateBeams(notesToAutoBeam, generateBeamOptions);
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/OpenSheetMusicDisplay/OSMDOptions.ts
Expand Up @@ -2,8 +2,10 @@ import { DrawingParametersEnum } from "../MusicalScore/Graphical/DrawingParamete

/** Possible options for the OpenSheetMusicDisplay constructor, none are mandatory. */
export interface IOSMDOptions {
/** Not yet supported. Will always beam automatically. */ // TODO
/** Whether to automatically create beams for notes that don't have beams set in XML. */
autoBeam?: boolean;
/** Options for autoBeaming like whether to beam over rests. See AutoBeamOptions interface. */
autoBeamOptions?: AutoBeamOptions;
/** Automatically resize score with canvas size. Default is true. */
autoResize?: boolean;
/** Not yet supported. Will always place stems automatically. */ // TODO
Expand Down Expand Up @@ -66,3 +68,17 @@ export class OSMDOptions {
};
}
}

export interface AutoBeamOptions {
/** Whether to extend beams over rests. Default false. */
beam_rests?: boolean;
/** Whether to extend beams only over rests that are in the middle of a potential beam. Default false. */
beam_middle_rests_only?: boolean;
/** Whether to maintain stem direction of autoBeamed notes. Discouraged, reduces beams. Default false. */
maintain_stem_directions?: boolean;
/** Groups of notes (fractions) to beam within a measure.
* List of fractions, each fraction being [nominator, denominator].
* E.g. [[3,4],[1,4]] will beam the first 3 quarters of a measure, then the last quarter.
*/
groups?: [number[]];
}
20 changes: 18 additions & 2 deletions src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts
Expand Up @@ -14,7 +14,7 @@ import {Promise} from "es6-promise";
import {AJAX} from "./AJAX";
import * as log from "loglevel";
import {DrawingParametersEnum, DrawingParameters} from "../MusicalScore/Graphical/DrawingParameters";
import {IOSMDOptions, OSMDOptions} from "./OSMDOptions";
import {IOSMDOptions, OSMDOptions, AutoBeamOptions} from "./OSMDOptions";
import {EngravingRules} from "../MusicalScore/Graphical/EngravingRules";
import {AbstractExpression} from "../MusicalScore/VoiceData/Expressions/AbstractExpression";

Expand Down Expand Up @@ -219,6 +219,23 @@ export class OpenSheetMusicDisplay {
}

// individual drawing parameters options
if (options.autoBeam !== undefined) {
EngravingRules.Rules.AutoBeamNotes = options.autoBeam;
}
const autoBeamOptions: AutoBeamOptions = options.autoBeamOptions;
if (autoBeamOptions) {
if (autoBeamOptions.maintain_stem_directions === undefined) {
autoBeamOptions.maintain_stem_directions = false;
}
EngravingRules.Rules.AutoBeamOptions = autoBeamOptions;
if (autoBeamOptions.groups && autoBeamOptions.groups.length) {
for (const fraction of autoBeamOptions.groups) {
if (fraction.length !== 2) {
throw new Error("Each fraction in autoBeamOptions.groups must be of length 2, e.g. [3,4] for beaming three fourths");
}
}
}
}
if (options.disableCursor) {
this.drawingParameters.drawCursors = false;
this.enableOrDisableCursor(this.drawingParameters.drawCursors);
Expand Down Expand Up @@ -459,6 +476,5 @@ export class OpenSheetMusicDisplay {
public set AutoResizeEnabled(value: boolean) {
this.autoResizeEnabled = value;
}

//#endregion
}

0 comments on commit 09170a2

Please sign in to comment.