Act 2
Filetype Agnostic Music Rendering?
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.
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.
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.
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
.
SvgGroups
are summarized in the following procedure:
SvgGroups
of all inner elements of a Score
(where all the work is done).SvgGroups
.SvgGroup
.SvgDocument
.SvgGroups
and sub groups are visualized in the picture below:
// 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:
Filetype Agnostic Music Rendering?