Class: HeadMusic::Notation::LilyPond::Writer
- Inherits:
-
Object
- Object
- HeadMusic::Notation::LilyPond::Writer
- 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
-
#arranger ⇒ Object
readonly
Returns the value of attribute arranger.
-
#flow ⇒ Object
readonly
Returns the value of attribute flow.
-
#transposed ⇒ Object
readonly
Returns the value of attribute transposed.
Class Method Summary collapse
-
.staff_id(part_index, staff_index) ⇒ Object
The name a grouped staff is declared with, and that a \change Staff command refers back to.
Instance Method Summary collapse
- #document_lines ⇒ Object private
- #group_open(part) ⇒ Object private
- #grouped_staff_lines(part, part_index, staff, staff_index) ⇒ Object private
- #header_lines ⇒ Object private
-
#initialize(flow, transposed: false, arranger: nil) ⇒ Writer
constructor
A new instance of Writer.
-
#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.
-
#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.
-
#piece_lines(piece) ⇒ Object
private
Absent from a document holding one score, where the \header title says it.
-
#plan ⇒ Object
private
The computed rendering facts.
-
#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.
- #score_lines(piece: nil) ⇒ Object private
- #single_staff_lines(part) ⇒ Object private
- #staff_block(opening, voices_lines, closer) ⇒ Object private
- #staff_open(name, opener) ⇒ Object private
- #to_s ⇒ Object
- #voice_block(lines) ⇒ Object private
- #voice_writer ⇒ Object private
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 |