Abjad / abjad

Abjad is a Python API for building LilyPond files. Use Abjad to make PDFs of music notation.
https://abjad.github.io
GNU General Public License v3.0
234 stars 41 forks source link

Exception when attaching bar lines to multiple staves in a score? #1252

Open gilbertohasnofb opened 3 years ago

gilbertohasnofb commented 3 years ago

When dealing with multiple staves that belong to an abjad.Score, attempting to attach a bar line to more than one stave at a same given bar will result in an exception:

staff_1 = abjad.Staff(r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4")
staff_2 = abjad.mutate(staff_1).copy()

score = abjad.Score([staff_1, staff_2])

abjad.attach(abjad.BarLine("||"), staff_1[1])
abjad.attach(abjad.BarLine("||"), staff_2[1])

The code above produces:

Traceback (most recent call last):
  File "/home/gilberto/git/auxjad/sandbox.py", line 109, in <module>
    abjad.attach(abjad.BarLine("||"), staff_2[1])
  File "/home/gilberto/.local/lib/python3.8/site-packages/abjad/top/attach.py", line 288, in attach
    wrapper_ = abjad.Wrapper(
  File "/home/gilberto/.local/lib/python3.8/site-packages/abjad/system/Wrapper.py", line 173, in __init__
    self._bind_component(component)
  File "/home/gilberto/.local/lib/python3.8/site-packages/abjad/system/Wrapper.py", line 360, in _bind_component
    self._warn_duplicate_indicator(component)
  File "/home/gilberto/.local/lib/python3.8/site-packages/abjad/system/Wrapper.py", line 520, in _warn_duplicate_indicator
    raise abjad.PersistentIndicatorError(message)
abjad.exceptions.PersistentIndicatorError: 

Can not attach ...

abjad.Wrapper(
    context='Score',
    indicator=abjad.BarLine('||', format_slot='after', ),
    tag=abjad.Tag(),
    )

... to MultimeasureRest('R1') in None because ...

abjad.Wrapper(
    context='Score',
    indicator=abjad.BarLine('||', format_slot='after', ),
    tag=abjad.Tag(),
    )

... is already attached to MultimeasureRest('R1') in None.

Attaching the barlines to the same positions but before adding the staves to a score will not result in an exception:

staff_1 = abjad.Staff(r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4")
staff_2 = abjad.mutate(staff_1).copy()

abjad.attach(abjad.BarLine("||"), staff_1[1])
abjad.attach(abjad.BarLine("||"), staff_2[1])

score = abjad.Score([staff_1, staff_2])  # appending staves after attaching barlines is fine

Is the first case happening by design or is that a bug? It seems odd that once a staff is part of a score it cannot receive barlines if another staff already has one at that given bar.

trevorbaca commented 3 years ago

Hi @gilbertohasnofb

By design. Though keep reading for thoughts about future tightening of the model.

Note that LilyPond scopes the context of LilyPond bar lines to the LilyPond score. We see that this is the case if we modify your first example to attach only a single bar line, and then look at the resulting LilyPond output:

string = r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4"
staff_1 = abjad.Staff(string)
staff_2 = abjad.Staff(string)
score = abjad.Score([staff_1, staff_2])
abjad.attach(abjad.BarLine("||"), staff_1[1])
#abjad.attach(abjad.BarLine("||"), staff_2[1])
abjad.f(score)
\new Score
<<
    \new Staff
    {
        \time 4/4
        R1
        R1
        \bar "||"
        \time 6/4
        R1 * 3/2
        R1 * 3/2
    }
    \new Staff
    {
        \time 4/4
        R1
        R1
        \time 6/4
        R1 * 3/2
        R1 * 3/2
    }
>>
barline-score-context

The LilyPond \bar command appears in only one staff. But of course LilyPond engraves the double bar in both staves.

Abjad does, in fact, model this:

bar_line = abjad.BarLine("||")
bar_line.context
'Score'

Then, at attach-time Abjad checks for "contention" between two indicators that happen at the same moment but are "scoped" high enough (like at the level of the score) that they clash.

That check that Abjad is making is unmotivated in the example here (because the bar lines both "mean" the same thing: they're double bar lines). But imagine that you tried to attach a double bar in one staff and then a final bar in the other staff (at the same time). Then it's actually quite helpful that Abjad is alerting you to the fact that you're effectively trying to overwrite your own intentions.

(Even more so if you imagine setting conflicting metronome mark indicators at the same moment in different staves. Metronome marks are also scoped at the score.)

So the check will stay.

Your second example is different, however: my intention is to forbid the second example. I just haven't gotten around to it yet. I think the way it will work is this: when you go to attach a bar line to a note, rest, chord Abjad will check and see if the note, rest, chord is (ultimately) enclosed in a score. If yes, you can attach. If no, you'll get an exception.

I know on first glance all this might "feel" like restricting user functionality. But think about it for a moment or two and you'll realize the restriction is actually super helpful!

While we're at it, there's an analogous behavior with time signatures. I'll point it out here. And time signatures will also be subject to a slight behavior change when I get around to this part of the code.

Recall that time signatures are scoped to the staff context in LilyPond. This is also true in Abjad. Now, check out the difference between these two examples.

This does what you expect:

staff = abjad.Staff("c'4 d' e'")
time_signature = abjad.TimeSignature((3, 4))
abjad.attach(time_signature, staff[0])
abjad.f(staff)
\new Staff
{
    \time 3/4
    c'4
    d'4
    e'4
}

This is probably a surprise!

voice = abjad.Voice("c'4 d' e'")
time_signature = abjad.TimeSignature((3, 4))
abjad.attach(time_signature, voice[0])
abjad.f(voice)
\new Voice
{
    %%% \time 3/4 %%%
    c'4
    d'4
    e'4
}

Crazy, huh?

This is ancient code I wrote a million years ago. What's going on here is that I'm forbidding the time signature from visibly appearing in LilyPond until the Abjad user encloses the whole thing in an Abjad staff. Notice that now stuff works how you'd expect, with no commented-out time signature:

staff = abjad.Staff([voice])
abjad.f(staff)
\new Staff
{
    \new Voice
    {
        \time 3/4
        c'4
        d'4
        e'4
    }
}

The behavior's less than ideal because it's too implicit: Abjad users can do all this stuff without realizing what's happening behind the scenes. (Quite possibly being unaware that a time signature won't show up.)

Much better is the bar line behavior you stumbled upon: at least the exception trains users into how to work with the system!

Finally to note is that there's another very important motivator to all this context-checking at attach-time. When you start using abjad.get.effective() [which was abjad.inspect().effective in Abjad 3.1] all this "effective indicator" stuff only works when an explicit abjad.Staff or abjad.Score context is present.

This works exactly as you expect:

staff = abjad.Staff("c'4 d' e'")                                                          
time_signature = abjad.TimeSignature((3, 4))                                              
abjad.attach(time_signature, staff[0])                                                    
for note in staff:                                                                        
    effective_time_signature = abjad.get.effective(note, abjad.TimeSignature)             
    print(note, effective_time_signature)                                                 
    c'4 3/4
    d'4 3/4
    e'4 3/4

But this doesn't:

voice = abjad.Voice("c'4 d' e'")
time_signature = abjad.TimeSignature((3, 4))
abjad.attach(time_signature, voice[0])
for note in voice:
    effective_time_signature = abjad.get.effective(note, abjad.TimeSignature)
    print(note, effective_time_signature)
    c'4 3/4
    d'4 None
    e'4 None

This is basically Abjad pushing you to make sure there's an explicit abjad.Staff present when you're working with indicators that are naturally scoped at the staff, and also that you have an explicit abjad.Score present when you are working with indicators that are naturally scoped at the score.

When I add the generalized raise-an-exception-at-attach-time code then the behavior will be consistent, and the system will nudge you towards having explicit contexts.

gilbertohasnofb commented 3 years ago

Hi Trevor, thanks for the reply (super detailed as always), this is really appreciated. I understand the motivations you state, but I hope you won't mind me sharing a couple of thoughts about this.

Then it's actually quite helpful that Abjad is alerting you to the fact that you're effectively trying to overwrite your own intentions.

In my view, the main issue with this is that sometimes you are not overwriting your own intentions, but you are explicitly adding identical bar lines or time signatures to multiple staves, which allows for easier part extraction. The code below is a perfectly valid LilyPond file:

\version "2.20.0"

music_a = {c'1 \bar "||" d'1}
music_b = {e'1 \bar "||" f'1}

\score{
    <<
        \new Staff \with {instrumentName = "A"} \music_a
        \new Staff \with {instrumentName = "B"} \music_b
    >>
    \layout{}
}

\score{
    \new Staff \with {instrumentName = "A"} \music_a
    \layout{}
}

\score{
    \new Staff \with {instrumentName = "B"} \music_b
    \layout{}
}

image

Sure, an user might enter some non-sense and get things overwritten, but is it really the job of Abjad to check for that? LilyPond doesn't even output a warning in the log when these things happen, e.g.:

\version "2.20.0"

music_a = {c'1 \bar "||" d'1}
music_b = {e'1 \bar "!" f'1}  % different bar line

\score{
    <<
        \new Staff \with {instrumentName = "A"} \music_a
        \new Staff \with {instrumentName = "B"} \music_b
    >>
    \layout{}
}

\score{
    \new Staff \with {instrumentName = "A"} \music_a
    \layout{}
}

\score{
    \new Staff \with {instrumentName = "B"} \music_b
    \layout{}
}

image

An Abjad user might try the first Abjad example in my issue, stumbled with that error message, and then assume when they write...:

staff_1 = abjad.Staff(r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4")
staff_2 = abjad.mutate(staff_1).copy()

score = abjad.Score([staff_1, staff_2])

abjad.attach(abjad.BarLine("||"), staff_1[1])

... that the bar line is added to both staves. Then when this user is ready to export parts, they might fail to notice that the bar line is in fact attached only to the first staff, i.e.:

>>> abjad.f(staff_1)
\new Staff
{
    \time 4/4
    R1
    R1
    \bar "||"
    \time 6/4
    R1 * 3/2
    R1 * 3/2
}
>>> abjad.f(staff_2)
\new Staff
{
    \time 4/4
    R1
    R1
    \time 6/4
    R1 * 3/2
    R1 * 3/2
}

In the worst case scenario, this user might discover the problem only after shipping parts for the performers.

Your second example is different, however: my intention is to forbid the second example.

You might argue that the better approach in these examples would be to have a global variable for common time signatures and bar lines, but a counterargument is that LilyPond allows for this sort of approach I used in my initial example and, by limiting it, you are effectively locking users into a single mode of working. From the user point of view, LilyPond uses this very clear underpinning notion that whatever comes last in a simultaneous situation overwrites the previous entry. This is consistently applied, and makes the behaviour of the software very consistent for users.

This is sort of similar with the time signature and voice issue you mentioned. I had already stumbled upon that (I believe I initially reported it as a bug). It's a behaviour that is somewhat cryptic to new users, particularly those who happen to be longer-time LilyPond users and who know that the sort of syntax below, albeit unclear, is perfectly valid once again:

\version "2.20.0"

\new Voice {\time 3/4 c'2. d'2.}

image

With all that said, would you be able to show me in my simple minimal example below how to adapt the code so that I can have the double bar lines both in the full score as well as when extracting parts with the minimal amount of code?

staff_1 = abjad.Staff(r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4")
staff_2 = abjad.mutate(staff_1).copy()

abjad.attach(abjad.BarLine("||"), staff_1[1])
# abjad.attach(abjad.BarLine("||"), staff_2[1])  # commenting this out

score = abjad.Score([staff_1, staff_2]) 

abjad.show(score)
abjad.show(staff_1)
abjad.show(staff_2)

Many thanks!

trevorbaca commented 3 years ago

Hi @gilbertohasnofb. There's a context keyword you can set in abjad.attach()!

What the final example is really wanting to do is to treat bar lines as though they're scoped to the staff instead of the score. You can do that like this:

string = r"\time 4/4 R1 R1 \time 6/4 R1 * 6/4 R1 * 6/4"
staff_1 = abjad.Staff(string)
staff_2 = abjad.Staff(string)

abjad.attach(abjad.BarLine("||"), staff_1[1], context="Staff")
abjad.attach(abjad.BarLine("||"), staff_2[1], context="Staff")

score = abjad.Score([staff_1, staff_2]) 
abjad.show(score)
score
abjad.show(staff_1)
staff-1
abjad.show(staff_2)
staff-2

(I replaced the call to abjad.mutate.copy() with staves that initialize from a copy of the same string. Wherever you're able to do that sort of thing, it will always make your code faster in the end. The mutate operations are the most expensive in the system.)

Also, all your ideas here are good ones. Let's save them for a future design discussion! The general concept that we're thinking through here is probably called something like "contexted indicators." I've been looking forward to rethinking the entire concept in Abjad from top to bottom. When the time comes for that then we should remember to come back to these great observations!

gilbertohasnofb commented 3 years ago

Thanks a lot @trevorbaca, I will use the context keyword!

trevorbaca commented 1 year ago

Note for future thought on this issue: it's possible that Abjad should be extended to allow "duplicate" indicators to be attached to different leaves in a new way that Abjad currently forbids.

Consider the first note (or rest) of each of the (probably) four voices in a string quartet. These four notes all begin at the same offset (zero), and these four notes all live in different contexts (primary music voices of violin 1, violin 2, viola, cello). When attaching the first abjad.MetronomeMark in the score, to which leaf should the metronome mark be attached? My usual solution to this is to use a global context (filled with skips) specifically to hold time signature and tempo information. But what if a user doesn't include a global context (or similar) in their score? There's then a decision to be made: should the first metronome mark of the piece attach to the first leaf of v1, v2, va or vc? The decision feels arbitrary: why attach a score-contexted indicator like abjad.MetronomeMark to a note in v1 as opposed to vc? The decision also feels 'unbalanced': why attach a score-contexted indicator like abjad.MetronomeMark to only 1 note at offset zero (instead of all 4 such notes)? And, more importantly, beyond these feelings of arbitrariness or asymmetry is the question of what should happen during the creation of parts: if the first metronome mark of the piece is attached (only) to the first note of v1, then what happens when music for vc is extracted to create the cello part?Will the first metronome mark of the piece be found? Or not? The situation probably forces users into something that functions like a global context.

Perhaps Abjad should instead allow the first metronome mark of the piece to be attached to as many different leaves at offset zero as the composer would like. This would presumably ease the construction of parts considerably, because metronome mark information could be attached to the first note in each part. The behind-the-scenes code in Abjad that determines effective indicators would have to be taught about this, but it would work. And the only constraint that users would have to follow is that multiple metronome marks attached to different leaves that all start at the same offset must all compare equal: if the starting tempo is q=66 then q=66 can be attached to as many leaves at offset zero as the user likes, but attaching q=44 to the viola would still be disallowed.