Class: OutroRails::Theory::Pitch

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/outro_rails/theory/pitch.rb

Overview

A NoteName pinned to an octave - a concrete key you could play. MIDI convention C4 == 60.

The written octave follows the letter, not the sounding pitch: Cb4 sounds as B3 (midi 59) and B#3 sounds as C4 (midi 60).

Constant Summary collapse

MIDDLE_C_MIDI =

The anchor of the octave numbering: middle C is midi 60 in octave 4.

60
MIDDLE_C_OCTAVE =
4
PARSE_PATTERN =

A letter with optional accidental followed by an octave, which may be negative: "C4", "F#3", "Bb-1".

/\A([A-Ga-g](?:#{NoteName::ACCIDENTAL_PATTERN})?)(-?\d+)\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(note_name, octave) ⇒ Pitch

Takes a NoteName (or anything it parses) and a written octave. Instances are immutable.



60
61
62
63
64
# File 'lib/outro_rails/theory/pitch.rb', line 60

def initialize(note_name, octave)
  @note_name = NoteName.parse(note_name)
  @octave = Integer(octave)
  freeze
end

Instance Attribute Details

#note_nameObject (readonly)

The spelled note and its written octave



23
24
25
# File 'lib/outro_rails/theory/pitch.rb', line 23

def note_name
  @note_name
end

#octaveObject (readonly)

The spelled note and its written octave



23
24
25
# File 'lib/outro_rails/theory/pitch.rb', line 23

def octave
  @octave
end

Class Method Details

.from_midi(midi, prefer: :sharps) ⇒ Object

60 => C4. Prefer :sharps (default) or :flats for black keys.



40
41
42
43
# File 'lib/outro_rails/theory/pitch.rb', line 40

def self.from_midi(midi, prefer: :sharps)
  note = NoteName.for_pitch_class(midi, prefer: prefer)
  new(note, (midi / Interval::SEMITONES_PER_OCTAVE) - 1)
end

.parse(value) ⇒ Object

Accepts a Pitch or a string like 'C4', 'F#3', 'Bb-1'.



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/outro_rails/theory/pitch.rb', line 26

def self.parse(value)
  return value if value.is_a?(self)

  match = value.to_s.strip.match(PARSE_PATTERN)
  unless match
    raise ArgumentError,
          "Invalid pitch: #{value.inspect} " \
          '(expected e.g. "C3", "F#4")'
  end

  new(NoteName.parse(match[1]), Integer(match[2]))
end

.sounding(note_name, midi) ⇒ Object

The pitch that writes as note_name and sounds at midi - the inverse of #midi for a chosen spelling. The written octave follows the letter, so Cb sounding at 59 is Cb4 (not B3), and B# sounding at 60 is B#3.



49
50
51
52
53
54
55
56
# File 'lib/outro_rails/theory/pitch.rb', line 49

def self.sounding(note_name, midi)
  note_name = NoteName.parse(note_name)
  octave =
    ((midi - note_name.accidental_offset) /
      Interval::SEMITONES_PER_OCTAVE) - 1

  new(note_name, octave)
end

Instance Method Details

#<=>(other) ⇒ Object

Sounding-pitch comparison (Cb4 == B3).



107
108
109
# File 'lib/outro_rails/theory/pitch.rb', line 107

def <=>(other)
  midi <=> other.midi
end

#==(other) ⇒ Object Also known as: eql?

Equality is by spelling, not sound: Cb4 != B3.



94
95
96
97
98
# File 'lib/outro_rails/theory/pitch.rb', line 94

def ==(other)
  other.is_a?(self.class) &&
    note_name == other.note_name &&
    octave == other.octave
end

#hashObject

Hashes on spelling too, to stay consistent with #eql?



102
103
104
# File 'lib/outro_rails/theory/pitch.rb', line 102

def hash
  [ note_name, octave ].hash
end

#inspectObject

Shows both the spelling and what it sounds as



89
90
91
# File 'lib/outro_rails/theory/pitch.rb', line 89

def inspect
  "#<#{self.class.name} #{self} midi=#{midi}>"
end

#midiObject

'C4' => 60, 'F#3' => 54, 'Fb4' => 64, 'Cb4' => 59, 'B#3' => 60



67
68
69
70
# File 'lib/outro_rails/theory/pitch.rb', line 67

def midi
  (octave + 1) * Interval::SEMITONES_PER_OCTAVE +
    note_name.letter_natural_midi_offset
end

#pitch_classObject

Sounding pitch class, 0-11, so Cb4 and B3 both give 11.



73
74
75
# File 'lib/outro_rails/theory/pitch.rb', line 73

def pitch_class
  midi % Interval::SEMITONES_PER_OCTAVE
end

#to_sObject

As written, e.g. "F#3"



84
85
86
# File 'lib/outro_rails/theory/pitch.rb', line 84

def to_s
  "#{note_name}#{octave}"
end

#transpose(semitones, prefer: :sharps) ⇒ Object

A new Pitch this many semitones away, respelled from midi - the original spelling is not preserved.



79
80
81
# File 'lib/outro_rails/theory/pitch.rb', line 79

def transpose(semitones, prefer: :sharps)
  self.class.from_midi(midi + semitones, prefer: prefer)
end