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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +172 -0
  4. data/Rakefile +17 -0
  5. data/examples/bach_cello_suite.rb +16 -0
  6. data/examples/piano_score.rb +24 -0
  7. data/examples/twinkle.rb +18 -0
  8. data/examples/vocal_score.rb +21 -0
  9. data/fonts/bravura/README.md +8 -0
  10. data/lib/clef/compiler.rb +46 -0
  11. data/lib/clef/core/chord.rb +34 -0
  12. data/lib/clef/core/clef.rb +40 -0
  13. data/lib/clef/core/duration.rb +96 -0
  14. data/lib/clef/core/key_signature.rb +79 -0
  15. data/lib/clef/core/measure.rb +46 -0
  16. data/lib/clef/core/note.rb +43 -0
  17. data/lib/clef/core/pitch.rb +151 -0
  18. data/lib/clef/core/rest.rb +21 -0
  19. data/lib/clef/core/score.rb +61 -0
  20. data/lib/clef/core/staff.rb +34 -0
  21. data/lib/clef/core/staff_group.rb +30 -0
  22. data/lib/clef/core/tempo.rb +19 -0
  23. data/lib/clef/core/time_signature.rb +38 -0
  24. data/lib/clef/core/voice.rb +29 -0
  25. data/lib/clef/engraving/font_manager.rb +35 -0
  26. data/lib/clef/engraving/glyph_table.rb +52 -0
  27. data/lib/clef/engraving/rules.rb +14 -0
  28. data/lib/clef/engraving/style.rb +27 -0
  29. data/lib/clef/ir/event.rb +22 -0
  30. data/lib/clef/ir/moment.rb +64 -0
  31. data/lib/clef/ir/music_tree.rb +54 -0
  32. data/lib/clef/ir/timeline.rb +69 -0
  33. data/lib/clef/layout/beam_layout.rb +42 -0
  34. data/lib/clef/layout/line_breaker.rb +60 -0
  35. data/lib/clef/layout/page_breaker.rb +16 -0
  36. data/lib/clef/layout/spacing.rb +56 -0
  37. data/lib/clef/layout/stem.rb +38 -0
  38. data/lib/clef/midi/channel_map.rb +14 -0
  39. data/lib/clef/midi/exporter.rb +81 -0
  40. data/lib/clef/notation/articulation.rb +18 -0
  41. data/lib/clef/notation/barline.rb +30 -0
  42. data/lib/clef/notation/beam.rb +25 -0
  43. data/lib/clef/notation/dynamic.rb +18 -0
  44. data/lib/clef/notation/lyric.rb +28 -0
  45. data/lib/clef/notation/slur.rb +28 -0
  46. data/lib/clef/notation/tie.rb +30 -0
  47. data/lib/clef/parser/dsl.rb +265 -0
  48. data/lib/clef/parser/lilypond_lexer.rb +22 -0
  49. data/lib/clef/parser/lilypond_parser.rb +57 -0
  50. data/lib/clef/plugins/base.rb +26 -0
  51. data/lib/clef/plugins/registry.rb +34 -0
  52. data/lib/clef/renderer/base.rb +26 -0
  53. data/lib/clef/renderer/notation_helpers.rb +71 -0
  54. data/lib/clef/renderer/pdf_renderer.rb +341 -0
  55. data/lib/clef/renderer/svg_renderer.rb +206 -0
  56. data/lib/clef/version.rb +5 -0
  57. data/lib/clef.rb +25 -0
  58. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clef
4
+ VERSION = "0.1.0"
5
+ end
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