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
|
@@ -3,41 +3,156 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Parser
|
|
5
5
|
class LilypondParser
|
|
6
|
+
SUPPORTED_COMMANDS = %w[\\clef \\key \\major \\minor \\time \\tempo \\relative \\new \\with].freeze
|
|
7
|
+
DYNAMIC_COMMANDS = Clef::Notation::Dynamic::TYPES.map { |type| "\\#{type}" }.freeze
|
|
8
|
+
STAFF_COMMAND = /\\new\s+(?:Staff|PianoStaff|StaffGroup)/
|
|
9
|
+
|
|
10
|
+
attr_reader :warnings, :plugins
|
|
11
|
+
|
|
12
|
+
# @param plugins [Clef::Plugins::Registry]
|
|
13
|
+
def initialize(plugins: Clef.plugins)
|
|
14
|
+
@plugins = plugins
|
|
15
|
+
@warnings = []
|
|
16
|
+
end
|
|
17
|
+
|
|
6
18
|
# @param input [String]
|
|
7
19
|
# @return [Clef::Core::Score]
|
|
8
20
|
def parse(input)
|
|
9
|
-
cleaned = input
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
cleaned = strip_comments(input)
|
|
22
|
+
@warnings = unsupported_command_warnings(cleaned)
|
|
23
|
+
score = build_score(cleaned)
|
|
24
|
+
plugins.run_hook(:on_after_parse, score)
|
|
25
|
+
score
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_score(input)
|
|
31
|
+
staff_blocks = extract_staff_blocks(input)
|
|
32
|
+
return build_single_staff_score(input) if staff_blocks.empty?
|
|
33
|
+
|
|
34
|
+
global_tempo_unit, global_tempo_bpm = extract_tempo(input)
|
|
35
|
+
use_staff_group = staff_group_input?(input)
|
|
36
|
+
parser = self
|
|
37
|
+
Clef.score(plugins: plugins) do
|
|
38
|
+
tempo beat_unit: global_tempo_unit, bpm: global_tempo_bpm if global_tempo_bpm
|
|
39
|
+
if use_staff_group
|
|
40
|
+
staff_group :bracket do
|
|
41
|
+
staff_blocks.each_with_index { |block, index| parser.send(:build_staff_from_block, self, block, index) }
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
staff_blocks.each_with_index { |block, index| parser.send(:build_staff_from_block, self, block, index) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_single_staff_score(input)
|
|
50
|
+
clef = extract_clef(input)
|
|
51
|
+
key_tonic = extract_key_tonic(input)
|
|
52
|
+
mode = extract_mode(input)
|
|
53
|
+
time_numerator, time_denominator = extract_time(input)
|
|
54
|
+
tempo_unit, tempo_bpm = extract_tempo(input)
|
|
55
|
+
voice_streams = extract_voice_streams(first_braced_body(input), context: input)
|
|
56
|
+
parser = self
|
|
57
|
+
|
|
58
|
+
Clef.score(plugins: plugins) do
|
|
59
|
+
tempo beat_unit: tempo_unit, bpm: tempo_bpm if tempo_bpm
|
|
17
60
|
staff :staff1, clef: clef do
|
|
18
61
|
key key_tonic, mode
|
|
19
62
|
time time_numerator, time_denominator
|
|
20
|
-
|
|
63
|
+
parser.send(:add_voice_streams, self, voice_streams)
|
|
21
64
|
end
|
|
22
65
|
end
|
|
23
66
|
end
|
|
24
67
|
|
|
25
|
-
|
|
68
|
+
def build_staff_from_block(builder, block, index)
|
|
69
|
+
context = "#{block[:header]}\n#{block[:body]}"
|
|
70
|
+
clef = extract_clef(context)
|
|
71
|
+
key_tonic = extract_key_tonic(context)
|
|
72
|
+
mode = extract_mode(context)
|
|
73
|
+
time_numerator, time_denominator = extract_time(context)
|
|
74
|
+
voice_streams = extract_voice_streams(block[:body], context: context)
|
|
75
|
+
staff_id = :"staff#{index + 1}"
|
|
76
|
+
parser = self
|
|
77
|
+
|
|
78
|
+
builder.staff staff_id, clef: clef do
|
|
79
|
+
key key_tonic, mode
|
|
80
|
+
time time_numerator, time_denominator
|
|
81
|
+
parser.send(:add_voice_streams, self, voice_streams)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def add_voice_streams(builder, voice_streams)
|
|
86
|
+
streams = voice_streams.map { |stream| measure_segments(stream) }
|
|
87
|
+
measure_count = streams.map(&:length).max.to_i
|
|
88
|
+
measure_count.times do |measure_index|
|
|
89
|
+
builder.measure(measure_index + 1) do |staff_builder|
|
|
90
|
+
streams.each_with_index do |segments, voice_index|
|
|
91
|
+
segment = segments[measure_index]
|
|
92
|
+
next if segment.to_s.empty?
|
|
93
|
+
|
|
94
|
+
staff_builder.voice(voice_id_for(voice_index)) { notes segment }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def measure_segments(stream)
|
|
101
|
+
stream.to_s.split("|").map(&:strip).reject(&:empty?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def voice_id_for(index)
|
|
105
|
+
index.zero? ? :default : :"voice#{index + 1}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def extract_staff_blocks(input)
|
|
109
|
+
blocks = []
|
|
110
|
+
scanner_index = 0
|
|
111
|
+
while (match = input.match(STAFF_COMMAND, scanner_index))
|
|
112
|
+
command_start = match.begin(0)
|
|
113
|
+
brace_start = input.index("{", match.end(0))
|
|
114
|
+
break unless brace_start
|
|
115
|
+
|
|
116
|
+
body, body_end = braced_body_at(input, brace_start)
|
|
117
|
+
header = input[command_start...brace_start]
|
|
118
|
+
blocks << {header: header, body: body}
|
|
119
|
+
scanner_index = body_end + 1
|
|
120
|
+
end
|
|
121
|
+
blocks
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def staff_group_input?(input)
|
|
125
|
+
input.match?(/\\new\s+(?:StaffGroup|PianoStaff)\b/) || input.include?("<<")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def extract_voice_streams(body, context: body)
|
|
129
|
+
simultaneous = first_simultaneous_body(body)
|
|
130
|
+
streams = if simultaneous
|
|
131
|
+
braced_bodies(simultaneous)
|
|
132
|
+
elsif (inner = braced_bodies(body).first)
|
|
133
|
+
[inner]
|
|
134
|
+
else
|
|
135
|
+
[body]
|
|
136
|
+
end
|
|
137
|
+
streams.map { |stream| note_stream(stream, context: context) }.reject(&:empty?)
|
|
138
|
+
end
|
|
26
139
|
|
|
27
|
-
def
|
|
28
|
-
body =
|
|
140
|
+
def note_stream(input, context:)
|
|
141
|
+
body = context.match?(/\\relative\b/) ? relativize_body(context, input) : input
|
|
29
142
|
tokens = LilypondLexer.new.tokenize(body)
|
|
30
|
-
tokens.select
|
|
143
|
+
tokens.select do |token|
|
|
144
|
+
token.match?(/\A<|\A[a-g]|\Ar|\A\||\A[()~\[\]]|\A(?:--|->|-\.)\z/) || DYNAMIC_COMMANDS.include?(token)
|
|
145
|
+
end.join(" ")
|
|
31
146
|
end
|
|
32
147
|
|
|
33
148
|
def extract_key_tonic(input)
|
|
34
|
-
match = /\\key\s+([a-g](?:is|es)?)/.match(input)
|
|
149
|
+
match = /\\key\s+([a-g](?:isis|eses|is|es)?)/.match(input)
|
|
35
150
|
tonic = (match && match[1]) || "c"
|
|
36
|
-
Clef::Core::Pitch.
|
|
151
|
+
Clef::Core::Pitch.parse_any(tonic)
|
|
37
152
|
end
|
|
38
153
|
|
|
39
154
|
def extract_mode(input)
|
|
40
|
-
match = /\\key\s+[a-g](?:is|es)?\s+\\(major|minor)/.match(input)
|
|
155
|
+
match = /\\key\s+[a-g](?:isis|eses|is|es)?\s+\\(major|minor)/.match(input)
|
|
41
156
|
(match && match[1]&.to_sym) || :major
|
|
42
157
|
end
|
|
43
158
|
|
|
@@ -50,7 +165,106 @@ module Clef
|
|
|
50
165
|
|
|
51
166
|
def extract_clef(input)
|
|
52
167
|
match = /\\clef\s+"?([a-z_]+)"?/.match(input)
|
|
53
|
-
(match && match[1]&.to_sym) || :treble
|
|
168
|
+
clef = (match && match[1]&.to_sym) || :treble
|
|
169
|
+
return clef if Clef::Core::Clef::TYPES.include?(clef)
|
|
170
|
+
|
|
171
|
+
warnings << "unsupported clef ignored: #{clef}"
|
|
172
|
+
:treble
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_tempo(input)
|
|
176
|
+
match = /\\tempo\s+(\d+)\s*=\s*(\d+)/.match(input)
|
|
177
|
+
return [nil, nil] unless match
|
|
178
|
+
|
|
179
|
+
[Clef::Core::Duration.from_lilypond(match[1].to_i), match[2].to_i]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def unsupported_command_warnings(input)
|
|
183
|
+
seen = {}
|
|
184
|
+
LilypondLexer.new.tokenize_with_locations(input).select { |token| token.value.start_with?("\\") }.filter_map do |token|
|
|
185
|
+
command = token.value
|
|
186
|
+
next if SUPPORTED_COMMANDS.include?(command) || DYNAMIC_COMMANDS.include?(command)
|
|
187
|
+
next if seen[command]
|
|
188
|
+
|
|
189
|
+
seen[command] = true
|
|
190
|
+
"unsupported LilyPond command ignored at line #{token.line}, column #{token.column}: #{command}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def strip_comments(input)
|
|
195
|
+
input.to_s.gsub(/%\{.*?%\}/m, "").each_line.map { |line| line.sub(/%.*/, "") }.join
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def first_braced_body(input)
|
|
199
|
+
start_index = input.index("{")
|
|
200
|
+
return "" unless start_index
|
|
201
|
+
|
|
202
|
+
braced_body_at(input, start_index).first
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def braced_body_at(input, start_index)
|
|
206
|
+
depth = 0
|
|
207
|
+
index = start_index
|
|
208
|
+
while index < input.length
|
|
209
|
+
char = input[index]
|
|
210
|
+
depth += 1 if char == "{"
|
|
211
|
+
depth -= 1 if char == "}"
|
|
212
|
+
return [input[(start_index + 1)...index], index] if depth.zero?
|
|
213
|
+
|
|
214
|
+
index += 1
|
|
215
|
+
end
|
|
216
|
+
["", input.length]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def first_simultaneous_body(input)
|
|
220
|
+
start_index = input.index("<<")
|
|
221
|
+
return nil unless start_index
|
|
222
|
+
|
|
223
|
+
depth = 0
|
|
224
|
+
index = start_index
|
|
225
|
+
while index < input.length - 1
|
|
226
|
+
token = input[index, 2]
|
|
227
|
+
depth += 1 if token == "<<"
|
|
228
|
+
depth -= 1 if token == ">>"
|
|
229
|
+
return input[(start_index + 2)...index] if depth.zero?
|
|
230
|
+
|
|
231
|
+
index += 1
|
|
232
|
+
end
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def braced_bodies(input)
|
|
237
|
+
bodies = []
|
|
238
|
+
scanner_index = 0
|
|
239
|
+
while (brace_start = input.index("{", scanner_index))
|
|
240
|
+
body, body_end = braced_body_at(input, brace_start)
|
|
241
|
+
bodies << body
|
|
242
|
+
scanner_index = body_end + 1
|
|
243
|
+
end
|
|
244
|
+
bodies
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def relativize_body(input, body)
|
|
248
|
+
match = /\\relative\s+([a-g](?:isis|eses|is|es)?[',]*)/.match(input)
|
|
249
|
+
return body unless match
|
|
250
|
+
|
|
251
|
+
previous = Clef::Core::Pitch.parse(match[1])
|
|
252
|
+
LilypondLexer.new.tokenize(body).map do |token|
|
|
253
|
+
if (note_match = /\A([a-g](?:isis|eses|is|es)?)(\d*\.*~?)\z/.match(token))
|
|
254
|
+
previous = closest_relative_pitch(note_match[1], previous)
|
|
255
|
+
"#{previous.to_lilypond}#{note_match[2]}"
|
|
256
|
+
else
|
|
257
|
+
token
|
|
258
|
+
end
|
|
259
|
+
end.join(" ")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def closest_relative_pitch(note_name, previous)
|
|
263
|
+
base = Clef::Core::Pitch.parse(note_name)
|
|
264
|
+
candidates = ((previous.octave - 2)..(previous.octave + 2)).map do |octave|
|
|
265
|
+
Clef::Core::Pitch.new(base.note_name, octave, alteration: base.alteration)
|
|
266
|
+
end
|
|
267
|
+
candidates.min_by { |candidate| (candidate.semitones - previous.semitones).abs }
|
|
54
268
|
end
|
|
55
269
|
end
|
|
56
270
|
end
|
data/lib/clef/plugins/base.rb
CHANGED
|
@@ -11,16 +11,36 @@ module Clef
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# @param _score [Clef::Core::Score]
|
|
14
|
-
def on_before_layout(_score)
|
|
14
|
+
def on_before_layout(_score)
|
|
15
|
+
end
|
|
15
16
|
|
|
16
17
|
# @param _layout_result [Hash]
|
|
17
|
-
def on_after_layout(_layout_result)
|
|
18
|
+
def on_after_layout(_layout_result)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param _items [Array<Clef::Layout::Item>]
|
|
22
|
+
def on_layout_items(_items)
|
|
23
|
+
end
|
|
18
24
|
|
|
19
25
|
# @param _renderer [Object]
|
|
20
|
-
def on_before_render(_renderer)
|
|
26
|
+
def on_before_render(_renderer)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param _path [String, #write]
|
|
30
|
+
def on_after_render(_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param _score [Clef::Core::Score]
|
|
34
|
+
def on_after_parse(_score)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param _exporter [Clef::Midi::Exporter]
|
|
38
|
+
def on_before_midi(_exporter)
|
|
39
|
+
end
|
|
21
40
|
|
|
22
41
|
# @param _glyph_table [Clef::Engraving::GlyphTable]
|
|
23
|
-
def register_glyphs(_glyph_table)
|
|
42
|
+
def register_glyphs(_glyph_table)
|
|
43
|
+
end
|
|
24
44
|
end
|
|
25
45
|
end
|
|
26
46
|
end
|
|
@@ -3,22 +3,53 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Plugins
|
|
5
5
|
class Registry
|
|
6
|
-
|
|
6
|
+
ERROR_POLICIES = %i[raise warn collect].freeze
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
attr_reader :errors
|
|
9
|
+
|
|
10
|
+
# @param error_policy [Symbol]
|
|
11
|
+
def initialize(error_policy: :raise)
|
|
12
|
+
raise ArgumentError, "unsupported hook error policy" unless ERROR_POLICIES.include?(error_policy)
|
|
13
|
+
|
|
14
|
+
@entries = []
|
|
15
|
+
@next_order = 0
|
|
16
|
+
@error_policy = error_policy
|
|
17
|
+
@errors = []
|
|
10
18
|
end
|
|
11
19
|
|
|
12
|
-
# @
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
# @return [Array<Plugins::Base>]
|
|
21
|
+
def plugins
|
|
22
|
+
@entries.map { |entry| entry[:plugin] }
|
|
23
|
+
end
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
# @param plugin_or_class [Class, Plugins::Base]
|
|
26
|
+
# @param args [Array]
|
|
27
|
+
# @param priority [Integer]
|
|
28
|
+
# @param kwargs [Hash]
|
|
29
|
+
# @return [Plugins::Base]
|
|
30
|
+
def register(plugin_or_class, *args, priority: 0, **kwargs)
|
|
31
|
+
plugin = build_plugin(plugin_or_class, *args, **kwargs)
|
|
32
|
+
@entries << {plugin: plugin, priority: priority, order: @next_order}
|
|
33
|
+
@next_order += 1
|
|
34
|
+
@entries.sort_by! { |entry| [entry[:priority], entry[:order]] }
|
|
19
35
|
plugin
|
|
20
36
|
end
|
|
21
37
|
|
|
38
|
+
# @param plugin_or_class [Class, Plugins::Base, String, Symbol]
|
|
39
|
+
# @return [Plugins::Base, nil]
|
|
40
|
+
def unregister(plugin_or_class)
|
|
41
|
+
entry = @entries.find { |candidate| plugin_match?(candidate[:plugin], plugin_or_class) }
|
|
42
|
+
return unless entry
|
|
43
|
+
|
|
44
|
+
@entries.delete(entry)
|
|
45
|
+
entry[:plugin]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Array]
|
|
49
|
+
def clear
|
|
50
|
+
@entries.clear
|
|
51
|
+
end
|
|
52
|
+
|
|
22
53
|
# @param hook_name [Symbol]
|
|
23
54
|
# @param args [Array<Object>]
|
|
24
55
|
# @return [Array<Object>]
|
|
@@ -26,7 +57,46 @@ module Clef
|
|
|
26
57
|
plugins.each_with_object([]) do |plugin, results|
|
|
27
58
|
next unless plugin.respond_to?(hook_name)
|
|
28
59
|
|
|
29
|
-
results << plugin
|
|
60
|
+
results << run_plugin_hook(plugin, hook_name, args)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_plugin(plugin_or_class, *args, **kwargs)
|
|
67
|
+
if plugin_or_class.is_a?(Class)
|
|
68
|
+
raise ArgumentError, "plugin must inherit Clef::Plugins::Base" unless plugin_or_class < Base
|
|
69
|
+
|
|
70
|
+
return plugin_or_class.new(*args, **kwargs)
|
|
71
|
+
end
|
|
72
|
+
return plugin_or_class if plugin_or_class.is_a?(Base)
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, "plugin must inherit Clef::Plugins::Base"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def plugin_match?(plugin, candidate)
|
|
78
|
+
return plugin.equal?(candidate) if candidate.is_a?(Base)
|
|
79
|
+
return plugin.is_a?(candidate) if candidate.is_a?(Class)
|
|
80
|
+
|
|
81
|
+
plugin.class.plugin_name.to_s == candidate.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def run_plugin_hook(plugin, hook_name, args)
|
|
85
|
+
plugin.public_send(hook_name, *args)
|
|
86
|
+
rescue => e
|
|
87
|
+
handle_hook_error(plugin, hook_name, e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def handle_hook_error(plugin, hook_name, error)
|
|
91
|
+
case @error_policy
|
|
92
|
+
when :raise
|
|
93
|
+
raise error
|
|
94
|
+
when :warn
|
|
95
|
+
warn("Clef plugin #{plugin.class.plugin_name} #{hook_name} failed: #{error.message}")
|
|
96
|
+
nil
|
|
97
|
+
when :collect
|
|
98
|
+
errors << {plugin: plugin, hook: hook_name, error: error}
|
|
99
|
+
nil
|
|
30
100
|
end
|
|
31
101
|
end
|
|
32
102
|
end
|
data/lib/clef/renderer/base.rb
CHANGED
|
@@ -9,8 +9,8 @@ module Clef
|
|
|
9
9
|
# @param glyph_table [Clef::Engraving::GlyphTable]
|
|
10
10
|
# @param font_manager [Clef::Engraving::FontManager]
|
|
11
11
|
def initialize(style: Clef::Engraving::Style.default,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
glyph_table: Clef::Engraving::GlyphTable.new,
|
|
13
|
+
font_manager: Clef::Engraving::FontManager.new)
|
|
14
14
|
@style = style
|
|
15
15
|
@glyph_table = glyph_table
|
|
16
16
|
@font_manager = font_manager
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Renderer
|
|
5
|
+
class DrawingContext
|
|
6
|
+
attr_reader :vertical_axis
|
|
7
|
+
|
|
8
|
+
# @param vertical_axis [Integer]
|
|
9
|
+
def initialize(vertical_axis:)
|
|
10
|
+
raise ArgumentError, "vertical_axis must be 1 or -1" unless [1, -1].include?(vertical_axis)
|
|
11
|
+
|
|
12
|
+
@vertical_axis = vertical_axis
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [DrawingContext]
|
|
16
|
+
def self.pdf
|
|
17
|
+
new(vertical_axis: 1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [DrawingContext]
|
|
21
|
+
def self.svg
|
|
22
|
+
new(vertical_axis: -1)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -6,15 +6,30 @@ module Clef
|
|
|
6
6
|
ACCIDENTAL_GLYPH_KEYS = {
|
|
7
7
|
-2 => :accidental_double_flat,
|
|
8
8
|
-1 => :accidental_flat,
|
|
9
|
+
0 => :accidental_natural,
|
|
9
10
|
1 => :accidental_sharp,
|
|
10
11
|
2 => :accidental_double_sharp
|
|
11
12
|
}.freeze
|
|
12
13
|
ACCIDENTAL_FALLBACK = {
|
|
13
14
|
-2 => "bb",
|
|
14
15
|
-1 => "b",
|
|
16
|
+
0 => "n",
|
|
15
17
|
1 => "#",
|
|
16
18
|
2 => "##"
|
|
17
19
|
}.freeze
|
|
20
|
+
SHARP_ORDER = %i[f c g d a e b].freeze
|
|
21
|
+
FLAT_ORDER = %i[b e a d g c f].freeze
|
|
22
|
+
REST_LABELS = {
|
|
23
|
+
whole: "W",
|
|
24
|
+
half: "H",
|
|
25
|
+
quarter: "r",
|
|
26
|
+
eighth: "8",
|
|
27
|
+
sixteenth: "16",
|
|
28
|
+
thirty_second: "32",
|
|
29
|
+
sixty_fourth: "64",
|
|
30
|
+
one_twenty_eighth: "128",
|
|
31
|
+
two_fifty_sixth: "256"
|
|
32
|
+
}.freeze
|
|
18
33
|
|
|
19
34
|
private
|
|
20
35
|
|
|
@@ -26,6 +41,14 @@ module Clef
|
|
|
26
41
|
duration.base != :whole
|
|
27
42
|
end
|
|
28
43
|
|
|
44
|
+
def flag_required?(duration)
|
|
45
|
+
%i[eighth sixteenth thirty_second sixty_fourth one_twenty_eighth two_fifty_sixth].include?(duration.base)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def beamable?(element)
|
|
49
|
+
element.is_a?(Clef::Core::Note) && flag_required?(element.duration)
|
|
50
|
+
end
|
|
51
|
+
|
|
29
52
|
def duration_spacing(element)
|
|
30
53
|
style.min_note_spacing * (element.length.to_f / Rational(1, 4).to_f)
|
|
31
54
|
end
|
|
@@ -54,7 +77,7 @@ module Clef
|
|
|
54
77
|
end
|
|
55
78
|
|
|
56
79
|
def chord_notes(chord)
|
|
57
|
-
chord.
|
|
80
|
+
chord.sorted_pitches.map { |pitch| Clef::Core::Note.new(pitch, chord.duration) }
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
def chord_stem_anchor_note(notes, direction)
|
|
@@ -66,6 +89,74 @@ module Clef
|
|
|
66
89
|
def chord_stem_length(notes, clef, direction)
|
|
67
90
|
notes.map { |note| Clef::Layout::Stem.length(note, clef, direction) }.max
|
|
68
91
|
end
|
|
92
|
+
|
|
93
|
+
def key_signature_alterations(key_signature)
|
|
94
|
+
return {} unless key_signature
|
|
95
|
+
|
|
96
|
+
accidentals = key_signature.accidentals
|
|
97
|
+
order = (accidentals[:type] == :sharp) ? SHARP_ORDER : FLAT_ORDER
|
|
98
|
+
alteration = (accidentals[:type] == :sharp) ? 1 : -1
|
|
99
|
+
order.first(accidentals[:count].to_i).to_h { |note_name| [note_name, alteration] }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def accidental_for_pitch(pitch, key_signature, state)
|
|
103
|
+
expected = key_signature_alterations(key_signature)[pitch.note_name] || 0
|
|
104
|
+
current = state.fetch(pitch.note_name, expected)
|
|
105
|
+
return nil if current == pitch.alteration
|
|
106
|
+
|
|
107
|
+
state[pitch.note_name] = pitch.alteration
|
|
108
|
+
pitch.alteration
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def rest_label(duration)
|
|
112
|
+
REST_LABELS.fetch(duration.base)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def element_notes(element)
|
|
116
|
+
case element
|
|
117
|
+
when Clef::Core::Note then [element]
|
|
118
|
+
when Clef::Core::Chord then chord_notes(element)
|
|
119
|
+
when Clef::Core::Tuplet then element.elements.flat_map { |child| element_notes(child) }
|
|
120
|
+
else []
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def dynamic_text(dynamic)
|
|
125
|
+
dynamic.type.to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def flatten_elements(elements)
|
|
129
|
+
elements.flat_map do |element|
|
|
130
|
+
element.is_a?(Clef::Core::Tuplet) ? flatten_elements(element.elements) : element
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def lyric_elements(elements)
|
|
135
|
+
flatten_elements(elements).select do |element|
|
|
136
|
+
element.is_a?(Clef::Core::Note) || element.is_a?(Clef::Core::Chord)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def lyric_events(lyric, elements)
|
|
141
|
+
note_index = 0
|
|
142
|
+
previous_element = nil
|
|
143
|
+
lyric.syllables.filter_map do |syllable|
|
|
144
|
+
if Clef::Notation::Lyric.hyphen?(syllable)
|
|
145
|
+
next {type: :hyphen, from: previous_element, to: elements[note_index]}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
element = elements[note_index]
|
|
149
|
+
note_index += 1
|
|
150
|
+
if Clef::Notation::Lyric.extender?(syllable)
|
|
151
|
+
event = {type: :extender, from: previous_element, to: element}
|
|
152
|
+
previous_element = element if element
|
|
153
|
+
next event
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
previous_element = element if element
|
|
157
|
+
{type: :text, syllable: syllable, element: element}
|
|
158
|
+
end
|
|
159
|
+
end
|
|
69
160
|
end
|
|
70
161
|
end
|
|
71
162
|
end
|