head_music 17.5.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 +67 -0
- data/CLAUDE.md +20 -0
- data/Gemfile.lock +3 -3
- data/README.md +28 -0
- data/lib/head_music/content/staff.rb +3 -5
- data/lib/head_music/instruments/instrument.rb +2 -7
- data/lib/head_music/instruments/instrument_name.rb +1 -1
- 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/key_mapper.rb +24 -4
- 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/rudiment/alteration.rb +20 -5
- data/lib/head_music/rudiment/alterations.yml +6 -0
- data/lib/head_music/rudiment/key_signature.rb +1 -1
- data/lib/head_music/rudiment/note.rb +6 -2
- data/lib/head_music/rudiment/scale.rb +3 -5
- 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/utilities/accidentals.rb +137 -0
- data/lib/head_music/utilities/hash_key.rb +44 -29
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +14 -14
- data/user-stories/done/accidental-string-normalization.md +552 -0
- data/user-stories/done/configurable-guide-registry.md +647 -0
- data/user-stories/index.html +11 -3
- metadata +17 -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,73 @@ 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
|
+
|
|
50
|
+
## [18.0.0] - 2026-07-27
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `HeadMusic::Utilities::Accidentals` converts accidentals in a string between ASCII spellings and canonical unicode glyphs, replacing the hand-rolled `gsub` and `tr` chains that were scattered across the gem and disagreed with each other. `.to_unicode` converts whole chord symbols rather than bare pitch names — `Bbmaj7#11` becomes `B♭maj7♯11` — and `.to_ascii` converts back, emitting the spellings the gem's own parsers accept (`x` for a double sharp, never `##`). Both directions are idempotent, and conversion is all-or-nothing per accidental so a double never half-converts.
|
|
55
|
+
- The forward direction is safe to run over prose. Conversion happens only inside a candidate chord token, an uppercase letter name that does not continue a longer word or number, so `Above`, `Bebop`, `1.0b2`, and `0x1B2F` are untouched while `Bb`, `Ab7`, `Bbm`, and `Ebmaj7` convert. A chord-quality allowlist rescues `Bbm` and `Ebmaj7` from the word-boundary guard that protects `Above`; the allowlist was measured against a full system word list and is pinned by a spec that sweeps it.
|
|
56
|
+
- `##` is now accepted everywhere `bb` already was, ending the asymmetry between the two doubles. `Alteration.get("##")` resolves to `double_sharp` and `Spelling.get("C##")` returns `C𝄪`.
|
|
57
|
+
- The utility is deliberately narrower than `Spelling` and `Pitch` in two ways, both consequences of being safe to run over prose. It is case-sensitive, because a case-insensitive rule reads the `B` of `B7` as a flat sign — so `to_unicode("eb")` is `"eb"` while `Spelling.get("eb")` is `E♭`. And a bare token is read as an accidental rather than a pitch name — `to_unicode("bb")` is `𝄫`, the double-flat sign, while `Spelling.get("bb")` is `B♭`. Parse with `Spelling` first when a pitch name may be lowercase. `to_ascii` preserves `♮` for the same reason: dropping it would turn the valid spelling `C♮` into the different spelling `C`.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **Breaking.** `Spelling.get("C##")` returns a `Spelling` instead of `nil`, and the same widening applies to `Pitch`, `Note`, `KeySignature`, and ABC `K:` fields. Code that treated that `nil` as a validation signal now silently accepts double sharps.
|
|
62
|
+
- **Breaking.** `Alteration::PATTERN`, `Alteration::MATCHER`, and `Alteration::SYMBOLS` gain `##` and are now sorted longest-first so that a two-character spelling always matches before its one-character prefix. Code embedding these in its own regular expressions will see different matches.
|
|
63
|
+
- **Breaking.** `Alteration.symbols` now returns `SYMBOLS` rather than a separately derived list that disagreed with it.
|
|
64
|
+
- **Breaking.** `Utilities::HashKey` normalizes ASCII accidentals before mapping them to word suffixes, so ASCII and unicode spellings of the same accidental now produce the same key. Previously it handled ASCII `#` but not ASCII `b`, so `"C#"` keyed to `:c_sharp` while `"Bb"` keyed to `:bb`. Now `"Bb"` keys to `:b_flat`, `"Bbb"` to `:b_double_flat`, and `"Cx"` to `:c_double_sharp`. Words containing those letters are unaffected — `:bass_clarinet` and `:blues_major_pentatonic` are unchanged — because normalization goes through the prose-safe `Utilities::Accidentals` rather than a bare substitution. The glyph-to-word table is now derived from `Alteration` rather than hard-coded, leaving `alterations.yml` as the single inventory.
|
|
65
|
+
- `Utilities::HashKey` is class-methods-only, matching `Utilities::Case`. `.new` and `#to_sym` are gone; `.for` was already the only entry point in use. The per-instance memo they wrapped could never be hit twice, since each instance was discarded after one call.
|
|
66
|
+
- **Breaking.** `Analysis::Dyad` gains double sharps in its enharmonic universe, which were previously built as candidates and then silently discarded by the parser. Candidate spellings grow from 28 to 35, and a major third gains three respellings.
|
|
67
|
+
- **Breaking.** `Rudiment::Scale::SCALE_REGEX` is removed. It was unreferenced, and its singles-only `[#b]?` was an instance of the asymmetry this release closes.
|
|
68
|
+
- **Breaking.** `KeySignature` and `Scale` memoization keys change for identifiers containing accidentals. `Fx`, `C##`, and `F𝄪` now unify onto one cache entry, as they name the same key signature.
|
|
69
|
+
- `Rudiment::Note::PITCH_PATTERN` no longer truncates a double sharp. `"C##4"` previously parsed as `["C", "#", nil]`, silently losing the register, because the optional quantifier bound to the pattern's final alternative rather than to the whole alternation.
|
|
70
|
+
- `Alteration#representations` now spans every symbol record rather than only the primary one, so a double sharp reports `"##"` alongside `"x"` and `𝄪`. It also accepts a `locale_code:` keyword and memoizes per locale, since the list includes the localized `#name`.
|
|
71
|
+
- An ABC `K:` field naming a double-altered tonic still raises `ParseError`, but with a message that says so — `Cannot express double-altered tonic Cx in an ABC K: field` rather than the incidental `Unrecognized mode` it produced when the narrower key pattern failed to match the accidental.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
- `Content::Staff` no longer prints to standard output when it cannot resolve a clef key. It previously wrote `Warning: Clef '...' not found.` and `Using instrument clef.` to stdout. The fallback behavior is unchanged — an unrecognized key still resolves to the instrument's clef when an instrument is present, and to the treble clef otherwise — it is simply no longer announced.
|
|
76
|
+
|
|
10
77
|
## [17.5.0] - 2026-07-21
|
|
11
78
|
|
|
12
79
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -150,6 +150,26 @@ The gem supports multiple languages through the HeadMusic::Named mixin:
|
|
|
150
150
|
- Shared examples in `spec/support/`
|
|
151
151
|
- `composition_context.rb` provides test utilities
|
|
152
152
|
|
|
153
|
+
**Never assert on console output.** Do not use `output(...).to_stdout` or `.to_stderr`. A library should not print to the console, so a spec that asserts it printed something is pinning behavior the gem should not have — and it makes the printing hard to remove later, because deleting the `puts` breaks a test that looks unrelated to the change.
|
|
154
|
+
|
|
155
|
+
Test the behavior the message was describing instead. When a fallback or recovery path is worth pinning, assert what it produces and that it does not raise:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# Not this
|
|
159
|
+
it "prints a warning" do
|
|
160
|
+
expect { staff }.to output(/Warning: Clef 'foo' not found/).to_stdout
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# This
|
|
164
|
+
it "recovers from the unrecognized key instead of raising" do
|
|
165
|
+
expect { staff }.not_to raise_error
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "prefers the instrument's clef over the generic fallback" do
|
|
169
|
+
expect(staff.clef).not_to eq described_class.new("foo").clef
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
153
173
|
### Documentation Philosophy
|
|
154
174
|
|
|
155
175
|
This project deliberately deprioritizes formal documentation in favor of clear, comprehensive tests.
|
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)
|
|
@@ -12,12 +12,10 @@ class HeadMusic::Content::Staff
|
|
|
12
12
|
begin
|
|
13
13
|
@default_clef = HeadMusic::Rudiment::Clef.get(default_clef_key)
|
|
14
14
|
rescue KeyError, NoMethodError
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
puts("Using instrument clef.")
|
|
18
|
-
@default_clef = @instrument.default_staves.first.clef
|
|
15
|
+
@default_clef = if @instrument
|
|
16
|
+
@instrument.default_staves.first.clef
|
|
19
17
|
else
|
|
20
|
-
|
|
18
|
+
HeadMusic::Rudiment::Clef.get(:treble_clef)
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
@line_count = line_count || DEFAULT_LINE_COUNT
|
|
@@ -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"
|