head_music 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5e3afc9754608ddd1d1c10b191f356d4c200733ebfd9e9294693309d7ab53d9
4
- data.tar.gz: f92fbe212261872b48e41d1189289f7a0b9aea7643e060a3e96ed223746f1089
3
+ metadata.gz: 5ce84c55ee00f24b71bcedf973bd5cab1c1d9a7150ebc92bb0508ccd40c3038d
4
+ data.tar.gz: '084c59660f40b4cc8746499939a38086d4df92180e4b75c37272d4835092fea0'
5
5
  SHA512:
6
- metadata.gz: 30fac39ee9b758610839343fb7e879ec5422485a83240be3b535be261fc5e31d9f6a2f66c8a34523f90bb95c9f6c3c836ba1a404d07a40634bc2963a1cb584c3
7
- data.tar.gz: ef1240d4dcc7b9d159967a1dcad186867c2dc1f7cac8e33df295d77452bb75d0a1888d9d3a99800aeff60d7a00dca94d7a9ece30d8190769ebcacffbceccedf5
6
+ metadata.gz: 91a8fc2f37f9d6b83be92b4b8d5a5f1efddaac4893202df14255fc456d6a60fa74807b01c6efa6e4f72013e6a33e02884a9685af904ee8b5e7ce07f75e08a939
7
+ data.tar.gz: 0a34937e07c4742d031055ccc5661a15b3373c4b86dbd02b12fe91df7b2c87e50f1c767e7d34119e4184fc76b103f74eb3fdb3b7735c94cb34c9cb97d3270d19
data/TODO.md CHANGED
@@ -1,18 +1,113 @@
1
1
  # TODO
2
2
 
3
+ Disambiguate PitchSet and Sonority
4
+
5
+ Sonority should be a name for a specific set of intervals
6
+ Sonority.get(identifier)
7
+ Sonority.for(pitch_set)
8
+ Sonority.pitch_set_for(root_pitch:, inversion:)
9
+
10
+ class PitchSet
11
+ def sonority
12
+ @sonority ||= Sonority.for(self)
13
+ end
14
+ end
15
+
16
+
17
+
18
+ open consonance (P5 P8)
19
+ soft consonance (m3 M3 m6 M6)
20
+ mild dissonance (M2 m7)
21
+ sharp dissonance (m2 M7)
22
+
23
+ P4 (consonant or dissonant)
24
+ T (neutral or restless)
25
+
26
+ Sets
27
+ DurationSet?
28
+
29
+
30
+ Make new analysis classes:
31
+ Dyad
32
+ .interval
33
+ .implied_triad (if a third)
34
+ .possible_triads
35
+ - returns major and minor if a perfect fifth
36
+ - returns minor and diminished if minor third
37
+ - returns major and augmented if major third
38
+ - returns augmented if augmented fifth
39
+ - returns diminished if diminished fifth
40
+ - should it take into account enharmonics? I think yes.
41
+ .possible_seventh_chords
42
+ - as above, with either seventh added
43
+ - returns 3rd inversion if second
44
+ .possible_chords
45
+ possible_triads + possible_seventh_chords
46
+ .possible_enharmonic_triads
47
+ .possible_enharmonic_seventh_chords
48
+ .possible_enharmonic_chords
49
+
50
+ the dyad will be super helpful in analyzing two-part counterpoint.
51
+
52
+ Triad
53
+ SeventhChord
54
+ Don't need anything beyond seventh chords to analyze pre-Romantic music.
55
+
56
+
3
57
  ## User stories
4
58
 
59
+
5
60
  ### Done
6
61
 
62
+ As a developer
63
+ When instantiating a DiatonicInterval
64
+ When passing an abbreviation, such as 'P5' or 'm2'
65
+ I want to receive that instance.
66
+
7
67
  As a developer
8
68
  Given a pitch
9
- I want to be able to add a functional interval to get another pitch.
69
+ I want to be able to add a diatonic interval to get another pitch.
10
70
 
11
- FunctionalInterval
71
+ DiatonicInterval
12
72
  - def above(pitch) -> pitch
13
- FunctionalInterval
73
+ DiatonicInterval
14
74
  - def below(pitch) -> pitch
15
75
 
16
76
  Pitch addition and subtraction
17
77
  - define `Pitch#+`, `Pitch#-`
18
- - use FunctionalInterval methods
78
+ - use DiatonicInterval methods
79
+
80
+ PitchSet
81
+
82
+ A PitchSet is unlike a PitchClassSet in that the pitches have spellings with octaves rather than Spellings only or octave-less 0-11 designations.
83
+
84
+ PitchClassSet
85
+ .size?
86
+ .monad?
87
+ .dyad?
88
+ .triad? (must be stacked thirds to be a 'triad')
89
+ .trichord? (all 3-pitch sets)
90
+
91
+ Should every group of pitches have one or more strategies for describing it? Such as Dyad?
92
+
93
+ Set (superclass?)
94
+ PitchSet
95
+ EmptySet
96
+ Monad
97
+ Dyad
98
+ Trichord (or Triad)
99
+ - triad?
100
+ Tetrachord (or Tetrad)
101
+ - seventh_chord?
102
+ Pentachord (or Pentad)
103
+ Hexachord (or Hexad)
104
+ Heptachords (or Heptad or, sometimes, mixing Latin and Greek roots, "Septachord")
105
+ Octachords (Octad)
106
+ Nonachords (Nonad)
107
+ Decachords (Decad)
108
+ Undecachords
109
+ Dodecachord
110
+
111
+ PitchClassSet
112
+ .normal_form? (most compact)
113
+ .prime_form (most compact normal form of the original or any inversion)
@@ -20,15 +20,14 @@ require 'head_music/utilities/hash_key'
20
20
  require 'head_music/named_rudiment'
21
21
 
22
22
  # rudiments
23
- require 'head_music/chord'
23
+ require 'head_music/chromatic_interval'
24
24
  require 'head_music/circle'
25
25
  require 'head_music/clef'
26
26
  require 'head_music/consonance'
27
- require 'head_music/functional_interval'
27
+ require 'head_music/diatonic_interval'
28
28
  require 'head_music/grand_staff'
29
29
  require 'head_music/harmonic_interval'
30
30
  require 'head_music/instrument'
31
- require 'head_music/interval'
32
31
  require 'head_music/key_signature'
33
32
  require 'head_music/letter_name'
34
33
  require 'head_music/melodic_interval'
