lydown 0.3.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.
@@ -0,0 +1,19 @@
1
+ module Lydown::Rendering
2
+ module Movement
3
+ def self.movement_title(work, name)
4
+ return nil if name.nil? || name.empty?
5
+
6
+ if name =~ /^(?:([0-9])+([a-z]*))\-(.+)$/
7
+ title = "#{$1.to_i}#{$2}. #{$3.capitalize}"
8
+ else
9
+ title = name
10
+ end
11
+
12
+ if work["movements/#{name}/parts"].empty?
13
+ title += " - tacet"
14
+ end
15
+
16
+ title
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,362 @@
1
+ require 'lydown/rendering/figures'
2
+
3
+ module Lydown::Rendering
4
+ module Accidentals
5
+ KEY_CYCLE = %w{c- g- d- a- e- b- f c g d a e b f+ c+ g+ d+ a+ e+ b+}
6
+ C_IDX = KEY_CYCLE.index('c'); A_IDX = KEY_CYCLE.index('a')
7
+ SHARPS_IDX = KEY_CYCLE.index('f+'); FLATS_IDX = KEY_CYCLE.index('b-')
8
+ KEY_ACCIDENTALS = {}
9
+
10
+ def self.accidentals_for_key_signature(signature)
11
+ KEY_ACCIDENTALS[signature] ||= calc_accidentals_for_key_signature(signature)
12
+ end
13
+
14
+ def self.calc_accidentals_for_key_signature(signature)
15
+ unless signature =~ /^([a-g][\+\-]*) (major|minor)$/
16
+ raise "Invalid key signature #{signature.inspect}"
17
+ end
18
+
19
+ key = $1; mode = $2
20
+
21
+ # calculate offset from c major / a minor
22
+ base_idx = (mode == 'major') ? C_IDX : A_IDX
23
+ offset = KEY_CYCLE.index(key) - base_idx
24
+
25
+ if offset >= 0
26
+ calc_accidentals_map(KEY_CYCLE[SHARPS_IDX, offset])
27
+ else
28
+ calc_accidentals_map(KEY_CYCLE[FLATS_IDX + offset + 1, -offset])
29
+ end
30
+ end
31
+
32
+ def self.calc_accidentals_map(accidentals)
33
+ accidentals.inject({}) { |h, a| h[a[0]] = (a[1] == '+') ? 1 : -1; h}
34
+ end
35
+
36
+ def self.lilypond_note_name(note, key_signature = 'c major')
37
+ value = 0
38
+ # accidental value from note
39
+ note = note.gsub(/[\-\+]/) { |c| value += (c == '+') ? 1 : -1; '' }
40
+ # add key signature value
41
+ value += accidentals_for_key_signature(key_signature)[note] || 0
42
+
43
+ note + (value >= 0 ? 'is' * value : 'es' * -value)
44
+ end
45
+
46
+ # Takes into account the accidentals mode
47
+ def self.translate_note_name(work, note)
48
+ if work[:accidentals] == 'manual'
49
+ key = 'c major'
50
+ else
51
+ key = work[:key]
52
+ end
53
+ lilypond_note_name(note, key)
54
+ end
55
+ end
56
+
57
+ module Notes
58
+ include Lydown::Rendering::Figures
59
+
60
+ def add_note(event)
61
+ return add_macro_note(event) if @work['process/duration_macro']
62
+
63
+ @work['process/last_note_head'] = event[:head]
64
+
65
+ value = @work['process/duration_values'].first
66
+ @work['process/duration_values'].rotate!
67
+
68
+ add_figures(event[:figures], value) if event[:figures]
69
+
70
+ # push value into running values accumulator. This is used to synthesize
71
+ # the bass figures durations.
72
+ unless event[:figures]
73
+ @work['process/running_values'] ||= []
74
+ if event[:rest_value]
75
+ @work['process/running_values'] << event[:rest_value]
76
+ else
77
+ @work['process/running_values'] << value
78
+ end
79
+ end
80
+
81
+ # only add the value if different than the last used
82
+ if value == @work['process/last_value']
83
+ value = ''
84
+ else
85
+ @work['process/last_value'] = value
86
+ end
87
+
88
+ @work.emit(event[:stream] || :music, lilypond_note(event, value: value))
89
+ end
90
+
91
+ def lilypond_note(event, options = {})
92
+ head = Accidentals.translate_note_name(@work, event[:head])
93
+ if options[:head_only]
94
+ head
95
+ else
96
+ "%s%s%s%s%s%s " % [
97
+ head,
98
+ event[:octave],
99
+ event[:accidental_flag],
100
+ options[:value],
101
+ lilypond_phrasing(event),
102
+ event[:expressions] ? event[:expressions].join : ''
103
+ ]
104
+ end
105
+ end
106
+
107
+ def lilypond_phrasing(event)
108
+ phrasing = ''
109
+ if @work['process/open_beam']
110
+ phrasing << '['
111
+ @work['process/open_beam'] = nil
112
+ end
113
+ if @work['process/open_slur']
114
+ phrasing << '('
115
+ @work['process/open_slur'] = nil
116
+ end
117
+ phrasing << ']' if event[:beam_close]
118
+ phrasing << ')' if event[:slur_close]
119
+ phrasing
120
+ end
121
+
122
+ def lydown_phrasing_open(event)
123
+ phrasing = ''
124
+ if @work['process/open_beam']
125
+ phrasing << '['
126
+ @work['process/open_beam'] = nil
127
+ end
128
+ if @work['process/open_slur']
129
+ phrasing << '('
130
+ @work['process/open_slur'] = nil
131
+ end
132
+ phrasing
133
+ end
134
+
135
+ def lydown_phrasing_close(event)
136
+ phrasing = ''
137
+ phrasing << ']' if event[:beam_close]
138
+ phrasing << ')' if event[:slur_close]
139
+ phrasing
140
+ end
141
+
142
+ def add_macro_note(event)
143
+ @work['process/macro_group'] ||= @work['process/duration_macro'].clone
144
+ underscore_count = 0
145
+
146
+ lydown_note = "%s%s%s%s%s%s%s" % [
147
+ lydown_phrasing_open(event),
148
+ event[:head], event[:octave], event[:accidental_flag],
149
+ lydown_phrasing_close(event),
150
+ event[:figures] ? "<#{event[:figures].join}>" : '',
151
+ event[:expressions] ? event[:expressions].join : ''
152
+ ]
153
+
154
+ # replace place holder and repeaters in macro group with actual note
155
+ @work['process/macro_group'].gsub!(/[_@]/) do |match|
156
+ case match
157
+ when '_'
158
+ underscore_count += 1
159
+ underscore_count == 1 ? lydown_note : match
160
+ when '@'
161
+ underscore_count < 2 ? event[:head] : match
162
+ end
163
+ end
164
+
165
+ # if group is complete, compile it just like regular code
166
+ unless @work['process/macro_group'].include?('_')
167
+ # stash macro, in order to compile macro group
168
+ macro = @work['process/duration_macro']
169
+ @work['process/duration_macro'] = nil
170
+
171
+ code = LydownParser.parse(@work['process/macro_group'])
172
+ @work.process(code)
173
+
174
+ # restore macro
175
+ @work['process/duration_macro'] = macro
176
+ @work['process/macro_group'] = nil
177
+ end
178
+ end
179
+ end
180
+
181
+ LILYPOND_DURATIONS = {
182
+ '6' => '16',
183
+ '3' => '32'
184
+ }
185
+
186
+ class Duration < Base
187
+ def translate
188
+ value = @event[:value].sub(/^[0-9]+/) {|m| LILYPOND_DURATIONS[m] || m}
189
+
190
+ if next_event && next_event[:type] == :stand_alone_figures
191
+ @work['process/figures_duration_value'] = value
192
+ else
193
+ @work['process/duration_values'] = [value]
194
+ @work['process/duration_macro'] = nil unless @work['process/macro_group']
195
+ end
196
+ end
197
+ end
198
+
199
+ class Note < Base
200
+ include Notes
201
+
202
+ def translate
203
+ translate_expressions
204
+
205
+ # look ahead and see if any beam or slur closing after note
206
+ look_ahead_idx = @idx + 1
207
+ while event = @stream[look_ahead_idx]
208
+ case @stream[@idx + 1][:type]
209
+ when :beam_close
210
+ @event[:beam_close] = true
211
+ when :slur_close
212
+ @event[:slur_close] = true
213
+ else
214
+ break
215
+ end
216
+ look_ahead_idx += 1
217
+ end
218
+
219
+ add_note(@event)
220
+ end
221
+
222
+ LILYPOND_EXPRESSIONS = {
223
+ '_' => '--',
224
+ '.' => '-.',
225
+ '`' => '-!'
226
+ }
227
+
228
+ def translate_expressions
229
+ return unless @event[:expressions]
230
+
231
+ @event[:expressions] = @event[:expressions].map do |expr|
232
+ if expr =~ /^\\/
233
+ expr
234
+ elsif LILYPOND_EXPRESSIONS[expr]
235
+ LILYPOND_EXPRESSIONS[expr]
236
+ else
237
+ raise LydownError, "Invalid expression #{expr.inspect}"
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ class StandAloneFigures < Base
244
+ include Notes
245
+
246
+ def translate
247
+ add_stand_alone_figures(@event[:figures])
248
+ end
249
+ end
250
+
251
+ class BeamOpen < Base
252
+ def translate
253
+ @work['process/open_beam'] = true
254
+ end
255
+ end
256
+
257
+ class BeamClose < Base
258
+ def translate
259
+ end
260
+ end
261
+
262
+ class SlurOpen < Base
263
+ def translate
264
+ @work['process/open_slur'] = true
265
+ end
266
+ end
267
+
268
+ class SlurClose < Base
269
+ end
270
+
271
+ class Tie < Base
272
+ def translate
273
+ @work.emit(:music, '~ ')
274
+ end
275
+ end
276
+
277
+ class ShortTie < Base
278
+ include Notes
279
+
280
+ def translate
281
+ note_head = @work['process/last_note_head']
282
+ @work.emit(:music, '~ ')
283
+ add_note({head: note_head})
284
+ end
285
+ end
286
+
287
+ class Rest < Base
288
+ include Notes
289
+
290
+ def full_bar_value(time)
291
+ r = Rational(time)
292
+ case r.numerator
293
+ when 1
294
+ r.denominator.to_s
295
+ when 3
296
+ "#{r.denominator / 2}."
297
+ else
298
+ nil
299
+ end
300
+ end
301
+
302
+ def translate
303
+ if @event[:multiplier]
304
+ value = full_bar_value(@work[:time])
305
+ if value
306
+ @event[:rest_value] = "#{value}*#{@event[:multiplier]}"
307
+ @event[:head] = "#{@event[:head]}#{@event[:rest_value]}"
308
+ else
309
+ @event[:head] = "#{@event[:head]}#{@event[:multiplier]}*#{@work[:time]}"
310
+ end
311
+ # reset the last value so the next note will be rendered with its value
312
+ @work['process/last_value'] = nil
313
+ @work['process/duration_values'] = []
314
+ end
315
+
316
+ add_note(@event)
317
+ end
318
+ end
319
+
320
+ class Silence < Base
321
+ include Notes
322
+
323
+ def translate
324
+ add_note(@event)
325
+ end
326
+ end
327
+
328
+ class FiguresSilence < Base
329
+ include Notes
330
+
331
+ def translate
332
+ @event[:stream] = :figures
333
+ add_note(@event)
334
+ end
335
+ end
336
+
337
+ class DurationMacro < Base
338
+ def translate
339
+ if @event[:macro] =~ /^[a-zA-Z_]/
340
+ macro = @work['macros'][@event[:macro]]
341
+ if macro
342
+ if macro =~ /^\{(.+)\}$/
343
+ macro = $1
344
+ end
345
+ @work['process/duration_macro'] = macro
346
+ else
347
+ raise LydownError, "Unknown macro #{@event[:macro]}"
348
+ end
349
+ else
350
+ @work['process/duration_macro'] = @event[:macro]
351
+ end
352
+ end
353
+ end
354
+
355
+ class Barline < Base
356
+ def translate
357
+ barline = @event[:barline]
358
+ barline = '' if barline == '?|'
359
+ @work.emit(:music, "\\bar \"#{barline}\" ")
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,107 @@
1
+ module Lydown::Rendering
2
+ class Setting < Base
3
+ SETTING_KEYS = [
4
+ 'key', 'time', 'pickup', 'clef', 'part', 'movement',
5
+ 'accidentals', 'beams', 'end_barline', 'macros'
6
+ ]
7
+
8
+ RENDERABLE_SETTING_KEYS = [
9
+ 'key', 'time', 'clef', 'beams'
10
+ ]
11
+
12
+ ALLOWED_SETTING_VALUES = {
13
+ 'accidentals' => ['manual', 'auto'],
14
+ 'beams' => ['manual', 'auto']
15
+ }
16
+
17
+ def translate
18
+ key = @event[:key]
19
+ value = @event[:value]
20
+ level = @event[:level] || 0
21
+
22
+ unless (level > 0) || SETTING_KEYS.include?(key)
23
+ raise Lydown, "Invalid setting (#{key})"
24
+ end
25
+
26
+ if level == 0
27
+ @work[key] = check_setting_value(key, value)
28
+ case key
29
+ when 'part'
30
+ # when changing parts we repeat the last set time and key signature
31
+ render_setting('time', @work[:time]) unless @work[:time] == '4/4'
32
+ key = @work[:key]
33
+ render_setting('key', key) unless key == 'c major'
34
+
35
+ @work.reset_context(:part)
36
+ when 'movement'
37
+ @work.reset_context(:movement)
38
+ end
39
+
40
+ if RENDERABLE_SETTING_KEYS.include?(key)
41
+ render_setting(key, value)
42
+ end
43
+ else
44
+ # nested settings
45
+ l, path = 0, ''
46
+ while l < level
47
+ path << "#{@work['process/setting_levels'][l]}/"; l += 1
48
+ end
49
+ path << key
50
+ @work[path] = value
51
+ end
52
+
53
+ @work['process/setting_levels'] ||= {}
54
+ @work['process/setting_levels'][level] = key
55
+ end
56
+
57
+ def check_setting_value(key, value)
58
+ if ALLOWED_SETTING_VALUES[key]
59
+ unless ALLOWED_SETTING_VALUES[key].include?(value)
60
+ raise LydownError, "Invalid value for setting #{key}: #{value.inspect}"
61
+ end
62
+ end
63
+ value
64
+ end
65
+
66
+ def render_setting(key, value)
67
+ setting = ""
68
+ case key
69
+ when 'time'
70
+ cadenza_mode = @work[:cadenza_mode]
71
+ should_cadence = value == 'unmetered'
72
+ @work[:cadenza_mode] = should_cadence
73
+
74
+ if should_cadence && !cadenza_mode
75
+ setting = "\\cadenzaOn "
76
+ elsif !should_cadence && cadenza_mode
77
+ setting = "\\cadenzaOff "
78
+ end
79
+
80
+ unless should_cadence
81
+ signature = value.sub(/[0-9]+$/) { |m| LILYPOND_DURATIONS[m] || m }
82
+ setting << "\\#{key} #{signature} "
83
+ end
84
+ when 'key'
85
+ # If the next event is a key signature, no need to emit this one
86
+ e = next_event
87
+ return if e && (e[:type] == :setting) && (e[:key] == 'key')
88
+
89
+ unless value =~ /^([a-g][\+\-]*) (major|minor)$/
90
+ raise LydownError, "Invalid key signature #{value.inspect}"
91
+ end
92
+
93
+ note = Lydown::Rendering::Accidentals.lilypond_note_name($1)
94
+ mode = $2
95
+ setting = "\\#{key} #{note} \\#{mode} "
96
+ when 'clef'
97
+ setting = "\\#{key} \"#{value}\" "
98
+ when 'beams'
99
+ setting = (value == 'manual') ? '\autoBeamOff ' : '\autoBeamOn '
100
+ else
101
+ setting = "\\#{key} #{value} "
102
+ end
103
+
104
+ @work.emit(:music, setting)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,83 @@
1
+ module Lydown::Rendering
2
+ module Staff
3
+ def self.staff_groups(work, movement, parts)
4
+ model = work['score/order'] || movement['score/order'] || DEFAULTS['score/order']
5
+ parts_copy = parts.clone
6
+
7
+ groups = []
8
+
9
+ model.each do |group|
10
+ group = [group] unless group.is_a?(Array)
11
+ filtered = group.select do |p|
12
+ if parts_copy.include?(p)
13
+ parts_copy.delete(p)
14
+ true
15
+ end
16
+ end
17
+ groups << filtered unless filtered.empty?
18
+ end
19
+
20
+ # add any remaining unknown parts, in their original order
21
+ parts_copy.each {|p| groups << [p]}
22
+
23
+ groups
24
+ end
25
+
26
+ SYSTEM_START = {
27
+ "brace" => "SystemStartBrace",
28
+ "bracket" => "SystemStartBracket"
29
+ }
30
+
31
+ BRACKET_PARTS = %w{soprano alto tenore basso}
32
+
33
+ def self.staff_group_directive(group)
34
+ if group.size == 1
35
+ nil
36
+ elsif BRACKET_PARTS.include?(group.first)
37
+ "SystemStartBracket"
38
+ else
39
+ "SystemStartBrace"
40
+ end
41
+ end
42
+
43
+ # renders a systemStartDelimiterHierarchy expression
44
+ def self.staff_hierarchy(staff_groups)
45
+ expr = staff_groups.inject('') do |m, group|
46
+ directive = staff_group_directive(group)
47
+ if directive
48
+ m << "(#{directive} #{group.join(' ')}) "
49
+ else
50
+ m << "#{group.join(' ')} "
51
+ end
52
+ m
53
+ end
54
+
55
+ "#'(SystemStartBracket #{expr})"
56
+ end
57
+
58
+ def self.clef(part)
59
+ DEFAULTS["parts/#{part}/clef"]
60
+ end
61
+
62
+ def self.beaming_mode(part)
63
+ beaming = DEFAULTS["parts/#{part}/beaming"]
64
+ return nil if beaming.nil?
65
+
66
+ case beaming
67
+ when 'auto'
68
+ '\autoBeamOn'
69
+ when 'manual'
70
+ '\autoBeamOff'
71
+ else
72
+ raise LydownError, "Invalid beaming mode (#{beaming.inspect})"
73
+ end
74
+ end
75
+
76
+ DEFAULT_END_BARLINE = '|.'
77
+
78
+ def self.end_barline(work, movement)
79
+ barline = movement['end_barline'] || work['end_barline'] || DEFAULT_END_BARLINE
80
+ barline == 'none' ? nil : barline
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,45 @@
1
+ require 'lydown/templates'
2
+ require 'lydown/work'
3
+ require 'lydown/rendering/base'
4
+ require 'lydown/rendering/comments'
5
+ require 'lydown/rendering/lyrics'
6
+ require 'lydown/rendering/music'
7
+ require 'lydown/rendering/settings'
8
+ require 'lydown/rendering/staff'
9
+ require 'lydown/rendering/movement'
10
+
11
+ require 'yaml'
12
+
13
+ module Lydown::Rendering
14
+ DEFAULTS = YAML.load(IO.read(File.join(File.dirname(__FILE__), 'rendering/defaults.yml'))).deep!
15
+
16
+ class << self
17
+ def translate(work, e, lydown_stream, idx)
18
+ klass = class_for_event(e)
19
+ klass.new(e, work, lydown_stream, idx).translate
20
+ end
21
+
22
+ def class_for_event(e)
23
+ Lydown::Rendering.const_get(e[:type].to_s.camelize)
24
+ rescue
25
+ raise LydownError, "Invalid lydown event: #{e.inspect}"
26
+ end
27
+
28
+ def part_title(part_name)
29
+ if part_name =~ /^([^\d]+)(\d+)$/
30
+ "#{$1.titlize} #{$2.to_i.to_roman}"
31
+ else
32
+ part_name.titlize
33
+ end
34
+ end
35
+ end
36
+
37
+ class Base
38
+ def initialize(event, work, stream, idx)
39
+ @event = event
40
+ @work = work
41
+ @stream = stream
42
+ @idx = idx
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ \version "2.18.2"
2
+
3
+ <% if self['layout'] %>
4
+ \layout {
5
+ }
6
+ <% end %>
7
+
8
+ \book {
9
+ \header {
10
+ }
11
+ <% self['movements'].each do |n, m| %>
12
+ <%= Lydown::Templates.render(:movement, self, name: n, movement: m)
13
+ %>
14
+ <% end %>
15
+ }
@@ -0,0 +1,48 @@
1
+ <%
2
+ score_mode = self['render_opts/mode'] == :score
3
+ staff_groups = Lydown::Rendering::Staff.staff_groups(
4
+ self, movement, movement['parts'].keys)
5
+ parts_in_order = staff_groups.flatten
6
+ staff_hierarchy = Lydown::Rendering::Staff.staff_hierarchy(staff_groups)
7
+
8
+ parts = parts_in_order.inject({}) do |m, p|
9
+ m[p] = movement['parts'][p]
10
+ m
11
+ end
12
+
13
+ movement_title = Lydown::Rendering::Movement.movement_title(self, name)
14
+ %>
15
+
16
+ \bookpart {
17
+ <% if movement_title %>
18
+ \header {
19
+ piece = \markup {
20
+ \column {
21
+ \fill-line {\bold \large "<%= movement_title %>"}
22
+
23
+ }
24
+ }
25
+ }
26
+ <% end %>
27
+
28
+ <% if score_mode %>
29
+ \score {
30
+ \new StaffGroup <<
31
+ \set StaffGroup.systemStartDelimiterHierarchy = <%= staff_hierarchy %>
32
+ <% end %>
33
+
34
+ <% if n = movement['bar_number'] %>
35
+ \set Score.currentBarNumber = #<%= n %>
36
+ \set Score.barNumberVisibility = #all-bar-numbers-visible
37
+ \bar ""
38
+ <% end %>
39
+
40
+ <% parts.each do |n, p| %>
41
+ <%= Lydown::Templates.render(:part, self,
42
+ name: n, part: p, movement: movement) %>
43
+ <% end %>
44
+
45
+ <% if score_mode %>
46
+ >> }
47
+ <% end %>
48
+ }