Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format voices across staves and parts #183

Merged
merged 71 commits into from
Dec 27, 2023
Merged

Format voices across staves and parts #183

merged 71 commits into from
Dec 27, 2023

Conversation

jaredjj3
Copy link
Collaborator

@jaredjj3 jaredjj3 commented Dec 12, 2023

This PR fixes #181, fixes #182.

vexml

multi_stave_single_part_formatting.musicxml

vexml_dev_0_0_0


multi_part_formatting.musicxml

vexml_dev_0_0_0

@jaredjj3 jaredjj3 self-assigned this Dec 12, 2023
@jaredjj3
Copy link
Collaborator Author

jaredjj3 commented Dec 16, 2023

I realized that I made a modeling mistake, but this should be straightforward to fix.

To properly format a rendering.Measure, I need to know all the vexflow.Voice and vexflow.Stave across all parts. I want to be able to do something like this:

new Formatter()
  .joinVoices([voice1]) // part 1
  .joinVoices([voice2]) // part 2
  .joinVoices([voice3, voice4]) // part 3
  .formatToStave([voice1, voice2, voice3, voice4], stave1);

Currently, the model is:

  • a system has many parts
  • a part has many measures
  • a measure has many measure fragments

With this structure, it's possible for a measure to know about its sibling part measures, but it has to originate from the system, the closest ancestor. I spent a great deal of effort to make rendering.* objects not depend on each other for creation and for them to be immutable. I would need pass the sibling part measures at measure and render times, which would be somewhat of a pain and error prone.

The model I need is:

  • a system has many measures
  • a measure has many measure fragments
  • a measure fragment has many parts

This allows measure fragments to format across all parts without having to pass around more context. This also simplifies Seed, since we would just need to figure out how to split Measure[] into System[] instead of a System[Part[]].

@rvilarl
Copy link
Collaborator

rvilarl commented Dec 16, 2023

You can joinVoices in steps, they are accumulative. And format and draw at the very end.
This is like System: addStave independently and draw in the very end.
@jaredjj3 What is a measure fragment? A voice?

@jaredjj3
Copy link
Collaborator Author

Behold! Formatting!

image

This PR was particularly difficult because I had to remodel Part to be a child of MeasureFragment out of necessity. This caused a lot of issues, and I needed to develop a new measure fragmentation algorithm which was difficult to get right.

I reviewed the new screenshots, and I'm feeling confident that I have ~correct implementation. Once I update and push those, the tests should start passing again in GitHub Actions.

There will inevitably be follow up bugs to clean up, but the hard part is done.

I'm putting the measure fragmentation algorithm notes here for posterity.


Measure Fragmentation Algorithm

Let’s say I have a list of parts that have measure fragment boundaries labeled by “X”.

   0 1 2 3 4
P1 - - - - -
P2 - X - -
P3 - - X - -
  • Parts don’t need to have the same number of measure entries.
  • A measure entry may be a directive to go forwards or backwards in terms of Divisions.

I want to partition them into measure fragments.

   0   1   2 3 4
P1 - | - | - - -
P2 - | X | - -
P3 - | - | X - -
  • Fragments end right before a boundary.
  • Measure entries are exhausted after the last boundary.

First, we need to declare all the boundaries in terms of divisions, since that is the common language amongst all parts.

interface MeasureEntryIterator {
  peek(): MeasureEntryIteration; // throws if next was never called
  next(): MeasureEntryIteration;
  getStaveSignature(): StaveSignature;
}

type MeasureEntryFragmentation = 'none' | 'new';

type MeasureEntryIteration = {
  done: true;
  value: null;
} | {
  done: false;
  value: {
    entry: MeasureEntry;
    start: Division;
    end: Division;
    fragmentation: MeasureEntryFragmentation;
  };
};

declare const iterator: MeasureEntryIterator;

const boundaries = new Array<Division>();

boundaries.push(Division.zero());

let iteration = iterator.next();

while (!iteration.done) {
  if (iteration.fragmentation === 'new') {
    boundaries.push(iteration.end);
  }
  iteration = iterator.next();
}

boundaries.push(Division.max());

// Make unique and sorted by beats across all parts.

Now, we should have an array of boundaries that look like this.

const boundaries = [
  Division.zero(),
  Division@(P2, 1),
  Division@(P3, 2),
  Division.max()
];

Those boundaries inform how much to collect from each part.

  • There is a measure fragment spanning from Division.zero() to Division@(P2, 1). For each part, take all of the measure entries starting within the range (end exclusive) and create a new measure fragment.
  • The next measure fragment is between Division@(P2, 1) and Division@(P3, 2). Same procedure as before.
  • The last measure fragment is between Division@(P3, 2) and Division.max();

The key insight for this part of the algorithm is that we only call MeasureEntryIterator.next if we consume it. Otherwise, we need to keep it around for a use in a different fragment.

// After creation, we need to call .next() on them to initialize them.
declare const iterators: Record<string, MeasureEntryIterator>;

for (const boundary of boundaries) {
  const staveSignatures = Array<PartScoped<StaveSignature>>();
  const measureEntries = Array<PartScoped<MeasureEntry>>();

  for (const partId of partIds) {
    const iterator = iterators[partId];
    staveSignatures.push({ partId, staveSignature: iterator.getStaveSignature() });

    // ...add barlines

    let iteration = iterator.peek();

    while (!iteration.done && iteration.start.isLessThan(boundary)) {
      measureEntries.push({ partId, entry: iteration.entry });
      iteration = iterator.next();
    }
  }

  // Ignore completely empty fragments.
  if (measureEntries.length > 0) {
    // ...add fragment 
  }
}

@jaredjj3 jaredjj3 marked this pull request as ready for review December 27, 2023 21:44
@jaredjj3 jaredjj3 merged commit d103a1d into master Dec 27, 2023
1 check passed
@jaredjj3 jaredjj3 deleted the format branch December 27, 2023 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants