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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -3
  3. data/CHANGELOG.md +71 -0
  4. data/CLAUDE.md +62 -25
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +91 -3
  7. data/MUSIC_THEORY.md +120 -0
  8. data/README.md +18 -0
  9. data/Rakefile +7 -2
  10. data/head_music.gemspec +1 -1
  11. data/lib/head_music/analysis/diatonic_interval.rb +29 -27
  12. data/lib/head_music/analysis/dyad.rb +229 -0
  13. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  14. data/lib/head_music/analysis/melodic_interval.rb +1 -1
  15. data/lib/head_music/analysis/pitch_class_set.rb +111 -14
  16. data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
  17. data/lib/head_music/analysis/sonority.rb +50 -12
  18. data/lib/head_music/content/note.rb +1 -1
  19. data/lib/head_music/content/placement.rb +1 -1
  20. data/lib/head_music/content/position.rb +1 -1
  21. data/lib/head_music/content/voice.rb +1 -1
  22. data/lib/head_music/instruments/alternate_tuning.rb +102 -0
  23. data/lib/head_music/instruments/alternate_tunings.yml +78 -0
  24. data/lib/head_music/instruments/instrument.rb +231 -72
  25. data/lib/head_music/instruments/instrument_configuration.rb +66 -0
  26. data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
  27. data/lib/head_music/instruments/instrument_configurations.yml +288 -0
  28. data/lib/head_music/instruments/instrument_families.yml +77 -0
  29. data/lib/head_music/instruments/instrument_family.rb +15 -5
  30. data/lib/head_music/instruments/instruments.yml +795 -965
  31. data/lib/head_music/instruments/playing_technique.rb +75 -0
  32. data/lib/head_music/instruments/playing_techniques.yml +826 -0
  33. data/lib/head_music/instruments/score_order.rb +136 -0
  34. data/lib/head_music/instruments/score_orders.yml +130 -0
  35. data/lib/head_music/instruments/staff.rb +61 -1
  36. data/lib/head_music/instruments/staff_scheme.rb +6 -4
  37. data/lib/head_music/instruments/stringing.rb +115 -0
  38. data/lib/head_music/instruments/stringing_course.rb +58 -0
  39. data/lib/head_music/instruments/stringings.yml +168 -0
  40. data/lib/head_music/instruments/variant.rb +6 -1
  41. data/lib/head_music/locales/de.yml +29 -0
  42. data/lib/head_music/locales/en.yml +106 -0
  43. data/lib/head_music/locales/es.yml +29 -0
  44. data/lib/head_music/locales/fr.yml +29 -0
  45. data/lib/head_music/locales/it.yml +29 -0
  46. data/lib/head_music/locales/ru.yml +29 -0
  47. data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
  48. data/lib/head_music/notation/staff_mapping.rb +70 -0
  49. data/lib/head_music/notation/staff_position.rb +62 -0
  50. data/lib/head_music/notation.rb +7 -0
  51. data/lib/head_music/rudiment/alteration.rb +34 -49
  52. data/lib/head_music/rudiment/alterations.yml +32 -0
  53. data/lib/head_music/rudiment/base.rb +9 -0
  54. data/lib/head_music/rudiment/chromatic_interval.rb +4 -7
  55. data/lib/head_music/rudiment/clef.rb +2 -2
  56. data/lib/head_music/rudiment/consonance.rb +39 -5
  57. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  58. data/lib/head_music/rudiment/key.rb +77 -0
  59. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  60. data/lib/head_music/rudiment/key_signature.rb +21 -8
  61. data/lib/head_music/rudiment/letter_name.rb +3 -3
  62. data/lib/head_music/rudiment/meter.rb +19 -9
  63. data/lib/head_music/rudiment/mode.rb +92 -0
  64. data/lib/head_music/rudiment/note.rb +112 -0
  65. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  66. data/lib/head_music/rudiment/pitch.rb +5 -6
  67. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  68. data/lib/head_music/rudiment/quality.rb +1 -1
  69. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  70. data/lib/head_music/rudiment/register.rb +4 -1
  71. data/lib/head_music/rudiment/rest.rb +36 -0
  72. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  73. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  74. data/lib/head_music/rudiment/rhythmic_unit.rb +13 -5
  75. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  76. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  77. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  78. data/lib/head_music/rudiment/scale.rb +4 -5
  79. data/lib/head_music/rudiment/scale_degree.rb +1 -1
  80. data/lib/head_music/rudiment/scale_type.rb +9 -3
  81. data/lib/head_music/rudiment/solmization.rb +1 -1
  82. data/lib/head_music/rudiment/spelling.rb +8 -4
  83. data/lib/head_music/rudiment/tempo.rb +85 -0
  84. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  85. data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
  86. data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
  87. data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
  88. data/lib/head_music/rudiment/tuning.rb +21 -1
  89. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  90. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  91. data/lib/head_music/style/medieval_tradition.rb +26 -0
  92. data/lib/head_music/style/modern_tradition.rb +31 -0
  93. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  94. data/lib/head_music/style/tradition.rb +21 -0
  95. data/lib/head_music/time/clock_position.rb +84 -0
  96. data/lib/head_music/time/conductor.rb +264 -0
  97. data/lib/head_music/time/meter_event.rb +37 -0
  98. data/lib/head_music/time/meter_map.rb +173 -0
  99. data/lib/head_music/time/musical_position.rb +188 -0
  100. data/lib/head_music/time/smpte_timecode.rb +164 -0
  101. data/lib/head_music/time/tempo_event.rb +40 -0
  102. data/lib/head_music/time/tempo_map.rb +187 -0
  103. data/lib/head_music/time.rb +32 -0
  104. data/lib/head_music/utilities/case.rb +27 -0
  105. data/lib/head_music/utilities/hash_key.rb +34 -2
  106. data/lib/head_music/version.rb +1 -1
  107. data/lib/head_music.rb +71 -22
  108. data/user_stories/active/string-pitches.md +41 -0
  109. data/user_stories/backlog/notation-style.md +183 -0
  110. data/user_stories/backlog/organizing-content.md +80 -0
  111. data/user_stories/done/consonance-dissonance-classification.md +117 -0
  112. data/user_stories/{backlog → done}/dyad-analysis.md +6 -16
  113. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  114. data/user_stories/done/expand-playing-techniques.md +38 -0
  115. data/user_stories/done/handle-time.md +7 -0
  116. data/user_stories/done/handle-time.rb +163 -0
  117. data/user_stories/done/instrument-architecture.md +238 -0
  118. data/user_stories/done/instrument-variant.md +65 -0
  119. data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
  120. data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
  121. data/user_stories/done/move-staff-position-to-notation.md +141 -0
  122. data/user_stories/done/notation-module-foundation.md +102 -0
  123. data/user_stories/done/percussion_set.md +260 -0
  124. data/user_stories/done/sonority-identification.md +37 -0
  125. data/user_stories/done/superclass-for-note.md +30 -0
  126. data/user_stories/epics/notation-module.md +135 -0
  127. data/user_stories/visioning/agentic-daw.md +2 -0
  128. metadata +84 -18
  129. data/TODO.md +0 -109
  130. data/check_instrument_consistency.rb +0 -0
  131. data/test_translations.rb +0 -15
  132. data/user_stories/backlog/consonance-dissonance-classification.md +0 -57
  133. data/user_stories/backlog/pitch-set-classification.md +0 -62
  134. data/user_stories/backlog/sonority-identification.md +0 -47
  135. /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
  136. /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
  137. /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
  138. /data/user_stories/{backlog → done}/pitch-class-set-analysis.md +0 -0