@@ -39,13 +38,17 @@ require 'head_music/pitch'
39
38
  require 'head_music/pitch/enharmonic_equivalence'
40
39
  require 'head_music/pitch/octave_equivalence'
41
40
  require 'head_music/pitch_class'
41
+ require 'head_music/pitch_class_set'
42
+ require 'head_music/pitch_set'
42
43
  require 'head_music/quality'
44
+ require 'head_music/reference_pitch'
43
45
  require 'head_music/rhythm'
44
46
  require 'head_music/rhythmic_unit'
45
47
  require 'head_music/scale'
46
48
  require 'head_music/scale_degree'
47
49
  require 'head_music/scale_type'
48
50
  require 'head_music/sign'
51
+ require 'head_music/sonority'
49
52
  require 'head_music/spelling'
50
53
  require 'head_music/staff'
51
54
  require 'head_music/tuning'
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # An interval is the distance between two pitches.
4
- class HeadMusic::Interval
4
+ class HeadMusic::ChromaticInterval
5
5
  include Comparable
6
6
 
7
7
  private_class_method :new
@@ -25,7 +25,7 @@ class HeadMusic::Interval
25
25
  end
26
26
 
27
27
  def simple
28
- HeadMusic::Interval.get(semitones % 12)
28
+ HeadMusic::ChromaticInterval.get(semitones % 12)
29
29
  end
30
30
 
31
31
  def simple?
@@ -40,12 +40,15 @@ class HeadMusic::Interval
40
40
  semitones
41
41
  end
42
42
 
43
+ # diatonic set theory
44
+ alias specific_interval semitones
45
+
43
46
  def +(other)
44
- HeadMusic::Interval.get(to_i + other.to_i)
47
+ HeadMusic::ChromaticInterval.get(to_i + other.to_i)
45
48
  end
46
49
 
47
50
  def -(other)
48
- HeadMusic::Interval.get((to_i - other.to_i).abs)
51
+ HeadMusic::ChromaticInterval.get((to_i - other.to_i).abs)
49
52
  end
50
53
 
51
54
  def <=>(other)
@@ -20,8 +20,8 @@ class HeadMusic::Circle
20
20
  attr_reader :interval, :pitch_classes
21
21
 
22
22
  def initialize(interval)
23
- @interval = HeadMusic::Interval.get(interval.to_i)
24
- @pitch_classes = pitch_classes_by_interval(interval)
23
+ @interval = interval.to_i
24
+ @pitch_classes = pitch_classes_by_interval
25
25
  end
26
26
 
27
27
  def index(pitch_class)
@@ -32,7 +32,7 @@ class HeadMusic::Circle
32
32
 
33
33
  private
34
34
 
35
- def pitch_classes_by_interval(interval)
35
+ def pitch_classes_by_interval
36
36
  [HeadMusic::PitchClass.get(0)].tap do |list|
37
37
  loop do
38
38
  next_pitch_class = list.last + interval
@@ -33,7 +33,7 @@ class HeadMusic::Placement
33
33
  end
34
34
 
35
35
  def to_s
36
- "#{rhythmic_value} #{pitch.presence || 'rest'} at #{position}"
36
+ "#{rhythmic_value} #{pitch || 'rest'} at #{position}"
37
37
  end
38
38
 
39
39
  private
@@ -54,7 +54,7 @@ class HeadMusic::Voice
54
54
  end
55
55
 
56
56
  def range
57
- HeadMusic::FunctionalInterval.new(lowest_pitch, highest_pitch)
57
+ HeadMusic::DiatonicInterval.new(lowest_pitch, highest_pitch)
58
58
  end
59
59
 
60
60
  def melodic_intervals
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A functional interval is the distance between two spelled pitches.
4
- class HeadMusic::FunctionalInterval
3
+ # A diatonic interval is the distance between two spelled pitches.
4
+ class HeadMusic::DiatonicInterval
5
5
  include Comparable
6
6
 
7
7
  NUMBER_NAMES = %w[
@@ -31,6 +31,14 @@ class HeadMusic::FunctionalInterval
31
31
  seventeenth: { major: 28 },
32
32
  }.freeze
33
33
 
34
+ QUALITY_ABBREVIATIONS = {
35
+ P: 'perfect',
36
+ M: 'major',
37
+ m: 'minor',
38
+ d: 'diminished',
39
+ A: 'augmented',
40
+ }.freeze
41
+
34
42
  attr_reader :lower_pitch, :higher_pitch
35
43
 
36
44
  delegate :to_s, to: :name
@@ -41,7 +49,7 @@ class HeadMusic::FunctionalInterval
41
49
  attr_reader :identifier
42
50
 
43
51
  def initialize(identifier)
44
- @identifier = identifier
52
+ @identifier = expand(identifier)
45
53
  end
46
54
 
47
55
  def words
@@ -63,6 +71,20 @@ class HeadMusic::FunctionalInterval
63
71
  def higher_letter
64
72
  HeadMusic::Pitch.middle_c.letter_name.steps_up(steps)
65
73
  end
74
+
75
+ def expand(identifier)
76
+ if /[A-Z]\d{1,2}/i.match?(identifier)
77
+ number = NUMBER_NAMES[identifier.gsub(/[A-Z]/i, '').to_i - 1]
78
+ return [quality_for(identifier[0]), number].join('_').to_sym
79
+ end
80
+ identifier
81
+ end
82
+
83
+ def quality_for(abbreviation)
84
+ QUALITY_ABBREVIATIONS[abbreviation.to_sym] ||
85
+ QUALITY_ABBREVIATIONS[abbreviation.upcase.to_sym] ||
86
+ QUALITY_ABBREVIATIONS[abbreviation.downcase.to_sym]
87
+ end
66
88
  end
67
89
 
68
90
  # Accepts a name and a quality and returns the number of semitones
@@ -70,7 +92,7 @@ class HeadMusic::FunctionalInterval
70
92
  attr_reader :count
71
93
 
72
94
  def initialize(name, quality_name)
73
- @count ||= Semitones.degree_quality_semitones.dig(name, quality_name)
95
+ @count = Semitones.degree_quality_semitones.dig(name, quality_name)
74
96
  end
75
97
 
76
98
  def self.degree_quality_semitones
@@ -167,6 +189,10 @@ class HeadMusic::FunctionalInterval
167
189
  number - 1
168
190
  end
169
191
 
192
+ def simple_steps
193
+ steps % 7
194
+ end
195
+
170
196
  private
171
197
 
172
198
  def octave_equivalent?
