head_music 8.2.1 → 8.3.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 +1 -1
- data/.github/workflows/release.yml +1 -1
- data/CLAUDE.md +134 -0
- data/Gemfile.lock +25 -25
- data/Rakefile +2 -2
- data/TODO.md +41 -150
- data/bin/check_instrument_consistency.rb +86 -0
- data/check_instrument_consistency.rb +0 -0
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +28 -7
- data/lib/head_music/instruments/instrument_families.yml +10 -9
- data/lib/head_music/instruments/instruments.yml +350 -368
- data/lib/head_music/locales/en.yml +92 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +93 -26
- data/lib/head_music/rudiment/scale_degree.rb +8 -3
- data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
- data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
- data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
- data/lib/head_music/rudiment/tuning.rb +17 -3
- data/lib/head_music/style/annotation.rb +4 -4
- data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +3 -0
- data/user_stories/backlog/band-score-order.md +38 -0
- data/user_stories/backlog/chamber-ensemble-score-order.md +33 -0
- data/user_stories/backlog/consonance-dissonance-classification.md +57 -0
- data/user_stories/backlog/dyad-analysis.md +65 -0
- data/user_stories/backlog/orchestral-score-order.md +43 -0
- data/user_stories/backlog/pitch-class-set-analysis.md +39 -0
- data/user_stories/backlog/pitch-set-classification.md +62 -0
- data/user_stories/backlog/sonority-identification.md +47 -0
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec01f26a90e391dbe250ca2587586ceb9bc062a8eba06d97f52767eaf08bb2f0
|
4
|
+
data.tar.gz: b2253758a487b28f9e02ae6479da2586da39ff45337bca0a24f9634988ff9eb4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6633eb9a47b72f81a44225d475d60b67924136809d469ba2a45ffe39ea6c1356ffbecd79e64f931c15146dd00980a9ff220c2b5c95deb955e5b3cf0eec7fef67
|
7
|
+
data.tar.gz: 5628ee5c9f0a442560f502d071f155e0d93771b3081572e9a98a86a154f387b0c9431662b051fd58bb9189b71906b5e7c9c29dbeddebd278f1d4a23d97416634
|
data/.github/workflows/ci.yml
CHANGED
data/CLAUDE.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
HeadMusic is a Ruby gem for Western music theory. It provides a comprehensive toolkit for working with pitches, scales, intervals, chords, and musical analysis. The gem supports internationalization with translations in 7 languages.
|
8
|
+
|
9
|
+
## Development Commands
|
10
|
+
|
11
|
+
### Essential Commands
|
12
|
+
|
13
|
+
```bash
|
14
|
+
# Install dependencies
|
15
|
+
bin/setup
|
16
|
+
|
17
|
+
# Run tests with coverage
|
18
|
+
bundle exec rake
|
19
|
+
|
20
|
+
# Run tests without coverage
|
21
|
+
bundle exec rspec
|
22
|
+
|
23
|
+
# Run a specific test file
|
24
|
+
bundle exec rspec spec/head_music/rudiments/pitch_spec.rb
|
25
|
+
|
26
|
+
# Run linting
|
27
|
+
bundle exec rubocop
|
28
|
+
|
29
|
+
# Run all quality checks (tests, linting, security)
|
30
|
+
bundle exec rake quality
|
31
|
+
|
32
|
+
# Open interactive console with gem loaded
|
33
|
+
bin/console
|
34
|
+
# or
|
35
|
+
bundle exec rake console
|
36
|
+
|
37
|
+
# Generate documentation
|
38
|
+
bundle exec rake doc
|
39
|
+
|
40
|
+
# Check documentation coverage
|
41
|
+
bundle exec rake doc_stats
|
42
|
+
```
|
43
|
+
|
44
|
+
### Linting and Formatting
|
45
|
+
|
46
|
+
The project uses Standard Ruby for style enforcement. Always run linting before committing:
|
47
|
+
|
48
|
+
```bash
|
49
|
+
bundle exec rubocop
|
50
|
+
```
|
51
|
+
|
52
|
+
### Testing
|
53
|
+
|
54
|
+
Tests are written in RSpec and located in the `/spec` directory, mirroring the `/lib` structure. The project requires 90% code coverage minimum.
|
55
|
+
|
56
|
+
## Architecture
|
57
|
+
|
58
|
+
### Module Structure
|
59
|
+
|
60
|
+
The codebase follows a domain-driven design with clear module boundaries:
|
61
|
+
|
62
|
+
1. **HeadMusic::Rudiment** - Core music theory elements
|
63
|
+
- Pitch, Note, Scale, Key, Interval, Chord
|
64
|
+
- Factory methods: `.get()` for most rudiments
|
65
|
+
|
66
|
+
2. **HeadMusic::Analysis** - Musical analysis tools
|
67
|
+
- Intervals, Chords, Motion analysis
|
68
|
+
- Harmonic and melodic analysis
|
69
|
+
|
70
|
+
3. **HeadMusic::Content** - Musical composition representation
|
71
|
+
- Composition, Voice, Note, Rest
|
72
|
+
- Rhythmic values and time signatures
|
73
|
+
|
74
|
+
4. **HeadMusic::Instruments** - Instrument definitions
|
75
|
+
- Instrument families and properties
|
76
|
+
- Pitch ranges and transposition
|
77
|
+
|
78
|
+
5. **HeadMusic::Style** - Composition rules and guidelines
|
79
|
+
- Counterpoint rules
|
80
|
+
- Voice leading guidelines
|
81
|
+
- Style analysis
|
82
|
+
|
83
|
+
### Key Design Patterns
|
84
|
+
|
85
|
+
- **Factory Pattern**: Most musical objects use `.get()` factory methods
|
86
|
+
- **Value Objects**: Immutable objects for musical concepts
|
87
|
+
- **Named Mixin**: Provides internationalization support
|
88
|
+
- **Delegation**: Extensive use of delegation for clean APIs
|
89
|
+
|
90
|
+
### Entry Points
|
91
|
+
|
92
|
+
- Main file: `lib/head_music.rb`
|
93
|
+
- Module loading order is important and defined in the main file
|
94
|
+
- Constants like GOLDEN_RATIO are defined at the top level
|
95
|
+
|
96
|
+
## Important Implementation Details
|
97
|
+
|
98
|
+
### Internationalization
|
99
|
+
|
100
|
+
The gem supports multiple languages through the HeadMusic::Named mixin:
|
101
|
+
- Translations in `lib/head_music/locales/`
|
102
|
+
- Languages: en, de, es, fr, it, ja, nl
|
103
|
+
- Use `I18n.locale = :de` to change language
|
104
|
+
|
105
|
+
### Testing Patterns
|
106
|
+
|
107
|
+
- Use `described_class` instead of hardcoding class names
|
108
|
+
- Test files must end with `_spec.rb`
|
109
|
+
- Shared examples in `spec/support/`
|
110
|
+
- `composition_context.rb` provides test utilities
|
111
|
+
|
112
|
+
### Code Style
|
113
|
+
|
114
|
+
- Ruby 3.3.0+ features are allowed
|
115
|
+
- Follow Standard Ruby style guide
|
116
|
+
- Use YARD documentation format for public methods
|
117
|
+
- Prefer delegation over inheritance
|
118
|
+
|
119
|
+
## Common Development Tasks
|
120
|
+
|
121
|
+
### Adding a New Musical Concept
|
122
|
+
|
123
|
+
1. Create the class in the appropriate module
|
124
|
+
2. Include `HeadMusic::Named` if it needs internationalization
|
125
|
+
3. Add factory method `.get()` if appropriate
|
126
|
+
4. Create corresponding spec file
|
127
|
+
5. Add translations to locale files if using Named
|
128
|
+
|
129
|
+
### Modifying Existing Classes
|
130
|
+
|
131
|
+
1. Check for dependent classes that might be affected
|
132
|
+
2. Run tests for the specific module: `bundle exec rspec spec/head_music/[module_name]`
|
133
|
+
3. Update documentation with YARD comments
|
134
|
+
4. Ensure translations are updated if names change
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
head_music (8.
|
4
|
+
head_music (8.3.0)
|
5
5
|
activesupport (~> 7.0)
|
6
6
|
humanize (~> 2.0)
|
7
7
|
i18n (~> 1.8)
|
@@ -22,54 +22,54 @@ GEM
|
|
22
22
|
securerandom (>= 0.3)
|
23
23
|
tzinfo (~> 2.0, >= 2.0.5)
|
24
24
|
ast (2.4.3)
|
25
|
-
base64 (0.
|
26
|
-
benchmark (0.4.
|
27
|
-
bigdecimal (3.
|
25
|
+
base64 (0.3.0)
|
26
|
+
benchmark (0.4.1)
|
27
|
+
bigdecimal (3.2.2)
|
28
28
|
bundler-audit (0.9.2)
|
29
29
|
bundler (>= 1.2.0, < 3)
|
30
30
|
thor (~> 1.0)
|
31
31
|
concurrent-ruby (1.3.5)
|
32
32
|
connection_pool (2.5.3)
|
33
|
-
diff-lcs (1.6.
|
33
|
+
diff-lcs (1.6.2)
|
34
34
|
docile (1.4.1)
|
35
|
-
drb (2.2.
|
35
|
+
drb (2.2.3)
|
36
36
|
humanize (2.5.1)
|
37
37
|
i18n (1.14.7)
|
38
38
|
concurrent-ruby (~> 1.0)
|
39
|
-
json (2.
|
39
|
+
json (2.13.1)
|
40
40
|
kramdown (2.5.1)
|
41
41
|
rexml (>= 3.3.9)
|
42
|
-
language_server-protocol (3.17.0.
|
42
|
+
language_server-protocol (3.17.0.5)
|
43
43
|
lint_roller (1.1.0)
|
44
44
|
logger (1.7.0)
|
45
45
|
minitest (5.25.5)
|
46
46
|
parallel (1.27.0)
|
47
|
-
parser (3.3.
|
47
|
+
parser (3.3.9.0)
|
48
48
|
ast (~> 2.4.1)
|
49
49
|
racc
|
50
50
|
prism (1.4.0)
|
51
51
|
racc (1.8.1)
|
52
52
|
rainbow (3.1.1)
|
53
|
-
rake (13.
|
53
|
+
rake (13.3.0)
|
54
54
|
regexp_parser (2.10.0)
|
55
55
|
rexml (3.4.1)
|
56
|
-
rspec (3.13.
|
56
|
+
rspec (3.13.1)
|
57
57
|
rspec-core (~> 3.13.0)
|
58
58
|
rspec-expectations (~> 3.13.0)
|
59
59
|
rspec-mocks (~> 3.13.0)
|
60
|
-
rspec-core (3.13.
|
60
|
+
rspec-core (3.13.5)
|
61
61
|
rspec-support (~> 3.13.0)
|
62
|
-
rspec-expectations (3.13.
|
62
|
+
rspec-expectations (3.13.5)
|
63
63
|
diff-lcs (>= 1.2.0, < 2.0)
|
64
64
|
rspec-support (~> 3.13.0)
|
65
|
-
rspec-its (
|
66
|
-
rspec-core (>= 3.
|
67
|
-
rspec-expectations (>= 3.
|
68
|
-
rspec-mocks (3.13.
|
65
|
+
rspec-its (2.0.0)
|
66
|
+
rspec-core (>= 3.13.0)
|
67
|
+
rspec-expectations (>= 3.13.0)
|
68
|
+
rspec-mocks (3.13.5)
|
69
69
|
diff-lcs (>= 1.2.0, < 2.0)
|
70
70
|
rspec-support (~> 3.13.0)
|
71
|
-
rspec-support (3.13.
|
72
|
-
rubocop (1.75.
|
71
|
+
rspec-support (3.13.4)
|
72
|
+
rubocop (1.75.8)
|
73
73
|
json (~> 2.3)
|
74
74
|
language_server-protocol (~> 3.17.0.2)
|
75
75
|
lint_roller (~> 1.1.0)
|
@@ -80,7 +80,7 @@ GEM
|
|
80
80
|
rubocop-ast (>= 1.44.0, < 2.0)
|
81
81
|
ruby-progressbar (~> 1.7)
|
82
82
|
unicode-display_width (>= 2.4.0, < 4.0)
|
83
|
-
rubocop-ast (1.
|
83
|
+
rubocop-ast (1.46.0)
|
84
84
|
parser (>= 3.3.7.2)
|
85
85
|
prism (~> 1.4)
|
86
86
|
rubocop-performance (1.25.0)
|
@@ -99,12 +99,12 @@ GEM
|
|
99
99
|
docile (~> 1.1)
|
100
100
|
simplecov-html (~> 0.11)
|
101
101
|
simplecov_json_formatter (~> 0.1)
|
102
|
-
simplecov-html (0.13.
|
102
|
+
simplecov-html (0.13.2)
|
103
103
|
simplecov_json_formatter (0.1.4)
|
104
|
-
standard (1.
|
104
|
+
standard (1.50.0)
|
105
105
|
language_server-protocol (~> 3.17.0.2)
|
106
106
|
lint_roller (~> 1.0)
|
107
|
-
rubocop (~> 1.75.
|
107
|
+
rubocop (~> 1.75.5)
|
108
108
|
standard-custom (~> 1.0.0)
|
109
109
|
standard-performance (~> 1.8)
|
110
110
|
standard-custom (1.0.2)
|
@@ -113,7 +113,7 @@ GEM
|
|
113
113
|
standard-performance (1.8.0)
|
114
114
|
lint_roller (~> 1.1)
|
115
115
|
rubocop-performance (~> 1.25.0)
|
116
|
-
thor (1.
|
116
|
+
thor (1.4.0)
|
117
117
|
tzinfo (2.0.6)
|
118
118
|
concurrent-ruby (~> 1.0)
|
119
119
|
unicode-display_width (3.1.4)
|
@@ -132,7 +132,7 @@ DEPENDENCIES
|
|
132
132
|
kramdown
|
133
133
|
rake (~> 13.0)
|
134
134
|
rspec (~> 3.0)
|
135
|
-
rspec-its (~>
|
135
|
+
rspec-its (~> 2.0)
|
136
136
|
rubocop
|
137
137
|
rubocop-rake
|
138
138
|
rubocop-rspec
|
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ begin
|
|
12
12
|
end
|
13
13
|
|
14
14
|
desc "Generate documentation and show stats"
|
15
|
-
task :
|
15
|
+
task doc_stats: :doc do
|
16
16
|
sh "yard stats --list-undoc"
|
17
17
|
end
|
18
18
|
rescue LoadError
|
@@ -29,7 +29,7 @@ end
|
|
29
29
|
task default: :spec
|
30
30
|
|
31
31
|
desc "Run all quality checks (tests, linting, security audit)"
|
32
|
-
task :
|
32
|
+
task quality: [:spec, :standard, "bundle:audit:check"]
|
33
33
|
|
34
34
|
desc "Open an irb session preloaded with this library"
|
35
35
|
task :console do
|
data/TODO.md
CHANGED
@@ -1,159 +1,10 @@
|
|
1
1
|
# TODO
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Robert Head:
|
6
|
-
Hey, Brian. You have a sec for a music question? I’m trying to come up with different terms for the way “family” is used. “Woodwind family” vs. “Oboe family”.
|
7
|
-
|
8
|
-
Brian Head:
|
9
|
-
Those are good, I’d say. In what context?
|
10
|
-
|
11
|
-
Robert Head:
|
12
|
-
In my software project, I’m trying to define those relationships. One is a section of the orchestra and the other is species of instrument. But usually people, in my experience, just say “family”.
|
13
|
-
Is there some more precise terminology or adjective that can disambiguate those two terms?
|
14
|
-
|
15
|
-
Brian Head:
|
16
|
-
Hmmm. Already “family” is informal. Blatter distinguishes between “choir” and “family” as in the trumpet family” within the “brass choir”.
|
17
|
-
|
18
|
-
Robert Head:
|
19
|
-
Ah, yes! I do like that.
|
20
|
-
But does it apply to percussion?
|
21
|
-
“Section” is another candidate, but that gets used for string parts as well.
|
22
|
-
|
23
|
-
Brian Head:
|
24
|
-
Strings and percussion probably don’t think of themselves as a choir, but if you’re mainly looking for a taxonomically consistent word, that’s the feat I can think of at the moment.
|
25
|
-
Section is a good word, too, which easily flows between smaller and larger meanings.
|
26
|
-
|
27
|
-
Robert Head:
|
28
|
-
Obviously, the four “families” is a garbage way to classify all instruments and it really is more applicable to the orchestral context, so maybe “orchestra family” or “orchestra section”.
|
29
|
-
|
30
|
-
Brian Head:
|
31
|
-
I’d say that “family” is best used at the instrument level, as in “saxophone family”. Choir or section are better for larger collections. Still, both of those words connote membership in an orchestra. Or large ensemble. “Woodwinds” or “percussion” describe the class of instruments themselves.
|
32
|
-
|
33
|
-
Robert Head:
|
34
|
-
Cool
|
35
|
-
|
36
|
-
Action Item: Call them orchestra_section
|
37
|
-
|
38
|
-
Add Score Order
|
39
|
-
|
40
|
-
Orchestral Score Order
|
41
|
-
|
42
|
-
# So, in orchestral scores, the groupings are by instrumental 'family':
|
43
|
-
# woodwinds on top of the page, and below them, in descending order,
|
44
|
-
# brass,
|
45
|
-
# percussion,
|
46
|
-
# harp and keyboards,
|
47
|
-
# soloists (instrumental or vocal),
|
48
|
-
# voices,
|
49
|
-
# strings
|
50
|
-
|
51
|
-
(Notice the different placement of percussion in orchestra and band scores)
|
52
|
-
|
53
|
-
Flutes (Fl or Fls)
|
54
|
-
Oboes (Ob or Obs)
|
55
|
-
Clarinets (Cl or Cls)
|
56
|
-
Bassoons (Bsn or Bsns)
|
57
|
-
Horns (Hn or Hns)
|
58
|
-
Trumpets (Tpt or Tpts)
|
59
|
-
Trombones (Trb or Trbs)
|
60
|
-
Tuba (Tuba)
|
61
|
-
Timpani (Timp)
|
62
|
-
Percussion (Perc)
|
63
|
-
Other Instruments
|
64
|
-
harp and keyboards
|
65
|
-
soloists
|
66
|
-
voices
|
67
|
-
Violins I (Vlns)
|
68
|
-
Violins II
|
69
|
-
Viola (Vla)
|
70
|
-
Violoncellos (Vcl)
|
71
|
-
Double Bass (DB)
|
72
|
-
|
73
|
-
Band Score Order
|
74
|
-
|
75
|
-
Flutes (Fl or Fls)
|
76
|
-
Oboes (Ob or Obs)
|
77
|
-
Bassoons (Bsn or Bsns)
|
78
|
-
Clarinets (Cl or Cls)
|
79
|
-
Saxophones (AS, or TS, or BS)
|
80
|
-
Cornets (Cor)
|
81
|
-
Trumpets (Tpt or Tpts)
|
82
|
-
Horns (Hn or Hns)
|
83
|
-
Trombones (Trb or Trbs)
|
84
|
-
Euphoniums (Euph)
|
85
|
-
Tubas (Tubas)
|
86
|
-
Timpani (Timp)
|
87
|
-
Percussion (Perc)
|
88
|
-
|
89
|
-
Brass Quintet
|
90
|
-
|
91
|
-
Trumpet I
|
92
|
-
Trumpet II
|
93
|
-
Horn
|
94
|
-
Trombone
|
95
|
-
Tuba
|
96
|
-
|
97
|
-
Woodwind Quintet
|
98
|
-
|
99
|
-
Flute
|
100
|
-
Oboe
|
101
|
-
Clarinet
|
102
|
-
Horn
|
103
|
-
Bassoon
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
Disambiguate PitchSet and Sonority
|
108
|
-
|
109
|
-
Sonority should be a name for a specific set of intervals
|
110
|
-
Sonority.get(identifier)
|
111
|
-
Sonority.for(pitch_set)
|
112
|
-
Sonority.pitch_set_for(root_pitch:, inversion:)
|
113
|
-
|
114
|
-
class PitchSet
|
115
|
-
def sonority
|
116
|
-
@sonority ||= Sonority.for(self)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
open consonance (P5 P8)
|
123
|
-
soft consonance (m3 M3 m6 M6)
|
124
|
-
mild dissonance (M2 m7)
|
125
|
-
sharp dissonance (m2 M7)
|
126
|
-
|
127
|
-
P4 (consonant or dissonant)
|
128
|
-
T (neutral or restless)
|
3
|
+
## User Stories
|
129
4
|
|
130
5
|
Sets
|
131
6
|
DurationSet?
|
132
7
|
|
133
|
-
|
134
|
-
Make new analysis classes:
|
135
|
-
Dyad
|
136
|
-
.interval
|
137
|
-
.implied_triad (if a third)
|
138
|
-
- returns most likely of possible triads
|
139
|
-
.possible_triads
|
140
|
-
- returns major and minor if a perfect fifth
|
141
|
-
- returns minor and diminished if minor third
|
142
|
-
- returns major and augmented if major third
|
143
|
-
- returns inverted major and root augmented if augmented fifth
|
144
|
-
- returns diminished if diminished fifth
|
145
|
-
- should it take into account enharmonics? I think yes.
|
146
|
-
.possible_seventh_chords
|
147
|
-
- as above, with either seventh added
|
148
|
-
- returns 3rd inversion if second
|
149
|
-
.possible_chords
|
150
|
-
possible_triads + possible_seventh_chords
|
151
|
-
.possible_enharmonic_triads
|
152
|
-
.possible_enharmonic_seventh_chords
|
153
|
-
.possible_enharmonic_chords
|
154
|
-
|
155
|
-
the dyad will be super helpful in analyzing two-part counterpoint.
|
156
|
-
|
157
8
|
Triad
|
158
9
|
SeventhChord
|
159
10
|
Don't need anything beyond seventh chords to analyze pre-Romantic music.
|
@@ -216,3 +67,43 @@ PitchSet
|
|
216
67
|
PitchClassSet
|
217
68
|
.normal_form? (most compact)
|
218
69
|
.prime_form (most compact normal form of the original or any inversion)
|
70
|
+
|
71
|
+
|
72
|
+
## Text Conversation with Brian Head (May 7, 2023)
|
73
|
+
|
74
|
+
Brian Head, brother and faculty at USC Thornton School of Music
|
75
|
+
https://music.usc.edu/brian-head/
|
76
|
+
|
77
|
+
Robert Head:
|
78
|
+
Hey, Brian. You have a sec for a music question? I’m trying to come up with different terms for the way “family” is used. “Woodwind family” vs. “Oboe family”.
|
79
|
+
|
80
|
+
Brian Head:
|
81
|
+
Those are good, I’d say. In what context?
|
82
|
+
|
83
|
+
Robert Head:
|
84
|
+
In my software project, I’m trying to define those relationships. One is a section of the orchestra and the other is species of instrument. But usually people, in my experience, just say “family”.
|
85
|
+
Is there some more precise terminology or adjective that can disambiguate those two terms?
|
86
|
+
|
87
|
+
Brian Head:
|
88
|
+
Hmmm. Already “family” is informal. Blatter distinguishes between “choir” and “family” as in the trumpet family” within the “brass choir”.
|
89
|
+
|
90
|
+
Robert Head:
|
91
|
+
Ah, yes! I do like that.
|
92
|
+
But does it apply to percussion?
|
93
|
+
“Section” is another candidate, but that gets used for string parts as well.
|
94
|
+
|
95
|
+
Brian Head:
|
96
|
+
Strings and percussion probably don’t think of themselves as a choir, but if you’re mainly looking for a taxonomically consistent word, that’s the feat I can think of at the moment.
|
97
|
+
Section is a good word, too, which easily flows between smaller and larger meanings.
|
98
|
+
|
99
|
+
Robert Head:
|
100
|
+
Obviously, the four “families” is a garbage way to classify all instruments and it really is more applicable to the orchestral context, so maybe “orchestra family” or “orchestra section”.
|
101
|
+
|
102
|
+
Brian Head:
|
103
|
+
I’d say that “family” is best used at the instrument level, as in “saxophone family”. Choir or section are better for larger collections. Still, both of those words connote membership in an orchestra. Or large ensemble. “Woodwinds” or “percussion” describe the class of instruments themselves.
|
104
|
+
|
105
|
+
Robert Head:
|
106
|
+
Cool
|
107
|
+
|
108
|
+
Action Item: Call them orchestra_section
|
109
|
+
DONE
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
# Load the YAML files
|
4
|
+
instruments_file = "/Users/roberthead/github.com/roberthead/head_music/lib/head_music/instruments/instruments.yml"
|
5
|
+
families_file = "/Users/roberthead/github.com/roberthead/head_music/lib/head_music/instruments/instrument_families.yml"
|
6
|
+
|
7
|
+
instruments = YAML.load_file(instruments_file)
|
8
|
+
families = YAML.load_file(families_file)
|
9
|
+
|
10
|
+
puts "Checking for inconsistencies between instruments and their families...\n\n"
|
11
|
+
|
12
|
+
inconsistencies = []
|
13
|
+
|
14
|
+
instruments.each do |instrument_name, instrument_data|
|
15
|
+
family_key = instrument_data["family_key"]
|
16
|
+
next unless family_key
|
17
|
+
|
18
|
+
family_data = families[family_key]
|
19
|
+
next unless family_data
|
20
|
+
|
21
|
+
# Check orchestra_section_key consistency
|
22
|
+
instrument_section = instrument_data["orchestra_section_key"]
|
23
|
+
family_section = family_data["orchestra_section_key"]
|
24
|
+
|
25
|
+
if instrument_section && family_section && instrument_section != family_section
|
26
|
+
inconsistencies << {
|
27
|
+
instrument: instrument_name,
|
28
|
+
family: family_key,
|
29
|
+
field: "orchestra_section_key",
|
30
|
+
instrument_value: instrument_section,
|
31
|
+
family_value: family_section
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Check classification_keys consistency
|
36
|
+
instrument_classifications = instrument_data["classification_keys"] || []
|
37
|
+
family_classifications = family_data["classification_keys"] || []
|
38
|
+
|
39
|
+
# Check if instrument has classifications that conflict with family
|
40
|
+
if !instrument_classifications.empty? && !family_classifications.empty?
|
41
|
+
conflicts = instrument_classifications - family_classifications
|
42
|
+
if !conflicts.empty?
|
43
|
+
inconsistencies << {
|
44
|
+
instrument: instrument_name,
|
45
|
+
family: family_key,
|
46
|
+
field: "classification_keys",
|
47
|
+
instrument_value: instrument_classifications,
|
48
|
+
family_value: family_classifications,
|
49
|
+
conflicts: conflicts
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if inconsistencies.empty?
|
56
|
+
puts "No inconsistencies found!"
|
57
|
+
else
|
58
|
+
puts "Found #{inconsistencies.length} inconsistencies:\n\n"
|
59
|
+
|
60
|
+
inconsistencies.each_with_index do |issue, index|
|
61
|
+
puts "#{index + 1}. #{issue[:instrument]} (family: #{issue[:family]})"
|
62
|
+
puts " Field: #{issue[:field]}"
|
63
|
+
puts " Instrument value: #{issue[:instrument_value]}"
|
64
|
+
puts " Family value: #{issue[:family_value]}"
|
65
|
+
if issue[:conflicts]
|
66
|
+
puts " Conflicting classifications: #{issue[:conflicts]}"
|
67
|
+
end
|
68
|
+
puts ""
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Also check for instruments with orchestra_section_key but no family
|
73
|
+
puts "\nInstruments with orchestra_section_key but no family_key:"
|
74
|
+
instruments.each do |instrument_name, instrument_data|
|
75
|
+
if instrument_data["orchestra_section_key"] && !instrument_data["family_key"]
|
76
|
+
puts "- #{instrument_name}: #{instrument_data["orchestra_section_key"]}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check for instruments with classification_keys but no family
|
81
|
+
puts "\nInstruments with classification_keys but no family_key:"
|
82
|
+
instruments.each do |instrument_name, instrument_data|
|
83
|
+
if instrument_data["classification_keys"] && !instrument_data["family_key"]
|
84
|
+
puts "- #{instrument_name}: #{instrument_data["classification_keys"]}"
|
85
|
+
end
|
86
|
+
end
|
File without changes
|
data/head_music.gemspec
CHANGED
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
38
38
|
|
39
39
|
spec.add_development_dependency "rake", "~> 13.0"
|
40
40
|
spec.add_development_dependency "rspec", "~> 3.0"
|
41
|
-
spec.add_development_dependency "rspec-its", "~>
|
41
|
+
spec.add_development_dependency "rspec-its", "~> 2.0"
|
42
42
|
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
43
43
|
spec.add_development_dependency "yard", "~> 0.9"
|
44
44
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Accepts a number and number of semitones and
|
1
|
+
# Accepts a number and number of semitones and provides the naming methods.
|
2
2
|
class HeadMusic::Analysis::DiatonicInterval::Naming
|
3
3
|
QUALITY_SEMITONES = HeadMusic::Analysis::DiatonicInterval::QUALITY_SEMITONES
|
4
4
|
NUMBER_NAMES = HeadMusic::Analysis::DiatonicInterval::NUMBER_NAMES
|
@@ -4,8 +4,8 @@ module HeadMusic::Analysis; end
|
|
4
4
|
# A diatonic interval is the distance between two spelled pitches.
|
5
5
|
class HeadMusic::Analysis::DiatonicInterval
|
6
6
|
include Comparable
|
7
|
+
include HeadMusic::Named
|
7
8
|
|
8
|
-
# TODO: include Named module
|
9
9
|
NUMBER_NAMES = %w[
|
10
10
|
unison second third fourth fifth sixth seventh octave
|
11
11
|
ninth tenth eleventh twelfth thirteenth fourteenth fifteenth
|
@@ -43,7 +43,6 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
43
43
|
|
44
44
|
attr_reader :lower_pitch, :higher_pitch
|
45
45
|
|
46
|
-
delegate :to_s, to: :name
|
47
46
|
delegate :perfect?, :major?, :minor?, :diminished?, :augmented?, :doubly_diminished?, :doubly_augmented?, to: :quality
|
48
47
|
|
49
48
|
delegate :step?, :skip?, :leap?, :large_leap?, to: :category
|
@@ -52,18 +51,40 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
52
51
|
to: :size
|
53
52
|
)
|
54
53
|
delegate(
|
55
|
-
:simple_name, :quality_name, :simple_number_name, :number_name, :
|
54
|
+
:simple_name, :quality_name, :simple_number_name, :number_name, :shorthand,
|
56
55
|
to: :naming
|
57
56
|
)
|
58
57
|
|
59
58
|
alias_method :to_i, :semitones
|
60
59
|
|
60
|
+
# Override Named module methods to use computed name from naming
|
61
|
+
def name(locale_code: nil)
|
62
|
+
if locale_code
|
63
|
+
# Try to get translation from locale files
|
64
|
+
name_key = naming.name.downcase.gsub(" ", "_").to_sym
|
65
|
+
translation = I18n.translate(name_key, scope: "head_music.diatonic_intervals", locale: locale_code, default: nil)
|
66
|
+
translation || naming.name
|
67
|
+
else
|
68
|
+
naming.name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
name
|
74
|
+
end
|
75
|
+
|
61
76
|
# Accepts a name and returns the interval with middle c on the bottom
|
62
77
|
def self.get(identifier)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
78
|
+
if identifier.is_a?(String) || identifier.is_a?(Symbol)
|
79
|
+
name = Parser.new(identifier)
|
80
|
+
semitones = Semitones.new(name.degree_name.to_sym, name.quality_name).count
|
81
|
+
higher_pitch = HeadMusic::Rudiment::Pitch.from_number_and_letter(HeadMusic::Rudiment::Pitch.middle_c + semitones, name.higher_letter)
|
82
|
+
interval = new(HeadMusic::Rudiment::Pitch.middle_c, higher_pitch)
|
83
|
+
interval.ensure_localized_name(name: identifier.to_s)
|
84
|
+
interval
|
85
|
+
else
|
86
|
+
identifier
|
87
|
+
end
|
67
88
|
end
|
68
89
|
|
69
90
|
def initialize(first_pitch, second_pitch)
|