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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -4
  3. data/.github/workflows/release.yml +2 -2
  4. data/.github/workflows/security.yml +1 -1
  5. data/CHANGELOG.md +67 -0
  6. data/CLAUDE.md +20 -0
  7. data/Gemfile.lock +3 -3
  8. data/README.md +28 -0
  9. data/lib/head_music/content/staff.rb +3 -5
  10. data/lib/head_music/instruments/instrument.rb +2 -7
  11. data/lib/head_music/instruments/instrument_name.rb +1 -1
  12. data/lib/head_music/locales/en.yml +5 -0
  13. data/lib/head_music/notation/abc/body_lexer.rb +222 -299
  14. data/lib/head_music/notation/abc/chord_reader.rb +56 -0
  15. data/lib/head_music/notation/abc/chord_scanner.rb +78 -0
  16. data/lib/head_music/notation/abc/key_mapper.rb +24 -4
  17. data/lib/head_music/notation/abc/parser.rb +19 -101
  18. data/lib/head_music/notation/abc/repeat_tagger.rb +67 -0
  19. data/lib/head_music/notation/abc/token.rb +21 -0
  20. data/lib/head_music/notation/abc/voice_registry.rb +51 -0
  21. data/lib/head_music/notation/abc/volta_passes.rb +28 -0
  22. data/lib/head_music/notation/music_xml/lyric_writer.rb +60 -0
  23. data/lib/head_music/notation/music_xml/note_writer.rb +105 -0
  24. data/lib/head_music/notation/music_xml/writer.rb +13 -142
  25. data/lib/head_music/notation/music_xml/xml_text.rb +21 -0
  26. data/lib/head_music/rudiment/alteration.rb +20 -5
  27. data/lib/head_music/rudiment/alterations.yml +6 -0
  28. data/lib/head_music/rudiment/key_signature.rb +1 -1
  29. data/lib/head_music/rudiment/note.rb +6 -2
  30. data/lib/head_music/rudiment/scale.rb +3 -5
  31. data/lib/head_music/style/analysis.rb +7 -0
  32. data/lib/head_music/style/guide.rb +100 -0
  33. data/lib/head_music/style/guides/base.rb +38 -2
  34. data/lib/head_music/style/guides/configured.rb +62 -0
  35. data/lib/head_music/style/guides/contour_melody.rb +61 -0
  36. data/lib/head_music/style/guides/diatonic_melody.rb +0 -24
  37. data/lib/head_music/style/guides/species_harmony.rb +5 -0
  38. data/lib/head_music/style/guides/species_melody.rb +5 -0
  39. data/lib/head_music/utilities/accidentals.rb +137 -0
  40. data/lib/head_music/utilities/hash_key.rb +44 -29
  41. data/lib/head_music/version.rb +1 -1
  42. data/lib/head_music.rb +14 -14
  43. data/user-stories/done/accidental-string-normalization.md +552 -0
  44. data/user-stories/done/configurable-guide-registry.md +647 -0
  45. data/user-stories/index.html +11 -3
  46. metadata +17 -8
  47. data/lib/head_music/style/guides/arch_contour_melody.rb +0 -4
  48. data/lib/head_music/style/guides/ascending_contour_melody.rb +0 -4
  49. data/lib/head_music/style/guides/descending_contour_melody.rb +0 -4
  50. data/lib/head_music/style/guides/static_contour_melody.rb +0 -5
  51. data/lib/head_music/style/guides/valley_contour_melody.rb +0 -4
  52. 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: 5a439a4b39c708eb38984b3878e7bc0c02ce92d7c693d34041ed3b8c69acfffe
4
- data.tar.gz: d0b702f38b7de8826fe7a345c3b17d0a8ea8c963bd922343889ba97a47f3538e
3
+ metadata.gz: 7462797fd33041d3ab9fea4cf10cfaf329213d6f96d3f3b9cbb4f6a73dfd2890
4
+ data.tar.gz: 01c4cff6e9f609ce9543be019c340b3c7a01c21928e7248345d5a50364d85705
5
5
  SHA512:
6
- metadata.gz: 266406dca2449e15a0d72e4ac315190929aa048d18c6236f07d5eea23d5b220dcc037408edb7f5a2a508e677dee5bbf70bb96b620e1bcdcf75df145623efff9e
7
- data.tar.gz: 81fcf78c8ec6234b1478d0c6ca92acdc096bb714de76e33d1bd19bf21e52fce8c0b3831fa72b2e8ec2cbe4dad21d399886cd0e62c416934fd6778ab08d03308d
6
+ metadata.gz: c14082d1dc7e8405227b46be83e803b3037808c817200e8cadf18ead15dda70f17248d92a2c370d52f92eb9ea4241aa7dad07869e016979edc301c9d30f077aa
7
+ data.tar.gz: '06868ca14c8cca41f22c395cb04d84a6291492471cf034a9efd69afc4a822951cd2fc9ad4d2c7b4a94ed7d68b8bd70a8003090092b96291733ad1378f48c3d19'
@@ -16,7 +16,7 @@ jobs:
16
16
  activesupport-version: ['7.2', '8.0']
17
17
 
18
18
  steps:
19
- - uses: actions/checkout@v5
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@v5
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@v5
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@v5
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@v5
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@v2
34
+ uses: softprops/action-gh-release@v3
35
35
  with:
36
36
  files: '*.gem'
37
37
  generate_release_notes: true
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v5
18
+ - uses: actions/checkout@v7
19
19
 
20
20
  - name: Set up Ruby
21
21
  uses: ruby/setup-ruby@v1
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 (17.5.0)
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.1)
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.2)
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
- puts("Warning: Clef '#{default_clef_key}' not found.")
16
- if @instrument
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
- @default_clef = HeadMusic::Rudiment::Clef.get(:treble_clef)
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, variant_key = nil)
35
+ def get(name)
37
36
  return name if name.is_a?(self)
38
37
 
39
38
  name_str = name.to_s
40
- if variant_key
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)
@@ -44,6 +44,6 @@ class HeadMusic::Instruments::InstrumentName
44
44
  end
45
45
 
46
46
  def format_pitch_name(pitch_designation)
47
- pitch_designation.to_s.tr("b", "♭").tr("#", "♯")
47
+ HeadMusic::Utilities::Accidentals.to_unicode(pitch_designation)
48
48
  end
49
49
  end
@@ -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"