Class: Musicality::Pitch

Inherits:
Object
  • Object
show all
Includes:
Comparable, Parseable, Packable
Defined in:
lib/musicality/notation/model/pitch.rb,
lib/musicality/pitch_class.rb,
lib/musicality/printing/lilypond/pitch_engraving.rb,
lib/musicality/notation/parsing/convenience_methods.rb

Overview

Author:

  • James Tunnell

Constant Summary collapse

SEMITONES_PER_OCTAVE =

The default number of semitones per octave is 12, corresponding to the twelve-tone equal temperment tuning system.

PitchClass::MOD
CENTS_PER_SEMITONE =
100
CENTS_PER_OCTAVE =
SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE
BASE_FREQ =

The base ferquency is C0

16.351597831287414
PARSER =
Parsing::PitchParser.new
CONVERSION_METHOD =
:to_pitch

Constants included from Parseable

Musicality::Parseable::DEFAULT_SPLIT_PATTERN

Constants included from Packable

Packable::PACKED_CLASS_KEY

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Parseable

included

Methods included from Packable

#class_str, included, #init_params, #pack, pack_val, recover_class, unpack_val

Constructor Details

#initialize(octave: 0, semitone: 0, cent: 0) ⇒ Pitch

Returns a new instance of Pitch.

Raises:



35
36
37
38
39
40
41
42
43
44
45
# File 'lib/musicality/notation/model/pitch.rb', line 35

def initialize octave:0, semitone:0, cent: 0
  raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
  raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
  raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
  
  @octave = octave
  @semitone = semitone
  @cent = cent
  @total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
  balance!
end

Instance Attribute Details

#centObject (readonly)

Returns the value of attribute cent.



24
25
26
# File 'lib/musicality/notation/model/pitch.rb', line 24

def cent
  @cent
end

#octaveInteger (readonly)

Returns The pitch octave.

Returns:



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/musicality/notation/model/pitch.rb', line 20

class Pitch
  include Comparable
  include Packable
  
  attr_reader :octave, :semitone, :cent, :total_cents

  #The default number of semitones per octave is 12, corresponding to
  # the twelve-tone equal temperment tuning system.
  SEMITONES_PER_OCTAVE = PitchClass::MOD
  CENTS_PER_SEMITONE = 100
  CENTS_PER_OCTAVE = SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE

  # The base ferquency is C0
  BASE_FREQ = 16.351597831287414

  def initialize octave:0, semitone:0, cent: 0
    raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
    raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
    raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
    
    @octave = octave
    @semitone = semitone
    @cent = cent
    @total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
    balance!
  end

  # Return the pitch's frequency, which is determined by multiplying the base
  # frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
  # but can be set during initialization to something else by specifying the
  # :base_freq key.
  def freq
    return self.ratio() * BASE_FREQ
  end

  # Calculate the pitch ratio. Raises 2 to the power of the total cent
  # count divided by cents-per-octave.
  # @return [Float] ratio
  def ratio
    2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
  end

  # Override default hash method.
  def hash
    return @total_cents
  end

  # Compare pitch equality using total semitone
  def ==(other)
    return (self.class == other.class &&
      @total_cents == other.total_cents)
  end

  def eql?(other)
    self == other
  end

  # Compare pitches. A higher ratio or total semitone is considered larger.
  # @param [Pitch] other The pitch object to compare.
  def <=> (other)
    @total_cents <=> other.total_cents
  end

  # rounds to the nearest semitone
  def round
    if @cent == 0
      self.clone
    else
      Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
    end
  end

  # diff in (rounded) semitones
  def diff other
    Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
  end

  def transpose semitones
    Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
  end
  
  def + semitones
    transpose(semitones)
  end

  def - semitones
    transpose(-semitones)
  end
  
  def total_semitones
    Rational(@total_cents, CENTS_PER_SEMITONE)
  end
  
  def self.from_semitones semitones
    Pitch.new(cent: (semitones * CENTS_PER_SEMITONE).round)
  end
  
  def clone
    Pitch.new(cent: @total_cents)
  end

  def natural?
    [0,2,4,5,7,9,11].include?(semitone)
  end

  def self.pc_str semitone, sharpit
    case semitone
    when 0 then "C"
    when 1 then sharpit  ? "C#" : "Db"
    when 2 then "D"
    when 3 then sharpit  ? "D#" : "Eb"
    when 4 then "E"
    when 5 then "F"
    when 6 then sharpit  ? "F#" : "Gb"
    when 7 then "G"
    when 8 then sharpit  ? "G#" : "Ab"
    when 9 then "A"
    when 10 then sharpit  ? "A#" : "Bb"
    when 11 then "B"
    end
  end

  def to_s(sharpit = false)
    letter = Pitch.pc_str(semitone, sharpit)
    if @cent == 0
      return letter + octave.to_s
    elsif @cent > 0
      return letter + octave.to_s + "+" + @cent.to_s
    else
      return letter + octave.to_s + @cent.to_s
    end
  end

  def self.from_ratio ratio
    raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
    x = Math.log2 ratio
    new(cent: (x * CENTS_PER_OCTAVE).round)
  end

  def self.from_freq freq
    from_ratio(freq / BASE_FREQ)
  end

  private

  # Balance out the octave and semitone count.
  def balance!
    centsTotal = @total_cents
    
    @octave = centsTotal / CENTS_PER_OCTAVE
    centsTotal -= @octave * CENTS_PER_OCTAVE
    
    @semitone = centsTotal / CENTS_PER_SEMITONE
    centsTotal -= @semitone * CENTS_PER_SEMITONE
    
    @cent = centsTotal
    return self
  end
