lydown 0.12.4 → 0.14.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/lydown +2 -0
  4. data/lib/lydown.rb +6 -2
  5. data/lib/lydown/cache.rb +5 -1
  6. data/lib/lydown/cli.rb +1 -1
  7. data/lib/lydown/cli/commands.rb +7 -11
  8. data/lib/lydown/cli/compiler.rb +5 -0
  9. data/lib/lydown/cli/proofing.rb +2 -2
  10. data/lib/lydown/cli/repl.rb +1 -1
  11. data/lib/lydown/cli/support.rb +0 -33
  12. data/lib/lydown/defaults.yml +50 -8
  13. data/lib/lydown/inverso.rb +84 -0
  14. data/lib/lydown/lilypond.rb +76 -10
  15. data/lib/lydown/ly_lib/lib.ly +140 -127
  16. data/lib/lydown/parsing/lydown.treetop +25 -12
  17. data/lib/lydown/parsing/nodes.rb +55 -19
  18. data/lib/lydown/rendering.rb +72 -1
  19. data/lib/lydown/rendering/base.rb +21 -0
  20. data/lib/lydown/rendering/command.rb +53 -0
  21. data/lib/lydown/rendering/layout.rb +83 -0
  22. data/lib/lydown/rendering/lyrics.rb +1 -1
  23. data/lib/lydown/rendering/markup.rb +23 -0
  24. data/lib/lydown/rendering/movement.rb +7 -4
  25. data/lib/lydown/rendering/music.rb +35 -29
  26. data/lib/lydown/rendering/notes.rb +75 -41
  27. data/lib/lydown/rendering/repeats.rb +27 -0
  28. data/lib/lydown/rendering/settings.rb +36 -9
  29. data/lib/lydown/rendering/skipping.rb +10 -2
  30. data/lib/lydown/rendering/staff.rb +38 -31
  31. data/lib/lydown/rendering/voices.rb +1 -1
  32. data/lib/lydown/templates.rb +8 -8
  33. data/lib/lydown/templates/layout.rb +40 -0
  34. data/lib/lydown/templates/lilypond_doc.rb +95 -0
  35. data/lib/lydown/templates/movement.rb +188 -0
  36. data/lib/lydown/templates/multi_voice.rb +25 -0
  37. data/lib/lydown/templates/part.rb +146 -0
  38. data/lib/lydown/templates/variables.rb +43 -0
  39. data/lib/lydown/translation/ripple.rb +1 -1
  40. data/lib/lydown/translation/ripple/nodes.rb +51 -2
  41. data/lib/lydown/translation/ripple/ripple.treetop +87 -10
  42. data/lib/lydown/version.rb +1 -1
  43. data/lib/lydown/work.rb +19 -2
  44. data/lib/lydown/work_context.rb +10 -2
  45. metadata +12 -8
  46. data/lib/lydown/cli/installer.rb +0 -175
  47. data/lib/lydown/templates/lilypond_doc.erb +0 -34
  48. data/lib/lydown/templates/movement.erb +0 -118
  49. data/lib/lydown/templates/multi_voice.erb +0 -16
  50. data/lib/lydown/templates/part.erb +0 -118
  51. data/lib/lydown/templates/variables.erb +0 -43
@@ -12,7 +12,7 @@ module Lydown::Rendering
12
12
  def lilypond_lyrics(lyrics)
13
13
  lyrics.
14
14
  gsub(/_+/) {|m| " __ #{'_ ' * (m.size - 1)}"}.
15
- gsub(/\-+/) {|m| " -- #{'_ ' * (m.size - 1)}"}
15
+ gsub(/(\\\-)?(\-+)/) {|m| "#{$1 && "-"} -- #{'_ ' * ($2.size - 1)}"}
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,23 @@
1
+ module Lydown::Rendering
2
+ module Markup
3
+ def self.convert(str)
4
+ convert_line_breaks(str)
5
+ end
6
+
7
+ def self.convert_styling(str)
8
+ str.
9
+ gsub(/__([^_]+)__/) {|m| "\\bold {#{$1} }" }.
10
+ gsub(/_([^_]+)_/) {|m| "\\italic {#{$1} }" }
11
+ gsub(/\^([^\^]+)\^/) {|m| "\\smallCaps {#{$1} }" }
12
+ end
13
+
14
+ def self.convert_line_breaks(str)
15
+ if str =~/\n/
16
+ lines = str.lines.map {|s| "\\fill-line { \"#{s.chomp}\" }"}
17
+ "\\markup \\column { #{lines.join(' ')} }"
18
+ else
19
+ "\"#{str}\""
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,7 +3,9 @@ module Lydown::Rendering
3
3
  def self.movement_title(context, name)
