head_music 18.0.0 → 19.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/.github/workflows/ci.yml +4 -4
- data/.github/workflows/release.yml +2 -2
- data/.github/workflows/security.yml +1 -1
- data/CHANGELOG.md +40 -0
- data/Gemfile.lock +3 -3
- data/README.md +28 -0
- data/lib/head_music/instruments/instrument.rb +2 -7
- data/lib/head_music/locales/en.yml +5 -0
- data/lib/head_music/notation/abc/body_lexer.rb +222 -299
- data/lib/head_music/notation/abc/chord_reader.rb +56 -0
- data/lib/head_music/notation/abc/chord_scanner.rb +78 -0
- data/lib/head_music/notation/abc/parser.rb +19 -101
- data/lib/head_music/notation/abc/repeat_tagger.rb +67 -0
- data/lib/head_music/notation/abc/token.rb +21 -0
- data/lib/head_music/notation/abc/voice_registry.rb +51 -0
- data/lib/head_music/notation/abc/volta_passes.rb +28 -0
- data/lib/head_music/notation/music_xml/lyric_writer.rb +60 -0
- data/lib/head_music/notation/music_xml/note_writer.rb +105 -0
- data/lib/head_music/notation/music_xml/writer.rb +13 -142
- data/lib/head_music/notation/music_xml/xml_text.rb +21 -0
- data/lib/head_music/style/analysis.rb +7 -0
- data/lib/head_music/style/guide.rb +100 -0
- data/lib/head_music/style/guides/base.rb +38 -2
- data/lib/head_music/style/guides/configured.rb +62 -0
- data/lib/head_music/style/guides/contour_melody.rb +61 -0
- data/lib/head_music/style/guides/diatonic_melody.rb +0 -24
- data/lib/head_music/style/guides/species_harmony.rb +5 -0
- data/lib/head_music/style/guides/species_melody.rb +5 -0
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +5 -6
- data/user-stories/done/configurable-guide-registry.md +647 -0
- data/user-stories/index.html +4 -0
- metadata +15 -8
- data/lib/head_music/style/guides/arch_contour_melody.rb +0 -4
- data/lib/head_music/style/guides/ascending_contour_melody.rb +0 -4
- data/lib/head_music/style/guides/descending_contour_melody.rb +0 -4
- data/lib/head_music/style/guides/static_contour_melody.rb +0 -5
- data/lib/head_music/style/guides/valley_contour_melody.rb +0 -4
- data/lib/head_music/style/guides/wave_contour_melody.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7462797fd33041d3ab9fea4cf10cfaf329213d6f96d3f3b9cbb4f6a73dfd2890
|
|
4
|
+
data.tar.gz: 01c4cff6e9f609ce9543be019c340b3c7a01c21928e7248345d5a50364d85705
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c14082d1dc7e8405227b46be83e803b3037808c817200e8cadf18ead15dda70f17248d92a2c370d52f92eb9ea4241aa7dad07869e016979edc301c9d30f077aa
|
|
7
|
+
data.tar.gz: '06868ca14c8cca41f22c395cb04d84a6291492471cf034a9efd69afc4a822951cd2fc9ad4d2c7b4a94ed7d68b8bd70a8003090092b96291733ad1378f48c3d19'
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
activesupport-version: ['7.2', '8.0']
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
20
|
|
|
21
21
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
22
22
|
uses: ruby/setup-ruby@v1
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
|
|
35
35
|
- name: Upload coverage to Codecov
|
|
36
36
|
if: matrix.ruby-version == '3.3' && matrix.activesupport-version == '8.0'
|
|
37
|
-
uses: codecov/codecov-action@
|
|
37
|
+
uses: codecov/codecov-action@v7
|
|
38
38
|
with:
|
|
39
39
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
40
40
|
fail_ci_if_error: false
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
lint:
|
|
44
44
|
runs-on: ubuntu-latest
|
|
45
45
|
steps:
|
|
46
|
-
- uses: actions/checkout@
|
|
46
|
+
- uses: actions/checkout@v7
|
|
47
47
|
|
|
48
48
|
- name: Set up Ruby
|
|
49
49
|
uses: ruby/setup-ruby@v1
|
|
@@ -57,7 +57,7 @@ jobs:
|
|
|
57
57
|
build:
|
|
58
58
|
runs-on: ubuntu-latest
|
|
59
59
|
steps:
|
|
60
|
-
- uses: actions/checkout@
|
|
60
|
+
- uses: actions/checkout@v7
|
|
61
61
|
|
|
62
62
|
- name: Set up Ruby
|
|
63
63
|
uses: ruby/setup-ruby@v1
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
id-token: write
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
17
|
|
|
18
18
|
- name: Set up Ruby
|
|
19
19
|
uses: ruby/setup-ruby@v1
|
|
@@ -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@
|
|
34
|
+
uses: softprops/action-gh-release@v3
|
|
35
35
|
with:
|
|
36
36
|
files: '*.gem'
|
|
37
37
|
generate_release_notes: true
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `HeadMusic::Style::Guide` — a lookup facade over every style guide in the gem. `Guide.get("first_species_harmony")` resolves a key to a guide; an unknown key returns `nil` rather than raising, so a consumer can ask about a guide the gem does not have. `Guide.get!` raises `KeyError` instead, and `Guide.known?`, `.all`, `.keys`, and `.key_for` round out the surface. Keys are stable strings suitable for storing in a database. The registry holds twenty-three:
|
|
13
|
+
|
|
14
|
+
| Category | Keys |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `:melody` | `fux_cantus_firmus`, `salzer_schachter_cantus_firmus`, `diatonic_melody`, `first_species_melody`, `second_species_melody`, `third_species_melody`, `third_species_triple_meter_melody`, `fourth_species_melody`, `combined_first_second_third_species_melody`, `fifth_species_melody`, `arch_contour_melody`, `ascending_contour_melody`, `descending_contour_melody`, `static_contour_melody`, `valley_contour_melody`, `wave_contour_melody` |
|
|
17
|
+
| `:harmony` | `first_species_harmony`, `second_species_harmony`, `third_species_harmony`, `third_species_triple_meter_harmony`, `fourth_species_harmony`, `combined_first_second_third_species_harmony`, `fifth_species_harmony` |
|
|
18
|
+
|
|
19
|
+
- Guides now carry identity: `.key` (snake_case of the class name), `.category` (`:melody` or `:harmony`, derived from the `SpeciesMelody`/`SpeciesHarmony` ancestry), and `.display_name` (localizable, with a computed English default). Consumers no longer need to hand-maintain a map of guide constants to categories.
|
|
20
|
+
- `HeadMusic::Style::Guides::Configured` — the guide-layer twin of `Annotation::Configured`. It pairs a guide class with options and answers `analyze(voice)`, so it drops into `Style::Analysis` wherever a guide class was expected. `Guides::Base.with(**options)` returns one, and `#with` layers further options without dropping earlier ones.
|
|
21
|
+
- `HeadMusic::Style::Guides::ContourMelody` — one configurable guide replacing the six contour subclasses: `ContourMelody.with(contour: :arch, minimum_melodic_intervals: 2)`. An invalid contour raises `ArgumentError` at configuration time, not at analysis.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Breaking.** The six contour guide classes are removed: `ArchContourMelody`, `AscendingContourMelody`, `DescendingContourMelody`, `StaticContourMelody`, `ValleyContourMelody`, and `WaveContourMelody`. Their rulesets are unchanged and still reachable — by key, through the registry. Migrate constant references to `Guide.get`:
|
|
26
|
+
|
|
27
|
+
| Removed constant | Replacement |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `Guides::ArchContourMelody` | `Style::Guide.get("arch_contour_melody")` |
|
|
30
|
+
| `Guides::AscendingContourMelody` | `Style::Guide.get("ascending_contour_melody")` |
|
|
31
|
+
| `Guides::DescendingContourMelody` | `Style::Guide.get("descending_contour_melody")` |
|
|
32
|
+
| `Guides::StaticContourMelody` | `Style::Guide.get("static_contour_melody")` |
|
|
33
|
+
| `Guides::ValleyContourMelody` | `Style::Guide.get("valley_contour_melody")` |
|
|
34
|
+
| `Guides::WaveContourMelody` | `Style::Guide.get("wave_contour_melody")` |
|
|
35
|
+
|
|
36
|
+
- **Breaking.** `Guides::DiatonicMelody.contour_ruleset` and `Guides::DiatonicMelody::CONTOUR_PEER_WEIGHT_BUDGET` are removed. The budget constant now lives on the guide that uses it, as `Guides::ContourMelody::PEER_WEIGHT_BUDGET`.
|
|
37
|
+
- **Breaking.** Neither a configured guide nor `Guides::ContourMelody` has a `::RULESET` constant — read `#ruleset` or `.ruleset(**options)` instead. `Guides::Base.ruleset` is now a method rather than a bare constant read, and `ContourMelody` deliberately does not descend from `DiatonicMelody`, so a guide whose ruleset depends on configuration raises rather than silently resolving an ancestor's ruleset through Ruby's constant lookup.
|
|
38
|
+
- **Breaking.** `guide.name` on a configured guide returns the underlying class name, which is shared across all six contour configurations. Persist `guide.key`, not `guide.name`; use `guide.display_name` for presentation.
|
|
39
|
+
- `Style::Analysis.new` now raises `ArgumentError` when given something that cannot answer `analyze`. Previously a `nil` guide — the result of an unrecognized key — surfaced much later as a `NoMethodError` on `nil`.
|
|
40
|
+
- **Breaking.** `Guides::Base.with` raises `ArgumentError` when given options for a guide that takes none, rather than accepting them and failing at the first `analyze` with Ruby's bare `wrong number of arguments (given 1, expected 0)`. A guide accepts configuration by declaring keywords on `.ruleset`, so `FuxCantusFirmus.with(contour: :arch)` now names the guide and the options it cannot take. `Guides::Configured` resolves its ruleset in its constructor for the same reason: a configuration error belongs at `.with`, not mid-grading.
|
|
41
|
+
- **Breaking.** `Instruments::Instrument.get` no longer accepts a second positional argument. The deprecated two-argument form joined its arguments with an underscore, so `Instrument.get("trumpet", "in_c")` becomes `Instrument.get("trumpet_in_c")`. It never emitted a runtime deprecation warning, so check call sites rather than logs. The two-argument form also fell back to the base instrument when the combined name did not exist — `get("trumpet", "in_q")` returned the trumpet — where `get("trumpet_in_q")` returns `nil`.
|
|
42
|
+
- The six contour rulesets are byte-for-byte what they were: the same ten rubric peers sharing φ⁻² evenly, `Contoured` at φ⁻¹, and the same per-contour motion gate (omitted for `static`). Fitness values are unchanged.
|
|
43
|
+
|
|
44
|
+
- **Breaking.** The ABC parser and lexer and the MusicXML writer each shed a responsibility to a collaborator, leaving no file in the gem above a RubyCritic C. Behavior is unchanged and no public method signature moved, but four constants did, and the old names no longer resolve:
|
|
45
|
+
- `Notation::ABC::BodyLexer::Token` is now `Notation::ABC::Token`. The token type is consumed by `Parser` and `Preflight`, not just the lexer.
|
|
46
|
+
- `Notation::ABC::BodyLexer::ChordNote` is now `Notation::ABC::ChordScanner::ChordNote`, alongside the bracket-chord scanning that produces it.
|
|
47
|
+
- `Notation::ABC::Parser::REPEAT_ENDING_STYLES`, `REPEAT_STARTING_STYLES`, and `SECTION_ENDING_STYLES` are now on `Notation::ABC::RepeatTagger`, which applies the repeat and volta marks they describe.
|
|
48
|
+
- `Notation::MusicXML::Writer::XML_ESCAPES` is now `Notation::MusicXML::XmlText::ESCAPES`, shared with the note and lyric writers. `Writer::INDENT` still resolves, through the same module.
|
|
49
|
+
|
|
10
50
|
## [18.0.0] - 2026-07-27
|
|
11
51
|
|
|
12
52
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
head_music (
|
|
4
|
+
head_music (19.0.0)
|
|
5
5
|
activesupport (>= 7.0, < 10)
|
|
6
6
|
humanize (>= 2, < 4)
|
|
7
7
|
i18n (~> 1.8)
|
|
@@ -87,7 +87,7 @@ GEM
|
|
|
87
87
|
i18n (1.15.2)
|
|
88
88
|
concurrent-ruby (~> 1.0)
|
|
89
89
|
ice_nine (0.11.2)
|
|
90
|
-
json (2.21.
|
|
90
|
+
json (2.21.2)
|
|
91
91
|
kramdown (2.5.2)
|
|
92
92
|
rexml (>= 3.4.4)
|
|
93
93
|
language_server-protocol (3.17.0.6)
|
|
@@ -180,7 +180,7 @@ GEM
|
|
|
180
180
|
virtus (~> 2.0)
|
|
181
181
|
securerandom (0.4.1)
|
|
182
182
|
sexp_processor (4.17.5)
|
|
183
|
-
simplecov (1.0.
|
|
183
|
+
simplecov (1.0.3)
|
|
184
184
|
standard (1.56.0)
|
|
185
185
|
language_server-protocol (~> 3.17.0.2)
|
|
186
186
|
lint_roller (~> 1.0)
|
data/README.md
CHANGED
|
@@ -52,6 +52,34 @@ chord = HeadMusic::Analysis::PitchSet.new(pitches)
|
|
|
52
52
|
puts chord.major_triad? # => true
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Style Analysis
|
|
56
|
+
|
|
57
|
+
Look up a style guide by key and analyze a voice against it. `Style::Guide.get` returns `nil` for an
|
|
58
|
+
unknown key, so a stored key can be validated before use.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
guide = HeadMusic::Style::Guide.get('first_species_harmony')
|
|
62
|
+
guide.category # => :harmony
|
|
63
|
+
guide.display_name # => "First Species Harmony"
|
|
64
|
+
|
|
65
|
+
analysis = HeadMusic::Style::Analysis.new(guide, voice)
|
|
66
|
+
analysis.fitness # => 0.0 to 1.0
|
|
67
|
+
analysis.messages # => ["Prefer contrary motion. Move voices in different melodic directions."]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Guides whose ruleset varies by configuration are built with `.with`. The six contour melodies are
|
|
71
|
+
registered under their own keys, and each key is exactly one such configuration:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
HeadMusic::Style::Guide.get('arch_contour_melody')
|
|
75
|
+
# the same guide, spelled out
|
|
76
|
+
HeadMusic::Style::Guides::ContourMelody.with(contour: :arch, minimum_melodic_intervals: 2)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Configure it differently and you get a different guide — one the registry does not hold, whose `key`
|
|
80
|
+
is `nil` and whose `display_name` falls back to the class. Prefer the key when you mean a registered
|
|
81
|
+
guide, and `.with` when you deliberately want a configuration of your own.
|
|
82
|
+
|
|
55
83
|
## Documentation
|
|
56
84
|
|
|
57
85
|
- **API Documentation**: [rubydoc.info/gems/head_music](https://rubydoc.info/gems/head_music)
|
|
@@ -31,17 +31,12 @@ class HeadMusic::Instruments::Instrument
|
|
|
31
31
|
class << self
|
|
32
32
|
# Factory method to get an Instrument instance
|
|
33
33
|
# @param name [String, Symbol] instrument name (e.g., "clarinet", "clarinet_in_a")
|
|
34
|
-
# @param variant_key [String, Symbol, nil] DEPRECATED: variant key (for backward compatibility)
|
|
35
34
|
# @return [Instrument, nil] instrument instance or nil if not found
|
|
36
|
-
def get(name
|
|
35
|
+
def get(name)
|
|
37
36
|
return name if name.is_a?(self)
|
|
38
37
|
|
|
39
38
|
name_str = name.to_s
|
|
40
|
-
|
|
41
|
-
find_valid_instrument("#{name_str}_#{variant_key}") || find_valid_instrument(name_str)
|
|
42
|
-
else
|
|
43
|
-
find_valid_instrument(name_str) || find_valid_instrument(normalize_variant_name(name_str))
|
|
44
|
-
end
|
|
39
|
+
find_valid_instrument(name_str) || find_valid_instrument(normalize_variant_name(name_str))
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
def find_valid_instrument(name)
|
|
@@ -566,3 +566,8 @@ en:
|
|
|
566
566
|
staff: staff
|
|
567
567
|
tuba: tuba
|
|
568
568
|
tuning: tuning
|
|
569
|
+
style:
|
|
570
|
+
guides:
|
|
571
|
+
# Only keys whose humanized default is wrong need an entry here;
|
|
572
|
+
# HeadMusic::Style::Guide.display_name_for derives the rest.
|
|
573
|
+
salzer_schachter_cantus_firmus: "Salzer–Schachter Cantus Firmus"
|