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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module HeadMusic::Instruments; end
|
|
2
|
+
|
|
3
|
+
class HeadMusic::Instruments::ScoreOrder
|
|
4
|
+
include HeadMusic::Named
|
|
5
|
+
|
|
6
|
+
SCORE_ORDERS = YAML.load_file(File.expand_path("score_orders.yml", __dir__)).freeze
|
|
7
|
+
|
|
8
|
+
DEFAULT_ENSEMBLE_TYPE_KEY = :orchestral
|
|
9
|
+
|
|
10
|
+
attr_reader :ensemble_type_key, :sections
|
|
11
|
+
|
|
12
|
+
# Factory method to get a ScoreOrder instance for a specific ensemble type
|
|
13
|
+
def self.get(ensemble_type)
|
|
14
|
+
@instances ||= {}
|
|
15
|
+
key = HeadMusic::Utilities::HashKey.for(ensemble_type)
|
|
16
|
+
return unless SCORE_ORDERS.key?(key.to_s)
|
|
17
|
+
|
|
18
|
+
@instances[key] ||= new(key)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convenience method to order instruments in orchestral order
|
|
22
|
+
def self.in_orchestral_order(instruments)
|
|
23
|
+
get(:orchestral).order(instruments)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Convenience method to order instruments in concert band order
|
|
27
|
+
def self.in_band_order(instruments)
|
|
28
|
+
get(:band).order(instruments)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Accepts a list of instruments and orders them according to this ensemble type's conventions
|
|
32
|
+
def order(instruments)
|
|
33
|
+
valid_inputs = instruments.compact.reject { |i| i.respond_to?(:empty?) && i.empty? }
|
|
34
|
+
instrument_objects = valid_inputs.map { |i| normalize_to_instrument(i) }.compact
|
|
35
|
+
|
|
36
|
+
# Build ordering index
|
|
37
|
+
ordering_index = build_ordering_index
|
|
38
|
+
|
|
39
|
+
# Separate known and unknown instruments
|
|
40
|
+
known_instruments = []
|
|
41
|
+
unknown_instruments = []
|
|
42
|
+
|
|
43
|
+
instrument_objects.each do |instrument|
|
|
44
|
+
position_info = find_position_with_transposition(instrument, ordering_index)
|
|
45
|
+
if position_info
|
|
46
|
+
known_instruments << [instrument, position_info]
|
|
47
|
+
else
|
|
48
|
+
unknown_instruments << instrument
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Sort known instruments by position (primary) and transposition (secondary)
|
|
53
|
+
sorted_known = known_instruments.sort_by { |_, pos_info|
|
|
54
|
+
[pos_info[:position], -pos_info[:transposition]]
|
|
55
|
+
}.map(&:first)
|
|
56
|
+
sorted_known + unknown_instruments.sort_by(&:to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private_class_method :new
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def initialize(ensemble_type_key = DEFAULT_ENSEMBLE_TYPE_KEY)
|
|
64
|
+
@ensemble_type_key = ensemble_type_key.to_sym
|
|
65
|
+
data = SCORE_ORDERS[ensemble_type_key.to_s]
|
|
66
|
+
|
|
67
|
+
@sections = data["sections"] || []
|
|
68
|
+
self.name = data["name"] || ensemble_type_key.to_s.tr("_", " ").capitalize
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_to_instrument(input)
|
|
72
|
+
# Return if already an Instrument instance
|
|
73
|
+
return input if input.is_a?(HeadMusic::Instruments::Instrument)
|
|
74
|
+
|
|
75
|
+
# Return other objects that respond to required methods (mock objects, etc.)
|
|
76
|
+
return input if input.respond_to?(:name_key) && input.respond_to?(:family_key)
|
|
77
|
+
|
|
78
|
+
# Create an Instrument instance for string inputs
|
|
79
|
+
HeadMusic::Instruments::Instrument.get(input)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Builds an index mapping instrument names to their position in the order
|
|
83
|
+
def build_ordering_index
|
|
84
|
+
index = {}
|
|
85
|
+
position = 0
|
|
86
|
+
|
|
87
|
+
sections.each do |section|
|
|
88
|
+
instruments = section["instruments"] || []
|
|
89
|
+
instruments.each do |instrument_key|
|
|
90
|
+
# Store position for this instrument key
|
|
91
|
+
index[instrument_key.to_s] = position
|
|
92
|
+
position += 1
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
index
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Finds the position of an instrument in the ordering
|
|
100
|
+
def find_position(instrument, ordering_index)
|
|
101
|
+
# Try exact match with name_key
|
|
102
|
+
return ordering_index[instrument.name_key.to_s] if instrument.name_key && ordering_index.key?(instrument.name_key.to_s)
|
|
103
|
+
|
|
104
|
+
# Try matching by family + range category (e.g., alto_saxophone -> saxophone family)
|
|
105
|
+
if instrument.family_key
|
|
106
|
+
family_base = instrument.family_key.to_s
|
|
107
|
+
instrument_key = instrument.name_key.to_s
|
|
108
|
+
|
|
109
|
+
# Check if this is a variant of a family (e.g., alto_saxophone)
|
|
110
|
+
if instrument_key.include?(family_base)
|
|
111
|
+
# Look for the specific variant first
|
|
112
|
+
return ordering_index[instrument_key] if ordering_index.key?(instrument_key)
|
|
113
|
+
|
|
114
|
+
# Fall back to generic family instrument if listed
|
|
115
|
+
return ordering_index[family_base] if ordering_index.key?(family_base)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Try normalized name (lowercase, underscored)
|
|
120
|
+
normalized = HeadMusic::Utilities::Case.to_snake_case(instrument.name)
|
|
121
|
+
return ordering_index[normalized] if ordering_index.key?(normalized)
|
|
122
|
+
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Finds the position and transposition information for an instrument
|
|
127
|
+
def find_position_with_transposition(instrument, ordering_index)
|
|
128
|
+
position = find_position(instrument, ordering_index)
|
|
129
|
+
return nil unless position
|
|
130
|
+
|
|
131
|
+
# Get the sounding transposition for secondary sorting
|
|
132
|
+
transposition = instrument.default_sounding_transposition || 0
|
|
133
|
+
|
|
134
|
+
{position: position, transposition: transposition}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -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
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Namespace for instrument definitions, categorization, and configuration
|
|
2
1
|
module HeadMusic::Instruments; end
|
|
3
2
|
|
|
4
3
|
class HeadMusic::Instruments::Staff
|
|
@@ -30,4 +29,65 @@ class HeadMusic::Instruments::Staff
|
|
|
30
29
|
def name
|
|
31
30
|
name_key.to_s.tr("_", " ")
|
|
32
31
|
end
|
|
32
|
+
|
|
33
|
+
# Get all staff mappings for composite instruments
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<Notation::StaffMapping>] array of staff mappings
|
|
36
|
+
# @example
|
|
37
|
+
# drum_kit_staff.mappings #=> [#<Notation::StaffMapping...>, #<Notation::StaffMapping...>]
|
|
38
|
+
def mappings
|
|
39
|
+
@mappings ||= parse_mappings
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Find the staff mapping at a specific position
|
|
43
|
+
#
|
|
44
|
+
# @param position_index [Integer] the staff position index
|
|
45
|
+
# @return [Notation::StaffMapping, nil] the mapping at that position or nil
|
|
46
|
+
# @example
|
|
47
|
+
# staff.mapping_for_position(4) #=> #<Notation::StaffMapping instrument: snare_drum...>
|
|
48
|
+
def mapping_for_position(position_index)
|
|
49
|
+
mappings.find { |mapping| mapping.position_index == position_index }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the instrument at a specific staff position
|
|
53
|
+
#
|
|
54
|
+
# @param position_index [Integer] the staff position index
|
|
55
|
+
# @return [Instrument, nil] the instrument at that position or nil
|
|
56
|
+
# @example
|
|
57
|
+
# staff.instrument_for_position(4) #=> #<Instrument name: "snare drum">
|
|
58
|
+
def instrument_for_position(position_index)
|
|
59
|
+
mapping = mapping_for_position(position_index)
|
|
60
|
+
mapping&.instrument
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get all staff positions for a given instrument
|
|
64
|
+
#
|
|
65
|
+
# This is useful for instruments that appear at multiple positions
|
|
66
|
+
# (e.g., hi-hat with stick and pedal techniques)
|
|
67
|
+
#
|
|
68
|
+
# @param instrument_key [String, Symbol] the instrument key
|
|
69
|
+
# @return [Array<Integer>] array of position indices
|
|
70
|
+
# @example
|
|
71
|
+
# staff.positions_for_instrument("hi_hat") #=> [-1, 9]
|
|
72
|
+
def positions_for_instrument(instrument_key)
|
|
73
|
+
mappings.select { |mapping| mapping.instrument_key.to_s == instrument_key.to_s }
|
|
74
|
+
.map(&:position_index)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get all unique instruments used in this staff's mappings
|
|
78
|
+
#
|
|
79
|
+
# @return [Array<Instrument>] array of unique instruments
|
|
80
|
+
# @example
|
|
81
|
+
# drum_kit_staff.components #=> [#<Instrument: bass_drum>, #<Instrument: snare_drum>, ...]
|
|
82
|
+
def components
|
|
83
|
+
mappings.map(&:instrument).compact.uniq
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def parse_mappings
|
|
89
|
+
mappings_data = attributes["mappings"] || []
|
|
90
|
+
|
|
91
|
+
mappings_data.map { |mapping_attrs| HeadMusic::Notation::StaffMapping.new(mapping_attrs) }
|
|
92
|
+
end
|
|
33
93
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
require_relative "staff"
|
|
2
2
|
|
|
3
|
-
# Namespace for instrument definitions, categorization, and configuration
|
|
4
3
|
module HeadMusic::Instruments; end
|
|
5
4
|
|
|
6
5
|
class HeadMusic::Instruments::StaffScheme
|
|
7
|
-
attr_reader :
|
|
6
|
+
attr_reader :instrument, :key, :list
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# For backward compatibility, also alias as variant
|
|
9
|
+
alias_method :variant, :instrument
|
|
10
|
+
|
|
11
|
+
def initialize(key:, list:, instrument: nil, variant: nil)
|
|
12
|
+
@instrument = instrument || variant
|
|
11
13
|
@key = key || "default"
|
|
12
14
|
@list = list
|
|
13
15
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module HeadMusic::Instruments; end
|
|
2
|
+
|
|
3
|
+
# The string configuration for a stringed instrument.
|
|
4
|
+
#
|
|
5
|
+
# A Stringing defines the courses (strings) of an instrument and their
|
|
6
|
+
# standard tuning pitches. Each course can have one or more strings.
|
|
7
|
+
#
|
|
8
|
+
# Examples:
|
|
9
|
+
# guitar = HeadMusic::Instruments::Instrument.get("guitar")
|
|
10
|
+
# stringing = HeadMusic::Instruments::Stringing.for_instrument(guitar)
|
|
11
|
+
# stringing.courses.map(&:standard_pitch) # => [E2, A2, D3, G3, B3, E4]
|
|
12
|
+
class HeadMusic::Instruments::Stringing
|
|
13
|
+
STRINGINGS = YAML.load_file(File.expand_path("stringings.yml", __dir__)).freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :instrument_key, :courses
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Find the stringing for an instrument
|
|
19
|
+
# @param instrument [HeadMusic::Instruments::Instrument, String, Symbol] The instrument
|
|
20
|
+
# @return [Stringing, nil]
|
|
21
|
+
def for_instrument(instrument)
|
|
22
|
+
instrument_key = normalize_instrument_key(instrument)
|
|
23
|
+
return nil unless instrument_key
|
|
24
|
+
|
|
25
|
+
data = find_stringing_data(instrument_key, instrument)
|
|
26
|
+
return nil unless data
|
|
27
|
+
|
|
28
|
+
new(instrument_key: instrument_key, courses_data: data["courses"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def normalize_instrument_key(instrument)
|
|
34
|
+
case instrument
|
|
35
|
+
when HeadMusic::Instruments::Instrument
|
|
36
|
+
instrument.name_key.to_s
|
|
37
|
+
else
|
|
38
|
+
instrument.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_stringing_data(instrument_key, instrument)
|
|
43
|
+
# Direct match
|
|
44
|
+
return STRINGINGS[instrument_key] if STRINGINGS.key?(instrument_key)
|
|
45
|
+
|
|
46
|
+
# Try parent instrument if this is an Instrument object
|
|
47
|
+
if instrument.is_a?(HeadMusic::Instruments::Instrument) && instrument.parent
|
|
48
|
+
parent_key = instrument.parent.name_key.to_s
|
|
49
|
+
return STRINGINGS[parent_key] if STRINGINGS.key?(parent_key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(instrument_key:, courses_data:)
|
|
57
|
+
@instrument_key = instrument_key.to_sym
|
|
58
|
+
@courses = build_courses(courses_data)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The instrument this stringing belongs to
|
|
62
|
+
# @return [HeadMusic::Instruments::Instrument]
|
|
63
|
+
def instrument
|
|
64
|
+
HeadMusic::Instruments::Instrument.get(instrument_key)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Number of courses
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
def course_count
|
|
70
|
+
courses.length
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Total number of physical strings across all courses
|
|
74
|
+
# @return [Integer]
|
|
75
|
+
def string_count
|
|
76
|
+
courses.sum(&:string_count)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Standard pitches for each course (primary string only)
|
|
80
|
+
# @return [Array<HeadMusic::Rudiment::Pitch>]
|
|
81
|
+
def standard_pitches
|
|
82
|
+
courses.map(&:standard_pitch)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Apply an alternate tuning to get adjusted pitches
|
|
86
|
+
# @param tuning [AlternateTuning] The alternate tuning to apply
|
|
87
|
+
# @return [Array<HeadMusic::Rudiment::Pitch>]
|
|
88
|
+
def pitches_with_tuning(tuning)
|
|
89
|
+
courses.each_with_index.map do |course, index|
|
|
90
|
+
semitone_adjustment = tuning.semitones[index] || 0
|
|
91
|
+
HeadMusic::Rudiment::Pitch.from_number(course.standard_pitch.to_i + semitone_adjustment)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ==(other)
|
|
96
|
+
return false unless other.is_a?(self.class)
|
|
97
|
+
|
|
98
|
+
instrument_key == other.instrument_key && courses == other.courses
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def to_s
|
|
102
|
+
"#{course_count}-course stringing for #{instrument_key}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def build_courses(courses_data)
|
|
108
|
+
courses_data.map do |course_data|
|
|
109
|
+
HeadMusic::Instruments::StringingCourse.new(
|
|
110
|
+
standard_pitch: course_data["pitch"],
|
|
111
|
+
course_semitones: course_data["course_semitones"] || []
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module HeadMusic::Instruments; end
|
|
2
|
+
|
|
3
|
+
# A single course (string or set of strings) on a stringed instrument.
|
|
4
|
+
#
|
|
5
|
+
# A "course" is a set of strings that are played together. On most guitars,
|
|
6
|
+
# each course has a single string. On a 12-string guitar or mandolin,
|
|
7
|
+
# courses have multiple strings tuned in unison or octaves.
|
|
8
|
+
#
|
|
9
|
+
# Examples:
|
|
10
|
+
# - 6-string guitar: 6 courses, each with 1 string
|
|
11
|
+
# - 12-string guitar: 6 courses, each with 2 strings (octave or unison)
|
|
12
|
+
# - Mandolin: 4 courses, each with 2 strings in unison
|
|
13
|
+
class HeadMusic::Instruments::StringingCourse
|
|
14
|
+
attr_reader :standard_pitch, :course_semitones
|
|
15
|
+
|
|
16
|
+
# @param standard_pitch [HeadMusic::Rudiment::Pitch, String] The pitch of the primary string
|
|
17
|
+
# @param course_semitones [Array<Integer>] Semitone offsets for additional strings in the course
|
|
18
|
+
def initialize(standard_pitch:, course_semitones: [])
|
|
19
|
+
@standard_pitch = HeadMusic::Rudiment::Pitch.get(standard_pitch)
|
|
20
|
+
@course_semitones = Array(course_semitones)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns all pitches in this course (primary + additional strings)
|
|
24
|
+
# @return [Array<HeadMusic::Rudiment::Pitch>]
|
|
25
|
+
def pitches
|
|
26
|
+
[standard_pitch] + additional_pitches
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the number of physical strings in this course
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
def string_count
|
|
32
|
+
1 + course_semitones.length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether this course has multiple strings
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def doubled?
|
|
38
|
+
course_semitones.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
return false unless other.is_a?(self.class)
|
|
43
|
+
|
|
44
|
+
standard_pitch == other.standard_pitch && course_semitones == other.course_semitones
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
standard_pitch.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def additional_pitches
|
|
54
|
+
course_semitones.map do |semitones|
|
|
55
|
+
HeadMusic::Rudiment::Pitch.from_number(standard_pitch.to_i + semitones)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Stringing configurations for stringed instruments.
|
|
2
|
+
#
|
|
3
|
+
# Each instrument has courses (strings played together).
|
|
4
|
+
# course_semitones defines additional strings in a course relative to the pitch.
|
|
5
|
+
#
|
|
6
|
+
# Examples:
|
|
7
|
+
# - [] = single string
|
|
8
|
+
# - [12] = doubled an octave higher
|
|
9
|
+
# - [0] = doubled in unison
|
|
10
|
+
|
|
11
|
+
guitar:
|
|
12
|
+
courses:
|
|
13
|
+
- pitch: E2
|
|
14
|
+
- pitch: A2
|
|
15
|
+
- pitch: D3
|
|
16
|
+
- pitch: G3
|
|
17
|
+
- pitch: B3
|
|
18
|
+
- pitch: E4
|
|
19
|
+
|
|
20
|
+
twelve_string_guitar:
|
|
21
|
+
courses:
|
|
22
|
+
- pitch: E2
|
|
23
|
+
course_semitones: [12]
|
|
24
|
+
- pitch: A2
|
|
25
|
+
course_semitones: [12]
|
|
26
|
+
- pitch: D3
|
|
27
|
+
course_semitones: [12]
|
|
28
|
+
- pitch: G3
|
|
29
|
+
course_semitones: [12]
|
|
30
|
+
- pitch: B3
|
|
31
|
+
course_semitones: [0]
|
|
32
|
+
- pitch: E4
|
|
33
|
+
course_semitones: [0]
|
|
34
|
+
|
|
35
|
+
bass_guitar:
|
|
36
|
+
courses:
|
|
37
|
+
- pitch: E1
|
|
38
|
+
- pitch: A1
|
|
39
|
+
- pitch: D2
|
|
40
|
+
- pitch: G2
|
|
41
|
+
|
|
42
|
+
five_string_bass:
|
|
43
|
+
courses:
|
|
44
|
+
- pitch: B0
|
|
45
|
+
- pitch: E1
|
|
46
|
+
- pitch: A1
|
|
47
|
+
- pitch: D2
|
|
48
|
+
- pitch: G2
|
|
49
|
+
|
|
50
|
+
six_string_bass:
|
|
51
|
+
courses:
|
|
52
|
+
- pitch: B0
|
|
53
|
+
- pitch: E1
|
|
54
|
+
- pitch: A1
|
|
55
|
+
- pitch: D2
|
|
56
|
+
- pitch: G2
|
|
57
|
+
- pitch: C3
|
|
58
|
+
|
|
59
|
+
ukulele:
|
|
60
|
+
courses:
|
|
61
|
+
- pitch: G4
|
|
62
|
+
- pitch: C4
|
|
63
|
+
- pitch: E4
|
|
64
|
+
- pitch: A4
|
|
65
|
+
|
|
66
|
+
baritone_ukulele:
|
|
67
|
+
courses:
|
|
68
|
+
- pitch: D3
|
|
69
|
+
- pitch: G3
|
|
70
|
+
- pitch: B3
|
|
71
|
+
- pitch: E4
|
|
72
|
+
|
|
73
|
+
mandolin:
|
|
74
|
+
courses:
|
|
75
|
+
- pitch: G3
|
|
76
|
+
course_semitones: [0]
|
|
77
|
+
- pitch: D4
|
|
78
|
+
course_semitones: [0]
|
|
79
|
+
- pitch: A4
|
|
80
|
+
course_semitones: [0]
|
|
81
|
+
- pitch: E5
|
|
82
|
+
course_semitones: [0]
|
|
83
|
+
|
|
84
|
+
banjo:
|
|
85
|
+
courses:
|
|
86
|
+
- pitch: G4
|
|
87
|
+
- pitch: D3
|
|
88
|
+
- pitch: G3
|
|
89
|
+
- pitch: B3
|
|
90
|
+
- pitch: D4
|
|
91
|
+
|
|
92
|
+
violin:
|
|
93
|
+
courses:
|
|
94
|
+
- pitch: G3
|
|
95
|
+
- pitch: D4
|
|
96
|
+
- pitch: A4
|
|
97
|
+
- pitch: E5
|
|
98
|
+
|
|
99
|
+
viola:
|
|
100
|
+
courses:
|
|
101
|
+
- pitch: C3
|
|
102
|
+
- pitch: G3
|
|
103
|
+
- pitch: D4
|
|
104
|
+
- pitch: A4
|
|
105
|
+
|
|
106
|
+
cello:
|
|
107
|
+
courses:
|
|
108
|
+
- pitch: C2
|
|
109
|
+
- pitch: G2
|
|
110
|
+
- pitch: D3
|
|
111
|
+
- pitch: A3
|
|
112
|
+
|
|
113
|
+
double_bass:
|
|
114
|
+
courses:
|
|
115
|
+
- pitch: E1
|
|
116
|
+
- pitch: A1
|
|
117
|
+
- pitch: D2
|
|
118
|
+
- pitch: G2
|
|
119
|
+
|
|
120
|
+
harp:
|
|
121
|
+
courses:
|
|
122
|
+
- pitch: C1
|
|
123
|
+
- pitch: D1
|
|
124
|
+
- pitch: E1
|
|
125
|
+
- pitch: F1
|
|
126
|
+
- pitch: G1
|
|
127
|
+
- pitch: A1
|
|
128
|
+
- pitch: B1
|
|
129
|
+
- pitch: C2
|
|
130
|
+
- pitch: D2
|
|
131
|
+
- pitch: E2
|
|
132
|
+
- pitch: F2
|
|
133
|
+
- pitch: G2
|
|
134
|
+
- pitch: A2
|
|
135
|
+
- pitch: B2
|
|
136
|
+
- pitch: C3
|
|
137
|
+
- pitch: D3
|
|
138
|
+
- pitch: E3
|
|
139
|
+
- pitch: F3
|
|
140
|
+
- pitch: G3
|
|
141
|
+
- pitch: A3
|
|
142
|
+
- pitch: B3
|
|
143
|
+
- pitch: C4
|
|
144
|
+
- pitch: D4
|
|
145
|
+
- pitch: E4
|
|
146
|
+
- pitch: F4
|
|
147
|
+
- pitch: G4
|
|
148
|
+
- pitch: A4
|
|
149
|
+
- pitch: B4
|
|
150
|
+
- pitch: C5
|
|
151
|
+
- pitch: D5
|
|
152
|
+
- pitch: E5
|
|
153
|
+
- pitch: F5
|
|
154
|
+
- pitch: G5
|
|
155
|
+
- pitch: A5
|
|
156
|
+
- pitch: B5
|
|
157
|
+
- pitch: C6
|
|
158
|
+
- pitch: D6
|
|
159
|
+
- pitch: E6
|
|
160
|
+
- pitch: F6
|
|
161
|
+
- pitch: G6
|
|
162
|
+
- pitch: A6
|
|
163
|
+
- pitch: B6
|
|
164
|
+
- pitch: C7
|
|
165
|
+
- pitch: D7
|
|
166
|
+
- pitch: E7
|
|
167
|
+
- pitch: F7
|
|
168
|
+
- pitch: G7
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Namespace for instrument definitions, categorization, and configuration
|
|
2
1
|
module HeadMusic::Instruments; end
|
|
3
2
|
|
|
4
3
|
class HeadMusic::Instruments::Variant
|
|
@@ -35,4 +34,10 @@ class HeadMusic::Instruments::Variant
|
|
|
35
34
|
@default_staff_scheme ||=
|
|
36
35
|
staff_schemes.find(&:default?) || staff_schemes.first
|
|
37
36
|
end
|
|
37
|
+
|
|
38
|
+
def ==(other)
|
|
39
|
+
return false unless other.is_a?(self.class)
|
|
40
|
+
|
|
41
|
+
key == other.key && attributes == other.attributes
|
|
42
|
+
end
|
|
38
43
|
end
|