Class: HeadMusic::Notation::LilyPond::Writer

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/notation/lily_pond/writer.rb

Overview

Renders a flow as a complete LilyPond document, assembled down to the \new Voice block; VoiceWriter serializes what goes inside it. Whole-flow problems raise before any assembly, so #to_s only ever returns a complete document.

Constant Summary collapse

LILYPOND_VERSION =
"2.24.0"
INDENT =
"  "

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(flow, transposed: false, arranger: nil) ⇒ Writer

Returns a new instance of Writer.



20
21
22
23
24
# File 'lib/head_music/notation/lily_pond/writer.rb', line 20

def initialize(flow, transposed: false, arranger: nil)
  @flow = flow
  @transposed = transposed
  @arranger = arranger
end

Instance Attribute Details

#arranger ⇒ Object (readonly)

Returns the value of attribute arranger.



10
11
12
# File 'lib/head_music/notation/lily_pond/writer.rb', line 10

def arranger
  @arranger
end

#flow ⇒ Object (readonly)

Returns the value of attribute flow.



10
11
12
# File 'lib/head_music/notation/lily_pond/writer.rb', line 10

def flow
  @flow
end

#transposed ⇒ Object (readonly)

Returns the value of attribute transposed.



10
11
12
# File 'lib/head_music/notation/lily_pond/writer.rb', line 10

def transposed
  @transposed
end

Class Method Details

.staff_id(part_index, staff_index) ⇒ Object

The name a grouped staff is declared with, and that a \change Staff command refers back to.



16
17
18
# File 'lib/head_music/notation/lily_pond/writer.rb', line 16

def self.staff_id(part_index, staff_index)
  "part#{part_index + 1}-staff#{staff_index + 1}"
end

Instance Method Details

#document_lines ⇒ Object (private)



52
53
54
55
56
57
58
# File 'lib/head_music/notation/lily_pond/writer.rb', line 52

def document_lines
  [
    %(\\version "#{LILYPOND_VERSION}"),
    *header_lines,
    *score_lines
  ]
end

#group_open(part) ⇒ Object (private)



107
108
109
# File 'lib/head_music/notation/lily_pond/writer.rb', line 107

def group_open(part)
  (part.staff_system.bracket == :bracket) ? "\\new StaffGroup <<" : "\\new PianoStaff <<"
end

#grouped_staff_lines(part, part_index, staff, staff_index) ⇒ Object (private)



111
112
113
114
115
116
# File 'lib/head_music/notation/lily_pond/writer.rb', line 111

def grouped_staff_lines(part, part_index, staff, staff_index)
  voices = part.voices.select { |voice| voice.staff.equal?(staff) }
  voices_lines = voices.map { |voice| voice_writer.lines(voice, part_index: part_index, staff: staff) }
  voices_lines = [voice_writer.silent_lines(staff, part: part)] if voices_lines.empty?
  staff_block(%(\\new Staff = "#{Writer.staff_id(part_index, staff_index)}" <<), voices_lines, ">>")
end

#header_lines ⇒ Object (private)



60
61
62
63
64
65
66
67
68
# File 'lib/head_music/notation/lily_pond/writer.rb', line 60

