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

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

Overview

Renders a HeadMusic::Content::Composition as a complete LilyPond document string: a \version line, a \header carrying the composition’s identity, and a \score with one staff per voice in absolute pitch mode, one line per bar with a trailing bar check.

Whole-composition problems (no voices, positional gaps, barline-crossing notes, unmappable keys, durations, or alterations) 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

Instance Method Summary collapse

Constructor Details

#initialize(composition) ⇒ Writer

Returns a new instance of Writer.



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

def initialize(composition)
  @composition = composition
end

Instance Attribute Details

#compositionObject (readonly)

Returns the value of attribute composition.



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

def composition
  @composition
end

Instance Method Details

#bar_line(voice, bar_number) ⇒ Object (private)

\key is per-staff inside « », so a mid-piece change is emitted in every voice’s stream; \time propagates score-wide, and the duplicate commands are harmless.



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/head_music/notation/lily_pond/writer.rb', line 101

def bar_line(voice, bar_number)
  tokens = []
  unless bar_number == plan.bar_numbers.first
    change_key = plan.measure_key_changes[bar_number]
    tokens << change_key if change_key
    change_meter = plan.measure_time_changes[bar_number]
    tokens << time_command(change_meter) if change_meter
  end
  tokens.concat(bar_tokens(voice, bar_number))
  tokens.join(" ") + " |"
end

#bar_tokens(voice, bar_number) ⇒ Object (private)

A bar with no placements for this voice — a voice that ended early, or an empty voice — fills with a whole-bar rest in the effective meter.



115
116
117
118
119
120
# File 'lib/head_music/notation/lily_pond/writer.rb', line 115

def bar_tokens(voice, bar_number)
  placements = plan.placements_by_bar(voice)[bar_number]
  return [whole_bar_rest(bar_number)] unless placements

  placements.map { |placement| plan.tokens_by_placement[placement] }
end

#clef_name(voice) ⇒ Object (private)

The selector only returns the treble or bass clef.



89
90
91
92
# File 'lib/head_music/notation/lily_pond/writer.rb', line 89

def clef_name(voice)
  clef = HeadMusic::Notation::ClefSelector.for(voice)
  (clef == HeadMusic::Rudiment::Clef.get(:bass_clef)) ? "bass" : "treble"
end

#document_linesObject (private)



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

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

#header_linesObject (private)



43
44
45
46
47
48
49
50
# File 'lib/head_music/notation/lily_pond/writer.rb', line 43

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

#music_lines(voice) ⇒ Object (private)



79
80
81
82
83
84
85
86
# File 'lib/head_music/notation/lily_pond/writer.rb', line 79

def music_lines(voice)
  [
    "\\clef #{clef_name(voice)}",
    plan.first_measure_key,
    time_command(plan.first_measure_meter),
    *plan.bar_numbers.map { |bar_number| bar_line(voice, bar_number) }
  ]
end

#planObject (private)

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



31
32
33
# File 'lib/head_music/notation/lily_pond/writer.rb', line 31

def plan
  @plan ||= RenderPlan.new(composition)
end

#score_linesObject (private)



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

def score_lines
  [
    "\\score {",
    "#{INDENT}<<",
    *composition.voices.flat_map { |voice| staff_lines(voice) },
    "#{INDENT}>>",
    "#{INDENT}\\layout { }",
    "}"
  ]
end

#staff_lines(voice) ⇒ Object (private)



63
64
65
66
67
68
69
70
71
# File 'lib/head_music/notation/lily_pond/writer.rb', line 63

def staff_lines(voice)
  [
    "#{INDENT * 2}#{staff_open(voice)}",
    "#{INDENT * 3}\\new Voice {",
    *music_lines(voice).map { |line| INDENT * 4 + line },
    "#{INDENT * 3}}",
    "#{INDENT * 2}}"
  ]
end

#staff_open(voice) ⇒ Object (private)



73
74
75
76
77
# File 'lib/head_music/notation/lily_pond/writer.rb', line 73

def staff_open(voice)
  return "\\new Staff {" unless voice.role

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

#time_command(meter) ⇒ Object (private)



94
95
96
# File 'lib/head_music/notation/lily_pond/writer.rb', line 94

def time_command(meter)
  "\\time #{meter.top_number}/#{meter.bottom_number}"
end

#to_sObject



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

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

#whole_bar_rest(bar_number) ⇒ Object (private)



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

def whole_bar_rest(bar_number)
  meter = plan.effective_meter(bar_number)
  "R1*#{meter.top_number}/#{meter.bottom_number}"
end