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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/CLAUDE.md +32 -15
  4. data/Gemfile.lock +1 -1
  5. data/MUSIC_THEORY.md +120 -0
  6. data/lib/head_music/analysis/diatonic_interval.rb +29 -27
  7. data/lib/head_music/analysis/interval_consonance.rb +51 -0
  8. data/lib/head_music/content/note.rb +1 -1
  9. data/lib/head_music/content/placement.rb +1 -1
  10. data/lib/head_music/content/position.rb +1 -1
  11. data/lib/head_music/content/staff.rb +1 -1
  12. data/lib/head_music/instruments/instrument.rb +103 -113
  13. data/lib/head_music/instruments/instrument_family.rb +13 -2
  14. data/lib/head_music/instruments/instrument_type.rb +188 -0
  15. data/lib/head_music/instruments/score_order.rb +139 -0
  16. data/lib/head_music/instruments/score_orders.yml +130 -0
  17. data/lib/head_music/instruments/variant.rb +6 -0
  18. data/lib/head_music/locales/de.yml +6 -0
  19. data/lib/head_music/locales/en.yml +6 -0
  20. data/lib/head_music/locales/es.yml +6 -0
  21. data/lib/head_music/locales/fr.yml +6 -0
  22. data/lib/head_music/locales/it.yml +6 -0
  23. data/lib/head_music/locales/ru.yml +6 -0
  24. data/lib/head_music/rudiment/alteration.rb +23 -8
  25. data/lib/head_music/rudiment/base.rb +9 -0
  26. data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
  27. data/lib/head_music/rudiment/clef.rb +1 -1
  28. data/lib/head_music/rudiment/consonance.rb +37 -4
  29. data/lib/head_music/rudiment/diatonic_context.rb +25 -0
  30. data/lib/head_music/rudiment/key.rb +77 -0
  31. data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
  32. data/lib/head_music/rudiment/key_signature.rb +46 -7
  33. data/lib/head_music/rudiment/letter_name.rb +3 -3
  34. data/lib/head_music/rudiment/meter.rb +19 -9
  35. data/lib/head_music/rudiment/mode.rb +92 -0
  36. data/lib/head_music/rudiment/musical_symbol.rb +1 -1
  37. data/lib/head_music/rudiment/note.rb +112 -0
  38. data/lib/head_music/rudiment/pitch/parser.rb +52 -0
  39. data/lib/head_music/rudiment/pitch.rb +5 -6
  40. data/lib/head_music/rudiment/pitch_class.rb +1 -1
  41. data/lib/head_music/rudiment/quality.rb +1 -1
  42. data/lib/head_music/rudiment/reference_pitch.rb +1 -1
  43. data/lib/head_music/rudiment/register.rb +4 -1
  44. data/lib/head_music/rudiment/rest.rb +36 -0
  45. data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
  46. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
  47. data/lib/head_music/rudiment/rhythmic_unit.rb +13 -5
  48. data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
  49. data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
  50. data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
  51. data/lib/head_music/rudiment/scale.rb +4 -5
  52. data/lib/head_music/rudiment/scale_degree.rb +1 -1
  53. data/lib/head_music/rudiment/scale_type.rb +9 -3
  54. data/lib/head_music/rudiment/solmization.rb +1 -1
  55. data/lib/head_music/rudiment/spelling.rb +5 -4
  56. data/lib/head_music/rudiment/tempo.rb +85 -0
  57. data/lib/head_music/rudiment/tonal_context.rb +35 -0
  58. data/lib/head_music/rudiment/tuning.rb +1 -1
  59. data/lib/head_music/rudiment/unpitched_note.rb +62 -0
  60. data/lib/head_music/style/medieval_tradition.rb +26 -0
  61. data/lib/head_music/style/modern_tradition.rb +34 -0
  62. data/lib/head_music/style/renaissance_tradition.rb +26 -0
  63. data/lib/head_music/style/tradition.rb +21 -0
  64. data/lib/head_music/utilities/hash_key.rb +34 -2
  65. data/lib/head_music/version.rb +1 -1
  66. data/lib/head_music.rb +31 -10
  67. data/user_stories/active/handle-time.md +7 -0
  68. data/user_stories/active/handle-time.rb +177 -0
  69. data/user_stories/done/epic--score-order/PLAN.md +244 -0
  70. data/user_stories/done/instrument-variant.md +65 -0
  71. data/user_stories/done/superclass-for-note.md +30 -0
  72. data/user_stories/todo/agentic-daw.md +3 -0
  73. data/user_stories/{backlog → todo}/dyad-analysis.md +2 -10
  74. data/user_stories/todo/material-and-scores.md +10 -0
  75. data/user_stories/todo/organizing-content.md +72 -0
  76. data/user_stories/todo/percussion_set.md +1 -0
  77. data/user_stories/{backlog → todo}/pitch-class-set-analysis.md +40 -0
  78. data/user_stories/{backlog → todo}/pitch-set-classification.md +10 -0
  79. data/user_stories/{backlog → todo}/sonority-identification.md +20 -0
  80. metadata +43 -12
  81. data/TODO.md +0 -109
  82. /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
  83. /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
  84. /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
  85. /data/user_stories/{backlog → todo}/consonance-dissonance-classification.md +0 -0
