clef 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +77 -90
- data/Rakefile +21 -1
- data/exe/clef +21 -0
- data/lib/clef/compiler.rb +107 -4
- data/lib/clef/core/chord.rb +9 -3
- data/lib/clef/core/duration.rb +7 -3
- data/lib/clef/core/key_signature.rb +43 -36
- data/lib/clef/core/measure.rb +14 -10
- data/lib/clef/core/metadata.rb +52 -0
- data/lib/clef/core/note.rb +50 -4
- data/lib/clef/core/pitch.rb +73 -4
- data/lib/clef/core/rest.rb +11 -3
- data/lib/clef/core/score.rb +148 -9
- data/lib/clef/core/staff.rb +13 -3
- data/lib/clef/core/staff_group.rb +8 -2
- data/lib/clef/core/tempo.rb +5 -0
- data/lib/clef/core/tuplet.rb +48 -0
- data/lib/clef/core/validation.rb +39 -0
- data/lib/clef/core/voice.rb +21 -5
- data/lib/clef/engraving/font_manager.rb +1 -1
- data/lib/clef/engraving/glyph_table.rb +18 -3
- data/lib/clef/engraving/style.rb +41 -2
- data/lib/clef/ir/moment.rb +2 -2
- data/lib/clef/ir/music_tree.rb +2 -2
- data/lib/clef/ir/timeline.rb +25 -5
- data/lib/clef/layout/beam_layout.rb +2 -2
- data/lib/clef/layout/item.rb +26 -0
- data/lib/clef/layout/spacing.rb +6 -4
- data/lib/clef/layout/stem.rb +10 -6
- data/lib/clef/layout/system_layout.rb +71 -0
- data/lib/clef/midi/channel_map.rb +5 -2
- data/lib/clef/midi/exporter.rb +316 -38
- data/lib/clef/notation/dynamic.rb +5 -0
- data/lib/clef/notation/lyric.rb +33 -1
- data/lib/clef/parser/dsl.rb +249 -58
- data/lib/clef/parser/lilypond_lexer.rb +43 -3
- data/lib/clef/parser/lilypond_parser.rb +231 -17
- data/lib/clef/plugins/base.rb +24 -4
- data/lib/clef/plugins/registry.rb +80 -10
- data/lib/clef/renderer/base.rb +2 -2
- data/lib/clef/renderer/drawing_context.rb +26 -0
- data/lib/clef/renderer/notation_helpers.rb +92 -1
- data/lib/clef/renderer/pdf_renderer.rb +487 -82
- data/lib/clef/renderer/svg_renderer.rb +510 -97
- data/lib/clef/version.rb +1 -1
- data/lib/clef.rb +60 -7
- data/sig/clef.rbs +292 -0
- metadata +14 -5
|
@@ -21,7 +21,7 @@ module Clef
|
|
|
21
21
|
def register_with(pdf, family_name: "Bravura")
|
|
22
22
|
return "Helvetica" unless font_available?
|
|
23
23
|
|
|
24
|
-
pdf.font_families.update(family_name => {
|
|
24
|
+
pdf.font_families.update(family_name => {normal: font_path})
|
|
25
25
|
family_name
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -30,22 +30,37 @@ module Clef
|
|
|
30
30
|
fermata: "\uE4C0"
|
|
31
31
|
}.merge((0..9).to_h { |n| [:"time_#{n}", (0xE080 + n).chr(Encoding::UTF_8)] }).freeze
|
|
32
32
|
|
|
33
|
+
# @param glyphs [Hash]
|
|
34
|
+
def initialize(glyphs: GLYPHS)
|
|
35
|
+
@glyphs = glyphs.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
# @param name [Symbol]
|
|
34
39
|
# @return [String]
|
|
35
40
|
def fetch(name)
|
|
36
|
-
|
|
41
|
+
@glyphs.fetch(name)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# @param name [Symbol]
|
|
40
45
|
# @return [String, nil]
|
|
41
46
|
def [](name)
|
|
42
|
-
|
|
47
|
+
@glyphs[name]
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# @param name [Symbol]
|
|
46
51
|
# @return [Boolean]
|
|
47
52
|
def key?(name)
|
|
48
|
-
|
|
53
|
+
@glyphs.key?(name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param name [Symbol]
|
|
57
|
+
# @param glyph [String]
|
|
58
|
+
# @return [String]
|
|
59
|
+
def register(name, glyph)
|
|
60
|
+
raise ArgumentError, "glyph name must be a Symbol" unless name.is_a?(Symbol)
|
|
61
|
+
raise ArgumentError, "glyph must be a String" unless glyph.is_a?(String)
|
|
62
|
+
|
|
63
|
+
@glyphs[name] = glyph
|
|
49
64
|
end
|
|
50
65
|
end
|
|
51
66
|
end
|
data/lib/clef/engraving/style.rb
CHANGED
|
@@ -3,25 +3,64 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Engraving
|
|
5
5
|
class Style
|
|
6
|
-
attr_reader :page_size, :margin, :staff_size, :staff_space, :min_note_spacing
|
|
6
|
+
attr_reader :page_size, :margin, :staff_size, :staff_space, :min_note_spacing,
|
|
7
|
+
:line_width, :system_gap, :staff_gap, :measure_padding,
|
|
8
|
+
:notehead_width, :beam_thickness
|
|
7
9
|
|
|
8
10
|
# @param page_size [String, Array]
|
|
9
11
|
# @param margin [Numeric]
|
|
10
12
|
# @param staff_size [Numeric]
|
|
11
13
|
# @param staff_space [Numeric]
|
|
12
14
|
# @param min_note_spacing [Numeric]
|
|
13
|
-
|
|
15
|
+
# @param line_width [Numeric]
|
|
16
|
+
# @param system_gap [Numeric]
|
|
17
|
+
# @param staff_gap [Numeric]
|
|
18
|
+
# @param measure_padding [Numeric]
|
|
19
|
+
# @param notehead_width [Numeric]
|
|
20
|
+
# @param beam_thickness [Numeric]
|
|
21
|
+
def initialize(page_size: "A4", margin: 36, staff_size: 20, staff_space: 10, min_note_spacing: nil,
|
|
22
|
+
line_width: 820, system_gap: 72, staff_gap: 90, measure_padding: 14,
|
|
23
|
+
notehead_width: 7, beam_thickness: nil)
|
|
24
|
+
min_note_spacing ||= Rules::MIN_NOTE_SPACING * staff_space
|
|
25
|
+
beam_thickness ||= Rules::BEAM_THICKNESS * staff_space
|
|
26
|
+
|
|
27
|
+
validate_positive!(
|
|
28
|
+
margin: margin,
|
|
29
|
+
staff_size: staff_size,
|
|
30
|
+
staff_space: staff_space,
|
|
31
|
+
min_note_spacing: min_note_spacing,
|
|
32
|
+
line_width: line_width,
|
|
33
|
+
system_gap: system_gap,
|
|
34
|
+
staff_gap: staff_gap,
|
|
35
|
+
measure_padding: measure_padding,
|
|
36
|
+
notehead_width: notehead_width,
|
|
37
|
+
beam_thickness: beam_thickness
|
|
38
|
+
)
|
|
39
|
+
|
|
14
40
|
@page_size = page_size
|
|
15
41
|
@margin = margin
|
|
16
42
|
@staff_size = staff_size
|
|
17
43
|
@staff_space = staff_space
|
|
18
44
|
@min_note_spacing = min_note_spacing
|
|
45
|
+
@line_width = line_width
|
|
46
|
+
@system_gap = system_gap
|
|
47
|
+
@staff_gap = staff_gap
|
|
48
|
+
@measure_padding = measure_padding
|
|
49
|
+
@notehead_width = notehead_width
|
|
50
|
+
@beam_thickness = beam_thickness
|
|
19
51
|
end
|
|
20
52
|
|
|
21
53
|
# @return [Style]
|
|
22
54
|
def self.default
|
|
23
55
|
new
|
|
24
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_positive!(values)
|
|
61
|
+
invalid = values.filter_map { |name, value| name unless value.is_a?(Numeric) && value.positive? }
|
|
62
|
+
raise ArgumentError, "style values must be positive: #{invalid.join(", ")}" unless invalid.empty?
|
|
63
|
+
end
|
|
25
64
|
end
|
|
26
65
|
end
|
|
27
66
|
end
|
data/lib/clef/ir/moment.rb
CHANGED
|
@@ -16,8 +16,8 @@ module Clef
|
|
|
16
16
|
|
|
17
17
|
# @param duration [Rational, #length]
|
|
18
18
|
# @return [Moment]
|
|
19
|
-
def +(
|
|
20
|
-
self.class.new(value + normalize_duration(
|
|
19
|
+
def +(other)
|
|
20
|
+
self.class.new(value + normalize_duration(other))
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @param other [Moment, Rational, Integer]
|
data/lib/clef/ir/music_tree.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Clef
|
|
|
19
19
|
staff.measures.each do |measure|
|
|
20
20
|
append_measure_metadata(timeline, measure, staff, current)
|
|
21
21
|
append_measure_voices(timeline, measure, staff, current)
|
|
22
|
-
current
|
|
22
|
+
current += measure_length_for(measure)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -39,7 +39,7 @@ module Clef
|
|
|
39
39
|
cursor = Moment.new(start_moment.value)
|
|
40
40
|
voice.elements.each do |element|
|
|
41
41
|
timeline.add(Event.new(moment: cursor, element: element, staff_id: staff_id, voice_id: voice_id))
|
|
42
|
-
cursor
|
|
42
|
+
cursor += element.length
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
data/lib/clef/ir/timeline.rb
CHANGED
|
@@ -17,6 +17,11 @@ module Clef
|
|
|
17
17
|
raise ArgumentError, "event must be a Clef::Ir::Event" unless event.is_a?(Event)
|
|
18
18
|
|
|
19
19
|
events << event
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Timeline]
|
|
24
|
+
def sort!
|
|
20
25
|
events.sort_by! { |item| item.moment.value }
|
|
21
26
|
self
|
|
22
27
|
end
|
|
@@ -25,7 +30,7 @@ module Clef
|
|
|
25
30
|
# @return [Array<Event>]
|
|
26
31
|
def events_at(moment)
|
|
27
32
|
target = moment_value(moment)
|
|
28
|
-
|
|
33
|
+
sorted_events.select { |event| event.moment.value == target }
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
# @param from [Moment, Rational, Integer]
|
|
@@ -34,7 +39,16 @@ module Clef
|
|
|
34
39
|
def events_between(from, to)
|
|
35
40
|
lower = moment_value(from)
|
|
36
41
|
upper = moment_value(to)
|
|
37
|
-
|
|
42
|
+
sorted_events.select { |event| (lower..upper).cover?(event.moment.value) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param from [Moment, Rational, Integer]
|
|
46
|
+
# @param to [Moment, Rational, Integer]
|
|
47
|
+
# @return [Array<Event>]
|
|
48
|
+
def events_in_range(from, to)
|
|
49
|
+
lower = moment_value(from)
|
|
50
|
+
upper = moment_value(to)
|
|
51
|
+
sorted_events.select { |event| event.moment.value >= lower && event.moment.value < upper }
|
|
38
52
|
end
|
|
39
53
|
|
|
40
54
|
# @yield [Moment]
|
|
@@ -46,19 +60,25 @@ module Clef
|
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
def each(&block)
|
|
49
|
-
|
|
63
|
+
sorted_events.each(&block)
|
|
50
64
|
end
|
|
51
65
|
|
|
52
66
|
# @return [Rational]
|
|
53
67
|
def shortest_duration
|
|
54
|
-
durations = events.filter_map
|
|
68
|
+
durations = events.filter_map do |event|
|
|
69
|
+
event.element.length if event.element.respond_to?(:length) && event.element.length.positive?
|
|
70
|
+
end
|
|
55
71
|
durations.min || Rational(1, 4)
|
|
56
72
|
end
|
|
57
73
|
|
|
58
74
|
private
|
|
59
75
|
|
|
60
76
|
def unique_moments
|
|
61
|
-
|
|
77
|
+
sorted_events.map(&:moment).uniq
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def sorted_events
|
|
81
|
+
events.sort_by { |item| item.moment.value }
|
|
62
82
|
end
|
|
63
83
|
|
|
64
84
|
def moment_value(value)
|
|
@@ -20,8 +20,8 @@ module Clef
|
|
|
20
20
|
def compute(beam_group, _clef, _spacing)
|
|
21
21
|
first_y = pitch_y(beam_group.first.pitch)
|
|
22
22
|
last_y = pitch_y(beam_group.last.pitch)
|
|
23
|
-
slope =
|
|
24
|
-
{
|
|
23
|
+
slope = ((last_y - first_y) / [beam_group.length - 1, 1].max).clamp(-0.5, 0.5)
|
|
24
|
+
{start_y: first_y, end_y: first_y + slope * (beam_group.length - 1), slope: slope}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Layout
|
|
5
|
+
class Item
|
|
6
|
+
attr_reader :type, :moment, :staff_id, :measure_number, :payload
|
|
7
|
+
|
|
8
|
+
# @param type [Symbol]
|
|
9
|
+
# @param moment [Clef::Ir::Moment]
|
|
10
|
+
# @param staff_id [Symbol, String, nil]
|
|
11
|
+
# @param measure_number [Integer, nil]
|
|
12
|
+
# @param payload [Hash]
|
|
13
|
+
def initialize(type:, moment:, staff_id: nil, measure_number: nil, payload: {})
|
|
14
|
+
raise ArgumentError, "type must be a Symbol" unless type.is_a?(Symbol)
|
|
15
|
+
raise ArgumentError, "moment must be a Clef::Ir::Moment" unless moment.is_a?(Clef::Ir::Moment)
|
|
16
|
+
raise ArgumentError, "payload must be a Hash" unless payload.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
@type = type
|
|
19
|
+
@moment = moment
|
|
20
|
+
@staff_id = staff_id
|
|
21
|
+
@measure_number = measure_number
|
|
22
|
+
@payload = payload.dup
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/clef/layout/spacing.rb
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Layout
|
|
5
5
|
class Spacing
|
|
6
|
-
attr_reader :timeline, :style, :stretch_factor
|
|
6
|
+
attr_reader :timeline, :style, :stretch_factor, :extra_moments
|
|
7
7
|
|
|
8
8
|
# @param timeline [Clef::Ir::Timeline]
|
|
9
9
|
# @param style [Clef::Engraving::Style]
|
|
10
|
-
|
|
10
|
+
# @param extra_moments [Array<Clef::Ir::Moment>]
|
|
11
|
+
def initialize(timeline, style, extra_moments: [])
|
|
11
12
|
@timeline = timeline
|
|
12
13
|
@style = style
|
|
14
|
+
@extra_moments = extra_moments
|
|
13
15
|
@stretch_factor = 1.0
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# @return [Hash{Clef::Ir::Moment=>Float}]
|
|
17
19
|
def compute
|
|
18
|
-
moments = timeline.each_moment.to_a.sort
|
|
20
|
+
moments = (timeline.each_moment.to_a + extra_moments).uniq.sort
|
|
19
21
|
return {} if moments.empty?
|
|
20
22
|
|
|
21
23
|
build_positions(moments)
|
|
@@ -36,7 +38,7 @@ module Clef
|
|
|
36
38
|
private
|
|
37
39
|
|
|
38
40
|
def build_positions(moments)
|
|
39
|
-
positions = {
|
|
41
|
+
positions = {moments.first => 0.0}
|
|
40
42
|
total = 0.0
|
|
41
43
|
moments.each_cons(2) do |left, right|
|
|
42
44
|
total += interval_width(right.value - left.value)
|
data/lib/clef/layout/stem.rb
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Layout
|
|
5
5
|
class Stem
|
|
6
|
-
B4_MIDI = 71
|
|
7
|
-
|
|
8
6
|
class << self
|
|
9
7
|
# @param note_or_notes [Clef::Core::Note, Array<Clef::Core::Note>]
|
|
10
|
-
# @param
|
|
8
|
+
# @param clef [Clef::Core::Clef]
|
|
11
9
|
# @return [Symbol]
|
|
12
|
-
def direction(note_or_notes,
|
|
10
|
+
def direction(note_or_notes, clef)
|
|
13
11
|
notes = Array(note_or_notes)
|
|
14
|
-
|
|
15
|
-
down_votes
|
|
12
|
+
middle = diatonic_step(clef.reference_pitch)
|
|
13
|
+
down_votes = notes.count { |note| diatonic_step(note.pitch) > middle }
|
|
14
|
+
(down_votes > (notes.length / 2.0)) ? :down : :up
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
# @param note [Clef::Core::Note]
|
|
@@ -26,6 +25,11 @@ module Clef
|
|
|
26
25
|
|
|
27
26
|
private
|
|
28
27
|
|
|
28
|
+
def diatonic_step(pitch)
|
|
29
|
+
note_index = Clef::Core::Pitch::VALID_NOTE_NAMES.index(pitch.note_name)
|
|
30
|
+
(pitch.octave * 7) + note_index
|
|
31
|
+
end
|
|
32
|
+
|
|
29
33
|
def ledger_extension(pitch)
|
|
30
34
|
return 1.5 if pitch.to_midi > 84
|
|
31
35
|
return 1.5 if pitch.to_midi < 48
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Layout
|
|
5
|
+
class SystemLayout
|
|
6
|
+
System = Struct.new(:page_index, :line_index, :line_top, :start_moment, :end_moment,
|
|
7
|
+
:position_offset, :x_origin, :y_origin, :staff_offsets, keyword_init: true) do
|
|
8
|
+
def staff_offset(staff_id)
|
|
9
|
+
staff_offsets.fetch(staff_id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def staff_origin(staff_id)
|
|
13
|
+
[x_origin, y_origin + staff_offset(staff_id)]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def include_moment?(moment)
|
|
17
|
+
value = moment.is_a?(Clef::Ir::Moment) ? moment.value : Rational(moment)
|
|
18
|
+
value.between?(start_moment.value, end_moment.value)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :score, :pages, :positions, :style
|
|
23
|
+
|
|
24
|
+
# @param score [Clef::Core::Score]
|
|
25
|
+
# @param pages [Array<Array<Array<Hash>>>]
|
|
26
|
+
# @param positions [Hash]
|
|
27
|
+
# @param style [Clef::Engraving::Style]
|
|
28
|
+
def initialize(score, pages:, positions:, style:)
|
|
29
|
+
@score = score
|
|
30
|
+
@pages = pages
|
|
31
|
+
@positions = positions
|
|
32
|
+
@style = style
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Array<System>]
|
|
36
|
+
def build
|
|
37
|
+
pages.flat_map.with_index do |page, page_index|
|
|
38
|
+
page.map.with_index do |line, line_index|
|
|
39
|
+
build_system(line, page_index, line_index)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_system(line, page_index, line_index)
|
|
47
|
+
start_moment = line.first.fetch(:moment)
|
|
48
|
+
end_moment = line.last.fetch(:moment)
|
|
49
|
+
System.new(
|
|
50
|
+
page_index: page_index,
|
|
51
|
+
line_index: line_index,
|
|
52
|
+
line_top: line_index * system_height,
|
|
53
|
+
start_moment: start_moment,
|
|
54
|
+
end_moment: end_moment,
|
|
55
|
+
position_offset: positions.fetch(start_moment, 0.0),
|
|
56
|
+
x_origin: -positions.fetch(start_moment, 0.0),
|
|
57
|
+
y_origin: line_index * system_height,
|
|
58
|
+
staff_offsets: staff_offsets
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def staff_offsets
|
|
63
|
+
score.staves.each_with_index.to_h { |staff, index| [staff.id, index * style.staff_gap] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def system_height
|
|
67
|
+
[(score.staves.length - 1), 0].max * style.staff_gap + style.system_gap
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -4,10 +4,13 @@ module Clef
|
|
|
4
4
|
module Midi
|
|
5
5
|
class ChannelMap
|
|
6
6
|
# @param staff_index [Integer]
|
|
7
|
+
# @param percussion [Boolean]
|
|
7
8
|
# @return [Integer]
|
|
8
|
-
def channel_for(staff_index)
|
|
9
|
+
def channel_for(staff_index, percussion: false)
|
|
10
|
+
return 9 if percussion
|
|
11
|
+
|
|
9
12
|
base = staff_index % 15
|
|
10
|
-
base >= 9 ? base + 1 : base
|
|
13
|
+
(base >= 9) ? base + 1 : base
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
end
|