Class: HeadMusic::Notation::LilyPond::PitchReader

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

Overview

Converts note tokens into pitches in absolute or \relative mode.

The two modes differ only in where octave marks count from: absolute mode counts from c' as middle C, relative mode from the octave that puts the letter within a fourth of the previous note. Accidentals never influence the relative calculation; only letters do.

Constant Summary collapse

LETTERS =
%w[c d e f g a b].freeze
RELATIVE_REACH =

The letter step count beyond which relative mode wraps to the other octave.

3
UNMARKED_REGISTER =

Register 3 is LilyPond's unmarked octave (c is C3), the inverse of PitchWriter.

3
FRAGMENTS_BY_SEMITONES =
{0 => "", 1 => "#", -1 => "b", 2 => "x", -2 => "bb"}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(reference) ⇒ PitchReader

Returns a new instance of PitchReader.



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

def initialize(reference)
  @reference = reference
end

Class Method Details

.absolute ⇒ Object



23
24
25
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 23

def self.absolute
  new(nil)
end

.alterations_by_suffix ⇒ Object

The inverse of the writer's table, built lazily because the reader loads before the writer.



19
20
21
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 19

def self.alterations_by_suffix
  @alterations_by_suffix ||= PitchWriter::ALTERATION_SUFFIXES.invert.freeze
end

.relative(reference_pitch) ⇒ Object



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

def self.relative(reference_pitch)
  new(HeadMusic::Rudiment::Pitch.get(reference_pitch))
end

Instance Method Details

#absolute_register(token) ⇒ Object (private)



56
57
58
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 56

def absolute_register(token)
  UNMARKED_REGISTER + mark_shift(token)
end

#build(token, register) ⇒ Object (private)



82
83
84
85
86
87
88
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 82

def build(token, register)
  resolve(token, register) ||
    raise(ParseError.new(
      %(Pitch "#{token.lexeme}" is out of range),
      line_number: token.line, column: token.column, snippet: token.lexeme
    ))
end

#chord_pitches(tokens) ⇒ Object

Within a chord each note is relative to the one before it, and the chord as a whole leaves its first note as the reference for what follows.



48
49
50
51
52
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 48

def chord_pitches(tokens)
  pitches = tokens.map { |token| pitch(token) }
  @reference = pitches.first if relative?
  pitches
end

#letter_index(token) ⇒ Object (private)



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

def letter_index(token)
  LETTERS.index(token.letter)
end

#mark_shift(token) ⇒ Object (private)



77
78
79
80
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 77

def mark_shift(token)
  marks = token.octave_marks.to_s
  marks.count("'") - marks.count(",")
end

#pitch(token) ⇒ Object



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

def pitch(token)
  register = relative? ? relative_register(token) : absolute_register(token)
  resolved = build(token, register)
  @reference = resolved if relative?
  resolved
end

#relative? ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 35

def relative?
  !@reference.nil?
end

#relative_register(token) ⇒ Object (private)



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

def relative_register(token)
  candidate = @reference.register * LETTERS.length + letter_index(token)
  difference = candidate - step(@reference)
  candidate -= LETTERS.length if difference > RELATIVE_REACH
  candidate += LETTERS.length if difference < -RELATIVE_REACH
  candidate += LETTERS.length * mark_shift(token)
  candidate.div(LETTERS.length)
end

#resolve(token, register) ⇒ Object (private)

A register outside the numbered range is checked before the name is built, because Register.get answers an unreachable number with its default rather than with nil, which would silently move the pitch.



93
94
95
96
97
98
# File 'lib/head_music/notation/lily_pond/pitch_reader.rb', line 93

def resolve(token, register)
  return unless HeadMusic::Rudiment::Register.from_number(register)

  semitones = self.class.alterations_by_suffix.fetch(token.suffix.to_s)
  HeadMusic::Rudiment::Pitch.from_name("#{token.letter.upcase}#{FRAGMENTS_BY_SEMITONES.fetch(semitones)}#{register}")
end

#step(pitch) ⇒ Object (private)



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

def step(pitch)
  pitch.register * LETTERS.length + LETTERS.index(pitch.letter_name.to_s.downcase)
end