Rastrum: format-neutral Music Notation (LilyPond, MusicXML)

Hey,

A new quark, an attempt at music notation in SC, not organized around a notation backend. Has been in the lab for the past few months alongside metasonic-score, a sibling built around the same format-neutral premise.

It is now useful enough that I would like feedback on it: GitHub - smoge/Rastrum: Music notation for SuperCollider: a format-neutral score model with LilyPond, MusicXML and JSON writers. · GitHub

Start here to get started with the public API: tutorial.scd is a tested, linear walk through the API. It builds a score, writes the same musical structure in several forms, and plays it. Its examples are meant to be evaluated in sequence.

For reading the implementation, developers can start here: Classes/Duration.sc, whose header lists the source files by their design layer.


Why a “format-neutral” model?

I wrote LilyCollider and the FOMUS quark, and presented both at the SuperCollider Symposium a while ago. I’ve been working with music notation, but nothing I felt was generally useful for other people. This one feels a bit different, with a relevant distinction this time: the old ones delegated the central notation model to an external system. That is the assumption I have since revised: a backend, given the chance, will mistake itself for the music.

When a library is built around one output format, that format’s vocabulary becomes the library’s internal model. Durations turn into strings, tuplets into backend calls, and typographic instructions into score objects. By the time a second backend is wanted, the model usually has to be rebuilt rather than extended. That’s a mistake some projects got into.

Rastrum had LilyPond and MusicXML writers from the first commit (plus a custom JSON format to communicate with my other projects). If a musical concept can be expressed properly by only one of them, the mismatch appears while the model is still cheap to revise.

More generally, Rastrum distinguishes between score facts supplied by the composer, notation structure derived from those facts, and backend-specific interpretation. Preparation and validation happen before a writer or playback projection receives the score, so a backend should not have to guess what the model meant.

ScoreJSON is the contract between the two sibling projects. It is a versioned interchange format local to Rastrum and metasonic-score, and I am not proposing it as a general notation standard. Today Rastrum reads and writes ScoreJSON. metasonic-score reads and checks it, with the Haskell-to-Rastrum path still being built. This lets the two projects preserve their own types and operations rather than requiring either one to adopt the other’s internal model. At the same time, since metasonic-score is more mature and has better tooling, this bridge is also useful to keep Rastrum safer even with dynamic typing and minimal tooling.


Rastrum represents a score as a tree of musical objects: scores, staves, measures, voices, tuplets, notes, chords, and rests. That tree can be written as LilyPond, MusicXML, or ScoreJSON (a format specific to this project and metasonic-score), also projected into SuperCollider events and patterns for playback. Note: none of those outputs is the model’s native form. The score tree contains no LilyPond, MusicXML, JSON, or playback syntax. That’s the design principle.

Installation instructions and a compact reference are in README. Keeping the executable examples in the repository, where they are tested, rather than duplicating them in this post. Go there to see them, so there is no risk of stale code getting forgotten here in the forum posts.

Time, pitch, intervals, and meter: Durations are exact rationals, which is why Rastrum depends on the Rational quark. Pitches retain their letter, accidental, and octave rather than collapsing into MIDI numbers. Quarter-tone alterations and interval qualities use the same machinery rather than a separate microtonal pitch type. Pitch and interval arithmetic follows an affine-space model*. Pitches are points and intervals are vectors. Subtracting one pitch from another produces the signed spelled interval between them. Adding an interval to a pitch produces another pitch, and intervals can be combined with other intervals. Two pitches cannot be added because pitch space has no privileged origin. Transposition therefore preserves letter motion as well as chromatic displacement. A Meter also carries a metric hierarchy, not only the pair printed as a time signature. Rastrum uses that hierarchy when deriving notation such as beam groups and readable tied note heads. The playback events represent sounding onsets rather than a literal traversal of the written note heads.


Rastrum also uses doctests. These examples are executable rather than decorative. The doctest runner evaluates each expression and compares the result with the documented answer. The verification tooling (a simple sclang script, nothing more than that) is included in the repository: tools/doctest.scd.

That bit can be useful for other projects. It was useful for me, at least.

