Lomse Hacking Guide
The internal model is just a container for the ImoDocument tree, and its organization is not oriented to any particular task, but to describe the document contents. But, as this chapter explains, this tree representation is not best suited for music processing. Therefore, it has been necessary to create another representation for scores, that I name the staff objects collection. This chapter explains in detail the problem, the representational needs, the solution found, and the way it is implemented.
Music has two dimensions, the horizontal one, the melody, and the vertical one, the harmony. And none of them is maintained in the ImoDocument tree (or ImoScore tree).
Traversing the ImoScore tree following a melody line is not difficult, as the tree is organized by instruments. Therefore it is enough to traverse each instrument children nodes in sequence (breadth-first tree traversing), taking nodes that represent notes and rests.
But traversing the tree following the vertical dimension of music (all notes and rests ordered by time, without segregating by instrument and voice) is impractical. Consider that:
Therefore, we can foresee that for those tasks that will require traversing the score in its vertical dimension, using the ImoScore tree will impose performance penalties. So, if traversing music in is vertical dimension is going to be a frequent task, it is better to precompute time, sort the nodes by time and save the resulting set. This is basically the idea behind the staff objects collection: a collection of nodes ordered by time.
The need for having this auxiliary staff objects collection did not come from musical tasks (play back, harmonic analysis, etc.) for which only musical events (notes and rests) are necessary. Also, for these tasks the cost of creating on demand this ordered collection of notes and rests would probably not cause any performance issue.
The need to create this auxiliary representation comes from rendering, that is, the algorithms for building a bitmap with the engraved score. For rendering music, both music dimensions has to be taken into account at the same time. For instance, notes, rests and other elements in one staff must satisfy the constrains imposed by music engraving rules (i.e. spacing between notes must be proportional to note duration) and, at the same time, must be vertically aligned with notes, rests, and other elements in saves for other instruments. Therefore, for rendering related tasks it is important not having to deal with two representations: the tree for horizontal traversing and the staff objects collection for vertical traversing. Also, We need to take into account not only musical events (notes and rests) but also all other musical symbols that do not take time has to be rendered between two notes and rests, such as clefs or barlines.
Therefore, the main objective of the staff objects collection auxiliary representation is to have an adequate representation for traversing all music objects either:
It should also facilitate:
Once we have a clearer idea of the needs lets develop the details.
To get a clearer picture of the kind of need representation let’s use the score shown in following picture and let’s identify the information needed to satisfy our objectives. I have created this score attending not to its musical content but for illustrating different issues; therefore, it has unusual content, such as two consecutive clef changes:
In LDP this score could be represented as (I have numbered the staff objects for later reference):
(score (vers 1.6)
(instrument
(musicData
(clef G) #0
(key D) #1
(time 2 4) #2
(n d5 h v1 (stem up)) #3
(goBack h)
(n d4 e g+ v2 (stem down)) #4
(n f4 e g- v2 (stem down)) #5
(n a4 q v2 (stem down)) #6
(barline) #7
)
)
(instrument (staves 2)
(musicData
(clef G p1) #8
(clef F4 p2) #9
(key D) #10
(time 2 4) #11
(n f4 q. p1) #12
(clef F4 p1) #13
(n a3 e) #14
(goBack h)
(n c3 q p2) #15
(n c3 e) #16
(clef G p2) #17
(clef F p2) #18
(n c3 e) #19
(barline) #20
)
)
)
Now consider how to transform the LDP tree into the rendered score. Let’s ignore all the details, just think on how to re-order tree nodes to resemble the score layout. First, we realize that the rendered score is similar to a table: each voice is a row and the different vertical alignment lines are the columns of this table:
Let’s create a table, that mimics the score layout:
voice | instr | 0 | 0 | 0 | 0 | 32 | 64 | 96 | 96 | 96 | 128 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | ClefG [#0] | KeyD [#1] | Time [#2] | half [#3] | BL [#7] | |||||
1 | 0 | ClefG [#0] | KeyD [#1] | Time [#2] | 8th [#4] | 8th [#5] | quarter [#6] | BL [#7] | |||
2 | 1 | ClefG [#8] | KeyD [#10] | Time [#11] | quarter_dot [#12] | ClefF [#13] | 8th [#14] | BL [#20] | |||
3 | 1 | ClefF [#9] | KeyD [#10] | Time [#11] | quarter [#15] | 8th [#16] | ClefG [#17] | ClefF [#18] | 8th [#19] | BL [#20] |
The first thing we observe is the need to duplicate certain objects (i.e. keys, clefs, time signatures, barlines) as the object is defined in LDP source code only once but it applies to more than one voice.
A table like this one satisfies our objectives:
A table like the one presented is the base for Lomse ‘staff objects collection’ table, but some changes are needed:
Taking these issues into account, the only table that we can build without using engraving knowledge is a table with a column per time value, such as the following one:
line | staff | instr | time 0 | time 32 | time 64 | time 96 | time 128 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | Clef-G-[#0] Key-D-[#1] Time-[#2] half-note-[#3] | BL [#7] | |||
1 | 0 | 0 | Clef-G-[#0] KeyD-[#1] Time-[#2] 8th-note-[#4] | 8th [#5] | quarter [#6] | BL [#7] | |
2 | 1 | 1 | Clef-G-[#8] Key-D-[#10] Time-[#11] quarter_dot [#12] | 8th [#5] | quarter [#6] | ClefF [#13] 8th [#14] | BL [#20] |
3 | 2 | 1 | Clef-F-[#9] Key-D-[#10] Time-[#11] quarter-[#15] | 8th [#16] | ClefG [#17] ClefF [#18] 8th [#19] | BL [#20] |
And this table is the basis for the staff objects collection table. In next sections we will see the algorithm for generating the table and how the table is implemented.
The starting point is the internal model tree, obtained when LDP code is compiled. To get the table, the algorithm is simple. It is just three steps:
Done. The resulting table satisfies our initial objectives.
Let’s see how this algorithm works with the score of our example. In the following picture I show the internal tree (simplified) for our example. I have labeled nodes with the number we assigned to each element in the LDP source code:
score -+- instr0 -+- #0 (clef G)
| +- #1 (key D)
| +- #2 (time 2 4)
| +- #3 (n d5 h v1)
| +- (goBack h)
| +- #4 (n d4 e g+ v2)
| +- #5 (n f4 e g- v2)
| +- #6 (n a4 q v2)
| +- #7 (barline)
|
+- instr1 -+- #8 (clef G p1)
+- #9 (clef F4 p2)
+- #10 (key D)
+- #11 (time 2 4)
+- #12 (n f4 q. p1)
+- #13 (clef F4 p1)
+- #14 (n a3 e)
+- (goBack h)
+- #15 (n c3 q p2)
+- #16 (n c3 e)
+- #17 (clef G p2)
+- #18 (clef F p2)
+- #19 (n c3 e)
+- #20 (barline)
To convert the tree into the table, we start traversing the tree. First, we set a counter to keep current time. whenever we start traversing a new instrument, the time counter is reset to zero. goFwd and goBack notes, also increments/decrements the time counter. Notes and rests increment the counter by note/rest duration. All other nodes do not change the counter value. As we traverse the tree, time is assigned to each node (just take current time counter value).
Also, while traversing nodes, the line or lines to assign to the node are computed (remember that some objects, such as key signatures, need to be duplicated in several lines). The algorithm for assigning lines is described later. Finally, once all the information for a node is collected, as many rows as assigned lines for the node are created in the table. To illustrate the process, first I have added the collected information to each node:
score -+- instr0 -+- #0 (clef G) bar0, t0, line0, staff0
| | bar0, t0, line1, staff0
| +- #1 (key D) bar0, t0, line0, staff0
| | bar0, t0, line1, staff0
| +- #2 (time 2 4) bar0, t0, line0, staff0
| | bar0, t0, line1, staff0
| +- #3 (n d5 h v1) bar0, t0, line0, staff0 t=t+128
| +- (goBack h) t=t-128
| +- #4 (n d4 e g+ v2) bar0, t0, line1, staff0 t=t+32
| +- #5 (n f4 e g- v2) bar0, t32, line1, staff0 t=t+32
| +- #6 (n a4 q v2) bar0, t64, line1, staff0 t=t+64
| +- #7 (barline) bar0, t128, line0, staff0
| bar0, t128, line1, staff0
|
+- instr1 -+- #8 (clef G p1) bar0, t0, line2, staff0
+- #9 (clef F4 p2) bar0, t0, line3, staff1
+- #10 (key D) bar0, t0, line2, staff0
| bar0, t0, line3, staff1
+- #11 (time 2 4) bar0, t0, line2, staff0
+- #11 (time 2 4) bar0, t0, line3, staff1
+- #12 (n f4 q. p1) bar0, t0, line2, staff0 t=t+96
+- #13 (clef F4 p1) bar0, t96, line2, staff0
+- #14 (n a3 e) bar0, t96, line2, staff0 t=t+32
+- (goBack h) t=t-128
+- #15 (n c3 q p2) bar0, t0, line3, staff1 t=t+64
+- #16 (n c3 e) bar0, t64, line3, staff1 t=t+32
+- #17 (clef G p2) bar0, t96, line3, staff1
+- #18 (clef F p2) bar0, t96, line3, staff1
+- #19 (n c3 e) bar0, t96, line3, staff1 t=t+32
+- #20 (barline) bar0, t128, line2, staff0
bar0, t128, line3, staff1
And now transform the tree into a table, duplicating those nodes that apply to several lines, and removing nodes goBack/goFwd:
instr0 #0 (clef G) bar0 t0 line0 staff0
instr0 #0 (clef G) bar0 t0 line1 staff0
instr0 #1 (key D) bar0 t0 line0 staff0
instr0 #1 (key D) bar0 t0 line1 staff0
instr0 #2 (time 2 4) bar0 t0 line0 staff0
instr0 #2 (time 2 4) bar0 t0 line1 staff0
instr0 #3 (n d5 h v1) bar0 t0 line0 staff0
instr0 #4 (n d4 e g+ v2) bar0 t0 line1 staff0
instr0 #5 (n f4 e g- v2) bar0 t32 line1 staff0
instr0 #6 (n a4 q v2) bar0 t64 line1 staff0
instr0 #7 (barline) bar0 t128 line0 staff0
instr0 #7 (barline) bar0 t128 line1 staff0
instr1 #8 (clef G p1) bar0 t0 line2 staff0
instr1 #9 (clef F4 p2) bar0 t0 line3 staff1
instr1 #10 (key D) bar0 t0 line2 staff0
instr1 #10 (key D) bar0 t0 line3 staff1
instr1 #11 (time 2 4) bar0 t0 line2 staff0
instr1 #11 (time 2 4) bar0 t0 line3 staff1
instr1 #12 (n f4 q. p1) bar0 t0 line2 staff0
instr1 #13 (clef F4 p1) bar0 t96 line2 staff0
instr1 #14 (n a3 e) bar0 t96 line2 staff0
instr1 #15 (n c3 q p2) bar0 t0 line3 staff1
instr1 #16 (n c3 e) bar0 t64 line3 staff1
instr1 #17 (clef G p2) bar0 t96 line3 staff1
instr1 #18 (clef F p2) bar0 t96 line3 staff1
instr1 #19 (n c3 e) bar0 t96 line3 staff1
instr1 #20 (barline) bar0 t128 line2 staff0
instr1 #20 (barline) bar0 t128 line3 staff1
Finally. sort by time, line and staff, but keep precedence (stable sort):
instr0 #0 (clef G) bar0 t0 line0 staff0
instr0 #1 (key D) bar0 t0 line0 staff0
instr0 #2 (time 2 4) bar0 t0 line0 staff0
instr0 #3 (n d5 h v1) bar0 t0 line0 staff0
instr0 #0 (clef G) bar0 t0 line1 staff0
instr0 #1 (key D) bar0 t0 line1 staff0
instr0 #2 (time 2 4) bar0 t0 line1 staff0
instr0 #4 (n d4 e g+ v2) bar0 t0 line1 staff0
instr1 #8 (clef G p1) bar0 t0 line2 staff0
instr1 #10 (key D) bar0 t0 line2 staff0
instr1 #11 (time 2 4) bar0 t0 line2 staff0
instr1 #12 (n f4 q. p1) bar0 t0 line2 staff0
instr1 #9 (clef F4 p2) bar0 t0 line3 staff1
instr1 #10 (key D) bar0 t0 line3 staff1
instr1 #11 (time 2 4) bar0 t0 line3 staff1
instr1 #15 (n c3 q p2) bar0 t0 line3 staff1
instr0 #5 (n f4 e g- v2) bar0 t32 line1 staff0
instr0 #6 (n a4 q v2) bar0 t64 line1 staff0
instr1 #16 (n c3 e) bar0 t64 line3 staff1
instr1 #13 (clef F4 p1) bar0 t96 line2 staff0
instr1 #14 (n a3 e) bar0 t96 line2 staff0
instr1 #17 (clef G p2) bar0 t96 line3 staff1
instr1 #18 (clef F p2) bar0 t96 line3 staff1
instr1 #19 (n c3 e) bar0 t96 line3 staff1
instr0 #7 (barline) bar0 t128 line0 staff0
instr0 #7 (barline) bar0 t128 line1 staff0
instr1 #20 (barline) bar0 t128 line2 staff0
instr1 #20 (barline) bar0 t128 line3 staff1
Done. The obtained table is the staff object collection auxiliary representation.
The staff object collection plays a fundamental role in the layout algorithms. It is fundamental to have staff objects orderer in the same order in which they will be rendered. In practice, keeping the table ordered is one of the key issues; pay attention to this when adding more features to the library. Some ordering issues:
First, let’s see the constraints and invariants:
With this information, the rules defining the algorithm to assign line to a musical symbol are simple:
Class ColStaffObjs represents the table. It encapsulates the staff objects collection for a score. It contains a vector of ColStaffObjsEntry objects. Each object represent an entry in the staff objects collection.
The algorithm to create the table is encapsulated in class ColStaffObjsBuilder.
And the algorithm assign line number to voices/staves is encapsulated in class StaffVoiceLineTable.