@@ -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
+ ## User Story: Load Playing Techniques from YAML Data File
2
+
3
+ **As a** client of the HeadMusic gem
4
+
5
+ **I want** access to a comprehensive catalog of playing techniques across all instrument families
6
+
7
+ **So that** I can notate and work with playing techniques for strings, winds, harp, keyboard, and percussion—not just drum kit techniques
8
+
9
+ ### Background
10
+
11
+ The current `PlayingTechnique` class uses a hardcoded `TECHNIQUES` constant containing only 14 percussion-focused techniques (stick, pedal, mallet, hand, brush, rim_shot, cross_stick, open, closed, damped, let_ring, choked, bow, bell).
12
+
13
+ A new `playing_techniques.yml` data file has been created with 50+ techniques organized by scope (common, strings, winds, percussion, harp, keyboard), including:
14
+
15
+ - **Common techniques**: legato, marcato, vibrato, con sordino, naturale, ordinario
16
+ - **String/wind techniques**: harmonics, bowing techniques
17
+ - **Percussion techniques**: rim shots, cross sticks, dead strokes, motor on/off
18
+ - **Harp techniques**: près de la table
19
+ - **Rich metadata**: origin language, meaning, notation variants
20
+
21
+ The YAML-driven approach aligns with how other HeadMusic components work (instruments, clefs, scales, etc.) and enables future extensibility.
22
+
23
+ ### Acceptance Criteria
24
+
25
+ - [ ] `PlayingTechnique.all` returns techniques loaded from `playing_techniques.yml`
26
+ - [ ] The `TECHNIQUES` constant is removed
27
+ - [ ] Each technique retains access to its metadata (scopes, origin, meaning, notations)
28
+ - [ ] `PlayingTechnique.get(identifier)` continues to work for any technique in the YAML
29
+ - [ ] New accessor methods for metadata: `#scopes`, `#origin`, `#meaning`, `#notations`
30
+ - [ ] Techniques can be filtered by scope (e.g., `PlayingTechnique.for_scope(:strings)`)
31
+ - [ ] Existing specs pass; new specs cover the expanded technique catalog
32
+ - [ ] Maintains 90%+ test coverage
33
+
34
+ ### Technical Notes
35
+
36
+ - Follow the existing YAML-loading pattern used by `Instrument`, `Clef`, and other data-driven classes
37
+ - The `HeadMusic::Named` mixin is already included, enabling future i18n support
38
+ - Consider caching loaded techniques for performance (consistent with other HeadMusic patterns)
@@ -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,163 @@
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.fetch(:starting_position, HeadMusic::Time::MusicalPosition.new)
25
+ @starting_smpte_timecode = attributes.fetch(: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
+ @meter = meter
35
+ end
36
+ end
37
+
38
+ class HeadMusic::Time::TempoEvent
39
+ attr_accessor :position, :tempo
40
+
41
+ # accepts a rhythmic value and a bpm
42
+ def initialize(position, beat_value, beats_per_minute)
43
+ @position = position
44
+ @tempo = HeadMusic::Rudiment::Tempo.new(beat_value, beats_per_minute)
45
+ end
46
+ end
47
+
48
+ # A value object representing ellapsed nanoseconds of clock time
49
+ class HeadMusic::Time::ClockPosition
50
+ include Comparable
51
+
52
+ attr_reader :nanoseconds
53
+
54
+ def initialize(nanoseconds)
55
+ @nanoseconds = nanoseconds
56
+ end
57
+
58
+ def to_i
59
+ nanoseconds
60
+ end
61
+
62
+ def to_microseconds
63
+ nanoseconds / 1_000.0
64
+ end
65
+
66
+ def to_milliseconds
67
+ nanoseconds / 1_000_000.0
68
+ end
69
+
70
+ def to_seconds
71
+ nanoseconds / 1_000_000_000.0
72
+ end
73
+
74
+ def +(other)
75
+ HeadMusic::Time::ClockPosition.new(nanoseconds + other.to_i)
76
+ end
77
+
78
+ def <=>(other)
79
+ nanoseconds <=> other.to_i
80
+ end
81
+ end
82
+
83
+ # Representation of a musical position.
84
+ # Consists of:
85
+ # - bar
86
+ # - beat (or count)
87
+ # - tick (960 ticks / quarter note value)
88
+ # - subtick (240 subticks / tick)
89
+ #
90
+ # Note: In the absence of a specific meter,
91
+ # no math can be performed on the position.
92
+ class HeadMusic::Time::MusicalPosition
93
+ attr_reader :bar, :beat, :tick, :subtick
94
+
95
+ DEFAULT_FIRST_BAR = 1
96
+ FIRST_BEAT = 1
97
+ FIRST_TICK = 0
98
+ FIRST_SUBTICK = 0
99
+
100
+ def self.parse(identifier)
101
+ new(*identifier.scan(/\d+/)[0..3])
102
+ end
103
+
104
+ def initialize(
105
+ bar = DEFAULT_FIRST_BAR,
106
+ beat = FIRST_BEAT,
107
+ tick = FIRST_TICK,
108
+ subtick = FIRST_SUBTICK
109
+ )
110
+ @bar = bar.to_i
111
+ @beat = beat.to_i
112
+ @tick = tick.to_i
113
+ @subtick = subtick.to_i
114
+ end
115
+
116
+ def to_a
117
+ [bar, beat, tick, subtick]
118
+ end
119
+
120
+ def to_s
121
+ "#{bar}:#{beat}:#{tick}:#{subtick}"
122
+ end
123
+
124
+ # Accept a meter and roll excessive values over to the next level
125
+ def normalize!(meter)
126
+ return self unless meter
127
+
128
+ # Carry subticks into ticks
129
+ if subtick >= HeadMusic::Time::SUBTICKS_PER_TICK || subtick.negative?
130
+ tick_delta, @subtick = subtick.divmod(HeadMusic::Time::SUBTICKS_PER_TICK)
131
+ @tick += tick_delta
132
+ end
133
+
134
+ # Carry ticks into beats
135
+ if tick >= meter.ticks_per_count || tick.negative?
136
+ beat_delta, @tick = tick.divmod(meter.ticks_per_count)
137
+ @beat += beat_delta
138
+ end
139
+
140
+ # Carry beats into bars
141
+ if beat >= meter.counts_per_bar || beat.negative?
142
+ bar_delta, @beat = beat.divmod(meter.counts_per_bar)
143
+ @bar += bar_delta
144
+ end
145
+ self
146
+ end
147
+ end
148
+
149
+ class HeadMusic::Time::SamplesPosition
150
+ end
151
+
152
+ class HeadMusic::Time::TimecodePosition # (SMPTE/MTC)
153
+ end
154
+
155
+ # Represents a SMPTE timecode position
156
+ # HH:MM:SS:FF (hours:minutes:seconds:frames)
157
+ class HeadMusic::Time::SmpteTimecode
158
+ attr_reader :hour, :minute, :second, :frame
159
+
160
+ def initialize(hour = 1, minute = 0, second = 0, frame = 0)
161
+ @hour, @minute, @second, @frame = hour.to_i, minute.to_i, second.to_i, frame.to_i
162
+ end
163
+ end
@@ -0,0 +1,238 @@
1
+ # Instrument Inheritance Architecture
2
+
3
+ AS a developer
4
+
5
+ I WANT instruments to resolve attributes through composable parent-based inheritance with configurable options
6
+
7
+ SO THAT I can model the full complexity of instruments with a simple, familiar pattern
8
+
9
+ ## Background
10
+
11
+ The current instrument architecture uses nested objects (GenericInstrument → Variant → StaffScheme → Staff) which conflates several independent concerns:
12
+
13
+ 1. **Species defaults** - What a trumpet typically is
14
+ 2. **Pitched identity** - This specific clarinet is in A
15
+ 3. **Physical configuration** - Piccolo trumpet with A leadpipe installed
16
+ 4. **Notation conventions** - British brass band uses treble clef for euphonium
17
+
18
+ This story addresses concerns 1-3 through parent-based inheritance. Notation (concern 4) is orthogonal and will be handled separately.
19
+
20
+ ## The Inheritance Pattern
21
+
22
+ ### Self-Referential Instrument
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────┐
26
+ │ Instrument │
27
+ │ belongs_to :parent (optional) │
28
+ │ has_many :instrument_configurations │
29
+ │ │
30
+ │ Attributes resolve via parent chain: │
31
+ │ pitch_key || parent&.pitch_key │
32
+ └─────────────────────────────────────────────────────────┘
33
+ ```
34
+
35
+ ### Inheritance Examples
36
+
37
+ ```
38
+ trumpet (no parent)
39
+ ├── piccolo_trumpet (parent: trumpet, different range)
40
+ ├── bass_trumpet (parent: trumpet, different range)
41
+ └── pocket_trumpet (parent: trumpet, same attributes)
42
+
43
+ clarinet (no parent, pitch_key: b_flat)
44
+ ├── clarinet_in_a (parent: clarinet, pitch_key: a)
45
+ ├── clarinet_in_c (parent: clarinet, pitch_key: c)
46
+ ├── clarinet_in_e_flat (parent: clarinet, pitch_key: e_flat)
47
+ └── bass_clarinet (parent: clarinet, different range)
48
+ ```
49
+
50
+ ### Resolution Example
51
+
52
+ ```ruby
53
+ clarinet = Instrument.get("clarinet")
54
+ clarinet.pitch_key # => "b_flat"
55
+ clarinet.family_key # => "clarinet"
56
+
57
+ clarinet_in_a = Instrument.get("clarinet_in_a")
58
+ clarinet_in_a.pitch_key # => "a" (own attribute)
59
+ clarinet_in_a.family_key # => "clarinet" (from parent)
60
+ clarinet_in_a.parent # => clarinet
61
+ ```
62
+
63
+ ### Key Characteristics
64
+
65
+ 1. **Simple**: One class, one relationship, familiar pattern
66
+ 2. **Natural resolution**: `attribute || parent&.attribute`
67
+ 3. **Arbitrary depth**: Can model instrument families at any level
68
+ 4. **Data-driven**: YAML or database records, not code
69
+
70
+ ## Core Attributes
71
+
72
+ | Attribute | Type | Description |
73
+ |-----------|------|-------------|
74
+ | `name_key` | String | Primary identifier ("trumpet", "clarinet_in_a") |
75
+ | `alias_name_keys` | Array | Alternative names |
76
+ | `pitch_key` | String | The pitch designation ("b_flat", "a", "f") |
77
+ | `family_key` | String | Instrument family ("trumpet", "clarinet") |
78
+ | `orchestra_section_key` | String | Section ("brass", "woodwind", "strings") |
79
+ | `classification_keys` | Array | Hornbostel-Sachs style classifications |
80
+ | `default_clef_key` | String | Primary clef for this instrument |
81
+ | `range_low` | String | Lowest playable pitch |
82
+ | `range_high` | String | Highest playable pitch |
83
+
84
+ ## Configuration System
85
+
86
+ Instruments can have configurable options that modify their attributes when selected.
87
+
88
+ ### Structure
89
+
90
+ ```ruby
91
+ class Instrument
92
+ belongs_to :parent, class_name: "Instrument", optional: true
93
+ has_many :instrument_configurations # applies to self and descendants
94
+ end
95
+
96
+ class InstrumentConfiguration
97
+ belongs_to :instrument
98
+ has_many :instrument_configuration_options
99
+
100
+ # name_key: "leadpipe", "mute", "crook", "extension", "attachment"
101
+ end
102
+
103
+ class InstrumentConfigurationOption
104
+ belongs_to :instrument_configuration
105
+
106
+ # name_key: "a_leadpipe", "straight_mute", "f_crook"
107
+ # transposition_semitones: Integer (e.g., -1 for A leadpipe on Bb instrument)
108
+ # lowest_pitch_semitones: Integer (e.g., -4 for C extension on double bass)
109
+ end
110
+ ```
111
+
112
+ ### Configuration Examples
113
+
114
+ | Instrument | Configuration | Options |
115
+ |------------|---------------|---------|
116
+ | Piccolo trumpet | `leadpipe` | `b_flat` (default), `a` (transposition: -1) |
117
+ | Bass trombone | `f_attachment` | `disengaged`, `engaged` (lowest_pitch: -6) |
118
+ | Double bass | `c_extension` | `without`, `with` (lowest_pitch: -4) |
119
+ | Natural horn | `crook` | `e_flat`, `f`, `g`, etc. (various transpositions) |
120
+ | Trumpet | `mute` | `open`, `straight`, `cup`, `harmon` |
121
+ | Guitar | `capo` | `fret_0` through `fret_12` (transposition: 0-12) |
122
+
123
+ ### Configuration Resolution
124
+
125
+ - `instrument.instrument_configurations` walks parent chain, collects all
126
+ - No configuration selected = base instrument attributes unchanged
127
+ - Configuration selection lives in composition context (Part/Voice), not on Instrument
128
+ - A Part specifies target characteristics; configuration selection is derived/validated
129
+
130
+ ```ruby
131
+ # The instrument defines what CAN be configured
132
+ piccolo_trumpet = Instrument.get("piccolo_trumpet")
133
+ piccolo_trumpet.instrument_configurations # => [leadpipe config from parent chain]
134
+
135
+ # A Part specifies what characteristics are needed
136
+ part.instrument # => piccolo_trumpet
137
+ part.target_pitch_key # => "a"
138
+ part.effective_transposition # resolved via configuration options
139
+ ```
140
+
141
+ ## User Stories
142
+
143
+ ### STORY 1: Implement parent-based attribute resolution
144
+
145
+ AS a developer
146
+ WHEN I access an attribute on an Instrument
147
+ I WANT it resolved through the parent chain
148
+ SO THAT child instruments inherit from parents
149
+
150
+ **Acceptance criteria:**
151
+ - `instrument.pitch_key` returns own value or walks parent chain
152
+ - All core attributes support parent chain resolution
153
+ - Returns nil if no instrument in chain provides the attribute
154
+
155
+ ### STORY 2: Implement configuration inheritance
156
+
157
+ AS a developer
158
+ WHEN I query an instrument's available configurations
159
+ I WANT to get configurations from the entire parent chain
160
+ SO THAT child instruments inherit configurable options
161
+
162
+ **Acceptance criteria:**
163
+ - `instrument.instrument_configurations` collects from self and all ancestors
164
+ - Configurations defined on parent apply to all descendants
165
+ - Child can define additional configurations
166
+
167
+ ### STORY 3: Model configuration options with effects
168
+
169
+ AS a developer
170
+ WHEN I define configuration options
171
+ I WANT to specify their effects on instrument attributes
172
+ SO THAT transposition and range changes are calculable
173
+
174
+ **Acceptance criteria:**
175
+ - `InstrumentConfigurationOption` has `transposition_semitones` attribute
176
+ - `InstrumentConfigurationOption` has `lowest_pitch_semitones` attribute
177
+ - Effects are integers representing semitone adjustments
178
+
179
+ ### STORY 4: Migrate existing instrument data
180
+
181
+ AS a developer
182
+ WHEN I use the existing `Instrument.get()` API
183
+ I WANT it to work with the new inheritance architecture
184
+ SO THAT existing code continues to function
185
+
186
+ **Acceptance criteria:**
187
+ - `Instrument.get("trumpet")` returns instrument with no parent
188
+ - `Instrument.get("clarinet_in_a")` returns instrument with parent: clarinet
189
+ - All existing tests pass
190
+
191
+ ### STORY 5: Remove Variant class
192
+
193
+ AS a developer
194
+ WHEN pitched variants are modeled as child instruments
195
+ I WANT to remove the separate Variant class
196
+ SO THAT the model is simplified
197
+
198
+ **Acceptance criteria:**
199
+ - `Variant` class removed
200
+ - All pitched variants are now `Instrument` records with parents
201
+ - YAML structure updated to reflect inheritance
202
+
203
+ ## Migration Strategy
204
+
205
+ 1. **Phase 1**: Add `parent` relationship to Instrument class
206
+ 2. **Phase 2**: Implement parent chain attribute resolution
207
+ 3. **Phase 3**: Migrate YAML data to parent-based structure
208
+ 4. **Phase 4**: Create InstrumentConfiguration and InstrumentConfigurationOption classes
209
+ 5. **Phase 5**: Remove Variant class and update GenericInstrument
210
+ 6. **Phase 6**: Update all specs
211
+
212
+ ## Acceptance Criteria
213
+
214
+ - [x] `Instrument` class has `belongs_to :parent` relationship
215
+ - [x] Attributes resolve through parent chain
216
+ - [x] `Instrument#instrument_configurations` collects from ancestor chain
217
+ - [x] `InstrumentConfiguration` class exists with `name_key`
218
+ - [x] `InstrumentConfigurationOption` class exists with effect attributes
219
+ - [x] `Variant` class removed
220
+ - [x] YAML structure uses parent-based inheritance
221
+ - [x] `Instrument.get()` API preserved for backward compatibility
222
+ - [x] All existing tests pass
223
+ - [x] New tests cover inheritance and configuration
224
+ - [x] Maintains 90%+ test coverage
225
+
226
+ ## Deferred
227
+
228
+ - **Tunings**: String instruments need individual string modeling before alternate tunings can be represented
229
+ - **Notation**: Clef and transposition display conventions are orthogonal; see 002-notation-style.md
230
+
231
+ ## Open Questions
232
+
233
+ 1. **Multiple staves**: Piano needs two staves (grand staff). Is this an attribute on the instrument, or a notation concern?
234
+
235
+ 2. **Configuration selection in composition**: How does a Part specify which configuration options are selected? Options:
236
+ - Part stores selected options directly
237
+ - Part stores target attributes, system validates/suggests configurations
238
+ - Both (target attributes preferred, explicit selection as override)