Skip to content

Make octave-shift stop size automatic in mx::api #372

Description

@rpatters1

Problem

OttavaStart::ottavaType determines the complete octave shift, including its size:

  • 8va / 8vb → size="8"
  • 15ma / 15mb → size="15"
  • 22ma / 22mb → size="22"

However, OttavaStop separately exposes std::optional<int> size. Authors must repeat the start's size on the stop when they want it emitted. This creates two fields for one musical fact, and the values can contradict each other.

Omitting the stop size is valid MusicXML, but some importers, including MuseScore, expect it to be present. Authoring interoperable output should not depend on manually copying the start size to the stop.

Using OttavaType directly on OttavaStop would not be ideal. A MusicXML stop has no up/down direction of its own, so the reader cannot distinguish 8va from 8vb using the stop element alone.

Proposed Direction

Derive an octave-shift stop's size automatically from its matching OttavaStart.

The writer already performs a part-wide spanner pass through SpannerNumberResolver. That pass could be extended, or generalized, to associate each ottava stop with its start and make the resolved size available to DirectionWriter::emitOttavaStop.

The intended authoring behavior would be:

  1. Set OttavaStart::ottavaType.
  2. Add the corresponding OttavaStop.
  3. The writer emits the appropriate stop size automatically.

OttavaStop::size could then be removed or replaced with a fidelity policy such as:

Bool writeSize = Bool::unspecified;

Possible semantics:

  • unspecified: automatically write the size derived from the start.
  • yes: explicitly write the derived size.
  • no: omit the size attribute.

The reader could set yes or no according to whether the source stop explicitly contained size, preserving that spelling when required. Authored data would leave the field unspecified and get interoperable output automatically.

Design Considerations

  • This would be a breaking public API change if OttavaStop::size is removed.
  • Explicit, identity-based, and unspecified spanner numbers all need defined pairing behavior.
  • Pairing must work across measures and with overlapping octave shifts.
  • A dangling stop must have a harmless fallback and must not throw. Writing the MusicXML default size of 8 is one possible fallback.
  • If a source stop's size contradicts its matching start, the start should probably be authoritative and the output normalized.
  • MusicXML permits any positive integer for size, although mx::api models the conventional 8, 15, and 22 values. Nonstandard sizes therefore need a documented normalization policy.

A larger alternative would be to model an ottava as one span containing both endpoints and one OttavaType, but that is outside the scope of this change. (I have proposed this for other spanners as well, but it is a big refactor.)

Acceptance Criteria

  • An authored ottava requires its type/size to be specified only on OttavaStart
  • Matching stops automatically emit the start's size
  • 8va, 8vb, 15ma, 15mb, 22ma, and 22mb are covered
  • Pairing works across measures and for overlapping numbered or identity spanners
  • Source presence or absence of the stop size has a documented fidelity policy
  • Contradictory and dangling endpoints have defined, non-throwing behavior
  • Existing pinned API round trips remain passing
  • Focused reader/writer and full API round-trip tests cover the behavior

Acceptance Criteria

  • An authored ottava requires its type/size to be specified only on OttavaStart
  • Matching stops automatically emit the start's size
  • 8va, 8vb, 15ma, 15mb, 22ma, and 22mb are covered
  • Pairing works across measures and for overlapping numbered or identity spanners
  • Source presence or absence of the stop size has a documented fidelity policy
  • Contradictory and dangling endpoints have defined, non-throwing behavior
  • Existing pinned API round trips remain passing
  • Focused reader/writer and full API round-trip tests cover the behavior

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions