Class: HeadMusic::Notation::LilyPond::VoiceWriter

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

Overview

The lines inside a \new Voice block. Writer decides which staves and voices exist; this decides what each one says.

Constant Summary collapse

CLEF_NAMES =

By LilyPond’s name. The octave clefs carry characters LilyPond only accepts inside quotes.

{
  treble_clef: "treble",
  french_violin_clef: "french",
  vocal_tenor_clef: %("treble_8"),
  double_treble_clef: %("treble^8"),
  soprano_clef: "soprano",
  mezzo_soprano_clef: "mezzosoprano",
  alto_clef: "alto",
  tenor_clef: "tenor",
  baritone_c_clef: "baritone",
  baritone_clef: "varbaritone",
  bass_clef: "bass",
  sub_bass_clef: "subbass",
  neutral_clef: "percussion"
}.freeze
MIDDLE_C =

\transposition names the sounding pitch of a written middle C.

"C4"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(plan) ⇒ VoiceWriter

Returns a new instance of VoiceWriter.



27
28
29
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 27

def initialize(plan)
  @plan = plan
end

Instance Attribute Details

#plan ⇒ Object (readonly, private)

Returns the value of attribute plan.



46
47
48
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 46

def plan
  @plan
end

Instance Method Details

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



89
90
91
92
93
94
95
96
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 89

def bar_line(voice, bar_number, part_index)
  tokens = [
    *change_commands(voice.part, bar_number),
    staff_change_command(voice, bar_number, part_index),
    *bar_tokens(voice, bar_number)
  ]
  "#{tokens.compact.join(" ")} |"
end

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



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

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

#change_commands(part, 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
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 101

def change_commands(part, bar_number)
  return [] if bar_number == first_bar_number

  meter = plan.measure_time_changes[bar_number]
  [plan.measure_key_changes(part)[bar_number], meter && time_command(meter)].compact
end

#clef_name(voice, staff) ⇒ Object (private)

The selector is the fallback for a part whose staves were never authored. It reads a voice’s pitch range, which is why the fallback lives here rather than on the staff.



64
65
66
67
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 64

def clef_name(voice, staff)
  clef = staff&.clef_at(first_bar_number) || HeadMusic::Notation::ClefSelector.for(voice)
  clef_word(clef)
end

#clef_word(clef) ⇒ Object (private)



69
70
71
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 69

def clef_word(clef)
  CLEF_NAMES.fetch(clef.name_key.to_sym)
end

#first_bar_number ⇒ Object (private)



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

def first_bar_number
  plan.bar_numbers.first
end

#lines(voice, part_index: nil, staff: nil) ⇒ Object



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

def lines(voice, part_index: nil, staff: nil)
  opening_lines(clef_name(voice, staff), voice.part) +
    plan.bar_numbers.map { |bar_number| bar_line(voice, bar_number, part_index) }
end

#opening_lines(clef_name, part) ⇒ Object (private)



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

def opening_lines(clef_name, part)
  [
    "\\clef #{clef_name}",
    *transposition_line(part),
    plan.first_measure_key(part),
    time_command(plan.first_measure_meter)
  ]
end

#silent_lines(staff, part: nil) ⇒ Object

A staff nobody is written on still has to appear, or a tacet part loses its line in the score. Its rest-filled voice opens like any other so the key and time print and the bars line up.



39
40
41
42
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 39

def silent_lines(staff, part: nil)
  opening_lines(clef_name(nil, staff), part) +
    plan.bar_numbers.map { |bar_number| "#{whole_bar_rest(bar_number)} |" }
end

#staff_change_command(voice, bar_number, part_index) ⇒ Object (private)

Emitted at exactly the bars the voice’s staff-assignment map holds events for, so the crossings and the commands are the same thing.



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

def staff_change_command(voice, bar_number, part_index)
  staff = voice.staff_assignments[bar_number]
  return if part_index.nil? || staff.nil? || bar_number == first_bar_number

  index = voice.part.staff_system_at(bar_number).staves.index { |candidate| candidate.equal?(staff) }
  index && %(\\change Staff = "#{Writer.staff_id(part_index, index)}")
end

#time_command(meter) ⇒ Object (private)



73
74
75
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 73

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

#transposition_line(part) ⇒ Object (private)

What the written pitches sound like. A part carries this rather than a \transpose wrapper, which would move notes that are already written.



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

def transposition_line(part)
  return [] unless plan.transposed?

  instrument = part&.instrument_at(first_bar_number)
  return [] unless instrument&.transposing?

  sounding = HeadMusic::Content::Layout::Transposition.for(instrument).sounding(MIDDLE_C)
  ["\\transposition #{PitchWriter.token(sounding)}"]
end

#whole_bar_rest(bar_number) ⇒ Object (private)



125
126
127
128
# File 'lib/head_music/notation/lily_pond/voice_writer.rb', line 125

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