head_music 9.0.1 → 11.1.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -3
  3. data/CHANGELOG.md +18 -0
  4. data/CLAUDE.md +35 -15
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +91 -3
  7. data/README.md +18 -0
  8. data/Rakefile +7 -2
  9. data/head_music.gemspec +1 -1
  10. data/lib/head_music/analysis/dyad.rb +229 -0
  11. data/lib/head_music/analysis/melodic_interval.rb +1 -1
  12. data/lib/head_music/analysis/pitch_class_set.rb +111 -14
  13. data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
  14. data/lib/head_music/analysis/sonority.rb +50 -12
  15. data/lib/head_music/content/cantus_firmus_examples.rb +58 -0
  16. data/lib/head_music/content/staff.rb +1 -1
  17. data/lib/head_music/content/voice.rb +1 -1
  18. data/lib/head_music/instruments/alternate_tuning.rb +102 -0
  19. data/lib/head_music/instruments/alternate_tunings.yml +78 -0
  20. data/lib/head_music/instruments/instrument.rb +251 -82
  21. data/lib/head_music/instruments/instrument_configuration.rb +66 -0
  22. data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
  23. data/lib/head_music/instruments/instrument_configurations.yml +288 -0
  24. data/lib/head_music/instruments/instrument_families.yml +77 -0
  25. data/lib/head_music/instruments/instrument_family.rb +3 -4
  26. data/lib/head_music/instruments/instruments.yml +795 -965
  27. data/lib/head_music/instruments/playing_technique.rb +75 -0
  28. data/lib/head_music/instruments/playing_techniques.yml +826 -0
  29. data/lib/head_music/instruments/score_order.rb +2 -5
  30. data/lib/head_music/instruments/staff.rb +61 -1
  31. data/lib/head_music/instruments/staff_scheme.rb +6 -4
  32. data/lib/head_music/instruments/stringing.rb +115 -0
  33. data/lib/head_music/instruments/stringing_course.rb +58 -0
  34. data/lib/head_music/instruments/stringings.yml +168 -0
  35. data/lib/head_music/instruments/variant.rb +0 -1
  36. data/lib/head_music/locales/de.yml +23 -0
  37. data/lib/head_music/locales/en.yml +100 -0
  38. data/lib/head_music/locales/es.yml +23 -0
  39. data/lib/head_music/locales/fr.yml +23 -0
  40. data/lib/head_music/locales/it.yml +23 -0
  41. data/lib/head_music/locales/ru.yml +23 -0
  42. data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
  43. data/lib/head_music/notation/staff_mapping.rb +70 -0
  44. data/lib/head_music/notation/staff_position.rb +62 -0
  45. data/lib/head_music/notation.rb +7 -0
  46. data/lib/head_music/rudiment/alteration.rb +17 -47
  47. data/lib/head_music/rudiment/alterations.yml +32 -0
  48. data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
  49. data/lib/head_music/rudiment/clef.rb +1 -1
  50. data/lib/head_music/rudiment/consonance.rb +14 -13
  51. data/lib/head_music/rudiment/key_signature.rb +0 -26
  52. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
  53. data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
  54. data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
  55. data/lib/head_music/rudiment/spelling.rb +3 -0
  56. data/lib/head_music/rudiment/tempo.rb +1 -1
  57. data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
  58. data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
  59. data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
  60. data/lib/head_music/rudiment/tuning.rb +20 -0
  61. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  62. data/lib/head_music/style/modern_tradition.rb +8 -11
  63. data/lib/head_music/style/tradition.rb +1 -1
  64. data/lib/head_music/time/clock_position.rb +84 -0
  65. data/lib/head_music/time/conductor.rb +264 -0
  66. data/lib/head_music/time/meter_event.rb +37 -0
  67. data/lib/head_music/time/meter_map.rb +173 -0
  68. data/lib/head_music/time/musical_position.rb +188 -0
  69. data/lib/head_music/time/smpte_timecode.rb +164 -0
  70. data/lib/head_music/time/tempo_event.rb +40 -0
  71. data/lib/head_music/time/tempo_map.rb +187 -0
  72. data/lib/head_music/time.rb +32 -0
  73. data/lib/head_music/utilities/case.rb +27 -0
  74. data/lib/head_music/utilities/hash_key.rb +1 -1
  75. data/lib/head_music/version.rb +1 -1
  76. data/lib/head_music.rb +42 -13
  77. data/user_stories/backlog/notation-style.md +183 -0
  78. data/user_stories/{todo → backlog}/organizing-content.md +9 -1
  79. data/user_stories/done/consonance-dissonance-classification.md +117 -0
  80. data/user_stories/{todo → done}/dyad-analysis.md +4 -6
  81. data/user_stories/done/expand-playing-techniques.md +38 -0
  82. data/user_stories/{active → done}/handle-time.rb +5 -19
  83. data/user_stories/done/instrument-architecture.md +238 -0
  84. data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
  85. data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
  86. data/user_stories/done/move-staff-position-to-notation.md +141 -0
  87. data/user_stories/done/notation-module-foundation.md +102 -0
  88. data/user_stories/done/percussion_set.md +260 -0
  89. data/user_stories/{todo → done}/pitch-class-set-analysis.md +0 -40
  90. data/user_stories/done/sonority-identification.md +37 -0
  91. data/user_stories/done/string-pitches.md +41 -0
  92. data/user_stories/epics/notation-module.md +135 -0
  93. data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
  94. metadata +56 -20
  95. data/check_instrument_consistency.rb +0 -0
  96. data/lib/head_music/instruments/instrument_type.rb +0 -188
  97. data/test_translations.rb +0 -15
  98. data/user_stories/todo/consonance-dissonance-classification.md +0 -57
  99. data/user_stories/todo/material-and-scores.md +0 -10
  100. data/user_stories/todo/percussion_set.md +0 -1
  101. data/user_stories/todo/pitch-set-classification.md +0 -72
  102. data/user_stories/todo/sonority-identification.md +0 -67
  103. /data/user_stories/{active → done}/handle-time.md +0 -0
