head_music 8.3.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -3
- data/CHANGELOG.md +71 -0
- data/CLAUDE.md +62 -25
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/MUSIC_THEORY.md +120 -0
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +29 -27
- data/lib/head_music/analysis/dyad.rb +229 -0
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/analysis/melodic_interval.rb +1 -1
- data/lib/head_music/analysis/pitch_class_set.rb +111 -14
- data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
- data/lib/head_music/analysis/sonority.rb +50 -12
- data/lib/head_music/content/note.rb +1 -1
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/position.rb +1 -1
- data/lib/head_music/content/voice.rb +1 -1
- data/lib/head_music/instruments/alternate_tuning.rb +102 -0
- data/lib/head_music/instruments/alternate_tunings.yml +78 -0
- data/lib/head_music/instruments/instrument.rb +231 -72
- data/lib/head_music/instruments/instrument_configuration.rb +66 -0
- data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
- data/lib/head_music/instruments/instrument_configurations.yml +288 -0
- data/lib/head_music/instruments/instrument_families.yml +77 -0
- data/lib/head_music/instruments/instrument_family.rb +15 -5
- data/lib/head_music/instruments/instruments.yml +795 -965
- data/lib/head_music/instruments/playing_technique.rb +75 -0
- data/lib/head_music/instruments/playing_techniques.yml +826 -0
- data/lib/head_music/instruments/score_order.rb +136 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/staff.rb +61 -1
- data/lib/head_music/instruments/staff_scheme.rb +6 -4
- data/lib/head_music/instruments/stringing.rb +115 -0
- data/lib/head_music/instruments/stringing_course.rb +58 -0
- data/lib/head_music/instruments/stringings.yml +168 -0
- data/lib/head_music/instruments/variant.rb +6 -1
- data/lib/head_music/locales/de.yml +29 -0
- data/lib/head_music/locales/en.yml +106 -0
- data/lib/head_music/locales/es.yml +29 -0
- data/lib/head_music/locales/fr.yml +29 -0
- data/lib/head_music/locales/it.yml +29 -0
- data/lib/head_music/locales/ru.yml +29 -0
- data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
- data/lib/head_music/notation/staff_mapping.rb +70 -0
- data/lib/head_music/notation/staff_position.rb +62 -0
- data/lib/head_music/notation.rb +7 -0
- data/lib/head_music/rudiment/alteration.rb +34 -49
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +4 -7
- data/lib/head_music/rudiment/clef.rb +2 -2
- data/lib/head_music/rudiment/consonance.rb +39 -5
- data/lib/head_music/rudiment/diatonic_context.rb +25 -0
- data/lib/head_music/rudiment/key.rb +77 -0
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
- data/lib/head_music/rudiment/key_signature.rb +21 -8
- data/lib/head_music/rudiment/letter_name.rb +3 -3
- data/lib/head_music/rudiment/meter.rb +19 -9
- data/lib/head_music/rudiment/mode.rb +92 -0
- data/lib/head_music/rudiment/note.rb +112 -0
- data/lib/head_music/rudiment/pitch/parser.rb +52 -0
- data/lib/head_music/rudiment/pitch.rb +5 -6
- data/lib/head_music/rudiment/pitch_class.rb +1 -1
- data/lib/head_music/rudiment/quality.rb +1 -1
- data/lib/head_music/rudiment/reference_pitch.rb +1 -1
- data/lib/head_music/rudiment/register.rb +4 -1
- data/lib/head_music/rudiment/rest.rb +36 -0
- data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +13 -5
- data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
- data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
- data/lib/head_music/rudiment/scale.rb +4 -5
- data/lib/head_music/rudiment/scale_degree.rb +1 -1
- data/lib/head_music/rudiment/scale_type.rb +9 -3
- data/lib/head_music/rudiment/solmization.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +8 -4
- data/lib/head_music/rudiment/tempo.rb +85 -0
- data/lib/head_music/rudiment/tonal_context.rb +35 -0
- data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
- data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
- data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
- data/lib/head_music/rudiment/tuning.rb +21 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +31 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/time/clock_position.rb +84 -0
- data/lib/head_music/time/conductor.rb +264 -0
- data/lib/head_music/time/meter_event.rb +37 -0
- data/lib/head_music/time/meter_map.rb +173 -0
- data/lib/head_music/time/musical_position.rb +188 -0
- data/lib/head_music/time/smpte_timecode.rb +164 -0
- data/lib/head_music/time/tempo_event.rb +40 -0
- data/lib/head_music/time/tempo_map.rb +187 -0
- data/lib/head_music/time.rb +32 -0
- data/lib/head_music/utilities/case.rb +27 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +71 -22
- data/user_stories/active/string-pitches.md +41 -0
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/backlog/organizing-content.md +80 -0
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{backlog → done}/dyad-analysis.md +6 -16
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/done/handle-time.md +7 -0
- data/user_stories/done/handle-time.rb +163 -0
- data/user_stories/done/instrument-architecture.md +238 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
- data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
- data/user_stories/done/move-staff-position-to-notation.md +141 -0
- data/user_stories/done/notation-module-foundation.md +102 -0
- data/user_stories/done/percussion_set.md +260 -0
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/visioning/agentic-daw.md +2 -0
- metadata +84 -18
- data/TODO.md +0 -109
- data/check_instrument_consistency.rb +0 -0
- data/test_translations.rb +0 -15
- data/user_stories/backlog/consonance-dissonance-classification.md +0 -57
- data/user_stories/backlog/pitch-set-classification.md +0 -62
- data/user_stories/backlog/sonority-identification.md +0 -47
- /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
- /data/user_stories/{backlog → done}/pitch-class-set-analysis.md +0 -0
|
@@ -0,0 +1,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,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)
|