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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +1 -1
  4. data/CHANGELOG.md +53 -0
  5. data/CLAUDE.md +151 -0
  6. data/Gemfile.lock +25 -25
  7. data/MUSIC_THEORY.md +120 -0
  8. data/Rakefile +2 -2
  9. data/bin/check_instrument_consistency.rb +86 -0
  10. data/check_instrument_consistency.rb +0 -0
  11. data/head_music.gemspec +1 -1
  12. data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
  13. data/lib/head_music/analysis/diatonic_interval.rb +50 -27
  14. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  15. data/lib/head_music/content/note.rb +1 -1
  16. data/lib/head_music/content/placement.rb +1 -1
  17. data/lib/head_music/content/position.rb +1 -1
  18. data/lib/head_music/content/staff.rb +1 -1
  19. data/lib/head_music/instruments/instrument.rb +103 -113
  20. data/lib/head_music/instruments/instrument_families.yml +10 -9
  21. data/lib/head_music/instruments/instrument_family.rb +13 -2
  22. data/lib/head_music/instruments/instrument_type.rb +188 -0
  23. data/lib/head_music/instruments/instruments.yml +350 -368
  24. data/lib/head_music/instruments/score_order.rb +139 -0
  25. data/lib/head_music/instruments/score_orders.yml +130 -0
  26. data/lib/head_music/instruments/variant.rb +6 -0
  27. data/lib/head_music/locales/de.yml +6 -0
  28. data/lib/head_music/locales/en.yml +98 -0
  29. data/lib/head_music/locales/es.yml +6 -0
  30. data/lib/head_music/locales/fr.yml +6 -0
  31. data/lib/head_music/locales/it.yml +6 -0
  32. data/lib/head_music/locales/ru.yml +6 -0
  33. data/lib/head_music/rudiment/alteration.rb +23 -8
  34. data/lib/head_music/rudiment/base.rb +9 -0
  35. data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
  36. data/lib/head_music/rudiment/clef.rb +1 -1
  37. data/lib/head_music/rudiment/consonance.rb +37 -4
  38. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  39. data/lib/head_music/rudiment/key.rb +77 -0
  40. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  41. data/lib/head_music/rudiment/key_signature.rb +46 -7
  42. data/lib/head_music/rudiment/letter_name.rb +3 -3
  43. data/lib/head_music/rudiment/meter.rb +19 -9
  44. data/lib/head_music/rudiment/mode.rb +92 -0
  45. data/lib/head_music/rudiment/musical_symbol.rb +1 -1
  46. data/lib/head_music/rudiment/note.rb +112 -0
  47. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  48. data/lib/head_music/rudiment/pitch.rb +5 -6
  49. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  50. data/lib/head_music/rudiment/quality.rb +1 -1
  51. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  52. data/lib/head_music/rudiment/register.rb +4 -1
  53. data/lib/head_music/rudiment/rest.rb +36 -0
  54. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  55. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  56. data/lib/head_music/rudiment/rhythmic_unit.rb +104 -29
  57. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  58. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  59. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  60. data/lib/head_music/rudiment/scale.rb +4 -5
  61. data/lib/head_music/rudiment/scale_degree.rb +9 -4
  62. data/lib/head_music/rudiment/scale_type.rb +9 -3
  63. data/lib/head_music/rudiment/solmization.rb +1 -1
  64. data/lib/head_music/rudiment/spelling.rb +5 -4
  65. data/lib/head_music/rudiment/tempo.rb +85 -0
  66. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  67. data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
  68. data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
  69. data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
  70. data/lib/head_music/rudiment/tuning.rb +18 -4
  71. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  72. data/lib/head_music/style/annotation.rb +4 -4
  73. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  74. data/lib/head_music/style/medieval_tradition.rb +26 -0
  75. data/lib/head_music/style/modern_tradition.rb +34 -0
  76. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  77. data/lib/head_music/style/tradition.rb +21 -0
  78. data/lib/head_music/utilities/hash_key.rb +34 -2
  79. data/lib/head_music/version.rb +1 -1
  80. data/lib/head_music.rb +33 -9
  81. data/user_stories/active/handle-time.md +7 -0
  82. data/user_stories/active/handle-time.rb +177 -0
  83. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  84. data/user_stories/done/epic--score-order/band-score-order.md +38 -0
  85. data/user_stories/done/epic--score-order/chamber-ensemble-score-order.md +33 -0
  86. data/user_stories/done/epic--score-order/orchestral-score-order.md +43 -0
  87. data/user_stories/done/instrument-variant.md +65 -0
  88. data/user_stories/done/superclass-for-note.md +30 -0
  89. data/user_stories/todo/agentic-daw.md +3 -0
  90. data/user_stories/todo/consonance-dissonance-classification.md +57 -0
  91. data/user_stories/todo/dyad-analysis.md +57 -0
  92. data/user_stories/todo/material-and-scores.md +10 -0
  93. data/user_stories/todo/organizing-content.md +72 -0
  94. data/user_stories/todo/percussion_set.md +1 -0
  95. data/user_stories/todo/pitch-class-set-analysis.md +79 -0
  96. data/user_stories/todo/pitch-set-classification.md +72 -0
  97. data/user_stories/todo/sonority-identification.md +67 -0
  98. metadata +51 -6
  99. data/TODO.md +0 -218
@@ -2,8 +2,40 @@
2
2
  module HeadMusic::Utilities; end
3
3
 
4
4
  # Util for converting an object to a consistent hash key
5
- module HeadMusic::Utilities::HashKey
5
+ class HeadMusic::Utilities::HashKey
6
6
  def self.for(identifier)
7
- I18n.transliterate(identifier.to_s).downcase.gsub(/\W+/, "_").to_sym
7
+ @hash_keys ||= {}
8
+ @hash_keys[identifier] ||= new(identifier).to_sym
9
+ end
10
+
11
+ attr_reader :original
12
+
13
+ def initialize(identifier)
14
+ @original = identifier
15
+ end
16
+
17
+ def to_sym
18
+ normalized_string.to_sym
19
+ end
20
+
21
+ private
22
+
23
+ def normalized_string
24
+ @normalized_string ||=
25
+ transliterated_string.downcase.gsub(/\W+/, "_")
26
+ end
27
+
28
+ def transliterated_string
29
+ I18n.transliterate(desymbolized_string)
30
+ end
31
+
32
+ def desymbolized_string
33
+ original.to_s
34
+ .gsub("𝄫", "_double_flat")
35
+ .gsub("♭", "_flat")
36
+ .gsub("♮", "_natural")
37
+ .gsub("♯", "_sharp")
38
+ .gsub("#", "_sharp")
39
+ .gsub("𝄪", "_double_sharp")
8
40
  end
9
41
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "8.2.1"
2
+ VERSION = "9.0.1"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -14,7 +14,7 @@ require "i18n"
14
14
  require "i18n/backend/fallbacks"
15
15
 
16
16
  I18n::Backend::Simple.include I18n::Backend::Fallbacks
17
- I18n.load_path << Dir[File.join(File.dirname(__dir__), "lib", "head_music", "locales", "*.yml")]
17
+ I18n.load_path += Dir[File.join(File.dirname(__dir__), "lib", "head_music", "locales", "*.yml")]
18
18
  I18n.config.available_locales = %i[en fr de it ru es en_US en_GB]
19
19
  I18n.default_locale = :en
20
20
  I18n.fallbacks[:de] = %i[de en_GB en]
@@ -32,37 +32,57 @@ require "head_music/utilities/hash_key"
32
32
  require "head_music/named"
33
33
 
34
34
  # rudiments
35
+ require "head_music/rudiment/base"
36
+ require "head_music/rudiment/letter_name"
35
37
  require "head_music/rudiment/alteration"
38
+ require "head_music/rudiment/spelling"
39
+ require "head_music/rudiment/rhythmic_unit"
40
+ require "head_music/rudiment/rhythmic_unit/parser"
41
+ require "head_music/rudiment/rhythmic_value"
42
+ require "head_music/rudiment/rhythmic_value/parser"
43
+ require "head_music/rudiment/register"
44
+ require "head_music/rudiment/pitch"
45
+ require "head_music/rudiment/pitch/parser"
46
+ require "head_music/rudiment/rhythmic_element"
47
+ require "head_music/rudiment/note"
48
+ require "head_music/rudiment/unpitched_note"
49
+ require "head_music/rudiment/rest"
50
+
36
51
  require "head_music/rudiment/chromatic_interval"
37
52
  require "head_music/rudiment/clef"
38
53
  require "head_music/rudiment/consonance"
54
+ require "head_music/rudiment/tonal_context"
55
+ require "head_music/rudiment/diatonic_context"
56
+ require "head_music/rudiment/key"
57
+ require "head_music/rudiment/mode"
39
58
  require "head_music/rudiment/key_signature"
40
59
  require "head_music/rudiment/key_signature/enharmonic_equivalence"
41
- require "head_music/rudiment/letter_name"
42
60
  require "head_music/rudiment/meter"
43
61
  require "head_music/rudiment/musical_symbol"
44
- require "head_music/rudiment/pitch"
45
62
  require "head_music/rudiment/pitch/enharmonic_equivalence"
46
63
  require "head_music/rudiment/pitch/octave_equivalence"
47
64
  require "head_music/rudiment/pitch_class"
48
65
  require "head_music/rudiment/quality"
49
66
  require "head_music/rudiment/reference_pitch"
50
- require "head_music/rudiment/register"
51
67
  require "head_music/rudiment/rhythm"
52
- require "head_music/rudiment/rhythmic_unit"
53
68
  require "head_music/rudiment/scale"
54
69
  require "head_music/rudiment/scale_degree"
55
70
  require "head_music/rudiment/scale_type"
56
71
  require "head_music/rudiment/solmization"
57
- require "head_music/rudiment/spelling"
72
+ require "head_music/rudiment/tempo"
58
73
  require "head_music/rudiment/tuning"
74
+ require "head_music/rudiment/tuning/just_intonation"
75
+ require "head_music/rudiment/tuning/meantone"
76
+ require "head_music/rudiment/tuning/pythagorean"
59
77
 
60
78
  # instruments
61
79
  require "head_music/instruments/instrument_family"
62
- require "head_music/instruments/instrument"
80
+ require "head_music/instruments/instrument_type"
81
+ require "head_music/instruments/variant"
63
82
  require "head_music/instruments/staff_scheme"
64
83
  require "head_music/instruments/staff"
65
- require "head_music/instruments/variant"
84
+ require "head_music/instruments/instrument"
85
+ require "head_music/instruments/score_order"
66
86
 
67
87
  # content
68
88
  require "head_music/content/bar"
@@ -70,7 +90,6 @@ require "head_music/content/composition"
70
90
  require "head_music/content/note"
71
91
  require "head_music/content/placement"
72
92
  require "head_music/content/position"
73
- require "head_music/content/rhythmic_value"
74
93
  require "head_music/content/staff"
75
94
  require "head_music/content/voice"
76
95
 
@@ -83,6 +102,7 @@ require "head_music/analysis/diatonic_interval/parser"
83
102
  require "head_music/analysis/diatonic_interval/semitones"
84
103
  require "head_music/analysis/diatonic_interval/size"
85
104
  require "head_music/analysis/harmonic_interval"
105
+ require "head_music/analysis/interval_consonance"
86
106
  require "head_music/analysis/interval_cycle"
87
107
  require "head_music/analysis/melodic_interval"
88
108
  require "head_music/analysis/motion"
@@ -91,6 +111,10 @@ require "head_music/analysis/pitch_set"
91
111
  require "head_music/analysis/sonority"
92
112
 
93
113
  # style analysis
114
+ require "head_music/style/tradition"
115
+ require "head_music/style/modern_tradition"
116
+ require "head_music/style/renaissance_tradition"
117
+ require "head_music/style/medieval_tradition"
94
118
  require "head_music/style/analysis"
95
119
  require "head_music/style/annotation"