@@ -0,0 +1,130 @@
1
+ ---
2
+ orchestral:
3
+ name: "Orchestral"
4
+ sections:
5
+ - section_key: woodwind
6
+ instruments:
7
+ - piccolo_flute
8
+ - flute
9
+ - alto_flute
10
+ - alto_recorder
11
+ - soprano_recorder
12
+ - tenor_recorder
13
+ - oboe
14
+ - english_horn
15
+ - clarinet
16
+ - alto_clarinet
17
+ - bass_clarinet
18
+ - soprano_saxophone
19
+ - alto_saxophone
20
+ - tenor_saxophone
21
+ - baritone_saxophone
22
+ - bassoon
23
+ - contrabassoon
24
+ - section_key: brass
25
+ instruments:
26
+ - french_horn
27
+ - trumpet
28
+ - cornet
29
+ - trombone
30
+ - bass_trombone
31
+ - tuba
32
+ - section_key: percussion
33
+ instruments:
34
+ - timpani
35
+ - snare_drum
36
+ - bass_drum
37
+ - cymbal
38
+ - gong
39
+ - xylophone
40
+ - glockenspiel
41
+ - marimba
42
+ - vibraphone
43
+ - percussion
44
+ - section_key: keyboard
45
+ instruments:
46
+ - harp
47
+ - piano
48
+ - celesta
49
+ - harpsichord
50
+ - organ
51
+ - section_key: voice
52
+ instruments:
53
+ - soprano_voice
54
+ - alto_voice
55
+ - tenor_voice
56
+ - bass_voice
57
+ - section_key: string
58
+ instruments:
59
+ - violin
60
+ - viola
61
+ - cello
62
+ - double_bass
63
+
64
+ band:
65
+ name: "Concert Band"
66
+ sections:
67
+ - section_key: woodwind
68
+ instruments:
69
+ - piccolo
70
+ - flute
71
+ - oboe
72
+ - english_horn
73
+ - bassoon
74
+ - contrabassoon
75
+ - clarinet
76
+ - alto_clarinet
77
+ - bass_clarinet
78
+ - soprano_saxophone
79
+ - alto_saxophone
80
+ - tenor_saxophone
81
+ - baritone_saxophone
82
+ - section_key: brass
83
+ instruments:
84
+ - cornet
85
+ - trumpet
86
+ - french_horn
87
+ - trombone
88
+ - bass_trombone
89
+ - euphonium
90
+ - baritone_horn
91
+ - tuba
92
+ - section_key: percussion
93
+ instruments:
94
+ - timpani
95
+ - snare_drum
96
+ - bass_drum
97
+ - cymbal
98
+ - percussion
99
+
100
+ brass_quintet:
101
+ name: "Brass Quintet"
102
+ sections:
103
+ - section_key: brass
104
+ instruments:
105
+ - trumpet # First trumpet
106
+ - trumpet # Second trumpet
107
+ - french_horn
108
+ - trombone
109
+ - tuba
110
+
111
+ woodwind_quintet:
112
+ name: "Woodwind Quintet"
113
+ sections:
114
+ - section_key: mixed
115
+ instruments:
116
+ - flute
117
+ - oboe
118
+ - clarinet
119
+ - french_horn # Horn is traditional in woodwind quintets
120
+ - bassoon
121
+
122
+ string_quartet:
123
+ name: "String Quartet"
124
+ sections:
125
+ - section_key: string
126
+ instruments:
127
+ - violin # First violin
128
+ - violin # Second violin
129
+ - viola
130
+ - cello
@@ -35,4 +35,10 @@ class HeadMusic::Instruments::Variant
35
35
  @default_staff_scheme ||=
36
36
  staff_schemes.find(&:default?) || staff_schemes.first
37
37
  end
38
+
39
+ def ==(other)
40
+ return false unless other.is_a?(self.class)
41
+
42
+ key == other.key && attributes == other.attributes
43
+ end
38
44
  end
@@ -1,5 +1,11 @@
1
1
  de:
2
2
  head_music:
3
+ alterations:
4
+ sharp: Kreuz
5
+ flat: Be
6
+ natural: Auflösungszeichen
7
+ double_sharp: Doppelkreuz
8
+ double_flat: Doppel-Be
3
9
  chromatic_intervals:
4
10
  perfect_unison: reine Prime
5
11
  minor_second: kleine Sekunde
@@ -1,5 +1,11 @@
1
1
  en:
2
2
  head_music:
3
+ alterations:
4
+ sharp: sharp
5
+ flat: flat
6
+ natural: natural
7
+ double_sharp: double sharp
8
+ double_flat: double flat
3
9
  chromatic_intervals:
4
10
  perfect_unison: perfect unison
5
11
  minor_second: minor second
@@ -1,5 +1,11 @@
1
1
  es:
2
2
  head_music:
3
+ alterations:
4
+ sharp: sostenido
5
+ flat: bemol
6
+ natural: becuadro
7
+ double_sharp: doble sostenido
8
+ double_flat: doble bemol
3
9
  chromatic_intervals:
4
10
  perfect_unison: unísono perfecto
5
11
  minor_second: segunda menor
@@ -1,5 +1,11 @@
1
1
  fr:
2
2
  head_music:
3
+ alterations:
4
+ sharp: dièse
5
+ flat: bémol
6
+ natural: bécarre
7
+ double_sharp: double dièse
8
+ double_flat: double bémol
3
9
  chromatic_intervals:
4
10
  perfect_unison: unisson parfait
5
11
  minor_second: seconde mineure
@@ -1,5 +1,11 @@
1
1
  it:
2
2
  head_music:
3
+ alterations:
4
+ sharp: diesis
5
+ flat: bemolle
6
+ natural: bequadro
7
+ double_sharp: doppio diesis
8
+ double_flat: doppio bemolle
3
9
  chromatic_intervals:
4
10
  perfect_unison: unisono perfetto
5
11
  minor_second: seconda minore
@@ -1,5 +1,11 @@
1
1
  ru:
2
2
  head_music:
3
+ alterations:
4
+ sharp: диез
5
+ flat: бемоль
6
+ natural: бекар
7
+ double_sharp: дубль-диез
8
+ double_flat: дубль-бемоль
3
9
  chromatic_intervals:
4
10
  perfect_unison: чистый унисон
5
11
  minor_second: малая секунда
@@ -5,8 +5,9 @@ module HeadMusic::Rudiment; end
5
5
 
6
6
  # An Alteration is a symbol that modifies pitch, such as a sharp, flat, or natural.
7
7
  # In French, sharps and flats in the key signature are called "altérations".
8
- class HeadMusic::Rudiment::Alteration
8
+ class HeadMusic::Rudiment::Alteration < HeadMusic::Rudiment::Base
9
9
  include Comparable
10
+ include HeadMusic::Named
10
11
 
