lydown 0.9.0 → 0.10.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -2
  3. data/lib/lydown.rb +8 -2
  4. data/lib/lydown/cache.rb +54 -0
  5. data/lib/lydown/cli.rb +1 -0
  6. data/lib/lydown/cli/commands.rb +27 -9
  7. data/lib/lydown/cli/compiler.rb +218 -54
  8. data/lib/lydown/cli/diff.rb +1 -1
  9. data/lib/lydown/cli/proofing.rb +3 -3
  10. data/lib/lydown/cli/signals.rb +23 -0
  11. data/lib/lydown/cli/support.rb +23 -1
  12. data/lib/lydown/core_ext.rb +41 -5
  13. data/lib/lydown/{rendering/defaults.yml → defaults.yml} +3 -3
  14. data/lib/lydown/errors.rb +3 -0
  15. data/lib/lydown/lilypond.rb +73 -31
  16. data/lib/lydown/ly_lib/lib.ly +297 -0
  17. data/lib/lydown/parsing.rb +1 -2
  18. data/lib/lydown/parsing/lydown.treetop +16 -10
  19. data/lib/lydown/parsing/nodes.rb +29 -5
  20. data/lib/lydown/rendering.rb +32 -6
  21. data/lib/lydown/rendering/command.rb +79 -2
  22. data/lib/lydown/rendering/figures.rb +29 -8
  23. data/lib/lydown/rendering/literal.rb +7 -0
  24. data/lib/lydown/rendering/movement.rb +61 -0
  25. data/lib/lydown/rendering/music.rb +37 -5
  26. data/lib/lydown/rendering/notes.rb +26 -8
  27. data/lib/lydown/rendering/settings.rb +41 -13
  28. data/lib/lydown/rendering/skipping.rb +43 -10
  29. data/lib/lydown/rendering/staff.rb +72 -16
  30. data/lib/lydown/templates.rb +8 -2
  31. data/lib/lydown/templates/lilypond_doc.erb +10 -1
  32. data/lib/lydown/templates/movement.erb +87 -34
  33. data/lib/lydown/templates/multi_voice.erb +1 -1
  34. data/lib/lydown/templates/part.erb +83 -55
  35. data/lib/lydown/templates/variables.erb +38 -0
  36. data/lib/lydown/version.rb +1 -1
  37. data/lib/lydown/work.rb +39 -26
  38. data/lib/lydown/work_context.rb +252 -14
  39. metadata +138 -8
  40. data/lib/lydown/rendering/lib.ly +0 -88
@@ -3,6 +3,8 @@ require 'lydown/rendering/notes'
3
3
 
4
4
  module Lydown::Rendering
5
5
  LILYPOND_DURATIONS = {
6
+ '0' => "\\breve",
7
+ 'l' => "\\longa",
6
8
  '6' => '16',
7
9
  '3' => '32'
8
10
  }
@@ -22,7 +24,7 @@ module Lydown::Rendering
22
24
  @context['process/grace_mode'] = nil
23
25
  end
24
26
 
25
- value = @event[:value].sub(/^[0-9]+/) {|m| LILYPOND_DURATIONS[m] || m}
27
+ value = @event[:value].sub(/^[0-9]+|l/) {|m| LILYPOND_DURATIONS[m] || m}
26
28
 
27
29
  if next_event && next_event[:type] == :stand_alone_figures
28
30
  @context['process/figures_duration_value'] = value
@@ -105,7 +107,7 @@ module Lydown::Rendering
105
107
  end
106
108
  look_ahead_idx += 1
107
109
  end
108
-
110
+
109
111
  add_note(@event)
110
112
  end
111
113
  end
@@ -114,6 +116,8 @@ module Lydown::Rendering
114
116
  include Notes
115
117
 
116
118
  def translate
119
+ translate_expressions
120
+
117
121
  look_ahead_idx = @idx + 1
118
122
  while event = @stream[look_ahead_idx]
119
123
  case event[:type]
@@ -194,13 +198,14 @@ module Lydown::Rendering
194
198
  translate_expressions
195
199
 
196
200
  if @event[:multiplier]
