Inconsistent spacing of same-duration notes

• Jan 15, 2020 - 19:38
Reported version
3.3
Type
Functional
Frequency
Few
Severity
S3 - Major
Reproducibility
Always
Status
closed
Regression
No
Workaround
Yes
Project

If a measure contains longer note values (quarter- and half-notes for example), then shorter notes (8ths, 16ths) in the same measure are spaced more tightly than when the measure contains only shorter note values. This causes same-duration notes to be spaced differently/inconsistently in different measures within the same system.

See how the 16ths are spaced differently in the two measures.
pic

All same-duration notes should have the same horizontal spacing within a system (while obviously taking into account extra space for accidentals and other elements)

The workaround is to carefully manually adjust layout stretch.

This issue's effect is most apparent in situations where tight spacing is needed.

This is probably related to the issue where accidentals and other elements causes unnecessary space to be added to a measure...
https://musescore.org/en/node/289446

Attachment Size
16ths_spacing.png 7.26 KB
Inconsistent_note_spacing.mscz 8.74 KB

Comments

It's related in that both involve the spacing algorithm, but the issues are rather different really. I'm a bit surprised to see such a big difference in this example, normally that's something the spacing algorithm does know it should be shooting for, but it doesn't really do this directly, so while it does more or less happen in many cases, it's not guaranteed by any means.

In reply to by Marc Sabatella

This issue occurs all the time. Its only in tight spacing that its becomes really apparent (I actually noticed this while writing strings of 16ths.). In wider spacing, the effect is much more subtle. But sometimes its still there to such a degree that you can sense something feels off, but can't put your finger on exactly why.

In this case, the difference in spacing between a pair of 8ths is the width of a stem. Yes, I have OCD :)
pic

Attachment Size
wider_spacing.PNG 7 KB

Yep, that's why I said it "more or less" happens :-). We aren't literally ensuring consistent spacing, but we're doing calculations that in many real world situations will result in it coming close enough that most people wouldn't notice.

No doubt, it would be good for someone to go into and rewrite these algorithms based on these examples. It would be a fairly big job, but not impossible.

It's not out of the question. To a large extent, programming is programming, you can get the gist of an algorithm without knowing the exact syntax to code it yourself. But, it's not really one standalone algorithm that does everything in one place in the code and moves on, it's a bunch of moving parts that come together. And you'd have to understand quite a bit about our data structures to understand the algorithm itself. Which is to say, even an experienced C++ programmer isn't likely to just be able to step in and figure it out easily.

Anyhow, that said, I can give a brief overview, either for you or for anyone (including me)( who might eventually work on this code somewhere down the line):

The layout happens in two main passes, both of which occur within Score::collectSystem():

https://github.com/musescore/MuseScore/blob/1279610c0be44c6abb0356b5eb7…

The first pass involves figuring out an appropriate basic "min" width for each measure so we can decide how many measures fit on a system, and the second pass then stretches those measures out to fill the system. The word "stretch" is used throughout the process, so don't be confused by references to stretching in the first pass.

The first pass begins with going looping through the measures, calling Measure::computeMinWidth() to compute their minimum widths:

https://github.com/musescore/MuseScore/blob/1279610c0be44c6abb0356b5eb7…

Within that function, we loop through the "segments" (time slices) of the measure computing the bare minimum width for each, via the calls to minHorizontalDistance(), which figures the amount of space needed by this segment, and we set the initial segment widths accordingly. This is actually mostly just about avoiding collisions, not so much about looking at durations of notes.

Still within that first pass in collectSystem(), right after calling computeMinWidth() we call Measure::stretchMeasure(), which I think is where we figure out relative spacing in terms of the actual durations of notes.

https://github.com/musescore/MuseScore/blob/1279610c0be44c6abb0356b5eb7…

This is where we space the segments within the measure. That's the part of the code that actually deals the most how much space to give sixteenth notes versus a half note - you'll see calculations involving logarithms, etc. Greek to me, but this is doubtless something that we'd need to look at.

Once we decide we have enough measures to fill the system, we need to stretch those measures out to the margin. That happens mostly right there in collectSystem(), after the comment "stretch incomplete row". It's a matter of figuring out how much weight to give each measure (calling layoutWeight() which cares only about total duration of the measure, so normally all measures get the same weight), then deciding how much space is leftover ("rest"), then pro-rating the that space among the measures according to their weight.

I think the issue here is probably mostly about the first pass, because it yields inconsistent spacing even if you effectively disable the second pass (a debug build allows you to do this by turning on Debug / No Horizontal Stretch, but you can simulate this simply adding a horizontal frame and sizing it take as much room as possible on the system). The issue with additional symbols throwing things off is more a second pass issue, we need to adjust the amount of "rest" given to each measure so account for the additional space they are already requiring for accidentals etc. I think the solution is to calculate and remember that value during computeMinWidth(), then apply it during the second pass calculation when we're giving each measure its weight.

I only barely have my brain wrapped around this well enough to document. Realistically, I don't expect anyone else to care enough about this enough to invest the effort it would take to deal with it (and risk breaking things in the process!), but by the time I might get around to it (need to do things to make a living in the mean time!), I'll probably forget, so these notes are as much for my benefit as yours or anyone else's.

In reply to by Marc Sabatella

Thanks for the detailed overview Marc! Yes, I expected it to be much more complicated than what one might think (its often like that in programming, right?). I'm going to start looking at the code and see where it leads me. If I'm very lucky, I'll be able to understand it enough to be able to point more precisely to where the spacing issues originate and offer possible solutions, and thereby making it easier for someone else to actually fix it.

I understand why many might not be bothered enough to invest a lot of effort into fixing this. After all, these are not score-breaking issues. The music turns out readable. The results are good enough for practical purposes. But on the other hand, if one wants to create really polished scores, these issues increase your work tenfold. These kind of problems are what will prevent MuseScore from ever really being taken seriously outside the hobbyist/student-circle. Hopefully in time the kinks can be ironed out.

On further reflection, something really interesting occurred to me, and checking it out within MuseScore even with no code changes at all, I have a suggestion for how you can get results you might find an improvement (I haven't done extensive testing).

The idea is this: when we calculate the initial minimum width, it's supposed literally just enough to avoid collisions (and honor the minimum note distance style setting). So, how do we manage to get such inconsistent spacing here even before the second pass? Answer: it's because during that first pass when computing the first pass, we multiply the minimum width of the measure by the measure spacing style setting (and until today I never really thought about what that setting did exactly). So, measures that were already wide get wider still, by a larger amount than narrow measures do. I think that's at least part of why the measure with more notes has wider spacing - it got stretched more than the narrower measure during the first pass.

So, armed with this insight, I thought, what would happen if I set "measure spacing" to 1.0 - meaning, no stretch at all is applied during the first pass. The would mean measures are narrower by default, but you can always add your own line breaks (and you probably do). You can also increase minimum note distance to compensate, to give you measures that are "roughly" the same width by default as before. Things will still be stretched in the second pass in ways that aren't really ideal, but at least it's not compounding the problems of the first pass.

It does give rather better spacing, I think, in limited testing. Give it a shot - measure spacing 1.0, maybe increase minimum note distance to 0.5 sp.

Playing with it a little more, it's really fascinating. Barnie, if you can at all manage to build yourself a copy of MuseScore - so the Debug menu becomes available - I think you'll really get a lot out of it. Even if you never touch a line of code.

The pictures below illustrate aspects of the algorithm I describe far better than words.

First, here is what I get for a an example inspired by the ones you have been using, if I set measure spacing = 1, note distance = 0, and then set Debug / No Horizontal Stretch. This basically shows that the result of computeMinWidth() is as I said: notes are spaced so as to not collide, and no additional space is added (well, except between accidentals notes, that is hardcoded I think) - not even to give half notes extra room compared to sixteenths:

note-spacing-0.png

Pretty crazy, right? Here is what you get if you set minimum note distance to 0.5sp, btw - now there is breathing room, but still, half notes get no more space than sixteenths, and all notes have exactly equal spacing:

note-spacing-1.png

OK, back to note distance = 0. But this time, I have applied stretch to the first measure - three presses of "}", for a stretch value of 1.3 (equivalent to setting measure spacing to 1.3, which is slightly more than the default of 1.2):

note-spacing-2.png

This is mimicing what normally happens in the first pass where we stretch the measure by the measure spacing value. You can see that MuseScore is giving the half note more breathing space but doesn't bother giving any to the sixteenths, because this is where it is doing the duration-based spacing. But as soon as I increase stretch another notch, then the sixteenths start getting space. By the time I've increase stretch to 2.0, it's looking like a normal measure - it's more or less how much the second pass would likely have stretched the measure, depending on what else is going on in that system:

note-spacing-3.png

To me this shows the "Greek" stretch algorithm is doing something pretty cool with those logarithm functions even if I don't understand it :-)

Finally, I reset the stretch for that measure but turn off the "No Horizontal Stretch" option. Now finally the second pass is going to do its thing. So now all the measures are stretched by whatever amount is needed to fill the page width. This is finally a "real world" example of what you can actually achieve today by using the "measure spacing = 1, add your own line breaks" approach:

note-spacing-4.png

In this particular case the spacing seems almost perfect between the first two measures. Perhaps we just got lucky there, but there is still too much space in the third. We really need to give that third measure a lot less of the remaining space. Probably, by an amount exactly equal to the space actually taken up by the accidentals.

Bottom line: the first pass is doing things right in terms of getting an absolute minimum size for the measure, and the algorithm to stretch any given measure does a good job of stretching it, and since no stretching happens in the first pass when using these settings, it tells me that any unevenness in the spacing really is do to figuring out how much to stretch each measure.

So, two changes would need to happen:

1) during the first pass, we can't uniformly stretch each measure by the same measure spacing value - this is what is resulting in uneven spacing right out of the box. Here is the result with that example if I disable the second pass again but return to default measure spacing:

note-spacing-5.png

Not good, as has been observed in this issue and related ones.

2) During the second pass, we similarly can't evenly divide the remaining space on the system between the measures. We the measures that have accidentals and so forth need to get corresponding less, but also we probably need to deal with pro-rating the stretch even between measures without extra symbols, like the first two in my example.

Thinking ahead towards possible implementation, changing anything about how the first pass works means some scores will render more noticeably differently than before - different minimum widths means possibly different number of measures per system. To avoid problems, I'd propose a new checkbox called something like "Engraver spacing", would default to off (at least for existing scores) and thus give you the old algorithm, but checking the box will give you the new algorithm. And the only differences between algorithm would be, not using a constant "measure spacing" for the stretch in the first pass, and accounting for the accidentals and other symbols in the second.

It's doable, for sure.

Wow, You're way ahead of me with this! Fascinating indeed! I was especially intrigued by the part where adding stretch increased the halfnote's spacing, but ignored the sixteenths until the stretch got big enough. So I ran the latest nightly with the --layout-debug option (which apparently disables the stretching done in the second pass) and played around. Here's what I've got...

Pass1_stretch_mimicking.PNG

If manually adding stretch mimics the stretching done in pass one, then this seems to demonstrate that the bug is somewhere within Measure::stretchMeasure() ... quite possibly hidden within those "Greek" algorithms that calculates the duration-based spacing.

So unless I'm misunderstading something, the fix for this would be one of two things...

1) Compensate for the apparent bug in Measure::stretchMeasure() by somehow applying different amounts of stretch per measure based on their note-duration content (this will probably be just as complicated as those "Greek" algorithms).

