head_music 12.5.0 → 13.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -0
  3. data/CLAUDE.md +2 -2
  4. data/Gemfile.lock +10 -8
  5. data/Rakefile +2 -2
  6. data/lib/head_music/content/bar.rb +57 -2
  7. data/lib/head_music/content/comment.rb +32 -0
  8. data/lib/head_music/content/composition.rb +10 -2
  9. data/lib/head_music/instruments/instrument.rb +22 -18
  10. data/lib/head_music/instruments/instruments.yml +0 -539
  11. data/lib/head_music/notation/abc/body_lexer.rb +264 -0
  12. data/lib/head_music/notation/abc/book_parser.rb +76 -0
  13. data/lib/head_music/notation/abc/duration_resolver.rb +110 -0
  14. data/lib/head_music/notation/abc/header.rb +119 -0
  15. data/lib/head_music/notation/abc/key_mapper.rb +65 -0
  16. data/lib/head_music/notation/abc/parser.rb +279 -0
  17. data/lib/head_music/notation/abc/pitch_builder.rb +73 -0
  18. data/lib/head_music/notation/abc.rb +30 -0
  19. data/lib/head_music/notation/instrument_notation.rb +57 -0
  20. data/lib/head_music/notation/notation_style.rb +79 -0
  21. data/lib/head_music/notation/notation_styles.yml +692 -0
  22. data/lib/head_music/notation.rb +6 -0
  23. data/lib/head_music/style/annotation.rb +31 -1
  24. data/lib/head_music/style/guidelines/direction_changes.rb +10 -2
  25. data/lib/head_music/style/guidelines/four_per_bar.rb +2 -12
  26. data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -4
  27. data/lib/head_music/style/guidelines/large_leaps.rb +149 -0
  28. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +11 -2
  29. data/lib/head_music/style/guidelines/maximum_notes.rb +28 -0
  30. data/lib/head_music/style/guidelines/{at_least_eight_notes.rb → minimum_notes.rb} +16 -7
  31. data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -4
  32. data/lib/head_music/style/guidelines/mostly_conjunct.rb +7 -2
  33. data/lib/head_music/style/guidelines/note_count_per_bar.rb +26 -1
  34. data/lib/head_music/style/guidelines/one_per_bar.rb +2 -12
  35. data/lib/head_music/style/guidelines/second_species_break.rb +5 -1
  36. data/lib/head_music/style/guidelines/singable_intervals.rb +31 -5
  37. data/lib/head_music/style/guidelines/singable_range.rb +18 -2
  38. data/lib/head_music/style/guidelines/three_per_bar.rb +2 -12
  39. data/lib/head_music/style/guidelines/two_per_bar.rb +2 -12
  40. data/lib/head_music/style/guides/base.rb +10 -0
  41. data/lib/head_music/style/guides/combined_first_second_third_species_harmony.rb +1 -7
  42. data/lib/head_music/style/guides/combined_first_second_third_species_melody.rb +4 -9
  43. data/lib/head_music/style/guides/diatonic_melody.rb +21 -0
  44. data/lib/head_music/style/guides/fifth_species_harmony.rb +2 -8
  45. data/lib/head_music/style/guides/fifth_species_melody.rb +5 -10
  46. data/lib/head_music/style/guides/first_species_harmony.rb +2 -8
  47. data/lib/head_music/style/guides/first_species_melody.rb +2 -7
  48. data/lib/head_music/style/guides/fourth_species_harmony.rb +1 -7
  49. data/lib/head_music/style/guides/fourth_species_melody.rb +4 -9
  50. data/lib/head_music/style/guides/fux_cantus_firmus.rb +10 -10
  51. data/lib/head_music/style/guides/salzer_schachter_cantus_firmus.rb +23 -0
  52. data/lib/head_music/style/guides/second_species_harmony.rb +1 -7
  53. data/lib/head_music/style/guides/second_species_melody.rb +4 -9
  54. data/lib/head_music/style/guides/species_harmony.rb +14 -5
  55. data/lib/head_music/style/guides/species_melody.rb +13 -5
  56. data/lib/head_music/style/guides/third_species_harmony.rb +1 -7
  57. data/lib/head_music/style/guides/third_species_melody.rb +4 -9
  58. data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +1 -7
  59. data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +4 -9
  60. data/lib/head_music/version.rb +1 -1
  61. data/lib/head_music.rb +8 -5
  62. data/user-stories/_template.md +29 -0
  63. data/user-stories/backlog/lilypond-interpreter.md +75 -0
  64. data/user-stories/backlog/sixteenth-century-style.md +113 -0
  65. data/user-stories/backlog/split-counterpoint-species-by-author.md +105 -0
  66. data/user-stories/done/abc-notation-interpreter.md +300 -0
  67. data/user-stories/done/configurable-large-leap-recovery.md +322 -0
  68. data/user-stories/done/notation-style.md +405 -0
  69. data/user-stories/index.html +389 -0
  70. metadata +52 -32
  71. data/lib/head_music/style/guidelines/recover_large_leaps.rb +0 -43
  72. data/lib/head_music/style/guidelines/single_large_leaps.rb +0 -18
  73. data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +0 -19
  74. data/lib/head_music/style/guides/modern_cantus_firmus.rb +0 -25
  75. data/user_stories/backlog/notation-style.md +0 -183
  76. /data/{user_stories → user-stories}/backlog/organizing-content.md +0 -0
  77. /data/{user_stories → user-stories}/done/consonance-dissonance-classification.md +0 -0
  78. /data/{user_stories → user-stories}/done/dyad-analysis.md +0 -0
  79. /data/{user_stories → user-stories}/done/epic--score-order/PLAN.md +0 -0
  80. /data/{user_stories → user-stories}/done/epic--score-order/band-score-order.md +0 -0
  81. /data/{user_stories → user-stories}/done/epic--score-order/chamber-ensemble-score-order.md +0 -0
  82. /data/{user_stories → user-stories}/done/epic--score-order/orchestral-score-order.md +0 -0
  83. /data/{user_stories → user-stories}/done/expand-playing-techniques.md +0 -0
  84. /data/{user_stories → user-stories}/done/handle-time.md +0 -0
  85. /data/{user_stories → user-stories}/done/handle-time.rb +0 -0
  86. /data/{user_stories → user-stories}/done/instrument-architecture.md +0 -0
  87. /data/{user_stories → user-stories}/done/instrument-variant.md +0 -0
  88. /data/{user_stories → user-stories}/done/move-musical-symbol-to-notation.md +0 -0
  89. /data/{user_stories → user-stories}/done/move-staff-mapping-to-notation.md +0 -0
  90. /data/{user_stories → user-stories}/done/move-staff-position-to-notation.md +0 -0
  91. /data/{user_stories → user-stories}/done/notation-module-foundation.md +0 -0
  92. /data/{user_stories → user-stories}/done/percussion_set.md +0 -0
  93. /data/{user_stories → user-stories}/done/pitch-class-set-analysis.md +0 -0
  94. /data/{user_stories → user-stories}/done/sonority-identification.md +0 -0
  95. /data/{user_stories → user-stories}/done/string-pitches.md +0 -0
  96. /data/{user_stories → user-stories}/done/superclass-for-note.md +0 -0
  97. /data/{user_stories → user-stories}/epics/notation-module.md +0 -0
  98. /data/{user_stories → user-stories}/visioning/agentic-daw.md +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a02a5ef281eecc76bcf953aa51c04078858b4588f2b85c0b1a4d1f4d88464d6
