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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +9 -8
- data/lib/head_music/content/bar.rb +57 -2
- data/lib/head_music/content/comment.rb +32 -0
- data/lib/head_music/content/composition.rb +10 -2
- data/lib/head_music/instruments/instrument.rb +22 -18
- data/lib/head_music/instruments/instruments.yml +0 -539
- data/lib/head_music/notation/abc/body_lexer.rb +264 -0
- data/lib/head_music/notation/abc/book_parser.rb +76 -0
- data/lib/head_music/notation/abc/duration_resolver.rb +110 -0
- data/lib/head_music/notation/abc/header.rb +119 -0
- data/lib/head_music/notation/abc/key_mapper.rb +65 -0
- data/lib/head_music/notation/abc/parser.rb +279 -0
- data/lib/head_music/notation/abc/pitch_builder.rb +73 -0
- data/lib/head_music/notation/abc.rb +30 -0
- data/lib/head_music/notation/instrument_notation.rb +57 -0
- data/lib/head_music/notation/notation_style.rb +79 -0
- data/lib/head_music/notation/notation_styles.yml +692 -0
- data/lib/head_music/notation.rb +6 -0
- data/lib/head_music/style/guidelines/contoured.rb +134 -0
- data/lib/head_music/style/guidelines/large_leaps.rb +149 -0
- data/lib/head_music/style/guidelines/maximum_notes.rb +6 -2
- data/lib/head_music/style/guidelines/minimum_notes.rb +6 -2
- data/lib/head_music/style/guidelines/singable_intervals.rb +31 -5
- data/lib/head_music/style/guides/arch_contour_melody.rb +7 -0
- data/lib/head_music/style/guides/ascending_contour_melody.rb +7 -0
- data/lib/head_music/style/guides/descending_contour_melody.rb +7 -0
- data/lib/head_music/style/guides/diatonic_melody.rb +14 -4
- data/lib/head_music/style/guides/fux_cantus_firmus.rb +9 -4
- data/lib/head_music/style/guides/salzer_schachter_cantus_firmus.rb +23 -0
- data/lib/head_music/style/guides/static_contour_melody.rb +7 -0
- data/lib/head_music/style/guides/valley_contour_melody.rb +7 -0
- data/lib/head_music/style/guides/wave_contour_melody.rb +7 -0
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +10 -5
- data/user-stories/_template.md +29 -0
- data/user-stories/backlog/lilypond-interpreter.md +75 -0
- data/user-stories/backlog/sixteenth-century-style.md +113 -0
- data/user-stories/backlog/split-counterpoint-species-by-author.md +105 -0
- data/user-stories/done/abc-notation-interpreter.md +300 -0
- data/user-stories/done/configurable-large-leap-recovery.md +322 -0
- data/user-stories/done/melody-contour-guides.md +267 -0
- data/user-stories/done/notation-style.md +405 -0
- data/user-stories/index.html +469 -0
- metadata +55 -31
- data/lib/head_music/style/guidelines/at_least_eight_notes.rb +0 -7
- data/lib/head_music/style/guidelines/recover_large_leaps.rb +0 -43
- data/lib/head_music/style/guidelines/single_large_leaps.rb +0 -18
- data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +0 -7
- data/lib/head_music/style/guides/modern_cantus_firmus.rb +0 -20
- data/user_stories/backlog/notation-style.md +0 -183
- /data/{user_stories → user-stories}/backlog/organizing-content.md +0 -0
- /data/{user_stories → user-stories}/done/consonance-dissonance-classification.md +0 -0
- /data/{user_stories → user-stories}/done/dyad-analysis.md +0 -0
- /data/{user_stories → user-stories}/done/epic--score-order/PLAN.md +0 -0
- /data/{user_stories → user-stories}/done/epic--score-order/band-score-order.md +0 -0
- /data/{user_stories → user-stories}/done/epic--score-order/chamber-ensemble-score-order.md +0 -0
- /data/{user_stories → user-stories}/done/epic--score-order/orchestral-score-order.md +0 -0
- /data/{user_stories → user-stories}/done/expand-playing-techniques.md +0 -0
- /data/{user_stories → user-stories}/done/handle-time.md +0 -0
- /data/{user_stories → user-stories}/done/handle-time.rb +0 -0
- /data/{user_stories → user-stories}/done/instrument-architecture.md +0 -0
- /data/{user_stories → user-stories}/done/instrument-variant.md +0 -0
- /data/{user_stories → user-stories}/done/move-musical-symbol-to-notation.md +0 -0
- /data/{user_stories → user-stories}/done/move-staff-mapping-to-notation.md +0 -0
- /data/{user_stories → user-stories}/done/move-staff-position-to-notation.md +0 -0
- /data/{user_stories → user-stories}/done/notation-module-foundation.md +0 -0
- /data/{user_stories → user-stories}/done/percussion_set.md +0 -0
- /data/{user_stories → user-stories}/done/pitch-class-set-analysis.md +0 -0
- /data/{user_stories → user-stories}/done/sonority-identification.md +0 -0
- /data/{user_stories → user-stories}/done/string-pitches.md +0 -0
- /data/{user_stories → user-stories}/done/superclass-for-note.md +0 -0
- /data/{user_stories → user-stories}/epics/notation-module.md +0 -0
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed605ba621e60e4222568efe6071c86daace2bbf21c6c577b7ccfda1fb67f769
|
|
4
|
+
data.tar.gz: b4a0f23c074d7d5bca6eb849a4e01ba099631c87d7cef89e3fd130c294a20803
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
25
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
122
|
+
[default_staff_scheme]
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
def default_staff_scheme
|
|
119
|
-
@default_staff_scheme ||=
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|