2) Fix Measure::stretchMeasure() itself. This would also ensure that, when manually adjusting stretch with "{" and "}", the results would be more consistent and predictable.

It also seems that your workaround of spacing=1.0 together with manually added linebreaks does indeed give 99.9999% perfect spacing (when no accidentals/etc are involved). Thanks!!

As a side note... switching between the old and new algorithm with an "Engraver spacing" checkbox so as to not mess up older scores... why not place all such "backwards-compatibility" settings together on a single page in style settings? And when a score is loaded, enable/disable those settings based on which version of MuseScore that score was made with? That way, older scores can be rendered as intended, while layout-bugfixes can be released without repercussions.

Attachment Size
Pass1_stretch_mimicking.PNG 38.98 KB

I realize there is a lot of info here, but I think you are misunderstanding the role of stretchMeasure(). It doesn't decide how much to stretch the measure; it is told (passed in as a parameter) the target width. It's just is merely to space the notes within the measure given that target width. So I think the experiment demonstrates exactly the opposite conclusion: stretchMeasure(0 does a marvelous job of spacing notes within a measure given a particular width. After all, if you look at any single measure in any of these examples in isolation (not comparing to others on the same line), each looks about as one would expect given the width (although of course, some of those widths are unreasonable)

So as far as i can tell, stretchMeasure() itself is fine, it's the width parameter we are passing in that is the issue. We call stretchMeasure(0 during both passes - during the first to apply the measure spacing setting, and during the second to divide up the remaining space. And in both cases, the target width being passed in causing problems.

To a rough approximation, the issue here in this thread is caused by the bad sizes during pass 1. Uniformly stretching everything by a factor of 1.2 means that measures with more notes (and hence are wider to begin with) get stretched more, and this often results in their sixteenths being more widely spaced. More particularly, measures without long notes to "soak up" the stretch end up passing it all on to their sixteenths. A value of 1.0 gives consistent spacing - but too consistent. So instead, we should calculate specific initial stretch values for each measure based on their content. Basically, we should stretch each measure enough to give extra space to the half notes but not so much that it affects sixteenths (for example).

The problem with accidentals and other symbols is caused by bad sizes during pass 2. Currently, we give each measure the same amount (assuming same time signatures). But instead, we need to reduce each measure's share by some amount corresponding to how much extra space is taken up by the accidentals etc.

I'm not really sure how to calculate these values, but I'm pretty confident that this would be an effective approach.

In reply to by Marc Sabatella

Thanks Marc, I think I understand better now :) So am I right that next on the todo-list is to figure out a formula to calculate the exact correct value to give to stretchMeasure() during the first pass?

I think I can help with that. If I manually correct a number of measures' spacing, and then look at the exact stretch-values per measure in the measure properties, I might be able to come up with a formula that gives those stretch-values based on the contents of the measure.

Am I thinking in the right direction?

PS - is the number of measures to be included in a system decided before or after this stretch phase in the first pass?

Yes, I think coming up with a better initial width for the first pass is a good next step. Really, it could be either pass we tackle first, but I think the first is easier, and more fundamental - you can't get good results in the second pass if they are bad in the first.

We already have the function computeMinWidth() that I mentioned, to calculate a base minimum width for the measure based on collisions only. Could be a matter of adjusting it to take durations into account also, using something like the diagram on Gould p. 30 as a guide. A complication we have glossed over completely are multiple voices and multiple staves, of course. That gets in the way of simply changing the width of each segment to account for duration.

In reply to by Marc Sabatella

Multiple voices and staves actually have been in the back of my mind from very early on. But I haven't thought yet how to handle them.

Also, a whole bunch of further first-pass-complications just occurred to me...

Pass1_stretch_complications.png

It seems that, in order to correctly calculate the stretch value for the first pass, we'll need the following info for each measure...
1) the total minimum width
2) the minimum width needed for non-notes, ledger lines and the "extra" space taken by 2nd intervals.

Perhaps one can collect this info within computeMinWidth() ?

I think there is a possibility that, with this info, I can figure out a way of calculating the correct stretch value without computeMinWidth() needing to consider note durations. Not sure if this would be more complicated though...

Attachment Size
Pass1_stretch_complications.png 51.92 KB

@Marc Sabatella I'm think I'm making progress on a possible way to calculate the correct stretch value to give to stretchMeasure() during the first pass, but I need some clarity on some things. Can you help please? Or point me to the right documentation?

How is a measure's duration represented in the code? For example, is a 4/4 measure given a duration "value" of 400 in the code and a 3/4 measure a value of 300?

(More questions will probably follow as I go along...)

Glad to hear your working on this! I'm busy with other things and wouldn't be able to look at this at all for some time.

Durations in MuseScore are represented as Fractions. A 4/4 measure has duration of Fraction(4, 4), you get that via Measure::ticks(). The function is called that for historical reasons - we used to use MIDI ticks as the unit rather than fractions. So a quarter note was 480 ticks, and a 4/4/ measure came out to 1920 ticks. Now, you get the old-style tick count via ticks().ticks() (as in, really tell me the duration in ticks, damnit).

Currently the layout code - well, the part of algorithm I have been describing, anyhow - does not worry about measure duration directly. Instead they call Measure::layoutWeight(). This defaults to returning ticks().ticks(), but performs some additional calculations for multimeasure rests in an effort to get them to have a reasonable size that is only indirectly proportional to their duration. We've tweaked that code a bit over the past year, it could still stand further refinement I'm sure.

Thanks @Marc Sabatella , I think that gives me one piece of the puzzle I need.

I've been trying to find a way to calculate the correct stretch value for each measure (stretch value as in 1.2 or 2.0 or 5.0, etc...). But I've noticed that the same stretch applied to different measures resulted in their widths increasing by different factors.