197
- value = full_bar_value(@context[:time])
201
+ value = full_bar_value(@context.get_current_setting(:time))
198
202
  @context['process/duration_macro'] = nil unless @context['process/macro_group']
199
203
  if value
200
204
  @event[:rest_value] = "#{value}*#{@event[:multiplier]}"
201
205
  @event[:head] = "#{@event[:head]}#{@event[:rest_value]}"
202
206
  else
203
- @event[:head] = "#{@event[:head]}#{@event[:multiplier]}*#{@context[:time]}"
207
+ @event[:head] = "#{@event[:head]}#{@event[:multiplier]}*#{
208
+ @context.get_current_setting(:time)}"
204
209
  end
205
210
  # reset the last value so the next note will be rendered with its value
206
211
  @context['process/last_value'] = nil
@@ -214,7 +219,34 @@ module Lydown::Rendering
214
219
  class Silence < Base
215
220
  include Notes
216
221
 
222
+ def full_bar_value(time)
223
+ r = Rational(time)
224
+ case r.numerator
225
+ when 1
226
+ r.denominator.to_s
227
+ when 3
228
+ "#{r.denominator / 2}."
229
+ else
230
+ nil
231
+ end
232
+ end
233
+
217
234
  def translate
235
+ if @event[:multiplier]
236
+ value = full_bar_value(@context.get_current_setting(:time))
237
+ @context['process/duration_macro'] = nil unless @context['process/macro_group']
238
+ if value
239
+ @event[:rest_value] = "#{value}*#{@event[:multiplier]}"
240
+ @event[:head] = "s#{@event[:rest_value]}"
241
+ else
242
+ @event[:head] = "s#{@event[:multiplier]}*#{
243
+ @context.get_current_setting(:time)}"
244
+ end
245
+ # reset the last value so the next note will be rendered with its value
246
+ @context['process/last_value'] = nil
247
+ @context['process/duration_values'] = []
248
+ end
249
+
218
250
  add_note(@event)
219
251
  end
220
252
  end
@@ -233,7 +265,7 @@ module Lydown::Rendering
233
265
  Notes.cleanup_duration_macro(@context)
234
266
 
235
267
  if @event[:macro] =~ /^[a-zA-Z_]/
236
- macro = @context['macros'][@event[:macro]]
268
+ macro = @context.get_current_setting('macros')[@event[:macro]]
237
269
  if macro
238
270
  if macro =~ /^\{(.+)\}$/
239
271
  macro = $1
@@ -74,10 +74,10 @@ module Lydown::Rendering
74
74
 
75
75
  # Takes into account the accidentals mode
76
76
  def self.translate_note_name(context, note)
77
- if context[:accidentals] == 'manual'
77
+ if context.get_current_setting(:accidentals) == 'manual'
78
78
  key = 'c major'
79
79
  else
80
- key = context[:key]
80
+ key = context.get_current_setting(:key)
81
81
  end
82
82
  lilypond_note_name(note, key)
83
83
  end
@@ -139,6 +139,8 @@ module Lydown::Rendering
139
139
  include Lydown::Rendering::Figures
140
140
 
141
141
  def add_note(event, options = {})
142
+ @context.set_setting(:got_music, true)
143
+
142
144
  return add_macro_note(event) if @context['process/duration_macro']
143
145
 
144
146
  # calculate relative octave markers for first note
@@ -170,7 +172,7 @@ module Lydown::Rendering
170
172
  @context['process/running_values'] << value
171
173
  end
172
174
  end
173
-
175
+
174
176
  # only add the value if different than the last used
175
177
  if options[:no_value] || (value == @context['process/last_value'])
176
178
  value = ''
@@ -253,6 +255,7 @@ module Lydown::Rendering
253
255
  options[:value],
254
256
  lilypond_phrasing(event),
255
257
  event[:expressions] ? event[:expressions].join : '',
258
+ ' '
256
259
  ].join
257
260
  end
258
261
 
@@ -379,15 +382,30 @@ module Lydown::Rendering
379
382
  '`' => '-!'
380
383
  }
381
384
 