@@ -246,7 +272,7 @@ class HeadMusic::FunctionalInterval
246
272
 
247
273
  delegate :step?, :skip?, :leap?, :large_leap?, to: :category
248
274
  delegate(
249
- :simple_number, :octaves, :number, :simple?, :compound?, :semitones, :simple_semitones, :steps,
275
+ :simple_number, :octaves, :number, :simple?, :compound?, :semitones, :simple_semitones, :steps, :simple_steps,
250
276
  to: :size
251
277
  )
252
278
  delegate(
@@ -277,7 +303,7 @@ class HeadMusic::FunctionalInterval
277
303
  while inverted_low_pitch < higher_pitch
278
304
  inverted_low_pitch = HeadMusic::Pitch.fetch_or_create(lower_pitch.spelling, inverted_low_pitch.octave + 1)
279
305
  end
280
- HeadMusic::FunctionalInterval.new(higher_pitch, inverted_low_pitch)
306
+ HeadMusic::DiatonicInterval.new(higher_pitch, inverted_low_pitch)
281
307
  end
282
308
  alias invert inversion
283
309
 
@@ -314,8 +340,12 @@ class HeadMusic::FunctionalInterval
314
340
  HeadMusic::Pitch.from_number_and_letter(pitch - semitones, pitch.letter_name.steps_down(number - 1))
315
341
  end
316
342
 
343
+ # diatonic set theory
344
+ alias specific_interval simple_semitones
345
+ alias diatonic_generic_interval simple_steps
346
+
317
347
  def <=>(other)
318
- other = self.class.get(other) unless other.is_a?(HeadMusic::FunctionalInterval)
348
+ other = self.class.get(other) unless other.is_a?(HeadMusic::DiatonicInterval)
319
349
  semitones <=> other.semitones
320
350
  end
321
351
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A harmonic interval is the functional interval between two notes sounding together.
3
+ # A harmonic interval is the diatonic interval between two notes sounding together.
4
4
  class HeadMusic::HarmonicInterval
5
5
  attr_reader :voice1, :voice2, :position
6
6
 
@@ -10,8 +10,8 @@ class HeadMusic::HarmonicInterval
10
10
  @position = position.is_a?(String) ? HeadMusic::Position.new(voice1.composition, position) : position
11
11
  end
12
12
 
13
- def functional_interval
14
- @functional_interval ||= HeadMusic::FunctionalInterval.new(lower_pitch, upper_pitch)
13
+ def diatonic_interval
14
+ @diatonic_interval ||= HeadMusic::DiatonicInterval.new(lower_pitch, upper_pitch)
15
15
  end
16
16
 
17
17
  def voices
@@ -53,14 +53,14 @@ class HeadMusic::HarmonicInterval
53
53
  end
54
54
 
55
55
  def to_s
56
- "#{functional_interval} at #{position}"
56
+ "#{diatonic_interval} at #{position}"
57
57
  end
58
58
 
59
59
  def method_missing(method_name, *args, &block)
60
- respond_to_missing?(method_name) ? functional_interval.send(method_name, *args, &block) : super
60
+ respond_to_missing?(method_name) ? diatonic_interval.send(method_name, *args, &block) : super
61
61
  end
62
62
 
63
63
  def respond_to_missing?(method_name, *_args)
64
- functional_interval.respond_to?(method_name)
64
+ diatonic_interval.respond_to?(method_name)
65
65
  end
66
66
  end
@@ -9,8 +9,8 @@ class HeadMusic::MelodicInterval
9
9
  @second_note = note2
10
10
  end
11
11
 
12
- def functional_interval
13
- @functional_interval ||= HeadMusic::FunctionalInterval.new(first_pitch, second_pitch)
12
+ def diatonic_interval
13
+ @diatonic_interval ||= HeadMusic::DiatonicInterval.new(first_pitch, second_pitch)
14
14
  end
15
15
 
16
16
  def position_start
@@ -38,7 +38,7 @@ class HeadMusic::MelodicInterval
38
38
  end
39
39
 
40
40
  def to_s
41
- [direction, functional_interval].join(' ')
41
+ [direction, diatonic_interval].join(' ')
42
42
  end
43
43
 
44
44
  def ascending?
@@ -86,14 +86,14 @@ class HeadMusic::MelodicInterval
86
86
  combined_pitches = (pitches + other_interval.pitches).uniq
87
87
  return false if combined_pitches.length < 3
88
88
 
89
- HeadMusic::Chord.new(combined_pitches).consonant_triad?
89
+ HeadMusic::PitchSet.new(combined_pitches).consonant_triad?
90
90
  end
91
91
 
92
92
  def method_missing(method_name, *args, &block)
93
- respond_to_missing?(method_name) ? functional_interval.send(method_name, *args, &block) : super
93
+ respond_to_missing?(method_name) ? diatonic_interval.send(method_name, *args, &block) : super
94
94
  end
95
95
 
96
96
  def respond_to_missing?(method_name, *_args)
97
- functional_interval.respond_to?(method_name)
97
+ diatonic_interval.respond_to?(method_name)
98
98
  end
99
99
  end
@@ -34,7 +34,7 @@ class HeadMusic::Octave
34
34
  delegate :to_i, :to_s, to: :number
35
35
 
36
36
  def initialize(number)
37
- @number ||= number
37
+ @number = number
38
38
  end
39
39
 
40
40
  def <=>(other)
@@ -64,7 +64,8 @@ class HeadMusic::Pitch
64
64
 
65
65
  def self.natural_letter_pitch(number, letter_name)
66
66
  natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class)
67
- natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
67
+ natural_letter_pitch += 12 while (number.to_i - natural_letter_pitch.to_i) >= 6
68
+ natural_letter_pitch -= 12 while (number.to_i - natural_letter_pitch.to_i) <= -6
68
69
  get(natural_letter_pitch)
69
70
  end
70
71
 
@@ -106,7 +107,7 @@ class HeadMusic::Pitch
106
107
  end
107
108
 
108
109
  def +(other)
109
- if other.is_a?(HeadMusic::FunctionalInterval)
110
+ if other.is_a?(HeadMusic::DiatonicInterval)
110
111
  # return a pitch
111
112
  other.above(self)
112
113
  else
@@ -116,12 +117,12 @@ class HeadMusic::Pitch
116
117
  end
117
118
 
118
119
  def -(other)
119
- if other.is_a?(HeadMusic::FunctionalInterval)
120
+ if other.is_a?(HeadMusic::DiatonicInterval)
120
121
  # return a pitch