To explain : the same stretch-value is applied to two measures. One measure ends up double its minimum width and the other triple its minimum width. Thus, a stretch value of 2.0 does not guarantee that a measure's width would increase by a factor of two (or any other constant factor). The factor of width-increase depends on the specific note-values in the measure.

This leads me to my questions for today:

1) You said the parameter passed to stretchMeasure() is the target width of the measure. Just to make sure I don't misunderstand... let's say a measure's minimum width is 200 units and stretchMeasure() is given a target width of 400 units. Does this then guarantee that the measure's width would be doubled?

2) Assuming that the answer to 1) is "yes", could Spacing (the style-setting) be used as a simple multiplier for a measure's minimum width?

(If the answer to 1) is "no" then I might still have a workaround to what I'm trying to achieve...)

The stretch value is applied before the "rest" of the system is divvied up, so that could be what you are seeing. Would need to see the context to understand.

Regarding you question, not sure I understand, doubled from what? Doubled from its originally calculated width? As in the width you saw when you turn off on "No horizontal stretch"? Well, yes, if the originally calculated width was 200, and we're stretch it to 400, that is factor of 2. But keep in mind, the first pass width is only the minimum width if the spacing style setting is 1.0. Otherwise it's already been stretched. And remember, that user stretch value is applied during that first pass. So I'm not really sure what you are comparing here.

Thanks Marc. However further experimentation rendered my questions moot.

Here's what led to my questions... I understood that the stretch value from the spacing style setting is intended to be the factor by which a measure's width is increased after its minimum width is calculated during the first pass.

This didn't appear to be the case when I was physically measuring measures' widths in order to better understand how stretchMeasure() behaves. ( "b ≠ 3.0 x a" in the picture below).

However, it turned out I was simply measuring the wrong parts of the measures and that stretching does indeed behave as expected (the "d = 3.0 x c" part).

In any case, my apologies for being so unclear. I could've explained better and asked clearer questions.

measures_measured.png

Attachment Size
measures_measured.png 19.9 KB

@Marc Sabatella I'm currently trying to identify the exact math that stretchMeasure() uses to decide how much space a note gets within the measure. I need this to be able to come up with a way to calculate a measure's pre-stretch width (during the first pass) based on the note values.

The calculation seems to be based on a note's number of ticks and the "minTick" value returned by computeTicks(). What exactly is this minTick value? And besides returning minTick, what does computeTicks() do otherwise?

From the code, it appears computeTicks() is going through each segment calculating the duration of the segment. Consider, it's different from the duration of a note, as there might be a whole note as the only note on the segment for beat 1, but the next segment might be a chord symbol on beat 3, so the duration of that beat 1 segment would be two beats. So computeTicks() is calling setTicks() on that beat 1 segment so it "knows" it is two beats long (that information is not otherwise available, and indeed is seldom used in the code). The return value is the duration of the shortest segment in the measure. So, if a measure contains a mix of half notes, eighths, and sixteenths, return value is Fraction(1, 16) it seems. So that's what minTick is in stretchMeasure(). At least that's how it looks to me.

I'm making progress and have been able to come up with an algorithm (based on the math in stretchMeasure() ) that gives more consistent spacing than the current algorithm. But its not 100% there yet...

I just need to make sure... is space AKA sp the unit of width used in the code? E.g, do we tell stretchMeasure() to resize a measure to a specified amount of spaces?

I look forward to seeing the results! The units used in the code is not not normally in sp, it's in absolute size that is that times whatever the spatium() value is. At the default scaling, I think it works out to 25 but don't quote me on that - just set breakpoints in places where this value is used and check the values for yourself.

Breakpoints? Um... I'm almost completely illiterate in C++ and haven't even yet figured out how to compile the code. So I can't use breakpoints at this stage. :)

My intention was to come up with a way to calculate a correct target-width for each measure (during the first pass) to pass to stretchMeasure() based on collisions as well as note-durations. Here's how I tried doing that in a nutshell...

First I identify the shortest note-duration on the system ('SYSTEM_MIN_TICKS' for brevity) and also get the measure-spacing-value from style settings ('SPACING' for short).

Then, for each system and for each measure on that system, I loop through the measure's segments (notes), calculating a target width for each and adding up these segment-widths to get a total target width for the measure. Thus:

(edit:the website deleted my indentation)
for all_systems do
for all_measures_on_the_system do {
measure_total_width=0;
for all_segments_in_the_measure do {
calculate_segment_width;
add_segment_width_to_measure_total_width;
}
}

I calculated a target-width for each segment as follows:

1) I get the segment's ticks ('TICKS' for short).

2) I calculate a duration-based width (DURATION_WIDTH) with this formula copied and adapted from stretchMeasure():

DURATION_WIDTH = SPACING * (1.0 + 0.865617 * log(TICKS / SYSTEM_MIN_TICKS));

3) Then I get the collision-avoidance-based minimum width between the start of the segment's notehead, and the start of the next segment's notehead (MIN_TRAILING_WIDTH).

4) Then I test whether DURATION_WIDTH is wide enough to accomodate the segment's notehead and all the symbols before the next notehead (dots, accidentals, arpeggios, etc) and decide the segments width (SEGMENT_WIDTH) accordingly. Thus:

(edit:the website deleted my indentation)
if (MIN_TRAILING_WIDTH > DURATION_WIDTH) then
SEGMENT_WIDTH=MIN_TRAILING_WIDTH
else
SEGMENT_WIDTH=DURATION_WIDTH

5) Then I sum all the SEGMENT_WIDTHs as well as other unaccounted-for widths within the measure (style settings such as barline-to-note-width, etc)

To test this, I created a minimum-width score like this...

MinWidth.PNG