11
12
  attr_reader :identifier, :cents, :musical_symbols
12
13
 
@@ -36,6 +37,9 @@ class HeadMusic::Rudiment::Alteration
36
37
  ].freeze
37
38
 
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? })
42
+ MATCHER = PATTERN
39
43
 
40
44
  def self.all
41
45
  ALTERATION_RECORDS.map { |attributes| new(attributes) }
@@ -45,12 +49,8 @@ class HeadMusic::Rudiment::Alteration
45
49
  @symbols ||= all.map { |alteration| [alteration.ascii, alteration.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
46
50
  end
47
51
 
48
- def self.matcher
49
- @matcher ||= Regexp.new symbols.join("|")
50
- end
51
-
52
52
  def self.symbol?(candidate)
53
- candidate =~ /^(#{matcher})$/
53
+ SYMBOLS.include?(candidate)
54
54
  end
55
55
 
56
56
  def self.get(identifier)
@@ -67,8 +67,16 @@ class HeadMusic::Rudiment::Alteration
67
67
  end
68
68
  end
69
69
 
70
- def name
71
- identifier.to_s.tr("_", " ")
70
+ def self.get_by_name(name)
71
+ all.detect { |alteration| alteration.name == name.to_s }
72
+ end
73
+
74
+ def self.from_pitched_item(input)
75
+ nil
76
+ end
77
+
78
+ def name(locale_code: I18n.locale)
79
+ super || identifier.to_s.tr("_", " ")
72
80
  end
73
81
 
74
82
  def representions
@@ -103,6 +111,13 @@ class HeadMusic::Rudiment::Alteration
103
111
  @identifier = attributes[:identifier]
104
112
  @cents = attributes[:cents]
105
113
  initialize_musical_symbols(attributes[:symbols])
114
+ initialize_localized_names
115
+ end
116
+
117
+ def initialize_localized_names
118
+ # Initialize default English names
119
+ ensure_localized_name(name: identifier.to_s.tr("_", " "), locale_code: :en)
120
+ # Additional localized names will be loaded from locale files
106
121
  end
107
122
 
108
123
  def initialize_musical_symbols(list)
@@ -0,0 +1,9 @@
1
+ module HeadMusic::Rudiment; end
2
+
3
+ class HeadMusic::Rudiment::Base
4
+ private
5
+
6
+ def initialize
7
+ raise NotImplementedError, "Cannot instantiate abstract rudiment base class"
8
+ end
9
+ end
@@ -2,7 +2,7 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # A chromatic interval is the distance between two pitches measured in half-steps.
5
- class HeadMusic::Rudiment::ChromaticInterval
5
+ class HeadMusic::Rudiment::ChromaticInterval < HeadMusic::Rudiment::Base
6
6
  include Comparable
7
7
  include HeadMusic::Named
8
8
 
@@ -23,11 +23,8 @@ class HeadMusic::Rudiment::ChromaticInterval
23
23
  end
24
24
 
25
25
  def initialize(identifier)
26
- if /^\D/i.match?(identifier.to_s.strip)
27
- candidate = identifier.to_s.downcase.gsub(/\W+/, "_")
28
- semitones = NAMES.index(candidate) || identifier.to_i
29
- end
30
- @semitones = semitones || identifier.to_i
26
+ candidate = HeadMusic::Utilities::HashKey.for(identifier).to_s
27
+ @semitones = NAMES.index(candidate) || identifier.to_i
31
28
  set_name
32
29
  end
33
30
 
@@ -4,7 +4,7 @@ require "yaml"
4
4
  module HeadMusic::Rudiment; end
5
5
 
6
6
  # A clef assigns pitches to the lines and spaces of a staff.
7
- class HeadMusic::Rudiment::Clef
7
+ class HeadMusic::Rudiment::Clef < HeadMusic::Rudiment::Base
8
8
  include HeadMusic::Named
9
9
 
10
10
  RECORDS = YAML.load_file(File.expand_path("clefs.yml", __dir__)).freeze
@@ -1,13 +1,30 @@
1
1
  # A module for music rudiments
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
- # Consonance describes a category or degree of harmonic pleasantness: perfect, imperfect, or dissonant
5
- class HeadMusic::Rudiment::Consonance
6
- LEVELS = %w[perfect imperfect dissonant].freeze
4
+ # Consonance describes a category or degree of harmonic pleasantness
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
+ PERFECT_CONSONANCE = :perfect_consonance
18
+ IMPERFECT_CONSONANCE = :imperfect_consonance
19
+ CONTEXTUAL = :contextual
20
+ MILD_DISSONANCE = :mild_dissonance
21
+ HARSH_DISSONANCE = :harsh_dissonance
22
+ DISSONANCE = :dissonance
7
23
 
8
24
  def self.get(name)
9
25
  @consonances ||= {}
10
- @consonances[name.to_sym] ||= new(name) if LEVELS.include?(name.to_s)
26
+ name_sym = name.to_sym
27
+ @consonances[name_sym] ||= new(name) if LEVELS.include?(name.to_s)
11
28
  end
12
29
 
13
30
  attr_reader :name
@@ -22,6 +39,22 @@ class HeadMusic::Rudiment::Consonance
22
39
  to_s == other.to_s
23
40
  end
24
41
 
42
+ # Check if this represents a consonance (perfect or imperfect)
43
+ def consonant?
44
+ [PERFECT_CONSONANCE, IMPERFECT_CONSONANCE].include?(name)
45
+ end
46
+
47
+ # Check if this represents any form of dissonance
48
+ def dissonant?
49
+ [MILD_DISSONANCE, HARSH_DISSONANCE, DISSONANCE].include?(name)
50
+ end
51
+
52
+ # Contextual is special - neither strictly consonant nor dissonant
53
+ def contextual?
54
+ name == CONTEXTUAL
55
+ end
56
+
57
+ # Predicate methods for each level
25
58
  LEVELS.each do |method_name|
26
59
  define_method(:"#{method_name}?") { to_s == method_name }
27
60
  end
@@ -0,0 +1,25 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Abstract class representing a diatonic tonal context (7-note scale system)
5
+ class HeadMusic::Rudiment::DiatonicContext < HeadMusic::Rudiment::TonalContext
6
+ def scale_type
7
+ raise NotImplementedError, "Subclasses must implement #scale_type"
8
+ end
9
+
10
+ def scale
11
+ @scale ||= HeadMusic::Rudiment::Scale.get(tonic_spelling, scale_type)
12
+ end
13
+
14
+ def key_signature
15
+ @key_signature ||= HeadMusic::Rudiment::KeySignature.from_scale(scale)
16
+ end
17
+
18
+ def relative
19
+ raise NotImplementedError, "Subclasses must implement #relative"
20
+ end
21
+
22
+ def parallel
23
+ raise NotImplementedError, "Subclasses must implement #parallel"
24
+ end
25
+ end
@@ -0,0 +1,77 @@
1
+ # A module for music rudiments
2
+ module HeadMusic::Rudiment; end
3
+
4
+ # Represents a musical key (major or minor)
5
+ class HeadMusic::Rudiment::Key < HeadMusic::Rudiment::DiatonicContext
6
+ include HeadMusic::Named
7
+
8
+ QUALITIES = %i[major minor].freeze
9
+
10
+ attr_reader :quality
11
+
12
+ def self.get(identifier)
13
+ return identifier if identifier.is_a?(HeadMusic::Rudiment::Key)
14
+
15
+ @keys ||= {}
16
+ tonic_spelling, quality_name = parse_identifier(identifier)
17
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier)
18
+ @keys[hash_key] ||= new(tonic_spelling, quality_name)
19
+ end
20
+
21
+ def self.parse_identifier(identifier)
22
+ tonic_spelling, quality_name = identifier.to_s.strip.split(/\s+/)
23
+ quality_name ||= "major"
24
+ [tonic_spelling, quality_name]
25
+ end
26
+
27
+ def initialize(tonic_spelling, quality = :major)
28
+ super(tonic_spelling)
29
+ @quality = quality.to_s.downcase.to_sym
30
+ raise ArgumentError, "Quality must be :major or :minor" unless QUALITIES.include?(@quality)
31
+ end
32
+
33
+ def scale_type
34
+ @scale_type ||= HeadMusic::Rudiment::ScaleType.get(quality)
35
+ end
36
+
37
+ def major?
38
+ quality == :major
39
+ end
40
+
41
+ def minor?
42
+ quality == :minor
43
+ end
44
+
45
+ def relative
46
+ if major?
47
+ # Major to relative minor: down a minor third (3 semitones)
48
+ relative_pitch = tonic_pitch + -3
49
+ self.class.get("#{relative_pitch.spelling} minor")
50
+ else
51
+ # Minor to relative major: up a minor third (3 semitones)
52
+ relative_pitch = tonic_pitch + 3
53
+ self.class.get("#{relative_pitch.spelling} major")
54
+ end
55
+ end
56
+
57
+ def parallel
58
+ if major?
59
+ self.class.get("#{tonic_spelling} minor")
60
+ else
61
+ self.class.get("#{tonic_spelling} major")
62
+ end
63
+ end
64
+
65
+ def name
66
+ "#{tonic_spelling} #{quality}"
67
+ end
68
+
69
+ def to_s
70
+ name
71
+ end
72
+
73
+ def ==(other)
74
+ other = self.class.get(other)
75
+ tonic_spelling == other.tonic_spelling && quality == other.quality
76
+ end
77
+ end
@@ -1,4 +1,4 @@
1
- # Key signatures are enharmonic when all pitch classes in one are respellings of the pitch classes in the other.
1
+ # Key signatures are enharmonic when they represent the same set of altered pitch classes but with different spellings.
2
2
  class HeadMusic::Rudiment::KeySignature::EnharmonicEquivalence
3
3
  attr_reader :key_signature
4
4
 
@@ -1,9 +1,10 @@
1
1
  # A module for music rudiments
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
- # Represents a key signature.
5
- class HeadMusic::Rudiment::KeySignature
6
- attr_reader :tonic_spelling, :scale_type, :scale
4
+ # Represents a key signature (traditionally associated with a key)
5
+ # This class maintains backward compatibility while delegating to Key/Mode internally
6
+ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
7
+ attr_reader :tonal_context
7
8
 
8
9
  ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
9
10
  ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
@@ -16,14 +17,32 @@ class HeadMusic::Rudiment::KeySignature
16
17
  return identifier if identifier.is_a?(HeadMusic::Rudiment::KeySignature)
17
18
 
18
19
  @key_signatures ||= {}
19
- tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
20
- hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
21
- @key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
20
+
21
+ if identifier.is_a?(String)
22
+ tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
23
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
24
+ @key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
25
+ elsif identifier.is_a?(HeadMusic::Rudiment::DiatonicContext)
26
+ identifier.key_signature
27
+ end
28
+ end
29
+
30
+ def self.from_tonal_context(tonal_context)
31
+ new_from_context(tonal_context)
22
32
  end
23
33
 
34
+ def self.from_scale(scale)
35
+ # Find a key or mode that uses this scale
36
+ tonic = scale.root_pitch.spelling
37
+ scale_type = scale.scale_type
38
+ new(tonic, scale_type)
39
+ end
40
+
41
+ attr_reader :tonic_spelling, :scale_type, :scale
42
+
24
43
  delegate :pitch_class, to: :tonic_spelling, prefix: :tonic
25
- delegate :to_s, to: :name
26
44
  delegate :pitches, :pitch_classes, to: :scale
45
+ delegate :to_s, to: :name
27
46
 
28
47
  def initialize(tonic_spelling, scale_type = nil)
29
48
  @tonic_spelling = HeadMusic::Rudiment::Spelling.get(tonic_spelling)
@@ -31,6 +50,26 @@ class HeadMusic::Rudiment::KeySignature
31
50
  @scale_type ||= HeadMusic::Rudiment::ScaleType.default
32
51
  @scale_type = @scale_type.parent || @scale_type
33
52
  @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
34
73
  end
35
74
 
36
75
  def spellings
@@ -2,8 +2,10 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # Music has seven lette names that are used to identify pitches and pitch classes.
5
- class HeadMusic::Rudiment::LetterName
5
+ class HeadMusic::Rudiment::LetterName < HeadMusic::Rudiment::Base
6
6
  NAMES = %w[C D E F G A B].freeze
7
+ PATTERN = /[A-Ga-g]/
8
+ MATCHER = /^#{PATTERN}$/
7
9
 
8
10
  NATURAL_PITCH_CLASS_NUMBERS = {
9
11
  "C" => 0,
@@ -96,6 +98,4 @@ class HeadMusic::Rudiment::LetterName
96
98
  series
97
99
  end
98
100
  end
99
-
100
- private_class_method :new
101
101
  end
@@ -2,7 +2,7 @@
2
2
  module HeadMusic::Rudiment; end
3
3
 
4
4
  # Meter is the rhythmic size of a measure, such as 4/4 or 6/8
5
- class HeadMusic::Rudiment::Meter
5
+ class HeadMusic::Rudiment::Meter < HeadMusic::Rudiment::Base
6
6
  attr_reader :top_number, :bottom_number
7
7
 
8
8
  NAMED = {
@@ -15,7 +15,7 @@ class HeadMusic::Rudiment::Meter
15
15
  hash_key = HeadMusic::Utilities::HashKey.for(identifier)
16
16
  time_signature_string = NAMED[hash_key] || identifier
17
17
  @meters ||= {}
18
- @meters[hash_key] ||= new(*time_signature_string.split("/").map(&:to_i))
18
+ @meters[hash_key] ||= new(*time_signature_string.split("/"))
19
19
  end
20
20
 
21
21
  def self.default
@@ -31,8 +31,8 @@ class HeadMusic::Rudiment::Meter
31
31
  end
32
32
 
33
33
  def initialize(top_number, bottom_number)
34
- @top_number = top_number
35
- @bottom_number = bottom_number
34
+ @top_number = top_number.to_i
35
+ @bottom_number = bottom_number.to_i
36
36
  end
37
37
 
38
38
  def simple?
@@ -63,6 +63,10 @@ class HeadMusic::Rudiment::Meter
63
63
  top_number
64
64
  end
65
65
 
66
+ def counts_per_quarter_note
67
+ 0.25 / count_unit.relative_value
68
+ end
69
+
66
70
  def beat_strength(count, tick: 0)
67
71
  return 100 if downbeat?(count, tick)
68
72
  return 80 if strong_beat?(count, tick)
@@ -76,19 +80,25 @@ class HeadMusic::Rudiment::Meter
76
80
  @ticks_per_count ||= count_unit.ticks
77
81
  end
78
82
 
83
+ # The rhythmic unit for the count (bottom number).
84
+ # This unit is also used as "beats" in a sequencer context
85
+ # For example, "1:3:000"
79
86
  def count_unit
80
87
  HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number)
81
88
  end
82
89
 
83
- def beat_unit
84
- @beat_unit ||=
90
+ def beat_value
91
+ @beat_value ||=
85
92
  if compound?
86
- HeadMusic::Content::RhythmicValue.new(HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
93
+ HeadMusic::Rudiment::RhythmicValue.new(HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
87
94
  else
88
- HeadMusic::Content::RhythmicValue.new(count_unit)
95
+ HeadMusic::Rudiment::RhythmicValue.new(count_unit)
89
96
  end
90
97
  end
91
98
 
99
+ # for consistency with conversational usage
100
+ alias_method :beat_unit, :beat_value
101
+
92
102
  def to_s
93
103
  [top_number, bottom_number].join("/")
94
104
  end
@@ -133,6 +143,6 @@ class HeadMusic::Rudiment::Meter
133
143
  end
134
144
 
135
145
  def beat?(tick)
136
- tick.zero?
146
+ tick.to_i.zero?
137
147
  end
138
148
  end