head_music 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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