... and then physically measured the minimum-width for each measure (in sp), and then calculated a stretch-value for each measure to achieve the previously calculated target-widths for each measure.

The result is that measures 2,3,4,7 and 8 gets spaced perfectly consistently, but the rest of the measures' spacing is slightly too narrow to a degree to be apparent just by looking. I haven't figured out yet why this is.

(And here is the part where I'm supposed to post a screenshot, but, to my embarrassment, I realised I've misplaced the score with the the calculated stretches applied as well as the textfile containing said stretch-values!)

However, I'm not so sure anymore whether this is the best approach. I've since had a new idea that might be much easier to implement. Details to follow in the my next reply...

Attachment Size
MinWidth.PNG 24.28 KB

@Marc Sabatella Continued from my previous reply... stretchMeasure() spaces same-duration notes consistently WITHIN a measure. So I thought :

What it we treated an entire system as if it is one big measure?
What if we stretched an entire system in one go instead of stretching measure by measure?
What if we had a "stretchSystem()" that stretched a system across the width of the page?
What if we simply copied the contents of stretchMeasure() into stretchSystem(), and modified the code to loop through an entire system's segments instead of just a single measure's segments?

My thinking is that, if stretchMeasure() spaces consistently within a measure, it would also space consistently if it could loop through the entire system's segments instead of just a specific measure.

I mimicked this hypothetical "stretchSystem()" by joining all the measures in a system and then manually adding barlines. The resultant spacing is just about PERFECT! (see the attached score for more examples.)

joining_measures_fixes_inconsistent_spacing.png

(This example was made with a normal non-development copy of MuseScore. So the second-pass stretching is also enabled)

So if we could somehow have a copy of the code in stretchMeasure() loop through an entire system in one go, I think that would fix the inconsistent spacing.

We might also need to modify the code that decides how many measures would fit on a system to calculate measures' width in a similar fashion as described in my previous post instead of calculating widths based on avoiding collisions only. Otherwise, we might get a situation where too many or too few measures are placed on a system, resulting in overly wide or narrow spacing on one system compared to the others.

(I hope I'm making sense. English isn't my first language and I often have trouble putting my thoughts to words.)

At the moment I think the idea described above (applying stretchMeasure() to a whole system instead of one measure at a time) provides the (seemingly) simplest fix for the inconsistent spacing. Any comment on this? Could this be the way forward?

Meanwhile I'm still trying to decipher how stretchMeasure() works. Any idea what the "springs" thing within stretchMeasure() is?

Well, we still need to calculate reasonable minimum widths in the first pass measure by measure because that's the only way to determine how many measures will fit. And that's really where the heart of the problem is - the minimum width calculation during the first pass. But it's possible indeed that after calculating what we think are reasonable minimum widths (even if they are too conservative for longer measures), we could indeed to the stretch across the entire system.

Realistically, I don't see any way you could possibly make any significant/usable progress on this without compiling the code and testing and debugging for yourself, though. So I'd focus on getting that happening.

Oh, and "springs" are the data structure used to try to track how much space to allocate to each segment, but it's pretty convoluted. At one time I worked the code enough to gain a rudimentary understanding of what it was doing, but I've pretty forgotten it, and would need to spend some serious time stepping through the code and examining the values with a debugger to regain that sort of understanding. Which is what I am suggesting you might want to start working on :-)

So this is about as far as I'll get by only observing how MuseScore behaves, reading your notes and reading the code (or at least trying to)? Got it. I'll get into compiling and messing with the code and see how that goes. Thanks for all your help so far! :)

You're welcome, and thank you for the work on this! But yeah, this stuff is complex enough that while you can learn much just by studying the code, doing thought exercises, and faking things using the debug menu options, manual stretch etc, you won't really know what's going on without trying some code. I encourage you to join the Telegram chat for real-time discussion as you do so - MuseScore Developers Chat
https://t.me/musescoreeditorchat

So I've managed to compile MuseScore (with Visual Studio) and setting breakpoints and stepping through the code. Its quite overwhelming and I'm in waaaaaaay over my head at this point, but maybe I'll get the hang of it over time.

Meanwhile, I thought of a possible solution based on a some assumptions (which I'll get to in a minute):

Looking at collectSystem() and your explanations, its seems the spacing currently happens in the following main steps...
1) Decide how many measures would fit on a system
2) Stretch those measures one by one to their minimum width times the spacing style setting
3) Stretch those measures to right-justify the system

What if this is changed to the following...
1) Decide how many measures would fit on a system (this code remains unchanged for now)
2) Treat the system like one large measure (as if its measures are all joined into one single measure... see the examples in my previous comments) and stretch it to page-width with the same code/algorithm contained in stretchMeasure(). This is based on the fact that stretchMeasure() currently spaces notes perfectly consistently within a measure, so it could probably do the same for an entire system.

Based on my limited understanding of the code, this approach seems to have the following advantages...
1) We don't need to rewrite any of the code that actually calculates the note-duration-based spacing.
2) We don't need to calculate a correct target width to pass to stretchMeasure() for each measure. If we had to, it would've become very complex as we would've needed to take into account things like tuplets, tremolos, multiple voices, etc. A further complication is that, in order to calculate a correct target width for a measure, we would seemingly need to know the shortest note-duration on the entire system.
3) No more second-pass stretching, thus eliminating the need to fix the issue of how to correctly divide up the remaining space between measures.

There would be the following disadvantages...
1) In continuous view, we would probably still need to use the current algorithm (otherwise I suspect MuseScore would grind to a halt). However, this is a minor disadvantage to me as I mainly care about how the score looks on the printed page.
2) Since the code deciding the number of measures per system remains the same, we might not always automatically get the right number of measures on a system. However, this is easily manually corrected by adjusting the spacing-style-setting and using system breaks. And the code can always be fixed at a later stage if needed.

