head_music 8.3.0 → 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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/CLAUDE.md +32 -15
  4. data/Gemfile.lock +1 -1
  5. data/MUSIC_THEORY.md +120 -0
  6. data/lib/head_music/analysis/diatonic_interval.rb +29 -27
  7. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  8. data/lib/head_music/content/note.rb +1 -1
  9. data/lib/head_music/content/placement.rb +1 -1
  10. data/lib/head_music/content/position.rb +1 -1
  11. data/lib/head_music/content/staff.rb +1 -1
  12. data/lib/head_music/instruments/instrument.rb +103 -113
  13. data/lib/head_music/instruments/instrument_family.rb +13 -2
  14. data/lib/head_music/instruments/instrument_type.rb +188 -0
  15. data/lib/head_music/instruments/score_order.rb +139 -0
  16. data/lib/head_music/instruments/score_orders.yml +130 -0
  17. data/lib/head_music/instruments/variant.rb +6 -0
  18. data/lib/head_music/locales/de.yml +6 -0
  19. data/lib/head_music/locales/en.yml +6 -0
  20. data/lib/head_music/locales/es.yml +6 -0
  21. data/lib/head_music/locales/fr.yml +6 -0
  22. data/lib/head_music/locales/it.yml +6 -0
  23. data/lib/head_music/locales/ru.yml +6 -0
  24. data/lib/head_music/rudiment/alteration.rb +23 -8
  25. data/lib/head_music/rudiment/base.rb +9 -0
  26. data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
  27. data/lib/head_music/rudiment/clef.rb +1 -1
  28. data/lib/head_music/rudiment/consonance.rb +37 -4
  29. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  30. data/lib/head_music/rudiment/key.rb +77 -0
  31. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  32. data/lib/head_music/rudiment/key_signature.rb +46 -7
  33. data/lib/head_music/rudiment/letter_name.rb +3 -3
  34. data/lib/head_music/rudiment/meter.rb +19 -9
  35. data/lib/head_music/rudiment/mode.rb +92 -0
  36. data/lib/head_music/rudiment/musical_symbol.rb +1 -1
  37. data/lib/head_music/rudiment/note.rb +112 -0
  38. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  39. data/lib/head_music/rudiment/pitch.rb +5 -6
  40. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  41. data/lib/head_music/rudiment/quality.rb +1 -1
  42. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  43. data/lib/head_music/rudiment/register.rb +4 -1
  44. data/lib/head_music/rudiment/rest.rb +36 -0
  45. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  46. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  47. data/lib/head_music/rudiment/rhythmic_unit.rb +13 -5
  48. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  49. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  50. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  51. data/lib/head_music/rudiment/scale.rb +4 -5
  52. data/lib/head_music/rudiment/scale_degree.rb +1 -1
  53. data/lib/head_music/rudiment/scale_type.rb +9 -3
  54. data/lib/head_music/rudiment/solmization.rb +1 -1
  55. data/lib/head_music/rudiment/spelling.rb +5 -4
  56. data/lib/head_music/rudiment/tempo.rb +85 -0
  57. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  58. data/lib/head_music/rudiment/tuning.rb +1 -1
  59. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  60. data/lib/head_music/style/medieval_tradition.rb +26 -0
  61. data/lib/head_music/style/modern_tradition.rb +34 -0
  62. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  63. data/lib/head_music/style/tradition.rb +21 -0
  64. data/lib/head_music/utilities/hash_key.rb +34 -2
  65. data/lib/head_music/version.rb +1 -1
  66. data/lib/head_music.rb +31 -10
  67. data/user_stories/active/handle-time.md +7 -0
  68. data/user_stories/active/handle-time.rb +177 -0
  69. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  70. data/user_stories/done/instrument-variant.md +65 -0
  71. data/user_stories/done/superclass-for-note.md +30 -0
  72. data/user_stories/todo/agentic-daw.md +3 -0
  73. data/user_stories/{backlog → todo}/dyad-analysis.md +2 -10
  74. data/user_stories/todo/material-and-scores.md +10 -0
  75. data/user_stories/todo/organizing-content.md +72 -0
  76. data/user_stories/todo/percussion_set.md +1 -0
  77. data/user_stories/{backlog → todo}/pitch-class-set-analysis.md +40 -0
  78. data/user_stories/{backlog → todo}/pitch-set-classification.md +10 -0
  79. data/user_stories/{backlog → todo}/sonority-identification.md +20 -0
  80. metadata +43 -12
  81. data/TODO.md +0 -109
  82. /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
  83. /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
  84. /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
  85. /data/user_stories/{backlog → todo}/consonance-dissonance-classification.md +0 -0
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,40 +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"
59
74
  require "head_music/rudiment/tuning/just_intonation"
60
- require "head_music/rudiment/tuning/pythagorean"
61
75
  require "head_music/rudiment/tuning/meantone"
76
+ require "head_music/rudiment/tuning/pythagorean"
62
77
 
63
78
  # instruments
64
79
  require "head_music/instruments/instrument_family"
65
- require "head_music/instruments/instrument"
80
+ require "head_music/instruments/instrument_type"
81
+ require "head_music/instruments/variant"
66
82
  require "head_music/instruments/staff_scheme"
67
83
  require "head_music/instruments/staff"
68
- require "head_music/instruments/variant"
84
+ require "head_music/instruments/instrument"
85
+ require "head_music/instruments/score_order"
69
86
 
70
87
  # content
71
88
  require "head_music/content/bar"
@@ -73,7 +90,6 @@ require "head_music/content/composition"
73
90
  require "head_music/content/note"
74
91
  require "head_music/content/placement"
75
92
  require "head_music/content/position"
76
- require "head_music/content/rhythmic_value"
77
93
  require "head_music/content/staff"
78
94
  require "head_music/content/voice"
79
95
 
@@ -86,6 +102,7 @@ require "head_music/analysis/diatonic_interval/parser"
86
102
  require "head_music/analysis/diatonic_interval/semitones"
87
103
  require "head_music/analysis/diatonic_interval/size"
88
104
  require "head_music/analysis/harmonic_interval"
105
+ require "head_music/analysis/interval_consonance"
89
106
  require "head_music/analysis/interval_cycle"
90
107
  require "head_music/analysis/melodic_interval"
91
108
  require "head_music/analysis/motion"
@@ -94,6 +111,10 @@ require "head_music/analysis/pitch_set"
94
111
  require "head_music/analysis/sonority"
95
112
 
96
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"
97
118
  require "head_music/style/analysis"
98
119
  require "head_music/style/annotation"
