VexFlow - Copyright (c) Mohit Muthanna 2010.

Description

This file implements the formatting and layout algorithms that are used to position notes in a voice. The algorithm can align multiple voices both within a stave, and across multiple staves.

To do this, the formatter breaks up voices into a grid of rational-valued ticks, to which each note is assigned. Then, minimum widths are assigned to each tick based on the widths of the notes and modifiers in that tick. This establishes the smallest amount of space required for each tick.

Finally, the formatter distributes the left over space proportionally to all the ticks, setting the x values of the notes in each tick.

See tests/formatter_tests.js for usage examples. The helper functions included here (FormatAndDraw, FormatAndDrawTab) also serve as useful usage examples.

import { Vex } from './vex';
import { Beam } from './beam';
import { Flow } from './tables';
import { Fraction } from './fraction';
import { Voice } from './voice';
import { StaveConnector } from './staveconnector';
import { StaveNote } from './stavenote';
import { Note } from './note';
import { ModifierContext } from './modifiercontext';
import { TickContext } from './tickcontext';

To enable logging for this class. Set Vex.Flow.Formatter.DEBUG to true.

function L(...args) { if (Formatter.DEBUG) Vex.L('Vex.Flow.Formatter', args); }

Helper function to locate the next non-rest note(s).

function lookAhead(notes, restLine, i, compare) {

If no valid next note group, nextRestLine is same as current.

  let nextRestLine = restLine;

Get the rest line for next valid non-rest note group.

  for (i += 1; i < notes.length; i += 1) {
    const note = notes[i];
    if (!note.isRest() && !note.shouldIgnoreTicks()) {
      nextRestLine = note.getLineForRest();
      break;
    }
  }

Locate the mid point between two lines.

  if (compare && restLine !== nextRestLine) {
    const top = Math.max(restLine, nextRestLine);
    const bot = Math.min(restLine, nextRestLine);
    nextRestLine = Vex.MidLine(top, bot);
  }
  return nextRestLine;
}

Take an array of voices and place aligned tickables in the same context. Returns a mapping from tick to ContextType, a list of ticks, and the resolution multiplier.

Params:

function createContexts(voices, ContextType, addToContext) {
  if (!voices || !voices.length) {
    throw new Vex.RERR('BadArgument', 'No voices to format');
  }

Find out highest common multiple of resolution multipliers. The purpose of this is to find out a common denominator for all fractional tick values in all tickables of all voices, so that the values can be expanded and the numerator used as an integer tick value.

  const totalTicks = voices[0].getTotalTicks();
  const resolutionMultiplier = voices.reduce((resolutionMultiplier, voice) => {
    if (!voice.getTotalTicks().equals(totalTicks)) {
      throw new Vex.RERR(
        'TickMismatch', 'Voices should have same total note duration in ticks.'
       );
    }

    if (voice.getMode() === Voice.Mode.STRICT && !voice.isComplete()) {
      throw new Vex.RERR(
        'IncompleteVoice', 'Voice does not have enough notes.'
      );
    }

    return Math.max(
      resolutionMultiplier,
      Fraction.LCM(resolutionMultiplier, voice.getResolutionMultiplier())
    );
  }, 1);

Initialize tick maps.

  const tickToContextMap = {};
  const tickList = [];
  const contexts = [];

For each voice, extract notes and create a context for every new tick that hasn’t been seen before.

  voices.forEach(voice => {

Use resolution multiplier as denominator to expand ticks to suitable integer values, so that no additional expansion of fractional tick values is needed.

    const ticksUsed = new Fraction(0, resolutionMultiplier);

    voice.getTickables().forEach(tickable => {
      const integerTicks = ticksUsed.numerator;

If we have no tick context for this tick, create one.

      if (!tickToContextMap[integerTicks]) {
        const newContext = new ContextType();
        contexts.push(newContext);
        tickToContextMap[integerTicks] = newContext;
      }

Add this tickable to the TickContext.

      addToContext(tickable, tickToContextMap[integerTicks]);

Maintain a sorted list of tick contexts.

      tickList.push(integerTicks);
      ticksUsed.add(tickable.getTicks());
    });
  });

  return {
    map: tickToContextMap,
    array: contexts,
    list: Vex.SortAndUnique(tickList, (a, b) => a - b, (a, b) => a === b),
    resolutionMultiplier,
  };
}

