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
|
@@ -8,38 +8,55 @@ module Clef
|
|
|
8
8
|
include NotationHelpers
|
|
9
9
|
|
|
10
10
|
LEFT_PADDING = 80
|
|
11
|
-
TOP_PADDING =
|
|
11
|
+
TOP_PADDING = 56
|
|
12
|
+
STAFF_START_X = 160
|
|
12
13
|
|
|
13
14
|
# @param score [Clef::Core::Score]
|
|
14
15
|
# @param path [String]
|
|
15
16
|
# @param positions [Hash]
|
|
16
|
-
|
|
17
|
+
# @param layout [Hash, nil]
|
|
18
|
+
def render(score, path, positions: nil, layout: nil, **_options)
|
|
19
|
+
return render_to_io(score, path, positions: positions, layout: layout) if path.respond_to?(:write)
|
|
20
|
+
|
|
21
|
+
ensure_parent_directory!(path)
|
|
17
22
|
Prawn::Document.generate(path, page_size: style.page_size, margin: style.margin) do |pdf|
|
|
18
23
|
prepare_canvas(pdf)
|
|
19
|
-
draw_score(pdf, score, positions)
|
|
24
|
+
draw_score(pdf, score, positions, layout: layout)
|
|
20
25
|
end
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
# @param pdf [Prawn::Document]
|
|
24
29
|
# @param score [Clef::Core::Score]
|
|
25
|
-
# @param
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
# @param positions [Hash, nil]
|
|
31
|
+
# @param layout [Hash, nil]
|
|
32
|
+
def draw_score(pdf, score, positions, layout: nil)
|
|
33
|
+
return draw_systems(pdf, score, layout) if layout&.dig(:systems)&.any?
|
|
34
|
+
|
|
35
|
+
draw_header(pdf, score)
|
|
36
|
+
staff_index = 0
|
|
37
|
+
score.staff_groups.each do |group|
|
|
38
|
+
group_start = pdf.cursor - TOP_PADDING - (staff_index * style.staff_gap)
|
|
39
|
+
group.staves.each do |staff|
|
|
40
|
+
baseline = pdf.cursor - TOP_PADDING - (staff_index * style.staff_gap)
|
|
41
|
+
draw_staff(pdf, staff, baseline, positions: positions, layout: layout)
|
|
42
|
+
staff_index += 1
|
|
43
|
+
end
|
|
44
|
+
draw_staff_group(pdf, group, group_start, staff_index - 1) if group.staves.length > 1
|
|
30
45
|
end
|
|
31
46
|
end
|
|
32
47
|
|
|
33
48
|
# @param pdf [Prawn::Document]
|
|
34
49
|
# @param staff [Clef::Core::Staff]
|
|
35
50
|
# @param baseline [Float]
|
|
36
|
-
|
|
51
|
+
# @param positions [Hash, nil]
|
|
52
|
+
# @param layout [Hash, nil]
|
|
53
|
+
def draw_staff(pdf, staff, baseline, positions: nil, layout: nil)
|
|
37
54
|
draw_staff_lines(pdf, baseline)
|
|
38
55
|
cursor = draw_clef(pdf, staff.clef, LEFT_PADDING - 24, baseline)
|
|
39
|
-
cursor
|
|
40
|
-
cursor =
|
|
41
|
-
cursor
|
|
42
|
-
|
|
56
|
+
cursor = draw_key_signature(pdf, staff.key_signature, cursor + style.staff_space, baseline)
|
|
57
|
+
cursor = draw_time_signature(pdf, staff.time_signature, cursor + style.staff_space, baseline)
|
|
58
|
+
note_points = draw_measures(pdf, staff, [cursor + style.measure_padding, STAFF_START_X].max, baseline, positions, layout)
|
|
59
|
+
draw_lyrics(pdf, staff, note_points, baseline)
|
|
43
60
|
end
|
|
44
61
|
|
|
45
62
|
# @param pdf [Prawn::Document]
|
|
@@ -57,6 +74,7 @@ module Clef
|
|
|
57
74
|
# @param baseline [Float]
|
|
58
75
|
def draw_clef(pdf, clef, x, baseline)
|
|
59
76
|
glyph = smufl_enabled? ? glyph_table[:"clef_#{clef.type}"] : fallback_clef_text(clef)
|
|
77
|
+
glyph ||= fallback_clef_text(clef)
|
|
60
78
|
size = smufl_enabled? ? 16 : 10
|
|
61
79
|
y = smufl_enabled? ? (baseline - style.staff_space) : (baseline + (style.staff_space * 1.5))
|
|
62
80
|
|
|
@@ -69,43 +87,62 @@ module Clef
|
|
|
69
87
|
# @param x [Float]
|
|
70
88
|
# @param baseline [Float]
|
|
71
89
|
def draw_metadata(pdf, staff, x, baseline)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
size = smufl_enabled? ? 11 : 9
|
|
76
|
-
y = smufl_enabled? ? (baseline - style.staff_space) : (baseline + (style.staff_space * 1.5))
|
|
77
|
-
pdf.text_box(text, at: [x, y], size: size)
|
|
78
|
-
x + text_width(pdf, text, size: size)
|
|
90
|
+
cursor = draw_key_signature(pdf, staff.key_signature, x, baseline)
|
|
91
|
+
draw_time_signature(pdf, staff.time_signature, cursor + style.staff_space, baseline)
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
# @param pdf [Prawn::Document]
|
|
82
95
|
# @param staff [Clef::Core::Staff]
|
|
83
96
|
# @param start_x [Float]
|
|
84
97
|
# @param baseline [Float]
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
# @param positions [Hash, nil]
|
|
99
|
+
# @param layout [Hash, nil]
|
|
100
|
+
def draw_measures(pdf, staff, start_x, baseline, positions = nil, layout = nil, system: nil)
|
|
101
|
+
note_points = {}
|
|
102
|
+
voice_elements = Hash.new { |hash, key| hash[key] = [] }
|
|
103
|
+
measure_start = Clef::Ir::Moment.new(0)
|
|
87
104
|
staff.measures.each do |measure|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
measure_points, measure_voice_elements = draw_measure(pdf, measure, staff, start_x, baseline, measure_start, positions, layout, system: system)
|
|
106
|
+
note_points.merge!(measure_points)
|
|
107
|
+
measure_voice_elements.each { |voice_id, elements| voice_elements[voice_id].concat(elements) }
|
|
108
|
+
bar_moment = measure_start + measure_length_for(measure)
|
|
109
|
+
if system.nil? || system.include_moment?(bar_moment)
|
|
110
|
+
bar_x = x_for_moment(positions, bar_moment, start_x, position_offset: system&.position_offset)
|
|
111
|
+
draw_barline(pdf, bar_x, baseline)
|
|
112
|
+
end
|
|
113
|
+
measure_start += measure_length_for(measure)
|
|
91
114
|
end
|
|
115
|
+
voice_elements.each_value { |elements| draw_note_connections(pdf, elements, note_points) }
|
|
116
|
+
note_points
|
|
92
117
|
end
|
|
93
118
|
|
|
94
119
|
# @param pdf [Prawn::Document]
|
|
95
120
|
# @param measure [Clef::Core::Measure]
|
|
96
|
-
# @param
|
|
97
|
-
# @param
|
|
121
|
+
# @param staff [Clef::Core::Staff]
|
|
122
|
+
# @param start_x [Float]
|
|
98
123
|
# @param baseline [Float]
|
|
99
|
-
# @
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
# @param measure_start [Clef::Ir::Moment]
|
|
125
|
+
# @param positions [Hash, nil]
|
|
126
|
+
# @param layout [Hash, nil]
|
|
127
|
+
# @return [Hash]
|
|
128
|
+
def draw_measure(pdf, measure, staff, start_x, baseline, measure_start = Clef::Ir::Moment.new(0), positions = nil, layout = nil, system: nil)
|
|
129
|
+
note_points = {}
|
|
130
|
+
voice_elements = {}
|
|
131
|
+
accidental_state = {}
|
|
132
|
+
measure.voices.each_with_index do |(voice_id, voice), index|
|
|
133
|
+
voice_elements[voice_id] = voice.elements
|
|
134
|
+
cursor = Clef::Ir::Moment.new(measure_start.value)
|
|
135
|
+
voice_baseline = baseline + voice_vertical_offset(index)
|
|
136
|
+
voice.elements.each do |element|
|
|
137
|
+
if system.nil? || system.include_moment?(cursor)
|
|
138
|
+
x = x_for_moment(positions, cursor, start_x, position_offset: system&.position_offset)
|
|
139
|
+
draw_element_with_context(pdf, element, x, voice_baseline, staff, measure, accidental_state, note_points)
|
|
140
|
+
end
|
|
141
|
+
cursor += element.length
|
|
142
|
+
end
|
|
143
|
+
draw_beams(pdf, layout, staff, measure, voice_id, note_points)
|
|
107
144
|
end
|
|
108
|
-
|
|
145
|
+
[note_points, voice_elements]
|
|
109
146
|
end
|
|
110
147
|
|
|
111
148
|
# @param pdf [Prawn::Document]
|
|
@@ -115,9 +152,12 @@ module Clef
|
|
|
115
152
|
# @param clef [Clef::Core::Clef]
|
|
116
153
|
def draw_element(pdf, element, x, baseline, clef)
|
|
117
154
|
case element
|
|
118
|
-
when Clef::Core::Note then draw_note(pdf, element, x, baseline, clef)
|
|
155
|
+
when Clef::Core::Note then draw_note(pdf, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
119
156
|
when Clef::Core::Rest then draw_rest(pdf, element, x, baseline)
|
|
120
|
-
when Clef::Core::Chord then draw_chord(pdf, element, x, baseline, clef)
|
|
157
|
+
when Clef::Core::Chord then draw_chord(pdf, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
158
|
+
when Clef::Core::Tuplet then draw_tuplet(pdf, element, x, baseline, clef, accidental_state: {}, key_signature: nil)
|
|
159
|
+
when Clef::Notation::Dynamic then draw_dynamic(pdf, element, x, baseline)
|
|
160
|
+
when Clef::Core::Tempo then draw_tempo_change(pdf, element, x, baseline)
|
|
121
161
|
end
|
|
122
162
|
end
|
|
123
163
|
|
|
@@ -126,11 +166,13 @@ module Clef
|
|
|
126
166
|
# @param x [Float]
|
|
127
167
|
# @param baseline [Float]
|
|
128
168
|
# @param clef [Clef::Core::Clef]
|
|
129
|
-
def draw_note(pdf, note, x, baseline, clef)
|
|
169
|
+
def draw_note(pdf, note, x, baseline, clef, accidental_state:, key_signature:)
|
|
130
170
|
y = pitch_to_y(note.pitch, baseline, clef)
|
|
171
|
+
draw_ledger_lines(pdf, x, y, baseline)
|
|
131
172
|
draw_notehead(pdf, x, y, duration: note.duration)
|
|
132
|
-
draw_accidental(pdf, note.pitch, x, y)
|
|
173
|
+
draw_accidental(pdf, note.pitch, x, y, key_signature, accidental_state)
|
|
133
174
|
draw_stem(pdf, note, x, y, clef) if stem_required?(note.duration)
|
|
175
|
+
draw_flag(pdf, note, x, y, clef) if flag_required?(note.duration)
|
|
134
176
|
draw_dot(pdf, note.duration, x, y)
|
|
135
177
|
draw_articulations(pdf, note.articulations, x, y)
|
|
136
178
|
end
|
|
@@ -140,13 +182,17 @@ module Clef
|
|
|
140
182
|
# @param x [Float]
|
|
141
183
|
# @param baseline [Float]
|
|
142
184
|
def draw_rest(pdf, rest, x, baseline)
|
|
185
|
+
return if rest.kind == :invisible || rest.kind == :spacer
|
|
186
|
+
|
|
143
187
|
glyph = if smufl_enabled?
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
188
|
+
glyph_table[rest_glyph_key(rest.duration)] || glyph_table[:rest_quarter]
|
|
189
|
+
else
|
|
190
|
+
rest_label(rest.duration)
|
|
191
|
+
end
|
|
192
|
+
pdf.text_box(glyph, at: [x, rest_y(rest, baseline)], size: 14)
|
|
193
|
+
return unless rest.kind == :multi_measure && rest.measures > 1
|
|
194
|
+
|
|
195
|
+
pdf.text_box(rest.measures.to_s, at: [x - 2, baseline + style.staff_space], size: 8)
|
|
150
196
|
end
|
|
151
197
|
|
|
152
198
|
# @param pdf [Prawn::Document]
|
|
@@ -154,17 +200,36 @@ module Clef
|
|
|
154
200
|
# @param x [Float]
|
|
155
201
|
# @param baseline [Float]
|
|
156
202
|
# @param clef [Clef::Core::Clef]
|
|
157
|
-
def draw_chord(pdf, chord, x, baseline, clef)
|
|
203
|
+
def draw_chord(pdf, chord, x, baseline, clef, accidental_state:, key_signature:)
|
|
158
204
|
notes = chord_notes(chord)
|
|
205
|
+
offsets = chord_note_offsets(notes)
|
|
159
206
|
ys = []
|
|
160
|
-
notes.
|
|
207
|
+
notes.each_with_index do |note, index|
|
|
161
208
|
y = pitch_to_y(note.pitch, baseline, clef)
|
|
162
209
|
ys << y
|
|
163
|
-
|
|
164
|
-
|
|
210
|
+
note_x = x + offsets[index]
|
|
211
|
+
draw_ledger_lines(pdf, note_x, y, baseline)
|
|
212
|
+
draw_notehead(pdf, note_x, y, duration: chord.duration)
|
|
213
|
+
draw_accidental(pdf, note.pitch, note_x - accidental_offset(index), y, key_signature, accidental_state)
|
|
165
214
|
end
|
|
166
215
|
draw_chord_stem(pdf, notes, x, ys, clef) if stem_required?(chord.duration)
|
|
167
|
-
draw_dot(pdf, chord.duration, x,
|
|
216
|
+
ys.each { |y| draw_dot(pdf, chord.duration, x, y) }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @param pdf [Prawn::Document]
|
|
220
|
+
# @param tuplet [Clef::Core::Tuplet]
|
|
221
|
+
# @param x [Float]
|
|
222
|
+
# @param baseline [Float]
|
|
223
|
+
# @param clef [Clef::Core::Clef]
|
|
224
|
+
def draw_tuplet(pdf, tuplet, x, baseline, clef, accidental_state:, key_signature:, note_points: {})
|
|
225
|
+
cursor = x
|
|
226
|
+
staff_context = Struct.new(:clef, :key_signature).new(clef, key_signature)
|
|
227
|
+
measure_context = Struct.new(:key_signature).new(key_signature)
|
|
228
|
+
tuplet.elements.each do |element|
|
|
229
|
+
draw_element_with_context(pdf, element, cursor, baseline, staff_context, measure_context, accidental_state, note_points)
|
|
230
|
+
cursor += duration_spacing(element) * tuplet.ratio
|
|
231
|
+
end
|
|
232
|
+
pdf.text_box(tuplet.actual.to_s, at: [x + ((cursor - x) / 2.0), baseline + style.staff_space], size: 8)
|
|
168
233
|
end
|
|
169
234
|
|
|
170
235
|
# @param pdf [Prawn::Document]
|
|
@@ -174,11 +239,11 @@ module Clef
|
|
|
174
239
|
def draw_notehead(pdf, x, y, duration:)
|
|
175
240
|
pdf.fill_color("000000")
|
|
176
241
|
if filled_notehead?(duration)
|
|
177
|
-
pdf.circle([x, y],
|
|
242
|
+
pdf.circle([x, y], style.notehead_width / 2.0)
|
|
178
243
|
pdf.fill
|
|
179
244
|
else
|
|
180
245
|
pdf.fill_color("FFFFFF")
|
|
181
|
-
pdf.ellipse([x, y],
|
|
246
|
+
pdf.ellipse([x, y], style.notehead_width / 2.0, 2.5)
|
|
182
247
|
pdf.fill_and_stroke
|
|
183
248
|
pdf.fill_color("000000")
|
|
184
249
|
end
|
|
@@ -192,8 +257,8 @@ module Clef
|
|
|
192
257
|
def draw_stem(pdf, note, x, y, clef)
|
|
193
258
|
direction = Clef::Layout::Stem.direction(note, clef)
|
|
194
259
|
stem_len = style.staff_space * Clef::Layout::Stem.length(note, clef, direction)
|
|
195
|
-
y2 = direction == :up ? y + stem_len : y - stem_len
|
|
196
|
-
stem_x = direction == :up ? x + 3 : x - 3
|
|
260
|
+
y2 = (direction == :up) ? y + stem_len : y - stem_len
|
|
261
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
197
262
|
pdf.stroke_line [stem_x, y], [stem_x, y2]
|
|
198
263
|
end
|
|
199
264
|
|
|
@@ -208,8 +273,8 @@ module Clef
|
|
|
208
273
|
anchor_index = notes.index(anchor_note)
|
|
209
274
|
anchor_y = ys[anchor_index]
|
|
210
275
|
stem_len = style.staff_space * chord_stem_length(notes, clef, direction)
|
|
211
|
-
y2 = direction == :up ? anchor_y + stem_len : anchor_y - stem_len
|
|
212
|
-
stem_x = direction == :up ? x + 3 : x - 3
|
|
276
|
+
y2 = (direction == :up) ? anchor_y + stem_len : anchor_y - stem_len
|
|
277
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
213
278
|
pdf.stroke_line [stem_x, anchor_y], [stem_x, y2]
|
|
214
279
|
end
|
|
215
280
|
|
|
@@ -217,11 +282,14 @@ module Clef
|
|
|
217
282
|
# @param pitch [Clef::Core::Pitch]
|
|
218
283
|
# @param x [Float]
|
|
219
284
|
# @param y [Float]
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
285
|
+
# @param key_signature [Clef::Core::KeySignature, nil]
|
|
286
|
+
# @param state [Hash]
|
|
287
|
+
def draw_accidental(pdf, pitch, x, y, key_signature, state)
|
|
288
|
+
alteration = accidental_for_pitch(pitch, key_signature, state)
|
|
289
|
+
return if alteration.nil?
|
|
290
|
+
|
|
291
|
+
key = accidental_glyph_key(alteration)
|
|
292
|
+
glyph = smufl_enabled? ? glyph_table[key] : accidental_text(alteration)
|
|
225
293
|
pdf.text_box(glyph, at: [x - 12, y + 5], size: 9)
|
|
226
294
|
end
|
|
227
295
|
|
|
@@ -243,10 +311,36 @@ module Clef
|
|
|
243
311
|
# @param x [Float]
|
|
244
312
|
# @param y [Float]
|
|
245
313
|
def draw_articulations(pdf, articulations, x, y)
|
|
246
|
-
|
|
314
|
+
articulations.each do |articulation|
|
|
315
|
+
case articulation
|
|
316
|
+
when :staccato
|
|
317
|
+
pdf.circle([x, y + 12], 1.5)
|
|
318
|
+
pdf.fill
|
|
319
|
+
when :tenuto
|
|
320
|
+
pdf.stroke_line [x - 4, y + 12], [x + 4, y + 12]
|
|
321
|
+
when :accent
|
|
322
|
+
pdf.text_box(">", at: [x - 4, y + 14], size: 9)
|
|
323
|
+
when :marcato
|
|
324
|
+
pdf.stroke_line [x - 4, y + 10], [x, y + 16]
|
|
325
|
+
pdf.stroke_line [x, y + 16], [x + 4, y + 10]
|
|
326
|
+
when :fermata
|
|
327
|
+
draw_fermata(pdf, x, y)
|
|
328
|
+
else
|
|
329
|
+
pdf.text_box(articulation.to_s, at: [x - 4, y + 12], size: 6)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
247
333
|
|
|
248
|
-
|
|
249
|
-
pdf.
|
|
334
|
+
def draw_fermata(pdf, x, y)
|
|
335
|
+
if pdf.respond_to?(:stroke_curve)
|
|
336
|
+
pdf.stroke_curve [x - 7, y + 12], [x + 7, y + 12],
|
|
337
|
+
bounds: [[x - 5, y + 18], [x + 5, y + 18]]
|
|
338
|
+
else
|
|
339
|
+
pdf.stroke_line [x - 7, y + 12], [x, y + 17]
|
|
340
|
+
pdf.stroke_line [x, y + 17], [x + 7, y + 12]
|
|
341
|
+
end
|
|
342
|
+
pdf.circle([x, y + 12], 1.2)
|
|
343
|
+
pdf.fill
|
|
250
344
|
end
|
|
251
345
|
|
|
252
346
|
# @param pdf [Prawn::Document]
|
|
@@ -258,29 +352,243 @@ module Clef
|
|
|
258
352
|
pdf.stroke_line [x, top], [x, bottom]
|
|
259
353
|
end
|
|
260
354
|
|
|
261
|
-
# @param
|
|
262
|
-
# @param
|
|
263
|
-
|
|
355
|
+
# @param pdf [Prawn::Document]
|
|
356
|
+
# @param slurs [Array<Clef::Notation::Slur>]
|
|
357
|
+
# @param note_points [Hash]
|
|
358
|
+
def draw_slurs(pdf, slurs = [], note_points: {})
|
|
359
|
+
slurs.each { |slur| draw_notation_connection(pdf, slur, note_points, lift: 14) }
|
|
360
|
+
end
|
|
264
361
|
|
|
265
|
-
# @param
|
|
266
|
-
# @param
|
|
267
|
-
|
|
362
|
+
# @param pdf [Prawn::Document]
|
|
363
|
+
# @param ties [Array<Clef::Notation::Tie>]
|
|
364
|
+
# @param note_points [Hash]
|
|
365
|
+
def draw_ties(pdf, ties = [], note_points: {})
|
|
366
|
+
ties.each { |tie| draw_notation_connection(pdf, tie, note_points, lift: 8) }
|
|
367
|
+
end
|
|
268
368
|
|
|
269
|
-
|
|
270
|
-
# @param _beams [Array]
|
|
271
|
-
def draw_beams(_pdf, _beams = []); end
|
|
369
|
+
private
|
|
272
370
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
371
|
+
def draw_systems(pdf, score, layout)
|
|
372
|
+
draw_header(pdf, score)
|
|
373
|
+
current_page = 0
|
|
374
|
+
layout[:systems].each_with_index do |system, index|
|
|
375
|
+
if index.positive? && system.page_index != current_page
|
|
376
|
+
pdf.start_new_page
|
|
377
|
+
current_page = system.page_index
|
|
378
|
+
draw_header(pdf, score)
|
|
379
|
+
end
|
|
380
|
+
score.staff_groups.each do |group|
|
|
381
|
+
baselines = group.staves.map { |staff| pdf_system_baseline(pdf, system, staff) }
|
|
382
|
+
group.staves.each do |staff|
|
|
383
|
+
draw_staff_system(pdf, staff, baselines[group.staves.index(staff)], system, layout)
|
|
384
|
+
end
|
|
385
|
+
draw_staff_group_at(pdf, group, baselines.first, baselines.last) if group.staves.length > 1
|
|
386
|
+
end
|
|
387
|
+
draw_layout_items(pdf, layout, system)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
276
390
|
|
|
277
|
-
|
|
391
|
+
def draw_staff_system(pdf, staff, baseline, system, layout)
|
|
392
|
+
draw_staff_lines(pdf, baseline)
|
|
393
|
+
cursor = draw_clef(pdf, staff.clef, LEFT_PADDING - 24, baseline)
|
|
394
|
+
cursor = draw_key_signature(pdf, staff.key_signature, cursor + style.staff_space, baseline)
|
|
395
|
+
cursor = draw_time_signature(pdf, staff.time_signature, cursor + style.staff_space, baseline)
|
|
396
|
+
note_points = draw_measures(pdf, staff, [cursor + style.measure_padding, STAFF_START_X].max,
|
|
397
|
+
baseline, layout[:positions], layout, system: system)
|
|
398
|
+
draw_lyrics(pdf, staff, note_points, baseline)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def pdf_system_baseline(pdf, system, staff)
|
|
402
|
+
pdf.bounds.top - TOP_PADDING - system.line_top - system.staff_offset(staff.id)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def draw_header(pdf, score)
|
|
406
|
+
pdf.text_box(score.title, at: [LEFT_PADDING, pdf.cursor + 18], size: 16) if score.title
|
|
407
|
+
if score.composer
|
|
408
|
+
pdf.text_box(score.composer, at: [pdf.bounds.right - 180, pdf.cursor + 18], width: 180, size: 10, align: :right)
|
|
409
|
+
end
|
|
410
|
+
return unless score.tempo
|
|
411
|
+
|
|
412
|
+
pdf.text_box(tempo_text(score.tempo), at: [LEFT_PADDING, pdf.cursor], size: 9)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def draw_staff_group(pdf, group, first_baseline, last_staff_index)
|
|
416
|
+
last_baseline = pdf.cursor - TOP_PADDING - (last_staff_index * style.staff_gap)
|
|
417
|
+
draw_staff_group_at(pdf, group, first_baseline, last_baseline)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def draw_staff_group_at(pdf, group, first_baseline, last_baseline)
|
|
421
|
+
x = LEFT_PADDING - 36
|
|
422
|
+
case group.bracket_type
|
|
423
|
+
when :brace
|
|
424
|
+
pdf.text_box("{", at: [x, first_baseline + style.staff_space], size: [last_baseline - first_baseline, 28].max.abs)
|
|
425
|
+
when :bracket
|
|
426
|
+
pdf.stroke_line [x, first_baseline + style.staff_space * 0.5], [x, last_baseline - style.staff_space * 4.5]
|
|
427
|
+
pdf.stroke_line [x, first_baseline + style.staff_space * 0.5], [x + 8, first_baseline + style.staff_space * 0.5]
|
|
428
|
+
pdf.stroke_line [x, last_baseline - style.staff_space * 4.5], [x + 8, last_baseline - style.staff_space * 4.5]
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def draw_element_with_context(pdf, element, x, baseline, staff, measure, accidental_state, note_points)
|
|
433
|
+
case element
|
|
434
|
+
when Clef::Core::Note
|
|
435
|
+
draw_note(pdf, element, x, baseline, staff.clef,
|
|
436
|
+
accidental_state: accidental_state,
|
|
437
|
+
key_signature: measure.key_signature || staff.key_signature)
|
|
438
|
+
note_points[element.object_id] = [x, pitch_to_y(element.pitch, baseline, staff.clef)]
|
|
439
|
+
when Clef::Core::Rest
|
|
440
|
+
draw_rest(pdf, element, x, baseline)
|
|
441
|
+
when Clef::Core::Chord
|
|
442
|
+
draw_chord(pdf, element, x, baseline, staff.clef,
|
|
443
|
+
accidental_state: accidental_state,
|
|
444
|
+
key_signature: measure.key_signature || staff.key_signature)
|
|
445
|
+
note_points[element.object_id] = [x, baseline]
|
|
446
|
+
when Clef::Core::Tuplet
|
|
447
|
+
draw_tuplet(pdf, element, x, baseline, staff.clef,
|
|
448
|
+
accidental_state: accidental_state,
|
|
449
|
+
key_signature: measure.key_signature || staff.key_signature,
|
|
450
|
+
note_points: note_points)
|
|
451
|
+
when Clef::Notation::Dynamic
|
|
452
|
+
draw_dynamic(pdf, element, x, baseline)
|
|
453
|
+
when Clef::Core::Tempo
|
|
454
|
+
draw_tempo_change(pdf, element, x, baseline)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def draw_dynamic(pdf, dynamic, x, baseline)
|
|
459
|
+
pdf.text_box(dynamic_text(dynamic), at: [x, baseline - (style.staff_space * 6)], size: 9)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def draw_tempo_change(pdf, tempo, x, baseline)
|
|
463
|
+
pdf.text_box(tempo_text(tempo), at: [x, baseline + (style.staff_space * 1.5)], size: 8)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def draw_flag(pdf, note, x, y, clef)
|
|
467
|
+
direction = Clef::Layout::Stem.direction(note, clef)
|
|
468
|
+
stem_len = style.staff_space * Clef::Layout::Stem.length(note, clef, direction)
|
|
469
|
+
stem_x = (direction == :up) ? x + 3 : x - 3
|
|
470
|
+
stem_y = (direction == :up) ? y + stem_len : y - stem_len
|
|
471
|
+
sweep = (direction == :up) ? -8 : 8
|
|
472
|
+
pdf.stroke_line [stem_x, stem_y], [stem_x + 8, stem_y + sweep]
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def draw_beams(pdf, layout, staff, measure, voice_id, note_points)
|
|
476
|
+
Array(layout&.dig(:beams, staff.id, measure.number, voice_id)).each do |group|
|
|
477
|
+
points = group.filter_map { |note| note_points[note.object_id] }
|
|
478
|
+
next if points.length < 2
|
|
479
|
+
|
|
480
|
+
y = points.map(&:last).max + (style.staff_space * 3.5)
|
|
481
|
+
previous_width = pdf.line_width if pdf.respond_to?(:line_width)
|
|
482
|
+
pdf.line_width = style.beam_thickness if pdf.respond_to?(:line_width=)
|
|
483
|
+
pdf.stroke_line [points.first.first + 3, y], [points.last.first + 3, y]
|
|
484
|
+
pdf.line_width = previous_width if previous_width && pdf.respond_to?(:line_width=)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def draw_note_connections(pdf, elements, note_points)
|
|
489
|
+
notes = flatten_elements(elements).select { |element| element.is_a?(Clef::Core::Note) }
|
|
490
|
+
notes.each_with_index do |note, index|
|
|
491
|
+
draw_connection_to_next(pdf, note, notes[(index + 1)..], note_points, :tie) if note.tie_state == :start
|
|
492
|
+
draw_connection_to_next(pdf, note, notes[(index + 1)..], note_points, :slur) if note.slur_start
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def draw_connection_to_next(pdf, note, candidates, note_points, kind)
|
|
497
|
+
target = (kind == :tie) ? candidates&.find { |candidate| candidate.pitch.enharmonic?(note.pitch) } : candidates&.find(&:slur_end)
|
|
498
|
+
return unless target
|
|
499
|
+
|
|
500
|
+
connection = (kind == :tie) ? Clef::Notation::Tie.new(note, target) : Clef::Notation::Slur.new(note, target)
|
|
501
|
+
return draw_ties(pdf, [connection], note_points: note_points) if kind == :tie
|
|
502
|
+
|
|
503
|
+
draw_slurs(pdf, [connection], note_points: note_points)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def draw_notation_connection(pdf, connection, note_points, lift:)
|
|
507
|
+
start_point = note_points[connection.start_note.object_id]
|
|
508
|
+
end_point = note_points[connection.end_note.object_id]
|
|
509
|
+
return unless start_point && end_point
|
|
510
|
+
|
|
511
|
+
if pdf.respond_to?(:stroke_curve)
|
|
512
|
+
pdf.stroke_curve [start_point[0] + 5, start_point[1] + 5],
|
|
513
|
+
[end_point[0] - 5, end_point[1] + 5],
|
|
514
|
+
bounds: [[start_point[0] + 18, start_point[1] + lift],
|
|
515
|
+
[end_point[0] - 18, end_point[1] + lift]]
|
|
516
|
+
else
|
|
517
|
+
pdf.stroke_line [start_point[0] + 5, start_point[1] + 5], [end_point[0] - 5, end_point[1] + 5]
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def draw_lyrics(pdf, staff, note_points, baseline)
|
|
522
|
+
Array(staff.metadata[:lyrics]).each do |lyric|
|
|
523
|
+
elements = staff.measures.flat_map { |measure| Array(measure.voices[lyric.voice_id]&.elements) }
|
|
524
|
+
.then { |elements| lyric_elements(elements) }
|
|
525
|
+
lyric_events(lyric, elements).each do |event|
|
|
526
|
+
draw_lyric_event(pdf, event, note_points, baseline)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def draw_lyric_event(pdf, event, note_points, baseline)
|
|
532
|
+
y = baseline - (style.staff_space * 6)
|
|
533
|
+
case event[:type]
|
|
534
|
+
when :text
|
|
535
|
+
point = note_points[event[:element]&.object_id]
|
|
536
|
+
return unless point
|
|
537
|
+
|
|
538
|
+
pdf.text_box(event[:syllable], at: [point.first - 10, y], size: 8, width: 24, align: :center)
|
|
539
|
+
when :hyphen
|
|
540
|
+
from = note_points[event[:from]&.object_id]
|
|
541
|
+
to = note_points[event[:to]&.object_id]
|
|
542
|
+
return unless from && to
|
|
543
|
+
|
|
544
|
+
pdf.text_box("-", at: [((from.first + to.first) / 2.0) - 4, y], size: 8, width: 8, align: :center)
|
|
545
|
+
when :extender
|
|
546
|
+
from = note_points[event[:from]&.object_id]
|
|
547
|
+
to = note_points[event[:to]&.object_id]
|
|
548
|
+
return unless from && to
|
|
549
|
+
|
|
550
|
+
pdf.stroke_line [from.first + 8, y + 4], [to.first - 8, y + 4]
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def draw_ledger_lines(pdf, x, y, baseline)
|
|
555
|
+
top = baseline
|
|
556
|
+
bottom = baseline - (style.staff_space * 4)
|
|
557
|
+
return if y.between?(bottom, top)
|
|
558
|
+
|
|
559
|
+
current = (y > top) ? top + style.staff_space : bottom - style.staff_space
|
|
560
|
+
while (y > top) ? current <= y : current >= y
|
|
561
|
+
pdf.stroke_line [x - 6, current], [x + 6, current]
|
|
562
|
+
current += (y > top) ? style.staff_space : -style.staff_space
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def draw_key_signature(pdf, key_signature, x, baseline)
|
|
567
|
+
return x unless key_signature
|
|
568
|
+
|
|
569
|
+
accidentals = key_signature.accidentals
|
|
570
|
+
order = (accidentals[:type] == :sharp) ? NotationHelpers::SHARP_ORDER : NotationHelpers::FLAT_ORDER
|
|
571
|
+
text = (accidentals[:type] == :sharp) ? "#" : "b"
|
|
572
|
+
order.first(accidentals[:count].to_i).each_with_index do |note_name, index|
|
|
573
|
+
y = baseline - key_signature_y(note_name)
|
|
574
|
+
pdf.text_box(text, at: [x + (index * 8), y], size: 10)
|
|
575
|
+
end
|
|
576
|
+
x + (accidentals[:count].to_i * 8)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def draw_time_signature(pdf, time_signature, x, baseline)
|
|
580
|
+
return x unless time_signature
|
|
581
|
+
|
|
582
|
+
pdf.text_box(time_signature.numerator.to_s, at: [x, baseline - (style.staff_space * 0.9)], size: 10, align: :center, width: 10)
|
|
583
|
+
pdf.text_box(time_signature.denominator.to_s, at: [x, baseline - (style.staff_space * 2.7)], size: 10, align: :center, width: 10)
|
|
584
|
+
x + 14
|
|
585
|
+
end
|
|
278
586
|
|
|
279
587
|
def prepare_canvas(pdf)
|
|
280
588
|
font_name = font_manager.register_with(pdf)
|
|
281
589
|
pdf.font(font_name)
|
|
282
590
|
@smufl_enabled = (font_name != "Helvetica")
|
|
283
|
-
rescue
|
|
591
|
+
rescue
|
|
284
592
|
pdf.font("Helvetica")
|
|
285
593
|
@smufl_enabled = false
|
|
286
594
|
end
|
|
@@ -294,7 +602,9 @@ module Clef
|
|
|
294
602
|
treble: "G",
|
|
295
603
|
bass: "F",
|
|
296
604
|
alto: "C",
|
|
297
|
-
tenor: "C"
|
|
605
|
+
tenor: "C",
|
|
606
|
+
percussion: "||",
|
|
607
|
+
tab: "TAB"
|
|
298
608
|
}.fetch(clef.type, clef.type.to_s[0].upcase)
|
|
299
609
|
end
|
|
300
610
|
|
|
@@ -317,7 +627,7 @@ module Clef
|
|
|
317
627
|
count = accidentals[:count].to_i
|
|
318
628
|
return nil if count.zero?
|
|
319
629
|
|
|
320
|
-
symbol = accidentals[:type] == :sharp ? "#" : "b"
|
|
630
|
+
symbol = (accidentals[:type] == :sharp) ? "#" : "b"
|
|
321
631
|
"#{count}#{symbol}"
|
|
322
632
|
end
|
|
323
633
|
|
|
@@ -328,7 +638,7 @@ module Clef
|
|
|
328
638
|
end
|
|
329
639
|
|
|
330
640
|
def pitch_to_y(pitch, baseline, clef)
|
|
331
|
-
calculate_pitch_y(pitch, baseline, clef, vertical_axis:
|
|
641
|
+
calculate_pitch_y(pitch, baseline, clef, vertical_axis: drawing_context.vertical_axis)
|
|
332
642
|
end
|
|
333
643
|
|
|
334
644
|
def staff_right_bound(pdf)
|
|
@@ -336,6 +646,101 @@ module Clef
|
|
|
336
646
|
|
|
337
647
|
LEFT_PADDING + 500
|
|
338
648
|
end
|
|
649
|
+
|
|
650
|
+
def x_for_moment(positions, moment, start_x, position_offset: 0.0)
|
|
651
|
+
return start_x unless positions
|
|
652
|
+
|
|
653
|
+
start_x + positions.fetch(moment, position_offset.to_f) - position_offset.to_f
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def measure_length_for(measure)
|
|
657
|
+
return measure.time_signature.measure_length if measure.time_signature
|
|
658
|
+
|
|
659
|
+
measure.voices.values.map(&:total_length).max || Rational(0, 1)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def voice_vertical_offset(index)
|
|
663
|
+
return 0 if index.zero?
|
|
664
|
+
|
|
665
|
+
index.odd? ? -(style.staff_space * 0.8) : style.staff_space * 0.8
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def rest_y(rest, baseline)
|
|
669
|
+
offset = {
|
|
670
|
+
whole: 2.0,
|
|
671
|
+
half: 2.2,
|
|
672
|
+
quarter: 1.6,
|
|
673
|
+
eighth: 1.4,
|
|
674
|
+
sixteenth: 1.4
|
|
675
|
+
}.fetch(rest.duration.base, 1.4)
|
|
676
|
+
baseline - (style.staff_space * offset)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def rest_glyph_key(duration)
|
|
680
|
+
{
|
|
681
|
+
whole: :rest_whole,
|
|
682
|
+
half: :rest_half,
|
|
683
|
+
quarter: :rest_quarter,
|
|
684
|
+
eighth: :rest_8th,
|
|
685
|
+
sixteenth: :rest_16th
|
|
686
|
+
}.fetch(duration.base, :rest_quarter)
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def key_signature_y(note_name)
|
|
690
|
+
{
|
|
691
|
+
f: style.staff_space * 0.5,
|
|
692
|
+
c: style.staff_space * 2.0,
|
|
693
|
+
g: style.staff_space * 0.1,
|
|
694
|
+
d: style.staff_space * 1.6,
|
|
695
|
+
a: style.staff_space * 3.0,
|
|
696
|
+
e: style.staff_space * 1.1,
|
|
697
|
+
b: style.staff_space * 2.6
|
|
698
|
+
}.fetch(note_name)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def chord_note_offsets(notes)
|
|
702
|
+
notes.each_with_index.map do |note, index|
|
|
703
|
+
previous = notes[index - 1]
|
|
704
|
+
(previous && (diatonic_step(note.pitch) - diatonic_step(previous.pitch)).abs == 1) ? 6 : 0
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def accidental_offset(index)
|
|
709
|
+
index * 4
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def tempo_text(tempo)
|
|
713
|
+
"#{tempo.beat_unit.to_lilypond} = #{tempo.bpm}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def draw_layout_items(pdf, layout, system)
|
|
717
|
+
Array(layout[:items]).each do |item|
|
|
718
|
+
next unless item.type == :text
|
|
719
|
+
next unless system.include_moment?(item.moment)
|
|
720
|
+
|
|
721
|
+
x = x_for_moment(layout[:positions], item.moment, STAFF_START_X, position_offset: system.position_offset)
|
|
722
|
+
y = pdf.bounds.top - TOP_PADDING - system.line_top + style.staff_space
|
|
723
|
+
pdf.text_box(item.payload.fetch(:text), at: [x, y], size: 8)
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def drawing_context
|
|
728
|
+
@drawing_context ||= Clef::Renderer::DrawingContext.pdf
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def render_to_io(score, io, positions:, layout:)
|
|
732
|
+
pdf = Prawn::Document.new(page_size: style.page_size, margin: style.margin)
|
|
733
|
+
prepare_canvas(pdf)
|
|
734
|
+
draw_score(pdf, score, positions, layout: layout)
|
|
735
|
+
io.write(pdf.render)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def ensure_parent_directory!(path)
|
|
739
|
+
parent = File.dirname(path.to_s)
|
|
740
|
+
return if parent.nil? || parent == "." || Dir.exist?(parent)
|
|
741
|
+
|
|
742
|
+
raise ArgumentError, "output directory does not exist: #{parent}"
|
|
743
|
+
end
|
|
339
744
|
end
|
|
340
745
|
end
|
|
341
746
|
end
|