4
- data.tar.gz: 5a31a94133ffbb97944524d75aedcbbfcbf7ce769fe6049559818a1940438442
3
+ metadata.gz: ef07dfecedad63290f5f352704f96967dff09ba5bc305adf04b25c033bf580fb
4
+ data.tar.gz: fd20210a40bd733fada3307f6b9b772bd317168132443d164aa417a0d2c490bd
5
5
  SHA512:
6
- metadata.gz: 369c959ae6f69cd609a65e4dee6b4aace0283d107132fec61f3b35afadfeb27d5386c60e969d88cfe626bdc0873e3c5dbed736513d93523e68c5dfca4127b6f1
7
- data.tar.gz: 0f04cf766ce824188270ad00fe29866f652befcb0ff0d5c3eeaadd21e286b088bfb8883fe8687f94c0eaf2e216c5a2ee0377afc91756df18e9e587b5fef0eb41
6
+ metadata.gz: cee6694acb9303262ac645153f47462d5e6870e62853bc800d00e731dc0fc5321c00d2e75109c6805a1c2ba0d1b62ddbfdc8196bd445d52062097fd05c24d9eb
7
+ data.tar.gz: 8ee30bdeaf48faf0f43411352763c6d67ec3f5919c707054bcdee4ab07514936d09c335a03fa031e7df59479c597aeb4c165765b8c6de5210d9d45edd5639e3a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [13.0.0] - 2026-07-05
11
+
12
+ ### Added
13
+
14
+ - `HeadMusic::Notation::NotationStyle` — named notation traditions (`british_brass_band`, `german`, `italian`, `concert_pitch`) resolved as sparse overlays on a `default` style, backed by `notation_styles.yml`, with `.get`/`.default` factories and `#notation_for`
15
+ - `HeadMusic::Notation::InstrumentNotation` — the resolved notation value object (clef, sounding transposition, staves, and recorded register/clef alternatives) with value equality
16
+ - `Instrument#notation(style:)` — notate an instrument through a chosen notation style, defaulting to `default`
17
+
18
+ ### Changed
19
+
20
+ - Notation concerns (clef, sounding transposition, staff structure) now live in `NotationStyle` instead of on the instrument. `Instrument`'s notation methods (`default_staves`, `default_clefs`, `sounding_transposition`, etc.) delegate to the default style and resolve to the same values as before.
21
+ - `Instrument#staff_schemes` now returns only the instrument's default scheme; named schemes (brass-band, German/Italian bass clarinet, and register/clef alternatives) have moved into notation styles.
22
+
23
+ ### Removed
24
+
25
+ - `staff_schemes` data from `instruments.yml` and the internal `staff_schemes` plumbing on `Instrument`. Per-instrument notation conventions are now expressed as notation styles. (Breaking change — hence the major version bump.)
26
+
27
+ ## [12.6.0] - 2026-07-03
28
+
29
+ ### Added
30
+
31
+ - `DiatonicMelody` guide: a free diatonic melody not bound to cantus firmus start/end constraints (note-count range configurable, defaulting to 5–24)
32
+ - Configurable guidelines — a guideline can now carry configuration into a `RULESET` via `Annotation.with(...)` (wrapped in `Annotation::Configured`):
33
+ - `MinimumNotes` / `MaximumNotes` — configurable note-count floor and ceiling (`AtLeastEightNotes` / `UpToFourteenNotes` retained as named defaults)
34
+ - `NoteCountPerBar` — configurable `count` and `rhythmic_value` (unifies `OnePerBar`, `TwoPerBar`, `ThreePerBar`, `FourPerBar`)
35
+ - `DirectionChanges` — configurable `maximum_notes_per_direction` (unifies `ModerateDirectionChanges` and `FrequentDirectionChanges`)
36
+ - Configurable thresholds on `SingableRange`, `MostlyConjunct`, `LimitOctaveLeaps`, and `SecondSpeciesBreak`
37
+
38
+ ### Changed
39
+
40
+ - Extracted `HeadMusic::Style::Guides::Base` for shared guide analysis behavior; `SpeciesMelody` and `SpeciesHarmony` now inherit from it
41
+ - Hoisted the guidelines common to every guide into `MELODIC_CORE` / `HARMONIC_CORE` constants on the species base classes
42
+ - `SingableRange`'s message now reflects the configured range
43
+ - Renamed the `quality` rake task to `validate`
44
+
45
+ ## [12.5.0] - 2026-04-08
46
+
47
+ ### Changed
48
+
49
+ - Improved fifth-species counterpoint guidelines
50
+ - Code quality improvements
51
+
52
+ ### Removed
53
+
54
+ - Combined 2+3+4 species guides and their guidelines
55
+
56
+ ## [12.4.0] - 2026-04-06
57
+
58
+ ### Added
59
+
60
+ - Fifth-species (florid) counterpoint guides
61
+ - Standard and alternate instrument tunings
62
+
63
+ ### Fixed
64
+
65
+ - Ukulele family stringings, tunings, and range data
66
+
67
+ ## [12.3.0] - 2026-02-25
68
+
69
+ ### Added
70
+
71
+ - Fourth-species counterpoint guides
72
+
73
+ ### Changed
74
+
75
+ - Code quality pass
76
+
77
+ ## [12.2.0] - 2026-02-24
78
+
79
+ ### Changed
80
+
81
+ - Improved guidelines for first-bar entry
82
+
83
+ ## [12.1.0] - 2026-02-24
84
+
85
+ ### Changed
86
+
87
+ - Refactored species guidelines into separate first-bar, middle-bar, and final-bar rules
88
+ - Unified `FinalBarWholeNote` and `FinalBarDottedHalfNote` into `NoteFillsFinalBar`
89
+
90
+ ## [12.0.1] - 2026-02-23
91
+
92
+ ### Changed
93
+
94
+ - Renamed triple-meter guides
95
+
96
+ ## [12.0.0] - 2026-02-21
97
+
98
+ ### Added
99
+
100
+ - Third-species 3:1 guidelines
101
+ - Allow a descending minor sixth as a singable interval
102
+
103
+ ### Changed
104
+
105
+ - Extracted shared base classes for guides and step-to-final-note guidelines
106
+ - Refactored third-species dissonance handling and other files to reduce code smells
107
+
108
+ ## [11.8.0] - 2026-02-16
109
+
110
+ ### Added
111
+
112
+ - `ThirdSpeciesMelody` and `ThirdSpeciesHarmony` guides
113
+ - `ThirdSpeciesDissonanceTreatment` guideline
114
+ - `FourToOne` guideline
115
+ - Third-species counterpoint reference document
116
+
117
+ ## [11.7.0] - 2026-02-13
118
+
119
+ ### Added
120
+
121
+ - Parallel-perfect check for first species
122
+
123
+ ### Changed
124
+
125
+ - `Analysis#fitness` now uses the geometric mean
126
+
127
+ ## [11.6.1] - 2026-02-12
128
+
129
+ ### Added
130
+
131
+ - Additional test coverage for the two-to-one guideline
132
+
133
+ ### Fixed
134
+
135
+ - Accept an implied rest in the first bar of second-species counterpoint
136
+
10
137
  ## [11.6.0] - 2026-02-10