export class Formatter {

Helper function to layout “notes” one after the other without regard for proportions. Useful for tests and debugging.

  static SimpleFormat(notes, x = 0) {
    notes.reduce((x, note) => {
      note.addToModifierContext(new ModifierContext());
      const tick = new TickContext().addTickable(note).preFormat();
      const extra = tick.getExtraPx();
      tick.setX(x + extra.left);

      return x + tick.getWidth() + extra.right + 10;
    }, x);
  }

Helper function to plot formatter debug info.

  static plotDebugging(ctx, formatter, xPos, y1, y2) {
    const x = xPos + Note.STAVEPADDING;
    const contextGaps = formatter.contextGaps;
    function stroke(x1, x2, color) {
      ctx.beginPath();
      ctx.setStrokeStyle(color);
      ctx.setFillStyle(color);
      ctx.setLineWidth(1);
      ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
    }

    ctx.save();
    ctx.setFont('Arial', 8, '');

    contextGaps.gaps.forEach(gap => {
      stroke(x + gap.x1, x + gap.x2, '#aaa');

Vex.drawDot(ctx, xPos + gap.x1, yPos, ‘blue’);

      ctx.fillText(Math.round(gap.x2 - gap.x1), x + gap.x1, y2 + 12);
    });

    ctx.fillText(Math.round(contextGaps.total) + 'px', x - 20, y2 + 12);
    ctx.setFillStyle('red');

    ctx.fillText('Loss: ' +
      formatter.lossHistory.map(loss => Math.round(loss)), x - 20, y2 + 22);
    ctx.restore();
  }

Helper function to format and draw a single voice. Returns a bounding box for the notation.

Parameters:

autobeam automatically generates beams for the notes. align_rests aligns rests with nearby notes.

  static FormatAndDraw(ctx, stave, notes, params) {
    const options = {
      auto_beam: false,
      align_rests: false,
    };

    if (typeof params === 'object') {
      Vex.Merge(options, params);
    } else if (typeof params === 'boolean') {
      options.auto_beam = params;
    }

Start by creating a voice and adding all the notes to it.

    const voice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(notes);

Then create beams, if requested.

    const beams = options.auto_beam ? Beam.applyAndGetBeams(voice) : [];

Instantiate a Formatter and format the notes.

    new Formatter()
      .joinVoices([voice], { align_rests: options.align_rests })
      .formatToStave([voice], stave, { align_rests: options.align_rests, stave });

Render the voice and beams to the stave.

    voice.setStave(stave).draw(ctx, stave);
    beams.forEach(beam => beam.setContext(ctx).draw());

Return the bounding box of the voice.

    return voice.getBoundingBox();
  }

Helper function to format and draw aligned tab and stave notes in two separate staves.

Parameters:

  static FormatAndDrawTab(ctx, tabstave, stave, tabnotes, notes, autobeam, params) {
    const opts = {
      auto_beam: autobeam,
      align_rests: false,
    };

    if (typeof params === 'object') {
      Vex.Merge(opts, params);
    } else if (typeof params === 'boolean') {
      opts.auto_beam = params;
    }

Create a 4/4 voice for notes.

    const notevoice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(notes);

Create a 4/4 voice for tabnotes.

    const tabvoice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(tabnotes);

Then create beams, if requested.

    const beams = opts.auto_beam ? Beam.applyAndGetBeams(notevoice) : [];

Instantiate a Formatter and align tab and stave notes.

    new Formatter()
      .joinVoices([notevoice], { align_rests: opts.align_rests })
      .joinVoices([tabvoice])
      .formatToStave([notevoice, tabvoice], stave, { align_rests: opts.align_rests });

Render voices and beams to staves.

    notevoice.draw(ctx, stave);
    tabvoice.draw(ctx, tabstave);
    beams.forEach(beam => beam.setContext(ctx).draw());

Draw a connector between tab and note staves.

    new StaveConnector(stave, tabstave).setContext(ctx).draw();
  }

Auto position rests based on previous/next note positions.

Params:

