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 +4 -4
- data/TODO.md +99 -4
- data/lib/head_music.rb +6 -3
- data/lib/head_music/{interval.rb → chromatic_interval.rb} +7 -4
- data/lib/head_music/circle.rb +3 -3
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/voice.rb +1 -1
- data/lib/head_music/{functional_interval.rb → diatonic_interval.rb} +37 -7
- data/lib/head_music/harmonic_interval.rb +6 -6
- data/lib/head_music/melodic_interval.rb +6 -6
- data/lib/head_music/octave.rb +1 -1
- data/lib/head_music/pitch.rb +5 -4
- data/lib/head_music/pitch_class.rb +7 -1
- data/lib/head_music/pitch_class_set.rb +84 -0
- data/lib/head_music/pitch_set.rb +194 -0
- data/lib/head_music/reference_pitch.rb +87 -0
- data/lib/head_music/rhythmic_unit.rb +19 -3
- data/lib/head_music/sonority.rb +123 -0
- data/lib/head_music/style/annotation.rb +3 -7
- data/lib/head_music/style/guidelines/approach_perfection_contrarily.rb +1 -1
- data/lib/head_music/style/guidelines/consonant_climax.rb +8 -8
- data/lib/head_music/style/guidelines/end_on_perfect_consonance.rb +1 -1
- data/lib/head_music/style/guidelines/start_on_perfect_consonance.rb +1 -1
- data/lib/head_music/tuning.rb +5 -7
- data/lib/head_music/version.rb +1 -1
- metadata +8 -5
- data/lib/head_music/chord.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ce84c55ee00f24b71bcedf973bd5cab1c1d9a7150ebc92bb0508ccd40c3038d
|
4
|
+
data.tar.gz: '084c59660f40b4cc8746499939a38086d4df92180e4b75c37272d4835092fea0'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
69
|
+
I want to be able to add a diatonic interval to get another pitch.
|
10
70
|
|
11
|
-
|
71
|
+
DiatonicInterval
|
12
72
|
- def above(pitch) -> pitch
|
13
|
-
|
73
|
+
DiatonicInterval
|
14
74
|
- def below(pitch) -> pitch
|
15
75
|
|
16
76
|
Pitch addition and subtraction
|
17
77
|
- define `Pitch#+`, `Pitch#-`
|
18
|
-
- use
|
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)
|
data/lib/head_music.rb
CHANGED
@@ -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/
|
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/
|
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::
|
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::
|
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::
|
47
|
+
HeadMusic::ChromaticInterval.get(to_i + other.to_i)
|
45
48
|
end
|
46
49
|
|
47
50
|
def -(other)
|
48
|
-
HeadMusic::
|
51
|
+
HeadMusic::ChromaticInterval.get((to_i - other.to_i).abs)
|
49
52
|
end
|
50
53
|
|
51
54
|
def <=>(other)
|
data/lib/head_music/circle.rb
CHANGED
@@ -20,8 +20,8 @@ class HeadMusic::Circle
|
|
20
20
|
attr_reader :interval, :pitch_classes
|
21
21
|
|
22
22
|
def initialize(interval)
|
23
|
-
@interval =
|
24
|
-
@pitch_classes = pitch_classes_by_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
|
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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# A
|
4
|
-
class HeadMusic::
|
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
|
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::
|
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::
|
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
|
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
|
14
|
-
@
|
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
|
-
"#{
|
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) ?
|
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
|
-
|
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
|
13
|
-
@
|
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,
|
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::
|
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) ?
|
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
|
-
|
97
|
+
diatonic_interval.respond_to?(method_name)
|
98
98
|
end
|
99
99
|
end
|
data/lib/head_music/octave.rb
CHANGED
data/lib/head_music/pitch.rb
CHANGED
@@ -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)
|
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::
|
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::
|
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::
|
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::
|
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
|
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
|
-
|
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
|
-
|
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
|
85
|
+
def diatonic_interval_from_tonic(note)
|
90
86
|
tonic_to_use = tonic_pitch
|
91
|
-
tonic_to_use -= HeadMusic::
|
92
|
-
HeadMusic::
|
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
|
-
|
32
|
+
diatonic_interval_to_highest_pitch.consonance?(:melodic)
|
33
33
|
end
|
34
34
|
|
35
35
|
def lowest_pitch_consonant_with_tonic?
|
36
|
-
|
36
|
+
diatonic_interval_to_lowest_pitch.consonance?(:melodic)
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
@
|
41
|
-
HeadMusic::
|
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
|
45
|
-
@
|
46
|
-
HeadMusic::
|
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
|
-
|
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
|
-
|
19
|
+
diatonic_interval_from_tonic(first_note).perfect_consonance?(:two_part_harmony)
|
20
20
|
end
|
21
21
|
end
|
data/lib/head_music/tuning.rb
CHANGED
@@ -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
|
-
|
7
|
-
REFERENCE_FREQUENCY = 440.0
|
6
|
+
attr_accessor :reference_pitch
|
8
7
|
|
9
|
-
|
8
|
+
delegate :pitch, :frequency, to: :reference_pitch, prefix: true
|
10
9
|
|
11
|
-
def initialize(reference_pitch:
|
12
|
-
@reference_pitch =
|
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
|
-
|
16
|
+
reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
data/lib/head_music/version.rb
CHANGED
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.
|
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:
|
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/
|
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/
|
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
|
data/lib/head_music/chord.rb
DELETED
@@ -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
|