11
138
 
12
139
  ### Added
data/CLAUDE.md CHANGED
@@ -26,8 +26,8 @@ bundle exec rspec spec/head_music/rudiments/pitch_spec.rb
26
26
  # Run linting
27
27
  bundle exec rubocop
28
28
 
29
- # Run all quality checks (tests, linting, security)
30
- bundle exec rake quality
29
+ # Run all validation checks (tests, linting, security)
30
+ bundle exec rake validate
31
31
 
32
32
  # Open interactive console with gem loaded
33
33
  bin/console
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (12.5.0)
4
+ head_music (13.0.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (~> 2.0)
7
7
  i18n (~> 1.8)
@@ -9,27 +9,27 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (7.2.2.1)
12
+ activesupport (8.1.3)
13
13
  base64
14
- benchmark (>= 0.3)
15
14
  bigdecimal
16
15
  concurrent-ruby (~> 1.0, >= 1.3.1)
17
16
  connection_pool (>= 2.2.5)
18
17
  drb
19
18
  i18n (>= 1.6, < 2)
19
+ json
20
20
  logger (>= 1.4.2)
21
21
  minitest (>= 5.1)
22
22
  securerandom (>= 0.3)
23
23
  tzinfo (~> 2.0, >= 2.0.5)
24
- addressable (2.8.7)
25
- public_suffix (>= 2.0.2, < 7.0)
24
+ uri (>= 0.13.1)
25
+ addressable (2.9.0)
26
+ public_suffix (>= 2.0.2, < 8.0)
26
27
  ast (2.4.3)
27
28
  axiom-types (0.1.1)
28
29
  descendants_tracker (~> 0.0.4)
29
30
  ice_nine (~> 0.11.0)
30
31
  thread_safe (~> 0.3, >= 0.3.1)
31
32
  base64 (0.3.0)
32
- benchmark (0.4.1)
33
33
  bigdecimal (3.2.2)
34
34
  bundler-audit (0.9.2)
35
35
  bundler (>= 1.2.0, < 3)
@@ -38,7 +38,7 @@ GEM
38
38
  logger (~> 1.5)
39
39
  coercible (1.0.0)
40
40
  descendants_tracker (~> 0.0.1)
41
- concurrent-ruby (1.3.5)
41
+ concurrent-ruby (1.3.7)
42
42
  connection_pool (2.5.3)
43
43
  descendants_tracker (0.0.4)
44
44
  thread_safe (~> 0.3, >= 0.3.1)
@@ -201,16 +201,18 @@ GEM
201
201
  unicode-display_width (3.1.4)
202
202
  unicode-emoji (~> 4.0, >= 4.0.4)
203
203
  unicode-emoji (4.0.4)
204
+ uri (1.1.1)
204
205
  virtus (2.0.0)
205
206
  axiom-types (~> 0.1)
206
207
  coercible (~> 1.0)
207
208
  descendants_tracker (~> 0.0, >= 0.0.3)
208
- yard (0.9.37)
209
+ yard (0.9.44)
209
210
  zeitwerk (2.7.3)
210
211
 
211
212
  PLATFORMS
212
213
  arm64-darwin-22
213
214
  arm64-darwin-23
215
+ arm64-darwin-25
214
216
  x86_64-linux
215
217
 
216
218
  DEPENDENCIES
data/Rakefile CHANGED
@@ -33,8 +33,8 @@ end
33
33
 
34
34
  task default: :spec
35
35
 
36
- desc "Run all quality checks (tests, linting, security audit, code quality)"
37
- task quality: [:spec, :standard, "bundle:audit:check", :rubycritic]
36
+ desc "Run all validation checks (tests, linting, security audit, code quality)"
37
+ task validate: [:spec, :standard, "bundle:audit:check", :rubycritic]
38
38
 
39
39
  desc "Open an irb session preloaded with this library"
40
40
  task :console do
@@ -3,17 +3,72 @@ module HeadMusic::Content; end
3
3
 
4
4
  # Representation of a bar in a composition
5
5
  # Encapsulates meter and key signature changes
6
+ # and repeat structure (repeat barlines and volta brackets) as content semantics
6
7
  class HeadMusic::Content::Bar
7
- attr_reader :composition
8
+ attr_reader :composition, :ends_repeat_after_num_plays, :plays_on_passes
8
9
  attr_accessor :key_signature, :meter
10
+ attr_writer :starts_repeat
9
11
 
10
12
  def initialize(composition, key_signature: nil, meter: nil)
11
13
  @composition = composition
12
14
  @key_signature = HeadMusic::Rudiment::KeySignature.get(key_signature) if key_signature
13
15
  @meter = HeadMusic::Rudiment::Meter.get(meter) if meter
16
+ @starts_repeat = false
17
+ @ends_repeat_after_num_plays = nil
18
+ @plays_on_passes = nil
19
+ end
20
+
21
+ def starts_repeat?
22
+ @starts_repeat
23
+ end
24
+
25
+ def ends_repeat_after_num_plays=(value)
26
+ unless valid_ends_repeat_after_num_plays?(value)
27
+ raise ArgumentError, "ends_repeat_after_num_plays must be nil or an integer of at least 2"
28
+ end
29
+ @ends_repeat_after_num_plays = value
30
+ end
31
+
32
+ def ends_repeat?
33
+ !ends_repeat_after_num_plays.nil?
34
+ end
35
+
36
+ def plays_on_passes=(value)
37
+ unless valid_plays_on_passes?(value)
38
+ raise ArgumentError, "plays_on_passes must be nil or a non-empty array of unique positive integers"
39
+ end
40
+ @plays_on_passes = value
41
+ end
42
+
43
+ def plays_on_pass?(pass_number)
44
+ plays_on_passes.nil? || plays_on_passes.include?(pass_number)
14
45
  end
15
46
 
16
47
  def to_s
17
- ["Bar", key_signature, meter].compact.join(" ")
48
+ ["Bar", key_signature, meter, repeat_summary].compact.join(" ")
49
+ end
50
+
51
+ private
52
+
53
+ def valid_ends_repeat_after_num_plays?(value)
54
+ return true if value.nil?
55
+
56
+ value.is_a?(Integer) && value >= 2
57
+ end
58
+
59
+ def valid_plays_on_passes?(value)
60
+ return true if value.nil?
61
+
62
+ value.is_a?(Array) && !value.empty? &&
63
+ value.all? { |pass| pass.is_a?(Integer) && pass.positive? } &&
64
+ value.uniq.length == value.length
65
+ end
66
+
67
+ def repeat_summary
68
+ parts = []
69
+ parts << "|:" if starts_repeat?
70
+ parts << ":|x#{ends_repeat_after_num_plays}" if ends_repeat?
71
+ parts << "(passes #{plays_on_passes.join(",")})" if plays_on_passes
72
+ parts.join(" ") unless parts.empty?
18
73
  end
19
74
  end
@@ -0,0 +1,32 @@
1
+ # A module for musical content
2
+ module HeadMusic::Content; end
3
+
4
+ # A comment is a free-text annotation, optionally anchored to a position in a composition.
5
+ class HeadMusic::Content::Comment
6
+ attr_reader :composition, :text, :position
7
+
8
+ def initialize(composition, text, position = nil)
9
+ @composition = composition
10
+ @text = text
11
+ ensure_position(position)
12
+ end
13
+
14
+ def to_s
15
+ text
16
+ end
17
+
18
+ private
19
+
20
+ def ensure_position(position)
21
+ return if position.nil?
22
+
23
+ @position = if position.is_a?(HeadMusic::Content::Position)
24
+ unless position.composition.equal?(composition)
25
+ raise ArgumentError, "position belongs to a different composition"
26
+ end
27
+ position
28
+ else
29
+ HeadMusic::Content::Position.new(composition, position)
30
+ end
31
+ end
32
+ end
@@ -3,11 +3,14 @@ module HeadMusic::Content; end
3
3
 
4
4
  # A composition is musical content.
5
5
  class HeadMusic::Content::Composition
6
- attr_reader :name, :key_signature, :meter, :voices
6
+ attr_reader :name, :key_signature, :meter, :voices, :composer, :origin, :comments
7
7
 
8
- def initialize(name: nil, key_signature: nil, meter: nil)
8
+ def initialize(name: nil, key_signature: nil, meter: nil, composer: nil, origin: nil, comments: nil)
9
9
  ensure_attributes(name, key_signature, meter)
10
+ @composer = composer
11
+ @origin = origin
10
12
  @voices = []
13
+ @comments = Array(comments).map { |text| HeadMusic::Content::Comment.new(self, text) }
11
14
  end
12
15
 
13
16
  def add_voice(role: nil)
@@ -15,6 +18,11 @@ class HeadMusic::Content::Composition
15
18
  @voices.last
16
19
  end
17
20
 
21
+ def add_comment(text, position = nil)
22
+ @comments << HeadMusic::Content::Comment.new(self, text, position)
23
+ @comments.last
24
+ end
25
+
18
26
  def meter_at(bar_number)
19
27
  meter_change = last_meter_change(bar_number)
20
28
  meter_change ? meter_change.meter : meter
@@ -21,13 +21,12 @@ module HeadMusic::Instruments; end
21
21
  # pitch_key: the pitch designation (e.g., "b_flat", "a", "c")
22
22
  # alias_name_keys: alternative names for the instrument
23
23
  # range_categories: size/range classifications
24
- # staff_schemes: notation schemes (to be moved to NotationStyle later)
25
24
  class HeadMusic::Instruments::Instrument
26
25
  include HeadMusic::Named
27
26
 
28
27
  INSTRUMENTS = YAML.load_file(File.expand_path("instruments.yml", __dir__)).freeze
29
28
 
30
- attr_reader :name_key, :parent_key, :alias_name_keys, :range_categories, :staff_schemes_data
29
+ attr_reader :name_key, :parent_key, :alias_name_keys, :range_categories
31
30
 
32
31
  class << self
33
32
  # Factory method to get an Instrument instance
@@ -110,18 +109,29 @@ class HeadMusic::Instruments::Instrument
110
109
  @pitch_designation ||= HeadMusic::Rudiment::Spelling.get(pitch_key_to_designation)
111
110
  end
112
111
 
113
- # Staff schemes (notation concern - kept for backward compatibility)
112
+ # Notation for this instrument in the given style (defaults to :default).
113
+ def notation(style: :default)
114
+ HeadMusic::Notation::NotationStyle.get(style).notation_for(self)
115
+ end
116
+
117
+ # Staff schemes are a notation concern; they now live in NotationStyle.
118
+ # These methods remain for backward compatibility and delegate to the
119
+ # default style. Referenced only inside method bodies, because the Notation
120
+ # module loads after Instruments (see head_music.rb load order).
114
121
  def staff_schemes
115
- @staff_schemes ||= build_staff_schemes
122
+ [default_staff_scheme]
116
123
  end
117
124
 
118
125
  def default_staff_scheme
119
- @default_staff_scheme ||=
120
- staff_schemes.find(&:default?) || staff_schemes.first
126
+ @default_staff_scheme ||= HeadMusic::Instruments::StaffScheme.new(
127
+ key: "default",
128
+ instrument: self,
129
+ list: default_notation_staves_data
130
+ )
121
131
  end
122
132
 
123
133
  def default_staves
124
- default_staff_scheme&.staves || []
134
+ default_staff_scheme.staves
125
135
  end
126
136
 
127
137
  def default_clefs
@@ -254,7 +264,6 @@ class HeadMusic::Instruments::Instrument
254
264
  @pitch_key = record["pitch_key"]
255
265
  @alias_name_keys = record["alias_name_keys"] || []
256
266
  @range_categories = record["range_categories"] || []
257
- @staff_schemes_data = record["staff_schemes"] || {}
258
267
 
259
268
  initialize_name
260
269
  end
@@ -305,15 +314,10 @@ class HeadMusic::Instruments::Instrument
305
314
  end
306
315
  end
307
316
 
308
- def build_staff_schemes
309
- return parent&.staff_schemes || [] if staff_schemes_data.empty?
310
-
311
- staff_schemes_data.map do |key, list|
312
- HeadMusic::Instruments::StaffScheme.new(
313
- key: key,
314
- instrument: self,
315
- list: list
316
- )
317
- end
317
+ # The raw staff-attribute list for this instrument's default notation,
318
+ # resolved from the default NotationStyle.
319
+ def default_notation_staves_data
320
+ notation = HeadMusic::Notation::NotationStyle.default.notation_for(self)
321
+ (notation&.staves || []).map(&:attributes)
318
322
  end
319
323
  end