clef 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +17 -0
- data/examples/bach_cello_suite.rb +16 -0
- data/examples/piano_score.rb +24 -0
- data/examples/twinkle.rb +18 -0
- data/examples/vocal_score.rb +21 -0
- data/fonts/bravura/README.md +8 -0
- data/lib/clef/compiler.rb +46 -0
- data/lib/clef/core/chord.rb +34 -0
- data/lib/clef/core/clef.rb +40 -0
- data/lib/clef/core/duration.rb +96 -0
- data/lib/clef/core/key_signature.rb +79 -0
- data/lib/clef/core/measure.rb +46 -0
- data/lib/clef/core/note.rb +43 -0
- data/lib/clef/core/pitch.rb +151 -0
- data/lib/clef/core/rest.rb +21 -0
- data/lib/clef/core/score.rb +61 -0
- data/lib/clef/core/staff.rb +34 -0
- data/lib/clef/core/staff_group.rb +30 -0
- data/lib/clef/core/tempo.rb +19 -0
- data/lib/clef/core/time_signature.rb +38 -0
- data/lib/clef/core/voice.rb +29 -0
- data/lib/clef/engraving/font_manager.rb +35 -0
- data/lib/clef/engraving/glyph_table.rb +52 -0
- data/lib/clef/engraving/rules.rb +14 -0
- data/lib/clef/engraving/style.rb +27 -0
- data/lib/clef/ir/event.rb +22 -0
- data/lib/clef/ir/moment.rb +64 -0
- data/lib/clef/ir/music_tree.rb +54 -0
- data/lib/clef/ir/timeline.rb +69 -0
- data/lib/clef/layout/beam_layout.rb +42 -0
- data/lib/clef/layout/line_breaker.rb +60 -0
- data/lib/clef/layout/page_breaker.rb +16 -0
- data/lib/clef/layout/spacing.rb +56 -0
- data/lib/clef/layout/stem.rb +38 -0
- data/lib/clef/midi/channel_map.rb +14 -0
- data/lib/clef/midi/exporter.rb +81 -0
- data/lib/clef/notation/articulation.rb +18 -0
- data/lib/clef/notation/barline.rb +30 -0
- data/lib/clef/notation/beam.rb +25 -0
- data/lib/clef/notation/dynamic.rb +18 -0
- data/lib/clef/notation/lyric.rb +28 -0
- data/lib/clef/notation/slur.rb +28 -0
- data/lib/clef/notation/tie.rb +30 -0
- data/lib/clef/parser/dsl.rb +265 -0
- data/lib/clef/parser/lilypond_lexer.rb +22 -0
- data/lib/clef/parser/lilypond_parser.rb +57 -0
- data/lib/clef/plugins/base.rb +26 -0
- data/lib/clef/plugins/registry.rb +34 -0
- data/lib/clef/renderer/base.rb +26 -0
- data/lib/clef/renderer/notation_helpers.rb +71 -0
- data/lib/clef/renderer/pdf_renderer.rb +341 -0
- data/lib/clef/renderer/svg_renderer.rb +206 -0
- data/lib/clef/version.rb +5 -0
- data/lib/clef.rb +25 -0
- metadata +141 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prawn"
|
|
4
|
+
|
|
5
|
+
module Clef
|
|
6
|
+
module Renderer
|
|
7
|
+
class PdfRenderer < Base
|
|
8
|
+
include NotationHelpers
|
|
9
|
+
|
|
10
|
+
LEFT_PADDING = 80
|
|
11
|
+
TOP_PADDING = 40
|
|
12
|
+
|
|
13
|
+
# @param score [Clef::Core::Score]
|
|
14
|
+
# @param path [String]
|
|
15
|
+
# @param positions [Hash]
|
|
16
|
+
def render(score, path, positions: nil, **_options)
|
|
17
|
+
Prawn::Document.generate(path, page_size: style.page_size, margin: style.margin) do |pdf|
|
|
18
|
+
prepare_canvas(pdf)
|
|
19
|
+
draw_score(pdf, score, positions)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param pdf [Prawn::Document]
|
|
24
|
+
# @param score [Clef::Core::Score]
|
|
25
|
+
# @param _positions [Hash, nil]
|
|
26
|
+
def draw_score(pdf, score, _positions)
|
|
27
|
+
score.staves.each_with_index do |staff, index|
|
|
28
|
+
baseline = pdf.cursor - TOP_PADDING - (index * style.staff_space * 10)
|
|
29
|
+
draw_staff(pdf, staff, baseline)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param pdf [Prawn::Document]
|
|
34
|
+
# @param staff [Clef::Core::Staff]
|
|
35
|
+
# @param baseline [Float]
|
|
36
|
+
def draw_staff(pdf, staff, baseline)
|
|
37
|
+
draw_staff_lines(pdf, baseline)
|
|
38
|
+
cursor = draw_clef(pdf, staff.clef, LEFT_PADDING - 24, baseline)
|
|
39
|
+
cursor += style.staff_space
|
|
40
|
+
cursor = draw_metadata(pdf, staff, cursor, baseline)
|
|
41
|
+
cursor += style.staff_space
|
|
42
|
+
draw_measures(pdf, staff, [cursor, LEFT_PADDING + (style.staff_space * 6)].max, baseline)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param pdf [Prawn::Document]
|
|
46
|
+
# @param baseline [Float]
|
|
47
|
+
def draw_staff_lines(pdf, baseline)
|
|
48
|
+
5.times do |index|
|
|
49
|
+
y = baseline - (index * style.staff_space)
|
|
50
|
+
pdf.stroke_line [LEFT_PADDING, y], [staff_right_bound(pdf), y]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param pdf [Prawn::Document]
|
|
55
|
+
# @param clef [Clef::Core::Clef]
|
|
56
|
+
# @param x [Float]
|
|
57
|
+
# @param baseline [Float]
|
|
58
|
+
def draw_clef(pdf, clef, x, baseline)
|
|
59
|
+
glyph = smufl_enabled? ? glyph_table[:"clef_#{clef.type}"] : fallback_clef_text(clef)
|
|
60
|
+
size = smufl_enabled? ? 16 : 10
|
|
61
|
+
y = smufl_enabled? ? (baseline - style.staff_space) : (baseline + (style.staff_space * 1.5))
|
|
62
|
+
|
|
63
|
+
pdf.text_box(glyph, at: [x, y], size: size)
|
|
64
|
+
x + text_width(pdf, glyph, size: size)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param pdf [Prawn::Document]
|
|
68
|
+
# @param staff [Clef::Core::Staff]
|
|
69
|
+
# @param x [Float]
|
|
70
|
+
# @param baseline [Float]
|
|
71
|
+
def draw_metadata(pdf, staff, x, baseline)
|
|
72
|
+
text = metadata_text(staff)
|
|
73
|
+
return x if text.nil?
|
|
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)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param pdf [Prawn::Document]
|
|
82
|
+
# @param staff [Clef::Core::Staff]
|
|
83
|
+
# @param start_x [Float]
|
|
84
|
+
# @param baseline [Float]
|
|
85
|
+
def draw_measures(pdf, staff, start_x, baseline)
|
|
86
|
+
cursor = start_x
|
|
87
|
+
staff.measures.each do |measure|
|
|
88
|
+
cursor = draw_measure(pdf, measure, staff.clef, cursor, baseline)
|
|
89
|
+
draw_barline(pdf, cursor, baseline)
|
|
90
|
+
cursor += style.staff_space * 1.2
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param pdf [Prawn::Document]
|
|
95
|
+
# @param measure [Clef::Core::Measure]
|
|
96
|
+
# @param clef [Clef::Core::Clef]
|
|
97
|
+
# @param cursor [Float]
|
|
98
|
+
# @param baseline [Float]
|
|
99
|
+
# @return [Float]
|
|
100
|
+
def draw_measure(pdf, measure, clef, cursor, baseline)
|
|
101
|
+
voice = measure.voices.values.first
|
|
102
|
+
return cursor unless voice
|
|
103
|
+
|
|
104
|
+
voice.elements.each do |element|
|
|
105
|
+
draw_element(pdf, element, cursor, baseline, clef)
|
|
106
|
+
cursor += duration_spacing(element)
|
|
107
|
+
end
|
|
108
|
+
cursor
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param pdf [Prawn::Document]
|
|
112
|
+
# @param element [Object]
|
|
113
|
+
# @param x [Float]
|
|
114
|
+
# @param baseline [Float]
|
|
115
|
+
# @param clef [Clef::Core::Clef]
|
|
116
|
+
def draw_element(pdf, element, x, baseline, clef)
|
|
117
|
+
case element
|
|
118
|
+
when Clef::Core::Note then draw_note(pdf, element, x, baseline, clef)
|
|
119
|
+
when Clef::Core::Rest then draw_rest(pdf, element, x, baseline)
|
|
120
|
+
when Clef::Core::Chord then draw_chord(pdf, element, x, baseline, clef)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @param pdf [Prawn::Document]
|
|
125
|
+
# @param note [Clef::Core::Note]
|
|
126
|
+
# @param x [Float]
|
|
127
|
+
# @param baseline [Float]
|
|
128
|
+
# @param clef [Clef::Core::Clef]
|
|
129
|
+
def draw_note(pdf, note, x, baseline, clef)
|
|
130
|
+
y = pitch_to_y(note.pitch, baseline, clef)
|
|
131
|
+
draw_notehead(pdf, x, y, duration: note.duration)
|
|
132
|
+
draw_accidental(pdf, note.pitch, x, y)
|
|
133
|
+
draw_stem(pdf, note, x, y, clef) if stem_required?(note.duration)
|
|
134
|
+
draw_dot(pdf, note.duration, x, y)
|
|
135
|
+
draw_articulations(pdf, note.articulations, x, y)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param pdf [Prawn::Document]
|
|
139
|
+
# @param rest [Clef::Core::Rest]
|
|
140
|
+
# @param x [Float]
|
|
141
|
+
# @param baseline [Float]
|
|
142
|
+
def draw_rest(pdf, rest, x, baseline)
|
|
143
|
+
glyph = if smufl_enabled?
|
|
144
|
+
key = rest.duration.base == :whole ? :rest_whole : :rest_quarter
|
|
145
|
+
glyph_table[key]
|
|
146
|
+
else
|
|
147
|
+
"r"
|
|
148
|
+
end
|
|
149
|
+
pdf.text_box(glyph, at: [x, baseline - (style.staff_space * 1.5)], size: 14)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param pdf [Prawn::Document]
|
|
153
|
+
# @param chord [Clef::Core::Chord]
|
|
154
|
+
# @param x [Float]
|
|
155
|
+
# @param baseline [Float]
|
|
156
|
+
# @param clef [Clef::Core::Clef]
|
|
157
|
+
def draw_chord(pdf, chord, x, baseline, clef)
|
|
158
|
+
notes = chord_notes(chord)
|
|
159
|
+
ys = []
|
|
160
|
+
notes.each do |note|
|
|
161
|
+
y = pitch_to_y(note.pitch, baseline, clef)
|
|
162
|
+
ys << y
|
|
163
|
+
draw_notehead(pdf, x, y, duration: chord.duration)
|
|
164
|
+
draw_accidental(pdf, note.pitch, x, y)
|
|
165
|
+
end
|
|
166
|
+
draw_chord_stem(pdf, notes, x, ys, clef) if stem_required?(chord.duration)
|
|
167
|
+
draw_dot(pdf, chord.duration, x, ys.sum / ys.length.to_f)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @param pdf [Prawn::Document]
|
|
171
|
+
# @param x [Float]
|
|
172
|
+
# @param y [Float]
|
|
173
|
+
# @param duration [Clef::Core::Duration]
|
|
174
|
+
def draw_notehead(pdf, x, y, duration:)
|
|
175
|
+
pdf.fill_color("000000")
|
|
176
|
+
if filled_notehead?(duration)
|
|
177
|
+
pdf.circle([x, y], 3)
|
|
178
|
+
pdf.fill
|
|
179
|
+
else
|
|
180
|
+
pdf.fill_color("FFFFFF")
|
|
181
|
+
pdf.ellipse([x, y], 3.5, 2.5)
|
|
182
|
+
pdf.fill_and_stroke
|
|
183
|
+
pdf.fill_color("000000")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @param pdf [Prawn::Document]
|
|
188
|
+
# @param note [Clef::Core::Note]
|
|
189
|
+
# @param x [Float]
|
|
190
|
+
# @param y [Float]
|
|
191
|
+
# @param clef [Clef::Core::Clef]
|
|
192
|
+
def draw_stem(pdf, note, x, y, clef)
|
|
193
|
+
direction = Clef::Layout::Stem.direction(note, clef)
|
|
194
|
+
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
|
|
197
|
+
pdf.stroke_line [stem_x, y], [stem_x, y2]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @param pdf [Prawn::Document]
|
|
201
|
+
# @param notes [Array<Clef::Core::Note>]
|
|
202
|
+
# @param x [Float]
|
|
203
|
+
# @param ys [Array<Float>]
|
|
204
|
+
# @param clef [Clef::Core::Clef]
|
|
205
|
+
def draw_chord_stem(pdf, notes, x, ys, clef)
|
|
206
|
+
direction = Clef::Layout::Stem.direction(notes, clef)
|
|
207
|
+
anchor_note = chord_stem_anchor_note(notes, direction)
|
|
208
|
+
anchor_index = notes.index(anchor_note)
|
|
209
|
+
anchor_y = ys[anchor_index]
|
|
210
|
+
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
|
|
213
|
+
pdf.stroke_line [stem_x, anchor_y], [stem_x, y2]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @param pdf [Prawn::Document]
|
|
217
|
+
# @param pitch [Clef::Core::Pitch]
|
|
218
|
+
# @param x [Float]
|
|
219
|
+
# @param y [Float]
|
|
220
|
+
def draw_accidental(pdf, pitch, x, y)
|
|
221
|
+
key = accidental_glyph_key(pitch.alteration)
|
|
222
|
+
return unless key
|
|
223
|
+
|
|
224
|
+
glyph = smufl_enabled? ? glyph_table[key] : accidental_text(pitch.alteration)
|
|
225
|
+
pdf.text_box(glyph, at: [x - 12, y + 5], size: 9)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @param pdf [Prawn::Document]
|
|
229
|
+
# @param duration [Clef::Core::Duration]
|
|
230
|
+
# @param x [Float]
|
|
231
|
+
# @param y [Float]
|
|
232
|
+
def draw_dot(pdf, duration, x, y)
|
|
233
|
+
return if duration.dots.zero?
|
|
234
|
+
|
|
235
|
+
duration.dots.times do |index|
|
|
236
|
+
pdf.circle([x + 8 + (index * 3), y], 1)
|
|
237
|
+
pdf.fill
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @param pdf [Prawn::Document]
|
|
242
|
+
# @param articulations [Array<Symbol>]
|
|
243
|
+
# @param x [Float]
|
|
244
|
+
# @param y [Float]
|
|
245
|
+
def draw_articulations(pdf, articulations, x, y)
|
|
246
|
+
return if articulations.empty?
|
|
247
|
+
|
|
248
|
+
text = articulations.join(",")
|
|
249
|
+
pdf.text_box(text, at: [x - 4, y + 12], size: 6)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @param pdf [Prawn::Document]
|
|
253
|
+
# @param x [Float]
|
|
254
|
+
# @param baseline [Float]
|
|
255
|
+
def draw_barline(pdf, x, baseline)
|
|
256
|
+
top = baseline + (style.staff_space * 0.5)
|
|
257
|
+
bottom = baseline - (style.staff_space * 4.5)
|
|
258
|
+
pdf.stroke_line [x, top], [x, bottom]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# @param _pdf [Prawn::Document]
|
|
262
|
+
# @param _slurs [Array]
|
|
263
|
+
def draw_slurs(_pdf, _slurs = []); end
|
|
264
|
+
|
|
265
|
+
# @param _pdf [Prawn::Document]
|
|
266
|
+
# @param _ties [Array]
|
|
267
|
+
def draw_ties(_pdf, _ties = []); end
|
|
268
|
+
|
|
269
|
+
# @param _pdf [Prawn::Document]
|
|
270
|
+
# @param _beams [Array]
|
|
271
|
+
def draw_beams(_pdf, _beams = []); end
|
|
272
|
+
|
|
273
|
+
# @param _pdf [Prawn::Document]
|
|
274
|
+
# @param _lyrics [Array]
|
|
275
|
+
def draw_lyrics(_pdf, _lyrics = []); end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
def prepare_canvas(pdf)
|
|
280
|
+
font_name = font_manager.register_with(pdf)
|
|
281
|
+
pdf.font(font_name)
|
|
282
|
+
@smufl_enabled = (font_name != "Helvetica")
|
|
283
|
+
rescue StandardError
|
|
284
|
+
pdf.font("Helvetica")
|
|
285
|
+
@smufl_enabled = false
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def smufl_enabled?
|
|
289
|
+
@smufl_enabled
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def fallback_clef_text(clef)
|
|
293
|
+
{
|
|
294
|
+
treble: "G",
|
|
295
|
+
bass: "F",
|
|
296
|
+
alto: "C",
|
|
297
|
+
tenor: "C"
|
|
298
|
+
}.fetch(clef.type, clef.type.to_s[0].upcase)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def metadata_text(staff)
|
|
302
|
+
parts = []
|
|
303
|
+
key_text = key_signature_text(staff.key_signature)
|
|
304
|
+
parts << key_text unless key_text.nil?
|
|
305
|
+
if staff.time_signature
|
|
306
|
+
parts << "#{staff.time_signature.numerator}/#{staff.time_signature.denominator}"
|
|
307
|
+
end
|
|
308
|
+
return nil if parts.empty?
|
|
309
|
+
|
|
310
|
+
parts.join(" ")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def key_signature_text(key_signature)
|
|
314
|
+
return nil if key_signature.nil?
|
|
315
|
+
|
|
316
|
+
accidentals = key_signature.accidentals
|
|
317
|
+
count = accidentals[:count].to_i
|
|
318
|
+
return nil if count.zero?
|
|
319
|
+
|
|
320
|
+
symbol = accidentals[:type] == :sharp ? "#" : "b"
|
|
321
|
+
"#{count}#{symbol}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def text_width(pdf, text, size:)
|
|
325
|
+
return pdf.width_of(text, size: size) if pdf.respond_to?(:width_of)
|
|
326
|
+
|
|
327
|
+
text.length * (size * 0.5)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def pitch_to_y(pitch, baseline, clef)
|
|
331
|
+
calculate_pitch_y(pitch, baseline, clef, vertical_axis: 1)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def staff_right_bound(pdf)
|
|
335
|
+
return pdf.bounds.right if pdf.respond_to?(:bounds)
|
|
336
|
+
|
|
337
|
+
LEFT_PADDING + 500
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Clef
|
|
6
|
+
module Renderer
|
|
7
|
+
class SvgRenderer < Base
|
|
8
|
+
include NotationHelpers
|
|
9
|
+
|
|
10
|
+
WIDTH = 1024
|
|
11
|
+
HEIGHT = 512
|
|
12
|
+
|
|
13
|
+
# @param score [Clef::Core::Score]
|
|
14
|
+
# @param path [String]
|
|
15
|
+
# @param _positions [Hash]
|
|
16
|
+
def render(score, path, _positions: nil, **_options)
|
|
17
|
+
document = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
18
|
+
xml.svg(xmlns: "http://www.w3.org/2000/svg", width: WIDTH, height: HEIGHT) do
|
|
19
|
+
draw_score(xml, score)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
File.write(path, document.to_xml)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
26
|
+
# @param score [Clef::Core::Score]
|
|
27
|
+
def draw_score(xml, score)
|
|
28
|
+
score.staves.each_with_index do |staff, index|
|
|
29
|
+
baseline = 80 + (index * style.staff_space * 9)
|
|
30
|
+
draw_staff_lines(xml, baseline)
|
|
31
|
+
draw_notes(xml, staff, baseline)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
36
|
+
# @param baseline [Float]
|
|
37
|
+
def draw_staff_lines(xml, baseline)
|
|
38
|
+
5.times do |line|
|
|
39
|
+
y = baseline + (line * style.staff_space)
|
|
40
|
+
xml.line(x1: 60, y1: y, x2: WIDTH - 60, y2: y, stroke: "black", "stroke-width": 1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
45
|
+
# @param staff [Clef::Core::Staff]
|
|
46
|
+
# @param baseline [Float]
|
|
47
|
+
def draw_notes(xml, staff, baseline)
|
|
48
|
+
x = 140
|
|
49
|
+
staff.measures.each do |measure|
|
|
50
|
+
voice = measure.voices.values.first
|
|
51
|
+
next unless voice
|
|
52
|
+
|
|
53
|
+
voice.elements.each do |element|
|
|
54
|
+
draw_element(xml, element, x, baseline, staff.clef)
|
|
55
|
+
x += duration_spacing(element)
|
|
56
|
+
end
|
|
57
|
+
x += 16
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
62
|
+
# @param element [Object]
|
|
63
|
+
# @param x [Float]
|
|
64
|
+
# @param baseline [Float]
|
|
65
|
+
# @param clef [Clef::Core::Clef]
|
|
66
|
+
def draw_element(xml, element, x, baseline, clef)
|
|
67
|
+
case element
|
|
68
|
+
when Clef::Core::Note then draw_note(xml, element, x, baseline, clef)
|
|
69
|
+
when Clef::Core::Rest then draw_rest(xml, element, x, baseline)
|
|
70
|
+
when Clef::Core::Chord then draw_chord(xml, element, x, baseline, clef)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
77
|
+
# @param note [Clef::Core::Note]
|
|
78
|
+
# @param x [Float]
|
|
79
|
+
# @param baseline [Float]
|
|
80
|
+
# @param clef [Clef::Core::Clef]
|
|
81
|
+
def draw_note(xml, note, x, baseline, clef)
|
|
82
|
+
y = pitch_y(note.pitch, baseline, clef)
|
|
83
|
+
draw_notehead(xml, x, y, duration: note.duration)
|
|
84
|
+
draw_accidental(xml, note.pitch, x, y)
|
|
85
|
+
draw_stem(xml, note, x, y, clef) if stem_required?(note.duration)
|
|
86
|
+
draw_dot(xml, note.duration, x, y)
|
|
87
|
+
draw_articulations(xml, note.articulations, x, y)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
91
|
+
# @param rest [Clef::Core::Rest]
|
|
92
|
+
# @param x [Float]
|
|
93
|
+
# @param baseline [Float]
|
|
94
|
+
def draw_rest(xml, rest, x, baseline)
|
|
95
|
+
if rest.duration.base == :whole
|
|
96
|
+
xml.rect(x: x - 4, y: baseline + (style.staff_space * 2.8), width: 8, height: 3, fill: "black")
|
|
97
|
+
else
|
|
98
|
+
draw_text(xml, "r", x: x - 4, y: baseline + (style.staff_space * 2.1), fill: "black", "font-size": 14)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
103
|
+
# @param chord [Clef::Core::Chord]
|
|
104
|
+
# @param x [Float]
|
|
105
|
+
# @param baseline [Float]
|
|
106
|
+
# @param clef [Clef::Core::Clef]
|
|
107
|
+
def draw_chord(xml, chord, x, baseline, clef)
|
|
108
|
+
notes = chord_notes(chord)
|
|
109
|
+
ys = []
|
|
110
|
+
notes.each do |note|
|
|
111
|
+
y = pitch_y(note.pitch, baseline, clef)
|
|
112
|
+
ys << y
|
|
113
|
+
draw_notehead(xml, x, y, duration: chord.duration)
|
|
114
|
+
draw_accidental(xml, note.pitch, x, y)
|
|
115
|
+
end
|
|
116
|
+
draw_chord_stem(xml, notes, x, ys, clef) if stem_required?(chord.duration)
|
|
117
|
+
draw_dot(xml, chord.duration, x, ys.sum / ys.length.to_f)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
121
|
+
# @param x [Float]
|
|
122
|
+
# @param y [Float]
|
|
123
|
+
# @param duration [Clef::Core::Duration]
|
|
124
|
+
def draw_notehead(xml, x, y, duration:)
|
|
125
|
+
if filled_notehead?(duration)
|
|
126
|
+
xml.circle(cx: x, cy: y, r: 3, fill: "black")
|
|
127
|
+
else
|
|
128
|
+
xml.ellipse(cx: x, cy: y, rx: 3.5, ry: 2.5, fill: "white", stroke: "black", "stroke-width": 1)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
133
|
+
# @param note [Clef::Core::Note]
|
|
134
|
+
# @param x [Float]
|
|
135
|
+
# @param y [Float]
|
|
136
|
+
# @param clef [Clef::Core::Clef]
|
|
137
|
+
def draw_stem(xml, note, x, y, clef)
|
|
138
|
+
direction = Clef::Layout::Stem.direction(note, clef)
|
|
139
|
+
stem_len = style.staff_space * Clef::Layout::Stem.length(note, clef, direction)
|
|
140
|
+
y2 = direction == :up ? y - stem_len : y + stem_len
|
|
141
|
+
stem_x = direction == :up ? x + 3 : x - 3
|
|
142
|
+
xml.line(x1: stem_x, y1: y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
146
|
+
# @param notes [Array<Clef::Core::Note>]
|
|
147
|
+
# @param x [Float]
|
|
148
|
+
# @param ys [Array<Float>]
|
|
149
|
+
# @param clef [Clef::Core::Clef]
|
|
150
|
+
def draw_chord_stem(xml, notes, x, ys, clef)
|
|
151
|
+
direction = Clef::Layout::Stem.direction(notes, clef)
|
|
152
|
+
anchor_note = chord_stem_anchor_note(notes, direction)
|
|
153
|
+
anchor_index = notes.index(anchor_note)
|
|
154
|
+
anchor_y = ys[anchor_index]
|
|
155
|
+
stem_len = style.staff_space * chord_stem_length(notes, clef, direction)
|
|
156
|
+
y2 = direction == :up ? anchor_y - stem_len : anchor_y + stem_len
|
|
157
|
+
stem_x = direction == :up ? x + 3 : x - 3
|
|
158
|
+
xml.line(x1: stem_x, y1: anchor_y, x2: stem_x, y2: y2, stroke: "black", "stroke-width": 1)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
162
|
+
# @param pitch [Clef::Core::Pitch]
|
|
163
|
+
# @param x [Float]
|
|
164
|
+
# @param y [Float]
|
|
165
|
+
def draw_accidental(xml, pitch, x, y)
|
|
166
|
+
key = accidental_glyph_key(pitch.alteration)
|
|
167
|
+
return unless key
|
|
168
|
+
|
|
169
|
+
draw_text(xml, accidental_text(pitch.alteration), x: x - 12, y: y + 3, fill: "black", "font-size": 9)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
173
|
+
# @param duration [Clef::Core::Duration]
|
|
174
|
+
# @param x [Float]
|
|
175
|
+
# @param y [Float]
|
|
176
|
+
def draw_dot(xml, duration, x, y)
|
|
177
|
+
return if duration.dots.zero?
|
|
178
|
+
|
|
179
|
+
duration.dots.times do |index|
|
|
180
|
+
xml.circle(cx: x + 8 + (index * 3), cy: y, r: 1, fill: "black")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @param xml [Nokogiri::XML::Builder]
|
|
185
|
+
# @param articulations [Array<Symbol>]
|
|
186
|
+
# @param x [Float]
|
|
187
|
+
# @param y [Float]
|
|
188
|
+
def draw_articulations(xml, articulations, x, y)
|
|
189
|
+
return if articulations.empty?
|
|
190
|
+
|
|
191
|
+
draw_text(xml, articulations.join(","), x: x - 4, y: y - 12, fill: "black", "font-size": 6)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def pitch_y(pitch, baseline, clef)
|
|
195
|
+
calculate_pitch_y(pitch, baseline, clef, vertical_axis: -1)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def draw_text(xml, content, **attributes)
|
|
199
|
+
node = Nokogiri::XML::Node.new("text", xml.doc)
|
|
200
|
+
attributes.each { |key, value| node[key.to_s] = value.to_s }
|
|
201
|
+
node.content = content.to_s
|
|
202
|
+
xml.parent << node
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/clef/version.rb
ADDED
data/lib/clef.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "clef/version"
|
|
4
|
+
Dir[File.expand_path("clef/**/*.rb", __dir__)].sort.each do |file|
|
|
5
|
+
require file unless file.end_with?("version.rb")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module Clef
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# @yield DSL block
|
|
13
|
+
# @return [Clef::Core::Score]
|
|
14
|
+
def score(&block)
|
|
15
|
+
builder = Clef::Parser::DSL::ScoreBuilder.new
|
|
16
|
+
builder.instance_eval(&block) if block
|
|
17
|
+
builder.build
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Clef::Plugins::Registry]
|
|
21
|
+
def plugins
|
|
22
|
+
@plugins ||= Clef::Plugins::Registry.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|