Just a holla at you guys who were in earlier discussions about music notation, in case the post above is of interest: @semiquaver @PvN @rdd and others over the years

@prko and @josh: the MusicXML writer is probably very close to what you discussed

Thank you for this!
I would like to test this, but I see the following error after installing your quark:

ERROR: Message 'at' not understood.
RECEIVER:
   nil
ARGS:
   Symbol 'linear'
KEYWORD ARGUMENTS:
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Object:doesNotUnderstand
		arg this = nil
		arg selector = 'at'
		arg args = [*1]
		arg kwargs = [*0]
	Symbol:asWarp
		arg this = 'linear'
		arg spec = <instance of ControlSpec>
	ControlSpec:init
		arg this = <instance of ControlSpec>
	Meta_ArgSpec:initClass
		arg this = <instance of Meta_ArgSpec>
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_ArgSpec>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*48]
		arg function = <instance of Function>
		var i = 19
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_Spec>
		var implementsInitClass = nil
	Meta_UIn:initClass
		arg this = <instance of Meta_UIn>
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_UIn>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*817]
		arg function = <instance of Function>
		var i = 177
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_Object>
		var implementsInitClass = nil
	Process:startup
		arg this = <instance of Main>
		var time = 0.978590458
...

^^ ERROR: Message 'at' not understood.
RECEIVER: nil

Messages with a similar name understood by the receiver:
	as
	rate
	halt

Many other objects respond to the message 'at' (found 104 superclasses).

Honestly, this error is not due to your quark since I do not see any error when only using your quark.
Do you see any reason in the following overwritten methods?

Overwritten methods in class library:
-------------------------------------

SequenceableCollection
----------------------
	SequenceableCollection:asAscii
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Strang/Strang.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/wslib/wslib-classes/Extensions/String/extString-collection.sc

View
----
	View:asFlowView
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:deepDo
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:allChildren
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:flow
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:horz
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:vert
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:comp
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	View:scroll
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc

Event
-----
	Event:delta
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Rational/SystemOverwrites/extArrayedCollection.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Collections/Event.sc

WFSPath_Old
-----------
	WFSPath_Old:asWFSPointGroup
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/WFSPointGroup.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/UPanCenter/USpeakerConf.sc

TempoClock
----------
	TempoClock:isRunning
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/ddwGUIEnhancements/SystemOverwrites/TempoClock-isRunning.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Core/Clock.sc

OutputProxy
-----------
	OutputProxy:controlIndex
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/sc3-dot/sc/SystemOverwrites/OutputProxy.ext.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Audio/UGen.sc

Pseq
----
	Pseq:embedInStream
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Rational/SystemOverwrites/extArrayedCollection.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Streams/ListPatterns.sc
	Pseq:storeArgs
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Rational/SystemOverwrites/extArrayedCollection.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Streams/ListPatterns.sc

FlowView
--------
	FlowView:flow
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/backwardsCompatibility/viewExtensions.sc
	FlowView:vert
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/backwardsCompatibility/viewExtensions.sc
	FlowView:horz
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/backwardsCompatibility/viewExtensions.sc
	FlowView:comp
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/backwardsCompatibility/viewExtensions.sc
	FlowView:scroll
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/backwardsCompatibility/viewExtensions.sc

Point
-----
	Point:@
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/MathLib/classes/SpherCoords/SystemOverwrites/extPoint.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Geometry/Point.sc
	Point:angle
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/wslib/wslib-classes/Extensions/Various/extPoint-variousOps.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/MathLib/classes/SpherCoords/SystemOverwrites/extPoint.sc
	Point:min
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/wslib/wslib-classes/Extensions/Various/extPoint-variousOps.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/ddwGUIEnhancements/cross-platform-recursiveResize.sc
	Point:max
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/wslib/wslib-classes/Extensions/Various/extPoint-variousOps.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/ddwGUIEnhancements/cross-platform-recursiveResize.sc

Window
------
	Window:asFlowView
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	Window:flow
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc
	Window:comp
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/crucial-library/Gui/SystemOverwrites/flowViewExtensions.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/GUI/Base/viewExtensionsQt.sc