  static AlignRestsToNotes(notes, alignAllNotes, alignTuplets) {
    notes.forEach((note, index) => {
      if (note instanceof StaveNote && note.isRest()) {
        if (note.tuplet && !alignTuplets) return;

If activated rests not on default can be rendered as specified.

        const position = note.getGlyph().position.toUpperCase();
        if (position !== 'R/4' && position !== 'B/4') return;

        if (alignAllNotes || note.beam != null) {

Align rests with previous/next notes.

          const props = note.getKeyProps()[0];
          if (index === 0) {
            props.line = lookAhead(notes, props.line, index, false);
            note.setKeyLine(0, props.line);
          } else if (index > 0 && index < notes.length) {

If previous note is a rest, use its line number.

            let restLine;
            if (notes[index - 1].isRest()) {
              restLine = notes[index - 1].getKeyProps()[0].line;
              props.line = restLine;
            } else {
              restLine = notes[index - 1].getLineForRest();

Get the rest line for next valid non-rest note group.

              props.line = lookAhead(notes, restLine, index, true);
            }
            note.setKeyLine(0, props.line);
          }
        }
      }
    });

    return this;
  }

  constructor() {

Minimum width required to render all the notes in the voices.

    this.minTotalWidth = 0;

This is set to true after minTotalWidth is calculated.

    this.hasMinTotalWidth = false;

Total number of ticks in the voice.

    this.totalTicks = new Fraction(0, 1);

Arrays of tick and modifier contexts.

    this.tickContexts = null;
    this.modiferContexts = null;

Gaps between contexts, for free movement of notes post formatting.

    this.contextGaps = {
      total: 0,
      gaps: [],
    };

    this.voices = [];
  }

Find all the rests in each of the voices and align them to neighboring notes. If alignAllNotes is false, then only align non-beamed notes.

  alignRests(voices, alignAllNotes) {
    if (!voices || !voices.length) {
      throw new Vex.RERR('BadArgument', 'No voices to format rests');
    }

    voices.forEach(voice =>
      Formatter.AlignRestsToNotes(voice.getTickables(), alignAllNotes));
  }

Calculate the minimum width required to align and format voices.

  preCalculateMinTotalWidth(voices) {

Cache results.

    if (this.hasMinTotalWidth) return this.minTotalWidth;

Create tick contexts if not already created.

    if (!this.tickContexts) {
      if (!voices) {
        throw new Vex.RERR(
          'BadArgument', "'voices' required to run preCalculateMinTotalWidth"
        );
      }

      this.createTickContexts(voices);
    }

    const { list: contextList, map: contextMap } = this.tickContexts;

Go through each tick context and calculate total width.

    this.minTotalWidth = contextList
      .map(tick => {
        const context = contextMap[tick];
        context.preFormat();
        return context.getWidth();
      })
      .reduce((a, b) => a + b, 0);

    this.hasMinTotalWidth = true;

    return this.minTotalWidth;
  }

Get minimum width required to render all voices. Either format or preCalculateMinTotalWidth must be called before this method.

  getMinTotalWidth() {
    if (!this.hasMinTotalWidth) {
      throw new Vex.RERR(
        'NoMinTotalWidth',
        "Call 'preCalculateMinTotalWidth' or 'preFormat' before calling 'getMinTotalWidth'"
      );
    }

    return this.minTotalWidth;
  }

Create ModifierContexts for each tick in voices.

  createModifierContexts(voices) {
    const contexts = createContexts(
      voices,
      ModifierContext,
      (tickable, context) => tickable.addToModifierContext(context)
    );

    this.modiferContexts = contexts;
    return contexts;
  }

Create TickContexts for each tick in voices. Also calculate the total number of ticks in voices.

  createTickContexts(voices) {
    const contexts = createContexts(
      voices,
      TickContext,
      (tickable, context) => context.addTickable(tickable)
    );

    contexts.array.forEach(context => {
      context.tContexts = contexts.array;
    });

    this.totalTicks = voices[0].getTicksUsed().clone();
    this.tickContexts = contexts;
    return contexts;
  }

This is the core formatter logic. Format voices and justify them to justifyWidth pixels. renderingContext is required to justify elements that can’t retreive widths without a canvas. This method sets the x positions of all the tickables/notes in the formatter.