121
122
  other.below(self)
122
123
  elsif other.is_a?(HeadMusic::Pitch)
123
124
  # return an interval
124
- HeadMusic::Interval.get(to_i - other.to_i)
125
+ HeadMusic::ChromaticInterval.get(to_i - other.to_i)
125
126
  else
126
127
  # assume value represents an interval in semitones and return another pitch
127
128
  HeadMusic::Pitch.get(to_i - other.to_i)
@@ -2,6 +2,8 @@
2
2
 
3
3
  # A pitch class is a set of pitches separated by octaves.
4
4
  class HeadMusic::PitchClass
5
+ include Comparable
6
+
5
7
  attr_reader :number
6
8
  attr_reader :spelling
7
9
 
@@ -58,10 +60,14 @@ class HeadMusic::PitchClass
58
60
  end
59
61
  alias enharmonic? ==
60
62
 
63
+ def <=>(other)
64
+ to_i <=> other.to_i
65
+ end
66
+
61
67
  def intervals_to(other)
62
68
  delta = other.to_i - to_i
63
69
  inverse = delta.positive? ? delta - 12 : delta + 12
64
- [delta, inverse].sort_by(&:abs).map { |interval| HeadMusic::Interval.get(interval) }
70
+ [delta, inverse].sort_by(&:abs).map { |interval| HeadMusic::ChromaticInterval.get(interval) }
65
71
  end
66
72
 
67
73
  def smallest_interval_to(other)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A PitchClassSet represents a pitch-class set or pitch collection.