All of this is based on the assumption that stretchMeasure() can be applied to a whole system instead of a single measure. Or alternatively stretchMeasure() can be copied and adapted into a new function like a hypothetical stretchSystem().

Looking at stretchMeasure(), it seems to me that it loops through a list of segments. So can it perhaps loop through an entire system's segments instead of a single measure's segments? My proposed solution is based on the answer being "yes".

So what do you think? Could this the solution? Could this be implemented without needing to do any really mind-breaking coding or major surgery on the code?

Before I get too excited that the above might be the answer to the spacing issue, I'm going to experiment with some real scores and see how good the spacing turns out.

I think this idea has real potential and is very possibly the way forward. However, there is one error in your assumption that complicates things: the initial stretch of the measure based on measure spacing setting (also considering user-applied stretch) happens within computeMinWidth() before deciding how measures would fit. For obvious reasons - otherwise we'd likely end up with measures that don't actually fit once stretched. The second stretch to right-justify happens after settling on the number of measures.

It might work to use that initial stretch calculation to decide on how many measures, but then run another pass across the whole system to even things out. The worry would be that the initial calculation might occasionally be a little too optimistic, and that second pass would actually find the measures get too long after stretching. Or vice versa, and after the second pass we discover another measure would have actually fit.

So maybe better would be, instead of calculation each measure's stretched width as we test whether it would fit, re-calculate the full system's stretched width each time we tentatively add a measure. It's a bit more expensive, but in theory we only do this for systems that have changed, so it shouldn't be noticeable. plus I don't think the stretching calculation is likely to be that big a part of the overall layout time.

Thanks for the clarification @Marc Sabatella I'll give it some (more) thought and rewrite the idea.

When deciding how many measures would fit on a system, I guess we could recalculate the full system's stretched width each time we tentatively add a measure. But consider this...

(measure1_minwidth * stretch) + (measure2_minwidth * stretch) + (measure3_minwidth * stretch)

is exactly the same as

stretch * (measure1_minwidth + measure2_minwidth + measure3_minwidth)

Whether we stretch the whole system or each measure in succession, the resultant width of the system would still be the same. So I cannot see any benefit in stretching the whole system each time we add a measure. Unless I'm still missing something.

In the meantime I joined systems' measures in real scores to see how the spacing would look. I noticed that longer notes seemed to have too little space. I tested this by placing different note-durations in a single long measure.

Notice how you can barely tell the difference in spacing between the wholenote, the halfnote, the quarter and the 8th. Not good.

rhythmic_spacing_MS.png

Luckily this wasn't too difficult to fix. I opted to increase note spacing by a fixed ratio as note durations increase. The ratio I chose was 1.41 (the same as Dorico SE's default setting). I changed that weird formula with the logarithms in stretchMeasure() to this...

formula.PNG

...and got the following much better spacing (which is almost identical to Dorico SE's spacing):

rhythmic_spacing_MS_fixed.png

This ratio of space increase could be made to be a style-setting, so one can have some flexibility with rhythmic spacing. (As a side-note... I've seen a few people ask how one could achieve proportional note spacing in MuseScore. Such a style setting set to a value of 2.0 would achieve this.)

I'll collect all of these ideas into a single coherent suggestion of how to fix the spacing issue soon.

Attachment Size
rhythmic_spacing_MS.png 3.4 KB
rhythmic_spacing_MS_fixed.png 3.43 KB
formula.PNG 10.48 KB

If we're OK with continuing to calculating the widths just by multiplying the original min width by the spacing factor, then indeed, it's OK to do that as you go. I thought you were working on an alternative calculation for that. But indeed, it does seem likely to work to do initial calculations this way but then just alter the calculation of the notes within the measures. I think so anyhow, at this point I suspect you've got your brain wrapped around this better than I.

I'm no expert on note spacing, but I personally don't see the Dorico spacing shown above as inherently better than what we do. But the chart on Gould p. 39 to me looks closer to what we do than what they do. Maybe that's just because it's stretched so much, though - what does that look like unstretched? Could be worth getting some confirmation from MET. BTW, maybe a simple lookup table based on that chart beats mathematical formulas?

I was indeed working on an alternative way to decide the number of measures per system based on both collision-avoidance and the note-durations in the measures. But truth be told, it got too complex for my brain to handle! :) More about that in a minute. But this doesn't mean I won't be able to figure it out somewhere in the future if the need arises!

About the MuseScore vs Dorico spacing... I should have been clearer as to what the problem is. And yes, the extreme stretch does skew one's perception. Here are the same and some better examples:

rhythmic_spacing_MS_Dorico.png

The problem for me is that the spacing-difference between a note-value and its immediately longer/shorter neighbour is so small at times that its not always readily apparent. By default Dorico makes this spacing-difference between note-values apparent enough without it getting too extreme (at normal stretch at least).

This problem tends to occur in MuseScore only when there is a large duration-difference between the longest and shortest notes in a measure (or a system, if we are going to stretch a whole system in one go.) Currently this problem doesn't manifest in the vast majority of cases because we only stretch one measure at a time, and the duration-difference between the longest and shortest notes within a single measure are almost never large enough to make the problem apparent. But when we start stretching whole systems, we get the problem. I noticed this when I experimented with real scores. When I joined a whole system's measures (to force stretchMeasure to process the system in one go), wholenotes often got dramatically compressed to a point that they didn't look right.

A practical example of this with a slightly modified version of your piece "Coming Back Home" :

coming_back_home.png

Doesn't that third example look gorgeous? :)

While I mostly agree with what Gould writes, the printed examples (also the one on p.39) kinda have the same problem for me. (As a side-note: I find it very bizarre that (on the next page) equal spacing of different-duration notes are labeled as "acceptable".) I realize that this is not only a "right" vs "wrong" thing, but also a matter of taste to a large extent. This is a good reason why one would want to be able to change the ratio at which different note-values are spaced relative to each other in style settings and also be able to switch to the "old" spacing algorithm.

Neither am I an engraving-expert, but my OCD usually tells me when something is amiss, like in this case. :) And even then I often miss many of the finer details.

