head_music 8.2.1 → 9.0.1
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/CHANGELOG.md +53 -0
- data/CLAUDE.md +151 -0
- data/Gemfile.lock +25 -25
- data/MUSIC_THEORY.md +120 -0
- data/Rakefile +2 -2
- 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 +50 -27
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/content/note.rb +1 -1
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/position.rb +1 -1
- data/lib/head_music/content/staff.rb +1 -1
- data/lib/head_music/instruments/instrument.rb +103 -113
- data/lib/head_music/instruments/instrument_families.yml +10 -9
- data/lib/head_music/instruments/instrument_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- data/lib/head_music/instruments/instruments.yml +350 -368
- data/lib/head_music/instruments/score_order.rb +139 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/variant.rb +6 -0
- data/lib/head_music/locales/de.yml +6 -0
- data/lib/head_music/locales/en.yml +98 -0
- data/lib/head_music/locales/es.yml +6 -0
- data/lib/head_music/locales/fr.yml +6 -0
- data/lib/head_music/locales/it.yml +6 -0
- data/lib/head_music/locales/ru.yml +6 -0
- data/lib/head_music/rudiment/alteration.rb +23 -8
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +37 -4
- data/lib/head_music/rudiment/diatonic_context.rb +25 -0
- data/lib/head_music/rudiment/key.rb +77 -0
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
- data/lib/head_music/rudiment/key_signature.rb +46 -7
- data/lib/head_music/rudiment/letter_name.rb +3 -3
- data/lib/head_music/rudiment/meter.rb +19 -9
- data/lib/head_music/rudiment/mode.rb +92 -0
- data/lib/head_music/rudiment/musical_symbol.rb +1 -1
- data/lib/head_music/rudiment/note.rb +112 -0
- data/lib/head_music/rudiment/pitch/parser.rb +52 -0
- data/lib/head_music/rudiment/pitch.rb +5 -6
- data/lib/head_music/rudiment/pitch_class.rb +1 -1
- data/lib/head_music/rudiment/quality.rb +1 -1
- data/lib/head_music/rudiment/reference_pitch.rb +1 -1
- data/lib/head_music/rudiment/register.rb +4 -1
- data/lib/head_music/rudiment/rest.rb +36 -0
- data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +104 -29
- data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
- data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
- data/lib/head_music/rudiment/scale.rb +4 -5
- data/lib/head_music/rudiment/scale_degree.rb +9 -4
- data/lib/head_music/rudiment/scale_type.rb +9 -3
- data/lib/head_music/rudiment/solmization.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +5 -4
- data/lib/head_music/rudiment/tempo.rb +85 -0
- data/lib/head_music/rudiment/tonal_context.rb +35 -0
- 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 +18 -4
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- 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/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +34 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +33 -9
- data/user_stories/active/handle-time.md +7 -0
- data/user_stories/active/handle-time.rb +177 -0
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/epic--score-order/band-score-order.md +38 -0
- data/user_stories/done/epic--score-order/chamber-ensemble-score-order.md +33 -0
- data/user_stories/done/epic--score-order/orchestral-score-order.md +43 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/todo/agentic-daw.md +3 -0
- data/user_stories/todo/consonance-dissonance-classification.md +57 -0
- data/user_stories/todo/dyad-analysis.md +57 -0
- data/user_stories/todo/material-and-scores.md +10 -0
- data/user_stories/todo/organizing-content.md +72 -0
- data/user_stories/todo/percussion_set.md +1 -0
- data/user_stories/todo/pitch-class-set-analysis.md +79 -0
- data/user_stories/todo/pitch-set-classification.md +72 -0
- data/user_stories/todo/sonority-identification.md +67 -0
- metadata +51 -6
- data/TODO.md +0 -218
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Abstract base class for rhythmic elements that have a rhythmic value.
|
|
5
|
+
# This includes notes (pitched), rests (silence), and unpitched notes (percussion).
|
|
6
|
+
class HeadMusic::Rudiment::RhythmicElement < HeadMusic::Rudiment::Base
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
LetterName = HeadMusic::Rudiment::LetterName
|
|
10
|
+
Alteration = HeadMusic::Rudiment::Alteration
|
|
11
|
+
Register = HeadMusic::Rudiment::Register
|
|
12
|
+
RhythmicValue = HeadMusic::Rudiment::RhythmicValue
|
|
13
|
+
|
|
14
|
+
attr_reader :rhythmic_value
|
|
15
|
+
|
|
16
|
+
delegate :unit, :dots, :tied_value, :ticks, to: :rhythmic_value
|
|
17
|
+
|
|
18
|
+
# Make new private to prevent direct instantiation of abstract class
|
|
19
|
+
private_class_method :new
|
|
20
|
+
|
|
21
|
+
def initialize(rhythmic_value)
|
|
22
|
+
@rhythmic_value = rhythmic_value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a new instance with a different rhythmic value
|
|
26
|
+
def with_rhythmic_value(new_rhythmic_value)
|
|
27
|
+
self.class.get(new_rhythmic_value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
return false unless other.is_a?(self.class)
|
|
32
|
+
rhythmic_value == other.rhythmic_value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def <=>(other)
|
|
36
|
+
return nil unless other.is_a?(HeadMusic::Rudiment::RhythmicElement)
|
|
37
|
+
rhythmic_value <=> other.rhythmic_value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Abstract method - must be implemented by subclasses
|
|
45
|
+
def name
|
|
46
|
+
raise NotImplementedError, "Subclasses must implement the name method"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Abstract method - must be implemented by subclasses
|
|
50
|
+
def sounded?
|
|
51
|
+
raise NotImplementedError, "Subclasses must implement the sounded? method"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
class HeadMusic::Rudiment::RhythmicUnit::Parser
|
|
2
|
+
attr_reader :rhythmic_unit, :identifier
|
|
3
|
+
|
|
4
|
+
RHYTHMIC_UNITS_DATA = HeadMusic::Rudiment::RhythmicUnit::RHYTHMIC_UNITS_DATA
|
|
5
|
+
|
|
6
|
+
TEMPO_SHORTHAND_PATTERN = RHYTHMIC_UNITS_DATA.map { |unit| unit["tempo_shorthand"] }.compact.uniq.sort_by { |s| -s.length }.join("|")
|
|
7
|
+
|
|
8
|
+
def self.parse(identifier)
|
|
9
|
+
return nil if identifier.nil? || identifier.to_s.strip.empty?
|
|
10
|
+
new(identifier).parsed_name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(identifier)
|
|
14
|
+
@identifier = identifier.to_s.strip
|
|
15
|
+
parse
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
@unit_data = from_american_name || from_british_name || from_tempo_shorthand || from_duration
|
|
20
|
+
@rhythmic_unit = @unit_data ? HeadMusic::Rudiment::RhythmicUnit.get_by_name(@unit_data["american_name"]) : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parsed_name
|
|
24
|
+
# Return the name format that was used in input
|
|
25
|
+
return nil unless @unit_data
|
|
26
|
+
|
|
27
|
+
# Check which type matched
|
|
28
|
+
if from_british_name == @unit_data && @unit_data["british_name"]
|
|
29
|
+
@unit_data["british_name"]
|
|
30
|
+
else
|
|
31
|
+
@unit_data["american_name"]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def american_name
|
|
36
|
+
# Always return the American name if a unit was found
|
|
37
|
+
@unit_data&.fetch("american_name", nil)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalized_identifier
|
|
41
|
+
@normalized_identifier ||= identifier.downcase.strip.gsub(/[^a-z0-9]/, "_").gsub(/_+/, "_").gsub(/^_|_$/, "")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def from_american_name
|
|
45
|
+
RHYTHMIC_UNITS_DATA.find do |unit|
|
|
46
|
+
normalize_name(unit["american_name"]) == normalized_identifier
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def from_british_name
|
|
51
|
+
RHYTHMIC_UNITS_DATA.find do |unit|
|
|
52
|
+
normalize_name(unit["british_name"]) == normalized_identifier
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def from_tempo_shorthand
|
|
57
|
+
# Handle shorthand with dots (e.g., "q." should match "q")
|
|
58
|
+
clean_identifier = identifier.downcase.strip.gsub(/\.*$/, "")
|
|
59
|
+
RHYTHMIC_UNITS_DATA.find do |unit|
|
|
60
|
+
unit["tempo_shorthand"] && unit["tempo_shorthand"].downcase == clean_identifier
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def from_duration
|
|
65
|
+
RHYTHMIC_UNITS_DATA.find do |unit|
|
|
66
|
+
# Match decimal duration (e.g., "0.25")
|
|
67
|
+
return unit if unit["duration"].to_s == identifier.strip
|
|
68
|
+
|
|
69
|
+
# Match fraction notation (e.g., "1/4" = 0.25)
|
|
70
|
+
if identifier.match?(%r{^\d+/\d+$})
|
|
71
|
+
numerator, denominator = identifier.split("/").map(&:to_f)
|
|
72
|
+
calculated_duration = numerator / denominator
|
|
73
|
+
return unit if (calculated_duration - unit["duration"]).abs < 0.0001
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def normalize_name(name)
|
|
83
|
+
return nil if name.nil?
|
|
84
|
+
name.to_s.downcase.strip.gsub(/[^a-z0-9]/, "_").gsub(/_+/, "_").gsub(/^_|_$/, "")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -2,32 +2,85 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A rhythmic unit is a rudiment of duration consisting of doublings and divisions of a whole note.
|
|
5
|
-
class HeadMusic::Rudiment::RhythmicUnit
|
|
5
|
+
class HeadMusic::Rudiment::RhythmicUnit < HeadMusic::Rudiment::Base
|
|
6
6
|
include HeadMusic::Named
|
|
7
|
+
include Comparable
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
RHYTHMIC_UNITS_DATA = YAML.load_file(File.expand_path("rhythmic_units.yml", __dir__)).freeze
|
|
10
|
+
|
|
11
|
+
AMERICAN_MULTIPLES_NAMES = [
|
|
12
|
+
"whole", "double whole", "longa", "maxima"
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
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
|
+
AMERICAN_DURATIONS = (AMERICAN_MULTIPLES_NAMES + AMERICAN_DIVISIONS_NAMES).freeze
|
|
21
|
+
|
|
22
|
+
PATTERN = /#{Regexp.union(AMERICAN_DURATIONS)}/i
|
|
23
|
+
|
|
24
|
+
# British terminology for note values longer than a whole note
|
|
25
|
+
BRITISH_MULTIPLES_NAMES = %w[semibreve breve longa maxima].freeze
|
|
26
|
+
|
|
27
|
+
# British terminology for standard note divisions
|
|
28
|
+
BRITISH_DIVISIONS_NAMES = %w[
|
|
16
29
|
semibreve minim crotchet quaver semiquaver demisemiquaver
|
|
17
30
|
hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver
|
|
18
31
|
].freeze
|
|
19
32
|
|
|
33
|
+
# Notehead symbols used for different note values
|
|
34
|
+
NOTEHEADS = {
|
|
35
|
+
maxima: 8.0,
|
|
36
|
+
longa: 4.0,
|
|
37
|
+
breve: 2.0,
|
|
38
|
+
open: [0.5, 1.0],
|
|
39
|
+
closed: :default
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
20
42
|
def self.for_denominator_value(denominator)
|
|
21
|
-
|
|
43
|
+
return nil unless denominator.is_a?(Numeric) && denominator > 0
|
|
44
|
+
return nil unless (denominator & (denominator - 1)) == 0 # Check if power of 2
|
|
45
|
+
|
|
46
|
+
index = Math.log2(denominator).to_i
|
|
47
|
+
return nil if index >= AMERICAN_DIVISIONS_NAMES.length
|
|
48
|
+
|
|
49
|
+
get(AMERICAN_DIVISIONS_NAMES[index])
|
|
22
50
|
end
|
|
23
51
|
|
|
24
52
|
attr_reader :numerator, :denominator
|
|
25
53
|
|
|
26
54
|
def self.get(name)
|
|
27
|
-
|
|
55
|
+
# Use the parser to handle tempo shorthand and other formats
|
|
56
|
+
parsed_name = HeadMusic::Rudiment::RhythmicUnit::Parser.parse(name)
|
|
57
|
+
return nil unless parsed_name
|
|
58
|
+
|
|
59
|
+
get_by_name(parsed_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.all
|
|
63
|
+
@all ||= (AMERICAN_MULTIPLES_NAMES.reverse + AMERICAN_DIVISIONS_NAMES).uniq.map { |name| get(name) }.compact
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if a name represents a valid rhythmic unit
|
|
67
|
+
def self.valid_name?(name)
|
|
68
|
+
normalized = normalize_name(name)
|
|
69
|
+
all_normalized_names.include?(normalized)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.all_normalized_names
|
|
73
|
+
@all_normalized_names ||= (
|
|
74
|
+
AMERICAN_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
|
|
75
|
+
AMERICAN_DIVISIONS_NAMES.map { |n| normalize_name(n) } +
|
|
76
|
+
BRITISH_MULTIPLES_NAMES.map { |n| normalize_name(n) } +
|
|
77
|
+
BRITISH_DIVISIONS_NAMES.map { |n| normalize_name(n) }
|
|
78
|
+
).uniq
|
|
28
79
|
end
|
|
29
80
|
|
|
30
81
|
def initialize(canonical_name)
|
|
82
|
+
raise ArgumentError, "Name cannot be nil or empty" if canonical_name.to_s.strip.empty?
|
|
83
|
+
|
|
31
84
|
self.name = canonical_name
|
|
32
85
|
@numerator = 2**numerator_exponent
|
|
33
86
|
@denominator = 2**denominator_exponent
|
|
@@ -38,69 +91,91 @@ class HeadMusic::Rudiment::RhythmicUnit
|
|
|
38
91
|
end
|
|
39
92
|
|
|
40
93
|
def ticks
|
|
41
|
-
HeadMusic::Rudiment::Rhythm::PPQN * 4 * relative_value
|
|
94
|
+
(HeadMusic::Rudiment::Rhythm::PPQN * 4 * relative_value).to_i
|
|
42
95
|
end
|
|
43
96
|
|
|
44
97
|
def notehead
|
|
45
|
-
|
|
46
|
-
return :
|
|
47
|
-
return :
|
|
48
|
-
return :
|
|
98
|
+
value = relative_value
|
|
99
|
+
return :maxima if value == NOTEHEADS[:maxima]
|
|
100
|
+
return :longa if value == NOTEHEADS[:longa]
|
|
101
|
+
return :breve if value == NOTEHEADS[:breve]
|
|
102
|
+
return :open if NOTEHEADS[:open].include?(value)
|
|
49
103
|
|
|
50
104
|
:closed
|
|
51
105
|
end
|
|
52
106
|
|
|
53
107
|
def flags
|
|
54
|
-
|
|
108
|
+
AMERICAN_DIVISIONS_NAMES.include?(name) ? [AMERICAN_DIVISIONS_NAMES.index(name) - 2, 0].max : 0
|
|
55
109
|
end
|
|
56
110
|
|
|
57
111
|
def stemmed?
|
|
58
112
|
relative_value < 1
|
|
59
113
|
end
|
|
60
114
|
|
|
115
|
+
# Returns true if this note value is commonly used in modern notation
|
|
116
|
+
def common?
|
|
117
|
+
AMERICAN_DIVISIONS_NAMES[0..6].include?(name) || BRITISH_DIVISIONS_NAMES[0..6].include?(name)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def <=>(other)
|
|
121
|
+
return nil unless other.is_a?(self.class)
|
|
122
|
+
|
|
123
|
+
relative_value <=> other.relative_value
|
|
124
|
+
end
|
|
125
|
+
|
|
61
126
|
def british_name
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
127
|
+
if has_american_multiple_name?
|
|
128
|
+
index = AMERICAN_MULTIPLES_NAMES.index(name)
|
|
129
|
+
return BRITISH_MULTIPLES_NAMES[index] unless index.nil?
|
|
130
|
+
elsif has_american_division_name?
|
|
131
|
+
index = AMERICAN_DIVISIONS_NAMES.index(name)
|
|
132
|
+
return BRITISH_DIVISIONS_NAMES[index] unless index.nil?
|
|
133
|
+
elsif BRITISH_MULTIPLES_NAMES.include?(name) || BRITISH_DIVISIONS_NAMES.include?(name)
|
|
134
|
+
return name
|
|
68
135
|
end
|
|
136
|
+
|
|
137
|
+
nil # Return nil if no British equivalent found
|
|
69
138
|
end
|
|
70
139
|
|
|
71
140
|
private_class_method :new
|
|
72
141
|
|
|
142
|
+
def self.normalize_name(name)
|
|
143
|
+
name.to_s.gsub(/\W+/, "_")
|
|
144
|
+
end
|
|
145
|
+
|
|
73
146
|
private
|
|
74
147
|
|
|
75
|
-
def
|
|
76
|
-
|
|
148
|
+
def has_american_multiple_name?
|
|
149
|
+
AMERICAN_MULTIPLES_NAMES.include?(name)
|
|
77
150
|
end
|
|
78
151
|
|
|
79
|
-
def
|
|
80
|
-
|
|
152
|
+
def has_american_division_name?
|
|
153
|
+
AMERICAN_DIVISIONS_NAMES.include?(name)
|
|
81
154
|
end
|
|
82
155
|
|
|
83
156
|
def numerator_exponent
|
|
84
|
-
|
|
157
|
+
normalized_name = self.class.normalize_name(name)
|
|
158
|
+
multiples_keys.index(normalized_name) || british_multiples_keys.index(normalized_name) || 0
|
|
85
159
|
end
|
|
86
160
|
|
|
87
161
|
def multiples_keys
|
|
88
|
-
|
|
162
|
+
AMERICAN_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
|
|
89
163
|
end
|
|
90
164
|
|
|
91
165
|
def british_multiples_keys
|
|
92
|
-
|
|
166
|
+
BRITISH_MULTIPLES_NAMES.map { |multiple| self.class.normalize_name(multiple) }
|
|
93
167
|
end
|
|
94
168
|
|
|
95
169
|
def denominator_exponent
|
|
96
|
-
|
|
170
|
+
normalized_name = self.class.normalize_name(name)
|
|
171
|
+
fractions_keys.index(normalized_name) || british_fractions_keys.index(normalized_name) || 0
|
|
97
172
|
end
|
|
98
173
|
|
|
99
174
|
def fractions_keys
|
|
100
|
-
|
|
175
|
+
AMERICAN_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
|
|
101
176
|
end
|
|
102
177
|
|
|
103
178
|
def british_fractions_keys
|
|
104
|
-
|
|
179
|
+
BRITISH_DIVISIONS_NAMES.map { |fraction| self.class.normalize_name(fraction) }
|
|
105
180
|
end
|
|
106
181
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
- american_name: maxima
|
|
2
|
+
british_name: maxima
|
|
3
|
+
duration: 8.0
|
|
4
|
+
stem: false
|
|
5
|
+
flags: 0
|
|
6
|
+
notehead: maxima
|
|
7
|
+
- american_name: longa
|
|
8
|
+
british_name: longa
|
|
9
|
+
duration: 4.0
|
|
10
|
+
stem: false
|
|
11
|
+
flags: 0
|
|
12
|
+
notehead: longa
|
|
13
|
+
- american_name: double whole
|
|
14
|
+
british_name: breve
|
|
15
|
+
duration: 2.0
|
|
16
|
+
stem: false
|
|
17
|
+
flags: 0
|
|
18
|
+
notehead: breve
|
|
19
|
+
- american_name: whole
|
|
20
|
+
british_name: semibreve
|
|
21
|
+
duration: 1.0
|
|
22
|
+
stem: false
|
|
23
|
+
flags: 0
|
|
24
|
+
notehead: open
|
|
25
|
+
tempo_shorthand: w
|
|
26
|
+
- american_name: half
|
|
27
|
+
british_name: minim
|
|
28
|
+
duration: 0.5
|
|
29
|
+
stem: true
|
|
30
|
+
flags: 0
|
|
31
|
+
notehead: open
|
|
32
|
+
tempo_shorthand: h
|
|
33
|
+
- american_name: quarter
|
|
34
|
+
british_name: crotchet
|
|
35
|
+
duration: 0.25
|
|
36
|
+
stem: true
|
|
37
|
+
flags: 0
|
|
38
|
+
notehead: closed
|
|
39
|
+
tempo_shorthand: q
|
|
40
|
+
- american_name: eighth
|
|
41
|
+
british_name: quaver
|
|
42
|
+
duration: 0.125
|
|
43
|
+
stem: true
|
|
44
|
+
flags: 1
|
|
45
|
+
notehead: closed
|
|
46
|
+
tempo_shorthand: e
|
|
47
|
+
- american_name: sixteenth
|
|
48
|
+
british_name: semiquaver
|
|
49
|
+
duration: 0.0625
|
|
50
|
+
stem: true
|
|
51
|
+
flags: 2
|
|
52
|
+
notehead: closed
|
|
53
|
+
tempo_shorthand: s
|
|
54
|
+
- american_name: thirty-second
|
|
55
|
+
british_name: demisemiquaver
|
|
56
|
+
duration: 0.03125
|
|
57
|
+
stem: true
|
|
58
|
+
flags: 3
|
|
59
|
+
notehead: closed
|
|
60
|
+
tempo_shorthand: t
|
|
61
|
+
- american_name: sixty-fourth
|
|
62
|
+
british_name: hemidemisemiquaver
|
|
63
|
+
duration: 0.015625
|
|
64
|
+
stem: true
|
|
65
|
+
flags: 4
|
|
66
|
+
notehead: closed
|
|
67
|
+
tempo_shorthand: x
|
|
68
|
+
- american_name: hundred twenty-eighth
|
|
69
|
+
british_name: semihemidemisemiquaver
|
|
70
|
+
duration: 0.0078125
|
|
71
|
+
stem: true
|
|
72
|
+
flags: 5
|
|
73
|
+
notehead: closed
|
|
74
|
+
tempo_shorthand: o
|
|
75
|
+
- american_name: two hundred fifty-sixth
|
|
76
|
+
british_name: demisemihemidemisemiquaver
|
|
77
|
+
duration: 0.00390625
|
|
78
|
+
stem: true
|
|
79
|
+
flags: 6
|
|
80
|
+
notehead: closed
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class HeadMusic::Rudiment::RhythmicValue::Parser
|
|
2
|
+
attr_reader :identifier, :rhythmic_value
|
|
3
|
+
|
|
4
|
+
RhythmicUnit = HeadMusic::Rudiment::RhythmicUnit
|
|
5
|
+
RhythmicValue = HeadMusic::Rudiment::RhythmicValue
|
|
6
|
+
|
|
7
|
+
PATTERN = /((double|triple)\W?)?(dotted)?.?(#{HeadMusic::Rudiment::RhythmicUnit::PATTERN})/
|
|
8
|
+
|
|
9
|
+
# For stuff like the "q." in "q. = 108"
|
|
10
|
+
SHORTHAND_PATTERN = /\A(#{HeadMusic::Rudiment::RhythmicUnit::Parser::TEMPO_SHORTHAND_PATTERN})(\.*)?\z/i
|
|
11
|
+
|
|
12
|
+
# Parse a rhythmic value identifier and return a RhythmicValue object
|
|
13
|
+
# Returns nil if the identifier cannot be parsed
|
|
14
|
+
def self.parse(identifier)
|
|
15
|
+
return nil if identifier.nil?
|
|
16
|
+
new(identifier).rhythmic_value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(identifier)
|
|
20
|
+
@identifier = identifier.to_s.strip
|
|
21
|
+
parse_components
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def parse_components
|
|
27
|
+
# First check for shorthand patterns like "q." to avoid infinite recursion
|
|
28
|
+
match = identifier.match(SHORTHAND_PATTERN)
|
|
29
|
+
if match && match[1]
|
|
30
|
+
unit_name = RhythmicUnit::Parser.parse(match[1].to_s.strip)
|
|
31
|
+
dots = match[2] ? match[2].strip.length : 0
|
|
32
|
+
@rhythmic_value = RhythmicValue.new(unit_name, dots: dots) if unit_name
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Try RhythmicUnit::Parser directly first (handles fractions, decimals, British names, etc.)
|
|
37
|
+
parser = RhythmicUnit::Parser.new(identifier)
|
|
38
|
+
if parser.american_name
|
|
39
|
+
@rhythmic_value = RhythmicValue.new(parser.american_name, dots: 0)
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Then try to parse with dots extracted for formats like "1/4."
|
|
44
|
+
# Count and strip dots (e.g., "1/4." -> "1/4" with 1 dot)
|
|
45
|
+
# But skip this if identifier looks like a decimal number
|
|
46
|
+
unless identifier.match?(/^\d+\.\d+$/)
|
|
47
|
+
dots = identifier.scan(".").count
|
|
48
|
+
base_identifier = identifier.gsub(".", "").strip
|
|
49
|
+
|
|
50
|
+
# Try RhythmicUnit::Parser on the base identifier
|
|
51
|
+
parser = RhythmicUnit::Parser.new(base_identifier)
|
|
52
|
+
if parser.american_name
|
|
53
|
+
@rhythmic_value = RhythmicValue.new(parser.american_name, dots: dots)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Finally check the word pattern for things like "dotted quarter"
|
|
59
|
+
match = identifier.match(PATTERN)
|
|
60
|
+
if match
|
|
61
|
+
matched_string = match[0].to_s.strip
|
|
62
|
+
# Extract unit and dots from the matched string
|
|
63
|
+
unit_part = matched_string.gsub(/^\W*(double|triple)?\W*(dotted)?\W*/, "")
|
|
64
|
+
unit = RhythmicUnit.get(unit_part)
|
|
65
|
+
if unit
|
|
66
|
+
dots = if matched_string.include?("triple")
|
|
67
|
+
3
|
|
68
|
+
elsif matched_string.include?("double")
|
|
69
|
+
2
|
|
70
|
+
else
|
|
71
|
+
matched_string.include?("dotted") ? 1 : 0
|
|
72
|
+
end
|
|
73
|
+
@rhythmic_value = RhythmicValue.new(unit, dots: dots)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# A module for musical content
|
|
2
|
-
module HeadMusic::
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A rhythmic value is a duration composed of a rhythmic unit, any number of dots, and a tied value.
|
|
5
|
-
class HeadMusic::
|
|
5
|
+
class HeadMusic::Rudiment::RhythmicValue
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
RhythmicUnit = HeadMusic::Rudiment::RhythmicUnit
|
|
9
|
+
|
|
6
10
|
attr_reader :unit, :dots, :tied_value
|
|
7
11
|
|
|
8
12
|
delegate :name, to: :unit, prefix: true
|
|
@@ -10,13 +14,23 @@ class HeadMusic::Content::RhythmicValue
|
|
|
10
14
|
|
|
11
15
|
def self.get(identifier)
|
|
12
16
|
case identifier
|
|
13
|
-
when HeadMusic::
|
|
17
|
+
when HeadMusic::Rudiment::RhythmicValue
|
|
14
18
|
identifier
|
|
15
19
|
when HeadMusic::Rudiment::RhythmicUnit
|
|
16
20
|
new(identifier)
|
|
17
21
|
when Symbol, String
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
original_identifier = identifier.to_s.strip
|
|
23
|
+
# First try the new parser which handles all formats
|
|
24
|
+
parsed = Parser.parse(original_identifier)
|
|
25
|
+
return parsed if parsed
|
|
26
|
+
|
|
27
|
+
# Then try the word-based approach as fallback
|
|
28
|
+
identifier = original_identifier.downcase.gsub(/\W+/, "_")
|
|
29
|
+
begin
|
|
30
|
+
from_words(identifier)
|
|
31
|
+
rescue
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
20
34
|
end
|
|
21
35
|
end
|
|
22
36
|
|
|
@@ -102,4 +116,8 @@ class HeadMusic::Content::RhythmicValue
|
|
|
102
116
|
def to_s
|
|
103
117
|
name.tr("_", "-")
|
|
104
118
|
end
|
|
119
|
+
|
|
120
|
+
def <=>(other)
|
|
121
|
+
total_value <=> other.total_value
|
|
122
|
+
end
|
|
105
123
|
end
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A scale contains ordered pitches starting at a tonal center.
|
|
5
|
-
class HeadMusic::Rudiment::Scale
|
|
5
|
+
class HeadMusic::Rudiment::Scale < HeadMusic::Rudiment::Base
|
|
6
6
|
SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
|
|
7
7
|
|
|
8
8
|
def self.get(root_pitch, scale_type = nil)
|
|
9
|
-
root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
|
|
10
9
|
root_pitch = HeadMusic::Rudiment::Pitch.get(root_pitch)
|
|
11
|
-
scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type ||
|
|
10
|
+
scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type || HeadMusic::Rudiment::ScaleType::DEFAULT)
|
|
12
11
|
@scales ||= {}
|
|
13
12
|
hash_key = HeadMusic::Utilities::HashKey.for(
|
|
14
13
|
[root_pitch, scale_type].join(" ").gsub(/#|♯/, "sharp").gsub(/(\w)[b♭]/, '\\1flat')
|
|
@@ -36,7 +35,7 @@ class HeadMusic::Rudiment::Scale
|
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def spellings(direction: :ascending, octaves: 1)
|
|
39
|
-
pitches(direction: direction, octaves: octaves).map(&:spelling)
|
|
38
|
+
pitches(direction: direction, octaves: octaves).map(&:spelling)
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
def pitch_names(direction: :ascending, octaves: 1)
|
|
@@ -83,7 +82,7 @@ class HeadMusic::Rudiment::Scale
|
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def parent_scale_pitches
|
|
86
|
-
HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches
|
|
85
|
+
HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def parent_scale_pitch_for(semitones_from_root)
|
|
@@ -3,7 +3,7 @@ module HeadMusic::Rudiment; end
|
|
|
3
3
|
|
|
4
4
|
# A scale degree is a number indicating the ordinality of the spelling in the key.
|
|
5
5
|
# TODO: Rewrite to accept a tonal_center and a scale type.
|
|
6
|
-
class HeadMusic::Rudiment::ScaleDegree
|
|
6
|
+
class HeadMusic::Rudiment::ScaleDegree < HeadMusic::Rudiment::Base
|
|
7
7
|
include Comparable
|
|
8
8
|
|
|
9
9
|
NAME_FOR_DIATONIC_DEGREE = [nil, "tonic", "supertonic", "mediant", "subdominant", "dominant", "submediant"].freeze
|
|
@@ -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
|
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A ScaleType represents a particular scale pattern, such as major, lydian, or minor pentatonic.
|
|
5
|
-
class HeadMusic::Rudiment::ScaleType
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
class HeadMusic::Rudiment::ScaleType < HeadMusic::Rudiment::Base
|
|
6
|
+
# TODO: load scale types from yaml configuration file
|
|
7
|
+
# Include a system of aliasing, e.g. :natural_minor, :aeolian
|
|
8
|
+
# Include ascending and descending intervals for scales that differ
|
|
9
|
+
|
|
10
|
+
H = 1 # half step
|
|
11
|
+
W = 2 # whole step
|
|
8
12
|
|
|
9
13
|
# Modal
|
|
10
14
|
I = [W, W, H, W, W, W, H].freeze
|
|
@@ -33,6 +37,8 @@ class HeadMusic::Rudiment::ScaleType
|
|
|
33
37
|
|
|
34
38
|
MINOR_PENTATONIC = [3, 2, 2, 3, 2].freeze
|
|
35
39
|
|
|
40
|
+
DEFAULT = :major
|
|
41
|
+
|
|
36
42
|
def self._modes
|
|
37
43
|
{}.tap do |modes|
|
|
38
44
|
MODE_NAMES.each do |roman_numeral, aliases|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A solmization is the rendering of scale degrees as syllables.
|
|
5
|
-
class HeadMusic::Rudiment::Solmization
|
|
5
|
+
class HeadMusic::Rudiment::Solmization < HeadMusic::Rudiment::Base
|
|
6
6
|
include HeadMusic::Named
|
|
7
7
|
|
|
8
8
|
DEFAULT_SOLMIZATION = "solfège"
|
|
@@ -4,8 +4,11 @@ module HeadMusic::Rudiment; end
|
|
|
4
4
|
# Represents the spelling of a pitch, such as C# or Db.
|
|
5
5
|
# Composite of a LetterName and an optional Alteration.
|
|
6
6
|
# Does not include the octave. See Pitch for that.
|
|
7
|
-
class HeadMusic::Rudiment::Spelling
|
|
8
|
-
|
|
7
|
+
class HeadMusic::Rudiment::Spelling < HeadMusic::Rudiment::Base
|
|
8
|
+
LetterName = HeadMusic::Rudiment::LetterName
|
|
9
|
+
Alteration = HeadMusic::Rudiment::Alteration
|
|
10
|
+
|
|
11
|
+
MATCHER = /^\s*(#{LetterName::PATTERN})(#{Alteration::PATTERN})?(-?\d+)?\s*$/i
|
|
9
12
|
|
|
10
13
|
attr_reader :pitch_class, :letter_name, :alteration
|
|
11
14
|
|
|
@@ -86,8 +89,6 @@ class HeadMusic::Rudiment::Spelling
|
|
|
86
89
|
!alteration || alteration.natural?
|
|
87
90
|
end
|
|
88
91
|
|
|
89
|
-
private_class_method :new
|
|
90
|
-
|
|
91
92
|
private
|
|
92
93
|
|
|
93
94
|
def enharmonic_equivalence
|