4
4
  return nil if name.nil? || name.empty?
5
5
 
6
- if name =~ /^(?:([0-9]+)([a-z]*))\-(.+)$/
6
+ if t = context.get_setting('movement_title', movement: name)
7
+ title = t
8
+ elsif name =~ /^(?:([0-9]+)([a-z]*))\-(.+)$/
7
9
  title = "#{$1.to_i}#{$2}. #{$3.capitalize}"
8
10
  else
9
11
  title = name
@@ -24,7 +26,8 @@ module Lydown::Rendering
24
26
  'before' => {before: true},
25
27
  'after' => {after: true},
26
28
  'before and after' => {before: true, after: true},
27
- 'blank page before' => {blank_page_before: true}
29
+ 'blank page before' => {blank_page_before: true},
30
+ 'bookpart before' => {bookpart_before: true}
28
31
  }
29
32
 
30
33
  def self.page_breaks(context, opts)
@@ -32,8 +35,8 @@ module Lydown::Rendering
32
35
  when :score
33
36
  context.get_setting('score/page_break', opts)
34
37
  when :part
35
- part = opts[:part] || context[:part] ||
36
- (context['options/parts'] ? context['options/parts'][0] : '')
38
+ part = opts[:part] || context[:part] || context['render_opts/parts']
39
+
37
40
  context.get_setting(:page_break, opts.merge(part: part)) ||
38
41
  context.get_setting('parts/page_break', opts)
39
42
  else
@@ -10,8 +10,25 @@ module Lydown::Rendering
10
10
  '6' => '16',
11
11
  '3' => '32'
12
12
  }
13
-
13
+
14
14
  class Duration < Base
15
+ def self.full_bar_value(time)
16
+ r = Rational(time)
17
+ case r.numerator
18
+ when 1
19
+ r.denominator.to_s
20
+ when 3
21
+ case r.denominator
22
+ when 1
23
+ "\\breve."
24
+ else
25
+ "#{r.denominator / 2}."
26
+ end
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
15
32
  def translate
16
33
  Notes.cleanup_duration_macro(@context)
17
34
 
@@ -32,6 +49,7 @@ module Lydown::Rendering
32
49
  @context['process/figures_duration_value'] = value
33
50
  else
34
51
  @context['process/duration_values'] = [value]
52
+ @context['process/cross_bar_dotting'] = @event[:cross_bar_dotting]
35
53
  @context['process/tuplet_mode'] = nil
36
54
  @context['process/duration_macro'] = nil unless @context['process/macro_group']
37
55
  end
@@ -184,23 +202,11 @@ module Lydown::Rendering
184
202
  class Rest < Base
185
203
  include Notes
186
204
 
187
- def full_bar_value(time)
188
- r = Rational(time)
189
- case r.numerator
190
- when 1
191
- r.denominator.to_s
192
- when 3
193
- "#{r.denominator / 2}."
194
- else
195
- nil
196
- end
197
- end
198
-
199
205
  def translate
200
206
  translate_expressions
201
207
 
202
208
  if @event[:multiplier]
203
- value = full_bar_value(@context.get_current_setting(:time))
209
+ value = Duration.full_bar_value(@context.get_current_setting(:time))
204
210
  @context['process/duration_macro'] = nil unless @context['process/macro_group']
205
211
  if value
206
212
  @event[:rest_value] = "#{value}*#{@event[:multiplier]}"
@@ -221,21 +227,9 @@ module Lydown::Rendering
221
227
  class Silence < Base
222
228
  include Notes
223
229
 
224
- def full_bar_value(time)
225
- r = Rational(time)
226
- case r.numerator
227
- when 1
228
- r.denominator.to_s
229
- when 3
230
- "#{r.denominator / 2}."
231
- else
232
- nil
233
- end
234
- end
235
-
236
230
  def translate
237
231
  if @event[:multiplier]
238
- value = full_bar_value(@context.get_current_setting(:time))
232
+ value = Duration.full_bar_value(@context.get_current_setting(:time))
239
233
  @context['process/duration_macro'] = nil unless @context['process/macro_group']
240
234
  if value
241
235
  @event[:rest_value] = "#{value}*#{@event[:multiplier]}"
@@ -285,10 +279,22 @@ module Lydown::Rendering
285
279
  end