def header_lines
  [
    "\\header {",
    %(#{INDENT}title = "#{StringText.escape(name)}"),
    composer && %(#{INDENT}composer = "#{StringText.escape(composer)}"),
    arranger && %(#{INDENT}arranger = "#{StringText.escape(arranger)}"),
    "}"
  ].compact
end

#part_lines(part, part_index) ⇒ Object (private)

A part on one staff holding one voice renders exactly as a voice used to, which is what keeps every existing document byte-identical.



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/head_music/notation/lily_pond/writer.rb', line 95

def part_lines(part, part_index)
  return single_staff_lines(part) if part.staff_system.length == 1

  [
    "#{INDENT * 2}#{group_open(part)}",
    *part.staff_system.staves.each_with_index.flat_map { |staff, staff_index|
      grouped_staff_lines(part, part_index, staff, staff_index).map { |line| INDENT + line }
    },
    "#{INDENT * 2}>>"
  ]
end

#part_name(part) ⇒ Object (private)

A staff of several voices, or of none, is named for its part rather than for any one voice's role.



129
130
131
# File 'lib/head_music/notation/lily_pond/writer.rb', line 129

def part_name(part)
  part.player&.name || part.instrument&.name
end

#piece_lines(piece) ⇒ Object (private)

Absent from a document holding one score, where the \header title says it.



83
84
85
86
87
88
89
90
91
# File 'lib/head_music/notation/lily_pond/writer.rb', line 83

def piece_lines(piece)
  return [] unless piece

  [
    "#{INDENT}\\header {",
    %(#{INDENT * 2}piece = "#{StringText.escape(piece)}"),
    "#{INDENT}}"
  ]
end

#plan ⇒ Object (private)

The computed rendering facts. Built here — before assembly — so an unmappable key or duration raises before any output is produced.



44
45
46
# File 'lib/head_music/notation/lily_pond/writer.rb', line 44

def plan
  @plan ||= RenderPlan.new(flow, transposed: transposed)
end

#score_block(piece: nil) ⇒ Object

The \score block alone, checked and planned as a whole document would be: a book shares one \version and one \header across several of these.



34
35
36
37
38
# File 'lib/head_music/notation/lily_pond/writer.rb', line 34

def score_block(piece: nil)
  Preflight.check!(flow)
  plan
  score_lines(piece: piece)
end

#score_lines(piece: nil) ⇒ Object (private)



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/head_music/notation/lily_pond/writer.rb', line 70

def score_lines(piece: nil)
  [
    "\\score {",
    "#{INDENT}<<",
    *parts.each_with_index.flat_map { |part, index| part_lines(part, index) },
    "#{INDENT}>>",
    *piece_lines(piece),
    "#{INDENT}\\layout { }",
    "}"
  ]
end

#single_staff_lines(part) ⇒ Object (private)



118
119
120
121
122
123
124
125
# File 'lib/head_music/notation/lily_pond/writer.rb', line 118

def single_staff_lines(part)
  staff = part.staff_system.first_staff
  case part.voices.length
  when 0 then staff_block(staff_open(part_name(part), "{"), [voice_writer.silent_lines(staff, part: part)], "}")
  when 1 then staff_block(staff_open(part.voices.first.role, "{"), [voice_writer.lines(part.voices.first, staff: staff)], "}")
  else staff_block(staff_open(part_name(part), "<<"), part.voices.map { |voice| voice_writer.lines(voice, staff: staff) }, ">>")
  end
end

#staff_block(opening, voices_lines, closer) ⇒ Object (private)



139
140
141
142
143
144
145
# File 'lib/head_music/notation/lily_pond/writer.rb', line 139

def staff_block(opening, voices_lines, closer)
  [
    "#{INDENT * 2}#{opening}",
    *voices_lines.flat_map { |lines| voice_block(lines) }.map { |line| INDENT * 3 + line },
    "#{INDENT * 2}#{closer}"
  ]
end

#staff_open(name, opener) ⇒ Object (private)



133
134
135
136
137
# File 'lib/head_music/notation/lily_pond/writer.rb', line 133

def staff_open(name, opener)
  return "\\new Staff #{opener}" unless name

  %(\\new Staff \\with { instrumentName = "#{StringText.escape(name)}" } #{opener})
end

#to_s ⇒ Object



26
27
28
29
30
# File 'lib/head_music/notation/lily_pond/writer.rb', line 26

def to_s
  Preflight.check!(flow)
  plan
  document_lines.join("\n") + "\n"
end

#voice_block(lines) ⇒ Object (private)



147
148
149
# File 'lib/head_music/notation/lily_pond/writer.rb', line 147

def voice_block(lines)
  ["\\new Voice {", *lines.map { |line| INDENT + line }, "}"]
end

#voice_writer ⇒ Object (private)



48
49
50
# File 'lib/head_music/notation/lily_pond/writer.rb', line 48

def voice_writer
  @voice_writer ||= VoiceWriter.new(plan)
end