  preFormat(justifyWidth = 0, renderingContext, voices, stave) {

Initialize context maps.

    const contexts = this.tickContexts;
    const { list: contextList, map: contextMap, resolutionMultiplier } = contexts;

If voices and a stave were provided, set the Stave for each voice and preFormat to apply Y values to the notes;

    if (voices && stave) {
      voices.forEach(voice => voice.setStave(stave).preFormat());
    }

Now distribute the ticks to each tick context, and assign them their own X positions.

    let x = 0;
    let shift = 0;
    const centerX = justifyWidth / 2;
    this.minTotalWidth = 0;

Pass 1: Give each note maximum width requested by context.

    contextList.forEach((tick) => {
      const context = contextMap[tick];
      if (renderingContext) context.setContext(renderingContext);

Make sure that all tickables in this context have calculated their space requirements.

      context.preFormat();

      const width = context.getWidth();
      this.minTotalWidth += width;

      const metrics = context.getMetrics();
      x = x + shift + metrics.extraLeftPx;
      context.setX(x);

Calculate shift for the next tick.

      shift = width - metrics.extraLeftPx;
    });

    this.minTotalWidth = x + shift;
    this.hasMinTotalWidth = true;

No justification needed. End formatting.

    if (justifyWidth <= 0) return;

Pass 2: Take leftover width, and distribute it to proportionately to all notes.

    const remainingX = justifyWidth - this.minTotalWidth;
    const leftoverPxPerTick = remainingX / (this.totalTicks.value() * resolutionMultiplier);
    let spaceAccum = 0;

    contextList.forEach((tick, index) => {
      const prevTick = contextList[index - 1] || 0;
      const context = contextMap[tick];
      const tickSpace = (tick - prevTick) * leftoverPxPerTick;

      spaceAccum += tickSpace;
      context.setX(context.getX() + spaceAccum);

Move center aligned tickables to middle

      context
        .getCenterAlignedTickables()
        .forEach(tickable => { // eslint-disable-line
          tickable.center_x_shift = centerX - context.getX();
        });
    });

Just one context. Done formatting.

    if (contextList.length === 1) return;

    this.justifyWidth = justifyWidth;
    this.lossHistory = [];
    this.evaluate();
  }

Calculate the total cost of this formatting decision.

  evaluate() {
    const justifyWidth = this.justifyWidth;

Calculate available slack per tick context. This works out how much freedom to move a context has in either direction, without affecting other notes.

    this.contextGaps = { total: 0, gaps: [] };
    this.tickContexts.list.forEach((tick, index) => {
      if (index === 0) return;
      const prevTick = this.tickContexts.list[index - 1];
      const prevContext = this.tickContexts.map[prevTick];
      const context = this.tickContexts.map[tick];
      const prevMetrics = prevContext.getMetrics();

      const insideRightEdge = prevContext.getX() + prevMetrics.width;
      const insideLeftEdge = context.getX();
      const gap = insideLeftEdge - insideRightEdge;
      this.contextGaps.total += gap;
      this.contextGaps.gaps.push({ x1: insideRightEdge, x2: insideLeftEdge });

Tell the tick contexts how much they can reposition themselves.

      context.getFormatterMetrics().freedom.left = gap;
      prevContext.getFormatterMetrics().freedom.right = gap;
    });

Calculate mean distance in each voice for each duration type, then calculate how far each note is from the mean.

    const durationStats = this.durationStats = {};

    function updateStats(duration, space) {
      const stats = durationStats[duration];
      if (stats === undefined) {
        durationStats[duration] = { mean: space, count: 1 };
      } else {
        stats.count += 1;
        stats.mean = (stats.mean + space) / 2;
      }
    }

    this.voices.forEach(voice => {
      voice.getTickables().forEach((note, i, notes) => {
        const duration = note.getTicks().clone().simplify().toString();
        const metrics = note.getMetrics();
        const formatterMetrics = note.getFormatterMetrics();
        const leftNoteEdge = note.getX() + metrics.noteWidth +
            metrics.modRightPx + metrics.extraRightPx;
        let space = 0;

        if (i < (notes.length - 1)) {
          const rightNote = notes[i + 1];
          const rightMetrics = rightNote.getMetrics();
          const rightNoteEdge = rightNote.getX() -
            rightMetrics.modLeftPx - rightMetrics.extraLeftPx;

          space = rightNoteEdge - leftNoteEdge;
          formatterMetrics.space.used = rightNote.getX() - note.getX();
          rightNote.getFormatterMetrics().freedom.left = space;
        } else {
          space = justifyWidth - leftNoteEdge;
          formatterMetrics.space.used = justifyWidth - note.getX();
        }

        formatterMetrics.freedom.right = space;
        updateStats(duration, formatterMetrics.space.used);
      });
    });

Calculate how much each note deviates from the mean. Loss function is square root of the sum of squared deviations.

    let totalDeviation = 0;
    this.voices.forEach(voice => {
      voice.getTickables().forEach((note) => {
        const duration = note.getTicks().clone().simplify().toString();
        const metrics = note.getFormatterMetrics();
        metrics.iterations += 1;
        metrics.space.deviation = metrics.space.used - durationStats[duration].mean;
        metrics.duration = duration;
        metrics.space.mean = durationStats[duration].mean;

        totalDeviation += Math.pow(durationStats[duration].mean, 2);
      });
    });

    this.totalCost = Math.sqrt(totalDeviation);
    this.lossHistory.push(this.totalCost);
    return this;
  }

Run a single iteration of rejustification. At a high level, this method calculates the overall “loss” (or cost) of this layout, and repositions tickcontexts in an attempt to reduce the cost. You can call this method multiple times until it finds and oscillates around a global minimum.