4
+ # See also: PitchSet, PitchClass
5
+ class HeadMusic::PitchClassSet
6
+ attr_reader :pitch_classes
7
+
8
+ delegate :empty?, to: :pitch_classes
9
+ alias empty_set? empty?
10
+
11
+ def initialize(identifiers)
12
+ @pitch_classes = identifiers.map { |identifier| HeadMusic::PitchClass.get(identifier) }.uniq.sort
13
+ end
14
+
15
+ def inspect
16
+ pitch_classes.map(&:to_s).join(' ')
17
+ end
18
+
19
+ def to_s
20
+ pitch_classes.map(&:to_s).join(' ')
21
+ end
22
+
23
+ def ==(other)
24
+ pitch_classes == other.pitch_classes
25
+ end
26
+
27
+ def equivalent?(other)
28
+ pitch_classes.sort == other.pitch_classes.sort
29
+ end
30
+
31
+ def size
32
+ @size ||= pitch_classes.length
33
+ end
34
+
35
+ def monochord?
36
+ pitch_classes.length == 1
37
+ end
38
+ alias monad? monochord?
39
+
40
+ def dichord?
41
+ pitch_classes.length == 2
42
+ end
43
+ alias dyad? dichord?
44
+
45
+ def trichord?
46
+ pitch_classes.length == 3
47
+ end
48
+
49
+ def tetrachord?
50
+ pitch_classes.length == 4
51
+ end
52
+
53
+ def pentachord?
54
+ pitch_classes.length == 5
55
+ end
56
+
57
+ def hexachord?
58
+ pitch_classes.length == 6
59
+ end
60
+
61
+ def heptachord?
62
+ pitch_classes.length == 7
63
+ end
64
+
65
+ def octachord?
66
+ pitch_classes.length == 8
67
+ end
68
+
69
+ def nonachord?
70
+ pitch_classes.length == 9
71
+ end
72
+
73
+ def decachord?
74
+ pitch_classes.length == 10
75
+ end
76
+
77
+ def undecachord?
78
+ pitch_classes.length == 11
79
+ end
80
+
81
+ def dodecachord?
82
+ pitch_classes.length == 12
83
+ end
84
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A PitchSet is a collection of one or more pitches.
4
+ # See also: PitchClassSet
5
+ class HeadMusic::PitchSet
6
+ TERTIAN_SONORITIES = {
7
+ implied_triad: [3],
8
+ triad: [3, 5],
9
+ seventh_chord: [3, 5, 7],
10
+ ninth_chord: [2, 3, 5, 7],
11
+ eleventh_chord: [2, 3, 4, 5, 7],
12
+ thirteenth_chord: [2, 3, 4, 5, 6, 7], # a.k.a. diatonic scale
13
+ }.freeze
14
+
15
+ attr_reader :pitches
16
+
17
+ delegate :diatonic_intervals, to: :reduction, prefix: true
18
+ delegate :empty?, :empty_set?, to: :pitch_class_set
19
+ delegate :monochord?, :monad?, :dichord?, :dyad?, to: :pitch_class_set
20
+ delegate :trichord?, :tetrachord?, :pentachord?, :hexachord?, to: :pitch_class_set
21
+ delegate :heptachord?, :octachord?, :nonachord?, :decachord?, :undecachord?, :dodecachord?, to: :pitch_class_set
22
+ delegate :size, to: :pitch_class_set, prefix: true
23
+
24
+ def initialize(pitches)
25
+ @pitches = pitches.map { |pitch| HeadMusic::Pitch.get(pitch) }.sort.uniq
26
+ end
27
+
28
+ def pitch_classes
29
+ @pitch_classes ||= reduction_pitches.map(&:pitch_class).uniq
30
+ end
31
+
32
+ def pitch_class_set
33
+ @pitch_class_set ||= HeadMusic::PitchClassSet.new(pitch_classes)
34
+ end
35
+
36
+ def reduction
37
+ @reduction ||= HeadMusic::PitchSet.new(reduction_pitches)
38
+ end
39
+
40
+ def diatonic_intervals
41
+ @diatonic_intervals ||= pitches.each_cons(2).map do |pitch_pair|
42
+ HeadMusic::DiatonicInterval.new(*pitch_pair)
43
+ end
44
+ end
45
+
46
+ def diatonic_intervals_above_bass_pitch
47
+ @diatonic_intervals_above_bass_pitch ||= pitches_above_bass_pitch.map do |pitch|
48
+ HeadMusic::DiatonicInterval.new(bass_pitch, pitch)
49
+ end
50
+ end
51
+
52
+ def pitches_above_bass_pitch
53
+ @pitches_above_bass_pitch ||= pitches.drop(1)
54
+ end
55
+
56
+ def integer_notation
57
+ @integer_notation ||= begin
58
+ return [] if pitches.empty?
59
+ diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0)
60
+ end
61
+ end
62
+
63
+ def invert
64
+ inverted_pitch = pitches[0] + HeadMusic::DiatonicInterval.get('perfect octave')
65
+ new_pitches = pitches.drop(1) + [inverted_pitch]
66
+ HeadMusic::PitchSet.new(new_pitches)
67
+ end
68
+
69
+ def uninvert
70
+ inverted_pitch = pitches[-1] - HeadMusic::DiatonicInterval.get('perfect octave')
71
+ new_pitches = [inverted_pitch] + pitches[0..-2]
72
+ HeadMusic::PitchSet.new(new_pitches)
73
+ end
74
+
75
+ def bass_pitch
76
+ @bass_pitch ||= pitches.first
77
+ end
78
+
79
+ def inspect
80
+ pitches.map(&:to_s).join(' ')
81
+ end
82
+
83
+ def to_s
84
+ pitches.map(&:to_s).join(' ')
85
+ end
86
+
87
+ def ==(other)
88
+ pitches.sort == other.pitches.sort
89
+ end
90
+
91
+ def equivalent?(other)
92
+ pitch_classes.sort == other.pitch_classes.sort
93
+ end
94
+
95
+ def size
96
+ pitches.length
97
+ end
98
+
99
+ def triad?
100
+ trichord? && tertian?
101
+ end
102
+
103
+ def consonant_triad?
104
+ major_triad? || minor_triad?
105
+ end
106
+
107
+ def major_triad?
108
+ [%w[M3 m3], %w[m3 P4], %w[P4 M3]].include? reduction_diatonic_intervals.map(&:shorthand)
109
+ end
110
+
111
+ def minor_triad?
112
+ [%w[m3 M3], %w[M3 P4], %w[P4 m3]].include? reduction_diatonic_intervals.map(&:shorthand)
113
+ end
114
+
115
+ def diminished_triad?
116
+ [%w[m3 m3], %w[m3 A4], %w[A4 m3]].include? reduction_diatonic_intervals.map(&:shorthand)
117
+ end
118
+
119
+ def augmented_triad?
120
+ [%w[M3 M3], %w[M3 d4], %w[d4 M3]].include? reduction_diatonic_intervals.map(&:shorthand)
121
+ end
122
+
123
+ def root_position_triad?
124
+ trichord? && reduction_diatonic_intervals.all?(&:third?)
125
+ end
126
+
127
+ def first_inversion_triad?
128
+ trichord? && reduction.uninvert.diatonic_intervals.all?(&:third?)
129
+ end
130
+
131
+ def second_inversion_triad?
132
+ trichord? && reduction.invert.diatonic_intervals.all?(&:third?)
133
+ end
134
+
135
+ def seventh_chord?
136
+ tetrachord? && tertian?
137
+ end
138
+
139
+ def root_position_seventh_chord?
140
+ tetrachord? && reduction_diatonic_intervals.all?(&:third?)
141
+ end
142
+
143
+ def first_inversion_seventh_chord?
144
+ tetrachord? && reduction.uninvert.diatonic_intervals.all?(&:third?)
145
+ end
146
+
147
+ def second_inversion_seventh_chord?
148
+ tetrachord? && reduction.uninvert.uninvert.diatonic_intervals.all?(&:third?)
149
+ end
150
+
151
+ def third_inversion_seventh_chord?
152
+ tetrachord? && reduction.invert.diatonic_intervals.all?(&:third?)
153
+ end
154
+
155
+ def ninth_chord?
156
+ pentachord? && tertian?
157
+ end
158
+
159
+ def eleventh_chord?
160
+ hexachord? && tertian?
161
+ end
162
+
163
+ def thirteenth_chord?
164
+ heptachord? && tertian?
165
+ end
166
+
167
+ def tertian?
168
+ return false unless diatonic_intervals.any?
169
+
170
+ inversion = reduction
171
+ pitches.length.times do
172
+ return true if TERTIAN_SONORITIES.value?(inversion.scale_degrees_above_bass_pitch)
173
+ inversion = inversion.invert
174
+ end
175
+ false
176
+ end
177
+
178
+ def scale_degrees
179
+ @scale_degrees ||= pitches.empty? ? [] : scale_degrees_above_bass_pitch.unshift(1)
180
+ end
181
+
182
+ def scale_degrees_above_bass_pitch
183
+ @scale_degrees_above_bass_pitch ||= diatonic_intervals_above_bass_pitch.map(&:simple_number).sort - [8]
184
+ end
185
+
186
+ private
187
+
188
+ def reduction_pitches
189
+ pitches.map do |pitch|
190
+ pitch = HeadMusic::Pitch.fetch_or_create(pitch.spelling, pitch.octave - 1) while pitch > bass_pitch + 12
191
+ pitch
192
+ end.sort
193
+ end
194
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A reference pitch has a pitch and a frequency
4
+ # With no arguments, it assumes that A4 = 440.0 Hz
5
+ class HeadMusic::ReferencePitch
6
+ include HeadMusic::NamedRudiment
7
+
8
+ DEFAULT_PITCH_NAME = 'A4'
9
+ DEFAULT_FREQUENCY = 440.0
10
+
11
+ NAMED_REFERENCE_PITCHES = [
12
+ { name: 'Baroque', pitch: 'A4', frequency: 415.0 },
13
+ { name: 'Classical', pitch: 'A4', frequency: 430.0 },
14
+ { name: 'Scientific', pitch: 'C4', frequency: 256.0 },
15
+ { name: 'Verdi', pitch: 'A4', frequency: 432.0 }, # Pythagorean tuning
16
+ { name: 'French', pitch: 'A4', frequency: 435.0 },
17
+ { name: 'New Philharmonic', pitch: 'A4', frequency: 439.0 },
18
+ { name: 'A440', pitch: 'A4', frequency: 440.0 },
19
+ { name: 'Sydney Symphony Orchestra', pitch: 'A4', frequency: 441.0 },
20
+ { name: 'New York Philharmonic', pitch: 'A4', frequency: 442.0 },
21
+ { name: 'Berlin Philharmonic', pitch: 'A4', frequency: 443.0 },
22
+ { name: 'Boston Symphony Orchestra', pitch: 'A4', frequency: 444.0 },
23
+ { name: 'Old Philharmonic', pitch: 'A4', frequency: 452.4 },
24
+ { name: 'Chorton', pitch: 'A4', frequency: 466.0 },
25
+ ].freeze
26
+
27
+ ALIASES = {
28
+ kammerton: :baroque,
29
+ chamber: :baroque,
30
+ haydn: :classical,
31
+ mozart: :classical,
32
+ philosophic: :scientific,
33
+ sauveur: :scientific,
34
+ schiller: :scientific,
35
+ continental: :french,
36
+ international: :french,
37
+ low: :new_philharmonic,
38
+ concert: :a440,
39
+ stuttgart: :a440,
40
+ scheibler: :a440,
41
+ iso_16: :a440,
42
+ high: :old_philharmonic,
43
+ choir: :chorton,
44
+ }.freeze
45
+
46
+ NAMED_REFERENCE_PITCH_NAMES = NAMED_REFERENCE_PITCHES.map { |pitch_data| pitch_data[:name] }
47
+
48
+ attr_reader :pitch, :frequency
49
+
50
+ def self.get(name)
51
+ return name if name.is_a?(self)
52
+ get_by_name(name)
53
+ end
54
+
55
+ def initialize(name = 'A440')
56
+ @name = name.to_s
57
+ reference_pitch_data = NAMED_REFERENCE_PITCHES.detect do |candidate|
58
+ candidate_name_key = HeadMusic::Utilities::HashKey.for(candidate[:name])
59
+ [candidate_name_key, candidate_name_key.to_s.delete('_').to_sym].include?(normalized_name)
60
+ end || {}
61
+ @pitch = HeadMusic::Pitch.get(reference_pitch_data.fetch(:pitch, DEFAULT_PITCH_NAME))
62
+ @frequency = reference_pitch_data.fetch(:frequency, DEFAULT_FREQUENCY)
63
+ end
64
+
65
+ def description
66
+ [
67
+ pitch.letter_name,
68
+ format(
69
+ '%<with_digits>g',
70
+ with_digits: format('%.2f', frequency)
71
+ ),
72
+ ].join('=')
73
+ end
74
+
75
+ def to_s
76
+ description
77
+ end
78
+
79
+ private
80
+
81
+ def normalized_name
82
+ @normalized_name ||= begin
83
+ key = HeadMusic::Utilities::HashKey.for(name.to_s.gsub(/\W?(pitch|tuning|tone)/, ''))
84
+ ALIASES[key] || key
85
+ end
86
+ end
87
+ end
@@ -27,7 +27,7 @@ class HeadMusic::RhythmicUnit
27
27
  end
