-
Notifications
You must be signed in to change notification settings - Fork 406
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
Saving a stream as musicxml alters the stream #1557
Comments
Not intended! Can you reduce the score to a minimum test set that displays the behavior? Does it do this if you leave out the stripTies? What happens around offset 87 that doesn't happen before in the piece? I ran into another case where showing something changed the original the other day, but unrelated to this. Show should not alter any non-private/"core" attributes |
I believe the problem occurs at notes that are tied across two measures (that's where La_Campanella.mxl goes awry as well). from music21 import stream, note, meter, tie
s = stream.Score(id='mainScore')
p0 = stream.Part(id='part0')
m01 = stream.Measure(number=1)
m01.append(meter.TimeSignature('4/4'))
d1 = note.Note('D', type="half", dots=1)
c1 = note.Note('C', type="quarter")
c1.tie = tie.Tie("start")
m01.append([d1, c1])
m02 = stream.Measure(number=2)
c2 = note.Note('C', type="quarter")
c2.tie = tie.Tie("stop")
c3 = note.Note("D", type="half")
m02.append([c2, c3])
p0.append([m01, m02])
s.insert(0, p0)
s = s.stripTies()
prev = sorted([e for e in s.flat], key=lambda x: x.offset)
for i, p in enumerate(prev):
print(f"{p.offset}, {p.measureNumber}, {p}, {p.duration.quarterLength}")
s.write("musicxml", "/tmp/_.mxl")
after = sorted([e for e in s.flat], key=lambda x: x.offset)
for i, p in enumerate(after):
print(f"{p.offset}, {p.measureNumber}, {p}, {p.duration.quarterLength}") I have three findings:
if isinstance(obj, stream.Stream) and self.makeNotation:
obj = obj.makeRests(refStreamOrTimeRange=[0.0, obj.highestTime],
fillGaps=True,
inPlace=False,
hideRests=True, # just to fill up MusicXML display
timeRangeFromBarDuration=True,
)
Feel free to let me know what you think of the proposed changes! |
Thanks for looking into this! 1 diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index c71de9596..859d1354c 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -496,9 +496,10 @@ class GeneralObjectExporter:
outObj = None
if isinstance(obj, stream.Stream) and self.makeNotation:
- obj.makeRests(refStreamOrTimeRange=[0.0, obj.highestTime],
+ # This is where the first (and hopefully only) copy is made.
+ obj = obj.makeRests(refStreamOrTimeRange=[0.0, obj.highestTime],
fillGaps=True,
- inPlace=True,
+ inPlace=False,
hideRests=True, # just to fill up MusicXML display
timeRangeFromBarDuration=True,
)
@@ -517,9 +518,9 @@ class GeneralObjectExporter:
def fromScore(self, sc):
'''
- Copies the input score and runs :meth:`~music21.stream.Score.makeNotation` on the copy.
+ Runs :meth:`~music21.stream.Score.makeNotation` on the copy.
'''
- scOut = sc.makeNotation(inPlace=False)
+ scOut = sc.makeNotation(inPlace=True)
if not scOut.isWellFormedNotation():
warnings.warn(f'{scOut} is not well-formed; see isWellFormedNotation()',
category=MusicXMLWarning)
@@ -548,7 +549,7 @@ class GeneralObjectExporter:
solutions in Part or Stream production.
'''
m.coreGatherMissingSpanners()
- mCopy = m.makeNotation()
+ mCopy = m.makeNotation(inPlace=True) 2 3 If you want to display the stripped ties score (are you sure you want to display that?) then you could makeTies() on it first, which restores the output I think you're expecting. Do you have an interest in preparing a PR for 1? |
This would be a new feature, but maybe the musicxml show pipeline could check to see if the most recent derivation was stripTies, and then makeTies() first. |
I don't think I want to go down this rabbit hole. stripTies() is an analysis function, there are tons of analysis functions that output cannot recover from so I don't want to set the precedent that we're supposed to be able to do that. |
I understand - it might be good to let the users know that they are responsible for not over/underfilling bars. |
As far as I can tell, (at least if programmatically created), without time signatures, measures expand to accept whatever notes are in them, making See this example (which is fixed if we replace from music21 import stream, note, meter, tie
s = stream.Score(id='mainScore')
p0 = stream.Part(id='part0')
m01 = stream.Measure(number=1)
m01.append(meter.TimeSignature('4/4'))
d1 = note.Note('D', type="half", dots=1)
c1 = note.Note('C', type="quarter")
c1.tie = tie.Tie("start")
m01.append([d1, c1])
m02 = stream.Measure(number=2)
c2 = note.Note('C', type="quarter")
c2.tie = tie.Tie("stop")
c3 = note.Note("D", type="half")
m02.append([c2, c3])
p0.append([m01, m02])
s.insert(0, p0)
s = s.stripTies()
for i, p in enumerate(s.flatten()):
print(f"{p.offset}, {p.measureNumber}, {p}, {p.duration.quarterLength}")
s = s.makeRests(refStreamOrTimeRange=[0, 8.0], fillGaps=True, inPlace=False, timeRangeFromBarDuration=True)
print("---------------")
for i, p in enumerate(s.flatten()):
print(f"{p.offset}, {p.measureNumber}, {p}, {p.duration.quarterLength}") Output
|
makeRests was called inPlace before copies were made. See cuthbertLab#1557
makeRests was called inPlace before copies were made. See #1557
Feel free to move this discussion point to a new issue. The reason I added measure repositioning to makeRests at all was in #922 to handle the case where underfilled measures are expanded to their bar length; without repositioning, you'd be left with measure overlap. You've presented here the converse, where overfilled measures, once repositioned to not overlap, produce an unintended musical result. I think your proposal makes sense. A regression test would be great. |
music21 version
8.1.0
Problem summary
When saving a Stream
s
to musicxml, the objects
is altered after saving to disk.The documentation does not mention this side effect, in fact quite the opposite: "Some formats, including .musicxml, create a copy of the stream, pack it into a well-formed score if necessary, and run makeNotation()." which suggests that the original stream is left unaltered.
Steps to reproduce
Get e.g., this .mxl file: https://musescore.com/user/27439014/scores/5316979
Expected vs. actual behavior
Expected:
No output
Actual:
More information
I traced the changes and believe these lines are responsible:
music21/music21/stream/makeNotation.py
Lines 852 to 858 in 696545a
Please let me know if the behaviour is intended.
Thanks for your help!
The text was updated successfully, but these errors were encountered: