head_music 8.2.1 → 9.0.1
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 +1 -1
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +53 -0
- data/CLAUDE.md +151 -0
- data/Gemfile.lock +25 -25
- data/MUSIC_THEORY.md +120 -0
- data/Rakefile +2 -2
- data/bin/check_instrument_consistency.rb +86 -0
- data/check_instrument_consistency.rb +0 -0
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +50 -27
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/content/note.rb +1 -1
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/position.rb +1 -1
- data/lib/head_music/content/staff.rb +1 -1
- data/lib/head_music/instruments/instrument.rb +103 -113
- data/lib/head_music/instruments/instrument_families.yml +10 -9
- data/lib/head_music/instruments/instrument_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- data/lib/head_music/instruments/instruments.yml +350 -368
- data/lib/head_music/instruments/score_order.rb +139 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/variant.rb +6 -0
- data/lib/head_music/locales/de.yml +6 -0
- data/lib/head_music/locales/en.yml +98 -0
- data/lib/head_music/locales/es.yml +6 -0
- data/lib/head_music/locales/fr.yml +6 -0
- data/lib/head_music/locales/it.yml +6 -0
- data/lib/head_music/locales/ru.yml +6 -0
- data/lib/head_music/rudiment/alteration.rb +23 -8
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +37 -4
- data/lib/head_music/rudiment/diatonic_context.rb +25 -0
- data/lib/head_music/rudiment/key.rb +77 -0
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
- data/lib/head_music/rudiment/key_signature.rb +46 -7
- data/lib/head_music/rudiment/letter_name.rb +3 -3
- data/lib/head_music/rudiment/meter.rb +19 -9
- data/lib/head_music/rudiment/mode.rb +92 -0
- data/lib/head_music/rudiment/musical_symbol.rb +1 -1
- data/lib/head_music/rudiment/note.rb +112 -0
- data/lib/head_music/rudiment/pitch/parser.rb +52 -0
- data/lib/head_music/rudiment/pitch.rb +5 -6
- data/lib/head_music/rudiment/pitch_class.rb +1 -1
- data/lib/head_music/rudiment/quality.rb +1 -1
- data/lib/head_music/rudiment/reference_pitch.rb +1 -1
- data/lib/head_music/rudiment/register.rb +4 -1
- data/lib/head_music/rudiment/rest.rb +36 -0
- data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +104 -29
- data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
- data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
- data/lib/head_music/rudiment/scale.rb +4 -5
- data/lib/head_music/rudiment/scale_degree.rb +9 -4
- data/lib/head_music/rudiment/scale_type.rb +9 -3
- data/lib/head_music/rudiment/solmization.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +5 -4
- data/lib/head_music/rudiment/tempo.rb +85 -0
- data/lib/head_music/rudiment/tonal_context.rb +35 -0
- data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
- data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
- data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
- data/lib/head_music/rudiment/tuning.rb +18 -4
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- data/lib/head_music/style/annotation.rb +4 -4
- data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
- data/lib/head_music/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +34 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +33 -9
- data/user_stories/active/handle-time.md +7 -0
- data/user_stories/active/handle-time.rb +177 -0
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/epic--score-order/band-score-order.md +38 -0
- data/user_stories/done/epic--score-order/chamber-ensemble-score-order.md +33 -0
- data/user_stories/done/epic--score-order/orchestral-score-order.md +43 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/todo/agentic-daw.md +3 -0
- data/user_stories/todo/consonance-dissonance-classification.md +57 -0
- data/user_stories/todo/dyad-analysis.md +57 -0
- data/user_stories/todo/material-and-scores.md +10 -0
- data/user_stories/todo/organizing-content.md +72 -0
- data/user_stories/todo/percussion_set.md +1 -0
- data/user_stories/todo/pitch-class-set-analysis.md +79 -0
- data/user_stories/todo/pitch-set-classification.md +72 -0
- data/user_stories/todo/sonority-identification.md +67 -0
- metadata +51 -6
- data/TODO.md +0 -218
|
@@ -2,8 +2,40 @@
|
|
|
2
2
|
module HeadMusic::Utilities; end
|
|
3
3
|
|
|
4
4
|
# Util for converting an object to a consistent hash key
|
|
5
|
-
|
|
5
|
+
class HeadMusic::Utilities::HashKey
|
|
6
6
|
def self.for(identifier)
|
|
7
|
-
|
|
7
|
+
@hash_keys ||= {}
|
|
8
|
+
@hash_keys[identifier] ||= new(identifier).to_sym
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :original
|
|
12
|
+
|
|
13
|
+
def initialize(identifier)
|
|
14
|
+
@original = identifier
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_sym
|
|
18
|
+
normalized_string.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def normalized_string
|
|
24
|
+
@normalized_string ||=
|
|
25
|
+
transliterated_string.downcase.gsub(/\W+/, "_")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def transliterated_string
|
|
29
|
+
I18n.transliterate(desymbolized_string)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def desymbolized_string
|
|
33
|
+
original.to_s
|
|
34
|
+
.gsub("𝄫", "_double_flat")
|
|
35
|
+
.gsub("♭", "_flat")
|
|
36
|
+
.gsub("♮", "_natural")
|
|
37
|
+
.gsub("♯", "_sharp")
|
|
38
|
+
.gsub("#", "_sharp")
|
|
39
|
+
.gsub("𝄪", "_double_sharp")
|
|
8
40
|
end
|
|
9
41
|
end
|
data/lib/head_music/version.rb
CHANGED
data/lib/head_music.rb
CHANGED
|
@@ -14,7 +14,7 @@ require "i18n"
|
|
|
14
14
|
require "i18n/backend/fallbacks"
|
|
15
15
|
|
|
16
16
|
I18n::Backend::Simple.include I18n::Backend::Fallbacks
|
|
17
|
-
I18n.load_path
|
|
17
|
+
I18n.load_path += Dir[File.join(File.dirname(__dir__), "lib", "head_music", "locales", "*.yml")]
|
|
18
18
|
I18n.config.available_locales = %i[en fr de it ru es en_US en_GB]
|
|
19
19
|
I18n.default_locale = :en
|
|
20
20
|
I18n.fallbacks[:de] = %i[de en_GB en]
|
|
@@ -32,37 +32,57 @@ require "head_music/utilities/hash_key"
|
|
|
32
32
|
require "head_music/named"
|
|
33
33
|
|
|
34
34
|
# rudiments
|
|
35
|
+
require "head_music/rudiment/base"
|
|
36
|
+
require "head_music/rudiment/letter_name"
|
|
35
37
|
require "head_music/rudiment/alteration"
|
|
38
|
+
require "head_music/rudiment/spelling"
|
|
39
|
+
require "head_music/rudiment/rhythmic_unit"
|
|
40
|
+
require "head_music/rudiment/rhythmic_unit/parser"
|
|
41
|
+
require "head_music/rudiment/rhythmic_value"
|
|
42
|
+
require "head_music/rudiment/rhythmic_value/parser"
|
|
43
|
+
require "head_music/rudiment/register"
|
|
44
|
+
require "head_music/rudiment/pitch"
|
|
45
|
+
require "head_music/rudiment/pitch/parser"
|
|
46
|
+
require "head_music/rudiment/rhythmic_element"
|
|
47
|
+
require "head_music/rudiment/note"
|
|
48
|
+
require "head_music/rudiment/unpitched_note"
|
|
49
|
+
require "head_music/rudiment/rest"
|
|
50
|
+
|
|
36
51
|
require "head_music/rudiment/chromatic_interval"
|
|
37
52
|
require "head_music/rudiment/clef"
|
|
38
53
|
require "head_music/rudiment/consonance"
|
|
54
|
+
require "head_music/rudiment/tonal_context"
|
|
55
|
+
require "head_music/rudiment/diatonic_context"
|
|
56
|
+
require "head_music/rudiment/key"
|
|
57
|
+
require "head_music/rudiment/mode"
|
|
39
58
|
require "head_music/rudiment/key_signature"
|
|
40
59
|
require "head_music/rudiment/key_signature/enharmonic_equivalence"
|
|
41
|
-
require "head_music/rudiment/letter_name"
|
|
42
60
|
require "head_music/rudiment/meter"
|
|
43
61
|
require "head_music/rudiment/musical_symbol"
|
|
44
|
-
require "head_music/rudiment/pitch"
|
|
45
62
|
require "head_music/rudiment/pitch/enharmonic_equivalence"
|
|
46
63
|
require "head_music/rudiment/pitch/octave_equivalence"
|
|
47
64
|
require "head_music/rudiment/pitch_class"
|
|
48
65
|
require "head_music/rudiment/quality"
|
|
49
66
|
require "head_music/rudiment/reference_pitch"
|
|
50
|
-
require "head_music/rudiment/register"
|
|
51
67
|
require "head_music/rudiment/rhythm"
|
|
52
|
-
require "head_music/rudiment/rhythmic_unit"
|
|
53
68
|
require "head_music/rudiment/scale"
|
|
54
69
|
require "head_music/rudiment/scale_degree"
|
|
55
70
|
require "head_music/rudiment/scale_type"
|
|
56
71
|
require "head_music/rudiment/solmization"
|
|
57
|
-
require "head_music/rudiment/
|
|
72
|
+
require "head_music/rudiment/tempo"
|
|
58
73
|
require "head_music/rudiment/tuning"
|
|
74
|
+
require "head_music/rudiment/tuning/just_intonation"
|
|
75
|
+
require "head_music/rudiment/tuning/meantone"
|
|
76
|
+
require "head_music/rudiment/tuning/pythagorean"
|
|
59
77
|
|
|
60
78
|
# instruments
|
|
61
79
|
require "head_music/instruments/instrument_family"
|
|
62
|
-
require "head_music/instruments/
|
|
80
|
+
require "head_music/instruments/instrument_type"
|
|
81
|
+
require "head_music/instruments/variant"
|
|
63
82
|
require "head_music/instruments/staff_scheme"
|
|
64
83
|
require "head_music/instruments/staff"
|
|
65
|
-
require "head_music/instruments/
|
|
84
|
+
require "head_music/instruments/instrument"
|
|
85
|
+
require "head_music/instruments/score_order"
|
|
66
86
|
|
|
67
87
|
# content
|
|
68
88
|
require "head_music/content/bar"
|
|
@@ -70,7 +90,6 @@ require "head_music/content/composition"
|
|
|
70
90
|
require "head_music/content/note"
|
|
71
91
|
require "head_music/content/placement"
|
|
72
92
|
require "head_music/content/position"
|
|
73
|
-
require "head_music/content/rhythmic_value"
|
|
74
93
|
require "head_music/content/staff"
|
|
75
94
|
require "head_music/content/voice"
|
|
76
95
|
|
|
@@ -83,6 +102,7 @@ require "head_music/analysis/diatonic_interval/parser"
|
|
|
83
102
|
require "head_music/analysis/diatonic_interval/semitones"
|
|
84
103
|
require "head_music/analysis/diatonic_interval/size"
|
|
85
104
|
require "head_music/analysis/harmonic_interval"
|
|
105
|
+
require "head_music/analysis/interval_consonance"
|
|
86
106
|
require "head_music/analysis/interval_cycle"
|
|
87
107
|
require "head_music/analysis/melodic_interval"
|
|
88
108
|
require "head_music/analysis/motion"
|
|
@@ -91,6 +111,10 @@ require "head_music/analysis/pitch_set"
|
|
|
91
111
|
require "head_music/analysis/sonority"
|
|
92
112
|
|
|
93
113
|
# style analysis
|
|
114
|
+
require "head_music/style/tradition"
|
|
115
|
+
require "head_music/style/modern_tradition"
|
|
116
|
+
require "head_music/style/renaissance_tradition"
|
|
117
|
+
require "head_music/style/medieval_tradition"
|
|
94
118
|
require "head_music/style/analysis"
|
|
95
119
|
require "head_music/style/annotation"
|
|
96
120
|
require "head_music/style/mark"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# The Time module provides classes and methods to handle representations
|
|
2
|
+
# of musical time and its relationship to clock time and SMPTE Time Code.
|
|
3
|
+
module HeadMusic::Time
|
|
4
|
+
# ticks per quarter note value
|
|
5
|
+
PPQN = PULSES_PER_QUARTER_NOTE = 960
|
|
6
|
+
SUBTICKS_PER_TICK = 240
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Representation of a conductor track for musical material
|
|
10
|
+
# Each moment in a track corresponds to the following positions:
|
|
11
|
+
# - ellapsed clock time
|
|
12
|
+
# - starts at 0.0 seconds
|
|
13
|
+
# - the source-of-truth clock time
|
|
14
|
+
# - nanosecond resolution
|
|
15
|
+
# - a musical position
|
|
16
|
+
# - bars:beats:ticks:subticks
|
|
17
|
+
# - a SMPTE timecode
|
|
18
|
+
# - hours:minutes:seconds:frames
|
|
19
|
+
class HeadMusic::Time::Conductor
|
|
20
|
+
attr_accessor :starting_position, :starting_smpte_timecode, :framerate
|
|
21
|
+
|
|
22
|
+
def initialize(attributes = {})
|
|
23
|
+
attributes = attributes.symbolize_keys
|
|
24
|
+
@starting_position = attributes.get(:starting_position, HeadMusic::Time::Position.new)
|
|
25
|
+
@starting_smpte_timecode = attributes.get(:starting_smpte_timecode, HeadMusic::Time::SmpteTimecode.new)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class HeadMusic::Time::MeterEvent
|
|
30
|
+
attr_accessor :position, :meter
|
|
31
|
+
|
|
32
|
+
def initialize(position, meter)
|
|
33
|
+
@position = position
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class HeadMusic::Time::TempoEvent
|
|
38
|
+
attr_accessor :position, :tempo
|
|
39
|
+
|
|
40
|
+
# accepts a rhythmic value and a bpm
|
|
41
|
+
def initialize(position, beat_value, beats_per_minute)
|
|
42
|
+
@position = position
|
|
43
|
+
@tempo = HeadMusic::Rudiment::Tempo.new(beat_value, beats_per_minute)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Abstract superclass
|
|
48
|
+
class HeadMusic::Time::Position
|
|
49
|
+
include Comparable
|
|
50
|
+
|
|
51
|
+
def initialize(value, meter: nil, tempo: nil)
|
|
52
|
+
@value = value
|
|
53
|
+
@meter = meter || HeadMusic::Rudiment::Meter.default
|
|
54
|
+
@tempo = tempo || HeadMusic::Rudiment::Tempo.default
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def <=>(other)
|
|
58
|
+
to_i <=> other.to_i
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A value object representing ellapsed nanoseconds of clock time
|
|
63
|
+
class HeadMusic::Time::ClockPosition
|
|
64
|
+
include Comparable
|
|
65
|
+
|
|
66
|
+
attr_reader :nanoseconds
|
|
67
|
+
|
|
68
|
+
def initialize(nanoseconds)
|
|
69
|
+
@nanoseconds = nanoseconds
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_i
|
|
73
|
+
nanoseconds
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_microseconds
|
|
77
|
+
nanoseconds / 1_000.0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_milliseconds
|
|
81
|
+
nanoseconds / 1_000_000.0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_seconds
|
|
85
|
+
nanoseconds / 1_000_000_000.0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def +(other)
|
|
89
|
+
HeadMusic::Time::Value.new(nanoseconds + other.to_i)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def <=>(other)
|
|
93
|
+
nanoseconds <=> other.to_i
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Representation of a musical position.
|
|
98
|
+
# Consists of:
|
|
99
|
+
# - bar
|
|
100
|
+
# - beat (or count)
|
|
101
|
+
# - tick (960 ticks / quarter note value)
|
|
102
|
+
# - subtick (240 subticks / tick)
|
|
103
|
+
#
|
|
104
|
+
# Note: In the absence of a specific meter,
|
|
105
|
+
# no math can be performed on the position.
|
|
106
|
+
class HeadMusic::Time::MusicalPosition
|
|
107
|
+
attr_reader :bar, :beat, :tick, :subtick
|
|
108
|
+
|
|
109
|
+
DEFAULT_FIRST_BAR = 1
|
|
110
|
+
FIRST_BEAT = 1
|
|
111
|
+
FIRST_TICK = 0
|
|
112
|
+
FIRST_SUBTICK = 0
|
|
113
|
+
|
|
114
|
+
def self.parse(identifier)
|
|
115
|
+
new(*identifier.scan(/\d+/)[0..3])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize(
|
|
119
|
+
bar = DEFAULT_FIRST_BAR,
|
|
120
|
+
beat = FIRST_BEAT,
|
|
121
|
+
tick = FIRST_TICK,
|
|
122
|
+
subtick = FIRST_SUBTICK
|
|
123
|
+
)
|
|
124
|
+
@bar = bar.to_i
|
|
125
|
+
@beat = beat.to_i
|
|
126
|
+
@tick = tick.to_i
|
|
127
|
+
@subtick = subtick.to_i
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def to_a
|
|
131
|
+
[bar, beat, tick, subtick]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def to_s
|
|
135
|
+
"#{bar}:#{beat}:#{tick}:#{subtick}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Accept a meter and roll excessive values over to the next level
|
|
139
|
+
def normalize!(meter)
|
|
140
|
+
return self unless meter
|
|
141
|
+
|
|
142
|
+
# Carry subticks into ticks
|
|
143
|
+
if subtick >= HeadMusic::Time::SUBTICKS_PER_TICK || subtick.negative?
|
|
144
|
+
tick_delta, @subtick = subtick.divmod(HeadMusic::Time::SUBTICKS_PER_TICK)
|
|
145
|
+
@tick += tick_delta
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Carry ticks into beats
|
|
149
|
+
if tick >= meter.ticks_per_count || tick.negative?
|
|
150
|
+
beat_delta, @tick = tick.divmod(meter.ticks_per_count)
|
|
151
|
+
@beat += beat_delta
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Carry beats into bars
|
|
155
|
+
if beat >= meter.counts_per_bar || beat.negative?
|
|
156
|
+
bar_delta, @beat = beat.divmod(meter.counts_per_bar)
|
|
157
|
+
@bar += bar_delta
|
|
158
|
+
end
|
|
159
|
+
HeadMusic::Time::Position.new(@bar, @beat, @tick, @subtick)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class HeadMusic::Time::SamplesPosition
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class HeadMusic::Time::TimecodePosition # (SMPTE/MTC)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Represents a SMPTE timecode position
|
|
170
|
+
# HH:MM:SS:FF (hours:minutes:seconds:frames)
|
|
171
|
+
class HeadMusic::Time::SmpteTimecode
|
|
172
|
+
attr_reader :hour, :minute, :second, :frame
|
|
173
|
+
|
|
174
|
+
def initialize(hour = 1, minute = 0, second = 0, frame = 0)
|
|
175
|
+
@hour, @minute, @second, @frame = hour.to_i, minute.to_i, second.to_i, frame.to_i
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Score-Order Epic: Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
|
|
5
|
+
The score-order feature addresses a fundamental need in music composition software - automatically organizing instruments in standardized score orders based on ensemble type. This is critical for professional composers and arrangers who need their scores to follow industry conventions.
|
|
6
|
+
|
|
7
|
+
## Value Proposition
|
|
8
|
+
|
|
9
|
+
- **Saves time**: Composers won't need to manually reorder instruments
|
|
10
|
+
- **Ensures accuracy**: Follows established conventions automatically
|
|
11
|
+
- **Flexibility**: Supports multiple ensemble types (orchestral, band, chamber)
|
|
12
|
+
- **Professional output**: Scores will meet industry standards
|
|
13
|
+
|
|
14
|
+
## Priority Recommendations
|
|
15
|
+
|
|
16
|
+
### Phase 1: Orchestral Order (MVP)
|
|
17
|
+
- Most complex and widely used
|
|
18
|
+
- Clear, established conventions
|
|
19
|
+
- Highest value for professional users
|
|
20
|
+
|
|
21
|
+
### Phase 2: Band Support
|
|
22
|
+
- Different enough to showcase flexibility
|
|
23
|
+
- Strong user base in educational settings
|
|
24
|
+
- Notable percussion placement differences
|
|
25
|
+
|
|
26
|
+
### Phase 3: Chamber Ensembles
|
|
27
|
+
- More specialized use cases
|
|
28
|
+
- Could potentially be template-based
|
|
29
|
+
|
|
30
|
+
## Technical Implementation Approach
|
|
31
|
+
|
|
32
|
+
### Architecture Insights
|
|
33
|
+
|
|
34
|
+
The existing Instruments module already provides:
|
|
35
|
+
- `orchestra_section_key` (woodwind, brass, percussion, string, keyboard, voice)
|
|
36
|
+
- Instrument families with proper classification
|
|
37
|
+
- Good foundation but NO ordering logic yet
|
|
38
|
+
|
|
39
|
+
### Proposed Implementation
|
|
40
|
+
|
|
41
|
+
Create a standalone **`HeadMusic::Instruments::ScoreOrder`** class that:
|
|
42
|
+
|
|
43
|
+
1. **Defines ordering rules** for different ensemble types (orchestral, band, chamber)
|
|
44
|
+
2. **Works with instrument instances** directly (not tied to Composition module)
|
|
45
|
+
3. **Supports custom overrides** while maintaining sensible defaults
|
|
46
|
+
4. **Handles instrument abbreviations** through existing i18n system
|
|
47
|
+
|
|
48
|
+
### Key Design Decisions
|
|
49
|
+
|
|
50
|
+
- **Keep it simple**: No complex inheritance, just a straightforward ordering system
|
|
51
|
+
- **Data-driven approach**: Store ordering rules in YAML like existing instrument data
|
|
52
|
+
- **Flexible API**: Accept arrays of instrument names/objects, return ordered list
|
|
53
|
+
- **Support multiple sections**: Handle both full instruments and section groupings
|
|
54
|
+
- **Independence**: Not dependent on Content module (which is being rewritten)
|
|
55
|
+
|
|
56
|
+
## Acceptance Criteria
|
|
57
|
+
|
|
58
|
+
### Basic Functionality
|
|
59
|
+
- ✅ Can order an array of instruments by orchestral convention
|
|
60
|
+
- ✅ Can order an array of instruments by band convention
|
|
61
|
+
- ✅ Can order an array of instruments by chamber ensemble type
|
|
62
|
+
- ✅ Returns instruments in correct family/section groupings
|
|
63
|
+
|
|
64
|
+
### Flexibility
|
|
65
|
+
- ✅ Handles unknown instruments gracefully (append at end)
|
|
66
|
+
- ✅ Supports both instrument objects and string names as input
|
|
67
|
+
- ✅ Allows custom ordering overrides
|
|
68
|
+
|
|
69
|
+
### Integration
|
|
70
|
+
- ✅ Works independently of Content module
|
|
71
|
+
- ✅ Follows existing HeadMusic patterns (`.get()` factory method)
|
|
72
|
+
- ✅ Maintains 90%+ test coverage
|
|
73
|
+
|
|
74
|
+
### Performance
|
|
75
|
+
- ✅ Orders 100+ instruments in < 100ms
|
|
76
|
+
- ✅ Caches ordering rules efficiently
|
|
77
|
+
|
|
78
|
+
## MVP Scope
|
|
79
|
+
|
|
80
|
+
Start with orchestral ordering only:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
instruments = ["violin", "trumpet", "flute", "timpani", "cello"]
|
|
84
|
+
ordered = HeadMusic::Instruments::ScoreOrder.in_orchestral_order(instruments)
|
|
85
|
+
# => ["flute", "trumpet", "timpani", "violin", "cello"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Detailed Orchestral Order
|
|
89
|
+
|
|
90
|
+
Standard orchestral score order from top to bottom:
|
|
91
|
+
|
|
92
|
+
1. **Woodwinds**
|
|
93
|
+
- Piccolo
|
|
94
|
+
- Flutes (I, II, III)
|
|
95
|
+
- Oboes (I, II, III)
|
|
96
|
+
- English Horn
|
|
97
|
+
- Clarinets (I, II, III)
|
|
98
|
+
- Bass Clarinet
|
|
99
|
+
- Bassoons (I, II, III)
|
|
100
|
+
- Contrabassoon
|
|
101
|
+
|
|
102
|
+
2. **Brass**
|
|
103
|
+
- Horns (I, II, III, IV)
|
|
104
|
+
- Trumpets (I, II, III)
|
|
105
|
+
- Trombones (I, II, III)
|
|
106
|
+
- Tuba
|
|
107
|
+
|
|
108
|
+
3. **Percussion**
|
|
109
|
+
- Timpani
|
|
110
|
+
- Percussion (various)
|
|
111
|
+
|
|
112
|
+
4. **Keyboards & Harp**
|
|
113
|
+
- Harp
|
|
114
|
+
- Piano
|
|
115
|
+
- Celesta
|
|
116
|
+
- Organ
|
|
117
|
+
|
|
118
|
+
5. **Soloists** (if any)
|
|
119
|
+
- Instrumental soloists
|
|
120
|
+
- Vocal soloists
|
|
121
|
+
|
|
122
|
+
6. **Voices** (if any)
|
|
123
|
+
- Soprano
|
|
124
|
+
- Alto
|
|
125
|
+
- Tenor
|
|
126
|
+
- Bass
|
|
127
|
+
|
|
128
|
+
7. **Strings**
|
|
129
|
+
- Violin I
|
|
130
|
+
- Violin II
|
|
131
|
+
- Viola
|
|
132
|
+
- Cello
|
|
133
|
+
- Double Bass
|
|
134
|
+
|
|
135
|
+
## Success Metrics
|
|
136
|
+
|
|
137
|
+
- Can correctly order any standard ensemble
|
|
138
|
+
- Supports custom overrides when needed
|
|
139
|
+
- Integrates smoothly with existing Instrument class
|
|
140
|
+
- Maintains 90%+ test coverage
|
|
141
|
+
- Performance: < 100ms for typical ensemble sizes
|
|
142
|
+
|
|
143
|
+
## Implementation Strategy
|
|
144
|
+
|
|
145
|
+
### Class Design: Single Class with Data-Driven Instances
|
|
146
|
+
|
|
147
|
+
We'll use a **single class with instances** approach, following HeadMusic's established patterns:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class HeadMusic::Instruments::ScoreOrder
|
|
151
|
+
include HeadMusic::Named
|
|
152
|
+
|
|
153
|
+
def self.get(ensemble_type)
|
|
154
|
+
# Returns an instance configured for that ensemble type
|
|
155
|
+
# Follows existing HeadMusic factory pattern
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.in_orchestral_order(instruments)
|
|
159
|
+
get(:orchestral).order(instruments)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.in_band_order(instruments)
|
|
163
|
+
get(:band).order(instruments)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def order(instruments)
|
|
167
|
+
# Apply ordering rules from YAML data
|
|
168
|
+
# Handle both instrument objects and strings
|
|
169
|
+
# Gracefully handle unknown instruments
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Why This Approach
|
|
175
|
+
|
|
176
|
+
1. **Consistency with HeadMusic patterns**:
|
|
177
|
+
- Matches `Instrument` and `InstrumentFamily` design
|
|
178
|
+
- Uses `.get()` factory method pattern
|
|
179
|
+
- Data-driven via YAML configuration
|
|
180
|
+
|
|
181
|
+
2. **Maintainability**:
|
|
182
|
+
- Single class to test and maintain
|
|
183
|
+
- Shared logic for common operations
|
|
184
|
+
- Easy to add new ensemble types without code changes
|
|
185
|
+
|
|
186
|
+
3. **Flexibility**:
|
|
187
|
+
- Rules defined in YAML, not hardcoded
|
|
188
|
+
- Simple to add custom orderings
|
|
189
|
+
- Could support user-defined orderings in future
|
|
190
|
+
|
|
191
|
+
### YAML Structure
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
# lib/head_music/instruments/score_orders.yml
|
|
195
|
+
orchestral:
|
|
196
|
+
name: "Orchestral"
|
|
197
|
+
sections:
|
|
198
|
+
- section_key: woodwind
|
|
199
|
+
instruments: [piccolo, flute, oboe, english_horn, clarinet, bass_clarinet, bassoon, contrabassoon]
|
|
200
|
+
- section_key: brass
|
|
201
|
+
instruments: [horn, trumpet, trombone, tuba]
|
|
202
|
+
- section_key: percussion
|
|
203
|
+
instruments: [timpani, percussion]
|
|
204
|
+
- section_key: keyboard
|
|
205
|
+
instruments: [harp, piano, celesta, organ]
|
|
206
|
+
- section_key: voice
|
|
207
|
+
instruments: [soprano_voice, alto_voice, tenor_voice, bass_voice]
|
|
208
|
+
- section_key: string
|
|
209
|
+
instruments: [violin, viola, cello, double_bass]
|
|
210
|
+
|
|
211
|
+
band:
|
|
212
|
+
name: "Concert Band"
|
|
213
|
+
sections:
|
|
214
|
+
- section_key: woodwind
|
|
215
|
+
instruments: [flute, oboe, bassoon, clarinet, saxophone]
|
|
216
|
+
- section_key: brass
|
|
217
|
+
instruments: [cornet, trumpet, horn, trombone, euphonium, tuba]
|
|
218
|
+
- section_key: percussion
|
|
219
|
+
instruments: [timpani, percussion]
|
|
220
|
+
|
|
221
|
+
brass_quintet:
|
|
222
|
+
name: "Brass Quintet"
|
|
223
|
+
sections:
|
|
224
|
+
- section_key: brass
|
|
225
|
+
instruments: [trumpet, trumpet, horn, trombone, tuba]
|
|
226
|
+
|
|
227
|
+
woodwind_quintet:
|
|
228
|
+
name: "Woodwind Quintet"
|
|
229
|
+
sections:
|
|
230
|
+
- section_key: woodwind
|
|
231
|
+
instruments: [flute, oboe, clarinet, horn, bassoon]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Implementation Steps
|
|
235
|
+
|
|
236
|
+
1. Create `HeadMusic::Instruments::ScoreOrder` class with factory pattern
|
|
237
|
+
2. Create `score_orders.yml` with orchestral ordering rules
|
|
238
|
+
3. Implement `#order` method with instrument resolution logic
|
|
239
|
+
4. Add RSpec tests for orchestral ordering
|
|
240
|
+
5. Add band ordering to YAML and test
|
|
241
|
+
6. Add chamber ensemble templates and tests
|
|
242
|
+
7. Add support for custom instrument positions
|
|
243
|
+
8. Document API with YARD comments
|
|
244
|
+
9. Consider future integration points with notation systems
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Band Score Order
|
|
2
|
+
|
|
3
|
+
As a band director or arranger
|
|
4
|
+
|
|
5
|
+
I want to organize instruments in band score order
|
|
6
|
+
|
|
7
|
+
So that my scores follow standard concert band conventions
|
|
8
|
+
|
|
9
|
+
## Scenario: Display instruments in band order
|
|
10
|
+
|
|
11
|
+
Given I have a composition for concert band
|
|
12
|
+
|
|
13
|
+
When I request the score order for a band arrangement
|
|
14
|
+
|
|
15
|
+
Then the instruments should be ordered as follows:
|
|
16
|
+
- Flutes
|
|
17
|
+
- Oboes
|
|
18
|
+
- Bassoons
|
|
19
|
+
- Clarinets
|
|
20
|
+
- Saxophones
|
|
21
|
+
- Cornets
|
|
22
|
+
- Trumpets
|
|
23
|
+
- Horns
|
|
24
|
+
- Trombones
|
|
25
|
+
- Euphoniums
|
|
26
|
+
- Tubas
|
|
27
|
+
- Timpani
|
|
28
|
+
- Percussion
|
|
29
|
+
|
|
30
|
+
## Scenario: Recognize different percussion placement
|
|
31
|
+
|
|
32
|
+
Given I am working with both orchestral and band scores
|
|
33
|
+
|
|
34
|
+
When I compare the score orders
|
|
35
|
+
|
|
36
|
+
Then I should note that percussion placement differs:
|
|
37
|
+
- In orchestral scores: percussion appears after brass
|
|
38
|
+
- In band scores: percussion appears at the bottom
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Chamber Ensemble Score Order
|
|
2
|
+
|
|
3
|
+
As a chamber music composer
|
|
4
|
+
|
|
5
|
+
I want to organize instruments in standard chamber ensemble orders
|
|
6
|
+
|
|
7
|
+
So that my scores follow established conventions for small ensembles
|
|
8
|
+
|
|
9
|
+
## Scenario: Display brass quintet in standard order
|
|
10
|
+
|
|
11
|
+
Given I have a brass quintet composition
|
|
12
|
+
|
|
13
|
+
When I request the score order
|
|
14
|
+
|
|
15
|
+
Then the instruments should be ordered as follows:
|
|
16
|
+
- Trumpet I
|
|
17
|
+
- Trumpet II
|
|
18
|
+
- Horn
|
|
19
|
+
- Trombone
|
|
20
|
+
- Tuba
|
|
21
|
+
|
|
22
|
+
## Scenario: Display woodwind quintet in standard order
|
|
23
|
+
|
|
24
|
+
Given I have a woodwind quintet composition
|
|
25
|
+
|
|
26
|
+
When I request the score order
|
|
27
|
+
|
|
28
|
+
Then the instruments should be ordered as follows:
|
|
29
|
+
- Flute
|
|
30
|
+
- Oboe
|
|
31
|
+
- Clarinet
|
|
32
|
+
- Horn
|
|
33
|
+
- Bassoon
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Orchestral Score Order
|
|
2
|
+
|
|
3
|
+
As a composer or conductor
|
|
4
|
+
|
|
5
|
+
I want to organize instruments in orchestral score order
|
|
6
|
+
|
|
7
|
+
So that my scores follow standard orchestral conventions
|
|
8
|
+
|
|
9
|
+
## Scenario: Display instruments in orchestral order
|
|
10
|
+
|
|
11
|
+
Given I have a composition with multiple instruments
|
|
12
|
+
|
|
13
|
+
When I request the score order for an orchestral arrangement
|
|
14
|
+
|
|
15
|
+
Then the instruments should be ordered as follows:
|
|
16
|
+
- Woodwinds (flutes, oboes, clarinets, bassoons)
|
|
17
|
+
- Brass (horns, trumpets, trombones, tuba)
|
|
18
|
+
- Percussion (timpani, percussion)
|
|
19
|
+
- Harp and keyboards
|
|
20
|
+
- Soloists (instrumental or vocal)
|
|
21
|
+
- Voices
|
|
22
|
+
- Strings (violins I, violins II, viola, violoncellos, double bass)
|
|
23
|
+
|
|
24
|
+
## Scenario: Use standard orchestral abbreviations
|
|
25
|
+
|
|
26
|
+
Given I am creating an orchestral score
|
|
27
|
+
|
|
28
|
+
When I display instrument names
|
|
29
|
+
|
|
30
|
+
Then I should see standard abbreviations:
|
|
31
|
+
- Fl or Fls for Flutes
|
|
32
|
+
- Ob or Obs for Oboes
|
|
33
|
+
- Cl or Cls for Clarinets
|
|
34
|
+
- Bsn or Bsns for Bassoons
|
|
35
|
+
- Hn or Hns for Horns
|
|
36
|
+
- Tpt or Tpts for Trumpets
|
|
37
|
+
- Trb or Trbs for Trombones
|
|
38
|
+
- Timp for Timpani
|
|
39
|
+
- Perc for Percussion
|
|
40
|
+
- Vlns for Violins
|
|
41
|
+
- Vla for Viola
|
|
42
|
+
- Vcl for Violoncellos
|
|
43
|
+
- DB for Double Bass
|