end

#semitoneInteger (readonly)

Returns The pitch semitone.

Returns:

  • (Integer)

    The pitch semitone.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/musicality/notation/model/pitch.rb', line 20

class Pitch
  include Comparable
  include Packable
  
  attr_reader :octave, :semitone, :cent, :total_cents

  #The default number of semitones per octave is 12, corresponding to
  # the twelve-tone equal temperment tuning system.
  SEMITONES_PER_OCTAVE = PitchClass::MOD
  CENTS_PER_SEMITONE = 100
  CENTS_PER_OCTAVE = SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE

  # The base ferquency is C0
  BASE_FREQ = 16.351597831287414

  def initialize octave:0, semitone:0, cent: 0
    raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
    raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
    raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
    
    @octave = octave
    @semitone = semitone
    @cent = cent
    @total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
    balance!
  end

  # Return the pitch's frequency, which is determined by multiplying the base
  # frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
  # but can be set during initialization to something else by specifying the
  # :base_freq key.
  def freq
    return self.ratio() * BASE_FREQ
  end

  # Calculate the pitch ratio. Raises 2 to the power of the total cent
  # count divided by cents-per-octave.
  # @return [Float] ratio
  def ratio
    2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
  end

  # Override default hash method.
  def hash
    return @total_cents
  end

  # Compare pitch equality using total semitone
  def ==(other)
    return (self.class == other.class &&
      @total_cents == other.total_cents)
  end

  def eql?(other)
    self == other
  end

  # Compare pitches. A higher ratio or total semitone is considered larger.
  # @param [Pitch] other The pitch object to compare.
  def <=> (other)
    @total_cents <=> other.total_cents
  end

  # rounds to the nearest semitone
  def round
    if @cent == 0
      self.clone
    else
      Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
    end
  end

  # diff in (rounded) semitones
  def diff other
    Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
  end

  def transpose semitones
    Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
  end
  
  def + semitones
    transpose(semitones)
  end

  def - semitones
    transpose(-semitones)
  end
  
  def total_semitones
    Rational(@total_cents, CENTS_PER_SEMITONE)
  end
  
  def self.from_semitones semitones
    Pitch.new(cent: (semitones * CENTS_PER_SEMITONE).round)
  end
  
  def clone
    Pitch.new(cent: @total_cents)
  end

  def natural?
    [0,2,4,5,7,9,11].include?(semitone)
  end

  def self.pc_str semitone, sharpit
    case semitone
    when 0 then "C"
    when 1 then sharpit  ? "C#" : "Db"
    when 2 then "D"
    when 3 then sharpit  ? "D#" : "Eb"
    when 4 then "E"
    when 5 then "F"
    when 6 then sharpit  ? "F#" : "Gb"
    when 7 then "G"
    when 8 then sharpit  ? "G#" : "Ab"
    when 9 then "A"
    when 10 then sharpit  ? "A#" : "Bb"
    when 11 then "B"
    end
  end

  def to_s(sharpit = false)
    letter = Pitch.pc_str(semitone, sharpit)
    if @cent == 0
      return letter + octave.to_s
    elsif @cent > 0
      return letter + octave.to_s + "+" + @cent.to_s
    else
      return letter + octave.to_s + @cent.to_s
    end
  end

  def self.from_ratio ratio
    raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
    x = Math.log2 ratio
    new(cent: (x * CENTS_PER_OCTAVE).round)
  end

  def self.from_freq freq
    from_ratio(freq / BASE_FREQ)
  end

  private

  # Balance out the octave and semitone count.
  def balance!
    centsTotal = @total_cents
    
    @octave = centsTotal / CENTS_PER_OCTAVE
    centsTotal -= @octave * CENTS_PER_OCTAVE
    
    @semitone = centsTotal / CENTS_PER_SEMITONE
    centsTotal -= @semitone * CENTS_PER_SEMITONE
    
    @cent = centsTotal
    return self
  end
end

#total_centsObject (readonly)

Returns the value of attribute total_cents.



24
25
26
# File 'lib/musicality/notation/model/pitch.rb', line 24

def total_cents
  @total_cents
end

Class Method Details

.from_freq(freq) ⇒ Object



159
160
161
# File 'lib/musicality/notation/model/pitch.rb', line 159

def self.from_freq freq
  from_ratio(freq / BASE_FREQ)
end

