Class: Clef::Core::Pitch

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/clef/core/pitch.rb

Constant Summary collapse

NOTE_VALUES =
{
  c: 0,
  d: 2,
  e: 4,
  f: 5,
  g: 7,
  a: 9,
  b: 11
}.freeze
VALID_NOTE_NAMES =
NOTE_VALUES.keys.freeze
ALTERATION_SUFFIX =
{
  -2 => "eses",
  -1 => "es",
  0 => "",
  1 => "is",
  2 => "isis"
}.freeze
SUFFIX_ALTERATION =
ALTERATION_SUFFIX.invert.freeze
MIDI_CLASS_TO_PITCH =
{
  0 => [:c, 0],
  1 => [:c, 1],
  2 => [:d, 0],
  3 => [:d, 1],
  4 => [:e, 0],
  5 => [:f, 0],
  6 => [:f, 1],
  7 => [:g, 0],
  8 => [:g, 1],
  9 => [:a, 0],
  10 => [:a, 1],
  11 => [:b, 0]
}.freeze
MIDI_CLASS_TO_FLAT_PITCH =
{
  0 => [:c, 0],
  1 => [:d, -1],
  2 => [:d, 0],
  3 => [:e, -1],
  4 => [:e, 0],
  5 => [:f, 0],
  6 => [:g, -1],
  7 => [:g, 0],
  8 => [:a, -1],
  9 => [:a, 0],
  10 => [:b, -1],
  11 => [:b, 0]
}.freeze
SCIENTIFIC_PITCH_REGEX =
/\A([A-Ga-g])([#b]{0,2})(-?\d+)\z/
LILYPOND_PITCH_REGEX =
/\A([a-g])(eses|isis|es|is)?([',]*)\z/
TRANSPOSE_PREFERENCES =
%i[sharp flat].freeze
MIDI_RANGE =
(0..127)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(note_name, octave, alteration: 0) ⇒ Pitch

Returns a new instance of Pitch.

Parameters:

  • note_name (Symbol)
  • octave (Integer)
  • alteration (Integer) (defaults to: 0)


64
65
66
67
68
69
70
71
72
73
# File 'lib/clef/core/pitch.rb', line 64

def initialize(note_name, octave, alteration: 0)
  validate_note_name!(note_name)
  validate_octave!(octave)
  validate_alteration!(alteration)

  @note_name = note_name
  @octave = octave
  @alteration = alteration
  freeze
end

Instance Attribute Details

#alterationObject (readonly)

Returns the value of attribute alteration.



59
60
61
# File 'lib/clef/core/pitch.rb', line 59

def alteration
  @alteration
end

#note_nameObject (readonly)

Returns the value of attribute note_name.



59
60
61
# File 'lib/clef/core/pitch.rb', line 59

def note_name
  @note_name
end

#octaveObject (readonly)

Returns the value of attribute octave.



59
60
61
# File 'lib/clef/core/pitch.rb', line 59

def octave
  @octave
end

Class Method Details

.parse(str) ⇒ Pitch

Parameters:

  • str (String)

Returns:

Raises:

  • (ArgumentError)


131
132
133
134
135
136
137
138
139
140
141
# File 'lib/clef/core/pitch.rb', line 131

def self.parse(str)
  raise ArgumentError, "pitch string must be a String" unless str.is_a?(String)

  match = LILYPOND_PITCH_REGEX.match(str)
  raise ArgumentError, "invalid lilypond pitch: #{str}" unless match

  note_name = match[1].to_sym
  suffix = match[2] || ""
  octave = 3 + match[3].count("'") - match[3].count(",")
  new(note_name, octave, alteration: SUFFIX_ALTERATION.fetch(suffix))
end

.parse_any(value) ⇒ Pitch

Parameters:

  • value (String, Symbol, Pitch)

Returns:



158
159
160
161
162
163
164
165
# File 'lib/clef/core/pitch.rb', line 158

def self.parse_any(value)
  return value if value.is_a?(self)
  return parse(value.to_s.downcase) if value.is_a?(Symbol)

  parse_scientific(value)
rescue ArgumentError
  parse(value.to_s.downcase)
end

.parse_scientific(str) ⇒ Pitch

Parameters:

  • str (String)

Returns:

Raises:

  • (ArgumentError)


145
146
147
148
149
150
151
152
153
154
# File 'lib/clef/core/pitch.rb', line 145

def self.parse_scientific(str)
  raise ArgumentError, "pitch string must be a String" unless str.is_a?(String)

  match = SCIENTIFIC_PITCH_REGEX.match(str)
  raise ArgumentError, "invalid scientific pitch: #{str}" unless match

  note_name = match[1].downcase.to_sym
  alteration = {"" => 0, "#" => 1, "##" => 2, "b" => -1, "bb" => -2}.fetch(match[2])
  new(note_name, match[3].to_i, alteration: alteration)
end

Instance Method Details

#<=>(other) ⇒ Integer?

Parameters:

Returns:

  • (Integer, nil)


118
119
120
121
122
# File 'lib/clef/core/pitch.rb', line 118

def <=>(other)
  return nil unless other.is_a?(self.class)

  semitones <=> other.semitones
end

#enharmonic?(other) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


112
113
114
# File 'lib/clef/core/pitch.rb', line 112

def enharmonic?(other)
  other.is_a?(self.class) && semitones == other.semitones
end

#semitonesInteger

Returns:

  • (Integer)


84
85
86
# File 'lib/clef/core/pitch.rb', line 84

def semitones
  octave * 12 + NOTE_VALUES.fetch(note_name) + alteration
end

#to_frequency(tuning: 440.0) ⇒ Float

Parameters:

  • tuning (Float) (defaults to: 440.0)

Returns:

  • (Float)


90
91
92
# File 'lib/clef/core/pitch.rb', line 90

def to_frequency(tuning: 440.0)
  tuning * (2.0**((to_midi - 69) / 12.0))
end

#to_lilypondString

Returns:

  • (String)


125
126
127
# File 'lib/clef/core/pitch.rb', line 125

def to_lilypond
  [note_name, ALTERATION_SUFFIX.fetch(alteration), octave_marks].join
end

#to_midiInteger

Returns:

  • (Integer)

Raises:

  • (RangeError)


76
77
78
79
80
81
# File 'lib/clef/core/pitch.rb', line 76

def to_midi
  midi = semitones + 12
  raise RangeError, "MIDI pitch out of range: #{midi}" unless MIDI_RANGE.cover?(midi)

  midi
end

#transpose(semitones_or_interval, prefer: nil, key_signature: nil) ⇒ Pitch

Parameters:

  • semitones_or_interval (Integer, #semitones)
  • prefer (Symbol, nil) (defaults to: nil)
  • key_signature (KeySignature, nil) (defaults to: nil)

Returns:

Raises:

  • (RangeError)


98
99
100
101
102
103
104
105
106
107
108
# File 'lib/clef/core/pitch.rb', line 98

def transpose(semitones_or_interval, prefer: nil, key_signature: nil)
  spelling = transpose_preference(prefer, key_signature)

  target_midi = to_midi + normalize_semitones(semitones_or_interval)
  raise RangeError, "MIDI pitch out of range: #{target_midi}" unless MIDI_RANGE.cover?(target_midi)

  octave = (target_midi / 12) - 1
  pitch_map = (spelling == :flat) ? MIDI_CLASS_TO_FLAT_PITCH : MIDI_CLASS_TO_PITCH
  note_name, alteration = pitch_map.fetch(target_midi % 12)
  self.class.new(note_name, octave, alteration: alteration)
end