Yea, the notation syntax comes before the writer, not after it. It builds precisely the same objects that the core data type constructors build, so what you get back can be selected, edited, transformed, diffed, played, exported, rendered, etc. It isn’t a LilyPond string with a writer attached at the end. Quite the contrary, the compact notation is just one of the interface surfaces (a more friendly user-facing syntax) to describe the same tree.
Here is a score I posted before:
~m1 = Measure("4/4",
"cresc[c4:pp:tenuto 3:2[d8 eb8 f+8] <d e-'>4:text{pizz.}:f] "
"<e' d#+'>16:ff:stac r8.")
.metronome("4", 96, text: "Allegro");
~m2 = Measure.proportions("5/8[3+2]", "(1 (1 (1 1 1)) -1 2)",
"c:mp:text{ord.} d e- g f#+:ppp:tenuto");
~phrase = ScoreSelection(~m2).runs.first;
Spanner.slur(Spanner.beam(~phrase));
Spanner.diminuendo(ScoreSelection(~m2).pitched);
~score = MusicScore.oneStaff([~m1, ~m2], "Violin", \treble, "Rastrum");
~score.postcs hands back the core data types constructor form. It’s regular evaluable sclang code that would be equally valid to write:
MusicScore([
Staff([
Measure(Meter(4, 4), [
MusicNote(MusicPitch("c[4]"), Duration(1, 4))
.markings_([Marking.dynamic(\pp), Marking.articulation(\tenuto)])
.spanners_([Spanner.hairpinStart(\crescendo, 1)]),
Tuplet.ratio(3, 2, [
MusicNote(MusicPitch("d[4]"), Duration(1, 8)),
MusicNote(MusicPitch("eb[4]"), Duration(1, 8)),
MusicNote(MusicPitch("f+[4]"), Duration(1, 8))
]),
Chord([MusicPitch("d[4]"), MusicPitch("e-[5]")], Duration(1, 4))
.markings_([Marking.text("pizz.", \above), Marking.dynamic(\f)])
.spanners_([Spanner.hairpinStop(1)]),
Chord([MusicPitch("e[5]"), MusicPitch("d#+[5]")], Duration(1, 16))
.markings_([Marking.dynamic(\ff), Marking.articulation(\staccato)]),
MusicRest(Duration(3, 16))
])
.directions_([
Direction.tempo("Allegro", unit: Duration(1, 4), perMinute: 96)
]),
Measure(Meter.grouped(5, 8, [3, 2]), [
MusicNote(MusicPitch("c[4]"), Duration(1, 8))
.markings_([Marking.dynamic(\mp), Marking.text("ord.", \above)])
.spanners_([
Spanner.beamStart(1),
Spanner.slurStart(1),
Spanner.hairpinStart(\diminuendo, 1)
]),
Tuplet.ratio(3, 2, [
MusicNote(MusicPitch("d[4]"), Duration(1, 16)),
MusicNote(MusicPitch("e-[4]"), Duration(1, 16)),
MusicNote(MusicPitch("g[4]"), Duration(1, 16))
.spanners_([Spanner.beamStop(1), Spanner.slurStop(1)])
]),
MusicRest(Duration(1, 8)),
MusicNote(MusicPitch("f#+[4]"), Duration(1, 4))
.markings_([Marking.dynamic(\ppp), Marking.articulation(\tenuto)])
.spanners_([Spanner.hairpinStop(1)])
])
], "Violin")
], "Rastrum")
And ScoreDiff returns an empty delta between them:
~src = ~score.asCompileString;
ScoreDiff.between(~score, ~src.interpret).isEmpty // -> true
~score.treeString gives a visual representation of that same tree:
-> [] Score "Rastrum" staves: 1
|
`- [0] Staff "Violin" clef: treble bars: 2
|
+- [0,0] Measure 4/4 holds: 1 offset: 0 dirs: tempo "Allegro" 1/4=96
| |
| +- [0,0,0] Note c[4] 1/4 marks: pp, tenuto spanners: hairpin:start#1 crescendo
| |
| +- [0,0,1] Tuplet 3:2 3/8=>1/4
| | |
| | +- [0,0,1,0] Note d[4] 1/8=>1/12
| | |
| | +- [0,0,1,1] Note eb[4] 1/8=>1/12
| | |
| | `- [0,0,1,2] Note f+[4] 1/8=>1/12
| |
| +- [0,0,2] Chord <d[4] e-[5]> 1/4 marks: "pizz.", f spanners: hairpin:stop#1
| |
| +- [0,0,3] Chord <e[5] d#+[5]> 1/16 marks: ff, staccato
| |
| `- [0,0,4] Rest 3/16
|
`- [0,1] Measure 5/8 holds: 5/8 offset: 0
|
+- [0,1,0] Note c[4] 1/8 marks: mp, "ord." spanners: beam:start#1, slur:start#1, hairpin:start#1 diminuendo
|
+- [0,1,1] Tuplet 3:2 3/16=>1/8
| |
| +- [0,1,1,0] Note d[4] 1/16=>1/24
| |
| +- [0,1,1,1] Note e-[4] 1/16=>1/24
| |
| `- [0,1,1,2] Note g[4] 1/16=>1/24 spanners: beam:stop#1, slur:stop#1
|
+- [0,1,2] Rest 1/8
|
`- [0,1,3] Note f#+[4] 1/4 marks: ppp, tenuto spanners: hairpin:stop#1
once the code turns to engraving the transforms are done
Matter of fact, it might not be really about a design disagreement at all. It is about how each holds the material. If your composition process keeps notation as the final engraving step, then one enters the music score vicinities earlier than other composers would.
My workflow is a bit different. I move back and forth between the material and the score. I generate material, spell it, inspect or edit it as a score, transform it again, and render whenever useful. Then in the score editor, I still do more editing. For my workflow, rendering isn’t the end of the process. .render prepares and writes a copy without mutating the source tree. Only the preparation layer splits material across barlines, carry markings and spanners, and stuff like that. That’s the model work.
Your <fermata> example points to the rule I’m using. I don’t want the toolkit to mirror every MusicXML element. The vocabulary expresses (portable and non-editorial) score semantics, some reflect writer policy, and some make sense for a particular backend. The idea is to model the first, leave the second to the writers, and not deal with the third by default.
That doesn’t mean the toolkits can’t add the more detailed fermata support you mentioned. If a fermata shape makes sense, one can give it a place in the model and implement it in the writers that can represent it (or any other musical element for that matter). If only one writer can represent it, turn it into a backend-specific extension, no problema.
Feature by feature, that’s my element. Swallow a backend’s vocabulary whole and one morning one wakes up with a model with LilyPond’s face trying out MusicXML’s clothes
There’s also a practical point: covering the whole musicxml schema is much more practical in languages like Haskell, not sclang. There are xml libraries, schema and code-generation tools, and a dependency ecosystem to support this kind of work. I did experiment with generating a Haskell module directly from the musicxml 4.0 schema. But I never used it for my work. The module felt too “heavy” to adopt. A lot of the schema has to do with editorial details, not the musical semantics I wanted to work directly in this back-and-forth workflow I described.
Even with all the tools, I chose the lighter approach anyway. I’m sure it made more sense for your context and workflow, of course.
In sclang terrain, the lighter approach isn’t even a preference. The context is more modest, the tools were never built.
You were totally right in your first message. Those kinds of project are opinionated. Because we are. Artist, composer, programmer, them not different people taking shifts, but a totallity. Science wants generality. Art, more about aura vibes, that thing happening once. We are between those worlds. A tool with no clear opinion doesn’t offer neutrality, maybe it offers the illusion of it. It’s our ten cents, the two cents is free.