head_music 12.6.0 → 14.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +9 -8
  4. data/lib/head_music/content/bar.rb +57 -2
  5. data/lib/head_music/content/comment.rb +32 -0
  6. data/lib/head_music/content/composition.rb +10 -2
  7. data/lib/head_music/instruments/instrument.rb +22 -18
  8. data/lib/head_music/instruments/instruments.yml +0 -539
  9. data/lib/head_music/notation/abc/body_lexer.rb +264 -0
  10. data/lib/head_music/notation/abc/book_parser.rb +76 -0
  11. data/lib/head_music/notation/abc/duration_resolver.rb +110 -0
  12. data/lib/head_music/notation/abc/header.rb +119 -0
  13. data/lib/head_music/notation/abc/key_mapper.rb +65 -0
  14. data/lib/head_music/notation/abc/parser.rb +279 -0
  15. data/lib/head_music/notation/abc/pitch_builder.rb +73 -0
  16. data/lib/head_music/notation/abc.rb +30 -0
  17. data/lib/head_music/notation/instrument_notation.rb +57 -0
  18. data/lib/head_music/notation/notation_style.rb +79 -0
  19. data/lib/head_music/notation/notation_styles.yml +692 -0
  20. data/lib/head_music/notation.rb +6 -0
  21. data/lib/head_music/style/guidelines/contoured.rb +134 -0
  22. data/lib/head_music/style/guidelines/large_leaps.rb +149 -0
  23. data/lib/head_music/style/guidelines/maximum_notes.rb +6 -2
  24. data/lib/head_music/style/guidelines/minimum_notes.rb +6 -2
  25. data/lib/head_music/style/guidelines/singable_intervals.rb +31 -5
  26. data/lib/head_music/style/guides/arch_contour_melody.rb +7 -0
  27. data/lib/head_music/style/guides/ascending_contour_melody.rb +7 -0
  28. data/lib/head_music/style/guides/descending_contour_melody.rb +7 -0
  29. data/lib/head_music/style/guides/diatonic_melody.rb +14 -4
  30. data/lib/head_music/style/guides/fux_cantus_firmus.rb +9 -4
  31. data/lib/head_music/style/guides/salzer_schachter_cantus_firmus.rb +23 -0
  32. data/lib/head_music/style/guides/static_contour_melody.rb +7 -0
  33. data/lib/head_music/style/guides/valley_contour_melody.rb +7 -0
  34. data/lib/head_music/style/guides/wave_contour_melody.rb +7 -0
  35. data/lib/head_music/version.rb +1 -1
  36. data/lib/head_music.rb +10 -5
  37. data/user-stories/_template.md +29 -0
  38. data/user-stories/backlog/lilypond-interpreter.md +75 -0
  39. data/user-stories/backlog/sixteenth-century-style.md +113 -0
  40. data/user-stories/backlog/split-counterpoint-species-by-author.md +105 -0
  41. data/user-stories/done/abc-notation-interpreter.md +300 -0
  42. data/user-stories/done/configurable-large-leap-recovery.md +322 -0
  43. data/user-stories/done/melody-contour-guides.md +267 -0
  44. data/user-stories/done/notation-style.md +405 -0
  45. data/user-stories/index.html +469 -0
  46. metadata +55 -31
  47. data/lib/head_music/style/guidelines/at_least_eight_notes.rb +0 -7
  48. data/lib/head_music/style/guidelines/recover_large_leaps.rb +0 -43
  49. data/lib/head_music/style/guidelines/single_large_leaps.rb +0 -18
  50. data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +0 -7
  51. data/lib/head_music/style/guides/modern_cantus_firmus.rb +0 -20
  52. data/user_stories/backlog/notation-style.md +0 -183
  53. /data/{user_stories → user-stories}/backlog/organizing-content.md +0 -0
  54. /data/{user_stories → user-stories}/done/consonance-dissonance-classification.md +0 -0
  55. /data/{user_stories → user-stories}/done/dyad-analysis.md +0 -0
  56. /data/{user_stories → user-stories}/done/epic--score-order/PLAN.md +0 -0
  57. /data/{user_stories → user-stories}/done/epic--score-order/band-score-order.md +0 -0
  58. /data/{user_stories → user-stories}/done/epic--score-order/chamber-ensemble-score-order.md +0 -0
  59. /data/{user_stories → user-stories}/done/epic--score-order/orchestral-score-order.md +0 -0
  60. /data/{user_stories → user-stories}/done/expand-playing-techniques.md +0 -0
  61. /data/{user_stories → user-stories}/done/handle-time.md +0 -0
  62. /data/{user_stories → user-stories}/done/handle-time.rb +0 -0
  63. /data/{user_stories → user-stories}/done/instrument-architecture.md +0 -0
  64. /data/{user_stories → user-stories}/done/instrument-variant.md +0 -0
  65. /data/{user_stories → user-stories}/done/move-musical-symbol-to-notation.md +0 -0
  66. /data/{user_stories → user-stories}/done/move-staff-mapping-to-notation.md +0 -0
  67. /data/{user_stories → user-stories}/done/move-staff-position-to-notation.md +0 -0
  68. /data/{user_stories → user-stories}/done/notation-module-foundation.md +0 -0
  69. /data/{user_stories → user-stories}/done/percussion_set.md +0 -0
  70. /data/{user_stories → user-stories}/done/pitch-class-set-analysis.md +0 -0
  71. /data/{user_stories → user-stories}/done/sonority-identification.md +0 -0
  72. /data/{user_stories → user-stories}/done/string-pitches.md +0 -0
  73. /data/{user_stories → user-stories}/done/superclass-for-note.md +0 -0
  74. /data/{user_stories → user-stories}/epics/notation-module.md +0 -0
  75. /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: db91f6ff65b081bb369f3d2a85e2bc6e6cd092bb0018c2c416bf36bcfc4d4e9d