28
28
 
29
29
  def initialize(canonical_name)
30
- @name ||= canonical_name
30
+ @name = canonical_name
31
31
  @numerator = 2**numerator_exponent
32
32
  @denominator = 2**denominator_exponent
33
33
  end
@@ -80,10 +80,26 @@ class HeadMusic::RhythmicUnit
80
80
  end
81
81
 
82
82
  def numerator_exponent
83
- MULTIPLES.index(name) || BRITISH_MULTIPLE_NAMES.index(name) || 0
83
+ multiples_keys.index(name.gsub(/\W+/, '_')) || british_multiples_keys.index(name.gsub(/\W+/, '_')) || 0
84
+ end
85
+
86
+ def multiples_keys
87
+ MULTIPLES.map { |multiple| multiple.gsub(/\W+/, '_') }
88
+ end
89
+
90
+ def british_multiples_keys
91
+ BRITISH_MULTIPLE_NAMES.map { |multiple| multiple.gsub(/\W+/, '_') }
84
92
  end
85
93
 
86
94
  def denominator_exponent
87
- FRACTIONS.index(name) || BRITISH_DIVISION_NAMES.index(name) || 0
95
+ fractions_keys.index(name.gsub(/\W+/, '_')) || british_fractions_keys.index(name.gsub(/\W+/, '_')) || 0
96
+ end
97
+
98
+ def fractions_keys
99
+ FRACTIONS.map { |fraction| fraction.gsub(/\W+/, '_') }
100
+ end
101
+
102
+ def british_fractions_keys
103
+ BRITISH_DIVISION_NAMES.map { |fraction| fraction.gsub(/\W+/, '_') }
88
104
  end
