lydown 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }