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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +1 -1
  4. data/CLAUDE.md +134 -0
  5. data/Gemfile.lock +25 -25
  6. data/Rakefile +2 -2
  7. data/TODO.md +41 -150
  8. data/bin/check_instrument_consistency.rb +86 -0
  9. data/check_instrument_consistency.rb +0 -0
  10. data/head_music.gemspec +1 -1
  11. data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
  12. data/lib/head_music/analysis/diatonic_interval.rb +28 -7
  13. data/lib/head_music/instruments/instrument_families.yml +10 -9
  14. data/lib/head_music/instruments/instruments.yml +350 -368
  15. data/lib/head_music/locales/en.yml +92 -0
  16. data/lib/head_music/rudiment/rhythmic_unit.rb +93 -26
  17. data/lib/head_music/rudiment/scale_degree.rb +8 -3
  18. data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
  19. data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
  20. data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
  21. data/lib/head_music/rudiment/tuning.rb +17 -3
  22. data/lib/head_music/style/annotation.rb +4 -4
  23. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  24. data/lib/head_music/version.rb +1 -1
  25. data/lib/head_music.rb +3 -0
  26. data/user_stories/backlog/band-score-order.md +38 -0
  27. data/user_stories/backlog/chamber-ensemble-score-order.md +33 -0
  28. data/user_stories/backlog/consonance-dissonance-classification.md +57 -0
  29. data/user_stories/backlog/dyad-analysis.md +65 -0
  30. data/user_stories/backlog/orchestral-score-order.md +43 -0
  31. data/user_stories/backlog/pitch-class-set-analysis.md +39 -0
  32. data/user_stories/backlog/pitch-set-classification.md +62 -0
  33. data/user_stories/backlog/sonority-identification.md +47 -0
  34. metadata +18 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77530f6691ab779c127d7324218a10a854bb8fb96ccaf0244accd9dbc878f24f
4
- data.tar.gz: 05b082c0029817aa3b3c6773c876000d61b7787915c0f157ad9af966da3ad73c
3
+ metadata.gz: ec01f26a90e391dbe250ca2587586ceb9bc062a8eba06d97f52767eaf08bb2f0
4
+ data.tar.gz: b2253758a487b28f9e02ae6479da2586da39ff45337bca0a24f9634988ff9eb4
5
5
  SHA512:
6
- metadata.gz: 965572974769529a7d971b0156c492195e9c7ea491d97e9445b3cc53714d802ffc0f2680559cb8449971391ef2b406f3a6283ca8ce6d8103c47713e71de954df
7
- data.tar.gz: 6e5444274c959f209861c1622002351d255050e6faff66d2b69033d90c372736e15a2c74b8fb66e01841dfc5c1f29887cc753acd736d54e21e1d29608c0ee11f
6
+ metadata.gz: 6633eb9a47b72f81a44225d475d60b67924136809d469ba2a45ffe39ea6c1356ffbecd79e64f931c15146dd00980a9ff220c2b5c95deb955e5b3cf0eec7fef67
7
+ data.tar.gz: 5628ee5c9f0a442560f502d071f155e0d93771b3081572e9a98a86a154f387b0c9431662b051fd58bb9189b71906b5e7c9c29dbeddebd278f1d4a23d97416634
@@ -28,7 +28,7 @@ jobs:
28
28
 
29
29
  - name: Upload coverage to Codecov
30
30
  if: matrix.ruby-version == '3.3.0'
31
- uses: codecov/codecov-action@v4
31
+ uses: codecov/codecov-action@v5
32
32
  with:
33
33
  token: ${{ secrets.CODECOV_TOKEN }}
34
34
  fail_ci_if_error: false
@@ -31,7 +31,7 @@ jobs:
31
31
  run: gem build *.gemspec
32
32
 
33
33
  - name: Create GitHub Release
34
- uses: softprops/action-gh-release@v1
34
+ uses: softprops/action-gh-release@v2
35
35
  with:
36
36
  files: '*.gem'
37
37
  generate_release_notes: true
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.2.0)
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.2.0)
26
- benchmark (0.4.0)
27
- bigdecimal (3.1.9)
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.1)
33
+ diff-lcs (1.6.2)
34
34
  docile (1.4.1)
35
- drb (2.2.1)
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.11.3)
39
+ json (2.13.1)
40
40
  kramdown (2.5.1)
41
41
  rexml (>= 3.3.9)
42
- language_server-protocol (3.17.0.4)
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.8.0)
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.2.1)
53
+ rake (13.3.0)
54
54
  regexp_parser (2.10.0)
55
55
  rexml (3.4.1)
56
- rspec (3.13.0)
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.3)
60
+ rspec-core (3.13.5)
61
61
  rspec-support (~> 3.13.0)
62
- rspec-expectations (3.13.4)
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 (1.3.1)
66
- rspec-core (>= 3.0.0)
67
- rspec-expectations (>= 3.0.0)
68
- rspec-mocks (3.13.3)
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.3)
72
- rubocop (1.75.4)
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.44.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.1)
102
+ simplecov-html (0.13.2)
103
103
  simplecov_json_formatter (0.1.4)
104
- standard (1.49.0)
104
+ standard (1.50.0)
105
105
  language_server-protocol (~> 3.17.0.2)
106
106
  lint_roller (~> 1.0)
107
- rubocop (~> 1.75.2)
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.3.2)
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 (~> 1.2)
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 :doc_stats => :doc do
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 :quality => [:spec, :standard, "bundle:audit:check"]
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
- May 7, 2023
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", "~> 1.2"
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 privides the naming methods.
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, :name, :shorthand,
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
- name = Parser.new(identifier)
64
- semitones = Semitones.new(name.degree_name.to_sym, name.quality_name).count
65
- higher_pitch = HeadMusic::Rudiment::Pitch.from_number_and_letter(HeadMusic::Rudiment::Pitch.middle_c + semitones, name.higher_letter)
66
- new(HeadMusic::Rudiment::Pitch.middle_c, higher_pitch)
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)