286
280
 
287
281
  class Barline < Base
282
+
283
+ LILYPOND_BARLINES = {
284
+ '|:' => '.|:',
285
+ ':|' => ':|.',
286
+ '|\''=> '\halfBarline',
287
+ '?|' => ''
288
+ }
289
+
288
290
  def translate
289
291
  barline = @event[:barline]
290
- barline = '' if barline == '?|'
291
- @context.emit(:music, "\\bar \"#{barline}\" ")
292
+ barline = LILYPOND_BARLINES[barline] || barline
293
+ if barline =~ /^\\/
294
+ @context.emit(:music, "#{barline} ")
295
+ else
296
+ @context.emit(:music, "\\bar \"#{barline}\" ")
297
+ end
292
298
  end
293
299
  end
294
300
  end
@@ -40,7 +40,7 @@ module Lydown::Rendering
40
40
  def self.calc_accidentals_map(accidentals)
41
41
  accidentals.inject({}) { |h, a| h[a[0]] = (a[1] == '+') ? 1 : -1; h}
42
42
  end
43
-
43
+
44
44
  ACCIDENTAL_VALUES = {
45
45
  '+' => 1,
46
46
  '-' => -1,
@@ -51,26 +51,26 @@ module Lydown::Rendering
51
51
  def self.lilypond_note_name(note, key_signature = 'c major')
52
52
  # if the natural sign (h) is used, no need to calculate the note name
53
53
  return $1 if note =~ /([a-g])h/
54
-
54
+
55
55
  value = 0
56
56
  # accidental value from note
57
57
  note = note.gsub(/[\-\+#ß]/) { |c| value += ACCIDENTAL_VALUES[c]; '' }
58
-
58
+
59
59
  # add key signature value
60
60
  value += accidentals_for_key_signature(key_signature)[note] || 0
61
61
 
62
62
  note + (value >= 0 ? 'is' * value : 'es' * -value)
63
63
  end
64
-
64
+
65
65
  def self.chromatic_to_diatonic(note, key_signature = 'c major')
66
66
  note =~ /([a-g])([\+\-]*)/
67
67
  diatonic_note = $1
68
68
  chromatic_value = $2.count('+') - $2.count('-')
69
-
69
+
70
70
  key_accidentals = accidentals_for_key_signature(key_signature)
71
71
  diatonic_value = key_accidentals[diatonic_note] || 0
72
72
  value = chromatic_value - diatonic_value
73
-
73
+
74
74
  "#{diatonic_note}#{value >= 0 ? '+' * value : '-' * -value}"
75
75
  end
76
76
 
@@ -84,35 +84,35 @@ module Lydown::Rendering
84
84
  lilypond_note_name(note, key)
85
85
  end
86
86
  end
87
-
87
+
88
88
  module Octaves
89
89
  DIATONICS = %w{a b c d e f g}
90
90
 
91
- # calculates the octave markers needed to put a first note in the right
91
+ # calculates the octave markers needed to put a first note in the right
92
92
  # octave. In lydown, octaves are relative (i.e. lilypond's relative mode).
93
- # But the first note gives the octave to start on, rather than a relative
93
+ # But the first note gives the octave to start on, rather than a relative
94
94
  # note to c (or any other reference note).
95
95
  #
96
- # In that manner, d' is d above middle c, g'' is g an octave and fifth
96
+ # In that manner, d' is d above middle c, g'' is g an octave and fifth
97
97
  # above middle c, a is a a below middle c, and eß, is great e flat.
98
- #
98
+ #
99
99
  # The return value is a string with octave markers for relative mode,
100
100
  # based on the refence note
101
101
  def self.relative_octave(note, ref_note = 'c')
102
102
  note_diatonic, ref_diatonic = note[0], ref_note[0]
103
103
  raise LydownError, "Invalid note #{note}" unless DIATONICS.index(note_diatonic)
104
104
  raise LydownError, "Invalid reference note #{ref_note}" unless DIATONICS.index(ref_diatonic)
105
-
105
+
106
106
  # calculate diatonic interval
107
107
  note_array = DIATONICS.rotate(DIATONICS.index(ref_diatonic))
108
108
  interval = note_array.index(note_diatonic)
109
109
 
110
- # calculate octave interval and
110
+ # calculate octave interval and
111
111
  octave_value = note.count("'") - note.count(',')
112
112
  ref_value = ref_note.count("'") - ref_note.count(',')
113
113
  octave_interval = octave_value - ref_value
114
114
  octave_interval += 1 if interval >= 4
115
-
115
+
116
116
  # generate octave markers
117
117
  octave_interval >= 0 ? "'" * octave_interval : "," * -octave_interval
118
118
  end
@@ -121,17 +121,17 @@ module Lydown::Rendering
121
121
  note_diatonic, ref_diatonic = note[0], ref_note[0]
122
122
  raise LydownError, "Invalid note #{note}" unless DIATONICS.index(note_diatonic)
123
123
  raise LydownError, "Invalid reference note #{ref_note}" unless DIATONICS.index(ref_diatonic)
124
-
124
+
125
125
  # calculate diatonic interval
126
126
  note_array = DIATONICS.rotate(DIATONICS.index(ref_diatonic))
127
127
  interval = note_array.index(note_diatonic)
128
128
 
129
- # calculate octave interval and
129
+ # calculate octave interval and
130
130
  note_value = note.count("'") - note.count(',')
131
131
  ref_value = ref_note.count("'") - ref_note.count(',')
132
132
  octave_interval = ref_value + note_value
133
133
  octave_interval -= 1 if interval >= 4
134
-
134
+
135
135
  # generate octave markers
136
136
  octave_interval >= 0 ? "'" * octave_interval : "," * -octave_interval
137
137
  end
@@ -142,19 +142,19 @@ module Lydown::Rendering
142
142
 
143
143
  def add_note(event, options = {})
144
144
  @context.set_setting(:got_music, true)
145
-
145
+
146
146
  return add_macro_note(event) if @context['process/duration_macro']
147
-
147
+
148
148
  # calculate relative octave markers for first note
149
149
  unless @context['process/first_note'] || event[:head] =~ /^[rsR]/
150
150
  note = event[:head] + (event[:octave] || '')
151
151
  event[:octave] = Lydown::Rendering::Octaves.relative_octave(note)
152
152
  @context['process/first_note'] = note
153
153
  end
154
-
154
+
155
155
  if event[:head] == '@'
156
156
  # replace repeating note head
157
- event[:head] = @context['process/last_note_head']
157
+ event[:head] = @context['process/last_note_head']
158
158
  else
159
159
  @context['process/last_note_head'] = event[:head]
160
160
  end
@@ -174,7 +174,7 @@ module Lydown::Rendering
174
174
  @context['process/running_values'] << value
175
175
  end
176
176
  end
177
-
177
+
178
178
  # only add the value if different than the last used
179
179
  if options[:no_value] || (value == @context['process/last_value'])
180
180
  value = ''
@@ -190,7 +190,7 @@ module Lydown::Rendering
190
190
  code = lilypond_note(event, options.merge(value: value))
191
191
  @context.emit(event[:stream] || :music, code)
192
192
  end
193
-
193
+
194
194
  def add_chord(event, options = {})
195
195
  value = @context['process/duration_values'].first
196
196
  @context['process/duration_values'].rotate!
@@ -214,16 +214,20 @@ module Lydown::Rendering
214
214
  else
215
215
  @context['process/last_value'] = value
216
216
  end
217
-
217
+
218
218
  notes = event[:notes].map do |note|
219
219
  lilypond_note(note)
220
220
  end
221
-
221
+
222
222
  options = options.merge(value: value)
223
223
  @context.emit(event[:stream] || :music, lilypond_chord(event, notes, options))
224
224
  end
225
-
225
+
226
226
  def lilypond_note(event, options = {})
227
+ if @context['process/cross_bar_dotting']
228
+ return cross_bar_dot_lilypond_note(event, options)
229
+ end
230
+
227
231
  head = Accidentals.translate_note_name(@context, event[:head])
228
232
  if options[:head_only]
229
233
  head
@@ -235,7 +239,7 @@ module Lydown::Rendering
235
239
  accidental_flag = event[:accidental_flag]
236
240
  prefix = ''
237
241
  end
238
-
242
+
239
243
  [
240
244
  prefix,
241
245
  head,
@@ -249,6 +253,37 @@ module Lydown::Rendering
249
253
  end
250
254
  end
251
255
 
256
+ TRANSPARENT_TIE = "\\once \\override Tie #'transparent = ##t"
257
+ TRANSPARENT_NOTE = <<EOF
258
+ \\once \\override NoteHead #'transparent = ##t
259
+ \\once \\override Dots #'extra-offset = #'(-1.3 . 0)
260
+ \\once \\override Stem #'transparent = ##t
261
+ EOF
262
+
263
+ def cross_bar_dot_lilypond_note(event, options)
264
+ @context['process/cross_bar_dotting'] = nil
265
+
266
+ original_duration = @context['process/duration_values'][0]
267
+ original_duration =~ /([0-9]+)(\.+)/
268
+ value, dots = $1, $2
269
+
270
+ main_note = lilypond_note(event, options.merge(value: value))
271
+
272
+ cross_bar_note_head = lilypond_note(event, options.merge(head_only: true))
273
+ cross_bar_note = "#{cross_bar_note_head}#{original_duration}*0"
274
+
275
+ silence = "s#{value.to_i * 2} "
276
+
277
+ [
278
+ TRANSPARENT_TIE,
279
+ main_note,
280
+ '~',
281
+ TRANSPARENT_NOTE,
282
+ cross_bar_note,
283
+ silence
284
+ ].join(' ')
285
+ end
286
+
252
287
  def lilypond_chord(event, notes, options = {})
253
288
  [
254
289
  '<',
@@ -309,7 +344,7 @@ module Lydown::Rendering
309
344
  event[:figures] ? "<#{event[:figures].join}>" : '',
310
345
  event[:expressions] ? event[:expressions].join + ' ' : ''
311
346
  ]
312
-
347
+
313
348
  # replace place holder and repeaters in macro group with actual note
314
349
  @context['process/macro_group'].gsub!(/[_∞]/) do |match|
315
350
  case match
@@ -325,7 +360,7 @@ module Lydown::Rendering
325
360
  # correct
326
361
  @context['process/macro_filename'] = event[:filename]
327
362
  @context['process/macro_source'] = event[:source]
328
-
363
+
329
364
  # increment group note count
330
365
  @context['process/macro_group_note_count'] ||= 0
331
366
  @context['process/macro_group_note_count'] += 1
@@ -335,20 +370,19 @@ module Lydown::Rendering
335
370
  Notes.add_duration_macro_group(@context, @context['process/macro_group'])
336
371
  end
337
372
  end
338
-
373
+
339
374
  # emits the current macro group up to the first placeholder character.
340
- # this method is called
375
+ # this method is called
341
376
  def self.cleanup_duration_macro(context)
342
377
  return unless context['process/macro_group_note_count'] &&
343
378
  context['process/macro_group_note_count'] > 0
344
-
379
+
345
380
  # truncate macro group up until first placeholder
346
- group = context['process/macro_group'].sub(/_.*$/, '')
381
+ group = context['process/macro_group'].sub(/(?<!\<)_.*$/, '')
347
382
 
348
- # Refrain from adding
349
383
  add_duration_macro_group(context, group)
350
384
  end
351
-
385
+
352
386
  def self.add_duration_macro_group(context, group)
353
387
  opts = (context[:options] || {}).merge({
354
388
  filename: context['process/macro_filename'],
@@ -367,7 +401,7 @@ module Lydown::Rendering
367
401
  # restore macro
368
402
  context['process/duration_macro'] = macro
369
403
  end
370
-
404
+
371
405
  def add_macro_event(code)
372
406
  case @context['process/macro_group']
373
407
  when nil
@@ -389,7 +423,7 @@ module Lydown::Rendering
389
423
  '>' => 'left-align',
390
424
  '|' => 'center-align'
391
425
  }
392
-
426
+
393
427
  DYNAMICS = %w{
394
428
  pppp ppp pp p mp mf f ff fff ffff fp sf sff sp spp sfz rfz
395
429
  }
@@ -421,19 +455,19 @@ module Lydown::Rendering
421
455
  gsub(/__([^_]+)__/) {|m| "\\bold { #{$1} }" }.
422
456
  gsub(/_([^_]+)_/) {|m| "\\italic { #{$1} }" }
423
457
  end
424
-
458
+
425
459
  TEXTMATE_URL = "txmt://open?url=file://%s&line=%d&column=%d"
426
-
460
+
427
461
  ADD_LINK_COMMAND = '\once \override NoteHead.after-line-breaking =
428
462
  #(add-link "%s") '
429
-
463
+
430
464
  def note_event_url_link(event)
431
465
  url = TEXTMATE_URL % [
432
466
  File.expand_path(event[:filename]).uri_escape,
433
467
  event[:line],
434
468
  event[:column]
435
469
  ]
436
-
470
+
437
471
  ADD_LINK_COMMAND % [url]
438
472
  end
439
473
  end