89
105
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A Sonority describes a set of pitch class intervalic relationships.
4
+ # For example, a minor triad, or a major-minor seventh chord.
5
+ # The Sonority class is a factory for returning one of its subclasses.
6
+ class HeadMusic::Sonority
7
+ SONORITIES = {
8
+ major_triad: %w[M3 P5],
9
+ minor_triad: %w[m3 P5],
10
+ diminished_triad: %w[m3 d5],
11
+ augmented_triad: %w[M3 A5],
12
+ major_minor_seventh_chord: %w[M3 P5 m7],
13
+ major_major_seventh_chord: %w[M3 P5 M7],
14
+ minor_minor_seventh_chord: %w[m3 P5 m7],
15
+ minor_major_seventh_chord: %w[m3 P5 M7],
16
+ half_diminished_seventh_chord: %w[m3 d5 m7],
17
+ diminished_seventh_chord: %w[m3 d5 d7],
18
+ dominant_ninth_chord: %w[M2 M3 P5 m7],
19
+ dominant_minor_ninth_chord: %w[m2 M3 P5 m7],
20
+ minor_ninth_chord: %w[M2 m3 P5 m7],
21
+ major_ninth_chord: %w[M2 M3 P5 M7],
22
+ six_nine_chord: %w[M2 M3 P5 M6],
23
+ minor_six_nine_chord: %w[M2 m3 P5 M6],
24
+ suspended_four_chord: %w[P4 P5],
25
+ suspended_two_chord: %w[M2 P5],
26
+ quartal_chord: %w[P4 m7],
27
+ }.freeze
28
+
29
+ attr_reader :pitch_set
30
+
31
+ delegate :reduction, to: :pitch_set
32
+ delegate :empty?, :empty_set?, to: :pitch_set
33
+ delegate :monochord?, :monad, :dichord?, :dyad?, :trichord?, :tetrachord?, :pentachord?, :hexachord?, to: :pitch_set
34
+ delegate :heptachord?, :octachord?, :nonachord?, :decachord?, :undecachord?, :dodecachord?, to: :pitch_set
35
+ delegate :pitch_class_set, :pitch_class_set_size, to: :pitch_set
36
+ delegate :scale_degrees_above_bass_pitch, to: :pitch_set
37
+
38
+ def initialize(pitch_set)
39
+ @pitch_set = pitch_set
40
+ identifier
41
+ end
42
+
43
+ def identifier
44
+ return @identifier if defined?(@identifier)
45
+
46
+ @identifier = SONORITIES.keys.detect do |key|
47
+ inversions.map do |inversion|
48
+ inversion.diatonic_intervals_above_bass_pitch.map(&:shorthand)
49
+ end.include?(SONORITIES[key])
50
+ end
51
+ end
52
+
53
+ def inversion
54
+ @inversion ||= inversions.index do |inversion|
55
+ SONORITIES[identifier] == inversion.diatonic_intervals_above_bass_pitch.map(&:shorthand)
56
+ end
57
+ end
58
+
59
+ def inversions
60
+ @inversions ||= begin
61
+ inversion = reduction
62
+ inversions = []
63
+ inversion.pitches.length.times do |_i|
64
+ inversions << inversion
65
+ inversion = inversion.uninvert
66
+ end
67
+ inversions
68
+ end
69
+ end
70
+
71
+ def root_position
72
+ @root_position ||= inversions[inversion]
73
+ end
74
+
75
+ def consonant?
76
+ @consonant ||=
77
+ pitch_set.reduction_diatonic_intervals.all?(&:consonant?) &&
78
+ root_position.diatonic_intervals_above_bass_pitch.all?(&:consonant?)
79
+ end
80
+
81
+ def triad?
82
+ @triad ||= trichord? && tertian?
83
+ end
84
+
85
+ def seventh_chord?
86
+ @seventh_chord ||= tetrachord? && tertian?
87
+ end
88
+
89
+ def tertian?
90
+ @tertian ||= inversions.detect do |inversion|
91
+ inversion.diatonic_intervals.count(&:third?).to_f / inversion.diatonic_intervals.length > 0.5 ||
92
+ (scale_degrees_above_bass_pitch && [3, 5, 7]).length == 3
93
+ end
94
+ end
95
+
96
+ def secundal?
97
+ @secundal ||= inversions.detect do |inversion|
98
+ inversion.diatonic_intervals.count(&:second?).to_f / inversion.diatonic_intervals.length > 0.5
99
+ end
100
+ end
101
+
102
+ def quartal?
103
+ @quartal ||= inversions.detect do |inversion|
104
+ inversion.diatonic_intervals.count do |interval|
105
+ interval.fourth? || interval.fifth?
106
+ end.to_f / inversion.diatonic_intervals.length > 0.5
107
+ end
108
+ end
109
+ alias quintal? quartal?
110
+
111
+ def diatonic_intervals_above_bass_pitch
112
+ return nil unless identifier
113
+
114
+ @diatonic_intervals_above_bass_pitch ||=
115
+ SONORITIES[identifier].map { |shorthand| HeadMusic::DiatonicInterval.get(shorthand) }
116
+ end
117
+
118
+ def ==(other)
119
+ other = HeadMusic::PitchSet.new(other) if other.is_a?(Array)
120
+ other = self.class.new(other) if other.is_a?(HeadMusic::PitchSet)
121
+ identifier == other.identifier
122
+ end
123
+ end
@@ -48,10 +48,6 @@ class HeadMusic::Style::Annotation
48
48
  [marks].flatten.compact.map(&:end_position).max
49
49
  end
50
50
 
51
- def marks
52
- raise NotImplementedError
53
- end
54
-
55
51
  def message
56
52
  self.class::MESSAGE
57
53
  end
@@ -86,10 +82,10 @@ class HeadMusic::Style::Annotation
86
82
  @lower_voices ||= unsorted_lower_voices.sort_by(&:lowest_pitch).reverse
87
83
  end
88
84
 
89
- def functional_interval_from_tonic(note)
85
+ def diatonic_interval_from_tonic(note)
90
86
  tonic_to_use = tonic_pitch
91
- tonic_to_use -= HeadMusic::Interval.get(:perfect_octave) while tonic_to_use > note.pitch
92
- HeadMusic::FunctionalInterval.new(tonic_to_use, note.pitch)
87
+ tonic_to_use -= HeadMusic::ChromaticInterval.get(:perfect_octave) while tonic_to_use > note.pitch
88
+ HeadMusic::DiatonicInterval.new(tonic_to_use, note.pitch)
93
89
  end
94
90
 
95
91
  def bass_voice?
@@ -25,6 +25,6 @@ class HeadMusic::Style::Guidelines::ApproachPerfectionContrarily < HeadMusic::St
25
25
  end
26
26
  end
27
27
 
28
- # Side effect is that you can't enter a perfect consonance by skip,
28
+ # Side effect is that you can't enter a perfect consonance by skip in similar or parallel motion,
29
29
  # which is a rule.
30
30
  end
@@ -29,21 +29,21 @@ class HeadMusic::Style::Guidelines::ConsonantClimax < HeadMusic::Style::Annotati
29
29
  end
30
30
 
31
31
  def highest_pitch_consonant_with_tonic?
32
- functional_interval_to_highest_pitch.consonance?(:melodic)
32
+ diatonic_interval_to_highest_pitch.consonance?(:melodic)
33
33
  end
34
34
 
35
35
  def lowest_pitch_consonant_with_tonic?
36
- functional_interval_to_lowest_pitch.consonance?(:melodic)
36
+ diatonic_interval_to_lowest_pitch.consonance?(:melodic)
37
37
  end
38
38
 
39
- def functional_interval_to_highest_pitch
40
- @functional_interval_to_highest_pitch ||=
41
- HeadMusic::FunctionalInterval.new(tonic_pitch, highest_pitch)
39
+ def diatonic_interval_to_highest_pitch
40
+ @diatonic_interval_to_highest_pitch ||=
41
+ HeadMusic::DiatonicInterval.new(tonic_pitch, highest_pitch)
42
42
  end
43
43
 
44
- def functional_interval_to_lowest_pitch
45
- @functional_interval_to_lowest_pitch ||=
46
- HeadMusic::FunctionalInterval.new(tonic_pitch, lowest_pitch)
44
+ def diatonic_interval_to_lowest_pitch
45
+ @diatonic_interval_to_lowest_pitch ||=
46
+ HeadMusic::DiatonicInterval.new(tonic_pitch, lowest_pitch)
47
47
  end
48
48
 
49
49
  def highest_pitch_appears_once?
@@ -14,6 +14,6 @@ class HeadMusic::Style::Guidelines::EndOnPerfectConsonance < HeadMusic::Style::A
14
14
  private
15
15
 
16
16
  def ends_on_perfect_consonance?
17
- functional_interval_from_tonic(last_note).perfect_consonance?(:two_part_harmony)
17
+ diatonic_interval_from_tonic(last_note).perfect_consonance?(:two_part_harmony)
18
18
  end
19
19
  end
