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,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Parser
|
|
5
|
+
module DSL
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
class ScoreBuilder
|
|
9
|
+
attr_reader :score
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@score = Clef::Core::Score.new
|
|
13
|
+
@default_group = Clef::Core::StaffGroup.new([], bracket_type: :none)
|
|
14
|
+
score.add_staff_group(@default_group)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param value [String]
|
|
18
|
+
def title(value)
|
|
19
|
+
score.title = value.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param value [String]
|
|
23
|
+
def composer(value)
|
|
24
|
+
score.composer = value.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param beat_unit [Symbol, Clef::Core::Duration]
|
|
28
|
+
# @param bpm [Integer]
|
|
29
|
+
def tempo(beat_unit:, bpm:)
|
|
30
|
+
duration = beat_unit.is_a?(Clef::Core::Duration) ? beat_unit : Clef::Core::Duration.new(beat_unit)
|
|
31
|
+
score.tempo = Clef::Core::Tempo.new(beat_unit: duration, bpm: bpm)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param id [Symbol]
|
|
35
|
+
# @param name [String, nil]
|
|
36
|
+
# @param clef [Symbol]
|
|
37
|
+
def staff(id, name: nil, clef: :treble, &block)
|
|
38
|
+
staff = Clef::Core::Staff.new(id, name: name, clef: Clef::Core::Clef.new(clef))
|
|
39
|
+
build_staff(staff, &block)
|
|
40
|
+
@default_group.add_staff(staff)
|
|
41
|
+
staff
|
|
42
|
+
rescue NoMethodError => e
|
|
43
|
+
raise Error, "invalid DSL method in staff block: #{e.name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param bracket_type [Symbol]
|
|
47
|
+
def staff_group(bracket_type, &block)
|
|
48
|
+
group = Clef::Core::StaffGroup.new([], bracket_type: bracket_type)
|
|
49
|
+
GroupBuilder.new(group).instance_eval(&block)
|
|
50
|
+
score.add_staff_group(group)
|
|
51
|
+
group
|
|
52
|
+
rescue NoMethodError => e
|
|
53
|
+
raise Error, "invalid DSL method in staff_group block: #{e.name}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Clef::Core::Score]
|
|
57
|
+
def build
|
|
58
|
+
score
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_staff(staff, &block)
|
|
64
|
+
return unless block
|
|
65
|
+
|
|
66
|
+
StaffBuilder.new(staff).instance_eval(&block)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class GroupBuilder
|
|
71
|
+
def initialize(group)
|
|
72
|
+
@group = group
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param id [Symbol]
|
|
76
|
+
# @param name [String, nil]
|
|
77
|
+
# @param clef [Symbol]
|
|
78
|
+
def staff(id, name: nil, clef: :treble, &block)
|
|
79
|
+
staff = Clef::Core::Staff.new(id, name: name, clef: Clef::Core::Clef.new(clef))
|
|
80
|
+
StaffBuilder.new(staff).instance_eval(&block) if block
|
|
81
|
+
@group.add_staff(staff)
|
|
82
|
+
staff
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class StaffBuilder
|
|
87
|
+
def initialize(staff)
|
|
88
|
+
@staff = staff
|
|
89
|
+
@current_measure = nil
|
|
90
|
+
@next_measure_number = 1
|
|
91
|
+
@lyrics = []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param tonic [Symbol, Clef::Core::Pitch]
|
|
95
|
+
# @param mode [Symbol]
|
|
96
|
+
def key(tonic, mode = :major)
|
|
97
|
+
@staff.key_signature = Clef::Core::KeySignature.new(tonic, mode)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @param numerator [Integer]
|
|
101
|
+
# @param denominator [Integer]
|
|
102
|
+
def time(numerator, denominator)
|
|
103
|
+
@staff.time_signature = Clef::Core::TimeSignature.new(numerator, denominator)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @param id [Symbol]
|
|
107
|
+
def voice(id = :default, &block)
|
|
108
|
+
measure = ensure_measure
|
|
109
|
+
voice = measure.voice(id)
|
|
110
|
+
return voice unless block
|
|
111
|
+
|
|
112
|
+
VoiceBuilder.new(voice).instance_eval(&block)
|
|
113
|
+
voice
|
|
114
|
+
rescue NoMethodError => e
|
|
115
|
+
raise Error, "invalid DSL method in voice block: #{e.name}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param lilypond_string [String]
|
|
119
|
+
def play(lilypond_string)
|
|
120
|
+
segments = split_measures(lilypond_string)
|
|
121
|
+
segments.each_with_index do |segment, idx|
|
|
122
|
+
measure = ensure_measure
|
|
123
|
+
VoiceBuilder.new(measure.voice(:default)).notes(segment)
|
|
124
|
+
advance_measure if idx < segments.length - 1
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param voice_id [Symbol]
|
|
129
|
+
# @param text [String]
|
|
130
|
+
def lyrics(voice_id, text)
|
|
131
|
+
@lyrics << Clef::Notation::Lyric.new(voice_id, text)
|
|
132
|
+
@staff.metadata ||= {}
|
|
133
|
+
@staff.metadata[:lyrics] = @lyrics
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def ensure_measure
|
|
139
|
+
return @current_measure if @current_measure
|
|
140
|
+
|
|
141
|
+
@current_measure = Clef::Core::Measure.new(@next_measure_number, time_signature: @staff.time_signature)
|
|
142
|
+
@current_measure.key_signature = @staff.key_signature
|
|
143
|
+
@current_measure.clef = @staff.clef
|
|
144
|
+
@staff.add_measure(@current_measure)
|
|
145
|
+
@next_measure_number += 1
|
|
146
|
+
@current_measure
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def advance_measure
|
|
150
|
+
@current_measure = nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def split_measures(input)
|
|
154
|
+
input.to_s.split("|").map(&:strip).reject(&:empty?)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class VoiceBuilder
|
|
159
|
+
def initialize(voice)
|
|
160
|
+
@voice = voice
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @param pitch_str [String]
|
|
164
|
+
# @param duration_sym [Symbol]
|
|
165
|
+
# @param opts [Hash]
|
|
166
|
+
def note(pitch_str, duration_sym, **opts)
|
|
167
|
+
pitch = parse_pitch(pitch_str)
|
|
168
|
+
duration = Clef::Core::Duration.new(duration_sym, dots: opts.fetch(:dots, 0))
|
|
169
|
+
articulations = Array(opts.fetch(:articulations, []))
|
|
170
|
+
@voice.add(Clef::Core::Note.new(pitch, duration, articulations: articulations, tied: opts[:tied]))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @param duration_sym [Symbol]
|
|
174
|
+
# @param opts [Hash]
|
|
175
|
+
def rest(duration_sym, **opts)
|
|
176
|
+
duration = Clef::Core::Duration.new(duration_sym, dots: opts.fetch(:dots, 0))
|
|
177
|
+
@voice.add(Clef::Core::Rest.new(duration))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @param pitch_strs [Array<String>]
|
|
181
|
+
# @param duration_sym [Symbol]
|
|
182
|
+
# @param opts [Hash]
|
|
183
|
+
def chord(pitch_strs, duration_sym, **opts)
|
|
184
|
+
duration = Clef::Core::Duration.new(duration_sym, dots: opts.fetch(:dots, 0))
|
|
185
|
+
pitches = pitch_strs.map { |pitch| parse_pitch(pitch) }
|
|
186
|
+
@voice.add(Clef::Core::Chord.new(pitches, duration))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @param lilypond_string [String]
|
|
190
|
+
def notes(lilypond_string)
|
|
191
|
+
parse_tokens(lilypond_string).each { |token| add_token(token) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @param actual [Integer]
|
|
195
|
+
# @param normal [Integer]
|
|
196
|
+
def tuplet(actual, normal)
|
|
197
|
+
raise ArgumentError, "tuplet values must be positive" unless actual.positive? && normal.positive?
|
|
198
|
+
|
|
199
|
+
return unless block_given?
|
|
200
|
+
|
|
201
|
+
yield
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def parse_tokens(input)
|
|
207
|
+
input.to_s.split(/\s+/).reject(&:empty?)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def add_token(token)
|
|
211
|
+
return if token == "|"
|
|
212
|
+
|
|
213
|
+
if token.start_with?("r")
|
|
214
|
+
add_rest_token(token)
|
|
215
|
+
elsif token.start_with?("<")
|
|
216
|
+
add_chord_token(token)
|
|
217
|
+
else
|
|
218
|
+
add_note_token(token)
|
|
219
|
+
end
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
raise Error, "failed to parse token '#{token}': #{e.message}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def add_rest_token(token)
|
|
225
|
+
match = /\Ar(\d+)(\.*)\z/.match(token)
|
|
226
|
+
raise ArgumentError, "invalid rest token" unless match
|
|
227
|
+
|
|
228
|
+
duration = Clef::Core::Duration.from_lilypond(match[1].to_i, match[2].length)
|
|
229
|
+
@voice.add(Clef::Core::Rest.new(duration))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def add_chord_token(token)
|
|
233
|
+
match = /\A<([^>]+)>(\d+)(\.*)\z/.match(token)
|
|
234
|
+
raise ArgumentError, "invalid chord token" unless match
|
|
235
|
+
|
|
236
|
+
pitches = match[1].split(/\s+/).map { |value| parse_pitch(value) }
|
|
237
|
+
duration = Clef::Core::Duration.from_lilypond(match[2].to_i, match[3].length)
|
|
238
|
+
@voice.add(Clef::Core::Chord.new(pitches, duration))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def add_note_token(token)
|
|
242
|
+
match = /\A([a-g](?:isis|eses|is|es)?[',]*)(\d+)(\.*)\z/.match(token)
|
|
243
|
+
raise ArgumentError, "invalid note token" unless match
|
|
244
|
+
|
|
245
|
+
pitch = parse_pitch(match[1])
|
|
246
|
+
duration = Clef::Core::Duration.from_lilypond(match[2].to_i, match[3].length)
|
|
247
|
+
@voice.add(Clef::Core::Note.new(pitch, duration))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def parse_pitch(value)
|
|
251
|
+
parse_scientific_pitch(value) || Clef::Core::Pitch.parse(value.downcase)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def parse_scientific_pitch(value)
|
|
255
|
+
match = /\A([A-Ga-g])([#b]{0,2})(-?\d+)\z/.match(value)
|
|
256
|
+
return nil unless match
|
|
257
|
+
|
|
258
|
+
note_name = match[1].downcase.to_sym
|
|
259
|
+
alteration = { "" => 0, "#" => 1, "##" => 2, "b" => -1, "bb" => -2 }.fetch(match[2])
|
|
260
|
+
Clef::Core::Pitch.new(note_name, match[3].to_i, alteration: alteration)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Parser
|
|
5
|
+
class LilypondLexer
|
|
6
|
+
TOKEN_REGEX = /\\[a-zA-Z]+|\{\}|\{|\}|<[^>]+>\d+\.*|[a-g](?:isis|eses|is|es)?[',]*\d+\.*|r\d+\.*|\|/m
|
|
7
|
+
|
|
8
|
+
# @param input [String]
|
|
9
|
+
# @return [Array<String>]
|
|
10
|
+
def tokenize(input)
|
|
11
|
+
sanitized = strip_comments(input)
|
|
12
|
+
sanitized.scan(TOKEN_REGEX)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def strip_comments(input)
|
|
18
|
+
input.to_s.each_line.map { |line| line.sub(/%.*/, "") }.join
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Parser
|
|
5
|
+
class LilypondParser
|
|
6
|
+
# @param input [String]
|
|
7
|
+
# @return [Clef::Core::Score]
|
|
8
|
+
def parse(input)
|
|
9
|
+
cleaned = input.to_s
|
|
10
|
+
clef = extract_clef(cleaned)
|
|
11
|
+
key_tonic = extract_key_tonic(cleaned)
|
|
12
|
+
mode = extract_mode(cleaned)
|
|
13
|
+
time_numerator, time_denominator = extract_time(cleaned)
|
|
14
|
+
note_stream = extract_note_stream(cleaned)
|
|
15
|
+
|
|
16
|
+
Clef.score do
|
|
17
|
+
staff :staff1, clef: clef do
|
|
18
|
+
key key_tonic, mode
|
|
19
|
+
time time_numerator, time_denominator
|
|
20
|
+
play note_stream
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def extract_note_stream(input)
|
|
28
|
+
body = input[/\{(.+)\}/m, 1] || ""
|
|
29
|
+
tokens = LilypondLexer.new.tokenize(body)
|
|
30
|
+
tokens.select { |token| token.match?(/\A<|\A[a-g]|\Ar|\A\|/) }.join(" ")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extract_key_tonic(input)
|
|
34
|
+
match = /\\key\s+([a-g](?:is|es)?)/.match(input)
|
|
35
|
+
tonic = (match && match[1]) || "c"
|
|
36
|
+
Clef::Core::Pitch.parse(tonic)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_mode(input)
|
|
40
|
+
match = /\\key\s+[a-g](?:is|es)?\s+\\(major|minor)/.match(input)
|
|
41
|
+
(match && match[1]&.to_sym) || :major
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_time(input)
|
|
45
|
+
match = /\\time\s+(\d+)\/(\d+)/.match(input)
|
|
46
|
+
return [4, 4] unless match
|
|
47
|
+
|
|
48
|
+
[match[1].to_i, match[2].to_i]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def extract_clef(input)
|
|
52
|
+
match = /\\clef\s+"?([a-z_]+)"?/.match(input)
|
|
53
|
+
(match && match[1]&.to_sym) || :treble
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Plugins
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
# @return [String]
|
|
8
|
+
def plugin_name
|
|
9
|
+
name.split("::").last.downcase
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param _score [Clef::Core::Score]
|
|
14
|
+
def on_before_layout(_score); end
|
|
15
|
+
|
|
16
|
+
# @param _layout_result [Hash]
|
|
17
|
+
def on_after_layout(_layout_result); end
|
|
18
|
+
|
|
19
|
+
# @param _renderer [Object]
|
|
20
|
+
def on_before_render(_renderer); end
|
|
21
|
+
|
|
22
|
+
# @param _glyph_table [Clef::Engraving::GlyphTable]
|
|
23
|
+
def register_glyphs(_glyph_table); end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Plugins
|
|
5
|
+
class Registry
|
|
6
|
+
attr_reader :plugins
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@plugins = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param plugin_class [Class]
|
|
13
|
+
# @return [Plugins::Base]
|
|
14
|
+
def register(plugin_class)
|
|
15
|
+
raise ArgumentError, "plugin must inherit Clef::Plugins::Base" unless plugin_class < Base
|
|
16
|
+
|
|
17
|
+
plugin = plugin_class.new
|
|
18
|
+
plugins << plugin
|
|
19
|
+
plugin
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param hook_name [Symbol]
|
|
23
|
+
# @param args [Array<Object>]
|
|
24
|
+
# @return [Array<Object>]
|
|
25
|
+
def run_hook(hook_name, *args)
|
|
26
|
+
plugins.each_with_object([]) do |plugin, results|
|
|
27
|
+
next unless plugin.respond_to?(hook_name)
|
|
28
|
+
|
|
29
|
+
results << plugin.public_send(hook_name, *args)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Renderer
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :style, :glyph_table, :font_manager
|
|
7
|
+
|
|
8
|
+
# @param style [Clef::Engraving::Style]
|
|
9
|
+
# @param glyph_table [Clef::Engraving::GlyphTable]
|
|
10
|
+
# @param font_manager [Clef::Engraving::FontManager]
|
|
11
|
+
def initialize(style: Clef::Engraving::Style.default,
|
|
12
|
+
glyph_table: Clef::Engraving::GlyphTable.new,
|
|
13
|
+
font_manager: Clef::Engraving::FontManager.new)
|
|
14
|
+
@style = style
|
|
15
|
+
@glyph_table = glyph_table
|
|
16
|
+
@font_manager = font_manager
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param _score [Clef::Core::Score]
|
|
20
|
+
# @param _path [String]
|
|
21
|
+
def render(_score, _path, **_options)
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clef
|
|
4
|
+
module Renderer
|
|
5
|
+
module NotationHelpers
|
|
6
|
+
ACCIDENTAL_GLYPH_KEYS = {
|
|
7
|
+
-2 => :accidental_double_flat,
|
|
8
|
+
-1 => :accidental_flat,
|
|
9
|
+
1 => :accidental_sharp,
|
|
10
|
+
2 => :accidental_double_sharp
|
|
11
|
+
}.freeze
|
|
12
|
+
ACCIDENTAL_FALLBACK = {
|
|
13
|
+
-2 => "bb",
|
|
14
|
+
-1 => "b",
|
|
15
|
+
1 => "#",
|
|
16
|
+
2 => "##"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def filled_notehead?(duration)
|
|
22
|
+
!%i[whole half].include?(duration.base)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stem_required?(duration)
|
|
26
|
+
duration.base != :whole
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def duration_spacing(element)
|
|
30
|
+
style.min_note_spacing * (element.length.to_f / Rational(1, 4).to_f)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def diatonic_step(pitch)
|
|
34
|
+
note_index = Clef::Core::Pitch::VALID_NOTE_NAMES.index(pitch.note_name)
|
|
35
|
+
(pitch.octave * 7) + note_index
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The staff baseline is the top line.
|
|
39
|
+
# vertical_axis: +1 for PDF coordinates, -1 for SVG coordinates.
|
|
40
|
+
def calculate_pitch_y(pitch, baseline, clef, vertical_axis:)
|
|
41
|
+
reference = clef.reference_pitch
|
|
42
|
+
diatonic = diatonic_step(pitch) - diatonic_step(reference)
|
|
43
|
+
bottom_line = baseline - (vertical_axis * style.staff_space * 4)
|
|
44
|
+
reference_y = bottom_line + (clef.reference_line * vertical_axis * style.staff_space)
|
|
45
|
+
reference_y - (diatonic * (style.staff_space / 2.0))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def accidental_glyph_key(alteration)
|
|
49
|
+
ACCIDENTAL_GLYPH_KEYS[alteration]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def accidental_text(alteration)
|
|
53
|
+
ACCIDENTAL_FALLBACK.fetch(alteration)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def chord_notes(chord)
|
|
57
|
+
chord.pitches.map { |pitch| Clef::Core::Note.new(pitch, chord.duration) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def chord_stem_anchor_note(notes, direction)
|
|
61
|
+
return notes.min_by { |note| note.pitch.to_midi } if direction == :up
|
|
62
|
+
|
|
63
|
+
notes.max_by { |note| note.pitch.to_midi }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def chord_stem_length(notes, clef, direction)
|
|
67
|
+
notes.map { |note| Clef::Layout::Stem.length(note, clef, direction) }.max
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|