WFSPath2
--------
	WFSPath2:asWFSPointGroup
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/WFSPointGroup.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/WFSPath2.sc

MultiRangeSpec
--------------
	MultiRangeSpec:originalSpec
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Unit-Lib/Classes/GUI/Arg/plusSpec-makeView.sc
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/Unit-Lib/Classes/Arg/Specs.sc

Number
------
	Number:@
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/MathLib/classes/SpherCoords/SystemOverwrites/extNumber.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Math/Number.sc

String
------
	String:postln
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/ddwStatusBox/SystemOverwrites/ext-StatusBox.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Collections/String.sc
	String:post
		/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/ddwStatusBox/SystemOverwrites/ext-StatusBox.sc
		/Users/prko/Dropbox/prko/__myDocs/Writings/Digital Sound Production/mixed/dev/SuperCollider-3.15.0_prko.app/Contents/Resources/SCClassLibrary/Common/Collections/String.sc

-> MethodOverride
1 Like

Thanks for reporting this. Could you test once more with only the Unit-Lib quark disabled, leaving everything else unchanged?

During class-library initialization, Rastrum only registers a startup function:

*initClass {
    StartUp.add {
        outputDirectory = Platform.userAppSupportDir +/+ "rastrum-output";
        lilypondPath = lilypondPath ?? { this.findLilypond };
    }
}

That code does not build a ControlSpec, call Warp or Spec, or touch Unit-Lib. So my current reading is: Rastrum may change class-library startup order enough to expose an initialization-order issue in Unit-Lib.

Unless you see the problem even with this quark disabled, then we need more digging.

Let me know

EDIT: @prko , I checked their repo and there is an update 2 days ago. Maybe you could just update the Unit-Lib quark and see if that was the problem? Again, it is just a wild guess. If that doesn’t solve it, maybe you could give me more details about your setup.

1 Like

Thanks for your feedback.

Disabling Unit-Lib returns more errors:

ERROR: Superclass 'SplitBufSndFile' of class 'UBinauralBufs' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/UDecodeBinaural.sc
ERROR: Superclass 'RectSpec' of class 'WFSRectSpec' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFS_Specs.sc
ERROR: Superclass 'GenericDef' of class 'SimpleTransformerDef' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/SimpleTransformer.sc
ERROR: Superclass 'AbstractRichBuffer' of class 'WFSPathBuffer' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/WFSPathBuffer.sc
ERROR: Superclass 'PointSpec' of class 'WFSMultiPointSpec' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFS_Specs.sc
ERROR: Superclass 'USharedValueIn' of class 'USharedPointIn' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/USharedPointIO.sc
ERROR: Superclass 'PointSpec' of class 'WFSPointSpec' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFS_Specs.sc
ERROR: Superclass 'SharedValueIDSpec' of class 'SharedPointIDSpec' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFS_Specs.sc
ERROR: Superclass 'ObjectWithArgs' of class 'SimpleTransformer' is not defined in any file.
/Users/prko/Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/WFSPath/SimpleTransformer.sc
ERROR: Class extension for nonexistent class 'SimpleTransformerDef'
     In file:'../../../../../../../../../../../Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/GUI/WFSPath/extSimpleTransformerDef-makeViews.sc'
ERROR: Class extension for nonexistent class 'SimpleTransformer'
     In file:'../../../../../../../../../../../Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/GUI/WFSPath/extSimpleTransformerDef-makeViews.sc'
ERROR: Class extension for nonexistent class 'WFSPathGeneratorDef'
     In file:'../../../../../../../../../../../Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/GUI/WFSPath/extWFSPathGeneratorDef-prMakeArgViews.sc'
ERROR: Class extension for nonexistent class 'ArrayGeneratorDef'
     In file:'../../../../../../../../../../../Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/ArrayTransformer/extArrayGeneratorDef-prMakeArgViews.sc'
ERROR: Class extension for nonexistent class 'UScore'
     In file:'../../../../../../../../../../../Library/Application Support/SuperCollider/downloaded-quarks/WFSCollider-Class-Library/WFS/UScore-openWFS.sc'
ERROR: There is a discrepancy.
numClassDeps 3014   gNumClasses 5982
ERROR: Library has not been compiled successfully.

