head_music 8.3.0 → 11.0.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 +9 -3
- data/CHANGELOG.md +71 -0
- data/CLAUDE.md +62 -25
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/MUSIC_THEORY.md +120 -0
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +29 -27
- data/lib/head_music/analysis/dyad.rb +229 -0
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/analysis/melodic_interval.rb +1 -1
- data/lib/head_music/analysis/pitch_class_set.rb +111 -14
- data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
- data/lib/head_music/analysis/sonority.rb +50 -12
- 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/voice.rb +1 -1
- data/lib/head_music/instruments/alternate_tuning.rb +102 -0
- data/lib/head_music/instruments/alternate_tunings.yml +78 -0
- data/lib/head_music/instruments/instrument.rb +231 -72
- data/lib/head_music/instruments/instrument_configuration.rb +66 -0
- data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
- data/lib/head_music/instruments/instrument_configurations.yml +288 -0
- data/lib/head_music/instruments/instrument_families.yml +77 -0
- data/lib/head_music/instruments/instrument_family.rb +15 -5
- data/lib/head_music/instruments/instruments.yml +795 -965
- data/lib/head_music/instruments/playing_technique.rb +75 -0
- data/lib/head_music/instruments/playing_techniques.yml +826 -0
- data/lib/head_music/instruments/score_order.rb +136 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/staff.rb +61 -1
- data/lib/head_music/instruments/staff_scheme.rb +6 -4
- data/lib/head_music/instruments/stringing.rb +115 -0
- data/lib/head_music/instruments/stringing_course.rb +58 -0
- data/lib/head_music/instruments/stringings.yml +168 -0
- data/lib/head_music/instruments/variant.rb +6 -1
- data/lib/head_music/locales/de.yml +29 -0
- data/lib/head_music/locales/en.yml +106 -0
- data/lib/head_music/locales/es.yml +29 -0
- data/lib/head_music/locales/fr.yml +29 -0
- data/lib/head_music/locales/it.yml +29 -0
- data/lib/head_music/locales/ru.yml +29 -0
- data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
- data/lib/head_music/notation/staff_mapping.rb +70 -0
- data/lib/head_music/notation/staff_position.rb +62 -0
- data/lib/head_music/notation.rb +7 -0
- data/lib/head_music/rudiment/alteration.rb +34 -49
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +4 -7
- data/lib/head_music/rudiment/clef.rb +2 -2
- data/lib/head_music/rudiment/consonance.rb +39 -5
- 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 +21 -8
- 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/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 +13 -5
- 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 +1 -1
- 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 +8 -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 +0 -39
- data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
- data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
- data/lib/head_music/rudiment/tuning.rb +21 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +31 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/time/clock_position.rb +84 -0
- data/lib/head_music/time/conductor.rb +264 -0
- data/lib/head_music/time/meter_event.rb +37 -0
- data/lib/head_music/time/meter_map.rb +173 -0
- data/lib/head_music/time/musical_position.rb +188 -0
- data/lib/head_music/time/smpte_timecode.rb +164 -0
- data/lib/head_music/time/tempo_event.rb +40 -0
- data/lib/head_music/time/tempo_map.rb +187 -0
- data/lib/head_music/time.rb +32 -0
- data/lib/head_music/utilities/case.rb +27 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +71 -22
- data/user_stories/active/string-pitches.md +41 -0
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/backlog/organizing-content.md +80 -0
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{backlog → done}/dyad-analysis.md +6 -16
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/done/handle-time.md +7 -0
- data/user_stories/done/handle-time.rb +163 -0
- data/user_stories/done/instrument-architecture.md +238 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
- data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
- data/user_stories/done/move-staff-position-to-notation.md +141 -0
- data/user_stories/done/notation-module-foundation.md +102 -0
- data/user_stories/done/percussion_set.md +260 -0
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/visioning/agentic-daw.md +2 -0
- metadata +84 -18
- data/TODO.md +0 -109
- data/check_instrument_consistency.rb +0 -0
- data/test_translations.rb +0 -15
- data/user_stories/backlog/consonance-dissonance-classification.md +0 -57
- data/user_stories/backlog/pitch-set-classification.md +0 -62
- data/user_stories/backlog/sonority-identification.md +0 -47
- /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
- /data/user_stories/{backlog → done}/pitch-class-set-analysis.md +0 -0
|
@@ -1,86 +1,64 @@
|
|
|
1
|
-
require "head_music/rudiment/musical_symbol"
|
|
2
|
-
|
|
3
1
|
# A module for music rudiments
|
|
4
2
|
module HeadMusic::Rudiment; end
|
|
5
3
|
|
|
6
4
|
# An Alteration is a symbol that modifies pitch, such as a sharp, flat, or natural.
|
|
7
5
|
# In French, sharps and flats in the key signature are called "altérations".
|
|
8
|
-
class HeadMusic::Rudiment::Alteration
|
|
6
|
+
class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
9
7
|
include Comparable
|
|
8
|
+
include HeadMusic::Named
|
|
10
9
|
|
|
11
|
-
attr_reader :identifier, :
|
|
10
|
+
attr_reader :identifier, :semitones, :musical_symbols
|
|
12
11
|
|
|
13
12
|
delegate :ascii, :unicode, :html_entity, to: :musical_symbol
|
|
14
13
|
|
|
15
|
-
ALTERATION_RECORDS =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
symbols: [{ascii: "b", unicode: "♭", html_entity: "♭"}]
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
identifier: :natural, cents: 0,
|
|
26
|
-
symbols: [{ascii: "", unicode: "♮", html_entity: "♮"}]
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
identifier: :double_sharp, cents: 200,
|
|
30
|
-
symbols: [{ascii: "x", unicode: "𝄪", html_entity: "𝄪"}]
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
identifier: :double_flat, cents: -200,
|
|
34
|
-
symbols: [{ascii: "bb", unicode: "𝄫", html_entity: "𝄫"}]
|
|
35
|
-
}
|
|
36
|
-
].freeze
|
|
37
|
-
|
|
38
|
-
ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.map { |attributes| attributes[:identifier] }.freeze
|
|
14
|
+
ALTERATION_RECORDS =
|
|
15
|
+
YAML.load_file(File.expand_path("alterations.yml", __dir__), symbolize_names: true)[:alterations].freeze
|
|
16
|
+
|
|
17
|
+
ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.keys.freeze
|
|
18
|
+
SYMBOLS = ALTERATION_RECORDS.map { |key, attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.reject { |s| s.nil? || s.empty? }.freeze
|
|
19
|
+
PATTERN = Regexp.union(SYMBOLS)
|
|
20
|
+
MATCHER = PATTERN
|
|
39
21
|
|
|
40
22
|
def self.all
|
|
41
|
-
ALTERATION_RECORDS.map { |attributes| new(attributes) }
|
|
23
|
+
@all ||= ALTERATION_RECORDS.map { |key, attributes| new(key, attributes) }
|
|
42
24
|
end
|
|
43
25
|
|
|
44
26
|
def self.symbols
|
|
45
27
|
@symbols ||= all.map { |alteration| [alteration.ascii, alteration.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
|
|
46
28
|
end
|
|
47
29
|
|
|
48
|
-
def self.matcher
|
|
49
|
-
@matcher ||= Regexp.new symbols.join("|")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
30
|
def self.symbol?(candidate)
|
|
53
|
-
candidate
|
|
31
|
+
SYMBOLS.include?(candidate)
|
|
54
32
|
end
|
|
55
33
|
|
|
56
34
|
def self.get(identifier)
|
|
57
35
|
return identifier if identifier.is_a?(HeadMusic::Rudiment::Alteration)
|
|
58
36
|
|
|
59
37
|
all.detect do |alteration|
|
|
60
|
-
alteration.
|
|
38
|
+
alteration.representations.include?(identifier)
|
|
61
39
|
end
|
|
62
40
|
end
|
|
63
41
|
|
|
64
42
|
def self.by(key, value)
|
|
65
43
|
all.detect do |alteration|
|
|
66
|
-
alteration.send(key) == value if %i[
|
|
44
|
+
alteration.send(key) == value if %i[semitones].include?(key.to_sym)
|
|
67
45
|
end
|
|
68
46
|
end
|
|
69
47
|
|
|
70
|
-
def name
|
|
71
|
-
|
|
48
|
+
def self.get_by_name(name)
|
|
49
|
+
all.detect { |alteration| alteration.name == name.to_s }
|
|
72
50
|
end
|
|
73
51
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
.reject { |representation| representation.to_s.strip == "" }
|
|
52
|
+
def name(locale_code: I18n.locale)
|
|
53
|
+
super || identifier.to_s.tr("_", " ")
|
|
77
54
|
end
|
|
78
55
|
|
|
79
|
-
def
|
|
80
|
-
|
|
56
|
+
def representations
|
|
57
|
+
[identifier, identifier.to_s, name, ascii, unicode, html_entity]
|
|
58
|
+
.reject { |representation| representation.to_s.strip == "" }
|
|
81
59
|
end
|
|
82
60
|
|
|
83
|
-
|
|
61
|
+
ALTERATION_RECORDS.keys.each do |key|
|
|
84
62
|
define_method(:"#{key}?") { identifier == key }
|
|
85
63
|
end
|
|
86
64
|
|
|
@@ -90,7 +68,7 @@ class HeadMusic::Rudiment::Alteration
|
|
|
90
68
|
|
|
91
69
|
def <=>(other)
|
|
92
70
|
other = HeadMusic::Rudiment::Alteration.get(other)
|
|
93
|
-
|
|
71
|
+
semitones <=> other.semitones
|
|
94
72
|
end
|
|
95
73
|
|
|
96
74
|
def musical_symbol
|
|
@@ -99,15 +77,22 @@ class HeadMusic::Rudiment::Alteration
|
|
|
99
77
|
|
|
100
78
|
private
|
|
101
79
|
|
|
102
|
-
def initialize(attributes)
|
|
103
|
-
@identifier =
|
|
104
|
-
@
|
|
80
|
+
def initialize(key, attributes)
|
|
81
|
+
@identifier = key
|
|
82
|
+
@semitones = attributes[:semitones]
|
|
105
83
|
initialize_musical_symbols(attributes[:symbols])
|
|
84
|
+
initialize_localized_names
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def initialize_localized_names
|
|
88
|
+
# Initialize default English names
|
|
89
|
+
ensure_localized_name(name: identifier.to_s.tr("_", " "), locale_code: :en)
|
|
90
|
+
# Additional localized names will be loaded from locale files
|
|
106
91
|
end
|
|
107
92
|
|
|
108
93
|
def initialize_musical_symbols(list)
|
|
109
94
|
@musical_symbols = (list || []).map do |record|
|
|
110
|
-
HeadMusic::
|
|
95
|
+
HeadMusic::Notation::MusicalSymbol.new(
|
|
111
96
|
unicode: record[:unicode],
|
|
112
97
|
ascii: record[:ascii],
|
|
113
98
|
html_entity: record[:html_entity]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
alterations:
|
|
2
|
+
double_flat:
|
|
3
|
+
semitones: -2
|
|
4
|
+
symbols:
|
|
5
|
+
- ascii: "bb"
|
|
6
|
+
unicode: "𝄫"
|
|
7
|
+
html_entity: "𝄫"
|
|
8
|
+
flat:
|
|
9
|
+
semitones: -1
|
|
10
|
+
symbols:
|
|
11
|
+
- ascii: "b"
|
|
12
|
+
unicode: "♭"
|
|
13
|
+
html_entity: "♭"
|
|
14
|
+
natural:
|
|
15
|
+
cents: 0
|
|
16
|
+
semitones: 0
|
|
17
|
+
symbols:
|
|
18
|
+
- ascii: ""
|
|
19
|
+
unicode: "♮"
|
|
20
|
+
html_entity: "♮"
|
|
21
|
+
sharp:
|
|
22
|
+
semitones: 1
|
|
23
|
+
symbols:
|
|
24
|
+
- ascii: "#"
|
|
25
|
+
unicode: "♯"
|
|
26
|
+
html_entity: "♯"
|
|
27
|
+
double_sharp:
|
|
28
|
+
semitones: 2
|
|
29
|
+
symbols:
|
|
30
|
+
- ascii: "x"
|
|
31
|
+
unicode: "𝄪"
|
|
32
|
+
html_entity: "𝄪"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A chromatic interval is the distance between two pitches measured in half-steps.
|
|
5
|
-
class HeadMusic::Rudiment::ChromaticInterval
|
|
5
|
+
class HeadMusic::Rudiment::ChromaticInterval < HeadMusic::Rudiment::Base
|
|
6
6
|
include Comparable
|
|
7
7
|
include HeadMusic::Named
|
|
8
8
|
|
|
@@ -17,17 +17,14 @@ class HeadMusic::Rudiment::ChromaticInterval
|
|
|
17
17
|
|
|
18
18
|
def self.get(identifier)
|
|
19
19
|
@intervals ||= {}
|
|
20
|
-
candidate =
|
|
20
|
+
candidate = HeadMusic::Utilities::Case.to_snake_case(identifier)
|
|
21
21
|
semitones = NAMES.index(candidate) || identifier.to_i
|
|
22
22
|
@intervals[semitones] ||= new(semitones.to_i)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def initialize(identifier)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
semitones = NAMES.index(candidate) || identifier.to_i
|
|
29
|
-
end
|
|
30
|
-
@semitones = semitones || identifier.to_i
|
|
26
|
+
candidate = HeadMusic::Utilities::HashKey.for(identifier).to_s
|
|
27
|
+
@semitones = NAMES.index(candidate) || identifier.to_i
|
|
31
28
|
set_name
|
|
32
29
|
end
|
|
33
30
|
|
|
@@ -4,7 +4,7 @@ require "yaml"
|
|
|
4
4
|
module HeadMusic::Rudiment; end
|
|
5
5
|
|
|
6
6
|
# A clef assigns pitches to the lines and spaces of a staff.
|
|
7
|
-
class HeadMusic::Rudiment::Clef
|
|
7
|
+
class HeadMusic::Rudiment::Clef < HeadMusic::Rudiment::Base
|
|
8
8
|
include HeadMusic::Named
|
|
9
9
|
|
|
10
10
|
RECORDS = YAML.load_file(File.expand_path("clefs.yml", __dir__)).freeze
|
|
@@ -98,7 +98,7 @@ class HeadMusic::Rudiment::Clef
|
|
|
98
98
|
|
|
99
99
|
def initialize_musical_symbols(list)
|
|
100
100
|
@musical_symbols = (list || []).map do |symbol_data|
|
|
101
|
-
HeadMusic::
|
|
101
|
+
HeadMusic::Notation::MusicalSymbol.new(**symbol_data.slice(:ascii, :html_entity, :unicode))
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
# A module for music rudiments
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
|
-
# Consonance describes a category or degree of harmonic pleasantness
|
|
5
|
-
class HeadMusic::Rudiment::Consonance
|
|
6
|
-
|
|
4
|
+
# Consonance describes a category or degree of harmonic pleasantness
|
|
5
|
+
class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
|
|
6
|
+
PERFECT_CONSONANCE = :perfect_consonance
|
|
7
|
+
IMPERFECT_CONSONANCE = :imperfect_consonance
|
|
8
|
+
CONTEXTUAL = :contextual
|
|
9
|
+
MILD_DISSONANCE = :mild_dissonance
|
|
10
|
+
HARSH_DISSONANCE = :harsh_dissonance
|
|
11
|
+
DISSONANCE = :dissonance
|
|
12
|
+
|
|
13
|
+
LEVELS = [
|
|
14
|
+
PERFECT_CONSONANCE,
|
|
15
|
+
IMPERFECT_CONSONANCE,
|
|
16
|
+
CONTEXTUAL,
|
|
17
|
+
MILD_DISSONANCE,
|
|
18
|
+
HARSH_DISSONANCE,
|
|
19
|
+
DISSONANCE
|
|
20
|
+
].freeze
|
|
7
21
|
|
|
8
22
|
def self.get(name)
|
|
23
|
+
return name if name.is_a?(self)
|
|
24
|
+
return nil if name.nil?
|
|
25
|
+
|
|
9
26
|
@consonances ||= {}
|
|
10
|
-
|
|
27
|
+
name_sym = name.to_sym
|
|
28
|
+
@consonances[name_sym] ||= new(name) if LEVELS.include?(name_sym)
|
|
11
29
|
end
|
|
12
30
|
|
|
13
31
|
attr_reader :name
|
|
@@ -22,7 +40,23 @@ class HeadMusic::Rudiment::Consonance
|
|
|
22
40
|
to_s == other.to_s
|
|
23
41
|
end
|
|
24
42
|
|
|
43
|
+
# Check if this represents a consonance (perfect or imperfect)
|
|
44
|
+
def consonant?
|
|
45
|
+
[PERFECT_CONSONANCE, IMPERFECT_CONSONANCE].include?(name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if this represents any form of dissonance
|
|
49
|
+
def dissonant?
|
|
50
|
+
[MILD_DISSONANCE, HARSH_DISSONANCE, DISSONANCE].include?(name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Contextual is special - neither strictly consonant nor dissonant
|
|
54
|
+
def contextual?
|
|
55
|
+
name == CONTEXTUAL
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Predicate methods for each level
|
|
25
59
|
LEVELS.each do |method_name|
|
|
26
|
-
define_method(:"#{method_name}?") { to_s == method_name }
|
|
60
|
+
define_method(:"#{method_name}?") { to_s == method_name.to_s }
|
|
27
61
|
end
|
|
28
62
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Abstract class representing a diatonic tonal context (7-note scale system)
|
|
5
|
+
class HeadMusic::Rudiment::DiatonicContext < HeadMusic::Rudiment::TonalContext
|
|
6
|
+
def scale_type
|
|
7
|
+
raise NotImplementedError, "Subclasses must implement #scale_type"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def scale
|
|
11
|
+
@scale ||= HeadMusic::Rudiment::Scale.get(tonic_spelling, scale_type)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def key_signature
|
|
15
|
+
@key_signature ||= HeadMusic::Rudiment::KeySignature.from_scale(scale)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def relative
|
|
19
|
+
raise NotImplementedError, "Subclasses must implement #relative"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parallel
|
|
23
|
+
raise NotImplementedError, "Subclasses must implement #parallel"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -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,10 +1,9 @@
|
|
|
1
1
|
# A module for music rudiments
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
|
-
# Represents a key signature
|
|
5
|
-
class
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
8
7
|
ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
|
|
9
8
|
ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
|
|
10
9
|
|
|
@@ -16,14 +15,28 @@ class HeadMusic::Rudiment::KeySignature
|
|
|
16
15
|
return identifier if identifier.is_a?(HeadMusic::Rudiment::KeySignature)
|
|
17
16
|
|
|
18
17
|
@key_signatures ||= {}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
|
|
19
|
+
if identifier.is_a?(String)
|
|
20
|
+
tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
|
|
21
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
|
|
22
|
+
@key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
|
|
23
|
+
elsif identifier.is_a?(HeadMusic::Rudiment::DiatonicContext)
|
|
24
|
+
identifier.key_signature
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.from_scale(scale)
|
|
29
|
+
# Find a key or mode that uses this scale
|
|
30
|
+
tonic = scale.root_pitch.spelling
|
|
31
|
+
scale_type = scale.scale_type
|
|
32
|
+
new(tonic, scale_type)
|
|
22
33
|
end
|
|
23
34
|
|
|
35
|
+
attr_reader :tonic_spelling, :scale_type, :scale
|
|
36
|
+
|
|
24
37
|
delegate :pitch_class, to: :tonic_spelling, prefix: :tonic
|
|
25
|
-
delegate :to_s, to: :name
|
|
26
38
|
delegate :pitches, :pitch_classes, to: :scale
|
|
39
|
+
delegate :to_s, to: :name
|
|
27
40
|
|
|
28
41
|
def initialize(tonic_spelling, scale_type = nil)
|
|
29
42
|
@tonic_spelling = HeadMusic::Rudiment::Spelling.get(tonic_spelling)
|
|
@@ -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
|