head_music 12.5.0 → 13.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 +127 -0
- data/CLAUDE.md +2 -2
- data/Gemfile.lock +10 -8
- data/Rakefile +2 -2
- 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/annotation.rb +31 -1
- data/lib/head_music/style/guidelines/direction_changes.rb +10 -2
- data/lib/head_music/style/guidelines/four_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -4
- data/lib/head_music/style/guidelines/large_leaps.rb +149 -0
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +11 -2
- data/lib/head_music/style/guidelines/maximum_notes.rb +28 -0
- data/lib/head_music/style/guidelines/{at_least_eight_notes.rb → minimum_notes.rb} +16 -7
- data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -4
- data/lib/head_music/style/guidelines/mostly_conjunct.rb +7 -2
- data/lib/head_music/style/guidelines/note_count_per_bar.rb +26 -1
- data/lib/head_music/style/guidelines/one_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/second_species_break.rb +5 -1
- data/lib/head_music/style/guidelines/singable_intervals.rb +31 -5
- data/lib/head_music/style/guidelines/singable_range.rb +18 -2
- data/lib/head_music/style/guidelines/three_per_bar.rb +2 -12
- data/lib/head_music/style/guidelines/two_per_bar.rb +2 -12
- data/lib/head_music/style/guides/base.rb +10 -0
- data/lib/head_music/style/guides/combined_first_second_third_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/combined_first_second_third_species_melody.rb +4 -9
- data/lib/head_music/style/guides/diatonic_melody.rb +21 -0
- data/lib/head_music/style/guides/fifth_species_harmony.rb +2 -8
- data/lib/head_music/style/guides/fifth_species_melody.rb +5 -10
- data/lib/head_music/style/guides/first_species_harmony.rb +2 -8
- data/lib/head_music/style/guides/first_species_melody.rb +2 -7
- data/lib/head_music/style/guides/fourth_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/fourth_species_melody.rb +4 -9
- data/lib/head_music/style/guides/fux_cantus_firmus.rb +10 -10
- data/lib/head_music/style/guides/salzer_schachter_cantus_firmus.rb +23 -0
- data/lib/head_music/style/guides/second_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/second_species_melody.rb +4 -9
- data/lib/head_music/style/guides/species_harmony.rb +14 -5
- data/lib/head_music/style/guides/species_melody.rb +13 -5
- data/lib/head_music/style/guides/third_species_harmony.rb +1 -7
- data/lib/head_music/style/guides/third_species_melody.rb +4 -9
- data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +1 -7
- data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +4 -9
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +8 -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/notation-style.md +405 -0
- data/user-stories/index.html +389 -0
- metadata +52 -32
- 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 -19
- data/lib/head_music/style/guides/modern_cantus_firmus.rb +0 -25
- 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: ef07dfecedad63290f5f352704f96967dff09ba5bc305adf04b25c033bf580fb
|
|
4
|
+
data.tar.gz: fd20210a40bd733fada3307f6b9b772bd317168132443d164aa417a0d2c490bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cee6694acb9303262ac645153f47462d5e6870e62853bc800d00e731dc0fc5321c00d2e75109c6805a1c2ba0d1b62ddbfdc8196bd445d52062097fd05c24d9eb
|
|
7
|
+
data.tar.gz: 8ee30bdeaf48faf0f43411352763c6d67ec3f5919c707054bcdee4ab07514936d09c335a03fa031e7df59479c597aeb4c165765b8c6de5210d9d45edd5639e3a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [13.0.0] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `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`
|
|
15
|
+
- `HeadMusic::Notation::InstrumentNotation` — the resolved notation value object (clef, sounding transposition, staves, and recorded register/clef alternatives) with value equality
|
|
16
|
+
- `Instrument#notation(style:)` — notate an instrument through a chosen notation style, defaulting to `default`
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
- `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.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- `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.)
|
|
26
|
+
|
|
27
|
+
## [12.6.0] - 2026-07-03
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `DiatonicMelody` guide: a free diatonic melody not bound to cantus firmus start/end constraints (note-count range configurable, defaulting to 5–24)
|
|
32
|
+
- Configurable guidelines — a guideline can now carry configuration into a `RULESET` via `Annotation.with(...)` (wrapped in `Annotation::Configured`):
|
|
33
|
+
- `MinimumNotes` / `MaximumNotes` — configurable note-count floor and ceiling (`AtLeastEightNotes` / `UpToFourteenNotes` retained as named defaults)
|
|
34
|
+
- `NoteCountPerBar` — configurable `count` and `rhythmic_value` (unifies `OnePerBar`, `TwoPerBar`, `ThreePerBar`, `FourPerBar`)
|
|
35
|
+
- `DirectionChanges` — configurable `maximum_notes_per_direction` (unifies `ModerateDirectionChanges` and `FrequentDirectionChanges`)
|
|
36
|
+
- Configurable thresholds on `SingableRange`, `MostlyConjunct`, `LimitOctaveLeaps`, and `SecondSpeciesBreak`
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Extracted `HeadMusic::Style::Guides::Base` for shared guide analysis behavior; `SpeciesMelody` and `SpeciesHarmony` now inherit from it
|
|
41
|
+
- Hoisted the guidelines common to every guide into `MELODIC_CORE` / `HARMONIC_CORE` constants on the species base classes
|
|
42
|
+
- `SingableRange`'s message now reflects the configured range
|
|
43
|
+
- Renamed the `quality` rake task to `validate`
|
|
44
|
+
|
|
45
|
+
## [12.5.0] - 2026-04-08
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Improved fifth-species counterpoint guidelines
|
|
50
|
+
- Code quality improvements
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
|
|
54
|
+
- Combined 2+3+4 species guides and their guidelines
|
|
55
|
+
|
|
56
|
+
## [12.4.0] - 2026-04-06
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Fifth-species (florid) counterpoint guides
|
|
61
|
+
- Standard and alternate instrument tunings
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- Ukulele family stringings, tunings, and range data
|
|
66
|
+
|
|
67
|
+
## [12.3.0] - 2026-02-25
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- Fourth-species counterpoint guides
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- Code quality pass
|
|
76
|
+
|
|
77
|
+
## [12.2.0] - 2026-02-24
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- Improved guidelines for first-bar entry
|
|
82
|
+
|
|
83
|
+
## [12.1.0] - 2026-02-24
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- Refactored species guidelines into separate first-bar, middle-bar, and final-bar rules
|
|
88
|
+
- Unified `FinalBarWholeNote` and `FinalBarDottedHalfNote` into `NoteFillsFinalBar`
|
|
89
|
+
|
|
90
|
+
## [12.0.1] - 2026-02-23
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Renamed triple-meter guides
|
|
95
|
+
|
|
96
|
+
## [12.0.0] - 2026-02-21
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- Third-species 3:1 guidelines
|
|
101
|
+
- Allow a descending minor sixth as a singable interval
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- Extracted shared base classes for guides and step-to-final-note guidelines
|
|
106
|
+
- Refactored third-species dissonance handling and other files to reduce code smells
|
|
107
|
+
|
|
108
|
+
## [11.8.0] - 2026-02-16
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- `ThirdSpeciesMelody` and `ThirdSpeciesHarmony` guides
|
|
113
|
+
- `ThirdSpeciesDissonanceTreatment` guideline
|
|
114
|
+
- `FourToOne` guideline
|
|
115
|
+
- Third-species counterpoint reference document
|
|
116
|
+
|
|
117
|
+
## [11.7.0] - 2026-02-13
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- Parallel-perfect check for first species
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
- `Analysis#fitness` now uses the geometric mean
|
|
126
|
+
|
|
127
|
+
## [11.6.1] - 2026-02-12
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- Additional test coverage for the two-to-one guideline
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- Accept an implied rest in the first bar of second-species counterpoint
|
|
136
|
+
|
|
10
137
|
## [11.6.0] - 2026-02-10
|
|
11
138
|
|
|
12
139
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -26,8 +26,8 @@ bundle exec rspec spec/head_music/rudiments/pitch_spec.rb
|
|
|
26
26
|
# Run linting
|
|
27
27
|
bundle exec rubocop
|
|
28
28
|
|
|
29
|
-
# Run all
|
|
30
|
-
bundle exec rake
|
|
29
|
+
# Run all validation checks (tests, linting, security)
|
|
30
|
+
bundle exec rake validate
|
|
31
31
|
|
|
32
32
|
# Open interactive console with gem loaded
|
|
33
33
|
bin/console
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
head_music (
|
|
4
|
+
head_music (13.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,16 +201,18 @@ 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
|
|
212
213
|
arm64-darwin-22
|
|
213
214
|
arm64-darwin-23
|
|
215
|
+
arm64-darwin-25
|
|
214
216
|
x86_64-linux
|
|
215
217
|
|
|
216
218
|
DEPENDENCIES
|
data/Rakefile
CHANGED
|
@@ -33,8 +33,8 @@ end
|
|
|
33
33
|
|
|
34
34
|
task default: :spec
|
|
35
35
|
|
|
36
|
-
desc "Run all
|
|
37
|
-
task
|
|
36
|
+
desc "Run all validation checks (tests, linting, security audit, code quality)"
|
|
37
|
+
task validate: [:spec, :standard, "bundle:audit:check", :rubycritic]
|
|
38
38
|
|
|
39
39
|
desc "Open an irb session preloaded with this library"
|
|
40
40
|
task :console do
|
|
@@ -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
|