head_music 8.2.1 → 8.3.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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/release.yml +1 -1
- data/CLAUDE.md +134 -0
- data/Gemfile.lock +25 -25
- data/Rakefile +2 -2
- data/TODO.md +41 -150
- data/bin/check_instrument_consistency.rb +86 -0
- data/check_instrument_consistency.rb +0 -0
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +28 -7
- data/lib/head_music/instruments/instrument_families.yml +10 -9
- data/lib/head_music/instruments/instruments.yml +350 -368
- data/lib/head_music/locales/en.yml +92 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +93 -26
- data/lib/head_music/rudiment/scale_degree.rb +8 -3
- data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
- data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
- data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
- data/lib/head_music/rudiment/tuning.rb +17 -3
- data/lib/head_music/style/annotation.rb +4 -4
- data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +3 -0
- data/user_stories/backlog/band-score-order.md +38 -0
- data/user_stories/backlog/chamber-ensemble-score-order.md +33 -0
- data/user_stories/backlog/consonance-dissonance-classification.md +57 -0
- data/user_stories/backlog/dyad-analysis.md +65 -0
- data/user_stories/backlog/orchestral-score-order.md +43 -0
- data/user_stories/backlog/pitch-class-set-analysis.md +39 -0
- data/user_stories/backlog/pitch-set-classification.md +62 -0
- data/user_stories/backlog/sonority-identification.md +47 -0
- metadata +18 -4
@@ -14,6 +14,98 @@ en:
|
|
14
14
|
minor_seventh: minor seventh
|
15
15
|
major_seventh: major seventh
|
16
16
|
perfect_octave: perfect octave
|
17
|
+
diatonic_intervals:
|
18
|
+
perfect_unison: perfect unison
|
19
|
+
minor_second: minor second
|
20
|
+
major_second: major second
|
21
|
+
diminished_second: diminished second
|
22
|
+
augmented_second: augmented second
|
23
|
+
doubly_diminished_second: doubly diminished second
|
24
|
+
doubly_augmented_second: doubly augmented second
|
25
|
+
minor_third: minor third
|
26
|
+
major_third: major third
|
27
|
+
diminished_third: diminished third
|
28
|
+
augmented_third: augmented third
|
29
|
+
doubly_diminished_third: doubly diminished third
|
30
|
+
doubly_augmented_third: doubly augmented third
|
31
|
+
perfect_fourth: perfect fourth
|
32
|
+
diminished_fourth: diminished fourth
|
33
|
+
augmented_fourth: augmented fourth
|
34
|
+
doubly_diminished_fourth: doubly diminished fourth
|
35
|
+
doubly_augmented_fourth: doubly augmented fourth
|
36
|
+
perfect_fifth: perfect fifth
|
37
|
+
diminished_fifth: diminished fifth
|
38
|
+
augmented_fifth: augmented fifth
|
39
|
+
doubly_diminished_fifth: doubly diminished fifth
|
40
|
+
doubly_augmented_fifth: doubly augmented fifth
|
41
|
+
minor_sixth: minor sixth
|
42
|
+
major_sixth: major sixth
|
43
|
+
diminished_sixth: diminished sixth
|
44
|
+
augmented_sixth: augmented sixth
|
45
|
+
doubly_diminished_sixth: doubly diminished sixth
|
46
|
+
doubly_augmented_sixth: doubly augmented sixth
|
47
|
+
minor_seventh: minor seventh
|
48
|
+
major_seventh: major seventh
|
49
|
+
diminished_seventh: diminished seventh
|
50
|
+
augmented_seventh: augmented seventh
|
51
|
+
doubly_diminished_seventh: doubly diminished seventh
|
52
|
+
doubly_augmented_seventh: doubly augmented seventh
|
53
|
+
perfect_octave: perfect octave
|
54
|
+
diminished_octave: diminished octave
|
55
|
+
augmented_octave: augmented octave
|
56
|
+
doubly_diminished_octave: doubly diminished octave
|
57
|
+
doubly_augmented_octave: doubly augmented octave
|
58
|
+
minor_ninth: minor ninth
|
59
|
+
major_ninth: major ninth
|
60
|
+
diminished_ninth: diminished ninth
|
61
|
+
augmented_ninth: augmented ninth
|
62
|
+
doubly_diminished_ninth: doubly diminished ninth
|
63
|
+
doubly_augmented_ninth: doubly augmented ninth
|
64
|
+
minor_tenth: minor tenth
|
65
|
+
major_tenth: major tenth
|
66
|
+
diminished_tenth: diminished tenth
|
67
|
+
augmented_tenth: augmented tenth
|
68
|
+
doubly_diminished_tenth: doubly diminished tenth
|
69
|
+
doubly_augmented_tenth: doubly augmented tenth
|
70
|
+
perfect_eleventh: perfect eleventh
|
71
|
+
diminished_eleventh: diminished eleventh
|
72
|
+
augmented_eleventh: augmented eleventh
|
73
|
+
doubly_diminished_eleventh: doubly diminished eleventh
|
74
|
+
doubly_augmented_eleventh: doubly augmented eleventh
|
75
|
+
perfect_twelfth: perfect twelfth
|
76
|
+
diminished_twelfth: diminished twelfth
|
77
|
+
augmented_twelfth: augmented twelfth
|
78
|
+
doubly_diminished_twelfth: doubly diminished twelfth
|
79
|
+
doubly_augmented_twelfth: doubly augmented twelfth
|
80
|
+
minor_thirteenth: minor thirteenth
|
81
|
+
major_thirteenth: major thirteenth
|
82
|
+
diminished_thirteenth: diminished thirteenth
|
83
|
+
augmented_thirteenth: augmented thirteenth
|
84
|
+
doubly_diminished_thirteenth: doubly diminished thirteenth
|
85
|
+
doubly_augmented_thirteenth: doubly augmented thirteenth
|
86
|
+
minor_fourteenth: minor fourteenth
|
87
|
+
major_fourteenth: major fourteenth
|
88
|
+
diminished_fourteenth: diminished fourteenth
|
89
|
+
augmented_fourteenth: augmented fourteenth
|
90
|
+
doubly_diminished_fourteenth: doubly diminished fourteenth
|
91
|
+
doubly_augmented_fourteenth: doubly augmented fourteenth
|
92
|
+
perfect_fifteenth: perfect fifteenth
|
93
|
+
diminished_fifteenth: diminished fifteenth
|
94
|
+
augmented_fifteenth: augmented fifteenth
|
95
|
+
doubly_diminished_fifteenth: doubly diminished fifteenth
|
96
|
+
doubly_augmented_fifteenth: doubly augmented fifteenth
|
97
|
+
minor_sixteenth: minor sixteenth
|
98
|
+
major_sixteenth: major sixteenth
|
99
|
+
diminished_sixteenth: diminished sixteenth
|
100
|
+
augmented_sixteenth: augmented sixteenth
|
101
|
+
doubly_diminished_sixteenth: doubly diminished sixteenth
|
102
|
+
doubly_augmented_sixteenth: doubly augmented sixteenth
|
103
|
+
minor_seventeenth: minor seventeenth
|
104
|
+
major_seventeenth: major seventeenth
|
105
|
+
diminished_seventeenth: diminished seventeenth
|
106
|
+
augmented_seventeenth: augmented seventeenth
|
107
|
+
doubly_diminished_seventeenth: doubly diminished seventeenth
|
108
|
+
doubly_augmented_seventeenth: doubly augmented seventeenth
|
17
109
|
clefs:
|
18
110
|
alto_clef: alto clef
|
19
111
|
baritone_c_clef: baritone C-clef
|
@@ -4,21 +4,45 @@ module HeadMusic::Rudiment; end
|
|
4
4
|
# A rhythmic unit is a rudiment of duration consisting of doublings and divisions of a whole note.
|
5
5
|
class HeadMusic::Rudiment::RhythmicUnit
|
6
6
|
include HeadMusic::Named
|
7
|
+
include Comparable
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
# Note values longer than a whole note
|
10
|
+
AMERICAN_MULTIPLES_NAMES = [
|
11
|
+
"whole", "double whole", "longa", "maxima"
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
# Note values from whole note down to very short subdivisions
|
15
|
+
AMERICAN_DIVISIONS_NAMES = [
|
10
16
|
"whole", "half", "quarter", "eighth", "sixteenth", "thirty-second",
|
11
17
|
"sixty-fourth", "hundred twenty-eighth", "two hundred fifty-sixth"
|
12
18
|
].freeze
|
13
19
|
|
14
|
-
|
15
|
-
|
20
|
+
# British terminology for note values longer than a whole note
|
21
|
+
BRITISH_MULTIPLES_NAMES = %w[semibreve breve longa maxima].freeze
|
22
|
+
|
23
|
+
# British terminology for standard note divisions
|
24
|
+
BRITISH_DIVISIONS_NAMES = %w[
|
16
25
|
semibreve minim crotchet quaver semiquaver demisemiquaver
|
17
26
|
hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver
|
18
27
|
].freeze
|
19
28
|
|
29
|
+
# Notehead symbols used for different note values
|
30
|
+
NOTEHEADS = {
|
31
|
+
maxima: 8.0,
|
32
|
+
longa: 4.0,
|
33
|
+
breve: 2.0,
|
34
|
+
open: [0.5, 1.0],
|
35
|
+
closed: :default
|
36
|
+
}.freeze
|
37
|
+
|
20
38
|
def self.for_denominator_value(denominator)
|
21
|
-
|
39
|
+
return nil unless denominator.is_a?(Numeric) && denominator > 0
|
40
|
+
return nil unless (denominator & (denominator - 1)) == 0 # Check if power of 2
|
41
|
+
|
42
|
+
index = Math.log2(denominator).to_i
|
43
|
+
return nil if index >= AMERICAN_DIVISIONS_NAMES.length
|
44
|
+
|
45
|
+
get(AMERICAN_DIVISIONS_NAMES[index])
|
22
46
|
end
|
23
47
|
|
24
48
|
attr_reader :numerator, :denominator
|
@@ -27,7 +51,28 @@ class HeadMusic::Rudiment::RhythmicUnit
|
|
27
51
|
get_by_name(name)
|
28
52
|
end
|
29
53
|
|
54
|
+
def self.all
|
55
|
+
@all ||= (AMERICAN_MULTIPLES_NAMES.reverse + AMERICAN_DIVISIONS_NAMES).uniq.map { |name| get(name) }.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if a name represents a valid rhythmic unit
|
59
|
+
def self.valid_name?(name)
|
60
|
+
normalized = normalize_name(name)
|
61
|
+
all_normalized_names.include?(normalized)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.all_normalized_names
|
65
|
+
@all_normalized_names ||= (
|
66
|
+
AMERICAN_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
|
67
|
+
AMERICAN_DIVISIONS_NAMES.map { |n| normalize_name(n) } +
|
68
|
+
BRITISH_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
|
69
|
+
BRITISH_DIVISIONS_NAMES.map { |n| normalize_name(n) }
|
70
|
+
).uniq
|
71
|
+
end
|
72
|
+
|
30
73
|
def initialize(canonical_name)
|
74
|
+
raise ArgumentError, "Name cannot be nil or empty" if canonical_name.to_s.strip.empty?
|
75
|
+
|
31
76
|
self.name = canonical_name
|
32
77
|
@numerator = 2**numerator_exponent
|
33
78
|
@denominator = 2**denominator_exponent
|
@@ -42,65 +87,87 @@ class HeadMusic::Rudiment::RhythmicUnit
|
|
42
87
|
end
|
43
88
|
|
44
89
|
def notehead
|
45
|
-
|
46
|
-
return :
|
47
|
-
return :
|
48
|
-
return :
|
90
|
+
value = relative_value
|
91
|
+
return :maxima if value == NOTEHEADS[:maxima]
|
92
|
+
return :longa if value == NOTEHEADS[:longa]
|
93
|
+
return :breve if value == NOTEHEADS[:breve]
|
94
|
+
return :open if NOTEHEADS[:open].include?(value)
|
49
95
|
|
50
96
|
:closed
|
51
97
|
end
|
52
98
|
|
53
99
|
def flags
|
54
|
-
|
100
|
+
AMERICAN_DIVISIONS_NAMES.include?(name) ? [AMERICAN_DIVISIONS_NAMES.index(name) - 2, 0].max : 0
|
55
101
|
end
|
56
102
|
|
57
103
|
def stemmed?
|
58
104
|
relative_value < 1
|
59
105
|
end
|
60
106
|
|
107
|
+
# Returns true if this note value is commonly used in modern notation
|
108
|
+
def common?
|
109
|
+
AMERICAN_DIVISIONS_NAMES[0..6].include?(name) || BRITISH_DIVISIONS_NAMES[0..6].include?(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
def <=>(other)
|
113
|
+
return nil unless other.is_a?(self.class)
|
114
|
+
|
115
|
+
relative_value <=> other.relative_value
|
116
|
+
end
|
117
|
+
|
61
118
|
def british_name
|
62
|
-
if
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
119
|
+
if has_american_multiple_name?
|
120
|
+
index = AMERICAN_MULTIPLES_NAMES.index(name)
|
121
|
+
return BRITISH_MULTIPLES_NAMES[index] unless index.nil?
|
122
|
+
elsif has_american_division_name?
|
123
|
+
index = AMERICAN_DIVISIONS_NAMES.index(name)
|
124
|
+
return BRITISH_DIVISIONS_NAMES[index] unless index.nil?
|
125
|
+
elsif BRITISH_MULTIPLES_NAMES.include?(name) || BRITISH_DIVISIONS_NAMES.include?(name)
|
126
|
+
return name
|
68
127
|
end
|
128
|
+
|
129
|
+
nil # Return nil if no British equivalent found
|
69
130
|
end
|
70
131
|
|
71
132
|
private_class_method :new
|
72
133
|
|
134
|
+
def self.normalize_name(name)
|
135
|
+
name.to_s.gsub(/\W+/, "_")
|
136
|
+
end
|
137
|
+
|
73
138
|
private
|
74
139
|
|
75
|
-
def
|
76
|
-
|
140
|
+
def has_american_multiple_name?
|
141
|
+
AMERICAN_MULTIPLES_NAMES.include?(name)
|
77
142
|
end
|
78
143
|
|
79
|
-
def
|
80
|
-
|
144
|
+
def has_american_division_name?
|
145
|
+
AMERICAN_DIVISIONS_NAMES.include?(name)
|
81
146
|
end
|
82
147
|
|
83
148
|
def numerator_exponent
|
84
|
-
|
149
|
+
normalized_name = self.class.normalize_name(name)
|
150
|
+
multiples_keys.index(normalized_name) || british_multiples_keys.index(normalized_name) || 0
|
85
151
|
end
|
86
152
|
|
87
153
|
def multiples_keys
|
88
|
-
|
154
|
+
AMERICAN_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
|
89
155
|
end
|
90
156
|
|
91
157
|
def british_multiples_keys
|
92
|
-
|
158
|
+
BRITISH_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
|
93
159
|
end
|
94
160
|
|
95
161
|
def denominator_exponent
|
96
|
-
|
162
|
+
normalized_name = self.class.normalize_name(name)
|
163
|
+
fractions_keys.index(normalized_name) || british_fractions_keys.index(normalized_name) || 0
|
97
164
|
end
|
98
165
|
|
99
166
|
def fractions_keys
|
100
|
-
|
167
|
+
AMERICAN_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
|
101
168
|
end
|
102
169
|
|
103
170
|
def british_fractions_keys
|
104
|
-
|
171
|
+
BRITISH_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
|
105
172
|
end
|
106
173
|
end
|
@@ -38,11 +38,16 @@ class HeadMusic::Rudiment::ScaleDegree
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def <=>(other)
|
41
|
-
|
41
|
+
case other
|
42
|
+
when HeadMusic::Rudiment::ScaleDegree
|
42
43
|
[degree, alteration_semitones] <=> [other.degree, other.alteration_semitones]
|
43
|
-
|
44
|
-
|
44
|
+
when Numeric
|
45
|
+
degree <=> other
|
46
|
+
when String
|
45
47
|
to_s <=> other.to_s
|
48
|
+
else
|
49
|
+
# If we can't meaningfully compare, return nil (Ruby standard)
|
50
|
+
nil
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# A module for music rudiments
|
2
|
+
module HeadMusic::Rudiment; end
|
3
|
+
|
4
|
+
# Just Intonation tuning system based on whole number frequency ratios
|
5
|
+
class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
|
6
|
+
# Frequency ratios for intervals in just intonation (relative to tonic)
|
7
|
+
# Based on the major scale with pure intervals
|
8
|
+
INTERVAL_RATIOS = {
|
9
|
+
unison: Rational(1, 1),
|
10
|
+
minor_second: Rational(16, 15),
|
11
|
+
major_second: Rational(9, 8),
|
12
|
+
minor_third: Rational(6, 5),
|
13
|
+
major_third: Rational(5, 4),
|
14
|
+
perfect_fourth: Rational(4, 3),
|
15
|
+
tritone: Rational(45, 32),
|
16
|
+
perfect_fifth: Rational(3, 2),
|
17
|
+
minor_sixth: Rational(8, 5),
|
18
|
+
major_sixth: Rational(5, 3),
|
19
|
+
minor_seventh: Rational(16, 9),
|
20
|
+
major_seventh: Rational(15, 8),
|
21
|
+
octave: Rational(2, 1)
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
attr_reader :tonal_center
|
25
|
+
|
26
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
27
|
+
super
|
28
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
29
|
+
end
|
30
|
+
|
31
|
+
def frequency_for(pitch)
|
32
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
33
|
+
|
34
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
35
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
36
|
+
|
37
|
+
# Calculate the interval from the tonal center to the requested pitch
|
38
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
39
|
+
|
40
|
+
# Get the just intonation ratio for this interval
|
41
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
42
|
+
|
43
|
+
# Calculate the frequency
|
44
|
+
tonal_center_frequency * ratio
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def calculate_tonal_center_frequency
|
50
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
51
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
52
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
53
|
+
end
|
54
|
+
|
55
|
+
def ratio_for_interval(semitones)
|
56
|
+
# Handle octaves
|
57
|
+
octaves = semitones / 12
|
58
|
+
interval_within_octave = semitones % 12
|
59
|
+
|
60
|
+
# Make sure we handle negative intervals
|
61
|
+
if interval_within_octave < 0
|
62
|
+
interval_within_octave += 12
|
63
|
+
octaves -= 1
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get the base ratio
|
67
|
+
base_ratio = case interval_within_octave
|
68
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
69
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
70
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
71
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
72
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
73
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
74
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
75
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
76
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
77
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
78
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
79
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Apply octave adjustments
|
83
|
+
base_ratio * (2**octaves)
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# A module for music rudiments
|
2
|
+
module HeadMusic::Rudiment; end
|
3
|
+
|
4
|
+
# Quarter-comma meantone temperament
|
5
|
+
# Optimizes major thirds to be pure (5:4) at the expense of perfect fifths
|
6
|
+
class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
|
7
|
+
# Frequency ratios for intervals in quarter-comma meantone temperament
|
8
|
+
# The defining characteristic is that major thirds are pure (5:4)
|
9
|
+
# and the syntonic comma is distributed equally among the four fifths
|
10
|
+
INTERVAL_RATIOS = {
|
11
|
+
unison: Rational(1, 1),
|
12
|
+
minor_second: 5.0**(1.0 / 4) / 2.0**(1.0 / 2), # ~1.0697
|
13
|
+
major_second: 5.0**(1.0 / 4), # ~1.1892 (fourth root of 5)
|
14
|
+
minor_third: 5.0**(1.0 / 2) / 2.0**(1.0 / 2), # ~1.5811
|
15
|
+
major_third: Rational(5, 4), # Pure major third (1.25)
|
16
|
+
perfect_fourth: 2.0**(1.0 / 2) / 5.0**(1.0 / 4), # ~1.3375
|
17
|
+
tritone: 5.0**(3.0 / 4) / 2.0**(1.0 / 2), # ~1.6719
|
18
|
+
perfect_fifth: Rational(3, 2), # ~1.4953 (slightly flat)
|
19
|
+
minor_sixth: 2.0**(3.0 / 2) / 5.0**(1.0 / 4), # ~1.6818
|
20
|
+
major_sixth: 5.0**(3.0 / 4), # ~1.8877
|
21
|
+
minor_seventh: 2.0**(3.0 / 2) / 5.0**(1.0 / 2), # ~1.8877
|
22
|
+
major_seventh: Rational(25, 16), # ~1.5625
|
23
|
+
octave: Rational(2, 1) # Octave (2.0)
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :tonal_center
|
27
|
+
|
28
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
29
|
+
super
|
30
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
31
|
+
end
|
32
|
+
|
33
|
+
def frequency_for(pitch)
|
34
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
35
|
+
|
36
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
37
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
38
|
+
|
39
|
+
# Calculate the interval from the tonal center to the requested pitch
|
40
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
41
|
+
|
42
|
+
# Get the meantone ratio for this interval
|
43
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
44
|
+
|
45
|
+
# Calculate the frequency
|
46
|
+
tonal_center_frequency * ratio
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def calculate_tonal_center_frequency
|
52
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
53
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
54
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
55
|
+
end
|
56
|
+
|
57
|
+
def ratio_for_interval(semitones)
|
58
|
+
# Handle octaves
|
59
|
+
octaves = semitones / 12
|
60
|
+
interval_within_octave = semitones % 12
|
61
|
+
|
62
|
+
# Make sure we handle negative intervals
|
63
|
+
if interval_within_octave < 0
|
64
|
+
interval_within_octave += 12
|
65
|
+
octaves -= 1
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get the base ratio
|
69
|
+
base_ratio = case interval_within_octave
|
70
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
71
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
72
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
73
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
74
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
75
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
76
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
77
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
78
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
79
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
80
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
81
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Apply octave adjustments
|
85
|
+
base_ratio * (2**octaves)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# A module for music rudiments
|
2
|
+
module HeadMusic::Rudiment; end
|
3
|
+
|
4
|
+
# Pythagorean tuning system based on stacking perfect fifths (3:2 ratio)
|
5
|
+
class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
|
6
|
+
# Frequency ratios for intervals in Pythagorean tuning (relative to tonic)
|
7
|
+
# Generated by stacking perfect fifths and reducing to within one octave
|
8
|
+
INTERVAL_RATIOS = {
|
9
|
+
unison: Rational(1, 1),
|
10
|
+
minor_second: Rational(256, 243), # Pythagorean minor second
|
11
|
+
major_second: Rational(9, 8), # Pythagorean major second
|
12
|
+
minor_third: Rational(32, 27), # Pythagorean minor third
|
13
|
+
major_third: Rational(81, 64), # Pythagorean major third (ditone)
|
14
|
+
perfect_fourth: Rational(4, 3), # Perfect fourth
|
15
|
+
tritone: Rational(729, 512), # Pythagorean tritone (augmented fourth)
|
16
|
+
perfect_fifth: Rational(3, 2), # Perfect fifth
|
17
|
+
minor_sixth: Rational(128, 81), # Pythagorean minor sixth
|
18
|
+
major_sixth: Rational(27, 16), # Pythagorean major sixth
|
19
|
+
minor_seventh: Rational(16, 9), # Pythagorean minor seventh
|
20
|
+
major_seventh: Rational(243, 128), # Pythagorean major seventh
|
21
|
+
octave: Rational(2, 1) # Octave
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# Additional chromatic intervals for enharmonic equivalents
|
25
|
+
CHROMATIC_RATIOS = {
|
26
|
+
augmented_unison: Rational(2187, 2048), # Pythagorean augmented unison (sharp)
|
27
|
+
diminished_second: Rational(256, 243) # Same as minor second in Pythagorean
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
attr_reader :tonal_center
|
31
|
+
|
32
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
33
|
+
super
|
34
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
35
|
+
end
|
36
|
+
|
37
|
+
def frequency_for(pitch)
|
38
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
39
|
+
|
40
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
41
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
42
|
+
|
43
|
+
# Calculate the interval from the tonal center to the requested pitch
|
44
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
45
|
+
|
46
|
+
# Get the Pythagorean ratio for this interval
|
47
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
48
|
+
|
49
|
+
# Calculate the frequency
|
50
|
+
tonal_center_frequency * ratio
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def calculate_tonal_center_frequency
|
56
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
57
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
58
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
59
|
+
end
|
60
|
+
|
61
|
+
def ratio_for_interval(semitones)
|
62
|
+
# Handle octaves
|
63
|
+
octaves = semitones / 12
|
64
|
+
interval_within_octave = semitones % 12
|
65
|
+
|
66
|
+
# Make sure we handle negative intervals
|
67
|
+
if interval_within_octave < 0
|
68
|
+
interval_within_octave += 12
|
69
|
+
octaves -= 1
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the base ratio
|
73
|
+
base_ratio = case interval_within_octave
|
74
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
75
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
76
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
77
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
78
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
79
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
80
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
81
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
82
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
83
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
84
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
85
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Apply octave adjustments
|
89
|
+
base_ratio * (2**octaves)
|
90
|
+
end
|
91
|
+
end
|
@@ -8,8 +8,24 @@ class HeadMusic::Rudiment::Tuning
|
|
8
8
|
|
9
9
|
delegate :pitch, :frequency, to: :reference_pitch, prefix: true
|
10
10
|
|
11
|
-
def
|
11
|
+
def self.get(tuning_type = :equal_temperament, **options)
|
12
|
+
case tuning_type.to_s.downcase
|
13
|
+
when "just_intonation", "just", "ji"
|
14
|
+
HeadMusic::Rudiment::Tuning::JustIntonation.new(**options)
|
15
|
+
when "pythagorean", "pythag"
|
16
|
+
HeadMusic::Rudiment::Tuning::Pythagorean.new(**options)
|
17
|
+
when "meantone", "quarter_comma_meantone", "1/4_comma"
|
18
|
+
HeadMusic::Rudiment::Tuning::Meantone.new(**options)
|
19
|
+
when "equal_temperament", "equal", "et", "12tet"
|
20
|
+
new(**options)
|
21
|
+
else
|
22
|
+
new(**options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
12
27
|
@reference_pitch = HeadMusic::Rudiment::ReferencePitch.get(reference_pitch)
|
28
|
+
@tonal_center = tonal_center
|
13
29
|
end
|
14
30
|
|
15
31
|
def frequency_for(pitch)
|
@@ -17,5 +33,3 @@ class HeadMusic::Rudiment::Tuning
|
|
17
33
|
reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
|
18
34
|
end
|
19
35
|
end
|
20
|
-
|
21
|
-
# TODO: other tunings
|
@@ -51,16 +51,16 @@ class HeadMusic::Style::Annotation
|
|
51
51
|
self.class::MESSAGE
|
52
52
|
end
|
53
53
|
|
54
|
-
protected
|
55
|
-
|
56
54
|
def first_note
|
57
|
-
notes.first
|
55
|
+
notes.first
|
58
56
|
end
|
59
57
|
|
60
58
|
def last_note
|
61
|
-
notes.last
|
59
|
+
notes.last
|
62
60
|
end
|
63
61
|
|
62
|
+
protected
|
63
|
+
|
64
64
|
def voices
|
65
65
|
@voices ||= voice.composition.voices
|
66
66
|
end
|
@@ -9,6 +9,22 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
|
|
9
9
|
HeadMusic::Style::Mark.for_each(all_wrong_length_notes)
|
10
10
|
end
|
11
11
|
|
12
|
+
def first_most_common_rhythmic_value
|
13
|
+
@first_most_common_rhythmic_value ||= begin
|
14
|
+
candidates = most_common_rhythmic_values
|
15
|
+
first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
|
16
|
+
first_match&.rhythmic_value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def most_common_rhythmic_values
|
21
|
+
return [] if notes.empty?
|
22
|
+
|
23
|
+
occurrences = occurrences_by_rhythmic_value
|
24
|
+
highest_count = occurrences.values.max
|
25
|
+
occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
|
26
|
+
end
|
27
|
+
|
12
28
|
private
|
13
29
|
|
14
30
|
def all_wrong_length_notes
|
@@ -37,22 +53,6 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
|
|
37
53
|
notes[0..-2]
|
38
54
|
end
|
39
55
|
|
40
|
-
def first_most_common_rhythmic_value
|
41
|
-
@first_most_common_rhythmic_value ||= begin
|
42
|
-
candidates = most_common_rhythmic_values
|
43
|
-
first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
|
44
|
-
first_match&.rhythmic_value
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def most_common_rhythmic_values
|
49
|
-
return [] if notes.empty?
|
50
|
-
|
51
|
-
occurrences = occurrences_by_rhythmic_value
|
52
|
-
highest_count = occurrences.values.max
|
53
|
-
occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
|
54
|
-
end
|
55
|
-
|
56
56
|
def occurrences_by_rhythmic_value
|
57
57
|
rhythmic_values.each_with_object(Hash.new(0)) { |value, hash| hash[value] += 1 }
|
58
58
|
end
|
data/lib/head_music/version.rb
CHANGED