head_music 9.0.1 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -3
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +35 -15
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/dyad.rb +229 -0
- data/lib/head_music/analysis/melodic_interval.rb +1 -1
- data/lib/head_music/analysis/pitch_class_set.rb +111 -14
- data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
- data/lib/head_music/analysis/sonority.rb +50 -12
- data/lib/head_music/content/cantus_firmus_examples.rb +58 -0
- data/lib/head_music/content/staff.rb +1 -1
- data/lib/head_music/content/voice.rb +1 -1
- data/lib/head_music/instruments/alternate_tuning.rb +102 -0
- data/lib/head_music/instruments/alternate_tunings.yml +78 -0
- data/lib/head_music/instruments/instrument.rb +251 -82
- data/lib/head_music/instruments/instrument_configuration.rb +66 -0
- data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
- data/lib/head_music/instruments/instrument_configurations.yml +288 -0
- data/lib/head_music/instruments/instrument_families.yml +77 -0
- data/lib/head_music/instruments/instrument_family.rb +3 -4
- data/lib/head_music/instruments/instruments.yml +795 -965
- data/lib/head_music/instruments/playing_technique.rb +75 -0
- data/lib/head_music/instruments/playing_techniques.yml +826 -0
- data/lib/head_music/instruments/score_order.rb +2 -5
- data/lib/head_music/instruments/staff.rb +61 -1
- data/lib/head_music/instruments/staff_scheme.rb +6 -4
- data/lib/head_music/instruments/stringing.rb +115 -0
- data/lib/head_music/instruments/stringing_course.rb +58 -0
- data/lib/head_music/instruments/stringings.yml +168 -0
- data/lib/head_music/instruments/variant.rb +0 -1
- data/lib/head_music/locales/de.yml +23 -0
- data/lib/head_music/locales/en.yml +100 -0
- data/lib/head_music/locales/es.yml +23 -0
- data/lib/head_music/locales/fr.yml +23 -0
- data/lib/head_music/locales/it.yml +23 -0
- data/lib/head_music/locales/ru.yml +23 -0
- data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
- data/lib/head_music/notation/staff_mapping.rb +70 -0
- data/lib/head_music/notation/staff_position.rb +62 -0
- data/lib/head_music/notation.rb +7 -0
- data/lib/head_music/rudiment/alteration.rb +17 -47
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +14 -13
- data/lib/head_music/rudiment/key_signature.rb +0 -26
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
- data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +3 -0
- data/lib/head_music/rudiment/tempo.rb +1 -1
- data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
- data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
- data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
- data/lib/head_music/rudiment/tuning.rb +20 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/modern_tradition.rb +8 -11
- data/lib/head_music/style/tradition.rb +1 -1
- data/lib/head_music/time/clock_position.rb +84 -0
- data/lib/head_music/time/conductor.rb +264 -0
- data/lib/head_music/time/meter_event.rb +37 -0
- data/lib/head_music/time/meter_map.rb +173 -0
- data/lib/head_music/time/musical_position.rb +188 -0
- data/lib/head_music/time/smpte_timecode.rb +164 -0
- data/lib/head_music/time/tempo_event.rb +40 -0
- data/lib/head_music/time/tempo_map.rb +187 -0
- data/lib/head_music/time.rb +32 -0
- data/lib/head_music/utilities/case.rb +27 -0
- data/lib/head_music/utilities/hash_key.rb +1 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +42 -13
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/{todo → backlog}/organizing-content.md +9 -1
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{todo → done}/dyad-analysis.md +4 -6
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/{active → done}/handle-time.rb +5 -19
- data/user_stories/done/instrument-architecture.md +238 -0
- data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
- data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
- data/user_stories/done/move-staff-position-to-notation.md +141 -0
- data/user_stories/done/notation-module-foundation.md +102 -0
- data/user_stories/done/percussion_set.md +260 -0
- data/user_stories/{todo → done}/pitch-class-set-analysis.md +0 -40
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/done/string-pitches.md +41 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
- metadata +56 -20
- data/check_instrument_consistency.rb +0 -0
- data/lib/head_music/instruments/instrument_type.rb +0 -188
- data/test_translations.rb +0 -15
- data/user_stories/todo/consonance-dissonance-classification.md +0 -57
- data/user_stories/todo/material-and-scores.md +0 -10
- data/user_stories/todo/percussion_set.md +0 -1
- data/user_stories/todo/pitch-set-classification.md +0 -72
- data/user_stories/todo/sonority-identification.md +0 -67
- /data/user_stories/{active → done}/handle-time.md +0 -0
|
@@ -162,6 +162,8 @@ fr:
|
|
|
162
162
|
descant: dessus
|
|
163
163
|
didgeridoo: didgeridoo
|
|
164
164
|
double_bass: contrebasse
|
|
165
|
+
drum_kit: batterie
|
|
166
|
+
floor_tom: tom de sol
|
|
165
167
|
double_contrabass_recorder: flûte à bec double-contrebasse
|
|
166
168
|
english_horn: cor anglais
|
|
167
169
|
euphonium: euphonium
|
|
@@ -183,6 +185,7 @@ fr:
|
|
|
183
185
|
harpsichord: clavecin
|
|
184
186
|
hautbois_d_amour: hautbois d'amour
|
|
185
187
|
hi-hat: charleston
|
|
188
|
+
high_tom: tom aigu
|
|
186
189
|
horn: cor
|
|
187
190
|
kettledrum: timbale
|
|
188
191
|
kick_drum: grosse caisse
|
|
@@ -194,6 +197,7 @@ fr:
|
|
|
194
197
|
mezzo: mezzo
|
|
195
198
|
mezzo_soprano: mezzo-soprano
|
|
196
199
|
mezzo_soprano_voice: voix de mezzo-soprano
|
|
200
|
+
mid_tom: tom grave
|
|
197
201
|
military_drum: tambour militaire
|
|
198
202
|
mouth_organ: harmonica
|
|
199
203
|
natural_horn: cor naturel
|
|
@@ -281,6 +285,25 @@ fr:
|
|
|
281
285
|
woodblock: bloc de bois
|
|
282
286
|
xylophone: xylophone
|
|
283
287
|
zither: cithare
|
|
288
|
+
instrument_families:
|
|
289
|
+
drum_kit: batterie
|
|
290
|
+
hi_hat: charleston
|
|
291
|
+
tom_tom: tom-tom
|
|
292
|
+
playing_techniques:
|
|
293
|
+
bell: cloche
|
|
294
|
+
bow: archet
|
|
295
|
+
brush: brosse
|
|
296
|
+
choked: étouffé
|
|
297
|
+
closed: fermé
|
|
298
|
+
cross_stick: baguette croisée
|
|
299
|
+
damped: amorti
|
|
300
|
+
hand: main
|
|
301
|
+
let_ring: laisser résonner
|
|
302
|
+
mallet: maillet
|
|
303
|
+
open: ouvert
|
|
304
|
+
pedal: pédale
|
|
305
|
+
rim_shot: coup de bord
|
|
306
|
+
stick: baguette
|
|
284
307
|
locales:
|
|
285
308
|
de: allemand
|
|
286
309
|
en: anglais
|
|
@@ -157,6 +157,8 @@ it:
|
|
|
157
157
|
descant: soprano
|
|
158
158
|
didgeridoo: didgeridoo
|
|
159
159
|
double_bass: contrabbasso
|
|
160
|
+
drum_kit: batteria
|
|
161
|
+
floor_tom: tom a terra
|
|
160
162
|
double_contrabass_recorder: doppio flauto dolce contrabbasso
|
|
161
163
|
english_horn: corno inglese
|
|
162
164
|
euphonium: eufonio
|
|
@@ -178,6 +180,7 @@ it:
|
|
|
178
180
|
harpsichord: clavicembalo
|
|
179
181
|
hautbois_d_amour: oboe d'amore
|
|
180
182
|
hi-hat: hi-hat
|
|
183
|
+
high_tom: tom acuto
|
|
181
184
|
horn: corno
|
|
182
185
|
kettledrum: timpano
|
|
183
186
|
kick_drum: grancassa
|
|
@@ -189,6 +192,7 @@ it:
|
|
|
189
192
|
metal_block: blocco metallico
|
|
190
193
|
mezzo: mezzosoprano
|
|
191
194
|
mezzo_soprano_voice: mezzosoprano
|
|
195
|
+
mid_tom: tom grave
|
|
192
196
|
military_drum: tamburo militare
|
|
193
197
|
mouth_organ: armonica a bocca
|
|
194
198
|
natural_horn: corno naturale
|
|
@@ -274,6 +278,25 @@ it:
|
|
|
274
278
|
woodblock: legnetto
|
|
275
279
|
xylophone: xilofono
|
|
276
280
|
zither: cetra da tavolo
|
|
281
|
+
instrument_families:
|
|
282
|
+
drum_kit: batteria
|
|
283
|
+
hi_hat: hi-hat
|
|
284
|
+
tom_tom: tom-tom
|
|
285
|
+
playing_techniques:
|
|
286
|
+
bell: campana
|
|
287
|
+
bow: arco
|
|
288
|
+
brush: spazzola
|
|
289
|
+
choked: soffocato
|
|
290
|
+
closed: chiuso
|
|
291
|
+
cross_stick: bacchetta incrociata
|
|
292
|
+
damped: smorzato
|
|
293
|
+
hand: mano
|
|
294
|
+
let_ring: lasciar vibrare
|
|
295
|
+
mallet: bacchetta
|
|
296
|
+
open: aperto
|
|
297
|
+
pedal: pedale
|
|
298
|
+
rim_shot: colpo di rimshot
|
|
299
|
+
stick: bacchetta
|
|
277
300
|
locales:
|
|
278
301
|
de: tedesco
|
|
279
302
|
en: inglese
|
|
@@ -103,6 +103,8 @@ ru:
|
|
|
103
103
|
descant: дискант
|
|
104
104
|
didgeridoo: диджериду
|
|
105
105
|
double_bass: контрабас
|
|
106
|
+
drum_kit: барабанная установка
|
|
107
|
+
floor_tom: том для пола
|
|
106
108
|
double_contrabass_recorder: двойная контрабасовая флейта
|
|
107
109
|
english_horn: английский рожок
|
|
108
110
|
euphonium: эуфониум
|
|
@@ -124,6 +126,7 @@ ru:
|
|
|
124
126
|
harpsichord: клавесин
|
|
125
127
|
hautbois_d_amour: гобой д'амур
|
|
126
128
|
hi-hat: хай-хет
|
|
129
|
+
high_tom: том высокий
|
|
127
130
|
horn: валторна
|
|
128
131
|
kettledrum: тимпан
|
|
129
132
|
kick_drum: бас-барабан
|
|
@@ -134,6 +137,7 @@ ru:
|
|
|
134
137
|
mezzo: меццо
|
|
135
138
|
mezzo_soprano: меццо-сопрано
|
|
136
139
|
mezzo_soprano_voice: голос меццо-сопрано
|
|
140
|
+
mid_tom: том средний
|
|
137
141
|
military_drum: военный барабан
|
|
138
142
|
mouth_organ: губная гармоника
|
|
139
143
|
natural_horn: натуральный рог
|
|
@@ -221,6 +225,25 @@ ru:
|
|
|
221
225
|
woodblock: деревянный блок
|
|
222
226
|
xylophone: ксилофон
|
|
223
227
|
zither: цитра
|
|
228
|
+
instrument_families:
|
|
229
|
+
drum_kit: барабанная установка
|
|
230
|
+
hi_hat: хай-хет
|
|
231
|
+
tom_tom: том-том
|
|
232
|
+
playing_techniques:
|
|
233
|
+
bell: колокол
|
|
234
|
+
bow: смычок
|
|
235
|
+
brush: щётка
|
|
236
|
+
choked: задушенный
|
|
237
|
+
closed: закрытый
|
|
238
|
+
cross_stick: крест-стик
|
|
239
|
+
damped: приглушённый
|
|
240
|
+
hand: рука
|
|
241
|
+
let_ring: дать звучать
|
|
242
|
+
mallet: молоточек
|
|
243
|
+
open: открытый
|
|
244
|
+
pedal: педаль
|
|
245
|
+
rim_shot: удар по ободу
|
|
246
|
+
stick: палочка
|
|
224
247
|
locales:
|
|
225
248
|
de: Немецкий
|
|
226
249
|
en: Английский
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# A module for music
|
|
2
|
-
module HeadMusic::
|
|
1
|
+
# A module for visual music notation
|
|
2
|
+
module HeadMusic::Notation; end
|
|
3
3
|
|
|
4
4
|
# A symbol is a mark or sign that signifies a particular rudiment of music
|
|
5
|
-
class HeadMusic::
|
|
5
|
+
class HeadMusic::Notation::MusicalSymbol
|
|
6
6
|
attr_reader :ascii, :unicode, :html_entity
|
|
7
7
|
|
|
8
8
|
def initialize(ascii: nil, unicode: nil, html_entity: nil)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# A module for visual music notation
|
|
2
|
+
module HeadMusic::Notation; end
|
|
3
|
+
|
|
4
|
+
# Represents the mapping of an instrument (and optional playing technique) to a staff position
|
|
5
|
+
#
|
|
6
|
+
# @example Basic mapping
|
|
7
|
+
# mapping = StaffMapping.new({
|
|
8
|
+
# "staff_position" => 4,
|
|
9
|
+
# "instrument" => "snare_drum"
|
|
10
|
+
# })
|
|
11
|
+
# mapping.instrument.name #=> "snare drum"
|
|
12
|
+
# mapping.position_index #=> 4
|
|
13
|
+
#
|
|
14
|
+
# @example Mapping with playing technique
|
|
15
|
+
# mapping = StaffMapping.new({
|
|
16
|
+
# "staff_position" => -1,
|
|
17
|
+
# "instrument" => "hi_hat",
|
|
18
|
+
# "playing_technique" => "pedal"
|
|
19
|
+
# })
|
|
20
|
+
# mapping.playing_technique.name #=> "pedal"
|
|
21
|
+
class HeadMusic::Notation::StaffMapping
|
|
22
|
+
attr_reader :staff_position, :instrument_key, :playing_technique_key
|
|
23
|
+
|
|
24
|
+
# Initialize a new StaffMapping
|
|
25
|
+
#
|
|
26
|
+
# @param attributes [Hash] the mapping attributes
|
|
27
|
+
# @option attributes [Integer, String] "staff_position" the staff position index
|
|
28
|
+
# @option attributes [String] "instrument" the instrument key
|
|
29
|
+
# @option attributes [String] "playing_technique" optional playing technique key
|
|
30
|
+
def initialize(attributes)
|
|
31
|
+
@staff_position = HeadMusic::Notation::StaffPosition.new(attributes["staff_position"].to_i)
|
|
32
|
+
@instrument_key = attributes["instrument"]
|
|
33
|
+
@playing_technique_key = attributes["playing_technique"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the Instrument object
|
|
37
|
+
#
|
|
38
|
+
# @return [Instrument, nil] the instrument or nil if not found
|
|
39
|
+
def instrument
|
|
40
|
+
HeadMusic::Instruments::Instrument.get(instrument_key) if instrument_key
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the PlayingTechnique object
|
|
44
|
+
#
|
|
45
|
+
# @return [PlayingTechnique, nil] the playing technique or nil if not specified
|
|
46
|
+
def playing_technique
|
|
47
|
+
HeadMusic::Instruments::PlayingTechnique.get(playing_technique_key) if playing_technique_key
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get the staff position index
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer] the position index
|
|
53
|
+
def position_index
|
|
54
|
+
staff_position.index
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# String representation of this mapping
|
|
58
|
+
#
|
|
59
|
+
# @return [String] human-readable description
|
|
60
|
+
# @example
|
|
61
|
+
# mapping.to_s #=> "snare drum at line 3"
|
|
62
|
+
# mapping.to_s #=> "hi hat (pedal) at Space 0"
|
|
63
|
+
def to_s
|
|
64
|
+
parts = []
|
|
65
|
+
parts << (instrument&.name || instrument_key) if instrument_key
|
|
66
|
+
parts << "(#{playing_technique})" if playing_technique_key
|
|
67
|
+
parts << "at #{staff_position}"
|
|
68
|
+
parts.compact.join(" ")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# A module for visual music notation
|
|
2
|
+
module HeadMusic::Notation; end
|
|
3
|
+
|
|
4
|
+
class HeadMusic::Notation::StaffPosition
|
|
5
|
+
attr_reader :index # Integer, even = line, odd = space; bottom line = 0
|
|
6
|
+
|
|
7
|
+
NAMES = {
|
|
8
|
+
-2 => ["ledger line below staff"],
|
|
9
|
+
-1 => ["space below staff"],
|
|
10
|
+
0 => ["bottom line", "line 1"],
|
|
11
|
+
1 => ["bottom space", "space 1"],
|
|
12
|
+
2 => ["line 2"],
|
|
13
|
+
3 => ["space 2"],
|
|
14
|
+
4 => ["middle line", "line 3"],
|
|
15
|
+
5 => ["space 3"],
|
|
16
|
+
6 => ["line 4"],
|
|
17
|
+
7 => ["space 4"],
|
|
18
|
+
8 => ["top line", "line 5"],
|
|
19
|
+
9 => ["space above staff"],
|
|
20
|
+
10 => ["ledger line above staff"]
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Accepts a name (string or symbol) and returns the corresponding StaffPosition index (integer), or nil if not found
|
|
24
|
+
def self.name_to_index(name)
|
|
25
|
+
NAMES.each do |index, names|
|
|
26
|
+
if names.map { |n|
|
|
27
|
+
HeadMusic::Utilities::Case.to_snake_case(n)
|
|
28
|
+
}.include?(HeadMusic::Utilities::Case.to_snake_case(name))
|
|
29
|
+
return index
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(index)
|
|
36
|
+
@index = index
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def line?
|
|
40
|
+
index.even?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def space?
|
|
44
|
+
index.odd?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def line_number
|
|
48
|
+
return nil unless line?
|
|
49
|
+
(index / 2) + 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def space_number
|
|
53
|
+
return nil unless space?
|
|
54
|
+
((index - 1) / 2) + 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
return NAMES[index].first if NAMES.key?(index)
|
|
59
|
+
|
|
60
|
+
line? ? "line #{line_number}" : "space #{space_number}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require "head_music/rudiment/musical_symbol"
|
|
2
|
-
|
|
3
1
|
# A module for music rudiments
|
|
4
2
|
module HeadMusic::Rudiment; end
|
|
5
3
|
|
|
@@ -9,40 +7,20 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
9
7
|
include Comparable
|
|
10
8
|
include HeadMusic::Named
|
|
11
9
|
|
|
12
|
-
attr_reader :identifier, :
|
|
10
|
+
attr_reader :identifier, :semitones, :musical_symbols
|
|
13
11
|
|
|
14
12
|
delegate :ascii, :unicode, :html_entity, to: :musical_symbol
|
|
15
13
|
|
|
16
|
-
ALTERATION_RECORDS =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
identifier: :flat, cents: -100,
|
|
23
|
-
symbols: [{ascii: "b", unicode: "♭", html_entity: "♭"}]
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
identifier: :natural, cents: 0,
|
|
27
|
-
symbols: [{ascii: "", unicode: "♮", html_entity: "♮"}]
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
identifier: :double_sharp, cents: 200,
|
|
31
|
-
symbols: [{ascii: "x", unicode: "𝄪", html_entity: "𝄪"}]
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
identifier: :double_flat, cents: -200,
|
|
35
|
-
symbols: [{ascii: "bb", unicode: "𝄫", html_entity: "𝄫"}]
|
|
36
|
-
}
|
|
37
|
-
].freeze
|
|
38
|
-
|
|
39
|
-
ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.map { |attributes| attributes[:identifier] }.freeze
|
|
40
|
-
SYMBOLS = ALTERATION_RECORDS.map { |attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.freeze
|
|
41
|
-
PATTERN = Regexp.union(SYMBOLS.reject { |s| s.nil? || s.empty? })
|
|
14
|
+
ALTERATION_RECORDS =
|
|
15
|
+
YAML.load_file(File.expand_path("alterations.yml", __dir__), symbolize_names: true)[:alterations].freeze
|
|
16
|
+
|
|
17
|
+
ALTERATION_IDENTIFIERS = ALTERATION_RECORDS.keys.freeze
|
|
18
|
+
SYMBOLS = ALTERATION_RECORDS.map { |key, attributes| attributes[:symbols].map { |symbol| [symbol[:unicode], symbol[:ascii]] } }.flatten.reject { |s| s.nil? || s.empty? }.freeze
|
|
19
|
+
PATTERN = Regexp.union(SYMBOLS)
|
|
42
20
|
MATCHER = PATTERN
|
|
43
21
|
|
|
44
22
|
def self.all
|
|
45
|
-
ALTERATION_RECORDS.map { |attributes| new(attributes) }
|
|
23
|
+
@all ||= ALTERATION_RECORDS.map { |key, attributes| new(key, attributes) }
|
|
46
24
|
end
|
|
47
25
|
|
|
48
26
|
def self.symbols
|
|
@@ -57,13 +35,13 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
57
35
|
return identifier if identifier.is_a?(HeadMusic::Rudiment::Alteration)
|
|
58
36
|
|
|
59
37
|
all.detect do |alteration|
|
|
60
|
-
alteration.
|
|
38
|
+
alteration.representations.include?(identifier)
|
|
61
39
|
end
|
|
62
40
|
end
|
|
63
41
|
|
|
64
42
|
def self.by(key, value)
|
|
65
43
|
all.detect do |alteration|
|
|
66
|
-
alteration.send(key) == value if %i[
|
|
44
|
+
alteration.send(key) == value if %i[semitones].include?(key.to_sym)
|
|
67
45
|
end
|
|
68
46
|
end
|
|
69
47
|
|
|
@@ -71,24 +49,16 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
71
49
|
all.detect { |alteration| alteration.name == name.to_s }
|
|
72
50
|
end
|
|
73
51
|
|
|
74
|
-
def self.from_pitched_item(input)
|
|
75
|
-
nil
|
|
76
|
-
end
|
|
77
|
-
|
|
78
52
|
def name(locale_code: I18n.locale)
|
|
79
53
|
super || identifier.to_s.tr("_", " ")
|
|
80
54
|
end
|
|
81
55
|
|
|
82
|
-
def
|
|
56
|
+
def representations
|
|
83
57
|
[identifier, identifier.to_s, name, ascii, unicode, html_entity]
|
|
84
58
|
.reject { |representation| representation.to_s.strip == "" }
|
|
85
59
|
end
|
|
86
60
|
|
|
87
|
-
|
|
88
|
-
cents / 100.0
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
ALTERATION_IDENTIFIERS.each do |key|
|
|
61
|
+
ALTERATION_RECORDS.keys.each do |key|
|
|
92
62
|
define_method(:"#{key}?") { identifier == key }
|
|
93
63
|
end
|
|
94
64
|
|
|
@@ -98,7 +68,7 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
98
68
|
|
|
99
69
|
def <=>(other)
|
|
100
70
|
other = HeadMusic::Rudiment::Alteration.get(other)
|
|
101
|
-
|
|
71
|
+
semitones <=> other.semitones
|
|
102
72
|
end
|
|
103
73
|
|
|
104
74
|
def musical_symbol
|
|
@@ -107,9 +77,9 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
107
77
|
|
|
108
78
|
private
|
|
109
79
|
|
|
110
|
-
def initialize(attributes)
|
|
111
|
-
@identifier =
|
|
112
|
-
@
|
|
80
|
+
def initialize(key, attributes)
|
|
81
|
+
@identifier = key
|
|
82
|
+
@semitones = attributes[:semitones]
|
|
113
83
|
initialize_musical_symbols(attributes[:symbols])
|
|
114
84
|
initialize_localized_names
|
|
115
85
|
end
|
|
@@ -122,7 +92,7 @@ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
|
|
|
122
92
|
|
|
123
93
|
def initialize_musical_symbols(list)
|
|
124
94
|
@musical_symbols = (list || []).map do |record|
|
|
125
|
-
HeadMusic::
|
|
95
|
+
HeadMusic::Notation::MusicalSymbol.new(
|
|
126
96
|
unicode: record[:unicode],
|
|
127
97
|
ascii: record[:ascii],
|
|
128
98
|
html_entity: record[:html_entity]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
alterations:
|
|
2
|
+
double_flat:
|
|
3
|
+
semitones: -2
|
|
4
|
+
symbols:
|
|
5
|
+
- ascii: "bb"
|
|
6
|
+
unicode: "𝄫"
|
|
7
|
+
html_entity: "𝄫"
|
|
8
|
+
flat:
|
|
9
|
+
semitones: -1
|
|
10
|
+
symbols:
|
|
11
|
+
- ascii: "b"
|
|
12
|
+
unicode: "♭"
|
|
13
|
+
html_entity: "♭"
|
|
14
|
+
natural:
|
|
15
|
+
cents: 0
|
|
16
|
+
semitones: 0
|
|
17
|
+
symbols:
|
|
18
|
+
- ascii: ""
|
|
19
|
+
unicode: "♮"
|
|
20
|
+
html_entity: "♮"
|
|
21
|
+
sharp:
|
|
22
|
+
semitones: 1
|
|
23
|
+
symbols:
|
|
24
|
+
- ascii: "#"
|
|
25
|
+
unicode: "♯"
|
|
26
|
+
html_entity: "♯"
|
|
27
|
+
double_sharp:
|
|
28
|
+
semitones: 2
|
|
29
|
+
symbols:
|
|
30
|
+
- ascii: "x"
|
|
31
|
+
unicode: "𝄪"
|
|
32
|
+
html_entity: "𝄪"
|
|
@@ -17,7 +17,7 @@ class HeadMusic::Rudiment::ChromaticInterval < HeadMusic::Rudiment::Base
|
|
|
17
17
|
|
|
18
18
|
def self.get(identifier)
|
|
19
19
|
@intervals ||= {}
|
|
20
|
-
candidate =
|
|
20
|
+
candidate = HeadMusic::Utilities::Case.to_snake_case(identifier)
|
|
21
21
|
semitones = NAMES.index(candidate) || identifier.to_i
|
|
22
22
|
@intervals[semitones] ||= new(semitones.to_i)
|
|
23
23
|
end
|
|
@@ -98,7 +98,7 @@ class HeadMusic::Rudiment::Clef < HeadMusic::Rudiment::Base
|
|
|
98
98
|
|
|
99
99
|
def initialize_musical_symbols(list)
|
|
100
100
|
@musical_symbols = (list || []).map do |symbol_data|
|
|
101
|
-
HeadMusic::
|
|
101
|
+
HeadMusic::Notation::MusicalSymbol.new(**symbol_data.slice(:ascii, :html_entity, :unicode))
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -3,17 +3,6 @@ module HeadMusic::Rudiment; end
|
|
|
3
3
|
|
|
4
4
|
# Consonance describes a category or degree of harmonic pleasantness
|
|
5
5
|
class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
|
|
6
|
-
# Detailed categories aligned with music theory
|
|
7
|
-
LEVELS = %w[
|
|
8
|
-
perfect_consonance
|
|
9
|
-
imperfect_consonance
|
|
10
|
-
contextual
|
|
11
|
-
mild_dissonance
|
|
12
|
-
harsh_dissonance
|
|
13
|
-
dissonance
|
|
14
|
-
].freeze
|
|
15
|
-
|
|
16
|
-
# Constants for each consonance level
|
|
17
6
|
PERFECT_CONSONANCE = :perfect_consonance
|
|
18
7
|
IMPERFECT_CONSONANCE = :imperfect_consonance
|
|
19
8
|
CONTEXTUAL = :contextual
|
|
@@ -21,10 +10,22 @@ class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
|
|
|
21
10
|
HARSH_DISSONANCE = :harsh_dissonance
|
|
22
11
|
DISSONANCE = :dissonance
|
|
23
12
|
|
|
13
|
+
LEVELS = [
|
|
14
|
+
PERFECT_CONSONANCE,
|
|
15
|
+
IMPERFECT_CONSONANCE,
|
|
16
|
+
CONTEXTUAL,
|
|
17
|
+
MILD_DISSONANCE,
|
|
18
|
+
HARSH_DISSONANCE,
|
|
19
|
+
DISSONANCE
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
24
22
|
def self.get(name)
|
|
23
|
+
return name if name.is_a?(self)
|
|
24
|
+
return nil if name.nil?
|
|
25
|
+
|
|
25
26
|
@consonances ||= {}
|
|
26
27
|
name_sym = name.to_sym
|
|
27
|
-
@consonances[name_sym] ||= new(name) if LEVELS.include?(
|
|
28
|
+
@consonances[name_sym] ||= new(name) if LEVELS.include?(name_sym)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
attr_reader :name
|
|
@@ -56,6 +57,6 @@ class HeadMusic::Rudiment::Consonance < HeadMusic::Rudiment::Base
|
|
|
56
57
|
|
|
57
58
|
# Predicate methods for each level
|
|
58
59
|
LEVELS.each do |method_name|
|
|
59
|
-
define_method(:"#{method_name}?") { to_s == method_name }
|
|
60
|
+
define_method(:"#{method_name}?") { to_s == method_name.to_s }
|
|
60
61
|
end
|
|
61
62
|
end
|
|
@@ -4,8 +4,6 @@ module HeadMusic::Rudiment; end
|
|
|
4
4
|
# Represents a key signature (traditionally associated with a key)
|
|
5
5
|
# This class maintains backward compatibility while delegating to Key/Mode internally
|
|
6
6
|
class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
7
|
-
attr_reader :tonal_context
|
|
8
|
-
|
|
9
7
|
ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
|
|
10
8
|
ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
|
|
11
9
|
|
|
@@ -27,10 +25,6 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
def self.from_tonal_context(tonal_context)
|
|
31
|
-
new_from_context(tonal_context)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
28
|
def self.from_scale(scale)
|
|
35
29
|
# Find a key or mode that uses this scale
|
|
36
30
|
tonic = scale.root_pitch.spelling
|
|
@@ -50,26 +44,6 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
|
50
44
|
@scale_type ||= HeadMusic::Rudiment::ScaleType.default
|
|
51
45
|
@scale_type = @scale_type.parent || @scale_type
|
|
52
46
|
@scale = HeadMusic::Rudiment::Scale.get(@tonic_spelling, @scale_type)
|
|
53
|
-
|
|
54
|
-
# Create appropriate tonal context
|
|
55
|
-
scale_type_str = scale_type.to_s.downcase if scale_type
|
|
56
|
-
|
|
57
|
-
@tonal_context = if %w[major minor].include?(scale_type_str)
|
|
58
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} #{scale_type}")
|
|
59
|
-
elsif scale_type
|
|
60
|
-
HeadMusic::Rudiment::Mode.get("#{tonic_spelling} #{scale_type}")
|
|
61
|
-
else
|
|
62
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
63
|
-
end
|
|
64
|
-
rescue ArgumentError
|
|
65
|
-
# Fall back to creating a major key if mode is not recognized
|
|
66
|
-
@tonal_context = HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def self.new_from_context(context)
|
|
70
|
-
instance = allocate
|
|
71
|
-
instance.instance_variable_set(:@tonal_context, context)
|
|
72
|
-
instance
|
|
73
47
|
end
|
|
74
48
|
|
|
75
49
|
def spellings
|
|
@@ -38,7 +38,7 @@ class HeadMusic::Rudiment::RhythmicUnit::Parser
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def normalized_identifier
|
|
41
|
-
@normalized_identifier ||=
|
|
41
|
+
@normalized_identifier ||= HeadMusic::Utilities::Case.to_snake_case(identifier)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def from_american_name
|
|
@@ -81,6 +81,6 @@ class HeadMusic::Rudiment::RhythmicUnit::Parser
|
|
|
81
81
|
|
|
82
82
|
def normalize_name(name)
|
|
83
83
|
return nil if name.nil?
|
|
84
|
-
|
|
84
|
+
HeadMusic::Utilities::Case.to_snake_case(name)
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -45,7 +45,7 @@ class HeadMusic::Rudiment::RhythmicValue::Parser
|
|
|
45
45
|
# But skip this if identifier looks like a decimal number
|
|
46
46
|
unless identifier.match?(/^\d+\.\d+$/)
|
|
47
47
|
dots = identifier.scan(".").count
|
|
48
|
-
base_identifier = identifier.
|
|
48
|
+
base_identifier = identifier.delete(".").strip
|
|
49
49
|
|
|
50
50
|
# Try RhythmicUnit::Parser on the base identifier
|
|
51
51
|
parser = RhythmicUnit::Parser.new(base_identifier)
|
|
@@ -25,7 +25,7 @@ class HeadMusic::Rudiment::RhythmicValue
|
|
|
25
25
|
return parsed if parsed
|
|
26
26
|
|
|
27
27
|
# Then try the word-based approach as fallback
|
|
28
|
-
identifier =
|
|
28
|
+
identifier = HeadMusic::Utilities::Case.to_snake_case(original_identifier)
|
|
29
29
|
begin
|
|
30
30
|
from_words(identifier)
|
|
31
31
|
rescue
|
|
@@ -10,6 +10,9 @@ class HeadMusic::Rudiment::Spelling < HeadMusic::Rudiment::Base
|
|
|
10
10
|
|
|
11
11
|
MATCHER = /^\s*(#{LetterName::PATTERN})(#{Alteration::PATTERN})?(-?\d+)?\s*$/i
|
|
12
12
|
|
|
13
|
+
# All chromatic spellings using single sharps and flats (ASCII notation)
|
|
14
|
+
CHROMATIC_SPELLINGS = %w[C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B].freeze
|
|
15
|
+
|
|
13
16
|
attr_reader :pitch_class, :letter_name, :alteration
|
|
14
17
|
|
|
15
18
|
delegate :number, to: :pitch_class, prefix: true
|
|
@@ -43,43 +43,4 @@ class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
|
|
|
43
43
|
# Calculate the frequency
|
|
44
44
|
tonal_center_frequency * ratio
|
|
45
45
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def calculate_tonal_center_frequency
|
|
50
|
-
# Use equal temperament to get the tonal center frequency from the reference pitch
|
|
51
|
-
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
|
52
|
-
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def ratio_for_interval(semitones)
|
|
56
|
-
# Handle octaves
|
|
57
|
-
octaves = semitones / 12
|
|
58
|
-
interval_within_octave = semitones % 12
|
|
59
|
-
|
|
60
|
-
# Make sure we handle negative intervals
|
|
61
|
-
if interval_within_octave < 0
|
|
62
|
-
interval_within_octave += 12
|
|
63
|
-
octaves -= 1
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Get the base ratio
|
|
67
|
-
base_ratio = case interval_within_octave
|
|
68
|
-
when 0 then INTERVAL_RATIOS[:unison]
|
|
69
|
-
when 1 then INTERVAL_RATIOS[:minor_second]
|
|
70
|
-
when 2 then INTERVAL_RATIOS[:major_second]
|
|
71
|
-
when 3 then INTERVAL_RATIOS[:minor_third]
|
|
72
|
-
when 4 then INTERVAL_RATIOS[:major_third]
|
|
73
|
-
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
|
74
|
-
when 6 then INTERVAL_RATIOS[:tritone]
|
|
75
|
-
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
|
76
|
-
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
|
77
|
-
when 9 then INTERVAL_RATIOS[:major_sixth]
|
|
78
|
-
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
|
79
|
-
when 11 then INTERVAL_RATIOS[:major_seventh]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Apply octave adjustments
|
|
83
|
-
base_ratio * (2**octaves)
|
|
84
|
-
end
|
|
85
46
|
end
|