385
+ MARKUP_ALIGNMENT = {
386
+ '<' => 'right-align',
387
+ '>' => 'left-align',
388
+ '|' => 'center-align'
389
+ }
390
+
391
+ DYNAMICS = %w{
392
+ pppp ppp pp p mp mf f ff fff ffff fp sf sff sp spp sfz rfz
393
+ }
394
+
382
395
  def translate_expressions
383
396
  return unless @event[:expressions]
384
397
 
385
398
  @event[:expressions] = @event[:expressions].map do |expr|
386
- if expr =~ /^(?:\\(_?))?"(.+)"$/
387
- placement = ($1 == '_') ? '_' : '^'
388
- "#{placement}\\markup { #{translate_string_expression($2)} }"
389
- elsif expr =~ /^\\/
390
- expr
399
+ if expr =~ /^(?:\\(_?)([<>\|])?)?"(.+)"$/
400
+ v_pos = ($1 == '_') ? '_' : '^'
401
+ content = translate_string_expression($3)
402
+ if MARKUP_ALIGNMENT[$2]
403
+ content = "\\#{MARKUP_ALIGNMENT[$2]} { #{content} }"
404
+ end
405
+ "#{v_pos}\\markup { #{content} }"
406
+ elsif expr =~ /^\\([_^]?)(.+)$/
407
+ v_pos = $1
408
+ "#{v_pos}\\#{$2}"
391
409
  elsif LILYPOND_EXPRESSIONS[expr]
392
410
  LILYPOND_EXPRESSIONS[expr]
393
411
  else
@@ -5,7 +5,9 @@ module Lydown::Rendering
5
5
  SETTING_KEYS = [
6
6
  'key', 'time', 'pickup', 'clef', 'part', 'movement', 'tempo',
7
7
  'accidentals', 'beams', 'end_barline', 'macros', 'empty_staves',
8
- 'midi_tempo'
8
+ 'midi_tempo', 'instrument_names', 'instrument_name_style',
9
+ 'parts', 'score', 'movement_source', 'colla_parte', 'include',
10
+ 'mode', 'nomode', 'bar_numbers'
9
11
  ]
10
12
 
