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 +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
|