Unit-Lib is a required dependency of WFSCollider-Class-Library in your setup. The crash is probably not Rastrum + Unit-Lib alone.

Maybe you can try something like this to dig a bit more:

  1. re-enable Unit-Lib (updated)
  2. disable WFSCollider-Class-Library (temporary, just for troubleshooting)
  3. keep Rastrum enabled
  4. recompile

This works!

However, what could be a resolution for the users who want to use both Quarks?

Not sure, dawg) Don’t think it’s a bug on this side, to the best of my knowledge. My guess: it uncovered an initialization-order issue in Unit-Lib. Would be great to have someone more familiar with those quarks examine it.

Anyhow, lemme know if it’s useful for you and if you encountered something to sweeten there.

Here is my first test result for the initial section of the provided SCD file. It opens a rendered PDF but contains errors.
I’ll report the process step by step.

~note = MusicNote(\c, Duration.quarter);

returns:

-> MusicNote(MusicPitch(c, Duration(0/1), 4), Duration(1/4))

~note.postln;

returns:

MusicNote(MusicPitch(c, Duration(0/1), 4), Duration(1/4))
-> MusicNote(MusicPitch(c, Duration(0/1), 4), Duration(1/4))

~note.class.postln;                  // MusicNote

returns:

MusicNote
-> MusicNote

~bar = Measure(Meter(4, 4), [
    MN(\c, Duration.quarter),        // MN is MusicNote, but shorter
    MN(\d, Duration.quarter),
    MN(\e, Duration.half)
]);

returns:

-> Measure(Meter(4/4))

~bar.leaves.first.class.postln;      // MusicNote, whichever way it was written

returns:

MusicNote
-> MusicNote

~score = MusicScore([Staff([~bar], "Violin", \treble)], "Overview", "You");

returns:

-> a MusicScore

~same = RhythmTree.measure(Meter(4, 4), [1, 1, 2], [\c, \d, \e]);

returns:

-> Measure(Meter(4/4))

(~same.leaves.collect { |leaf| leaf.dur }
    == ~bar.leaves.collect { |leaf| leaf.dur }).postln;

returns:

true
-> true

~alsoSame = Measure.durations(Meter(4, 4), [1%/4, 1%/4, 1%/2], [\c, \d, \e]);

returns:

-> Measure(Meter(4/4))

(~alsoSame.leaves.collect { |leaf| leaf.dur }
    == ~bar.leaves.collect { |leaf| leaf.dur }).postln;

returns:

true
-> true

Rastrum.preview(~score, "overview");

returns:

ERROR: Changing working directory to: `/Users/prko/Library/Application Support/SuperCollider/rastrum-output'ERROR: 
Processing `/Users/prko/Library/Application Support/SuperCollider/rastrum-output/overview.ly'
Parsing...ERROR: 
Interpreting music...ERROR: 
Preprocessing graphical objects...
Interpreting music...
MIDI output to `overview.midi'...
ERROR: Finding the ideal number of pages...
Fitting music on 1 page...
Drawing systems...ERROR: 
Converting to `overview.pdf'...
ERROR: Success: compilation successfully completed
-> /Users/prko/Library/Application Support/SuperCollider/rastrum-output/overview.ly

~show = { |bars, clef = \treble, layout = \default, name = "overview"|
    var score = MusicScore([Staff(bars.asArray, "Violin", clef)], "Overview");
    Validator.validate(ScorePrepare.run(score));
    Rastrum.preview(score, name, layout: layout)
};

returns:

-> a Function

~refused = { |label, thunk|
    var caught = false;
    try { thunk.value } { |error|
        caught = true;
        ("  % : %".format(label, error.errorString)).postln;
    };
    if (caught.not) { Error("% was accepted".format(label)).throw };
};

returns:

-> a Function


My impression

Providing classes such as Measure, Duration, MusicNote, MusicRest, and Chord is a great idea. However, the syntax feels rather verbose in practice.

For example:

~bar = Measure(Meter(4, 4), [
    MN(\c, Duration.quarter),
    MN(\d, Duration.quarter),
    MN(\e, Duration.half)
]);