11
13
  RENDERABLE_SETTING_KEYS = [
@@ -15,7 +17,12 @@ module Lydown::Rendering
15
17
  ALLOWED_SETTING_VALUES = {
16
18
  'accidentals' => ['manual', 'auto'],
17
19
  'beams' => ['manual', 'auto'],
18
- 'empty_staves' => ['hide', 'show']
20
+ 'empty_staves' => ['hide', 'show'],
21
+ 'instrument_names' => ['hide', 'show', 'inline', 'inline-right-align', 'inline-center-align'],
22
+ 'instrument_name_style' => ['normal', 'smallcaps'],
23
+ 'page_break' => ['none', 'before', 'after', 'before and after', 'blank page before'],
24
+ 'mode' => ['score', 'part', 'none'],
25
+ 'bar_numbers' => ['hide', 'shows']
19
26
  }
20
27
 
21
28
  def translate
@@ -32,26 +39,37 @@ module Lydown::Rendering
32
39
  raise LydownError, "Invalid setting (#{key})"
33
40
  end
34
41
 
42
+ value = check_setting_value(key, value)
43
+
35
44
  if level == 0
36
- value = check_setting_value(key, value)
37
- @context[key] = value
45
+ movement = @context[:movement]
38
46
  case key
39
47
  when 'part'
48
+ @context[:part] = value
40
49
  @context.set_part_context(value)
41
50
 
42
51
  # when changing parts we repeat the last set time and key signature
43
- render_setting('time', @context[:time]) unless @context[:time] == '4/4'
44
-
45
- key = @context[:key]
52
+ time = @context.get_current_setting(:time)
53
+ key = @context.get_current_setting(:key)
54
+
55
+ render_setting('time', time) unless time == '4/4'
46
56
  render_setting('key', key) unless key == 'c major'
47
57
 
48
58
  @context.reset(:part)
49
59
  when 'movement'
60
+ @context[:movement] = value
50
61
  @context.reset(:movement)
51
- when 'midi_tempo'
52
- movement = @context[:movement]
53
- path = "movements/#{movement}/settings/midi_tempo"
54
- @context[path] = value
62
+ when 'include'
63
+ includes = @context.get_current_setting(:includes)
64
+ includes ||= []
65
+ includes << value
66
+ @context.set_setting(:includes, includes)
67
+ when 'mode'
68
+ set_mode(value.nil? ? :none : value.to_sym)
69
+ when 'nomode'
70
+ set_mode(:none)
71
+ else
72
+ @context.set_setting(key, value) unless @event[:ephemeral]
55
73
  end
56
74
 
57
75
  if RENDERABLE_SETTING_KEYS.include?(key)
@@ -64,7 +82,7 @@ module Lydown::Rendering
64
82
  path << "#{@context['process/setting_levels'][l]}/"; l += 1
65
83
  end
66
84
  path << key
67
- @context[path] = value
85
+ @context.set_setting(path, value)
68
86
  end
69
87
 
70
88
  @context['process/setting_levels'] ||= {}
@@ -102,7 +120,7 @@ module Lydown::Rendering
102
120
 
103
121
  unless should_cadence
104
122
  signature = value.sub(/[0-9]+$/) { |m| LILYPOND_DURATIONS[m] || m }
105
- setting << "\\#{key} #{signature} "
123
+ setting << "\\time #{signature} "
106
124
  end
107
125
  when 'key'
108
126
  # If the next event is a key signature, no need to emit this one
@@ -118,6 +136,12 @@ module Lydown::Rendering
118
136
  setting = "\\#{key} #{note} \\#{mode} "
119
137
  when 'clef'
120
138
  setting = "\\#{key} \"#{value}\" "
139
+ # If no music is there, and we're rendering a clef command, we need
140
+ # tell lydown to not render a first clef command inside the Staff
141
+ # context.
142
+ unless @context.get_current_setting(:got_music)
143
+ @context.set_setting(:inhibit_first_clef, true)
144
+ end
121
145
  when 'beams'
122
146
  setting = (value == 'manual') ? '\autoBeamOff ' : '\autoBeamOn '
123
147
  else
@@ -126,6 +150,10 @@ module Lydown::Rendering
126
150
 
127
151
  @context.emit(:music, setting)
128
152
  end
153
+
154
+ def set_mode(mode)
155
+ @context['process/mode'] = (mode == :none) ? nil : mode
156
+ end
129
157
  end
130
158
  end
131
159
 
@@ -1,35 +1,68 @@
1
1
  module Lydown::Rendering
2
+ PROOFING_LY_SETTINGS = <<EOF
3
+ \\paper {
4
+ indent=0\\mm
5
+ line-width=110\\mm
6
+ oddFooterMarkup=##f
7
+ oddHeaderMarkup=##f
8
+ bookTitleMarkup = ##f
9
+ scoreTitleMarkup = ##f
10
+ page-breaking = #ly:minimal-breaking
11
+ }
12
+ EOF
13
+
2
14
  class << self
3
- SKIP_ON_CMD = {type: :command, key: 'set', arguments: ['Score.skipTypesetting = ##t']}
4
- SKIP_OFF_CMD = {type: :command, key: 'set', arguments: ['Score.skipTypesetting = ##f']}
15
+ SKIP_GRACE = {type: :literal, content: "\\grace s8"}
5
16
 
6
- BARLINE = {type: :barline, barline: '|'}
7
- BAR_NUMBERS_CMD = {type: :command, key: 'set', arguments: ['Score.barNumberVisibility = #all-bar-numbers-visible']}
17
+ SKIP_ON_CMD = {type: :literal, content: "\\set Score.skipTypesetting = ##t"}
18
+ SKIP_OFF_CMD = {type: :literal, content: "\\set Score.skipTypesetting = ##f"}
19
+
20
+ SKIP_BARLINE = {type: :literal, content: "\\bar \"\""}
21
+ SKIP_BAR_NUMBERS_CMD = {type: :literal, content: "\\override Score.BarNumber.break-visibility = ##(#t #t #t) \
22
+ \\set Score.barNumberVisibility = #(every-nth-bar-number-visible 1)"}
8
23
 
9
- HIGHLIGHT_NOTES_CMD = {type: :command, key: 'override', arguments: ['NoteHead.color = #red']}
10
- NORMAL_NOTES_CMD = {type: :command, key: 'override', arguments: ['NoteHead.color = #black']}
24
+ HIGHLIGHT_NOTES_CMD = {type: :literal, content: "\\override NoteHead.color = #red"}
25
+ NORMAL_NOTES_CMD = {type: :literal, content: "\\override NoteHead.color = #black"}
11
26
 
12
27
  def insert_skip_markers(stream, line_range)
13
28
  # find indexes of first and last changed lines
14
- changed_first_idx = find_line_idx(stream, line_range.first)
15
- changed_last_idx = find_line_idx(stream, line_range.last, true)
29
+ changed_first_idx = find_line_idx(stream, line_range[0])
30
+ changed_last_idx = find_line_idx(stream, line_range[1], true)
16
31
 
17
32
  # find index of first line to include
18
33
  start_line = line_range.first - 2; start_line = 0 if start_line < 0
19
34
  start_idx = find_line_idx(stream, start_line)
20
35
 
36
+ # find index of last line to include
37
+ end_line = line_range.last + 3
38
+ end_idx = find_line_idx(stream, end_line)
39
+
40
+ if end_idx && end_idx > changed_last_idx
41
+ stream.insert(end_idx, SKIP_ON_CMD)
42
+ stream.insert(end_idx, SKIP_GRACE)
43
+ end
44
+
21
45
  if changed_last_idx
22
46
  stream.insert(changed_last_idx + 1, NORMAL_NOTES_CMD)
23
47
  end
24
48
 
25
49
  if changed_first_idx
26
- stream.insert(changed_first_idx, BARLINE)
27
- stream.insert(changed_first_idx, BAR_NUMBERS_CMD)
28
50
  stream.insert(changed_first_idx, HIGHLIGHT_NOTES_CMD)
29
51
  end
30
52
 
31
53
  if start_line > 0 && start_idx
54
+ # Insert an invisible barline (in order to show bar number for first
55
+ # bar), and show bar numbers on every subsequent barline.
56
+ stream.insert(start_idx, SKIP_BARLINE)
57
+ stream.insert(start_idx, SKIP_BAR_NUMBERS_CMD)
58
+
32
59
  stream.insert(start_idx, SKIP_OFF_CMD)
60
+ # "Help" lilypond correctly render rhythms when skipping on/off by
61
+ # inserting a silent grace note.
62
+ # See https://code.google.com/p/lilypond/issues/detail?id=1543&q=skiptypesetting&colspec=ID%20Type%20Status%20Stars%20Owner%20Patch%20Needs%20Summary
63
+ stream.insert(start_idx, SKIP_GRACE)
64
+
65
+ # Skip beginning
33
66
  stream.insert(0, SKIP_ON_CMD)
34
67
  end
35
68
  end
@@ -1,7 +1,7 @@
1
1
  module Lydown::Rendering
2
2
  module Staff
3
- def self.staff_groups(context, movement, parts)
4
- model = context['score/order'] || movement['score/order'] || DEFAULTS['score/order']
3
+ def self.staff_groups(context, opts, parts)
4
+ model = context.get_setting('score/order', opts)
5
5
  parts_copy = parts.clone
6
6
 
7
7
  groups = []
@@ -28,7 +28,8 @@ module Lydown::Rendering
28
28
  "bracket" => "SystemStartBracket"
29
29
  }
30
30
 
31
- BRACKET_PARTS = %w{soprano alto tenore basso}
31
+ BRACKET_PARTS = %w{soprano alto tenore basso soprano1 soprano2
32
+ alto1 alto2 tenore1 tenore2 basso1 basso2}
32
33
 
33
34
  def self.staff_group_directive(group)
34
35
  if group.size == 1
@@ -42,6 +43,7 @@ module Lydown::Rendering
42
43
 
43
44
  # renders a systemStartDelimiterHierarchy expression
44
45
  def self.staff_hierarchy(staff_groups)
46
+ directive = nil
45
47
  expr = staff_groups.inject('') do |m, group|
46
48
  directive = staff_group_directive(group)
47
49
  if directive
@@ -51,36 +53,90 @@ module Lydown::Rendering
51
53
  end
52
54
  m
53
55
  end
54
-
55
- "#'(SystemStartBracket #{expr})"
56
+
57
+ if (staff_groups.size == 1) && (staff_groups[0].size > 1) && directive == "SystemStartBracket"
58
+ # If all staves are already group, no need to add the system bracket
59
+ "#'#{expr}"
60
+ else
61
+ "#'(SystemStartBar #{expr})"
62
+ end
56
63
  end
57
64
 
58
- def self.clef(part)
59
- DEFAULTS["parts/#{part}/clef"]
65
+ def self.clef(context, opts)
66
+ !context.get_setting(:inhibit_first_clef, opts) &&
67
+ context.get_setting(:clef, opts)
68
+ end
69
+
70
+ def self.prevent_remove_empty(context, opts)
71
+ case context.get_setting(:remove_empty, opts)
72
+ when false, 'false'
73
+ true
74
+ else
75
+ false
76
+ end
60
77
  end
61
78
 
62
- def self.midi_instrument(part)
63
- DEFAULTS["parts/#{part}/midi_instrument"]
79
+ def self.midi_instrument(context, opts)
80
+ context.get_setting(:midi_instrument, opts)
64
81
  end
65
82
 
66
- def self.beaming_mode(part)
67
- beaming = DEFAULTS["parts/#{part}/beaming"]
83
+ def self.beaming_mode(context, opts)
84
+ beaming = context.get_setting(:beaming, opts)
68
85
  return nil if beaming.nil?
69
86
 
70
87
  case beaming
71
88
  when 'auto'
72
- '\autoBeamOn'
89
+ '\\set Staff.autoBeaming = ##t'
73
90
  when 'manual'
74
- '\autoBeamOff'
91
+ '\\set Staff.autoBeaming = ##f'
75
92
  else
76
93
  raise LydownError, "Invalid beaming mode (#{beaming.inspect})"
77
94
  end
78
95
  end
79
96
 
80
- DEFAULT_END_BARLINE = '|.'
97
+ def self.staff_id(part)
98
+ title = Lydown::Rendering.default_part_title(part).gsub(/\s+/, '')
99
+ "#{title}Staff"
100
+ end
101
+
102
+ def self.qualified_part_title(context, opts)
103
+ title = context.get_setting("parts/#{opts[:part]}/title", opts) ||
104
+ Lydown::Rendering.default_part_title(opts[:part])
105
+ title.strip
106
+ end
81
107
 
82
- def self.end_barline(context, movement)
83
- barline = movement['end_barline'] || context['end_barline'] || DEFAULT_END_BARLINE
108
+ def self.part_title(context, opts)
109
+ title = qualified_part_title(context, opts)
110
+
111
+ if title.empty?
112
+ "#\"\" "
113
+ else
114
+ if context.get_setting('instrument_name_style', opts) == 'smallcaps'
115
+ "\\markup { \\smallCaps { #{title} } } "
116
+ else
117
+ "#\"#{title}\" "
118
+ end
119
+ end
120
+ end
121
+
122
+ def self.inline_part_title(context, opts)
123
+ title = qualified_part_title(context, opts)
124
+
125
+ if title.empty?
126
+ "#\"\""
127
+ else
128
+ if context.get_setting('instrument_name_style', opts) == 'smallcaps'
129
+ "<>^\\markup { #{opts[:alignment]} \\smallCaps { #{title} } } "
130
+ else
131
+ "<>^\\markup { #{opts[:alignment]} { #{title} } } "
132
+ end
133
+ end
134
+ end
135
+
136
+ def self.end_barline(context, opts)
137
+ return nil if context['global/settings/inhibit_end_barline']
138
+
139
+ barline = context.get_setting('end_barline', opts)
84
140
  barline == 'none' ? nil : barline
85
141
  end
86
142
  end