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,77 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Represents a musical key (major or minor)
|
|
5
|
+
class HeadMusic::Rudiment::Key < HeadMusic::Rudiment::DiatonicContext
|
|
6
|
+
include HeadMusic::Named
|
|
7
|
+
|
|
8
|
+
QUALITIES = %i[major minor].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :quality
|
|
11
|
+
|
|
12
|
+
def self.get(identifier)
|
|
13
|
+
return identifier if identifier.is_a?(HeadMusic::Rudiment::Key)
|
|
14
|
+
|
|
15
|
+
@keys ||= {}
|
|
16
|
+
tonic_spelling, quality_name = parse_identifier(identifier)
|
|
17
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
18
|
+
@keys[hash_key] ||= new(tonic_spelling, quality_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.parse_identifier(identifier)
|
|
22
|
+
tonic_spelling, quality_name = identifier.to_s.strip.split(/\s+/)
|
|
23
|
+
quality_name ||= "major"
|
|
24
|
+
[tonic_spelling, quality_name]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(tonic_spelling, quality = :major)
|
|
28
|
+
super(tonic_spelling)
|
|
29
|
+
@quality = quality.to_s.downcase.to_sym
|
|
30
|
+
raise ArgumentError, "Quality must be :major or :minor" unless QUALITIES.include?(@quality)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scale_type
|
|
34
|
+
@scale_type ||= HeadMusic::Rudiment::ScaleType.get(quality)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def major?
|
|
38
|
+
quality == :major
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def minor?
|
|
42
|
+
quality == :minor
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def relative
|
|
46
|
+
if major?
|
|
47
|
+
# Major to relative minor: down a minor third (3 semitones)
|
|
48
|
+
relative_pitch = tonic_pitch + -3
|
|
49
|
+
self.class.get("#{relative_pitch.spelling} minor")
|
|
50
|
+
else
|
|
51
|
+
# Minor to relative major: up a minor third (3 semitones)
|
|
52
|
+
relative_pitch = tonic_pitch + 3
|
|
53
|
+
self.class.get("#{relative_pitch.spelling} major")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parallel
|
|
58
|
+
if major?
|
|
59
|
+
self.class.get("#{tonic_spelling} minor")
|
|
60
|
+
else
|
|
61
|
+
self.class.get("#{tonic_spelling} major")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def name
|
|
66
|
+
"#{tonic_spelling} #{quality}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_s
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ==(other)
|
|
74
|
+
other = self.class.get(other)
|
|
75
|
+
tonic_spelling == other.tonic_spelling && quality == other.quality
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Key signatures are enharmonic when
|
|
1
|
+
# Key signatures are enharmonic when they represent the same set of altered pitch classes but with different spellings.
|
|
2
2
|
class HeadMusic::Rudiment::KeySignature::EnharmonicEquivalence
|
|
3
3
|
attr_reader :key_signature
|
|
4
4
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# A module for music rudiments
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
|
-
# Represents a key signature
|
|
5
|
-
class
|
|
6
|
-
|
|
4
|
+
# Represents a key signature (traditionally associated with a key)
|
|
5
|
+
# This class maintains backward compatibility while delegating to Key/Mode internally
|
|
6
|
+
class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
7
|
+
attr_reader :tonal_context
|
|
7
8
|
|
|
8
9
|
ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
|
|
9
10
|
ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
|
|
@@ -16,14 +17,32 @@ class HeadMusic::Rudiment::KeySignature
|
|
|
16
17
|
return identifier if identifier.is_a?(HeadMusic::Rudiment::KeySignature)
|
|
17
18
|
|
|
18
19
|
@key_signatures ||= {}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
if identifier.is_a?(String)
|
|
22
|
+
tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
|
|
23
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
|
|
24
|
+
@key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
|
|
25
|
+
elsif identifier.is_a?(HeadMusic::Rudiment::DiatonicContext)
|
|
26
|
+
identifier.key_signature
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.from_tonal_context(tonal_context)
|
|
31
|
+
new_from_context(tonal_context)
|
|
22
32
|
end
|
|
23
33
|
|
|
34
|
+
def self.from_scale(scale)
|
|
35
|
+
# Find a key or mode that uses this scale
|
|
36
|
+
tonic = scale.root_pitch.spelling
|
|
37
|
+
scale_type = scale.scale_type
|
|
38
|
+
new(tonic, scale_type)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :tonic_spelling, :scale_type, :scale
|
|
42
|
+
|
|
24
43
|
delegate :pitch_class, to: :tonic_spelling, prefix: :tonic
|
|
25
|
-
delegate :to_s, to: :name
|
|
26
44
|
delegate :pitches, :pitch_classes, to: :scale
|
|
45
|
+
delegate :to_s, to: :name
|
|
27
46
|
|
|
28
47
|
def initialize(tonic_spelling, scale_type = nil)
|
|
29
48
|
@tonic_spelling = HeadMusic::Rudiment::Spelling.get(tonic_spelling)
|
|
@@ -31,6 +50,26 @@ class HeadMusic::Rudiment::KeySignature
|
|
|
31
50
|
@scale_type ||= HeadMusic::Rudiment::ScaleType.default
|
|
32
51
|
@scale_type = @scale_type.parent || @scale_type
|
|
33
52
|
@scale = HeadMusic::Rudiment::Scale.get(@tonic_spelling, @scale_type)
|
|
53
|
+
|
|
54
|
+
# Create appropriate tonal context
|
|
55
|
+
scale_type_str = scale_type.to_s.downcase if scale_type
|
|
56
|
+
|
|
57
|
+
@tonal_context = if %w[major minor].include?(scale_type_str)
|
|
58
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} #{scale_type}")
|
|
59
|
+
elsif scale_type
|
|
60
|
+
HeadMusic::Rudiment::Mode.get("#{tonic_spelling} #{scale_type}")
|
|
61
|
+
else
|
|
62
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
63
|
+
end
|
|
64
|
+
rescue ArgumentError
|
|
65
|
+
# Fall back to creating a major key if mode is not recognized
|
|
66
|
+
@tonal_context = HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.new_from_context(context)
|
|
70
|
+
instance = allocate
|
|
71
|
+
instance.instance_variable_set(:@tonal_context, context)
|
|
72
|
+
instance
|
|
34
73
|
end
|
|
35
74
|
|
|
36
75
|
def spellings
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# Music has seven lette names that are used to identify pitches and pitch classes.
|
|
5
|
-
class HeadMusic::Rudiment::LetterName
|
|
5
|
+
class HeadMusic::Rudiment::LetterName < HeadMusic::Rudiment::Base
|
|
6
6
|
NAMES = %w[C D E F G A B].freeze
|
|
7
|
+
PATTERN = /[A-Ga-g]/
|
|
8
|
+
MATCHER = /^#{PATTERN}$/
|
|
7
9
|
|
|
8
10
|
NATURAL_PITCH_CLASS_NUMBERS = {
|
|
9
11
|
"C" => 0,
|
|
@@ -96,6 +98,4 @@ class HeadMusic::Rudiment::LetterName
|
|
|
96
98
|
series
|
|
97
99
|
end
|
|
98
100
|
end
|
|
99
|
-
|
|
100
|
-
private_class_method :new
|
|
101
101
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# Meter is the rhythmic size of a measure, such as 4/4 or 6/8
|
|
5
|
-
class HeadMusic::Rudiment::Meter
|
|
5
|
+
class HeadMusic::Rudiment::Meter < HeadMusic::Rudiment::Base
|
|
6
6
|
attr_reader :top_number, :bottom_number
|
|
7
7
|
|
|
8
8
|
NAMED = {
|
|
@@ -15,7 +15,7 @@ class HeadMusic::Rudiment::Meter
|
|
|
15
15
|
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
16
16
|
time_signature_string = NAMED[hash_key] || identifier
|
|
17
17
|
@meters ||= {}
|
|
18
|
-
@meters[hash_key] ||= new(*time_signature_string.split("/")
|
|
18
|
+
@meters[hash_key] ||= new(*time_signature_string.split("/"))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.default
|
|
@@ -31,8 +31,8 @@ class HeadMusic::Rudiment::Meter
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def initialize(top_number, bottom_number)
|
|
34
|
-
@top_number = top_number
|
|
35
|
-
@bottom_number = bottom_number
|
|
34
|
+
@top_number = top_number.to_i
|
|
35
|
+
@bottom_number = bottom_number.to_i
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def simple?
|
|
@@ -63,6 +63,10 @@ class HeadMusic::Rudiment::Meter
|
|
|
63
63
|
top_number
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
def counts_per_quarter_note
|
|
67
|
+
0.25 / count_unit.relative_value
|
|
68
|
+
end
|
|
69
|
+
|
|
66
70
|
def beat_strength(count, tick: 0)
|
|
67
71
|
return 100 if downbeat?(count, tick)
|
|
68
72
|
return 80 if strong_beat?(count, tick)
|
|
@@ -76,19 +80,25 @@ class HeadMusic::Rudiment::Meter
|
|
|
76
80
|
@ticks_per_count ||= count_unit.ticks
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
# The rhythmic unit for the count (bottom number).
|
|
84
|
+
# This unit is also used as "beats" in a sequencer context
|
|
85
|
+
# For example, "1:3:000"
|
|
79
86
|
def count_unit
|
|
80
87
|
HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number)
|
|
81
88
|
end
|
|
82
89
|
|
|
83
|
-
def
|
|
84
|
-
@
|
|
90
|
+
def beat_value
|
|
91
|
+
@beat_value ||=
|
|
85
92
|
if compound?
|
|
86
|
-
HeadMusic::
|
|
93
|
+
HeadMusic::Rudiment::RhythmicValue.new(HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
|
|
87
94
|
else
|
|
88
|
-
HeadMusic::
|
|
95
|
+
HeadMusic::Rudiment::RhythmicValue.new(count_unit)
|
|
89
96
|
end
|
|
90
97
|
end
|
|
91
98
|
|
|
99
|
+
# for consistency with conversational usage
|
|
100
|
+
alias_method :beat_unit, :beat_value
|
|
101
|
+
|
|
92
102
|
def to_s
|
|
93
103
|
[top_number, bottom_number].join("/")
|
|
94
104
|
end
|
|
@@ -133,6 +143,6 @@ class HeadMusic::Rudiment::Meter
|
|
|
133
143
|
end
|
|
134
144
|
|
|
135
145
|
def beat?(tick)
|
|
136
|
-
tick.zero?
|
|
146
|
+
tick.to_i.zero?
|
|
137
147
|
end
|
|
138
148
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Represents a musical mode (church modes)
|
|
5
|
+
class HeadMusic::Rudiment::Mode < HeadMusic::Rudiment::DiatonicContext
|
|
6
|
+
include HeadMusic::Named
|
|
7
|
+
|
|
8
|
+
MODES = %i[ionian dorian phrygian lydian mixolydian aeolian locrian].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :mode_name
|
|
11
|
+
|
|
12
|
+
def self.get(identifier)
|
|
13
|
+
return identifier if identifier.is_a?(HeadMusic::Rudiment::Mode)
|
|
14
|
+
|
|
15
|
+
@modes ||= {}
|
|
16
|
+
tonic_spelling, mode_name = parse_identifier(identifier)
|
|
17
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
18
|
+
@modes[hash_key] ||= new(tonic_spelling, mode_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.parse_identifier(identifier)
|
|
22
|
+
identifier = identifier.to_s.strip
|
|
23
|
+
parts = identifier.split(/\s+/)
|
|
24
|
+
tonic_spelling = parts[0]
|
|
25
|
+
mode_name = parts[1] || "ionian"
|
|
26
|
+
[tonic_spelling, mode_name]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(tonic_spelling, mode_name = :ionian)
|
|
30
|
+
super(tonic_spelling)
|
|
31
|
+
@mode_name = mode_name.to_s.downcase.to_sym
|
|
32
|
+
raise ArgumentError, "Mode must be one of: #{MODES.join(", ")}" unless MODES.include?(@mode_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def scale_type
|
|
36
|
+
@scale_type ||= HeadMusic::Rudiment::ScaleType.get(mode_name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def relative_major
|
|
40
|
+
case mode_name
|
|
41
|
+
when :ionian
|
|
42
|
+
return HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
43
|
+
when :dorian
|
|
44
|
+
relative_pitch = tonic_pitch + -2
|
|
45
|
+
when :phrygian
|
|
46
|
+
relative_pitch = tonic_pitch + -4
|
|
47
|
+
when :lydian
|
|
48
|
+
relative_pitch = tonic_pitch + -5
|
|
49
|
+
when :mixolydian
|
|
50
|
+
relative_pitch = tonic_pitch + -7
|
|
51
|
+
when :aeolian
|
|
52
|
+
relative_pitch = tonic_pitch + -9
|
|
53
|
+
when :locrian
|
|
54
|
+
relative_pitch = tonic_pitch + -11
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
HeadMusic::Rudiment::Key.get("#{relative_pitch.spelling} major")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def relative
|
|
61
|
+
relative_major
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parallel
|
|
65
|
+
# Return the parallel major or minor key
|
|
66
|
+
case mode_name
|
|
67
|
+
when :ionian
|
|
68
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
69
|
+
when :aeolian
|
|
70
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
71
|
+
when :dorian, :phrygian
|
|
72
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
73
|
+
when :lydian, :mixolydian
|
|
74
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
75
|
+
when :locrian
|
|
76
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def name
|
|
81
|
+
"#{tonic_spelling} #{mode_name}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ==(other)
|
|
89
|
+
other = self.class.get(other)
|
|
90
|
+
tonic_spelling == other.tonic_spelling && mode_name == other.mode_name
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A symbol is a mark or sign that signifies a particular rudiment of music
|
|
5
|
-
class HeadMusic::Rudiment::MusicalSymbol
|
|
5
|
+
class HeadMusic::Rudiment::MusicalSymbol < HeadMusic::Rudiment::Base
|
|
6
6
|
attr_reader :ascii, :unicode, :html_entity
|
|
7
7
|
|
|
8
8
|
def initialize(ascii: nil, unicode: nil, html_entity: nil)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# A Note is a fundamental musical element consisting of a pitch and a duration.
|
|
5
|
+
# This is the rudiment version, representing the abstract concept of a note
|
|
6
|
+
# independent of its placement in a composition.
|
|
7
|
+
#
|
|
8
|
+
# For notes placed within a composition context, see HeadMusic::Content::Note
|
|
9
|
+
class HeadMusic::Rudiment::Note < HeadMusic::Rudiment::RhythmicElement
|
|
10
|
+
include HeadMusic::Named
|
|
11
|
+
|
|
12
|
+
attr_reader :pitch
|
|
13
|
+
|
|
14
|
+
delegate :spelling, :register, :letter_name, :alteration, to: :pitch
|
|
15
|
+
delegate :sharp?, :flat?, :natural?, to: :pitch
|
|
16
|
+
delegate :pitch_class, :midi_note_number, :frequency, to: :pitch
|
|
17
|
+
|
|
18
|
+
# Regex pattern for parsing note strings like "C#4 quarter" or "Eb3 dotted half"
|
|
19
|
+
# Extract the core pattern from Spelling::MATCHER without anchors
|
|
20
|
+
PITCH_PATTERN = /([A-G])(#{HeadMusic::Rudiment::Alteration::PATTERN.source}?)(-?\d+)?/i
|
|
21
|
+
MATCHER = /^\s*(#{PITCH_PATTERN.source})\s+(.+)$/i
|
|
22
|
+
|
|
23
|
+
def self.get(pitch, rhythmic_value = nil)
|
|
24
|
+
return pitch if pitch.is_a?(HeadMusic::Rudiment::Note)
|
|
25
|
+
|
|
26
|
+
if rhythmic_value.nil? && pitch.is_a?(String)
|
|
27
|
+
# Try to parse as "pitch rhythmic_value" format (e.g., "F#4 dotted-quarter")
|
|
28
|
+
match = pitch.match(MATCHER)
|
|
29
|
+
if match
|
|
30
|
+
pitch_str = match[1] # The full pitch part
|
|
31
|
+
rhythmic_value_str = match[5] # The rhythmic value part
|
|
32
|
+
|
|
33
|
+
pitch_obj = HeadMusic::Rudiment::Pitch.get(pitch_str)
|
|
34
|
+
rhythmic_value_obj = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value_str)
|
|
35
|
+
|
|
36
|
+
return fetch_or_create(pitch_obj, rhythmic_value_obj) if pitch_obj && rhythmic_value_obj
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# If parsing fails, treat it as just a pitch with default quarter note
|
|
40
|
+
pitch_obj = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
41
|
+
return fetch_or_create(pitch_obj, HeadMusic::Rudiment::RhythmicValue.get(:quarter)) if pitch_obj
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
else
|
|
45
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
46
|
+
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value || :quarter)
|
|
47
|
+
fetch_or_create(pitch, rhythmic_value)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.fetch_or_create(pitch, rhythmic_value)
|
|
52
|
+
@notes ||= {}
|
|
53
|
+
hash_key = [pitch.to_s, rhythmic_value.to_s].join("_")
|
|
54
|
+
@notes[hash_key] ||= new(pitch, rhythmic_value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize(pitch, rhythmic_value)
|
|
58
|
+
super(rhythmic_value)
|
|
59
|
+
@pitch = pitch
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Make new public for this concrete class
|
|
63
|
+
public_class_method :new
|
|
64
|
+
|
|
65
|
+
def name
|
|
66
|
+
"#{pitch} #{rhythmic_value}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_s
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ==(other)
|
|
74
|
+
other = HeadMusic::Rudiment::Note.get(other)
|
|
75
|
+
super && pitch == other.pitch
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def <=>(other)
|
|
79
|
+
return nil unless other.is_a?(HeadMusic::Rudiment::RhythmicElement)
|
|
80
|
+
return super unless other.is_a?(self.class)
|
|
81
|
+
|
|
82
|
+
[rhythmic_value, pitch] <=> [other.rhythmic_value, other.pitch]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Transpose the note up by an interval or semitones
|
|
86
|
+
def +(other)
|
|
87
|
+
new_pitch = pitch + other
|
|
88
|
+
self.class.get(new_pitch, rhythmic_value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Transpose the note down by an interval or semitones
|
|
92
|
+
def -(other)
|
|
93
|
+
new_pitch = pitch - other
|
|
94
|
+
self.class.get(new_pitch, rhythmic_value)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Override to maintain pitch when changing rhythmic value
|
|
98
|
+
def with_rhythmic_value(new_rhythmic_value)
|
|
99
|
+
self.class.get(pitch, new_rhythmic_value)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Change the pitch while keeping the same rhythmic value
|
|
103
|
+
def with_pitch(new_pitch)
|
|
104
|
+
self.class.get(new_pitch, rhythmic_value)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def sounded?
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_class_method :fetch_or_create
|
|
112
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class HeadMusic::Rudiment::Pitch::Parser
|
|
2
|
+
attr_reader :identifier, :letter_name, :alteration, :register
|
|
3
|
+
|
|
4
|
+
LetterName = HeadMusic::Rudiment::LetterName
|
|
5
|
+
Alteration = HeadMusic::Rudiment::Alteration
|
|
6
|
+
Spelling = HeadMusic::Rudiment::Spelling
|
|
7
|
+
Register = HeadMusic::Rudiment::Register
|
|
8
|
+
Pitch = HeadMusic::Rudiment::Pitch
|
|
9
|
+
|
|
10
|
+
# Pattern that handles negative registers (e.g., -1) and positive registers
|
|
11
|
+
# Anchored to match complete pitch strings only
|
|
12
|
+
PATTERN = /\A(#{LetterName::PATTERN})?(#{Alteration::PATTERN.source})?(-?\d+)?\z/
|
|
13
|
+
|
|
14
|
+
# Parse a pitch identifier and return a Pitch object
|
|
15
|
+
# Returns nil if the identifier cannot be parsed into a valid pitch
|
|
16
|
+
def self.parse(identifier)
|
|
17
|
+
return nil if identifier.nil?
|
|
18
|
+
new(identifier).pitch
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(identifier)
|
|
22
|
+
@identifier = identifier.to_s.strip
|
|
23
|
+
parse_components
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pitch
|
|
27
|
+
return unless spelling
|
|
28
|
+
# Default to register 4 if not provided (matching old behavior)
|
|
29
|
+
# Convert Register object to integer for fetch_or_create
|
|
30
|
+
reg = register ? register.to_i : Register::DEFAULT
|
|
31
|
+
|
|
32
|
+
@pitch ||= Pitch.fetch_or_create(spelling, reg)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def spelling
|
|
36
|
+
return unless letter_name
|
|
37
|
+
|
|
38
|
+
@spelling ||= Spelling.new(letter_name, alteration)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_components
|
|
44
|
+
match = identifier.match(PATTERN)
|
|
45
|
+
|
|
46
|
+
if match
|
|
47
|
+
@letter_name = LetterName.get(match[1].upcase) unless match[1].to_s.empty?
|
|
48
|
+
@alteration = Alteration.get(match[2] || "") unless match[2].to_s.empty?
|
|
49
|
+
@register = Register.get(match[3]&.to_i) unless match[3].to_s.empty?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
# A module for music rudiments
|
|
2
1
|
module HeadMusic::Rudiment; end
|
|
3
2
|
|
|
4
3
|
# A pitch is a named frequency represented by a spelling and a register.
|
|
5
|
-
class HeadMusic::Rudiment::Pitch
|
|
4
|
+
class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
|
|
6
5
|
include Comparable
|
|
7
6
|
|
|
8
7
|
attr_reader :spelling, :register
|
|
@@ -29,6 +28,8 @@ class HeadMusic::Rudiment::Pitch
|
|
|
29
28
|
# - a name string, such as 'Ab4'
|
|
30
29
|
# - a number corresponding to the midi note number
|
|
31
30
|
def self.get(value)
|
|
31
|
+
return value if value.is_a?(HeadMusic::Rudiment::Pitch)
|
|
32
|
+
|
|
32
33
|
from_pitch_class(value) ||
|
|
33
34
|
from_name(value) ||
|
|
34
35
|
from_number(value)
|
|
@@ -51,7 +52,7 @@ class HeadMusic::Rudiment::Pitch
|
|
|
51
52
|
def self.from_name(name)
|
|
52
53
|
return nil unless name == name.to_s
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
Parser.parse(name)
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def self.from_number(number)
|
|
@@ -114,7 +115,7 @@ class HeadMusic::Rudiment::Pitch
|
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
def natural
|
|
117
|
-
HeadMusic::Rudiment::Pitch.get(to_s.gsub(HeadMusic::Rudiment::Alteration
|
|
118
|
+
HeadMusic::Rudiment::Pitch.get(to_s.gsub(HeadMusic::Rudiment::Alteration::PATTERN, ""))
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
def +(other)
|
|
@@ -167,8 +168,6 @@ class HeadMusic::Rudiment::Pitch
|
|
|
167
168
|
letter_name_steps_to(other) + 7 * octave_changes_to(other)
|
|
168
169
|
end
|
|
169
170
|
|
|
170
|
-
private_class_method :new
|
|
171
|
-
|
|
172
171
|
private
|
|
173
172
|
|
|
174
173
|
def octave_changes_to(other)
|
|
@@ -3,7 +3,7 @@ module HeadMusic::Rudiment; end
|
|
|
3
3
|
|
|
4
4
|
# A reference pitch has a pitch and a frequency
|
|
5
5
|
# With no arguments, it assumes that A4 = 440.0 Hz
|
|
6
|
-
class HeadMusic::Rudiment::ReferencePitch
|
|
6
|
+
class HeadMusic::Rudiment::ReferencePitch < HeadMusic::Rudiment::Base
|
|
7
7
|
include HeadMusic::Named
|
|
8
8
|
|
|
9
9
|
DEFAULT_PITCH_NAME = "A4"
|
|
@@ -6,9 +6,12 @@ module HeadMusic::Rudiment; end
|
|
|
6
6
|
# A pitch is a spelling plus a register. For example, C4 is middle C and C5 is the C one octave higher.
|
|
7
7
|
# The number changes between the letter names B and C regardless of sharps and flats,
|
|
8
8
|
# so as an extreme example, Cb5 is actually a semitone below B#4.
|
|
9
|
-
class HeadMusic::Rudiment::Register
|
|
9
|
+
class HeadMusic::Rudiment::Register < HeadMusic::Rudiment::Base
|
|
10
10
|
include Comparable
|
|
11
11
|
|
|
12
|
+
AUDIBLE_REGISTERS = (0..10).map.freeze
|
|
13
|
+
PATTERN = Regexp.union(AUDIBLE_REGISTERS.map(&:to_s))
|
|
14
|
+
|
|
12
15
|
DEFAULT = 4
|
|
13
16
|
|
|
14
17
|
def self.get(identifier)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# A Rest represents a period of silence with a specific rhythmic value.
|
|
5
|
+
# It inherits from RhythmicElement and has a duration but no pitch.
|
|
6
|
+
class HeadMusic::Rudiment::Rest < HeadMusic::Rudiment::RhythmicElement
|
|
7
|
+
include HeadMusic::Named
|
|
8
|
+
|
|
9
|
+
# Make new public for this concrete class
|
|
10
|
+
public_class_method :new
|
|
11
|
+
|
|
12
|
+
def self.get(rhythmic_value)
|
|
13
|
+
return rhythmic_value if rhythmic_value.is_a?(HeadMusic::Rudiment::Rest)
|
|
14
|
+
|
|
15
|
+
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value)
|
|
16
|
+
return nil unless rhythmic_value
|
|
17
|
+
|
|
18
|
+
fetch_or_create(rhythmic_value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.fetch_or_create(rhythmic_value)
|
|
22
|
+
@rests ||= {}
|
|
23
|
+
hash_key = rhythmic_value.to_s
|
|
24
|
+
@rests[hash_key] ||= new(rhythmic_value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def name
|
|
28
|
+
"#{rhythmic_value} rest"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sounded?
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method :fetch_or_create
|
|
36
|
+
end
|