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
|
@@ -4,8 +4,8 @@ module HeadMusic::Analysis; end
|
|
|
4
4
|
# A diatonic interval is the distance between two spelled pitches.
|
|
5
5
|
class HeadMusic::Analysis::DiatonicInterval
|
|
6
6
|
include Comparable
|
|
7
|
+
include HeadMusic::Named
|
|
7
8
|
|
|
8
|
-
# TODO: include Named module
|
|
9
9
|
NUMBER_NAMES = %w[
|
|
10
10
|
unison second third fourth fifth sixth seventh octave
|
|
11
11
|
ninth tenth eleventh twelfth thirteenth fourteenth fifteenth
|
|
@@ -43,7 +43,6 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
43
43
|
|
|
44
44
|
attr_reader :lower_pitch, :higher_pitch
|
|
45
45
|
|
|
46
|
-
delegate :to_s, to: :name
|
|
47
46
|
delegate :perfect?, :major?, :minor?, :diminished?, :augmented?, :doubly_diminished?, :doubly_augmented?, to: :quality
|
|
48
47
|
|
|
49
48
|
delegate :step?, :skip?, :leap?, :large_leap?, to: :category
|
|
@@ -52,18 +51,41 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
52
51
|
to: :size
|
|
53
52
|
)
|
|
54
53
|
delegate(
|
|
55
|
-
:simple_name, :quality_name, :simple_number_name, :number_name, :
|
|
54
|
+
:simple_name, :quality_name, :simple_number_name, :number_name, :shorthand,
|
|
56
55
|
to: :naming
|
|
57
56
|
)
|
|
58
57
|
|
|
59
58
|
alias_method :to_i, :semitones
|
|
60
59
|
|
|
60
|
+
# Override Named module method to try I18n and fall back to computed name
|
|
61
|
+
def name(locale_code: nil)
|
|
62
|
+
if locale_code
|
|
63
|
+
name_key = HeadMusic::Utilities::HashKey.for(naming.name)
|
|
64
|
+
if I18n.backend.translations[locale_code]
|
|
65
|
+
locale_data = I18n.backend.translations[locale_code][:head_music] || {}
|
|
66
|
+
return locale_data[:diatonic_intervals][name_key] if locale_data.dig(:diatonic_intervals, name_key)
|
|
67
|
+
return locale_data[:chromatic_intervals][name_key] if locale_data.dig(:chromatic_intervals, name_key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
naming.name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s
|
|
74
|
+
name
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
# Accepts a name and returns the interval with middle c on the bottom
|
|
62
78
|
def self.get(identifier)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if identifier.is_a?(String) || identifier.is_a?(Symbol)
|
|
80
|
+
name = Parser.new(identifier)
|
|
81
|
+
semitones = Semitones.new(name.degree_name.to_sym, name.quality_name).count
|
|
82
|
+
higher_pitch = HeadMusic::Rudiment::Pitch.from_number_and_letter(HeadMusic::Rudiment::Pitch.middle_c + semitones, name.higher_letter)
|
|
83
|
+
interval = new(HeadMusic::Rudiment::Pitch.middle_c, higher_pitch)
|
|
84
|
+
interval.ensure_localized_name(name: identifier.to_s)
|
|
85
|
+
interval
|
|
86
|
+
else
|
|
87
|
+
identifier
|
|
88
|
+
end
|
|
67
89
|
end
|
|
68
90
|
|
|
69
91
|
def initialize(first_pitch, second_pitch)
|
|
@@ -90,26 +112,39 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
90
112
|
alias_method :invert, :inversion
|
|
91
113
|
|
|
92
114
|
def consonance(style = :standard_practice)
|
|
93
|
-
|
|
94
|
-
consonance_for_major_and_minor ||
|
|
95
|
-
HeadMusic::Rudiment::Consonance.get(:dissonant)
|
|
115
|
+
consonance_analysis(style).consonance
|
|
96
116
|
end
|
|
97
117
|
|
|
98
118
|
def consonance?(style = :standard_practice)
|
|
99
|
-
consonance(style).
|
|
119
|
+
consonance(style).consonant?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def consonant?(style = :standard_practice)
|
|
123
|
+
consonance_analysis(style).consonant?
|
|
100
124
|
end
|
|
101
|
-
alias_method :consonant?, :consonance?
|
|
102
125
|
|
|
103
126
|
def perfect_consonance?(style = :standard_practice)
|
|
104
|
-
|
|
127
|
+
consonance_analysis(style).perfect_consonance?
|
|
105
128
|
end
|
|
106
129
|
|
|
107
130
|
def imperfect_consonance?(style = :standard_practice)
|
|
108
|
-
|
|
131
|
+
consonance_analysis(style).imperfect_consonance?
|
|
109
132
|
end
|
|
110
133
|
|
|
111
134
|
def dissonance?(style = :standard_practice)
|
|
112
|
-
|
|
135
|
+
consonance_analysis(style).dissonant?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def dissonant?(style = :standard_practice)
|
|
139
|
+
consonance_analysis(style).dissonant?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def consonance_analysis(style = :standard_practice)
|
|
143
|
+
HeadMusic::Analysis::IntervalConsonance.new(self, style)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def consonance_classification(style: :standard_practice)
|
|
147
|
+
consonance_analysis(style).classification
|
|
113
148
|
end
|
|
114
149
|
|
|
115
150
|
def above(pitch)
|
|
@@ -160,16 +195,4 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
160
195
|
def naming
|
|
161
196
|
@naming ||= Naming.new(number: number, semitones: semitones)
|
|
162
197
|
end
|
|
163
|
-
|
|
164
|
-
def consonance_for_perfect(style = :standard_practice)
|
|
165
|
-
HeadMusic::Rudiment::Consonance.get(dissonant_fourth?(style) ? :dissonant : :perfect) if perfect?
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def consonance_for_major_and_minor
|
|
169
|
-
HeadMusic::Rudiment::Consonance.get((third_or_compound? || sixth_or_compound?) ? :imperfect : :dissonant) if major? || minor?
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def dissonant_fourth?(style = :standard_practice)
|
|
173
|
-
fourth_or_compound? && style == :two_part_harmony
|
|
174
|
-
end
|
|
175
198
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Analysis class that combines an interval with a style tradition to determine consonance
|
|
2
|
+
class HeadMusic::Analysis::IntervalConsonance
|
|
3
|
+
attr_reader :interval, :style_tradition
|
|
4
|
+
|
|
5
|
+
def initialize(interval, style_tradition = HeadMusic::Style::ModernTradition.new)
|
|
6
|
+
@interval = interval
|
|
7
|
+
@style_tradition = style_tradition.is_a?(HeadMusic::Style::Tradition) ?
|
|
8
|
+
style_tradition :
|
|
9
|
+
HeadMusic::Style::Tradition.get(style_tradition)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def classification
|
|
13
|
+
@classification ||= style_tradition.consonance_classification(interval)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def consonance
|
|
17
|
+
@consonance ||= HeadMusic::Rudiment::Consonance.get(classification)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def consonant?
|
|
21
|
+
[HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE, HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE].include?(classification)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dissonant?
|
|
25
|
+
[HeadMusic::Rudiment::Consonance::MILD_DISSONANCE, HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE, HeadMusic::Rudiment::Consonance::DISSONANCE].include?(classification)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def contextual?
|
|
29
|
+
classification == HeadMusic::Rudiment::Consonance::CONTEXTUAL
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def perfect_consonance?
|
|
33
|
+
classification == HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def imperfect_consonance?
|
|
37
|
+
classification == HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mild_dissonance?
|
|
41
|
+
classification == HeadMusic::Rudiment::Consonance::MILD_DISSONANCE
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def harsh_dissonance?
|
|
45
|
+
classification == HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dissonance?
|
|
49
|
+
classification == HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -11,7 +11,7 @@ class HeadMusic::Content::Note
|
|
|
11
11
|
|
|
12
12
|
def initialize(pitch, rhythmic_value, voice = nil, position = nil)
|
|
13
13
|
@pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
14
|
-
@rhythmic_value = HeadMusic::
|
|
14
|
+
@rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value)
|
|
15
15
|
@voice = voice || HeadMusic::Content::Voice.new
|
|
16
16
|
@position = position || HeadMusic::Content::Position.new(@voice.composition, "1:1")
|
|
17
17
|
end
|
|
@@ -55,7 +55,7 @@ class HeadMusic::Content::Placement
|
|
|
55
55
|
def ensure_attributes(voice, position, rhythmic_value, pitch)
|
|
56
56
|
@voice = voice
|
|
57
57
|
ensure_position(position)
|
|
58
|
-
@rhythmic_value = HeadMusic::
|
|
58
|
+
@rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value)
|
|
59
59
|
@pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -53,7 +53,7 @@ class HeadMusic::Content::Position
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def +(other)
|
|
56
|
-
other = HeadMusic::
|
|
56
|
+
other = HeadMusic::Rudiment::RhythmicValue.new(other) if [HeadMusic::Rudiment::RhythmicUnit, Symbol, String].include?(other.class)
|
|
57
57
|
self.class.new(composition, bar_number, count, tick + other.ticks)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -8,7 +8,7 @@ class HeadMusic::Content::Staff
|
|
|
8
8
|
attr_reader :default_clef, :line_count, :instrument
|
|
9
9
|
|
|
10
10
|
def initialize(default_clef_key, instrument: nil, line_count: nil)
|
|
11
|
-
@instrument = HeadMusic::Instruments::
|
|
11
|
+
@instrument = HeadMusic::Instruments::InstrumentType.get(instrument) if instrument
|
|
12
12
|
begin
|
|
13
13
|
@default_clef = HeadMusic::Rudiment::Clef.get(default_clef_key)
|
|
14
14
|
rescue KeyError, NoMethodError
|
|
@@ -1,67 +1,74 @@
|
|
|
1
1
|
# Namespace for instrument definitions, categorization, and configuration
|
|
2
2
|
module HeadMusic::Instruments; end
|
|
3
3
|
|
|
4
|
-
# A musical instrument.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# Associations:
|
|
18
|
-
# family: the family of the instrument (e.g. "saxophone")
|
|
19
|
-
# orchestra_section: the section of the orchestra (e.g. "strings")
|
|
4
|
+
# A specific musical instrument instance with a selected variant.
|
|
5
|
+
# Represents an actual playable instrument with its transposition and configuration.
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# trumpet_in_c = HeadMusic::Instruments::Instrument.get("trumpet_in_c")
|
|
9
|
+
# trumpet_in_c = HeadMusic::Instruments::Instrument.get("trumpet", "in_c")
|
|
10
|
+
# clarinet = HeadMusic::Instruments::Instrument.get("clarinet") # uses default Bb variant
|
|
11
|
+
#
|
|
12
|
+
# Attributes accessible via delegation to instrument_type and variant:
|
|
13
|
+
# name: display name including variant (e.g. "Trumpet in C")
|
|
14
|
+
# transposition: sounding transposition in semitones
|
|
15
|
+
# clefs: array of clefs for this instrument
|
|
16
|
+
# pitch_designation: the pitch designation for transposing instruments
|
|
20
17
|
class HeadMusic::Instruments::Instrument
|
|
21
18
|
include HeadMusic::Named
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
attr_reader :instrument_type, :variant
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
# Factory method to get an Instrument instance
|
|
23
|
+
# @param type_or_name [String, Symbol] instrument type name or full name with variant
|
|
24
|
+
# @param variant_key [String, Symbol, nil] optional variant key if not included in name
|
|
25
|
+
# @return [Instrument] instrument instance with specified or default variant
|
|
26
|
+
def self.get(type_or_name, variant_key = nil)
|
|
27
|
+
return type_or_name if type_or_name.is_a?(self)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@all ||=
|
|
32
|
-
INSTRUMENTS.map { |key, _data| get(key) }.sort_by { |instrument| instrument.name.downcase }
|
|
33
|
-
end
|
|
29
|
+
type_name, parsed_variant_key = parse_instrument_name(type_or_name)
|
|
30
|
+
variant_key ||= parsed_variant_key
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
:family_key, :orchestra_section_key,
|
|
38
|
-
:variants, :classification_keys
|
|
39
|
-
)
|
|
32
|
+
instrument_type = HeadMusic::Instruments::InstrumentType.get(type_name)
|
|
33
|
+
return nil unless instrument_type&.name_key
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
variant = find_variant(instrument_type, variant_key)
|
|
36
|
+
new(instrument_type, variant)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(instrument_type, variant)
|
|
40
|
+
@instrument_type = instrument_type
|
|
41
|
+
@variant = variant
|
|
42
|
+
initialize_name
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# Delegations to instrument_type
|
|
46
|
+
delegate :name_key, :family_key, :family, :orchestra_section_key, :classification_keys,
|
|
47
|
+
:alias_name_keys, :variants, :translation, to: :instrument_type
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
# Delegations to variant
|
|
50
|
+
delegate :pitch_designation, :staff_schemes, :default_staff_scheme, to: :variant
|
|
51
|
+
|
|
52
|
+
def default_staves
|
|
53
|
+
default_staff_scheme&.staves || []
|
|
49
54
|
end
|
|
50
55
|
|
|
51
|
-
def
|
|
52
|
-
|
|
56
|
+
def default_clefs
|
|
57
|
+
default_staves&.map(&:clef) || []
|
|
58
|
+
end
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
def sounding_transposition
|
|
61
|
+
default_staves&.first&.sounding_transposition || 0
|
|
55
62
|
end
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
alias_method :default_sounding_transposition, :sounding_transposition
|
|
65
|
+
|
|
58
66
|
def transposing?
|
|
59
|
-
|
|
67
|
+
sounding_transposition != 0
|
|
60
68
|
end
|
|
61
69
|
|
|
62
|
-
# Returns true if the instrument sounds at a different register than written.
|
|
63
70
|
def transposing_at_the_octave?
|
|
64
|
-
transposing? &&
|
|
71
|
+
transposing? && sounding_transposition % 12 == 0
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def single_staff?
|
|
@@ -78,94 +85,77 @@ class HeadMusic::Instruments::Instrument
|
|
|
78
85
|
default_clefs.any?
|
|
79
86
|
end
|
|
80
87
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
delegate :default_staff_scheme, to: :default_variant
|
|
88
|
+
def ==(other)
|
|
89
|
+
return false unless other.is_a?(self.class)
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
default_staff_scheme&.staves || []
|
|
91
|
+
instrument_type == other.instrument_type && variant == other.variant
|
|
89
92
|
end
|
|
90
93
|
|
|
91
|
-
def
|
|
92
|
-
|
|
94
|
+
def to_s
|
|
95
|
+
name
|
|
93
96
|
end
|
|
94
97
|
|
|
95
|
-
def default_sounding_transposition
|
|
96
|
-
default_staves&.first&.sounding_transposition || 0
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private_class_method :new
|
|
100
|
-
|
|
101
98
|
private
|
|
102
99
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
def initialize_name
|
|
101
|
+
if variant.default? || !pitch_designation
|
|
102
|
+
self.name = instrument_type.name
|
|
103
|
+
elsif pitch_designation
|
|
104
|
+
pitch_name = format_pitch_name(pitch_designation)
|
|
105
|
+
self.name = "#{instrument_type.name} in #{pitch_name}"
|
|
107
106
|
else
|
|
108
|
-
|
|
107
|
+
variant_name = variant.key.to_s.tr("_", " ")
|
|
108
|
+
self.name = "#{instrument_type.name} (#{variant_name})"
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
def format_pitch_name(pitch_designation)
|
|
113
|
+
# Format the pitch designation for display
|
|
114
|
+
# e.g. "Bb" -> "B♭", "C" -> "C", "Eb" -> "E♭"
|
|
115
|
+
pitch_designation.to_s.gsub("b", "♭").gsub("#", "♯")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.parse_instrument_name(name)
|
|
119
|
+
name_str = name.to_s
|
|
120
|
+
|
|
121
|
+
# Check for variant patterns like "trumpet_in_e_flat"
|
|
122
|
+
if name_str =~ /(.+)_in_([a-g])_(flat|sharp)$/i
|
|
123
|
+
type_name = Regexp.last_match(1)
|
|
124
|
+
note = Regexp.last_match(2).downcase
|
|
125
|
+
accidental = Regexp.last_match(3)
|
|
126
|
+
variant_key = :"in_#{note}_#{accidental}"
|
|
127
|
+
[type_name, variant_key]
|
|
128
|
+
# Check for variant patterns like "trumpet_in_c" or "clarinet_in_a" or "trumpet_in_eb"
|
|
129
|
+
elsif name_str =~ /(.+)_in_([a-g][b#]?)$/i
|
|
130
|
+
type_name = Regexp.last_match(1)
|
|
131
|
+
variant_note = Regexp.last_match(2).downcase
|
|
132
|
+
# Convert "eb" to "e_flat", "bb" to "b_flat", etc.
|
|
133
|
+
if variant_note.end_with?("b") && variant_note.length == 2
|
|
134
|
+
note_letter = variant_note[0]
|
|
135
|
+
variant_key = :"in_#{note_letter}_flat"
|
|
136
|
+
elsif variant_note.end_with?("#") && variant_note.length == 2
|
|
137
|
+
note_letter = variant_note[0]
|
|
138
|
+
variant_key = :"in_#{note_letter}_sharp"
|
|
139
|
+
else
|
|
140
|
+
variant_key = :"in_#{variant_note}"
|
|
122
141
|
end
|
|
142
|
+
[type_name, variant_key]
|
|
143
|
+
else
|
|
144
|
+
[name_str, nil]
|
|
123
145
|
end
|
|
124
|
-
nil
|
|
125
146
|
end
|
|
126
147
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
return data.merge!("name_key" => name_key) if name_key.to_s == key.to_s
|
|
130
|
-
end
|
|
131
|
-
nil
|
|
132
|
-
end
|
|
148
|
+
def self.find_variant(instrument_type, variant_key)
|
|
149
|
+
return instrument_type.default_variant unless variant_key
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
inherit_family_attributes(record)
|
|
137
|
-
initialize_names(record)
|
|
138
|
-
initialize_attributes(record)
|
|
139
|
-
end
|
|
151
|
+
# Convert to symbol for comparison
|
|
152
|
+
variant_sym = variant_key.to_sym
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
# Find the variant by key
|
|
155
|
+
variants = instrument_type.variants || []
|
|
156
|
+
variant = variants.find { |v| v.key == variant_sym }
|
|
157
|
+
variant || instrument_type.default_variant
|
|
144
158
|
end
|
|
145
159
|
|
|
146
|
-
|
|
147
|
-
return unless family
|
|
148
|
-
|
|
149
|
-
@orchestra_section_key = family.orchestra_section_key
|
|
150
|
-
@classification_keys = family.classification_keys || []
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def initialize_names(record)
|
|
154
|
-
@name_key = record["name_key"].to_sym
|
|
155
|
-
self.name = I18n.translate(name_key, scope: "head_music.instruments", locale: "en", default: inferred_name)
|
|
156
|
-
@alias_name_keys = record["alias_name_keys"] || []
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def initialize_attributes(record)
|
|
160
|
-
@orchestra_section_key ||= record["orchestra_section_key"]
|
|
161
|
-
@classification_keys = [@classification_keys, record["classification_keys"]].flatten.compact.uniq
|
|
162
|
-
@variants =
|
|
163
|
-
(record["variants"] || {}).map do |key, attributes|
|
|
164
|
-
HeadMusic::Instruments::Variant.new(key, attributes)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def inferred_name
|
|
169
|
-
name_key.to_s.tr("_", " ")
|
|
170
|
-
end
|
|
160
|
+
private_class_method :parse_instrument_name, :find_variant
|
|
171
161
|
end
|
|
@@ -28,6 +28,15 @@ bassoon:
|
|
|
28
28
|
- wind
|
|
29
29
|
- woodwind
|
|
30
30
|
orchestra_section_key: woodwind
|
|
31
|
+
celesta:
|
|
32
|
+
classification_keys:
|
|
33
|
+
- idiophone
|
|
34
|
+
- keyboard
|
|
35
|
+
- percussion
|
|
36
|
+
- struck
|
|
37
|
+
- metal
|
|
38
|
+
- pitched
|
|
39
|
+
orchestra_section_key: keyboard
|
|
31
40
|
clarinet:
|
|
32
41
|
classification_keys:
|
|
33
42
|
- aerophone
|
|
@@ -42,15 +51,6 @@ clavichord:
|
|
|
42
51
|
- keyboard
|
|
43
52
|
- string
|
|
44
53
|
orchestra_section_key: keyboard
|
|
45
|
-
celesta:
|
|
46
|
-
classification_keys:
|
|
47
|
-
- idiophone
|
|
48
|
-
- keyboard
|
|
49
|
-
- percussion
|
|
50
|
-
- struck
|
|
51
|
-
- metal
|
|
52
|
-
- pitched
|
|
53
|
-
orchestra_section_key: percussion
|
|
54
54
|
cornet:
|
|
55
55
|
classification_keys:
|
|
56
56
|
- aerophone
|
|
@@ -239,6 +239,7 @@ tambourine:
|
|
|
239
239
|
classification_keys:
|
|
240
240
|
- percussion
|
|
241
241
|
- membranophone
|
|
242
|
+
- shaken
|
|
242
243
|
- idiophone
|
|
243
244
|
- unpitched
|
|
244
245
|
orchestra_section_key: percussion
|
|
@@ -3,8 +3,19 @@ module HeadMusic::Instruments; end
|
|
|
3
3
|
|
|
4
4
|
# An *InstrumentFamily* is a species of instrument
|
|
5
5
|
# that may exist in a variety of keys or other variations.
|
|
6
|
-
# For example
|
|
7
|
-
#
|
|
6
|
+
# For example:
|
|
7
|
+
# - _saxophone_ is an instrument family, while
|
|
8
|
+
# _alto saxophone_ and _baritone saxophone_ are specific instruments.
|
|
9
|
+
# - _oboe_ is an instrument family, while
|
|
10
|
+
# _oboe d'amore_ and _English horn_ are specific instruments.
|
|
11
|
+
#
|
|
12
|
+
# Instrument families are categorized by:
|
|
13
|
+
# - orchestra section (e.g. woodwind, brass, percussion, strings)
|
|
14
|
+
# - classification (e.g. bowed string, plucked string, double reed, single reed, brass, keyboard, electronic, percussion)
|
|
15
|
+
#
|
|
16
|
+
# Instrument families are defined in `lib/head_music/instruments/instrument_families.yml`.
|
|
17
|
+
#
|
|
18
|
+
# @see HeadMusic::Instruments::InstrumentType
|
|
8
19
|
class HeadMusic::Instruments::InstrumentFamily
|
|
9
20
|
include HeadMusic::Named
|
|
10
21
|
|