OMR Act 1 - Writing a Music Renderer from Scratch

At first, let's develop a library named notensatz to render existing sheet music. As opposed to LilyPond, this library should not determine where to place notes, whether a note stem should be drawn upwards or downwards or which notes should be connected via beams. Instead, this library expects a full description of rendered music. It should offer an API to create such descriptions programmatically, parse digitized music in the form of MusicXML as well as PDFs or pictures of engraved music.

Symbols for music notation mostly consist of
A more or less complete list of (western) musical symbols can be found on Wikipedia or the MusicXML Reference. Due to the simplicity of the symbols and with PDF as the de-facto standard of exchanging music engravings, a renderer should produce something that PDF understands and is not bound to a pre-defined size (e.g. what a bitmap would be).
Therefore, rendering mathematical descriptions of a score in form of an SVG seems like a reasonable approach! The fundamental libraries or resources we will build upon are the following: The core of the library i.e. how the music is represented in classes is predestined for a programming language such as Rust. All the information is kept at one place and organized in many Structs and most importantly, Enums. The general structure is heavily inspired (but shortened) by MusicXML with some variations here and there.
For example, a Score is a collection of Measures. Each Measure contains TimedChords which are Chords that are played at the same time. Each Chord is represented as collection of Notes as well as information about Beams.
#[derive(Clone, Debug)]
pub struct Score {
    measures: Vec<Measure>,
    // more here ...
}

#[derive(Clone, Debug, Default)]
pub struct Measure {
    pub timed_chords: Vec<TimedChords>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct TimedChords {
    pub chords: Vec<Chord>,
    // more here ...
}

#[derive(Clone, Debug, PartialEq)]
pub struct Chord {
    notes: Vec<Note>,
    beams: Vec<Beam>,
}
A Note inherits the most important information and can be either a PitchedNote, a Grace or a Rest. For a more detailed inspection, we'll just look at the most important type: The PitchedNote.
#[derive(Clone, Debug, PartialEq)]
pub enum Note {
    PitchedNote(PitchedNote),
    Grace(Grace),
    Rest(Rest),
}

#[derive(Clone, Debug, PartialEq)]
pub struct PitchedNote {
    pitch: Pitch,
    note_type: NoteType,
    dot: Option<Dot>,
    stem: Option<Stem>,
    accidental: Option<Accidental>,
    staff: Option<usize>,
}
Each PitchedNote has to have a Pitch and a NoteType and may have optional attributes such as whether the note is dotted, a stem is present or any of the possible accidentals should be drawn. Each of these attributes are represented by Enums taking full advantage of the strong typing system rust offers.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pitch {
    step: Step,
    octave: i32,
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
#[repr(u32)]
pub enum Step {
    C = 0,
    D = 1,
    E = 2,
    F = 3,
    G = 4,
    A = 5,
    B = 6,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Stem {
    Up,
    Down,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Accidental {
    Natural,
    Flat,
    DoubleFlat,
    Sharp,
    DoubleSharp,
}
With these building blocks, a first rendering engine can be developed.

The way we render a score (i.e. creating an SVG) is based on a hierarchical tree of SvgGroups. Each group has a semantic meaning. For example there will be a group that represents a Measure, a TimedChord, a Chord or a Note. These groups will be translated based on semantics of these groups. For example, a Measure based on where it is located in the Score, or a Note based on where it is located in a Chord.
One fundamental requirement is to render scores with varying document widths, so one has to account for that. The steps to create this hierarchical tree of SvgGroups are summarized in the following procedure:
  1. Create SvgGroups of all inner elements of a Score (where all the work is done).
  2. Distribute them to rows based on the width of the SvgGroups.
  3. Calculate a perfect padding, so each measure has the same width.
  4. Apply this padding by translating each SvgGroup.
  5. Translate measures.
  6. Collect everything in a root SvgDocument.
The hierarchy and translation of the individual SvgGroups and sub groups are visualized in the picture below:
The procedure of calculating the padding and connecting the svg groups is shown in the following pseudo rust code:
// Pseudo Code
impl Score {
    pub fn draw_svg(&self, score_max_width: f64) -> SvgDocument {
        let mut groups_measure = Vec::new();
        // Collect all rendered elements (SvgGroups) per measure
        for measure in &self.measures {
            let rendered_chords: Vec<SvgGroup> = measure
                .timed_chords                           // For each TimedChord in measure...
                .iter()
                .map(|timed_chord| timed_chord.draw())  // draw SvgGroup (where all the work is done)
                .collect();                             // and collect them
            groups_measure.push(rendered_chords);
        }

        // At this point, all the lowest instances have been drawn
          
        // 1. Calculate which measures still fit and distribute to rows
        let rows_of_measures = distribute_to_rows(min_padding);

        // 2. Based on results, iterate through "rows"
        let mut group_inner = SvgGroup::new();
        rows_of_measures
            .iter()
            .for_each(|measures_row| {
                // 3. For each row, calculate the perfect padding
                let width_min_row = measures_row.iter()
                    .map(|measure_groups| {
                        measure_groups
                            .iter()
                            .map(|group| group.width() + min_padding)
                            .sum::<f64>()
                    })
                    .sum::<f64>();
                  let elements_count = measures_row
                    .iter()
                    .map(|results_measure| results_measure.iter().count())
                    .sum::<usize>();
                let padding_x = (score_max_width - width_min_row) / elements_count;

                // 4. Apply padding while drawing groups
                let mut group_measure = SvgGroup::new();
                measures_row.iter().for_each(|measure_groups| {
                    let mut width_measure = 0.0;
                    for group in measure_groups {
                      width_measure += padding_x;
                      group_measure.append(group.translate(width_measure, 0.0));
                group_inner.append(group_measure);
                
                // Draw staff lines
                group_inner.append(measure_groups.draw_staff_lines());

        // Collect them all in a document and return it
        SvgDocument::new().
            .set("width", score_max_width + 3.0 * space)
            .append(group_inner)
    }
}
With this procedure, our renderer is able to produce an output that maintains a minimum padding between notes and distributes measures to different rows where needed, as the following GIF demonstrates:

Act 2

Filetype Agnostic Music Rendering?