4
- data.tar.gz: 266914dfbc382dc27591ea37302b57571b8693f2e0e4bd5189d2fffdff2a7c00
3
+ metadata.gz: ed605ba621e60e4222568efe6071c86daace2bbf21c6c577b7ccfda1fb67f769
4
+ data.tar.gz: b4a0f23c074d7d5bca6eb849a4e01ba099631c87d7cef89e3fd130c294a20803
5
5
  SHA512:
6
- metadata.gz: d53ea28ee01b301ae1070a94fe8929d00b649c7d581976183d20e0bbfd080ccf4c08003719f2280001fb7d6ed00cfe1a5a634bd69b046d5859db56dfab6c6bb5
7
- data.tar.gz: 9cd2929bddff0033d3caa84832e97a77a920cfc61e4e2d55d4d0be338f1ef7904beaad73748f6d6cc4ef93aea5a3b06292e16b285d0b61d5ad78b4ba2818932b
6
+ metadata.gz: 888d95ec1fab0517520899150d8caf794f7ec4edc7fa19e19d370d761d4541287712d207beb4b6f6772e0da62615258607c8ce5dbfb93d9311d5e32c692bbbc8
7
+ data.tar.gz: 759c97ee1504589dfbd6f91c2ac2bad6aa685ce70cf413ffc76eea2bdd4864fc6598b56d742d5e34d5f3ac528f88548830ca502ef7f866d034b0a349c5cf7e8f
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [14.0.0] - 2026-07-05
11
+
12
+ ### Added
13
+
14
+ - `HeadMusic::Style::Guidelines::Contoured` — configurable guideline judging a melody against a chosen contour (`Contoured.with(:arch)` and five other keys: `ascending`, `descending`, `valley`, `wave`, `static`). Predicates are trend-based rather than strictly monotonic; a wrong contour receives a single mark spanning the melody. Unknown contour keys raise `ArgumentError` at guide-definition time.
15
+ - Six contour guides subclassing `Guides::DiatonicMelody`, each appending the configured `Contoured` guideline to the inherited ruleset: `ArchContourMelody`, `AscendingContourMelody`, `DescendingContourMelody`, `StaticContourMelody`, `ValleyContourMelody`, `WaveContourMelody`
16
+ - Contour judgments deliberately complement `ConsonantClimax`: an arch requires only an interior climax pitch level, leaving climax uniqueness and consonance to the existing guideline
17
+
18
+ ## [13.0.0] - 2026-07-05
19
+
20
+ ### Added
21
+
22
+ - `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`
23
+ - `HeadMusic::Notation::InstrumentNotation` — the resolved notation value object (clef, sounding transposition, staves, and recorded register/clef alternatives) with value equality
24
+ - `Instrument#notation(style:)` — notate an instrument through a chosen notation style, defaulting to `default`
25
+
26
+ ### Changed
27
+
28
+ - 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.
29
+ - `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.
30
+
31
+ ### Removed
32
+
33
+ - `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.)
34
+
10
35
  ## [12.6.0] - 2026-07-03
11
36
 
12
37
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (12.6.0)
4
+ head_music (14.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,11 +201,12 @@ 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
@@ -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