A spacing lookup table would certainly offer more flexibility, but I think it would ultimately be more complicated to implement. You can't specify a spacing value for every possible note value. How would one decide how to space triple-dotted notes? Or tuplets? Or nested tuplets? Or triple-dotted notes within nested tuplets? I have no idea how at this stage. This is also kind-of the reason why I couldn't figure out another way of deciding the number of measures per system. For that one would need to "predict" the correct width of the measure.

My intention is to keep the solution as simple and small as possible so that it would be as quick and easy as possible to implement.

Attachment Size
rhythmic_spacing_MS_Dorico.png 25 KB
coming_back_home.png 55.79 KB

Thanks for the explanation and examples! BTW, though, my lead sheet is not the greatest example to use, because actually, there is a pretty strong tendency in that world toward equalizing measure widths, so that chord symbol spacing of one per measure looks consistent. Really that should just be an option we support (as well as other options to set a fixed width for measures). Should be trivial to implement really, as you can presumably see, once a design is proposed and agreed upon.

Anyhow, I think the issue with Gould's example is that it is basically showing an unstretched case only, and doesn't really address what might happen as things stretch. I guess we are more or less keeping to only having an additional 0.5sp for each duration type, rather than keeping the ratios from her example?

Equalized/fixed measure widths? Fascinating! I never knew this was a thing in some music-circles!

Something important I forgot to mention... I understand now what that weird logarithm-formula in stretchMeasure() does. It simply calculates a duration-based stretch value for a note. The shortest note in the measure (or system if we stretch the whole system) will always get a stretch value of 1.0 and all the longer notes would get larger stretch values.

Knowing this, one could add the option in style-settings to choose between one of several different spacing-calculation-formulas, each with one or two of its own configurable settings/parameters. And coding this would simply mean a series of if-then-statements within stretchMeasure() to use the correct formula.

For example, one could have the following spacing-formulas to choose from:

1) exponential increase in note spacing: each note's spacing is a user-defined multiplication of the immediately shorter note's space. This is what Dorico SE does. I've read Finale also have this option.

2) linear increase in note spacing: note spacing increases by a fixed amount of space as duration increases. This is essentially what Gould's example shows.

3) a note-spacing lookup table like you suggested. My brain is currently in hyperdrive and I might have an idea of how to calculate the spacing of undefined note-durations like tuplets/nested tuplets!

4) Equalized/fixed measure widths

5) MuseScore's current algorithm for backwards compatibility in older scores.

This set of options should cater to almost everyone's preferences. Most users would probably be happy with whatever the default settings are. But such a set of options would make me and others like me extremely happy!

So before I start writing a structured and complete proposal on how to fix note spacing, what are your thoughts on such a comprehensive set of settings? Do you think it would be worth the effort? I would be all for it.

I like it. Although there are actually a couple of different algorithms, even if they possibly interrelate - one for determining measure width, another for spacing notes within measure. Conceivably one might choose equal measure widths but still want to tweak how the spacing within those measures works. Maybe, though that's just a matter of a checkbox for that on top of the other options for spacing?

Another option that needs to be there - well, maybe it's a special case of 1) - is true proportional, where a quarter note gets exactly twice as much space as an eighth, etc. That's a pretty popular request actually.

True proportional spacing would be achieved with 1) by setting the user-defined multiplication to 2.

I can definitely see how one could turn fixed measure-widths on/off with a checkbox. In the code it would simply mean stretching measures one by one to a fixed size instead of stretching a whole system. StretchMeasure() would still apply the selected spacing formula within the measure.

I'm going to start writing a fix-proposal. Its going to take a while to get everything as clear and unambiguous as possible and I'll probably post several drafts here for your insight before we agree on a final fix/design.

Quick question... if you have fixed-width measures and you get a time-signature change mid-system, does the measures with the new time signature get a different width based on its new duration or do the measures still stay the same width?

Good question, no matter what we decide I’m sure it’s what people want sometimes, not other times. But it would be relatively rare to have time signature changes in the kinds of music where equal measure widths are used. If stretch can be used to override - or better yet, a new measure property to specify a width directly - it won’t matter.

Within the fix proposal document I'm writing, I also want to include a list of issues/bug reports/ feature requests (including links to each forum thread) that would be solved by implementing these spacing changes. So far I have:
- the topic of this thread (I see this has been reported as far back as December 2018)
- request for a fixed ratio of increasing space between successively longer notes
- request for true proportional spacing
- request for equal measure widths

Can you add any other bugs/requests that would be solved?

The target-width to which we stretch measures when we decide how many measures would fit on a system... I might have an idea how to calculate a correct target-width based on note-durations.

The basic idea is to calculate a duration-based width for each note-segment. If the width of a note-segment is less than the combined width of its notehead and all the following symbols up to (but not including) the next note-segment, then the current note-segment's width is increased to the said symbols' combined width.

All of the note-segments' widths calculated this way are added up to give us the target width to pass to stretchMeasure()

I'll try to make it clearer:

So within collectSystem() we have these two lines during pass 1:

m->computeMinWidth();
m->stretchMeasure(curWidth);

Before calling stretchMeasure, could curWidth be calculated in this way:

(switching into pseudocode mode now)...

pseudocode.PNG

This calculation depends on the following info being available at the specific point in the code...
- each segment's tick value
- each segment's absolute minimum width (without any stretch applied either via Spacing style-setting or user-applied stretch, but including minimum spacing style settings like "minimum note distance")

