head_music 8.3.0 → 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/CHANGELOG.md +53 -0
- data/CLAUDE.md +32 -15
- data/Gemfile.lock +1 -1
- data/MUSIC_THEORY.md +120 -0
- data/lib/head_music/analysis/diatonic_interval.rb +29 -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_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- 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 +6 -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 +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 +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.rb +1 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- 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 +31 -10
- 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/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/{backlog → todo}/dyad-analysis.md +2 -10
- 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/{backlog → todo}/pitch-class-set-analysis.md +40 -0
- data/user_stories/{backlog → todo}/pitch-set-classification.md +10 -0
- data/user_stories/{backlog → todo}/sonority-identification.md +20 -0
- metadata +43 -12
- data/TODO.md +0 -109
- /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 → todo}/consonance-dissonance-classification.md +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
orchestral:
|
|
3
|
+
name: "Orchestral"
|
|
4
|
+
sections:
|
|
5
|
+
- section_key: woodwind
|
|
6
|
+
instruments:
|
|
7
|
+
- piccolo_flute
|
|
8
|
+
- flute
|
|
9
|
+
- alto_flute
|
|
10
|
+
- alto_recorder
|
|
11
|
+
- soprano_recorder
|
|
12
|
+
- tenor_recorder
|
|
13
|
+
- oboe
|
|
14
|
+
- english_horn
|
|
15
|
+
- clarinet
|
|
16
|
+
- alto_clarinet
|
|
17
|
+
- bass_clarinet
|
|
18
|
+
- soprano_saxophone
|
|
19
|
+
- alto_saxophone
|
|
20
|
+
- tenor_saxophone
|
|
21
|
+
- baritone_saxophone
|
|
22
|
+
- bassoon
|
|
23
|
+
- contrabassoon
|
|
24
|
+
- section_key: brass
|
|
25
|
+
instruments:
|
|
26
|
+
- french_horn
|
|
27
|
+
- trumpet
|
|
28
|
+
- cornet
|
|
29
|
+
- trombone
|
|
30
|
+
- bass_trombone
|
|
31
|
+
- tuba
|
|
32
|
+
- section_key: percussion
|
|
33
|
+
instruments:
|
|
34
|
+
- timpani
|
|
35
|
+
- snare_drum
|
|
36
|
+
- bass_drum
|
|
37
|
+
- cymbal
|
|
38
|
+
- gong
|
|
39
|
+
- xylophone
|
|
40
|
+
- glockenspiel
|
|
41
|
+
- marimba
|
|
42
|
+
- vibraphone
|
|
43
|
+
- percussion
|
|
44
|
+
- section_key: keyboard
|
|
45
|
+
instruments:
|
|
46
|
+
- harp
|
|
47
|
+
- piano
|
|
48
|
+
- celesta
|
|
49
|
+
- harpsichord
|
|
50
|
+
- organ
|
|
51
|
+
- section_key: voice
|
|
52
|
+
instruments:
|
|
53
|
+
- soprano_voice
|
|
54
|
+
- alto_voice
|
|
55
|
+
- tenor_voice
|
|
56
|
+
- bass_voice
|
|
57
|
+
- section_key: string
|
|
58
|
+
instruments:
|
|
59
|
+
- violin
|
|
60
|
+
- viola
|
|
61
|
+
- cello
|
|
62
|
+
- double_bass
|
|
63
|
+
|
|
64
|
+
band:
|
|
65
|
+
name: "Concert Band"
|
|
66
|
+
sections:
|
|
67
|
+
- section_key: woodwind
|
|
68
|
+
instruments:
|
|
69
|
+
- piccolo
|
|
70
|
+
- flute
|
|
71
|
+
- oboe
|
|
72
|
+
- english_horn
|
|
73
|
+
- bassoon
|
|
74
|
+
- contrabassoon
|
|
75
|
+
- clarinet
|
|
76
|
+
- alto_clarinet
|
|
77
|
+
- bass_clarinet
|
|
78
|
+
- soprano_saxophone
|
|
79
|
+
- alto_saxophone
|
|
80
|
+
- tenor_saxophone
|
|
81
|
+
- baritone_saxophone
|
|
82
|
+
- section_key: brass
|
|
83
|
+
instruments:
|
|
84
|
+
- cornet
|
|
85
|
+
- trumpet
|
|
86
|
+
- french_horn
|
|
87
|
+
- trombone
|
|
88
|
+
- bass_trombone
|
|
89
|
+
- euphonium
|
|
90
|
+
- baritone_horn
|
|
91
|
+
- tuba
|
|
92
|
+
- section_key: percussion
|
|
93
|
+
instruments:
|
|
94
|
+
- timpani
|
|
95
|
+
- snare_drum
|
|
96
|
+
- bass_drum
|
|
97
|
+
- cymbal
|
|
98
|
+
- percussion
|
|
99
|
+
|
|
100
|
+
brass_quintet:
|
|
101
|
+
name: "Brass Quintet"
|
|
102
|
+
sections:
|
|
103
|
+
- section_key: brass
|
|
104
|
+
instruments:
|
|
105
|
+
- trumpet # First trumpet
|
|
106
|
+
- trumpet # Second trumpet
|
|
107
|
+
- french_horn
|
|
108
|
+
- trombone
|
|
109
|
+
- tuba
|
|
110
|
+
|
|
111
|
+
woodwind_quintet:
|
|
112
|
+
name: "Woodwind Quintet"
|
|
113
|
+
sections:
|
|
114
|
+
- section_key: mixed
|
|
115
|
+
instruments:
|
|
116
|
+
- flute
|
|
117
|
+
- oboe
|
|
118
|
+
- clarinet
|
|
119
|
+
- french_horn # Horn is traditional in woodwind quintets
|
|
120
|
+
- bassoon
|
|
121
|
+
|
|
122
|
+
string_quartet:
|
|
123
|
+
name: "String Quartet"
|
|
124
|
+
sections:
|
|
125
|
+
- section_key: string
|
|
126
|
+
instruments:
|
|
127
|
+
- violin # First violin
|
|
128
|
+
- violin # Second violin
|
|
129
|
+
- viola
|
|
130
|
+
- cello
|
|
@@ -35,4 +35,10 @@ class HeadMusic::Instruments::Variant
|
|
|
35
35
|
@default_staff_scheme ||=
|
|
36
36
|
staff_schemes.find(&:default?) || staff_schemes.first
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
def ==(other)
|
|
40
|
+
return false unless other.is_a?(self.class)
|
|
41
|
+
|
|
42
|
+
key == other.key && attributes == other.attributes
|
|
43
|
+
end
|
|
38
44
|
end
|
|
@@ -5,8 +5,9 @@ module HeadMusic::Rudiment; end
|
|
|
5
5
|
|
|
6
6
|
# An Alteration is a symbol that modifies pitch, such as a sharp, flat, or natural.
|
|
7
7
|
# In French, sharps and flats in the key signature are called "altérations".
|
|
8
|
-
class HeadMusic::Rudiment::Alteration
|
|
8
|
+
class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
9
9
|
include Comparable
|
|
10
|
+
include HeadMusic::Named
|
|
10
11
|
|
|
11
12
|
attr_reader :identifier, :cents, :musical_symbols
|
|
12
13
|
|
|
@@ -36,6 +37,9 @@ class HeadMusic::Rudiment::Alteration
|
|
|
36
37
|
].freeze
|
|
37
38
|
|
|
38
39
|
ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.map { |attributes| attributes[:identifier] }.freeze
|
|
40
|
+
SYMBOLS = ALTERATION_RECORDS.map { |attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.freeze
|
|
41
|
+
PATTERN = Regexp.union(SYMBOLS.reject { |s| s.nil? || s.empty? })
|
|
42
|
+
MATCHER = PATTERN
|
|
39
43
|
|
|
40
44
|
def self.all
|
|
41
45
|
ALTERATION_RECORDS.map { |attributes| new(attributes) }
|
|
@@ -45,12 +49,8 @@ class HeadMusic::Rudiment::Alteration
|
|
|
45
49
|
@symbols ||= all.map { |alteration| [alteration.ascii, alteration.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
def self.matcher
|
|
49
|
-
@matcher ||= Regexp.new symbols.join("|")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
52
|
def self.symbol?(candidate)
|
|
53
|
-
candidate
|
|
53
|
+
SYMBOLS.include?(candidate)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def self.get(identifier)
|
|
@@ -67,8 +67,16 @@ class HeadMusic::Rudiment::Alteration
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def name
|
|
71
|
-
|
|
70
|
+
def self.get_by_name(name)
|
|
71
|
+
all.detect { |alteration| alteration.name == name.to_s }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.from_pitched_item(input)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def name(locale_code: I18n.locale)
|
|
79
|
+
super || identifier.to_s.tr("_", " ")
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
def representions
|
|
@@ -103,6 +111,13 @@ class HeadMusic::Rudiment::Alteration
|
|
|
103
111
|
@identifier = attributes[:identifier]
|
|
104
112
|
@cents = attributes[:cents]
|
|
105
113
|
initialize_musical_symbols(attributes[:symbols])
|
|
114
|
+
initialize_localized_names
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def initialize_localized_names
|
|
118
|
+
# Initialize default English names
|
|
119
|
+
ensure_localized_name(name: identifier.to_s.tr("_", " "), locale_code: :en)
|
|
120
|
+
# Additional localized names will be loaded from locale files
|
|
106
121
|
end
|
|
107
122
|
|
|
108
123
|
def initialize_musical_symbols(list)
|
|
@@ -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
|
|
|
@@ -23,11 +23,8 @@ class HeadMusic::Rudiment::ChromaticInterval
|
|
|
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
|
|
@@ -1,13 +1,30 @@
|
|
|
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
|
+
# Detailed categories aligned with music theory
|
|
7
|
+
LEVELS = %w[
|
|
8
|
+
perfect_consonance
|
|
9
|
+
imperfect_consonance
|
|
10
|
+
contextual
|
|
11
|
+
mild_dissonance
|
|
12
|
+
harsh_dissonance
|
|
13
|
+
dissonance
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# Constants for each consonance level
|
|
17
|
+
PERFECT_CONSONANCE = :perfect_consonance
|
|
18
|
+
IMPERFECT_CONSONANCE = :imperfect_consonance
|
|
19
|
+
CONTEXTUAL = :contextual
|
|
20
|
+
MILD_DISSONANCE = :mild_dissonance
|
|
21
|
+
HARSH_DISSONANCE = :harsh_dissonance
|
|
22
|
+
DISSONANCE = :dissonance
|
|
7
23
|
|
|
8
24
|
def self.get(name)
|
|
9
25
|
@consonances ||= {}
|
|
10
|
-
|
|
26
|
+
name_sym = name.to_sym
|
|
27
|
+
@consonances[name_sym] ||= new(name) if LEVELS.include?(name.to_s)
|
|
11
28
|
end
|
|
12
29
|
|
|
13
30
|
attr_reader :name
|
|
@@ -22,6 +39,22 @@ class HeadMusic::Rudiment::Consonance
|
|
|
22
39
|
to_s == other.to_s
|
|
23
40
|
end
|
|
24
41
|
|
|
42
|
+
# Check if this represents a consonance (perfect or imperfect)
|
|
43
|
+
def consonant?
|
|
44
|
+
[PERFECT_CONSONANCE, IMPERFECT_CONSONANCE].include?(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if this represents any form of dissonance
|
|
48
|
+
def dissonant?
|
|
49
|
+
[MILD_DISSONANCE, HARSH_DISSONANCE, DISSONANCE].include?(name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Contextual is special - neither strictly consonant nor dissonant
|
|
53
|
+
def contextual?
|
|
54
|
+
name == CONTEXTUAL
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Predicate methods for each level
|
|
25
58
|
LEVELS.each do |method_name|
|
|
26
59
|
define_method(:"#{method_name}?") { to_s == method_name }
|
|
27
60
|
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,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
|