head_music 8.3.0 → 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/CHANGELOG.md +53 -0
- data/CLAUDE.md +32 -15
- data/Gemfile.lock +1 -1
- data/MUSIC_THEORY.md +120 -0
- data/lib/head_music/analysis/diatonic_interval.rb +29 -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_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- 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 +6 -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 +13 -5
- 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 +1 -1
- 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.rb +1 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- 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 +31 -10
- 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/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/{backlog → todo}/dyad-analysis.md +2 -10
- 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/{backlog → todo}/pitch-class-set-analysis.md +40 -0
- data/user_stories/{backlog → todo}/pitch-set-classification.md +10 -0
- data/user_stories/{backlog → todo}/sonority-identification.md +20 -0
- metadata +43 -12
- data/TODO.md +0 -109
- /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
- /data/user_stories/{backlog → todo}/consonance-dissonance-classification.md +0 -0
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,40 +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"
|
|
59
74
|
require "head_music/rudiment/tuning/just_intonation"
|
|
60
|
-
require "head_music/rudiment/tuning/pythagorean"
|
|
61
75
|
require "head_music/rudiment/tuning/meantone"
|
|
76
|
+
require "head_music/rudiment/tuning/pythagorean"
|
|
62
77
|
|
|
63
78
|
# instruments
|
|
64
79
|
require "head_music/instruments/instrument_family"
|
|
65
|
-
require "head_music/instruments/
|
|
80
|
+
require "head_music/instruments/instrument_type"
|
|
81
|
+
require "head_music/instruments/variant"
|
|
66
82
|
require "head_music/instruments/staff_scheme"
|
|
67
83
|
require "head_music/instruments/staff"
|
|
68
|
-
require "head_music/instruments/
|
|
84
|
+
require "head_music/instruments/instrument"
|
|
85
|
+
require "head_music/instruments/score_order"
|
|
69
86
|
|
|
70
87
|
# content
|
|
71
88
|
require "head_music/content/bar"
|
|
@@ -73,7 +90,6 @@ require "head_music/content/composition"
|
|
|
73
90
|
require "head_music/content/note"
|
|
74
91
|
require "head_music/content/placement"
|
|
75
92
|
require "head_music/content/position"
|
|
76
|
-
require "head_music/content/rhythmic_value"
|
|
77
93
|
require "head_music/content/staff"
|
|
78
94
|
require "head_music/content/voice"
|
|
79
95
|
|
|
@@ -86,6 +102,7 @@ require "head_music/analysis/diatonic_interval/parser"
|
|
|
86
102
|
require "head_music/analysis/diatonic_interval/semitones"
|
|
87
103
|
require "head_music/analysis/diatonic_interval/size"
|
|
88
104
|
require "head_music/analysis/harmonic_interval"
|
|
105
|
+
require "head_music/analysis/interval_consonance"
|
|
89
106
|
require "head_music/analysis/interval_cycle"
|
|
90
107
|
require "head_music/analysis/melodic_interval"
|
|
91
108
|
require "head_music/analysis/motion"
|
|
@@ -94,6 +111,10 @@ require "head_music/analysis/pitch_set"
|
|
|
94
111
|
require "head_music/analysis/sonority"
|
|
95
112
|
|
|
96
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"
|
|
97
118
|
require "head_music/style/analysis"
|
|
98
119
|
require "head_music/style/annotation"
|
|
99
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,65 @@
|
|
|
1
|
+
## Instrument Variant Refactoring
|
|
2
|
+
|
|
3
|
+
### Background
|
|
4
|
+
The current architecture conflates instrument catalog data with specific instrument instances. We need to separate these concerns to better represent how instruments are actually used in musical scores.
|
|
5
|
+
|
|
6
|
+
### Hierarchy
|
|
7
|
+
The refactored hierarchy will provide three levels of abstraction:
|
|
8
|
+
- **InstrumentFamily** (existing): Broad category like "saxophone" or "trumpet"
|
|
9
|
+
- **InstrumentType** (renamed from Instrument): Catalog entry with all possible variants (e.g., "trumpet" which can be in Bb, C, D, Eb)
|
|
10
|
+
- **Instrument** (new): Specific instance with selected variant (e.g., "Trumpet in C")
|
|
11
|
+
|
|
12
|
+
### User Stories
|
|
13
|
+
|
|
14
|
+
**STORY 1: Rename Instrument to InstrumentType**
|
|
15
|
+
AS a developer
|
|
16
|
+
WHEN I want to access instrument catalog data
|
|
17
|
+
I WANT to use InstrumentType.get("trumpet")
|
|
18
|
+
SO THAT it's clear I'm getting a type definition, not a specific instrument instance
|
|
19
|
+
|
|
20
|
+
**STORY 2: Create new Instrument class for specific variants**
|
|
21
|
+
AS a developer
|
|
22
|
+
WHEN I need a specific instrument for a score
|
|
23
|
+
I WANT to call Instrument.get("trumpet_in_c") or Instrument.get("trumpet", "in_c")
|
|
24
|
+
SO THAT I get a specific, usable instrument instance with proper transposition
|
|
25
|
+
|
|
26
|
+
**STORY 3: Instrument instances are sortable**
|
|
27
|
+
AS a developer
|
|
28
|
+
WHEN I have multiple Instrument instances in a score
|
|
29
|
+
I WANT them to sort properly by orchestral order and transposition
|
|
30
|
+
SO THAT "Trumpet in Eb" appears before "Trumpet in C" in the score
|
|
31
|
+
|
|
32
|
+
**STORY 4: Clear API for common use cases**
|
|
33
|
+
AS a developer
|
|
34
|
+
WHEN I create an Instrument without specifying a variant
|
|
35
|
+
I WANT to get the default variant automatically
|
|
36
|
+
SO THAT Instrument.get("clarinet") returns a Bb clarinet (the default)
|
|
37
|
+
|
|
38
|
+
**STORY 5: Instrument provides unified interface**
|
|
39
|
+
AS a developer
|
|
40
|
+
WHEN I have an Instrument instance
|
|
41
|
+
I WANT to access properties like name, transposition, clefs, and pitch_designation
|
|
42
|
+
SO THAT I don't need to navigate between instrument type and variant objects
|
|
43
|
+
|
|
44
|
+
### Implementation Notes
|
|
45
|
+
|
|
46
|
+
1. The Instrument class should:
|
|
47
|
+
- Wrap both an InstrumentType and a specific Variant
|
|
48
|
+
- Generate appropriate display names (e.g., "Clarinet in A")
|
|
49
|
+
- Provide methods for transposition, clefs, staff schemes
|
|
50
|
+
- Be directly usable in scores and parts
|
|
51
|
+
|
|
52
|
+
2. Factory methods should support:
|
|
53
|
+
- `Instrument.get("trumpet_in_c")` - parse variant from name
|
|
54
|
+
- `Instrument.get("trumpet", "in_c")` - explicit variant
|
|
55
|
+
- `Instrument.get("trumpet")` - use default variant
|
|
56
|
+
|
|
57
|
+
3. ScoreOrder should work with Instrument instances directly
|
|
58
|
+
|
|
59
|
+
### Migration Path
|
|
60
|
+
|
|
61
|
+
1. Rename existing Instrument class to InstrumentType
|
|
62
|
+
2. Update all references to use InstrumentType where appropriate
|
|
63
|
+
3. Create new Instrument class for variant instances
|
|
64
|
+
4. Update ScoreOrder to work with new Instrument instances
|
|
65
|
+
5. Update documentation and tests
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
IN ORDER TO accurately model sound events
|
|
2
|
+
AS a developer
|
|
3
|
+
I WANT a clear way to group the notion of a Note (pitch + rhythmic value) and an unpitched note.
|
|
4
|
+
|
|
5
|
+
We need a hierarchy of classes
|
|
6
|
+
|
|
7
|
+
class RhythmicEvent
|
|
8
|
+
attr_accessor :rhythmic_value
|
|
9
|
+
|
|
10
|
+
class Note < RhythmicEvent
|
|
11
|
+
attr_accessor :pitch
|
|
12
|
+
def sounded?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class UnpitchedNote < RhythmicEvent
|
|
17
|
+
def sounded?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Rest < RhythmicEvent
|
|
22
|
+
def sounded?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
acceptance criteria
|
|
28
|
+
- the above class hierarchy and implementation requirements
|
|
29
|
+
- full test coverage
|
|
30
|
+
- use NotImplementedError instead of NotImplementedError in RhythmicEvent if and where appropriate.
|
|
@@ -6,6 +6,8 @@ I want to analyze two-note combinations (dyads)
|
|
|
6
6
|
|
|
7
7
|
So that I can understand harmonic implications in two-part music
|
|
8
8
|
|
|
9
|
+
Constructor should accept to pitches (or pitch classes) and an optional key
|
|
10
|
+
|
|
9
11
|
## Scenario: Identify interval in dyad
|
|
10
12
|
|
|
11
13
|
Given I have two pitches forming a dyad
|
|
@@ -14,16 +16,6 @@ When I access the interval property
|
|
|
14
16
|
|
|
15
17
|
Then I should receive the correct interval between the pitches
|
|
16
18
|
|
|
17
|
-
## Scenario: Find implied triads from thirds
|
|
18
|
-
|
|
19
|
-
Given I have a dyad that forms a third
|
|
20
|
-
|
|
21
|
-
When I request the implied triad
|
|
22
|
-
|
|
23
|
-
Then I should receive the most likely triad containing those pitches
|
|
24
|
-
|
|
25
|
-
And it should consider the musical context
|
|
26
|
-
|
|
27
19
|
## Scenario: List possible triads from fifth
|
|
28
20
|
|
|
29
21
|
Given I have a dyad forming a perfect fifth
|