  tune() {
    const sum = (means) => means.reduce((a, b) => a + b);

Move current tickcontext by shift pixels, and adjust the freedom on adjacent tickcontexts.

    function move(current, prev, next, shift) {
      current.setX(current.getX() + shift);
      current.getFormatterMetrics().freedom.left += shift;
      current.getFormatterMetrics().freedom.right -= shift;

      if (prev) prev.getFormatterMetrics().freedom.right += shift;
      if (next) next.getFormatterMetrics().freedom.left -= shift;
    }

    let shift = 0;
    this.tickContexts.list.forEach((tick, index, list) => {
      const context = this.tickContexts.map[tick];
      const prevContext = (index > 0) ? this.tickContexts.map[list[index - 1]] : null;
      const nextContext = (index < list.length - 1) ? this.tickContexts.map[list[index + 1]] : null;

      move(context, prevContext, nextContext, shift);

      const cost = -sum(
        context.getTickables().map(t => t.getFormatterMetrics().space.deviation));

      if (cost > 0) {
        shift = -Math.min(context.getFormatterMetrics().freedom.right, Math.abs(cost));
      } else if (cost < 0) {
        if (nextContext) {
          shift = Math.min(nextContext.getFormatterMetrics().freedom.right, Math.abs(cost));
        } else {
          shift = 0;
        }
      }

      const minShift = Math.min(5, Math.abs(shift));
      shift = shift > 0 ? minShift : -minShift;
    });

    return this.evaluate();
  }

This is the top-level call for all formatting logic completed after x and y values have been computed for the notes in the voices.

  postFormat() {
    const postFormatContexts = (contexts) =>
      contexts.list.forEach(tick => contexts.map[tick].postFormat());

    postFormatContexts(this.modiferContexts);
    postFormatContexts(this.tickContexts);

    return this;
  }

Take all voices and create ModifierContexts out of them. This tells the formatters that the voices belong on a single stave.

  joinVoices(voices) {
    this.createModifierContexts(voices);
    this.hasMinTotalWidth = false;
    return this;
  }

Align rests in voices, justify the contexts, and position the notes so voices are aligned and ready to render onto the stave. This method mutates the x positions of all tickables in voices.

Voices are full justified to fit in justifyWidth pixels.

Set options.context to the rendering context. Set options.align_rests to true to enable rest alignment.

  format(voices, justifyWidth, options) {
    const opts = {
      align_rests: false,
      context: null,
      stave: null,
    };

    Vex.Merge(opts, options);
    this.voices = voices;
    this.alignRests(voices, opts.align_rests);
    this.createTickContexts(voices);
    this.preFormat(justifyWidth, opts.context, voices, opts.stave);

Only postFormat if a stave was supplied for y value formatting

    if (opts.stave) this.postFormat();

    return this;
  }

This method is just like format except that the justifyWidth is inferred from the stave.

  formatToStave(voices, stave, options) {
    const justifyWidth = stave.getNoteEndX() - stave.getNoteStartX() - 10;
    L('Formatting voices to width: ', justifyWidth);
    const opts = { context: stave.getContext() };
    Vex.Merge(opts, options);
    return this.format(voices, justifyWidth, opts);
  }
}
h