could be shortened to something like:

~bar = B(M(4%/4), [
    N(c: D(1%/4)),
    N(d: (1%/4).dur),
    N(e: (1%/2).rhy)
]);

Even so, it still feels lengthy. It would be fantastic if there were a simple set of syntactic sugar for these classes.

I also had very similar thoughts when developing my Notator quark.
For this reason, I decided not to provide those classes and instead focused on developing simpler ways to notate diverse musical symbols in my quark:

I still find these approaches somewhat complicated.
Other quarks and extensions also offer simple notation methods, but they don’t seem to include what I implemented:

My question

We already have FOSC, which is also introduced in The SuperCollider Book, Second Edition.
Do we really need another LilyPond implementation? Would it be better to improve FOSC instead?

  • Creating similar quarks may confuse users, in my opinion, due to too many different approaches.

  • Also, when publishing a new quark, careful and diverse testing is essential to avoid compilation errors.

  • However, an algorithmic composition-specialised language should, of course, include classes and syntax for music notation!

Thanks, man. This is more helpful.

About the render messages in the IDE: yea, it looks confusing. In this case, it is not a failed render. The reason is something else. LilyPond writes its normal messages, including the success message, to stderr , and the IDE labels that stream as ERROR . Since the PDF opened and Rastrum returned the .ly path, LilyPond finished successfully. But from a user’s point of view this is definitely unsettling, so I should find a way to make successful renders quieter/clean.

Also agree about verbosity. But I should give a bit of context here, and some of that I touched on in the first post. Rastrum starts from explicit score objects, because the model is meant to be independent from any single backend: the same score tree can write lilypond, muysicxml, json, Events, and Patterns. But that does not mean users should have to write the full scaffolding all the time! I plan to add smaller helpers and tiny parsers too, and at some point syntax work will focus on making things more concise and expressive. I went through a similar phase in metasonic-scorel, though of course sclang is a different world.

fosc and abjad are LilyPond-centered, in the reasonable sense: it gives the users a broad set of tools for building and controlling LilyPond notation. Rastrum has a different design premise. That is the point, difference is probably not clear enough yet in the examples and readme, so your feedback helps a lot (I should document that point more clearly). There is no need for a major redesign to address the useful parts, including concise and expressive user-facing syntax. The current architecture should be able to support that by adding layers above the model and in the writers. That’s just a different way to get there. Rastrum has a different model at the core, it’s that simple)

In any case, there’s nothing wrong with implementing something with a different design. In the past, I got something like this when the design was different from other earlier implementations. I think this is okay, not a problem. Maybe that’s on me for not making that distinction more transparent.

I will try to get back to you when there are improvements on those lanes. To be honest, I had those things in mind already, it was a fine thing you perceived them the same way

Thank you for your opinion.

Rastrum starts from explicit score objects

I love this point and wish you every success in developing both well‑structured classes for music notation and a user‑friendly interface. As I focused on my Notator, I realised that the more well‑structured the classes for music notation become, the more complex it is to notate music in the form of code. However, you have more experience in designing such systems and can surely find an optimal balance.

My hope is that sclang will eventually offer an official way to notate music scores simply by understanding sclang and music theory, without relying on other notation systems such as LilyPond, Guido, ABC, Music21 and so on.

We don’t always need to write the model directly. A concise helper, parser, or notation DSL can sit above it and generate the same score objects underneath. The model is there so the result has a precise musical meaning and can still be safely validated, transformed, manipulated, exported, and played as Events or Patterns.

I agree with your hope for a more expressive notation. My view is just that a clean internal model helps that happen, rather than getting in the way.

1 Like

The Unit-Lib problem is a quite strange error. I have to admit that I don’t see why it’s happening.

  • Unit Lib’s ArgSpec *initClass calls Class.initClassTree( ControlSpec );
    • ControlSpec *initClass initializes Spec and Warp.
      • Warp *initClass initializes its warps array.
  • So then, by the time ArgSpec *initClass starts creating a bunch of control specs, the warps array should already exist.

But Symbol:asWarp does Warp.warps.at(this), and this is where it dies.