96
120
  require "head_music/style/mark"
@@ -0,0 +1,7 @@
1
+ # Handling Time and Tempo
2
+
3
+ AS a developer
4
+ WHEN I am dealing with musical material
5
+ I WANT to be able to synchronize the position of bars and beats with the flow of clock time
6
+
7
+ See `./handle-time.rb`
@@ -0,0 +1,177 @@
1
+ # The Time module provides classes and methods to handle representations
2
+ # of musical time and its relationship to clock time and SMPTE Time Code.
3
+ module HeadMusic::Time
4
+ # ticks per quarter note value
5
+ PPQN = PULSES_PER_QUARTER_NOTE = 960
6
+ SUBTICKS_PER_TICK = 240
7
+ end
8
+
9
+ # Representation of a conductor track for musical material
10
+ # Each moment in a track corresponds to the following positions:
11
+ # - ellapsed clock time
12
+ # - starts at 0.0 seconds
13
+ # - the source-of-truth clock time
14
+ # - nanosecond resolution
15
+ # - a musical position
16
+ # - bars:beats:ticks:subticks
17
+ # - a SMPTE timecode
18
+ # - hours:minutes:seconds:frames
19
+ class HeadMusic::Time::Conductor
20
+ attr_accessor :starting_position, :starting_smpte_timecode, :framerate
21
+
22
+ def initialize(attributes = {})
23
+ attributes = attributes.symbolize_keys
24
+ @starting_position = attributes.get(:starting_position, HeadMusic::Time::Position.new)
25
+ @starting_smpte_timecode = attributes.get(:starting_smpte_timecode, HeadMusic::Time::SmpteTimecode.new)
26
+ end
27
+ end
28
+
29
+ class HeadMusic::Time::MeterEvent
30
+ attr_accessor :position, :meter
31
+
32
+ def initialize(position, meter)
33
+ @position = position
34
+ end
35
+ end
36
+
37
+ class HeadMusic::Time::TempoEvent
38
+ attr_accessor :position, :tempo
39
+
40
+ # accepts a rhythmic value and a bpm
41
+ def initialize(position, beat_value, beats_per_minute)
42
+ @position = position
43
+ @tempo = HeadMusic::Rudiment::Tempo.new(beat_value, beats_per_minute)
44
+ end
45
+ end
46
+
47
+ # Abstract superclass
48
+ class HeadMusic::Time::Position
49
+ include Comparable
50
+
51
+ def initialize(value, meter: nil, tempo: nil)
52
+ @value = value
53
+ @meter = meter || HeadMusic::Rudiment::Meter.default
54
+ @tempo = tempo || HeadMusic::Rudiment::Tempo.default
55
+ end
56
+
57
+ def <=>(other)
58
+ to_i <=> other.to_i
59
+ end
60
+ end
61
+
62
+ # A value object representing ellapsed nanoseconds of clock time
63
+ class HeadMusic::Time::ClockPosition
64
+ include Comparable
65
+
66
+ attr_reader :nanoseconds
67
+
68
+ def initialize(nanoseconds)
69
+ @nanoseconds = nanoseconds
70
+ end
71
+
72
+ def to_i
73
+ nanoseconds
74
+ end
75
+
76
+ def to_microseconds
77
+ nanoseconds / 1_000.0
78
+ end
79
+
80
+ def to_milliseconds
81
+ nanoseconds / 1_000_000.0
82
+ end
83
+
84
+ def to_seconds
85
+ nanoseconds / 1_000_000_000.0
86
+ end
87
+
88
+ def +(other)
89
+ HeadMusic::Time::Value.new(nanoseconds + other.to_i)
90
+ end
91
+
92
+ def <=>(other)
93
+ nanoseconds <=> other.to_i
94
+ end
95
+ end
96
+
97
+ # Representation of a musical position.
98
+ # Consists of:
99
+ # - bar
100
+ # - beat (or count)
101
+ # - tick (960 ticks / quarter note value)
102
+ # - subtick (240 subticks / tick)
103
+ #
104
+ # Note: In the absence of a specific meter,
105
+ # no math can be performed on the position.
106
+ class HeadMusic::Time::MusicalPosition
107
+ attr_reader :bar, :beat, :tick, :subtick
108
+
109
+ DEFAULT_FIRST_BAR = 1
110
+ FIRST_BEAT = 1
111
+ FIRST_TICK = 0
112
+ FIRST_SUBTICK = 0
113
+
114
+ def self.parse(identifier)
115
+ new(*identifier.scan(/\d+/)[0..3])
116
+ end
117
+
118
+ def initialize(
119
+ bar = DEFAULT_FIRST_BAR,
120
+ beat = FIRST_BEAT,
121
+ tick = FIRST_TICK,
122
+ subtick = FIRST_SUBTICK
123
+ )
124
+ @bar = bar.to_i
125
+ @beat = beat.to_i
126
+ @tick = tick.to_i
127
+ @subtick = subtick.to_i
128
+ end
129
+
130
+ def to_a
131
+ [bar, beat, tick, subtick]
132
+ end
133
+
134
+ def to_s
135
+ "#{bar}:#{beat}:#{tick}:#{subtick}"
136
+ end
137
+
138
+ # Accept a meter and roll excessive values over to the next level
139
+ def normalize!(meter)
140
+ return self unless meter
141
+
142
+ # Carry subticks into ticks
143
+ if subtick >= HeadMusic::Time::SUBTICKS_PER_TICK || subtick.negative?
144
+ tick_delta, @subtick = subtick.divmod(HeadMusic::Time::SUBTICKS_PER_TICK)
145
+ @tick += tick_delta
146
+ end
147
+
148
+ # Carry ticks into beats
149
+ if tick >= meter.ticks_per_count || tick.negative?
150
+ beat_delta, @tick = tick.divmod(meter.ticks_per_count)
151
+ @beat += beat_delta
152
+ end
153
+
154
+ # Carry beats into bars
155
+ if beat >= meter.counts_per_bar || beat.negative?
156
+ bar_delta, @beat = beat.divmod(meter.counts_per_bar)
157
+ @bar += bar_delta
158
+ end
159
+ HeadMusic::Time::Position.new(@bar, @beat, @tick, @subtick)
160
+ end
161
+ end
162
+
163
+ class HeadMusic::Time::SamplesPosition
164
+ end
165
+
166
+ class HeadMusic::Time::TimecodePosition # (SMPTE/MTC)
167
+ end
168
+
169
+ # Represents a SMPTE timecode position
170
+ # HH:MM:SS:FF (hours:minutes:seconds:frames)
171
+ class HeadMusic::Time::SmpteTimecode
172
+ attr_reader :hour, :minute, :second, :frame
173
+
174
+ def initialize(hour = 1, minute = 0, second = 0, frame = 0)
175
+ @hour, @minute, @second, @frame = hour.to_i, minute.to_i, second.to_i, frame.to_i
176
+ end
177
+ end
@@ -0,0 +1,244 @@
1
+ # Score-Order Epic: Implementation Plan
2
+
3
+ ## Executive Summary
4
+
5
+ The score-order feature addresses a fundamental need in music composition software - automatically organizing instruments in standardized score orders based on ensemble type. This is critical for professional composers and arrangers who need their scores to follow industry conventions.
6
+
7
+ ## Value Proposition
8
+
9
+ - **Saves time**: Composers won't need to manually reorder instruments
10
+ - **Ensures accuracy**: Follows established conventions automatically
11
+ - **Flexibility**: Supports multiple ensemble types (orchestral, band, chamber)
12
+ - **Professional output**: Scores will meet industry standards
13
+
14
+ ## Priority Recommendations
15
+
16
+ ### Phase 1: Orchestral Order (MVP)
17
+ - Most complex and widely used
18
+ - Clear, established conventions
19
+ - Highest value for professional users
20
+
21
+ ### Phase 2: Band Support
22
+ - Different enough to showcase flexibility
23
+ - Strong user base in educational settings
24
+ - Notable percussion placement differences
25
+
26
+ ### Phase 3: Chamber Ensembles
27
+ - More specialized use cases
28
+ - Could potentially be template-based
29
+
30
+ ## Technical Implementation Approach
31
+
32
+ ### Architecture Insights
33
+
34
+ The existing Instruments module already provides:
35
+ - `orchestra_section_key` (woodwind, brass, percussion, string, keyboard, voice)
36
+ - Instrument families with proper classification
37
+ - Good foundation but NO ordering logic yet
38
+
39
+ ### Proposed Implementation
40
+
41
+ Create a standalone **`HeadMusic::Instruments::ScoreOrder`** class that:
42
+
43
+ 1. **Defines ordering rules** for different ensemble types (orchestral, band, chamber)
44
+ 2. **Works with instrument instances** directly (not tied to Composition module)
45
+ 3. **Supports custom overrides** while maintaining sensible defaults
46
+ 4. **Handles instrument abbreviations** through existing i18n system
47
+
48
+ ### Key Design Decisions
49
+
50
+ - **Keep it simple**: No complex inheritance, just a straightforward ordering system
51
+ - **Data-driven approach**: Store ordering rules in YAML like existing instrument data
52
+ - **Flexible API**: Accept arrays of instrument names/objects, return ordered list
53
+ - **Support multiple sections**: Handle both full instruments and section groupings
54
+ - **Independence**: Not dependent on Content module (which is being rewritten)
55
+
56
+ ## Acceptance Criteria
57
+
58
+ ### Basic Functionality
59
+ - ✅ Can order an array of instruments by orchestral convention
60
+ - ✅ Can order an array of instruments by band convention
61
+ - ✅ Can order an array of instruments by chamber ensemble type
62
+ - ✅ Returns instruments in correct family/section groupings
63
+
64
+ ### Flexibility
65
+ - ✅ Handles unknown instruments gracefully (append at end)
66
+ - ✅ Supports both instrument objects and string names as input
67
+ - ✅ Allows custom ordering overrides
68
+
69
+ ### Integration
70
+ - ✅ Works independently of Content module
71
+ - ✅ Follows existing HeadMusic patterns (`.get()` factory method)
72
+ - ✅ Maintains 90%+ test coverage
73
+
74
+ ### Performance
75
+ - ✅ Orders 100+ instruments in < 100ms
76
+ - ✅ Caches ordering rules efficiently
77
+
78
+ ## MVP Scope
79
+
80
+ Start with orchestral ordering only:
81
+
82
+ ```ruby
83
+ instruments = ["violin", "trumpet", "flute", "timpani", "cello"]
84
+ ordered = HeadMusic::Instruments::ScoreOrder.in_orchestral_order(instruments)
85
+ # => ["flute", "trumpet", "timpani", "violin", "cello"]
86
+ ```
87
+
88
+ ## Detailed Orchestral Order
89
+
90
+ Standard orchestral score order from top to bottom:
91
+
92
+ 1. **Woodwinds**
93
+ - Piccolo
94
+ - Flutes (I, II, III)
95
+ - Oboes (I, II, III)
96
+ - English Horn
97
+ - Clarinets (I, II, III)
98
+ - Bass Clarinet
99
+ - Bassoons (I, II, III)
100
+ - Contrabassoon
101
+
102
+ 2. **Brass**
103
+ - Horns (I, II, III, IV)
104
+ - Trumpets (I, II, III)
105
+ - Trombones (I, II, III)
106
+ - Tuba
107
+
108
+ 3. **Percussion**
109
+ - Timpani
110
+ - Percussion (various)
111
+
112
+ 4. **Keyboards & Harp**
113
+ - Harp
114
+ - Piano
115
+ - Celesta
116
+ - Organ
117
+
118
+ 5. **Soloists** (if any)
119
+ - Instrumental soloists
120
+ - Vocal soloists
121
+
122
+ 6. **Voices** (if any)
123
+ - Soprano
124
+ - Alto
125
+ - Tenor
126
+ - Bass
127
+
128
+ 7. **Strings**
129
+ - Violin I
130
+ - Violin II
131
+ - Viola
132
+ - Cello
133
+ - Double Bass
134
+
135
+ ## Success Metrics
136
+
137
+ - Can correctly order any standard ensemble
138
+ - Supports custom overrides when needed
139
+ - Integrates smoothly with existing Instrument class
140
+ - Maintains 90%+ test coverage
141
+ - Performance: < 100ms for typical ensemble sizes
142
+
143
+ ## Implementation Strategy
144
+
145
+ ### Class Design: Single Class with Data-Driven Instances
146
+
147
+ We'll use a **single class with instances** approach, following HeadMusic's established patterns:
148
+
149
+ ```ruby
150
+ class HeadMusic::Instruments::ScoreOrder
151
+ include HeadMusic::Named
152
+
153
+ def self.get(ensemble_type)
154
+ # Returns an instance configured for that ensemble type
155
+ # Follows existing HeadMusic factory pattern
156
+ end
157
+
158
+ def self.in_orchestral_order(instruments)
159
+ get(:orchestral).order(instruments)
160
+ end
161
+
162
+ def self.in_band_order(instruments)
163
+ get(:band).order(instruments)
164
+ end
165
+
166
+ def order(instruments)
167
+ # Apply ordering rules from YAML data
168
+ # Handle both instrument objects and strings
169
+ # Gracefully handle unknown instruments
170
+ end
171
+ end
172
+ ```
173
+
174
+ ### Why This Approach
175
+
176
+ 1. **Consistency with HeadMusic patterns**:
177
+ - Matches `Instrument` and `InstrumentFamily` design
178
+ - Uses `.get()` factory method pattern
179
+ - Data-driven via YAML configuration
180
+
181
+ 2. **Maintainability**:
182
+ - Single class to test and maintain
183
+ - Shared logic for common operations
184
+ - Easy to add new ensemble types without code changes
185
+
186
+ 3. **Flexibility**:
187
+ - Rules defined in YAML, not hardcoded
188
+ - Simple to add custom orderings
189
+ - Could support user-defined orderings in future
190
+
191
+ ### YAML Structure
192
+
193
+ ```yaml
194
+ # lib/head_music/instruments/score_orders.yml
195
+ orchestral:
196
+ name: "Orchestral"
197
+ sections:
198
+ - section_key: woodwind
199
+ instruments: [piccolo, flute, oboe, english_horn, clarinet, bass_clarinet, bassoon, contrabassoon]
200
+ - section_key: brass
201
+ instruments: [horn, trumpet, trombone, tuba]
202
+ - section_key: percussion
203
+ instruments: [timpani, percussion]
204
+ - section_key: keyboard
205
+ instruments: [harp, piano, celesta, organ]
206
+ - section_key: voice
207
+ instruments: [soprano_voice, alto_voice, tenor_voice, bass_voice]
208
+ - section_key: string
209
+ instruments: [violin, viola, cello, double_bass]
210
+
211
+ band:
212
+ name: "Concert Band"
213
+ sections:
214
+ - section_key: woodwind
215
+ instruments: [flute, oboe, bassoon, clarinet, saxophone]
216
+ - section_key: brass
217
+ instruments: [cornet, trumpet, horn, trombone, euphonium, tuba]
218
+ - section_key: percussion
219
+ instruments: [timpani, percussion]
220
+
221
+ brass_quintet:
222
+ name: "Brass Quintet"
223
+ sections:
224
+ - section_key: brass
225
+ instruments: [trumpet, trumpet, horn, trombone, tuba]
226
+
227
+ woodwind_quintet:
228
+ name: "Woodwind Quintet"
229
+ sections:
230
+ - section_key: woodwind
231
+ instruments: [flute, oboe, clarinet, horn, bassoon]
232
+ ```
233
+
234
+ ## Implementation Steps
235
+
236
+ 1. Create `HeadMusic::Instruments::ScoreOrder` class with factory pattern
237
+ 2. Create `score_orders.yml` with orchestral ordering rules
238
+ 3. Implement `#order` method with instrument resolution logic
239
+ 4. Add RSpec tests for orchestral ordering
240
+ 5. Add band ordering to YAML and test
241
+ 6. Add chamber ensemble templates and tests
242
+ 7. Add support for custom instrument positions
243
+ 8. Document API with YARD comments
244
+ 9. Consider future integration points with notation systems
@@ -0,0 +1,38 @@
1
+ # Band Score Order
2
+
3
+ As a band director or arranger
4
+
5
+ I want to organize instruments in band score order
6
+
7
+ So that my scores follow standard concert band conventions
8
+
9
+ ## Scenario: Display instruments in band order
10
+
11
+ Given I have a composition for concert band
12
+
13
+ When I request the score order for a band arrangement
14
+
15
+ Then the instruments should be ordered as follows:
16
+ - Flutes
17
+ - Oboes
18
+ - Bassoons
19
+ - Clarinets
20
+ - Saxophones
21
+ - Cornets
22
+ - Trumpets
23
+ - Horns
24
+ - Trombones
25
+ - Euphoniums
26
+ - Tubas
27
+ - Timpani
28
+ - Percussion
29
+
30
+ ## Scenario: Recognize different percussion placement
31
+
32
+ Given I am working with both orchestral and band scores
33
+
34
+ When I compare the score orders
35
+
36
+ Then I should note that percussion placement differs:
37
+ - In orchestral scores: percussion appears after brass
38
+ - In band scores: percussion appears at the bottom
@@ -0,0 +1,33 @@
1
+ # Chamber Ensemble Score Order
2
+
3
+ As a chamber music composer
4
+
5
+ I want to organize instruments in standard chamber ensemble orders
6
+
7
+ So that my scores follow established conventions for small ensembles
8
+
9
+ ## Scenario: Display brass quintet in standard order
10
+
11
+ Given I have a brass quintet composition
12
+
13
+ When I request the score order
14
+
15
+ Then the instruments should be ordered as follows:
16
+ - Trumpet I
17
+ - Trumpet II
18
+ - Horn
19
+ - Trombone
20
+ - Tuba
21
+
22
+ ## Scenario: Display woodwind quintet in standard order
23
+
24
+ Given I have a woodwind quintet composition
25
+
26
+ When I request the score order
27
+
28
+ Then the instruments should be ordered as follows:
29
+ - Flute
30
+ - Oboe
31
+ - Clarinet
32
+ - Horn
33
+ - Bassoon
@@ -0,0 +1,43 @@
1
+ # Orchestral Score Order
2
+
3
+ As a composer or conductor
4
+
5
+ I want to organize instruments in orchestral score order
6
+
7
+ So that my scores follow standard orchestral conventions
8
+
9
+ ## Scenario: Display instruments in orchestral order
10
+
11
+ Given I have a composition with multiple instruments
12
+
13
+ When I request the score order for an orchestral arrangement
14
+
15
+ Then the instruments should be ordered as follows:
16
+ - Woodwinds (flutes, oboes, clarinets, bassoons)
17
+ - Brass (horns, trumpets, trombones, tuba)
18
+ - Percussion (timpani, percussion)
19
+ - Harp and keyboards
20
+ - Soloists (instrumental or vocal)
21
+ - Voices
22
+ - Strings (violins I, violins II, viola, violoncellos, double bass)
23
+
24
+ ## Scenario: Use standard orchestral abbreviations
25
+
26
+ Given I am creating an orchestral score
27
+
28
+ When I display instrument names
29
+
30
+ Then I should see standard abbreviations:
31
+ - Fl or Fls for Flutes
32
+ - Ob or Obs for Oboes
33
+ - Cl or Cls for Clarinets
34
+ - Bsn or Bsns for Bassoons
35
+ - Hn or Hns for Horns
36
+ - Tpt or Tpts for Trumpets
37
+ - Trb or Trbs for Trombones
38
+ - Timp for Timpani
39
+ - Perc for Percussion
40
+ - Vlns for Violins
41
+ - Vla for Viola
42
+ - Vcl for Violoncellos
43
+ - DB for Double Bass