99
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,65 @@
1
+ ## Instrument Variant Refactoring
2
+
3
+ ### Background
4
+ The current architecture conflates instrument catalog data with specific instrument instances. We need to separate these concerns to better represent how instruments are actually used in musical scores.
5
+
6
+ ### Hierarchy
7
+ The refactored hierarchy will provide three levels of abstraction:
8
+ - **InstrumentFamily** (existing): Broad category like "saxophone" or "trumpet"
9
+ - **InstrumentType** (renamed from Instrument): Catalog entry with all possible variants (e.g., "trumpet" which can be in Bb, C, D, Eb)
10
+ - **Instrument** (new): Specific instance with selected variant (e.g., "Trumpet in C")
11
+
12
+ ### User Stories
13
+
14
+ **STORY 1: Rename Instrument to InstrumentType**
15
+ AS a developer
16
+ WHEN I want to access instrument catalog data
17
+ I WANT to use InstrumentType.get("trumpet")
18
+ SO THAT it's clear I'm getting a type definition, not a specific instrument instance
19
+
20
+ **STORY 2: Create new Instrument class for specific variants**
21
+ AS a developer
22
+ WHEN I need a specific instrument for a score
23
+ I WANT to call Instrument.get("trumpet_in_c") or Instrument.get("trumpet", "in_c")
24
+ SO THAT I get a specific, usable instrument instance with proper transposition
25
+
26
+ **STORY 3: Instrument instances are sortable**
27
+ AS a developer
28
+ WHEN I have multiple Instrument instances in a score
29
+ I WANT them to sort properly by orchestral order and transposition
30
+ SO THAT "Trumpet in Eb" appears before "Trumpet in C" in the score
31
+
32
+ **STORY 4: Clear API for common use cases**
33
+ AS a developer
34
+ WHEN I create an Instrument without specifying a variant
35
+ I WANT to get the default variant automatically
36
+ SO THAT Instrument.get("clarinet") returns a Bb clarinet (the default)
37
+
38
+ **STORY 5: Instrument provides unified interface**
39
+ AS a developer
40
+ WHEN I have an Instrument instance
41
+ I WANT to access properties like name, transposition, clefs, and pitch_designation
42
+ SO THAT I don't need to navigate between instrument type and variant objects
43
+
44
+ ### Implementation Notes
45
+
46
+ 1. The Instrument class should:
47
+ - Wrap both an InstrumentType and a specific Variant
48
+ - Generate appropriate display names (e.g., "Clarinet in A")
49
+ - Provide methods for transposition, clefs, staff schemes
50
+ - Be directly usable in scores and parts
51
+
52
+ 2. Factory methods should support:
53
+ - `Instrument.get("trumpet_in_c")` - parse variant from name
54
+ - `Instrument.get("trumpet", "in_c")` - explicit variant
55
+ - `Instrument.get("trumpet")` - use default variant
56
+
57
+ 3. ScoreOrder should work with Instrument instances directly
58
+
59
+ ### Migration Path
60
+
61
+ 1. Rename existing Instrument class to InstrumentType
62
+ 2. Update all references to use InstrumentType where appropriate
63
+ 3. Create new Instrument class for variant instances
64
+ 4. Update ScoreOrder to work with new Instrument instances
65
+ 5. Update documentation and tests
@@ -0,0 +1,30 @@
1
+ IN ORDER TO accurately model sound events
2
+ AS a developer
3
+ I WANT a clear way to group the notion of a Note (pitch + rhythmic value) and an unpitched note.
4
+
5
+ We need a hierarchy of classes
6
+
7
+ class RhythmicEvent
8
+ attr_accessor :rhythmic_value
9
+
10
+ class Note < RhythmicEvent
11
+ attr_accessor :pitch
12
+ def sounded?
13
+ true
14
+ end
15
+
16
+ class UnpitchedNote < RhythmicEvent
17
+ def sounded?
18
+ true
19
+ end
20
+
21
+ class Rest < RhythmicEvent
22
+ def sounded?
23
+ false
24
+ end
25
+
26
+
27
+ acceptance criteria
28
+ - the above class hierarchy and implementation requirements
29
+ - full test coverage
30
+ - use NotImplementedError instead of NotImplementedError in RhythmicEvent if and where appropriate.
@@ -0,0 +1,3 @@
1
+
2
+ agent-centric multimedia scoring
3
+ (A/V DAW)
@@ -6,6 +6,8 @@ I want to analyze two-note combinations (dyads)
6
6
 
7
7
  So that I can understand harmonic implications in two-part music
8
8
 
9
+ Constructor should accept to pitches (or pitch classes) and an optional key
10
+
9
11
  ## Scenario: Identify interval in dyad
10
12
 
11
13
  Given I have two pitches forming a dyad
@@ -14,16 +16,6 @@ When I access the interval property
14
16
 
15
17
  Then I should receive the correct interval between the pitches
16
18
 
17
- ## Scenario: Find implied triads from thirds
18
-
19
- Given I have a dyad that forms a third
20
-
21
- When I request the implied triad
22
-
23
- Then I should receive the most likely triad containing those pitches
24
-
25
- And it should consider the musical context
26
-
27
19
  ## Scenario: List possible triads from fifth
28
20
 
29
21
  Given I have a dyad forming a perfect fifth
@@ -0,0 +1,10 @@
1
+
2
+
3
+
4
+ Material
5
+
6
+ Fragment < Material
7
+
8
+ Score < Material
9
+ - name
10
+ -