The only way this error would occur is if Warp *initClass didn’t happen, but the class library already guarantees that it will happen. If the Class.initClassTree(OtherClass) interface is no longer a guarantee, that’s a pretty serious problem.

hjh

2 Likes

If MusicNote is an abstract class for C, D, E, F, G, A, and B, an instance of MusicNote can be defined as follows:

  • .new(octave, centShift (when symbol) or intervalShift (when rational), length, articulation, tie, slur, accidentalShow)
  • Examples
    • Middle C♯ with a crotchet rhythm, staccato, a tie start, and a slur start:
      • C(4, \p100, 1%/4, $., \tb, \sb)
      • C(4, 1%/2, 1%/4, \stc, \b, \b, true)
      • C(4, 1%/2, 4, \stc, \b, \b, true)
      • C(4, 1%/2, \c, \stc, \b, \b, true)
      • C(4, 1%/2, \q, \stc, \b, \b, true)
      • C(4, 1%/2, D(1%/4), \stc, \b, \b, true)
      • C(4, 1%/2, D(4), \stc, \b, \b, true)
      • C(4, 1%/2, D.q, \stc, \b, \b, true)
    • Middle E♭, lowered by 1/4, with a crotchet rhythm, staccato, a tie start, and a slur start:
    • E(4, \m150, 1%/4, $., \tb, \sb)
    • E(4, -3%/4, 1%/4, \stc, \b, \b, true)
    • E(4, -3%/4, 4, \stc, \b, \b, true)
    • E(4, -3%/4, \c, \stc, \b, \b, true)
    • E(4, -3%/4, \q, \stc, \b, \b, true)
    • E(4, -3%/4, D(1%/4), \stc, \b, \b, true)
    • E(4, -3%/4, D(4), \stc, \b, \b, true)
    • E(4, -3%/4, D.q, \stc, \b, \b, true)

If MusicNote is an abstract class for Cf, C, Cs, Df, D, Ds, Ef, E, Es, Ff, F, Fs, Gf, G, Gs, Af, A, As, Bf, B, and Bs, an instance of MusicNote can be defined as follows:

  • .new(octave, centShift or intervalShift, length, articulation, tie, slur, accidentalShow)
  • Examples
    • Middle C♯ with a crotchet rhythm, staccato, a tie start, and a slur start:
      • Cs(4, 0, 1%/4, $., \tb, \sb)
      • Cs(4, 0, 1%/4, \stc, \b, \b, true)
      • Cs(4, 0, 4, \stc, \b, \b, true)
      • Cs(4, 0, \c, \stc, \b, \b, true)
      • Cs(4, 0, \q, \stc, \b, \b, true)
      • Cs(4, 0, D(1%/4), \stc, \b, \b, true)
      • Cs(4, 0, D(4), \stc, \b, \b, true)
      • Cs(4, 0, D.q, \stc, \b, \b, true)
    • Middle E♭, lowered by 1/4, with a crotchet rhythm, staccato, a tie start, and a slur start:
      • Ef(4, \m50, 1%/4, $., \tb, \sb)
      • Ef(4, -1%/4, 1%/4, \stc, \b, \b, true)
      • Ef(4, -1%/4, 4, \stc, \b, \b, true)
      • Ef(4, -1%/4, \c, \stc, \b, \b, true)
      • Ef(4, -1%/4, \q, \stc, \b, \b, true)
      • Ef(4, -1%/4, D(1%/4), \stc, \b, \b, true)
      • Ef(4, -1%/4, D(4), \stc, \b, \b, true)
      • Ef(4, -1%/4, D.q, \stc, \b, \b, true)

Then,

~bar = B(M(4%/4), [
    N(c: D(1%/4)),
    N(d: (1%/4).dur),
    N(e: (1%/2).rhy)
]);

could be written as:

~bar = B(M(4%/4), [
    C(4, 0, \q),
    D(4, 0, D.q),
    E(4, 0, D(4))
]);

Your current syntax is:

~bar = Measure(Meter(4, 4), [
    MN(\c, Duration.quarter),
    MN(\d, Duration.quarter),
    MN(\e, Duration.half)
]);