@@ -16,6 +16,6 @@ class HeadMusic::Style::Guidelines::StartOnPerfectConsonance < HeadMusic::Style:
16
16
  private
17
17
 
18
18
  def starts_on_perfect_consonance?
19
- functional_interval_from_tonic(first_note).perfect_consonance?(:two_part_harmony)
19
+ diatonic_interval_from_tonic(first_note).perfect_consonance?(:two_part_harmony)
20
20
  end
21
21
  end
@@ -3,19 +3,17 @@
3
3
  # A tuning has a reference pitch and frequency and provides frequencies for all pitches
4
4
  # The base class assumes equal temperament tuning. By default, A4 = 440.0 Hz
5
5
  class HeadMusic::Tuning
6
- REFERENCE_PITCH_NAME = 'A4'
7
- REFERENCE_FREQUENCY = 440.0
6
+ attr_accessor :reference_pitch
8
7
 
9
- attr_reader :reference_pitch, :reference_frequency
8
+ delegate :pitch, :frequency, to: :reference_pitch, prefix: true
10
9
 
11
- def initialize(reference_pitch: nil, reference_frequency: nil)
12
- @reference_pitch = reference_pitch || HeadMusic::Pitch.get(REFERENCE_PITCH_NAME)
13
- @reference_frequency = reference_frequency || REFERENCE_FREQUENCY
10
+ def initialize(reference_pitch: :a440)
11
+ @reference_pitch = HeadMusic::ReferencePitch.get(reference_pitch)
14
12
  end
15
13
 
16
14
  def frequency_for(pitch)
17
15
  pitch = HeadMusic::Pitch.get(pitch) unless pitch.is_a?(HeadMusic::Pitch)
18
- reference_frequency * (2**(1.0 / 12))**(pitch - reference_pitch).semitones
16
+ reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
19
17
  end
20
18
  end
21
19
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HeadMusic
4
- VERSION = '0.22.0'
4
+ VERSION = '0.23.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: head_music
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Head
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-13 00:00:00.000000000 Z
11
+ date: 2019-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -120,7 +120,7 @@ files:
120
120
  - bin/setup
121
121
  - head_music.gemspec
122
122
  - lib/head_music.rb
123
- - lib/head_music/chord.rb
123
+ - lib/head_music/chromatic_interval.rb
124
124
  - lib/head_music/circle.rb
125
125
  - lib/head_music/clef.rb
126
126
  - lib/head_music/consonance.rb
@@ -131,11 +131,10 @@ files:
131
131
  - lib/head_music/content/position.rb
132
132
  - lib/head_music/content/rhythmic_value.rb
133
133
  - lib/head_music/content/voice.rb
134
- - lib/head_music/functional_interval.rb
134
+ - lib/head_music/diatonic_interval.rb
135
135
  - lib/head_music/grand_staff.rb
136
136
  - lib/head_music/harmonic_interval.rb
137
137
  - lib/head_music/instrument.rb
138
- - lib/head_music/interval.rb
139
138
  - lib/head_music/key_signature.rb
140
139
  - lib/head_music/letter_name.rb
141
140
  - lib/head_music/melodic_interval.rb
@@ -147,13 +146,17 @@ files:
147
146
  - lib/head_music/pitch/enharmonic_equivalence.rb
148
147
  - lib/head_music/pitch/octave_equivalence.rb
149
148
  - lib/head_music/pitch_class.rb
149
+ - lib/head_music/pitch_class_set.rb
150
+ - lib/head_music/pitch_set.rb
150
151
  - lib/head_music/quality.rb
152
+ - lib/head_music/reference_pitch.rb
151
153
  - lib/head_music/rhythm.rb
152
154
  - lib/head_music/rhythmic_unit.rb
153
155
  - lib/head_music/scale.rb
154
156
  - lib/head_music/scale_degree.rb
155
157
  - lib/head_music/scale_type.rb
156
158
  - lib/head_music/sign.rb
159
+ - lib/head_music/sonority.rb
157
160
  - lib/head_music/spelling.rb
158
161
  - lib/head_music/staff.rb
159
162
  - lib/head_music/style/analysis.rb
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A Chord is a collection of three or more pitches
4
- class HeadMusic::Chord
5
- attr_reader :pitches
6
-
7
- def initialize(pitches)
8
- raise ArgumentError if pitches.length < 3
9
-
10
- @pitches = pitches.map { |pitch| HeadMusic::Pitch.get(pitch) }.sort
11
- end
12
-
13
- def consonant_triad?
14
- reduction.root_triad? || reduction.first_inversion_triad? || reduction.second_inversion_triad?
15
- end
16
-
17
- def root_triad?
18
- return false unless triad?
19
-
20
- intervals.map(&:shorthand).sort == %w[M3 m3]
21
- end
22
-
23
- # TODO: look up definition of first and second inversion triads. Can they be spread?
24
- def first_inversion_triad?
25
- return false unless triad?
26
-
27
- invert.invert.intervals.map(&:shorthand).sort == %w[M3 m3]
28
- end
29
-
30
- def second_inversion_triad?
31
- return false unless triad?
32
-
33
- invert.intervals.map(&:shorthand).sort == %w[M3 m3]
34
- end
35
-
36
- def reduction
37
- @reduction ||= HeadMusic::Chord.new(reduction_pitches)
38
- end
39
-
40
- def triad?
41
- pitches.length == 3
42
- end
43
-
44
- def intervals
45
- pitches.each_cons(2).map do |pitch_pair|
46
- HeadMusic::FunctionalInterval.new(*pitch_pair)
47
- end
48
- end
49
-
50
- def invert
51
- inverted_pitch = pitches[0] + HeadMusic::Interval.get(12)
52
- new_pitches = pitches.drop(1) + [inverted_pitch]
53
- HeadMusic::Chord.new(new_pitches)
54
- end
55
-
56
- def bass_pitch
57
- @bass_pitch ||= pitches.first
58
- end
59
-
60
- def inspect
61
- pitches.map(&:to_s).join(' ')
62
- end
63
-
64
- def to_s
65
- pitches.map(&:to_s).join(' ')
66
- end
67
-
68
- def ==(other)
69
- pitches & other.pitches == pitches
70
- end
71
-
72
- private
73
-
74
- def reduction_pitches
75
- pitches.map do |pitch|
76
- pitch = HeadMusic::Pitch.fetch_or_create(pitch.spelling, pitch.octave - 1) while pitch > bass_pitch + 12
77
- pitch
78
- end.sort
79
- end
80
- end