This is also based on my understanding that non-note symbols (like accidentals, dots, arpeggios, etc) all live in their own segments.

I'm also not sure yet how user-applied stretch or tremolos would fit into this.

So do you this this might work to calculate a measure's target-width?

In the meantime I'm still busy writing a document that puts all these puzzle pieces together in a design-proposal. Hoping to complete it soon.

Attachment Size
pseudocode.PNG 13.67 KB

In reply to by BarnieSnyman

Well, there are some incorrect assumptions - accidentals, dots, arpeggios etc. generally do not live in their own segments but are factored into the width of the note/chord and potentially its segment.

But from the beginning I had been assuming we'd want to calculate to width values during the first pass: the actual minimum as we currently do, and "nominal" minimum based on durations only, Stretch would be calculated on the latter basis only, during both passes. I'm not saying that's a better approach than somehow looking at the system as a whole, but it's what I initially assumed we would be doing. Seems logical that the same sort of calculation would end up being relevant in the approach you've been talking about as well. I'm still not what the tradeoffs really are here., but probably either approach could be made to work.

Thanks everyone! I think my mistaken assumptions and the fact that I'm making the conversation start to circle kind of shows that I've reached the limit of what I can contribute with my skillset for now. I think in order to really make useful contributions with the nitty-gritty coding-details, I'll need to take at least a few months to get familiar with both C++ and MuseScore's inner workings.

That being said, here is the promised design proposal! It basically sums up most of my and @Marc Sabatella 's conversations on this issue so far plus some more. The document is intended to get anyone who'd like to work on this quickly up to speed with what has been discussed so far and what the way forward may be.

I describe...
- what the problem is
- why its bad
- why it happens
- the proposed new design
- the math for the new design
- how to code it (in broad terms at least)

I include the original word-document for when someone needs to add or correct something. I expect there will be lots of the latter! Anyway... even if I made lots of errors (hopefully no dumb errors!), I hope the logic of my suggestions is sound and useful for moving this forward.

In reply to by Tantacrul

Yes indeed
This thread has been the most interesting to me in a long time. It looks like Musescore could begin to have a much nicer default spacing. Would be nice to see how it works in a leadsheet with a verse or two. In lilypond that is a case where spacing issues show up. I think that might as well be the case here. But for just notated music I think this is a huge step.

So thank you to everyone involved ;-))
Lars Henrik Ørn

Merely mentioning [+1] as a layman my anticipation of any progress in future versions related to a more proportionally correct default layout resulting from the efforts of those involved.

P.S., provided is a screenshot of two-voices interacting in a particular way, resulting in inconsistent spacing. First voice uses an augmentation, the second voice 'hides' into the first, uses no augmentation and continues, yet the second voice gets uneven spacing in the meantime. E.g.:
uneven_spacing_voices.png

Indeed, it's not about duration as can be seen by changing the top note to a quarter or an eighth or a sixteenth (but check out, the hooks have the same effect).

In reply to by Marc Sabatella

P.S., an altogether other issue: the joined voices have their downstems a little offset to the left, as seen while zoomed in. Looks nasty compared to the other ones when up close, but when zoomed out its difficult to notice (just revisited after being prompted through the above response). Although that's off the subject, in a sense it is slightly on it because that offsetting will have contributed to the perception of the spacing in a very small degree.

In reply to by Marc Sabatella

@Marc So the question arises whether hooks/flags should also be able to tuck? Or even noteheads themselves? And if so, would it be worth having such a feature? Intriguing... I think I'm gonna see what info I can find on this, and if anything pops up I'll add it to the accidentals-don't-tuck thread.

In reply to by BarnieSnyman

What you describe is already the method by which the formatting was achieved. Testing with a new score, the problem doesn't occur, and yet the score depicted in the screenshot is having this issue. Apparently it's some non-default setting that's doing this... I wonder what it could be. Hate to derail the thread for a moment, but attached is a .mscz file with the problem. If anyone on side-time sees what the setting is that's causing the issue, please mention it!

test.png

Attachment Size
test.mscz 6.79 KB

The score is set to not show invisible items, that's just a checkbox in the View menu.

Turning invisible items back on, I can see the two noteheads don't properly overlap completely. But even though there are lots of style changes, also custom beaming nothing jumps out at me as being the cause. Best to ask on forum, maybe someone else feels like playing detective on this :-)

I see there's talks of MuseScore 4 starting to go around. It seems that MuseScore is moving to become a DAW-like composer software in addition to being a scorewriter. Given that, is there currently any prospect that these spacing issues might be addressed, now that there is an opportunity to introduce backwards compatibility-breaking layout changes?

Hopefully so. Meanwhile, hopefully you’ve seen my comments elsewhere that as if 3.5, disabling autoplace for accidentals will at least allow reclaiming that space, so there is now an easy and safe workaround for that particular problem.

Ooh, that's nice! Thank you! I just saw your comment under the accidentals-don't-tuck thread.

Just a note about the spacing issue so I won't forget if I (or someone else) looks at it again: stretchMeasure() knows how to calculate how much of the measure's target width is given to a segment. Thus, we start with a measure's target width and end with a segment's width. And space needed for accidentals, dots, etc are all taken into account.

So it might be possible to do these same calculations/algorithm in "reverse", by starting with a defined segment-width (based on its duration or a spacing lookup-table) and ending with the measure's width (sum of the segment-widths).

I know this is oversimplified and very vague, but the point is I think stretchMeasure() contains all the magic ingredients needed to fix the spacing. And thinking about doing the algorithm in "reverse" (by starting with defined segment-widths instead of a defined measure-width) might provide another angle to approach the spacing problems from.

I hope I'm making sense. I find it difficult to put my thoughts to words.

Fix version
4.0.0