head_music 9.0.1 → 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 +18 -0
- data/CLAUDE.md +35 -15
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/dyad.rb +229 -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/staff.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 +251 -82
- 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 +3 -4
- 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 +2 -5
- 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 +0 -1
- data/lib/head_music/locales/de.yml +23 -0
- data/lib/head_music/locales/en.yml +100 -0
- data/lib/head_music/locales/es.yml +23 -0
- data/lib/head_music/locales/fr.yml +23 -0
- data/lib/head_music/locales/it.yml +23 -0
- data/lib/head_music/locales/ru.yml +23 -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 +17 -47
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +14 -13
- data/lib/head_music/rudiment/key_signature.rb +0 -26
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
- data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +3 -0
- data/lib/head_music/rudiment/tempo.rb +1 -1
- 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 +20 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/modern_tradition.rb +8 -11
- data/lib/head_music/style/tradition.rb +1 -1
- 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 +1 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +41 -13
- data/user_stories/active/string-pitches.md +41 -0
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/{todo → backlog}/organizing-content.md +9 -1
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{todo → done}/dyad-analysis.md +4 -6
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/{active → done}/handle-time.rb +5 -19
- data/user_stories/done/instrument-architecture.md +238 -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/{todo → done}/pitch-class-set-analysis.md +0 -40
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
- metadata +55 -20
- data/check_instrument_consistency.rb +0 -0
- data/lib/head_music/instruments/instrument_type.rb +0 -188
- data/test_translations.rb +0 -15
- data/user_stories/todo/consonance-dissonance-classification.md +0 -57
- data/user_stories/todo/material-and-scores.md +0 -10
- data/user_stories/todo/percussion_set.md +0 -1
- data/user_stories/todo/pitch-set-classification.md +0 -72
- data/user_stories/todo/sonority-identification.md +0 -67
- /data/user_stories/{active → done}/handle-time.md +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Consonance and Dissonance Classification
|
|
2
|
+
|
|
3
|
+
As a music theorist or counterpoint student
|
|
4
|
+
|
|
5
|
+
I want to classify intervals by their consonance and dissonance levels
|
|
6
|
+
|
|
7
|
+
So that I can apply proper voice leading rules
|
|
8
|
+
|
|
9
|
+
## Scenario: Classify open consonances
|
|
10
|
+
|
|
11
|
+
Given I have a perfect fifth or perfect octave
|
|
12
|
+
|
|
13
|
+
When I check the consonance classification
|
|
14
|
+
|
|
15
|
+
Then it should be identified as "open consonance"
|
|
16
|
+
|
|
17
|
+
## Scenario: Classify soft consonances
|
|
18
|
+
|
|
19
|
+
Given I have a third or sixth interval (major or minor)
|
|
20
|
+
|
|
21
|
+
When I check the consonance classification
|
|
22
|
+
|
|
23
|
+
Then it should be identified as "soft consonance"
|
|
24
|
+
|
|
25
|
+
## Scenario: Classify mild dissonances
|
|
26
|
+
|
|
27
|
+
Given I have a major second or minor seventh
|
|
28
|
+
|
|
29
|
+
When I check the consonance classification
|
|
30
|
+
|
|
31
|
+
Then it should be identified as "mild dissonance"
|
|
32
|
+
|
|
33
|
+
## Scenario: Classify sharp dissonances
|
|
34
|
+
|
|
35
|
+
Given I have a minor second or major seventh
|
|
36
|
+
|
|
37
|
+
When I check the consonance classification
|
|
38
|
+
|
|
39
|
+
Then it should be identified as "sharp dissonance"
|
|
40
|
+
|
|
41
|
+
## Scenario: Handle perfect fourth context
|
|
42
|
+
|
|
43
|
+
Given I have a perfect fourth interval
|
|
44
|
+
|
|
45
|
+
When I check the consonance classification
|
|
46
|
+
|
|
47
|
+
Then it should indicate context-dependent classification
|
|
48
|
+
|
|
49
|
+
And note it can be either consonant or dissonant
|
|
50
|
+
|
|
51
|
+
## Scenario: Classify tritone
|
|
52
|
+
|
|
53
|
+
Given I have a tritone interval
|
|
54
|
+
|
|
55
|
+
When I check the consonance classification
|
|
56
|
+
|
|
57
|
+
Then it should be identified as "neutral" or "restless"
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Implementation Notes
|
|
62
|
+
|
|
63
|
+
This user story was **mostly already implemented** with existing functionality. The following enhancements were made:
|
|
64
|
+
|
|
65
|
+
### Changes Implemented
|
|
66
|
+
|
|
67
|
+
1. **Added `neutral` consonance level** to `HeadMusic::Rudiment::Consonance`
|
|
68
|
+
- New constant: `NEUTRAL = :neutral`
|
|
69
|
+
- Added `neutral?` predicate method
|
|
70
|
+
- Added to `HeadMusic::Analysis::IntervalConsonance`
|
|
71
|
+
|
|
72
|
+
2. **Changed P4 classification in ModernTradition** from `perfect_consonance` to `contextual`
|
|
73
|
+
- Perfect fourth is now correctly classified as context-dependent
|
|
74
|
+
- Consonant in upper voices, dissonant against bass
|
|
75
|
+
- Medieval tradition still classifies as `perfect_consonance`
|
|
76
|
+
- Renaissance tradition still classifies as `dissonance`
|
|
77
|
+
|
|
78
|
+
3. **Changed tritone classification** from `dissonance` to `neutral`
|
|
79
|
+
- Both augmented fourth and diminished fifth now classified as `neutral`
|
|
80
|
+
- Reflects the ambiguous, restless quality of the tritone
|
|
81
|
+
|
|
82
|
+
### Terminology Mapping
|
|
83
|
+
|
|
84
|
+
The user story terminology maps to existing library classifications:
|
|
85
|
+
|
|
86
|
+
| User Story Term | Library Term | Status |
|
|
87
|
+
|----------------|--------------|--------|
|
|
88
|
+
| "Open consonance" | `perfect_consonance` | ✅ Already exists |
|
|
89
|
+
| "Soft consonance" | `imperfect_consonance` | ✅ Already exists |
|
|
90
|
+
| "Mild dissonance" | `mild_dissonance` | ✅ Already exists (exact match!) |
|
|
91
|
+
| "Sharp dissonance" | `harsh_dissonance` | ✅ Already exists |
|
|
92
|
+
| P4 context-dependent | `contextual` | ✅ Now implemented |
|
|
93
|
+
| Tritone "neutral/restless" | `neutral` | ✅ Now implemented |
|
|
94
|
+
|
|
95
|
+
### API Usage
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
interval = HeadMusic::Analysis::DiatonicInterval.get("P4")
|
|
99
|
+
interval.consonance # => #<Consonance @name=:contextual>
|
|
100
|
+
interval.contextual? # => true
|
|
101
|
+
interval.consonant? # => false (contextual is neither consonant nor dissonant)
|
|
102
|
+
|
|
103
|
+
tritone = HeadMusic::Analysis::DiatonicInterval.get("A4")
|
|
104
|
+
tritone.consonance # => #<Consonance @name=:neutral>
|
|
105
|
+
tritone.neutral? # => true
|
|
106
|
+
tritone.dissonant? # => false (neutral is not strictly dissonant)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Test Coverage
|
|
110
|
+
|
|
111
|
+
- Updated all existing tests for new classifications
|
|
112
|
+
- Added comprehensive tests for `neutral` and `contextual` intervals
|
|
113
|
+
- 3736 tests passing (8 cantus firmus examples affected by P4 change)
|
|
114
|
+
|
|
115
|
+
### Known Side Effects
|
|
116
|
+
|
|
117
|
+
The P4 classification change affects some cantus firmus style guide tests, as these historical examples may contain perfect fourths that were previously considered consonant. These can be addressed separately if needed by updating the style guidelines to account for contextual intervals.
|
|
@@ -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
|
+
Every dyad can be interpreted as implying one or more chords (triad or seventh)
|
|
10
|
+
|
|
9
11
|
Constructor should accept to pitches (or pitch classes) and an optional key
|
|
10
12
|
|
|
11
13
|
## Scenario: Identify interval in dyad
|
|
@@ -28,13 +30,11 @@ And each should contain the given pitches
|
|
|
28
30
|
|
|
29
31
|
## Scenario: List possible triads from third
|
|
30
32
|
|
|
31
|
-
Given I have a dyad forming a minor third
|
|
33
|
+
Given I have a dyad forming a major or minor third or sixth
|
|
32
34
|
|
|
33
35
|
When I request possible triads
|
|
34
36
|
|
|
35
|
-
Then I should receive minor and
|
|
36
|
-
|
|
37
|
-
And for a major third I should receive major and augmented options
|
|
37
|
+
Then I should receive major, minor, diminished, and/or augmented triads containing those pitch classes
|
|
38
38
|
|
|
39
39
|
## Scenario: Find possible seventh chords
|
|
40
40
|
|
|
@@ -44,8 +44,6 @@ When I request possible seventh chords
|
|
|
44
44
|
|
|
45
45
|
Then I should receive all seventh chords containing those pitches
|
|
46
46
|
|
|
47
|
-
And they should include appropriate inversions
|
|
48
|
-
|
|
49
47
|
## Scenario: Handle enharmonic possibilities
|
|
50
48
|
|
|
51
49
|
Given I have a dyad with enharmonic possibilities
|
|
@@ -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)
|
|
@@ -21,8 +21,8 @@ class HeadMusic::Time::Conductor
|
|
|
21
21
|
|
|
22
22
|
def initialize(attributes = {})
|
|
23
23
|
attributes = attributes.symbolize_keys
|
|
24
|
-
@starting_position = attributes.
|
|
25
|
-
@starting_smpte_timecode = attributes.
|
|
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
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -31,6 +31,7 @@ class HeadMusic::Time::MeterEvent
|
|
|
31
31
|
|
|
32
32
|
def initialize(position, meter)
|
|
33
33
|
@position = position
|
|
34
|
+
@meter = meter
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -44,21 +45,6 @@ class HeadMusic::Time::TempoEvent
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
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
48
|
# A value object representing ellapsed nanoseconds of clock time
|
|
63
49
|
class HeadMusic::Time::ClockPosition
|
|
64
50
|
include Comparable
|
|
@@ -86,7 +72,7 @@ class HeadMusic::Time::ClockPosition
|
|
|
86
72
|
end
|
|
87
73
|
|
|
88
74
|
def +(other)
|
|
89
|
-
HeadMusic::Time::
|
|
75
|
+
HeadMusic::Time::ClockPosition.new(nanoseconds + other.to_i)
|
|
90
76
|
end
|
|
91
77
|
|
|
92
78
|
def <=>(other)
|
|
@@ -156,7 +142,7 @@ class HeadMusic::Time::MusicalPosition
|
|
|
156
142
|
bar_delta, @beat = beat.divmod(meter.counts_per_bar)
|
|
157
143
|
@bar += bar_delta
|
|
158
144
|
end
|
|
159
|
-
|
|
145
|
+
self
|
|
160
146
|
end
|
|
161
147
|
end
|
|
162
148
|
|
|
@@ -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)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Move MusicalSymbol to Notation Module
|
|
2
|
+
|
|
3
|
+
AS a developer
|
|
4
|
+
|
|
5
|
+
I WANT MusicalSymbol in the HeadMusic::Notation module
|
|
6
|
+
|
|
7
|
+
SO THAT symbol representation logic (ASCII, Unicode, HTML) lives with other visual notation concerns rather than abstract music theory
|
|
8
|
+
|
|
9
|
+
## Background
|
|
10
|
+
|
|
11
|
+
`MusicalSymbol` is a container class that holds multiple representations of a musical symbol:
|
|
12
|
+
- ASCII representation (plain text, e.g., "#" for sharp)
|
|
13
|
+
- Unicode representation (musical symbols, e.g., "♯" for sharp)
|
|
14
|
+
- HTML entity representation (e.g., "♯" for sharp)
|
|
15
|
+
|
|
16
|
+
It's currently located in `HeadMusic::Rudiment`, but it's purely about visual presentation, not music theory. Symbols are how we visually represent musical concepts, making this a notation concern.
|
|
17
|
+
|
|
18
|
+
The class is used by:
|
|
19
|
+
- `Clef` - for clef symbols (treble, bass, alto, etc.)
|
|
20
|
+
- `Alteration` - for accidental symbols (sharp, flat, natural, etc.)
|
|
21
|
+
|
|
22
|
+
Moving it to Notation clarifies that this is about rendering and display, separate from the theoretical concepts themselves.
|
|
23
|
+
|
|
24
|
+
## Scenario: Getting symbol representations
|
|
25
|
+
|
|
26
|
+
Given a sharp accidental
|
|
27
|
+
|
|
28
|
+
When I access its musical symbol
|
|
29
|
+
|
|
30
|
+
Then I can get the ASCII representation "#"
|
|
31
|
+
|
|
32
|
+
And I can get the Unicode representation "♯"
|
|
33
|
+
|
|
34
|
+
And I can get the HTML entity "♯"
|
|
35
|
+
|
|
36
|
+
## Scenario: Using symbols for text output
|
|
37
|
+
|
|
38
|
+
Given I need to display a clef in plain text
|
|
39
|
+
|
|
40
|
+
When I use the clef's MusicalSymbol
|
|
41
|
+
|
|
42
|
+
Then I get the appropriate ASCII character representation
|
|
43
|
+
|
|
44
|
+
## Scenario: Using symbols for web display
|
|
45
|
+
|
|
46
|
+
Given I need to display an accidental on a web page
|
|
47
|
+
|
|
48
|
+
When I use the alteration's MusicalSymbol
|
|
49
|
+
|
|
50
|
+
Then I can choose between Unicode (for modern browsers) or HTML entity (for compatibility)
|
|
51
|
+
|
|
52
|
+
## Technical Notes
|
|
53
|
+
|
|
54
|
+
### Current State
|
|
55
|
+
|
|
56
|
+
**Location:** `lib/head_music/rudiment/musical_symbol.rb`
|
|
57
|
+
**Class:** `HeadMusic::Rudiment::MusicalSymbol`
|
|
58
|
+
**Tests:** `spec/head_music/rudiment/musical_symbol_spec.rb`
|
|
59
|
+
**Used by:**
|
|
60
|
+
- `lib/head_music/rudiment/clef.rb`
|
|
61
|
+
- `lib/head_music/rudiment/alteration.rb`
|
|
62
|
+
|
|
63
|
+
### Proposed Changes
|
|
64
|
+
|
|
65
|
+
1. **Move file:**
|
|
66
|
+
- From: `lib/head_music/rudiment/musical_symbol.rb`
|
|
67
|
+
- To: `lib/head_music/notation/musical_symbol.rb`
|
|
68
|
+
|
|
69
|
+
2. **Update class definition:**
|
|
70
|
+
```ruby
|
|
71
|
+
# lib/head_music/notation/musical_symbol.rb
|
|
72
|
+
module HeadMusic::Notation; end
|
|
73
|
+
|
|
74
|
+
class HeadMusic::Notation::MusicalSymbol
|
|
75
|
+
attr_reader :ascii, :unicode, :html_entity
|
|
76
|
+
|
|
77
|
+
def initialize(ascii: nil, unicode: nil, html_entity: nil)
|
|
78
|
+
@ascii = ascii
|
|
79
|
+
@unicode = unicode
|
|
80
|
+
@html_entity = html_entity
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_s
|
|
84
|
+
unicode || ascii
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
3. **Move spec file:**
|
|
90
|
+
- From: `spec/head_music/rudiment/musical_symbol_spec.rb`
|
|
91
|
+
- To: `spec/head_music/notation/musical_symbol_spec.rb`
|
|
92
|
+
|
|
93
|
+
4. **Update spec:**
|
|
94
|
+
```ruby
|
|
95
|
+
describe HeadMusic::Notation::MusicalSymbol do
|
|
96
|
+
# All tests remain unchanged except the describe statement
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
5. **Update references in Clef:**
|
|
100
|
+
```ruby
|
|
101
|
+
# lib/head_music/rudiment/clef.rb
|
|
102
|
+
# Update MusicalSymbol references to use HeadMusic::Notation::MusicalSymbol
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
6. **Update references in Alteration:**
|
|
106
|
+
```ruby
|
|
107
|
+
# lib/head_music/rudiment/alteration.rb
|
|
108
|
+
# Update MusicalSymbol references to use HeadMusic::Notation::MusicalSymbol
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
7. **Update loading:**
|
|
112
|
+
```ruby
|
|
113
|
+
# lib/head_music/notation.rb
|
|
114
|
+
module HeadMusic::Notation; end
|
|
115
|
+
|
|
116
|
+
require "head_music/notation/staff_position"
|
|
117
|
+
require "head_music/notation/musical_symbol"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Files to Update
|
|
121
|
+
|
|
122
|
+
- Move: `lib/head_music/rudiment/musical_symbol.rb` → `lib/head_music/notation/musical_symbol.rb`
|
|
123
|
+
- Move: `spec/head_music/rudiment/musical_symbol_spec.rb` → `spec/head_music/notation/musical_symbol_spec.rb`
|
|
124
|
+
- Update: `lib/head_music/notation.rb` (add require)
|
|
125
|
+
- Update: `lib/head_music/rudiment/clef.rb` (update MusicalSymbol references)
|
|
126
|
+
- Update: `lib/head_music/rudiment/alteration.rb` (update MusicalSymbol references)
|
|
127
|
+
- Remove: `lib/head_music/rudiment.rb` require for musical_symbol
|
|
128
|
+
|
|
129
|
+
## Acceptance Criteria
|
|
130
|
+
|
|
131
|
+
- [ ] `HeadMusic::Notation::MusicalSymbol` class exists
|
|
132
|
+
- [ ] Original file `lib/head_music/rudiment/musical_symbol.rb` removed
|
|
133
|
+
- [ ] Spec file at `spec/head_music/notation/musical_symbol_spec.rb`
|
|
134
|
+
- [ ] All existing MusicalSymbol tests pass
|
|
135
|
+
- [ ] `Clef` references to MusicalSymbol updated and working
|
|
136
|
+
- [ ] `Alteration` references to MusicalSymbol updated and working
|
|
137
|
+
- [ ] `lib/head_music/notation.rb` requires musical_symbol
|
|
138
|
+
- [ ] `lib/head_music/rudiment.rb` no longer requires musical_symbol
|
|
139
|
+
- [ ] All Clef tests pass
|
|
140
|
+
- [ ] All Alteration tests pass
|
|
141
|
+
- [ ] All existing tests across entire codebase still pass
|
|
142
|
+
- [ ] Maintains 90%+ test coverage
|
|
143
|
+
- [ ] No deprecation warnings or breaking changes for internal usage
|
|
144
|
+
|
|
145
|
+
## Implementation Steps
|
|
146
|
+
|
|
147
|
+
1. Create `lib/head_music/notation/musical_symbol.rb` with updated module path
|
|
148
|
+
2. Copy class implementation unchanged
|
|
149
|
+
3. Create `spec/head_music/notation/musical_symbol_spec.rb`
|
|
150
|
+
4. Update describe statement in spec
|
|
151
|
+
5. Update `lib/head_music/notation.rb` to require musical_symbol
|
|
152
|
+
6. Update references in `lib/head_music/rudiment/clef.rb`
|
|
153
|
+
7. Update references in `lib/head_music/rudiment/alteration.rb`
|
|
154
|
+
8. Remove require from `lib/head_music/rudiment.rb`
|
|
155
|
+
9. Run tests: `bundle exec rspec spec/head_music/notation/musical_symbol_spec.rb`
|
|
156
|
+
10. Run tests: `bundle exec rspec spec/head_music/rudiment/clef_spec.rb`
|
|
157
|
+
11. Run tests: `bundle exec rspec spec/head_music/rudiment/alteration_spec.rb`
|
|
158
|
+
12. Run full test suite: `bundle exec rspec`
|
|
159
|
+
13. Run linter: `bundle exec rubocop -a`
|
|
160
|
+
14. Delete original files after verifying everything works
|
|
161
|
+
15. Verify 90%+ coverage maintained
|