@@ -0,0 +1,158 @@
1
+ # Move StaffMapping to Notation Module
2
+
3
+ AS a developer
4
+
5
+ I WANT StaffMapping in the HeadMusic::Notation module
6
+
7
+ SO THAT percussion notation mapping logic lives with visual notation concerns rather than instrument properties
8
+
9
+ ## Background
10
+
11
+ `StaffMapping` maps percussion instruments (and playing techniques) to specific positions on a staff. For example, in a standard drum kit staff:
12
+ - Bass drum → space below staff
13
+ - Snare drum → space 2
14
+ - Hi-hat → space 4
15
+ - Ride cymbal → top line
16
+
17
+ This is purely about visual notation - where on the staff each drum appears - not about the acoustic properties of the instruments themselves. The drum's sound (pitch range, timbre) is an instrument property, but where it appears on a staff is a notation convention.
18
+
19
+ Currently located in `HeadMusic::Instruments`, this class should move to `HeadMusic::Notation` to properly separate visual layout concerns from instrument acoustics.
20
+
21
+ ## Scenario: Mapping drums to staff positions
22
+
23
+ Given a drum kit with a snare drum
24
+
25
+ When I need to notate the snare on a staff
26
+
27
+ Then StaffMapping tells me it appears on space 2
28
+
29
+ And I can use that position for rendering
30
+
31
+ ## Scenario: Getting all mappings for an instrument
32
+
33
+ Given a drum kit instrument
34
+
35
+ When I access its staff mapping
36
+
37
+ Then I get all the drum-to-position mappings for that kit
38
+
39
+ And each mapping includes the instrument, playing technique, and staff position
40
+
41
+ ## Scenario: Supporting different percussion notation schemes
42
+
43
+ Given different percussion instruments use different staff conventions
44
+
45
+ When I create a StaffMapping for a specific scheme (e.g., "standard drum kit")
46
+
47
+ Then instruments are mapped to their conventional positions for that notation style
48
+
49
+ ## Technical Notes
50
+
51
+ ### Current State
52
+
53
+ **Location:** `lib/head_music/instruments/staff_mapping.rb`
54
+ **Class:** `HeadMusic::Instruments::StaffMapping`
55
+ **Tests:** `spec/head_music/instruments/staff_mapping_spec.rb`
56
+ **Used by:**
57
+ - `lib/head_music/instruments/staff.rb`
58
+ - `lib/head_music/instruments/staff_scheme.rb`
59
+ - Percussion instrument configurations
60
+
61
+ ### Proposed Changes
62
+
63
+ 1. **Move file:**
64
+ - From: `lib/head_music/instruments/staff_mapping.rb`
65
+ - To: `lib/head_music/notation/staff_mapping.rb`
66
+
67
+ 2. **Update class definition:**
68
+ ```ruby
69
+ # lib/head_music/notation/staff_mapping.rb
70
+ module HeadMusic::Notation; end
71
+
72
+ class HeadMusic::Notation::StaffMapping
73
+ attr_reader :instrument, :playing_technique, :staff_position
74
+
75
+ def initialize(instrument:, playing_technique: nil, staff_position:)
76
+ @instrument = instrument
77
+ @playing_technique = playing_technique
78
+ @staff_position = HeadMusic::Notation::StaffPosition.new(staff_position)
79
+ end
80
+
81
+ # ... rest of class unchanged
82
+ end
83
+ ```
84
+
85
+ 3. **Move spec file:**
86
+ - From: `spec/head_music/instruments/staff_mapping_spec.rb`
87
+ - To: `spec/head_music/notation/staff_mapping_spec.rb`
88
+
89
+ 4. **Update spec:**
90
+ ```ruby
91
+ describe HeadMusic::Notation::StaffMapping do
92
+ # All tests remain unchanged except the describe statement
93
+ ```
94
+
95
+ 5. **Update references:**
96
+ - `lib/head_music/instruments/staff.rb` - Update StaffMapping references
97
+ - `lib/head_music/instruments/staff_scheme.rb` - Update StaffMapping references
98
+ - Instrument YAML files if they reference the class directly
99
+
100
+ 6. **Update loading:**
101
+ ```ruby
102
+ # lib/head_music/notation.rb
103
+ module HeadMusic::Notation; end
104
+
105
+ require "head_music/notation/staff_position"
106
+ require "head_music/notation/musical_symbol"
107
+ require "head_music/notation/staff_mapping"
108
+ ```
109
+
110
+ ### Files to Update
111
+
112
+ - Move: `lib/head_music/instruments/staff_mapping.rb` → `lib/head_music/notation/staff_mapping.rb`
113
+ - Move: `spec/head_music/instruments/staff_mapping_spec.rb` → `spec/head_music/notation/staff_mapping_spec.rb`
114
+ - Update: `lib/head_music/notation.rb` (add require)
115
+ - Update: `lib/head_music/instruments/staff.rb` (update references)
116
+ - Update: `lib/head_music/instruments/staff_scheme.rb` (update references)
117
+ - Remove: `lib/head_music/instruments.rb` require for staff_mapping
118
+
119
+ ### Dependencies
120
+
121
+ This story depends on `StaffPosition` already being in the Notation module, since `StaffMapping` uses `StaffPosition`. Recommended to complete the "Move StaffPosition to Notation" story first.
122
+
123
+ ## Acceptance Criteria
124
+
125
+ - [ ] `HeadMusic::Notation::StaffMapping` class exists
126
+ - [ ] Original file `lib/head_music/instruments/staff_mapping.rb` removed
127
+ - [ ] Spec file at `spec/head_music/notation/staff_mapping_spec.rb`
128
+ - [ ] All existing StaffMapping tests pass
129
+ - [ ] StaffMapping correctly references `Notation::StaffPosition`
130
+ - [ ] References in `Instruments::Staff` updated and working
131
+ - [ ] References in `Instruments::StaffScheme` updated and working
132
+ - [ ] `lib/head_music/notation.rb` requires staff_mapping
133
+ - [ ] `lib/head_music/instruments.rb` no longer requires staff_mapping
134
+ - [ ] All percussion instrument tests still pass
135
+ - [ ] All existing tests across entire codebase still pass
136
+ - [ ] Maintains 90%+ test coverage
137
+ - [ ] No deprecation warnings or breaking changes for internal usage
138
+
139
+ ## Implementation Steps
140
+
141
+ 1. Verify `StaffPosition` is already in `HeadMusic::Notation`
142
+ 2. Create `lib/head_music/notation/staff_mapping.rb` with updated module path
143
+ 3. Update StaffPosition reference to use `Notation::StaffPosition`
144
+ 4. Copy rest of class implementation unchanged
145
+ 5. Create `spec/head_music/notation/staff_mapping_spec.rb`
146
+ 6. Update describe statement in spec
147
+ 7. Update `lib/head_music/notation.rb` to require staff_mapping
148
+ 8. Update references in `lib/head_music/instruments/staff.rb`
149
+ 9. Update references in `lib/head_music/instruments/staff_scheme.rb`
150
+ 10. Remove require from `lib/head_music/instruments.rb`
151
+ 11. Run tests: `bundle exec rspec spec/head_music/notation/staff_mapping_spec.rb`
152
+ 12. Run tests: `bundle exec rspec spec/head_music/instruments/staff_spec.rb`
153
+ 13. Run tests: `bundle exec rspec spec/head_music/instruments/staff_scheme_spec.rb`
154
+ 14. Run percussion-related tests
155
+ 15. Run full test suite: `bundle exec rspec`
156
+ 16. Run linter: `bundle exec rubocop -a`
157
+ 17. Delete original files after verifying everything works
158
+ 18. Verify 90%+ coverage maintained
@@ -0,0 +1,141 @@
1
+ # Move StaffPosition to Notation Module
2
+
3
+ AS a developer
4
+
5
+ I WANT StaffPosition in the HeadMusic::Notation module
6
+
7
+ SO THAT staff positioning logic lives with other visual notation concerns rather than instrument properties
8
+
9
+ ## Background
10
+
11
+ `StaffPosition` represents positions on a 5-line musical staff (lines, spaces, and ledger lines). It's a pure visual notation concept with no connection to instrument acoustical properties.
12
+
13
+ Currently located in `HeadMusic::Instruments`, it has a TODO comment (line 5) suggesting it should move to a HeadMusic::Notation module.
14
+
15
+ The class:
16
+ - Maps integer indices to staff positions (even = lines, odd = spaces)
17
+ - Handles positions within the staff (0-8) and ledger lines above/below
18
+ - Provides named positions ("bottom line", "middle line", "top line", etc.)
19
+ - Has comprehensive test coverage (264 lines of tests)
20
+
21
+ This move resolves the TODO and establishes StaffPosition as the first concrete class in the Notation module.
22
+
23
+ ## Scenario: Accessing staff positions for rendering
24
+
25
+ Given I need to position a note on a musical staff
26
+
27
+ When I use `HeadMusic::Notation::StaffPosition.new(4)`
28
+
29
+ Then I get the middle line staff position
30
+
31
+ And I can query whether it's a line or space
32
+
33
+ And I can get its display name
34
+
35
+ ## Scenario: Using named staff positions
36
+
37
+ Given I need to reference a specific staff position
38
+
39
+ When I use `HeadMusic::Notation::StaffPosition.name_to_index("top line")`
40
+
41
+ Then I get the index 8
42
+
43
+ And I can create a StaffPosition from that index
44
+
45
+ ## Scenario: Handling ledger lines
46
+
47
+ Given I need a position above or below the standard staff
48
+
49
+ When I create `HeadMusic::Notation::StaffPosition.new(10)`
50
+
51
+ Then I get "ledger line above staff"
52
+
53
+ And when I create `HeadMusic::Notation::StaffPosition.new(-2)`
54
+
55
+ Then I get "ledger line below staff"
56
+
57
+ ## Technical Notes
58
+
59
+ ### Current State
60
+
61
+ **Location:** `lib/head_music/instruments/staff_position.rb`
62
+ **Class:** `HeadMusic::Instruments::StaffPosition`
63
+ **Tests:** `spec/head_music/instruments/staff_position_spec.rb` (264 lines)
64
+ **Dependencies:** Used by `Clef` for pitch-to-staff mapping
65
+
66
+ ### Proposed Changes
67
+
68
+ 1. **Move file:**
69
+ - From: `lib/head_music/instruments/staff_position.rb`
70
+ - To: `lib/head_music/notation/staff_position.rb`
71
+
72
+ 2. **Update class definition:**
73
+ ```ruby
74
+ # lib/head_music/notation/staff_position.rb
75
+ module HeadMusic::Notation; end
76
+
77
+ class HeadMusic::Notation::StaffPosition
78
+ # Remove TODO comment about moving to Notation module
79
+ # ... rest of class unchanged
80
+ ```
81
+
82
+ 3. **Move spec file:**
83
+ - From: `spec/head_music/instruments/staff_position_spec.rb`
84
+ - To: `spec/head_music/notation/staff_position_spec.rb`
85
+
86
+ 4. **Update spec:**
87
+ ```ruby
88
+ describe HeadMusic::Notation::StaffPosition do
89
+ # All tests remain unchanged except the describe statement
90
+ ```
91
+
92
+ 5. **Update references:**
93
+ - `lib/head_music/rudiment/clef.rb` - Update StaffPosition references
94
+ - `lib/head_music/notation.rb` - Add require for staff_position
95
+
96
+ 6. **Update loading:**
97
+ ```ruby
98
+ # lib/head_music/notation.rb
99
+ module HeadMusic::Notation; end
100
+
101
+ require "head_music/notation/staff_position"
102
+ ```
103
+
104
+ ### Files to Update
105
+
106
+ - Move: `lib/head_music/instruments/staff_position.rb` → `lib/head_music/notation/staff_position.rb`
107
+ - Move: `spec/head_music/instruments/staff_position_spec.rb` → `spec/head_music/notation/staff_position_spec.rb`
108
+ - Update: `lib/head_music/notation.rb` (add require)
109
+ - Update: `lib/head_music/rudiment/clef.rb` (update references)
110
+ - Remove: `lib/head_music/instruments.rb` require for staff_position
111
+ - Update: CLAUDE.md (note StaffPosition location)
112
+
113
+ ## Acceptance Criteria
114
+
115
+ - [ ] `HeadMusic::Notation::StaffPosition` class exists
116
+ - [ ] Original file `lib/head_music/instruments/staff_position.rb` removed
117
+ - [ ] Spec file at `spec/head_music/notation/staff_position_spec.rb`
118
+ - [ ] All 264 lines of existing tests pass unchanged (except describe statement)
119
+ - [ ] TODO comment removed from class
120
+ - [ ] All references in `Clef` updated and working
121
+ - [ ] `lib/head_music/notation.rb` requires staff_position
122
+ - [ ] `lib/head_music/instruments.rb` no longer requires staff_position
123
+ - [ ] All existing tests across entire codebase still pass
124
+ - [ ] Maintains 90%+ test coverage
125
+ - [ ] No deprecation warnings or breaking changes for internal usage
126
+
127
+ ## Implementation Steps
128
+
129
+ 1. Create `lib/head_music/notation/staff_position.rb` with updated module path
130
+ 2. Copy class implementation, removing TODO comment
131
+ 3. Create `spec/head_music/notation/staff_position_spec.rb`
132
+ 4. Update describe statement in spec
133
+ 5. Update `lib/head_music/notation.rb` to require staff_position
134
+ 6. Update references in `lib/head_music/rudiment/clef.rb`
135
+ 7. Remove require from `lib/head_music/instruments.rb`
136
+ 8. Run tests: `bundle exec rspec spec/head_music/notation/staff_position_spec.rb`
137
+ 9. Run tests: `bundle exec rspec spec/head_music/rudiment/clef_spec.rb`
138
+ 10. Run full test suite: `bundle exec rspec`
139
+ 11. Run linter: `bundle exec rubocop -a`
140
+ 12. Delete original files after verifying everything works
141
+ 13. Verify 90%+ coverage maintained
@@ -0,0 +1,102 @@
1
+ # Notation Module Foundation
2
+
3
+ AS a developer
4
+
5
+ I WANT a dedicated HeadMusic::Notation module structure
6
+
7
+ SO THAT I have a clear, organized place for notation-related features separate from music theory, instrument properties, and musical content
8
+
9
+ ## Background
10
+
11
+ The HeadMusic gem currently organizes code into four main modules:
12
+ - **Rudiment** - Abstract music theory concepts (pitch, interval, scale, chord)
13
+ - **Instruments** - Instrument properties (range, transposition, families)
14
+ - **Content** - Musical compositions and temporal organization
15
+ - **Analysis** - Music analysis tools
16
+
17
+ However, there's no dedicated module for visual notation or representational concerns. Notation-related code is scattered across Instruments and Rudiment, making it unclear where notation features should live.
18
+
19
+ A TODO comment in `lib/head_music/instruments/staff_position.rb:5` suggests creating a HeadMusic::Notation module. Before moving any classes, we need the foundational infrastructure in place.
20
+
21
+ ## Scenario: Loading the notation module
22
+
23
+ Given the HeadMusic gem is loaded
24
+
25
+ When I require 'head_music'
26
+
27
+ Then the HeadMusic::Notation module should be available
28
+
29
+ And it should not break any existing functionality
30
+
31
+ ## Scenario: Documenting module boundaries
32
+
33
+ Given a developer wants to add notation features
34
+
35
+ When they consult CLAUDE.md
36
+
37
+ Then they should see clear guidance on what belongs in Notation vs Rudiment vs Instruments vs Content
38
+
39
+ ## Technical Notes
40
+
41
+ ### Module Boundaries
42
+
43
+ **HeadMusic::Notation** (visual representation):
44
+ - Staff positions, lines, spaces, ledger lines
45
+ - Musical symbols (ASCII, Unicode, HTML entities)
46
+ - Clef placement and rendering
47
+ - Notehead shapes, stems, flags, beams
48
+ - Accidental placement rules
49
+ - Future: ties, slurs, articulations, dynamics
50
+
51
+ **HeadMusic::Rudiment** (music theory):
52
+ - Abstract concepts: pitch, interval, scale, chord
53
+ - Duration concepts (without visual representation)
54
+ - Keys, meters (as musical concepts)
55
+
56
+ **HeadMusic::Instruments** (instrument properties):
57
+ - Instrument families and classification
58
+ - Pitch ranges and transposition
59
+ - Playing techniques
60
+ - Score ordering
61
+
62
+ **HeadMusic::Content** (musical content):
63
+ - Compositions, voices, bars
64
+ - Notes in context (pitch + duration + placement)
65
+ - Temporal organization
66
+
67
+ ### Implementation
68
+
69
+ Create minimal module infrastructure:
70
+
71
+ **File: `lib/head_music/notation.rb`**
72
+ ```ruby
73
+ # A module for visual music notation
74
+ module HeadMusic::Notation; end
75
+
76
+ # Load notation classes
77
+ # (Initially empty - classes will be added in subsequent stories)
78
+ ```
79
+
80
+ **Update: `lib/head_music.rb`**
81
+ ```ruby
82
+ # Add after Content module loading:
83
+ require "head_music/notation"
84
+ ```
85
+
86
+ **Update: `CLAUDE.md`**
87
+ Add section documenting the Notation module and its boundaries.
88
+
89
+ ## Acceptance Criteria
90
+
91
+ - [ ] `lib/head_music/notation/` directory exists
92
+ - [ ] `lib/head_music/notation.rb` file exists and defines module
93
+ - [ ] `lib/head_music.rb` requires the notation module
94
+ - [ ] `HeadMusic::Notation` module is accessible after requiring 'head_music'
95
+ - [ ] All existing tests pass without modification
96
+ - [ ] Maintains 90%+ test coverage
97
+ - [ ] CLAUDE.md updated with Notation module boundaries
98
+ - [ ] No existing functionality broken
99
+
100
+ ## Implementation Notes
101
+
102
+ This is pure infrastructure - no classes are moved or created yet. This provides the foundation for subsequent stories that will move StaffPosition, MusicalSymbol, and StaffMapping into the Notation module.
@@ -0,0 +1,260 @@
1
+ # Percussion Set Staff
2
+
3
+ As a composer or arranger
4
+
5
+ I want to create percussion set or drum kit parts in a score
6
+
7
+ So that I can notate multiple unpitched percussion instruments on a single staff
8
+
9
+ ## Background
10
+
11
+ Individual percussion instruments (timpani, snare_drum, bass_drum, etc.) already exist in the codebase and use the `neutral_clef` (also known as percussion_clef). However, there's no way to represent a **percussion set** or **drum kit** where multiple unpitched instruments are notated on a single staff with each instrument assigned to a specific line or space.
12
+
13
+ This is distinct from a clef. The `neutral_clef` already exists in `HeadMusic::Rudiment::Clef`. What's needed is:
14
+ 1. A new instrument definition for percussion sets (e.g., `drum_kit`, `percussion_set`)
15
+ 2. A staff scheme that uses the `neutral_clef`
16
+ 3. A percussion mapping system that defines which instruments appear on which staff lines/spaces
17
+
18
+ ## Scenario: Create a standard drum kit staff
19
+
20
+ Given I am composing a piece with drum kit
21
+
22
+ When I create a drum_kit instrument
23
+
24
+ Then it should use the neutral_clef
25
+
26
+ And it should have a default percussion mapping for standard drum kit instruments
27
+
28
+ ## Scenario: Map percussion instruments to staff positions
29
+
30
+ Given I have a drum_kit instrument
31
+
32
+ When I request the percussion mapping
33
+
34
+ Then I should see which line or space each percussion instrument occupies
35
+
36
+ Examples:
37
+ - Crash cymbal → line 5
38
+ - Hi-hat → line 4 (or space above)
39
+ - Snare drum → line 3 (center line)
40
+ - Bass drum → line 1 (or space below)
41
+ - Floor tom → line 2
42
+
43
+ ## Scenario: Create custom percussion set configurations
44
+
45
+ Given I want to notate a non-standard percussion ensemble
46
+
47
+ When I define a custom percussion_set
48
+
49
+ Then I should be able to specify which percussion instruments map to which staff positions
50
+
51
+ And the system should use the neutral_clef for the staff
52
+
53
+ ## Technical Notes
54
+
55
+ ### Architecture
56
+
57
+ A percussion set is:
58
+ - **NOT a clef** - it uses the existing `neutral_clef`
59
+ - **An instrument** with a special staff scheme configuration
60
+ - **A mapping system** that assigns percussion instruments to staff positions
61
+
62
+ ### Proposed Implementation
63
+
64
+ Add to `lib/head_music/instruments/instruments.yml`:
65
+
66
+ ```yaml
67
+ drum_kit:
68
+ family_key: percussion_set
69
+ variants:
70
+ default:
71
+ staff_schemes:
72
+ default:
73
+ - clef: neutral_clef
74
+ percussion_mapping:
75
+ line_5: crash_cymbal
76
+ space_4: ride_cymbal
77
+ line_4: hi_hat
78
+ space_3: high_tom
79
+ line_3: snare_drum
80
+ space_2: mid_tom
81
+ line_2: floor_tom
82
+ space_1: bass_drum
83
+ line_1: bass_drum
84
+
85
+ percussion_set:
86
+ family_key: percussion_set
87
+ variants:
88
+ default:
89
+ staff_schemes:
90
+ default:
91
+ - clef: neutral_clef
92
+ ```
93
+
94
+ ### New Concepts to Implement
95
+
96
+ 1. **Percussion mapping attribute** on `HeadMusic::Instruments::Staff`
97
+ 2. **Percussion family or category** to identify instruments as unpitched/percussion
98
+ 3. **Position resolution** to determine which line/space a percussion instrument should use
99
+
100
+ ### Related Components
101
+
102
+ - `HeadMusic::Rudiment::Clef` - neutral_clef already exists (line 115-128 in clefs.yml)
103
+ - `HeadMusic::Instruments::Staff` - would need to handle percussion_mapping attribute
104
+ - `HeadMusic::Instruments::StaffScheme` - provides the staff configuration
105
+ - Individual percussion instruments (timpani, snare_drum, etc.) already exist
106
+
107
+ ## Acceptance Criteria
108
+
109
+ - Can create a drum_kit instrument
110
+ - drum_kit uses neutral_clef by default
111
+ - Can query which percussion instrument is assigned to each staff position
112
+ - Can create custom percussion_set configurations with custom mappings
113
+ - Maintains 90%+ test coverage
114
+ - Follows existing HeadMusic patterns (factory methods, YAML data-driven)
115
+
116
+ ## Implementation Plan
117
+
118
+ ### Design Decisions
119
+
120
+ Based on research and discussion:
121
+
122
+ 1. **Naming**: Use `instrument_mapping` (not `percussion_mapping`) for flexibility
123
+ 2. **Mapping Format**: Use instrument keys (strings/symbols), not objects
124
+ 3. **Validation**:
125
+ - Mapped instruments must exist
126
+ - No requirement that they be percussion instruments
127
+ - Valid positions: `line_0` through `line_6`, `space_0` through `space_5`
128
+ 4. **Flexibility**: Mappings are immutable (YAML-defined only) for now
129
+ 5. **Stem Direction**: Rendering concern, not part of infrastructure
130
+ 6. **Composite Instruments**: No special type needed - just regular instruments with `instrument_mapping`
131
+ 7. **Family**: Use `drum_kit` family (not generic `percussion_set`)
132
+
133
+ ### Standard Drum Kit Notation Mapping
134
+
135
+ Research shows there is no single universal standard, but most common convention:
136
+
137
+ ```yaml
138
+ instrument_mapping:
139
+ space_0: hi_hat_pedal # below staff
140
+ line_1: bass_drum
141
+ line_2: floor_tom
142
+ line_3: snare_drum # middle line - most consistent
143
+ line_4: mid_tom
144
+ space_4: high_tom
145
+ line_5: ride_cymbal
146
+ space_5: hi_hat # above staff
147
+ line_6: crash_cymbal # first ledger line above
148
+ ```
149
+
150
+ ### Implementation Steps
151
+
152
+ #### 1. Add New Instrument Families
153
+ To `lib/head_music/instruments/instrument_families.yml`:
154
+ - `tom_tom` family
155
+ - `hi_hat` family
156
+ - `drum_kit` family
157
+
158
+ #### 2. Add New Individual Instruments
159
+ To `lib/head_music/instruments/instruments.yml`:
160
+ - `hi_hat` (family: hi_hat)
161
+ - `hi_hat_pedal` (family: hi_hat)
162
+ - `crash_cymbal` (family: cymbal)
163
+ - `ride_cymbal` (family: cymbal)
164
+ - `high_tom` (family: tom_tom)
165
+ - `mid_tom` (family: tom_tom)
166
+ - `floor_tom` (family: tom_tom)
167
+
168
+ Each with:
169
+ - Appropriate aliases
170
+ - `family_key` reference
171
+ - Single `default` variant
172
+ - `neutral_clef` staff scheme
173
+
174
+ #### 3. Add Composite Instrument
175
+ To `lib/head_music/instruments/instruments.yml`:
176
+ - `drum_kit` (alias: `drum_set`) with standard instrument_mapping
177
+
178
+ #### 4. Enhance Staff Class
179
+ To `lib/head_music/instruments/staff.rb`:
180
+ - Add `instrument_mapping` attribute (hash)
181
+ - Add `#instrument_for_position(position_key)` method
182
+ - Add `#position_for_instrument(instrument_name)` method
183
+ - Add `#components` method (derives instruments from mapping)
184
+ - Handle parsing `instrument_mapping` from YAML
185
+ - Validate:
186
+ - Position keys match pattern (line_0-6, space_0-5)
187
+ - Referenced instruments exist
188
+
189
+ #### 5. Tests
190
+ - Specs for all new instrument families
191
+ - Specs for all new individual instruments
192
+ - Specs for `Staff#instrument_mapping` functionality
193
+ - Specs for `drum_kit` composite instrument
194
+ - Maintain 90%+ code coverage
195
+
196
+ ---
197
+
198
+ ## Refactoring Notes (Post-Implementation)
199
+
200
+ After the initial implementation, the design was refactored to better model playing techniques as a first-class concept rather than treating them as separate instruments.
201
+
202
+ ### Key Changes:
203
+
204
+ 1. **Introduced `PlayingTechnique` class**
205
+ - Playing techniques (pedal, stick, mallet, etc.) are now modeled as objects
206
+ - Includes `HeadMusic::Named` for potential future i18n support
207
+ - Common techniques defined: stick, pedal, mallet, hand, brush, rim_shot, cross_stick, open, closed, etc.
208
+
209
+ 2. **Created `StaffMapping` class** (replaces simple hash-based approach)
210
+ - Represents the mapping of an instrument + optional playing technique to a staff position
211
+ - Uses `StaffPosition` with index-based positions (even = lines, odd = spaces)
212
+ - Attributes: `staff_position`, `instrument_key`, `playing_technique_key`
213
+ - Methods: `#instrument`, `#playing_technique`, `#position_index`, `#to_s`
214
+
215
+ 3. **Removed `hi_hat_pedal` as separate instrument**
216
+ - Hi-hat pedal is now correctly modeled as a playing technique of the hi-hat instrument
217
+ - The hi-hat appears at two positions in drum_kit mapping:
218
+ - Position -1 (space below staff): hi_hat with pedal technique
219
+ - Position 9 (space above staff): hi_hat with stick technique
220
+
221
+ 4. **Updated `Staff` class API**
222
+ - `#mappings` → returns array of `StaffMapping` objects
223
+ - `#mapping_for_position(index)` → get full mapping at position
224
+ - `#instrument_for_position(index)` → get instrument at position
225
+ - `#positions_for_instrument(key)` → returns all positions (handles multiple mappings)
226
+ - `#components` → returns unique instruments
227
+
228
+ 5. **Changed YAML format** from hash to array:
229
+ ```yaml
230
+ # Old format:
231
+ instrument_mapping:
232
+ line_3: snare_drum
233
+ space_5: hi_hat
234
+
235
+ # New format:
236
+ mappings:
237
+ - staff_position: 4 # index 4 = line 3
238
+ instrument: snare_drum
239
+ - staff_position: 9 # index 9 = space above staff
240
+ instrument: hi_hat
241
+ playing_technique: stick
242
+ ```
243
+
244
+ 6. **Added comprehensive YARD documentation**
245
+ - All new classes and public methods documented
246
+ - Examples provided for common use cases
247
+
248
+ ### Benefits of Refactoring:
249
+
250
+ - **Semantic clarity**: Hi-hat pedal is a technique, not an instrument
251
+ - **Extensibility**: Easy to add new playing techniques (bow techniques for strings, breath techniques for winds, etc.)
252
+ - **Flexibility**: Instruments can appear at multiple positions with different techniques
253
+ - **Type safety**: StaffPosition validates position indices, StaffMapping provides structured access
254
+
255
+ ### Test Results:
256
+
257
+ - **1040 examples, 0 failures** ✅
258
+ - **91.54% line coverage, 84.44% branch coverage** ✅
259
+ - All existing functionality preserved
260
+ - New tests verify multiple technique mappings