It would be nice if we could have a single‑character operator for fractional expressions.

Thanks!
I opened an issue: ERROR: Message 'at' not understood. · Issue #62 · GameOfLife/Unit-Lib · GitHub
However, it seems unmaintained currently.

This is fixed by the UNIT-Lib maintainer! Thanks!

about your Cs (Ds, Ef, &c.) class suggestion: I understand the appeal of saving characters early, but we want no class per spelling: it saves a few characters, but it adds many new classes to sclang’s flat global namespace (a reality of the language we can’t change).

Even if this were not a problem, there is something even more relevant: it also works against the model Rastrum uses for intervals: a pitch needs to carry notename (or letter), accidental, octave, and optional cents, because that spelling is what lets MusicInterval know whether something is major, minor, perfect, neutral, semi-augmented, and so on. (See the code in MusicInterval.sc for more)

Makes more sense now?

Yes, thanks!

However, making sense and using it in practice are different things.
I’m glad to see that you’ve switched to LilyPond‑style pitch names instead of Duration(x/y).
Still, it feels a bit too long.
Compare it with the LilyPond syntax for the same note:

  • MusicNote(MusicPitch("cqf4"), Duration(1/4))
  • cqf'4
  • C(4, \m50, \q) (my suggestion)
  • C(4, -1%/4, \q) (my suggestion)
  • (c: [\qf, 4, \q]) (my new suggestion)

Moreover, the following options should be attached to MusicNote:

  • dynamic mark
  • articulation
  • tie information, if any (for ties, I think \start, \continue, and \end are necessary; once a \start is notated, the next note of the same pitch should necessarily have \continue or \end)
  • slur information, if any (for slurs, notating only the start and end should be sufficient)
  • holding mark (fermata)
  • playing technique
  • ornaments
  • tempo
  • other text

The following matters should also be considered separately:

  • how to notate gradual tempo changes
  • how to notate gradual dynamic changes
  • how to notate multiple voices

In Western music notation, these are represented intuitively with simple symbols, but writing them in code is not nearly as intuitive.


I’m not sure whether adding many new classes to sclang’s flat global namespace would be a serious problem. The disadvantage of my idea is that users would need to hold the Shift key quite often.


Every SC user and developer can do whatever they want, whether for themselves or for the community. However, under the current environment — where using multiple externals or quarks often causes too many errors — publishing similar quarks or extensions may confuse users who are primarily musicians and not familiar with programming languages such as SuperCollider, LilyPond, Max, Pd, Python, or music21. I think having many similar packages isn’t very helpful.

Still, who knows — maybe in the third edition of The SuperCollider Book, Rastrum will be introduced! Cheers!

An opinion (not really a suggestion since Rastrum is already opinionated as it should be!) from a fellow sclanger/haskeller/music notation nerd (I have fadno-xml on hackage, a musicxml backend for my own fadno music model):

Preserving note information, pitch not being addable, and the whole spelling-oriented approach: this can be quite limiting when you want to leave the land of functional harmony. Pitches are still just numbers after all, so being able to add/subtract/even multiply simply increases expressive and generative power, whereas insuring that every motif can be sourced back to western spelling can be at a minimum awkward if not totally irrelevant for many compositional approaches. It’s certainly nifty in a Haskellian type safety way but in sclang’s far more crufty types this seems like a way to get frustrated once you leave functional-land (a pun, but doubly true).

In haskell I just parameterized Note p d and have a bunch of cases constraining p to Num, which is admittedly gross, but I’ve definitely enjoyed using arithmetic operators directly, and turning intervals directly into pitches (Rational for duration though is indeed the way.) In sc you of course can ducktype a lot of things. Perhaps a polymorphic way to use a sclang Number subclass could recover that utility although no idea if that would be an oo inheritance mess (probably).

It’s not just post-tonal btw: a lot of electronic music also has no need for key centers, and benefits from bizarre translations. What I often find is that the spelling is most often a way to write down a motif or a chord, after which I’m not that interested in them except as a debugging/logging utility – very important, but not one I’d give up numeric flexibility, especially given that sclang has such great tools for numeric and array manipulation.

1 Like