.from_ratio(ratio) ⇒ Object

Raises:



153
154
155
156
157
# File 'lib/musicality/notation/model/pitch.rb', line 153

def self.from_ratio ratio
  raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
  x = Math.log2 ratio
  new(cent: (x * CENTS_PER_OCTAVE).round)
end

.from_semitones(semitones) ⇒ Object



113
114
115
# File 'lib/musicality/notation/model/pitch.rb', line 113

def self.from_semitones semitones
  Pitch.new(cent: (semitones * CENTS_PER_SEMITONE).round)
end

.pc_str(semitone, sharpit) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/musicality/notation/model/pitch.rb', line 125

def self.pc_str semitone, sharpit
  case semitone
  when 0 then "C"
  when 1 then sharpit  ? "C#" : "Db"
  when 2 then "D"
  when 3 then sharpit  ? "D#" : "Eb"
  when 4 then "E"
  when 5 then "F"
  when 6 then sharpit  ? "F#" : "Gb"
  when 7 then "G"
  when 8 then sharpit  ? "G#" : "Ab"
  when 9 then "A"
  when 10 then sharpit  ? "A#" : "Bb"
  when 11 then "B"
  end
end

Instance Method Details

#+(semitones) ⇒ Object



101
102
103
# File 'lib/musicality/notation/model/pitch.rb', line 101

def + semitones
  transpose(semitones)
end

#-(semitones) ⇒ Object



105
106
107
# File 'lib/musicality/notation/model/pitch.rb', line 105

def - semitones
  transpose(-semitones)
end

#<=>(other) ⇒ Object

Compare pitches. A higher ratio or total semitone is considered larger.

Parameters:

  • other (Pitch)

    The pitch object to compare.



79
80
81
# File 'lib/musicality/notation/model/pitch.rb', line 79

def <=> (other)
  @total_cents <=> other.total_cents
end

#==(other) ⇒ Object

Compare pitch equality using total semitone



68
69
70
71
# File 'lib/musicality/notation/model/pitch.rb', line 68

def ==(other)
  return (self.class == other.class &&
    @total_cents == other.total_cents)
end

#cloneObject



117
118
119
# File 'lib/musicality/notation/model/pitch.rb', line 117

def clone
  Pitch.new(cent: @total_cents)
end

#diff(other) ⇒ Object

diff in (rounded) semitones



93
94
95
# File 'lib/musicality/notation/model/pitch.rb', line 93

def diff other
  Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


73
74
75
# File 'lib/musicality/notation/model/pitch.rb', line 73

def eql?(other)
  self == other
end

#freqObject

Return the pitch’s frequency, which is determined by multiplying the base frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ, but can be set during initialization to something else by specifying the :base_freq key.



51
52
53
# File 'lib/musicality/notation/model/pitch.rb', line 51

def freq
  return self.ratio() * BASE_FREQ
end

#hashObject

Override default hash method.



63
64
65
# File 'lib/musicality/notation/model/pitch.rb', line 63

def hash
  return @total_cents
end

#natural?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'lib/musicality/notation/model/pitch.rb', line 121

def natural?
  [0,2,4,5,7,9,11].include?(semitone)
end

#ratioFloat

Calculate the pitch ratio. Raises 2 to the power of the total cent count divided by cents-per-octave.

Returns:

  • (Float)

    ratio



58
59
60
# File 'lib/musicality/notation/model/pitch.rb', line 58

def ratio
  2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
end

#roundObject

rounds to the nearest semitone



84
85
86
87
88
89
90
# File 'lib/musicality/notation/model/pitch.rb', line 84

def round
  if @cent == 0
    self.clone
  else
    Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
  end
end

#to_lilypond(sharpit = false) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
# File 'lib/musicality/printing/lilypond/pitch_engraving.rb', line 4

def to_lilypond sharpit = false
  output = PitchClass.to_lilypond(semitone, sharpit)

  if octave > 3
    output += "'"*(octave - 3)
  elsif octave < 3
    output += ","*(3 - octave)
  end

  return output
end

#to_pcObject



16
17
18
# File 'lib/musicality/pitch_class.rb', line 16

def to_pc
  PitchClass.from_i semitone
end

#to_s(sharpit = false) ⇒ Object



142
143
144
145
146
147
148
149
150
151
# File 'lib/musicality/notation/model/pitch.rb', line 142

def to_s(sharpit = false)
  letter = Pitch.pc_str(semitone, sharpit)
  if @cent == 0
    return letter + octave.to_s
  elsif @cent > 0
    return letter + octave.to_s + "+" + @cent.to_s
  else
    return letter + octave.to_s + @cent.to_s
  end
end

#total_semitonesObject



109
110
111
# File 'lib/musicality/notation/model/pitch.rb', line 109

def total_semitones
  Rational(@total_cents, CENTS_PER_SEMITONE)
end

#transpose(semitones) ⇒ Object



97
98
99
# File 'lib/musicality/notation/model/pitch.rb', line 97

def transpose semitones
  Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
end