musa-dsl 0.30.2 → 0.41.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.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -1,8 +1,115 @@
1
1
  require_relative 'chords'
2
2
 
3
3
  module Musa
4
+ # Musical scale system framework.
5
+ #
6
+ # The Scales module provides a comprehensive framework for working with musical scales,
7
+ # supporting multiple scale systems (equal temperament, just intonation, etc.), scale
8
+ # types (major, minor, chromatic, etc.), and musical operations (transposition, interval
9
+ # calculation, frequency generation, etc.).
10
+ #
11
+ # ## Architecture
12
+ #
13
+ # The framework has a hierarchical structure:
14
+ #
15
+ # 1. **ScaleSystem**: Defines the tuning system (e.g., 12-tone equal temperament)
16
+ # 2. **ScaleSystemTuning**: A scale system with specific A frequency (e.g., A=440Hz)
17
+ # 3. **ScaleKind**: Type of scale (major, minor, chromatic, etc.)
18
+ # 4. **Scale**: A scale kind rooted on a specific pitch (e.g., C major, A minor)
19
+ # 5. **NoteInScale**: A specific note within a scale
20
+ #
21
+ # ## Basic Usage
22
+ #
23
+ # # Access the default system (12-tone equal temperament at A=440Hz)
24
+ # tuning = Scales::Scales.default_system.default_tuning
25
+ #
26
+ # # Get a C major scale (root pitch 60 = middle C)
27
+ # c_major = tuning.major[60]
28
+ #
29
+ # # Access notes by grade or function
30
+ # c_major[0] # => Tonic (C)
31
+ # c_major.tonic # => Tonic (C)
32
+ # c_major.dominant # => Dominant (G)
33
+ # c_major[:V] # => Dominant (G)
34
+ #
35
+ # ## Advanced Features
36
+ #
37
+ # - **Multiple tuning systems**: Support for different A frequencies
38
+ # - **Interval calculations**: Named intervals (M3, P5, etc.) and numeric offsets
39
+ # - **Chromatic operations**: Sharp, flat, and chromatic movements
40
+ # - **Scale navigation**: Move between related scales
41
+ # - **Frequency calculation**: Convert pitches to frequencies
42
+ # - **Chord generation**: Build chords from scale degrees
43
+ #
44
+ # @see Scales Module for registering scale systems
45
+ # @see ScaleSystem Abstract base for scale systems
46
+ # @see EquallyTempered12ToneScaleSystem The default 12-tone equal temperament system
4
47
  module Scales
48
+ # Scale system registry.
49
+ #
50
+ # The Scales module provides a central registry for scale systems, allowing access
51
+ # by symbol ID or method name.
52
+ #
53
+ # ## Registration
54
+ #
55
+ # Scale systems register themselves using {register}:
56
+ #
57
+ # Scales.register EquallyTempered12ToneScaleSystem, default: true
58
+ #
59
+ # ## Access Methods
60
+ #
61
+ # **By symbol**:
62
+ #
63
+ # Scales[:et12] # => EquallyTempered12ToneScaleSystem
64
+ # Scales[:et12][440.0] # => ScaleSystemTuning with A=440Hz
65
+ #
66
+ # **By method name**:
67
+ #
68
+ # Scales.et12 # => EquallyTempered12ToneScaleSystem
69
+ # Scales.et12[440.0] # => ScaleSystemTuning with A=440Hz
70
+ #
71
+ # **Default system**:
72
+ #
73
+ # Scales.default_system # => The default scale system
74
+ # Scales.default_system.default_tuning # => Default tuning (A=440Hz)
75
+ #
76
+ # @example Accessing scale systems
77
+ # # Get system by symbol
78
+ # system = Scales::Scales[:et12]
79
+ #
80
+ # # Get system by method
81
+ # system = Scales::Scales.et12
82
+ #
83
+ # # Get default system
84
+ # system = Scales::Scales.default_system
85
+ #
86
+ # @example Working with tunings
87
+ # # Get tuning with A=440Hz (default)
88
+ # tuning = Scales::Scales[:et12][440.0]
89
+ #
90
+ # # Get tuning with baroque pitch A=415Hz
91
+ # baroque = Scales::Scales[:et12][415.0]
92
+ #
93
+ # @example Building scales
94
+ # tuning = Scales::Scales.default_system.default_tuning
95
+ #
96
+ # # C major scale
97
+ # c_major = tuning.major[60]
98
+ #
99
+ # # A minor scale
100
+ # a_minor = tuning.minor[69]
5
101
  module Scales
102
+ # Registers a scale system.
103
+ #
104
+ # Makes the scale system available via symbol lookup and dynamic method.
105
+ # Optionally marks it as the default system.
106
+ #
107
+ # @param scale_system [Class] the ScaleSystem subclass to register
108
+ # @param default [Boolean] whether to set as default system
109
+ # @return [self]
110
+ #
111
+ # @example
112
+ # Scales.register EquallyTempered12ToneScaleSystem, default: true
6
113
  def self.register(scale_system, default: nil)
7
114
  @scale_systems ||= {}
8
115
  @scale_systems[scale_system.id] = scale_system
@@ -16,46 +123,147 @@ module Musa
16
123
  self
17
124
  end
18
125
 
126
+ # Retrieves a registered scale system by ID.
127
+ #
128
+ # @param id [Symbol] the scale system identifier
129
+ # @return [Class] the ScaleSystem subclass
130
+ # @raise [KeyError] if scale system not found
131
+ #
132
+ # @example
133
+ # Scales[:et12] # => EquallyTempered12ToneScaleSystem
19
134
  def self.[](id)
20
135
  raise KeyError, "Scale system :#{id} not found" unless @scale_systems.key?(id)
21
136
 
22
137
  @scale_systems[id]
23
138
  end
24
139
 
140
+ # Returns the default scale system.
141
+ #
142
+ # @return [Class] the default ScaleSystem subclass
143
+ #
144
+ # @example
145
+ # Scales.default_system # => EquallyTempered12ToneScaleSystem
25
146
  def self.default_system
26
147
  @default_scale_system
27
148
  end
149
+
150
+ # Convenience method to extend metadata for a scale kind by ID.
151
+ #
152
+ # Finds the ScaleKind class by its ID symbol and adds custom metadata to it.
153
+ # This is a shortcut for accessing the class directly and calling extend_metadata.
154
+ #
155
+ # @param scale_kind_id [Symbol] the scale kind identifier (e.g., :major, :dorian)
156
+ # @param metadata [Hash] key-value pairs to add as custom metadata
157
+ # @return [Hash] the updated custom_metadata hash
158
+ # @raise [KeyError] if scale kind not found
159
+ #
160
+ # @example
161
+ # Scales.extend_metadata(:major, my_tag: :favorite)
162
+ # Scales.extend_metadata(:dorian, mood: :nostalgic, suitable_for: [:jazz])
163
+ #
164
+ # @see ScaleKind.extend_metadata
165
+ def self.extend_metadata(scale_kind_id, **metadata)
166
+ system = default_system
167
+ raise KeyError, "No default scale system registered" unless system
168
+
169
+ klass = system.scale_kind_class(scale_kind_id)
170
+ raise KeyError, "Scale kind :#{scale_kind_id} not found" unless klass
171
+
172
+ klass.extend_metadata(**metadata)
173
+ end
28
174
  end
29
175
 
176
+ # Abstract base class for musical scale systems.
177
+ #
178
+ # ScaleSystem defines the foundation of a tuning system, including:
179
+ #
180
+ # - Number of notes per octave
181
+ # - Available intervals
182
+ # - Frequency calculation method
183
+ # - Registered scale kinds (major, minor, etc.)
184
+ #
185
+ # ## Subclass Requirements
186
+ #
187
+ # Subclasses must implement:
188
+ #
189
+ # - {.id}: Unique symbol identifier
190
+ # - {.notes_in_octave}: Number of notes in an octave
191
+ # - {.part_of_tone_size}: Size of smallest pitch unit (for sharps/flats)
192
+ # - {.intervals}: Hash of named intervals to semitone offsets
193
+ # - {.frequency_of_pitch}: Pitch to frequency conversion
194
+ #
195
+ # Optionally override:
196
+ #
197
+ # - {.default_a_frequency}: Reference A frequency (defaults to 440.0 Hz)
198
+ #
199
+ # ## Usage
200
+ #
201
+ # ScaleSystem is accessed via {Scales} module, not instantiated directly:
202
+ #
203
+ # system = Scales[:et12] # Get system
204
+ # tuning = system[440.0] # Get tuning
205
+ # scale = tuning.major[60] # Get scale
206
+ #
207
+ # @abstract Subclass and implement abstract methods
208
+ # @see EquallyTempered12ToneScaleSystem Concrete 12-tone implementation
209
+ # @see ScaleSystemTuning Tuning with specific A frequency
30
210
  class ScaleSystem
31
- # @abstract Subclass is expected to implement names
32
- # @!method id
33
- # @return [Symbol] the id of the ScaleSystem as a symbol
211
+ # Returns the unique identifier for this scale system.
212
+ #
213
+ # @abstract Subclass must implement
214
+ # @return [Symbol] the scale system ID (e.g., :et12)
215
+ # @raise [RuntimeError] if not implemented in subclass
34
216
  #
217
+ # @example
218
+ # EquallyTempered12ToneScaleSystem.id # => :et12
35
219
  def self.id
36
220
  raise 'Method not implemented. Should be implemented in subclass.'
37
221
  end
38
222
 
39
- # @abstract Subclass is expected to implement notes_in_octave
40
- # @!method notes_in_octave
41
- # @return [Integer] the number of notes in one octave in the ScaleSystem
223
+ # Returns the number of notes in one octave.
42
224
  #
225
+ # @abstract Subclass must implement
226
+ # @return [Integer] notes per octave (e.g., 12 for chromatic)
227
+ # @raise [RuntimeError] if not implemented in subclass
228
+ #
229
+ # @example
230
+ # EquallyTempered12ToneScaleSystem.notes_in_octave # => 12
43
231
  def self.notes_in_octave
44
232
  raise 'Method not implemented. Should be implemented in subclass.'
45
233
  end
46
234
 
47
- # @abstract Subclass is expected to implement part_of_tone_size
48
- # @!method part_of_tone_size
49
- # @return [Integer] the size inside the ScaleSystem of the smaller part of a tone; used for calculate sharp and flat notes
235
+ # Returns the size of the smallest pitch unit.
236
+ #
237
+ # Used for calculating sharp (#) and flat (♭) alterations.
238
+ # In equal temperament, this is 1 semitone.
50
239
  #
240
+ # @abstract Subclass must implement
241
+ # @return [Integer] smallest unit size
242
+ # @raise [RuntimeError] if not implemented in subclass
243
+ #
244
+ # @example
245
+ # EquallyTempered12ToneScaleSystem.part_of_tone_size # => 1
51
246
  def self.part_of_tone_size
52
247
  raise 'Method not implemented. Should be implemented in subclass.'
53
248
  end
54
249
 
55
- # @abstract Subclass is expected to implement intervals
56
- # @!method intervals
57
- # @return [Hash] the intervals of the ScaleSystem as { name: semitones#, ... }
250
+ # Returns available intervals as name-to-offset mapping.
251
+ #
252
+ # Intervals are named using standard music theory notation:
253
+ #
254
+ # - **P** (Perfect): P1, P4, P5, P8
255
+ # - **M** (Major): M2, M3, M6, M7
256
+ # - **m** (minor): m2, m3, m6, m7
257
+ # - **TT**: Tritone
58
258
  #
259
+ # @abstract Subclass must implement
260
+ # @return [Hash{Symbol => Integer}] interval names to semitone offsets
261
+ # @raise [RuntimeError] if not implemented in subclass
262
+ #
263
+ # @example
264
+ # intervals[:M3] # => 4 (major third = 4 semitones)
265
+ # intervals[:P5] # => 7 (perfect fifth = 7 semitones)
266
+ # intervals[:m7] # => 10 (minor seventh = 10 semitones)
59
267
  def self.intervals
60
268
  # TODO: implementar intérvalos sinónimos (p.ej, m3 = A2)
61
269
  # TODO: implementar identificación de intérvalos, teniendo en cuenta no sólo los semitonos sino los grados de separación
@@ -63,25 +271,54 @@ module Musa
63
271
  raise 'Method not implemented. Should be implemented in subclass.'
64
272
  end
65
273
 
66
- # @abstract Subclass is expected to implement frequency_of_pitch
67
- # @!method frequency_of_pitch
68
- # @param pitch [Number] The pitch (MIDI note numbers based) of the note to get the fundamental frequency
69
- # @param root_pitch [Number] The pitch (MIDI note numbers based) of the root note of the scale (needed for not equally tempered scales)
70
- # @param a_frequency [Number] The reference frequency of the mid A note
71
- # @return [Number] the frequency of the fundamental tone of the pitch
274
+ # Calculates frequency for a given pitch.
275
+ #
276
+ # Converts MIDI pitch numbers to frequencies in Hz. The calculation method
277
+ # depends on the tuning system (equal temperament, just intonation, etc.).
72
278
  #
279
+ # @abstract Subclass must implement
280
+ # @param pitch [Numeric] MIDI pitch number (60 = middle C, 69 = A440)
281
+ # @param root_pitch [Numeric] root pitch of scale (for non-equal temperaments)
282
+ # @param a_frequency [Numeric] reference A frequency in Hz
283
+ # @return [Float] frequency in Hz
284
+ # @raise [RuntimeError] if not implemented in subclass
285
+ #
286
+ # @example Equal temperament
287
+ # # A440 (MIDI 69)
288
+ # frequency_of_pitch(69, 60, 440.0) # => 440.0
289
+ #
290
+ # # Middle C (MIDI 60)
291
+ # frequency_of_pitch(60, 60, 440.0) # => ~261.63 Hz
73
292
  def self.frequency_of_pitch(pitch, root_pitch, a_frequency)
74
293
  raise 'Method not implemented. Should be implemented in subclass.'
75
294
  end
76
295
 
77
- # @abstract Subclass can implement default_a_frequency. If subclass doesn't implement default_a_frequency 440.0 Hz is assumed.
78
- # @!method default_a_frequency
79
- # @return [Number] the frequency A by default
296
+ # Returns the default A frequency.
80
297
  #
298
+ # @return [Float] default A frequency in Hz (440.0 standard concert pitch)
299
+ #
300
+ # @example
301
+ # ScaleSystem.default_a_frequency # => 440.0
81
302
  def self.default_a_frequency
82
303
  440.0
83
304
  end
84
305
 
306
+ # Creates or retrieves a tuning for this scale system.
307
+ #
308
+ # Returns a {ScaleSystemTuning} instance for the specified A frequency.
309
+ # Tunings are cached—repeated calls with same frequency return same instance.
310
+ #
311
+ # @param a_frequency [Numeric] reference A frequency in Hz
312
+ # @return [ScaleSystemTuning] tuning instance
313
+ #
314
+ # @example Standard pitch
315
+ # tuning = ScaleSystem[440.0]
316
+ #
317
+ # @example Baroque pitch
318
+ # baroque = ScaleSystem[415.0]
319
+ #
320
+ # @example Modern high pitch
321
+ # modern = ScaleSystem[442.0]
85
322
  def self.[](a_frequency)
86
323
  a_frequency = a_frequency.to_f
87
324
 
@@ -91,14 +328,34 @@ module Musa
91
328
  @a_tunings[a_frequency]
92
329
  end
93
330
 
331
+ # Returns semitone offset for a named interval.
332
+ #
333
+ # @param name [Symbol] interval name (e.g., :M3, :P5)
334
+ # @return [Integer] semitone offset
335
+ #
336
+ # @example
337
+ # offset_of_interval(:P5) # => 7
94
338
  def self.offset_of_interval(name)
95
339
  intervals[name]
96
340
  end
97
341
 
342
+ # Returns the default tuning (A=440Hz).
343
+ #
344
+ # @return [ScaleSystemTuning] default tuning instance
345
+ #
346
+ # @example
347
+ # tuning = ScaleSystem.default_tuning
98
348
  def self.default_tuning
99
349
  self[default_a_frequency]
100
350
  end
101
351
 
352
+ # Registers a scale kind (major, minor, etc.) with this system.
353
+ #
354
+ # @param scale_kind_class [Class] ScaleKind subclass to register
355
+ # @return [self]
356
+ #
357
+ # @example
358
+ # EquallyTempered12ToneScaleSystem.register MajorScaleKind
102
359
  def self.register(scale_kind_class)
103
360
  @scale_kind_classes ||= {}
104
361
  @scale_kind_classes[scale_kind_class.id] = scale_kind_class
@@ -108,31 +365,90 @@ module Musa
108
365
  self
109
366
  end
110
367
 
368
+ # Retrieves a registered scale kind by ID.
369
+ #
370
+ # @param id [Symbol] scale kind identifier
371
+ # @return [Class] ScaleKind subclass
372
+ # @raise [KeyError] if not found
111
373
  def self.scale_kind_class(id)
112
374
  raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
113
375
 
114
376
  @scale_kind_classes[id]
115
377
  end
116
378
 
379
+ # Checks if a scale kind is registered.
380
+ #
381
+ # @param id [Symbol] scale kind identifier
382
+ # @return [Boolean]
117
383
  def self.scale_kind_class?(id)
118
384
  @scale_kind_classes.key? id
119
385
  end
120
386
 
387
+ # Returns all registered scale kinds.
388
+ #
389
+ # @return [Hash{Symbol => Class}] scale kind classes
121
390
  def self.scale_kind_classes
122
391
  @scale_kind_classes
123
392
  end
124
393
 
394
+ # Returns the chromatic scale kind class.
395
+ #
396
+ # @return [Class] chromatic ScaleKind subclass
397
+ # @raise [RuntimeError] if chromatic scale not defined
125
398
  def self.chromatic_class
126
399
  raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
127
400
 
128
401
  @chromatic_scale_kind_class
129
402
  end
130
403
 
404
+ # Compares scale systems for equality.
405
+ #
406
+ # @param other [ScaleSystem]
407
+ # @return [Boolean]
131
408
  def ==(other)
132
409
  self.class == other.class
133
410
  end
134
411
  end
135
412
 
413
+ # Scale system with specific A frequency tuning.
414
+ #
415
+ # ScaleSystemTuning combines a {ScaleSystem} with a specific reference A frequency,
416
+ # providing access to scale kinds (major, minor, chromatic, etc.) tuned to that
417
+ # frequency.
418
+ #
419
+ # ## Usage
420
+ #
421
+ # Tunings are created via {ScaleSystem.[]}:
422
+ #
423
+ # tuning = Scales[:et12][440.0] # Standard pitch
424
+ # baroque = Scales[:et12][415.0] # Baroque pitch
425
+ #
426
+ # ## Accessing Scales
427
+ #
428
+ # **By symbol**:
429
+ #
430
+ # tuning[:major][60] # C major scale
431
+ #
432
+ # **By method name**:
433
+ #
434
+ # tuning.major[60] # C major scale
435
+ # tuning.minor[69] # A minor scale
436
+ #
437
+ # **Chromatic scale**:
438
+ #
439
+ # tuning.chromatic[60] # C chromatic scale
440
+ #
441
+ # @example Standard usage
442
+ # tuning = Scales::Scales.default_system.default_tuning
443
+ # c_major = tuning.major[60]
444
+ # a_minor = tuning.minor[69]
445
+ #
446
+ # @example Historical pitch
447
+ # baroque = Scales[:et12][415.0]
448
+ # scale = baroque.major[60] # C major at A=415Hz
449
+ #
450
+ # @see ScaleSystem Parent scale system
451
+ # @see ScaleKind Scale types (major, minor, etc.)
136
452
  class ScaleSystemTuning
137
453
  extend Forwardable
138
454
 
@@ -168,6 +484,113 @@ module Musa
168
484
  @scale_system.frequency_of_pitch(pitch, root, @a_frequency)
169
485
  end
170
486
 
487
+ # Returns scale kinds matching the given metadata criteria.
488
+ #
489
+ # Without arguments, returns all registered scale kinds.
490
+ # With keyword arguments, filters by metadata values.
491
+ # With a block, filters using custom predicate on ScaleKind class.
492
+ #
493
+ # @param metadata_criteria [Hash] metadata key-value pairs to match
494
+ # @yield [kind_class] optional block for custom filtering
495
+ # @yieldparam kind_class [Class] the ScaleKind subclass
496
+ # @yieldreturn [Boolean] true to include this scale kind
497
+ # @return [Array<ScaleKind>] matching scale kind instances
498
+ #
499
+ # @example All scale kinds
500
+ # tuning.scale_kinds
501
+ # # => [major_kind, minor_kind, dorian_kind, ...]
502
+ #
503
+ # @example Filter by metadata
504
+ # tuning.scale_kinds(family: :diatonic)
505
+ # tuning.scale_kinds(brightness: -1..1)
506
+ # tuning.scale_kinds(character: :jazz)
507
+ #
508
+ # @example Filter with block
509
+ # tuning.scale_kinds { |klass| klass.intrinsic_metadata[:has_leading_tone] }
510
+ #
511
+ # @example Combined
512
+ # tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness] < 0 }
513
+ #
514
+ # @see ScaleKind.metadata
515
+ # @see ScaleKind.has_metadata?
516
+ def scale_kinds(**metadata_criteria, &block)
517
+ result = @scale_system.scale_kind_classes.keys.map { |id| self[id] }
518
+
519
+ unless metadata_criteria.empty?
520
+ result = result.select do |kind|
521
+ matches_metadata?(kind.class, metadata_criteria)
522
+ end
523
+ end
524
+
525
+ if block
526
+ result = result.select { |kind| block.call(kind.class) }
527
+ end
528
+
529
+ result
530
+ end
531
+
532
+ # Searches for a chord across multiple scale types.
533
+ #
534
+ # Iterates through the specified scale kinds and pitch roots to find
535
+ # all scales that contain the given chord. Returns chords with their
536
+ # containing scale as context.
537
+ #
538
+ # @param chord [Musa::Chords::Chord] the chord to search for
539
+ # @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
540
+ # @param metadata_criteria [Hash] metadata filters for scale kinds
541
+ # @return [Array<Musa::Chords::Chord>] chords with their containing scales
542
+ #
543
+ # @example Search G7 in greek mode scales
544
+ # tuning = Scales.et12[440.0]
545
+ # g7 = tuning.major[60].dominant.chord :seventh
546
+ # tuning.chords_of(g7, family: :greek_modes)
547
+ #
548
+ # @example Search with brightness filter
549
+ # tuning.chords_of(g7, brightness: -1..1)
550
+ #
551
+ # @example Search in all scale types
552
+ # tuning.chords_of(g7)
553
+ #
554
+ # @see ScaleKind#scales_containing
555
+ # @see Musa::Chords::Chord#in_scales
556
+ def chords_of(chord, roots: nil, **metadata_criteria)
557
+ roots ||= 0...notes_in_octave
558
+ kinds = filtered_scale_kind_ids(**metadata_criteria)
559
+
560
+ kinds.flat_map do |kind_id|
561
+ self[kind_id].scales_containing(chord, roots: roots)
562
+ end
563
+ end
564
+
565
+ private
566
+
567
+ def filtered_scale_kind_ids(**metadata_criteria)
568
+ kinds = @scale_system.scale_kind_classes.keys
569
+
570
+ return kinds if metadata_criteria.empty?
571
+
572
+ kinds.select do |kind_id|
573
+ kind_class = @scale_system.scale_kind_class(kind_id)
574
+ matches_metadata?(kind_class, metadata_criteria)
575
+ end
576
+ end
577
+
578
+ def matches_metadata?(kind_class, criteria)
579
+ criteria.all? do |key, value|
580
+ actual = kind_class.metadata[key]
581
+ case value
582
+ when Range
583
+ actual.is_a?(Numeric) && value.include?(actual)
584
+ when Array
585
+ value.any? { |v| actual == v || (actual.is_a?(Array) && actual.include?(v)) }
586
+ else
587
+ actual == value || (actual.is_a?(Array) && actual.include?(value))
588
+ end
589
+ end
590
+ end
591
+
592
+ public
593
+
171
594
  def ==(other)
172
595
  self.class == other.class &&
173
596
  @scale_system == other.scale_system &&
@@ -181,77 +604,447 @@ module Musa
181
604
  alias to_s inspect
182
605
  end
183
606
 
607
+ # Abstract base class for scale types (major, minor, chromatic, etc.).
608
+ #
609
+ # ScaleKind defines a type of scale (major, minor, chromatic, etc.) independent
610
+ # of root pitch or tuning. It specifies:
611
+ #
612
+ # - Scale degrees and their pitch offsets
613
+ # - Function names for each degree (tonic, dominant, etc.)
614
+ # - Number of grades per octave
615
+ # - Whether the scale is chromatic (contains all pitches)
616
+ #
617
+ # ## Subclass Requirements
618
+ #
619
+ # Subclasses must implement:
620
+ #
621
+ # - {.id}: Unique symbol identifier (:major, :minor, :chromatic, etc.)
622
+ # - {.pitches}: Array defining scale structure
623
+ # - {.chromatic?}: Whether this is the chromatic scale (default: false)
624
+ # - {.grades}: Number of grades per octave (if different from pitches.length)
625
+ #
626
+ # ## Pitch Structure
627
+ #
628
+ # The {.pitches} array defines the scale structure:
629
+ #
630
+ # [{ functions: [:I, :tonic, :_1], pitch: 0 },
631
+ # { functions: [:II, :supertonic, :_2], pitch: 2 },
632
+ # ...]
633
+ #
634
+ # - **functions**: Array of symbols that can access this degree
635
+ # - **pitch**: Semitone offset from root
636
+ #
637
+ # ## Dynamic Method Creation
638
+ #
639
+ # Each scale instance gets methods for all registered scale kinds:
640
+ #
641
+ # note.major # Get major scale rooted on this note
642
+ # note.minor # Get minor scale rooted on this note
643
+ #
644
+ # ## Usage
645
+ #
646
+ # ScaleKind instances are accessed via tuning:
647
+ #
648
+ # tuning = Scales[:et12][440.0]
649
+ # major_kind = tuning[:major] # ScaleKind instance
650
+ # c_major = major_kind[60] # Scale instance
651
+ #
652
+ # Or directly via convenience methods:
653
+ #
654
+ # c_major = tuning.major[60]
655
+ #
656
+ # @abstract Subclass and implement abstract methods
657
+ # @see MajorScaleKind Concrete major scale implementation
658
+ # @see MinorNaturalScaleKind Concrete minor scale implementation
659
+ # @see ChromaticScaleKind Concrete chromatic scale implementation
660
+ # @see Scale Instantiated scale with root pitch
184
661
  class ScaleKind
662
+ # Creates a scale kind instance.
663
+ #
664
+ # @param tuning [ScaleSystemTuning] the tuning context
665
+ #
666
+ # @api private
185
667
  def initialize(tuning)
186
668
  @tuning = tuning
187
669
  @scales = {}
188
670
  end
189
671
 
672
+ # The tuning context.
673
+ # @return [ScaleSystemTuning]
190
674
  attr_reader :tuning
191
675
 
676
+ # Creates or retrieves a scale rooted on specific pitch.
677
+ #
678
+ # Scales are cached—repeated calls with same pitch return same instance.
679
+ #
680
+ # @param root_pitch [Integer] MIDI root pitch (60 = middle C)
681
+ # @return [Scale] scale instance
682
+ #
683
+ # @example
684
+ # major_kind = tuning[:major]
685
+ # c_major = major_kind[60] # C major
686
+ # g_major = major_kind[67] # G major
192
687
  def [](root_pitch)
193
688
  @scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
194
689
  @scales[root_pitch]
195
690
  end
196
691
 
692
+ # Returns scale with default root (middle C, MIDI 60).
693
+ #
694
+ # @return [Scale] scale rooted on middle C
695
+ #
696
+ # @example
697
+ # tuning.major.default_root # C major
197
698
  def default_root
198
699
  self[60]
199
700
  end
200
701
 
702
+ # Returns scale with absolute root (MIDI 0).
703
+ #
704
+ # @return [Scale] scale rooted on MIDI 0
705
+ #
706
+ # @example
707
+ # tuning.major.absolut # Scale rooted at MIDI 0
201
708
  def absolut
202
709
  self[0]
203
710
  end
204
711
 
712
+ # Finds all scales of this kind that contain the given chord.
713
+ #
714
+ # Searches through scales rooted on different pitches to find which ones
715
+ # contain all the notes of the given chord. Returns chords with their
716
+ # containing scale as context.
717
+ #
718
+ # @param chord [Musa::Chords::Chord] the chord to search for
719
+ # @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
720
+ # @return [Array<Musa::Chords::Chord>] chords with their containing scales
721
+ #
722
+ # @example Find G major triad in all major scales
723
+ # tuning = Scales.et12[440.0]
724
+ # g_triad = tuning.major[60].dominant.chord
725
+ # tuning.major.scales_containing(g_triad)
726
+ # # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]
727
+ #
728
+ # @see Scale#chord_on
729
+ # @see ScaleSystemTuning#chords_in_scales
730
+ def scales_containing(chord, roots: nil)
731
+ roots ||= 0...tuning.notes_in_octave
732
+ base_pitch = chord.root.pitch % tuning.notes_in_octave
733
+
734
+ roots.filter_map do |root_offset|
735
+ root_pitch = base_pitch + root_offset
736
+ scale = self[root_pitch]
737
+ scale.chord_on(chord)
738
+ end
739
+ end
740
+
741
+ # Checks scale kind equality.
742
+ #
743
+ # @param other [ScaleKind]
744
+ # @return [Boolean]
205
745
  def ==(other)
206
746
  self.class == other.class && @tuning == other.tuning
207
747
  end
208
748
 
749
+ # Returns string representation.
750
+ #
751
+ # @return [String]
209
752
  def inspect
210
753
  "<#{self.class.name}: tuning = #{@tuning}>"
211
754
  end
212
755
 
213
756
  alias to_s inspect
214
757
 
215
- # @abstract Subclass is expected to implement id
216
- # @!method id
217
- # @return [Symbol] the id of the ScaleKind as a symbol
758
+ # Returns the unique identifier for this scale kind.
759
+ #
760
+ # @abstract Subclass must implement
761
+ # @return [Symbol] scale kind ID (e.g., :major, :minor, :chromatic)
762
+ # @raise [RuntimeError] if not implemented in subclass
763
+ #
764
+ # @example
765
+ # MajorScaleKind.id # => :major
218
766
  def self.id
219
767
  raise 'Method not implemented. Should be implemented in subclass.'
220
768
  end
221
769
 
222
- # @abstract Subclass is expected to implement pitches
223
- # @!method pitches
224
- # @return [Array] the pitches array of the ScaleKind as [ { functions: [ <symbol>, ...], pitch: <Number> }, ... ]
770
+ # Returns the pitch structure definition.
771
+ #
772
+ # Defines the scale degrees and their pitch offsets from the root.
773
+ # Each entry specifies function names and semitone offset.
774
+ #
775
+ # @abstract Subclass must implement
776
+ # @return [Array<Hash>] array of pitch definitions with:
777
+ # - **:functions** [Array<Symbol>]: function names for this degree
778
+ # - **:pitch** [Integer]: semitone offset from root
779
+ # @raise [RuntimeError] if not implemented in subclass
780
+ #
781
+ # @example Major scale structure (partial)
782
+ # [{ functions: [:I, :tonic, :_1], pitch: 0 },
783
+ # { functions: [:II, :supertonic, :_2], pitch: 2 },
784
+ # { functions: [:III, :mediant, :_3], pitch: 4 },
785
+ # ...]
225
786
  def self.pitches
226
787
  raise 'Method not implemented. Should be implemented in subclass.'
227
788
  end
228
789
 
229
- # @abstract Subclass is expected to implement chromatic?. Only one of the subclasses should return true.
230
- # @!method chromatic?
231
- # @return [Boolean] wether the scales is a full scale (with all the notes in the ScaleSystem), sorted and to be considered canonical. I.e. a chromatic 12 semitones uprising serie in a 12 tone tempered ScaleSystem.
790
+ # Indicates whether this is the chromatic scale.
791
+ #
792
+ # Only one scale kind per system should return true. The chromatic scale
793
+ # contains all notes in the scale system and is used as a fallback for
794
+ # non-diatonic notes.
795
+ #
796
+ # @return [Boolean] true if chromatic scale (default: false)
797
+ #
798
+ # @example
799
+ # ChromaticScaleKind.chromatic? # => true
800
+ # MajorScaleKind.chromatic? # => false
232
801
  def self.chromatic?
233
802
  false
234
803
  end
235
804
 
236
- # @abstract Subclass is expected to implement grades when the ScaleKind is defining more pitches than notes by octave has the scale. This can happen when there are pitches defined on upper octaves (i.e., to define XII grade, as a octave + fifth)
237
- # @!method grades
238
- # @return [Integer] Number of grades inside of a octave of the scale
805
+ # Returns the number of grades per octave.
806
+ #
807
+ # For scales defining extended harmony (8th, 9th, etc.), this returns
808
+ # the number of diatonic degrees within one octave. Defaults to the
809
+ # number of pitch definitions.
810
+ #
811
+ # @return [Integer] number of grades per octave
812
+ #
813
+ # @example
814
+ # MajorScaleKind.grades # => 7 (not 13, even with extended degrees)
239
815
  def self.grades
240
816
  pitches.length
241
817
  end
242
818
 
819
+ # Returns grade index for a function symbol.
820
+ #
821
+ # @param symbol [Symbol] function name (e.g., :tonic, :dominant, :V)
822
+ # @return [Integer, nil] grade index or nil if not found
823
+ #
824
+ # @example
825
+ # MajorScaleKind.grade_of_function(:tonic) # => 0
826
+ # MajorScaleKind.grade_of_function(:dominant) # => 4
827
+ # MajorScaleKind.grade_of_function(:V) # => 4
828
+ #
829
+ # @api private
243
830
  def self.grade_of_function(symbol)
244
831
  create_grade_functions_index unless @grade_names_index
245
832
  @grade_names_index[symbol]
246
833
  end
247
834
 
835
+ # Returns all function symbols for accessing scale degrees.
836
+ #
837
+ # @return [Array<Symbol>] all function names
838
+ #
839
+ # @example
840
+ # MajorScaleKind.grades_functions
841
+ # # => [:I, :_1, :tonic, :first, :II, :_2, :supertonic, :second, ...]
842
+ #
843
+ # @api private
248
844
  def self.grades_functions
249
845
  create_grade_functions_index unless @grade_names_index
250
846
  @grade_names_index.keys
251
847
  end
252
848
 
849
+ # Returns intrinsic metadata derived from scale structure.
850
+ #
851
+ # This metadata is automatically calculated from the scale's pitch
852
+ # structure and cannot be modified. It includes:
853
+ #
854
+ # - **:id**: Scale kind identifier
855
+ # - **:grades**: Number of diatonic degrees
856
+ # - **:pitches**: Array of pitch offsets from root
857
+ # - **:intervals**: Intervals between consecutive degrees
858
+ # - **:has_leading_tone**: Whether scale has pitch 11 (semitone below octave)
859
+ # - **:has_tritone**: Whether scale contains tritone (pitch 6)
860
+ # - **:symmetric**: Type of symmetry if any (:equal, :palindrome, :repeating)
861
+ #
862
+ # @return [Hash] intrinsic metadata derived from structure
863
+ #
864
+ # @example
865
+ # MajorScaleKind.intrinsic_metadata
866
+ # # => { id: :major, grades: 7, pitches: [0, 2, 4, 5, 7, 9, 11],
867
+ # # intervals: [2, 2, 1, 2, 2, 2], has_leading_tone: true,
868
+ # # has_tritone: true, symmetric: nil }
869
+ def self.intrinsic_metadata
870
+ result = {}
871
+ result[:id] = id if respond_to?(:id)
872
+ result[:grades] = grades if respond_to?(:grades)
873
+ if respond_to?(:pitches)
874
+ result[:pitches] = pitches.map { |p| p[:pitch] }
875
+ result[:intervals] = compute_intervals
876
+ result[:has_leading_tone] = pitches.any? { |p| p[:pitch] == 11 }
877
+ result[:has_tritone] = pitches.any? { |p| p[:pitch] == 6 }
878
+ result[:symmetric] = compute_symmetry
879
+ end
880
+ result.compact
881
+ end
882
+
883
+ # Returns base metadata defined by the musa-dsl library.
884
+ #
885
+ # This metadata is defined in each ScaleKind subclass using the
886
+ # `@base_metadata` class instance variable. It typically includes:
887
+ #
888
+ # - **:family**: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
889
+ # - **:brightness**: Relative brightness (-3 to +3, major = 0)
890
+ # - **:character**: Array of descriptive tags
891
+ # - **:parent**: Parent scale and degree for modes
892
+ #
893
+ # @return [Hash] library-defined metadata
894
+ #
895
+ # @example
896
+ # MajorScaleKind.base_metadata
897
+ # # => { family: :diatonic, brightness: 0, character: [:bright, :stable] }
898
+ def self.base_metadata
899
+ @base_metadata || {}
900
+ end
901
+
902
+ # Returns custom metadata added by users at runtime.
903
+ #
904
+ # This metadata is added via {.extend_metadata} and can be cleared
905
+ # with {.reset_custom_metadata}. Takes precedence over base_metadata.
906
+ #
907
+ # @return [Hash] user-defined metadata
908
+ #
909
+ # @example
910
+ # MajorScaleKind.extend_metadata(my_tag: :favorite)
911
+ # MajorScaleKind.custom_metadata # => { my_tag: :favorite }
912
+ def self.custom_metadata
913
+ @custom_metadata || {}
914
+ end
915
+
916
+ # Adds custom metadata to this scale kind.
917
+ #
918
+ # Custom metadata takes precedence over base_metadata when queried
919
+ # via {.metadata}. Multiple calls merge metadata together.
920
+ #
921
+ # @param metadata [Hash] key-value pairs to add
922
+ # @return [Hash] the updated custom_metadata hash (frozen)
923
+ #
924
+ # @example
925
+ # MajorScaleKind.extend_metadata(my_mood: :happy, rating: 5)
926
+ # MajorScaleKind.extend_metadata(suitable_for: [:pop, :classical])
927
+ # MajorScaleKind.custom_metadata
928
+ # # => { my_mood: :happy, rating: 5, suitable_for: [:pop, :classical] }
929
+ def self.extend_metadata(**metadata)
930
+ @custom_metadata ||= {}
931
+ @custom_metadata = @custom_metadata.merge(metadata).freeze
932
+ end
933
+
934
+ # Clears all custom metadata from this scale kind.
935
+ #
936
+ # @return [nil]
937
+ #
938
+ # @example
939
+ # MajorScaleKind.extend_metadata(temp: :data)
940
+ # MajorScaleKind.reset_custom_metadata
941
+ # MajorScaleKind.custom_metadata # => {}
942
+ def self.reset_custom_metadata
943
+ @custom_metadata = nil
944
+ end
945
+
946
+ # Returns combined metadata from all three layers.
947
+ #
948
+ # Layers are merged with later layers taking precedence:
949
+ # intrinsic_metadata < base_metadata < custom_metadata
950
+ #
951
+ # @return [Hash] combined metadata from all layers
952
+ #
953
+ # @example
954
+ # MajorScaleKind.metadata
955
+ # # => { id: :major, grades: 7, pitches: [...], family: :diatonic, ... }
956
+ def self.metadata
957
+ intrinsic_metadata
958
+ .merge(base_metadata)
959
+ .merge(custom_metadata)
960
+ end
961
+
962
+ # Returns a specific metadata value.
963
+ #
964
+ # @param key [Symbol] the metadata key
965
+ # @return [Object, nil] the value or nil if not found
966
+ #
967
+ # @example
968
+ # MajorScaleKind.metadata_value(:family) # => :diatonic
969
+ def self.metadata_value(key)
970
+ metadata[key]
971
+ end
972
+
973
+ # Checks whether metadata contains a key or key-value match.
974
+ #
975
+ # When called with just a key, checks for key existence.
976
+ # When called with key and value, checks for exact match or
977
+ # array inclusion (if metadata value is an array).
978
+ #
979
+ # @param key [Symbol] the metadata key
980
+ # @param value [Object, nil] optional value to match
981
+ # @return [Boolean] whether the condition is satisfied
982
+ #
983
+ # @example Key existence
984
+ # MajorScaleKind.has_metadata?(:family) # => true
985
+ # MajorScaleKind.has_metadata?(:nonexistent) # => false
986
+ #
987
+ # @example Value matching
988
+ # MajorScaleKind.has_metadata?(:family, :diatonic) # => true
989
+ # MajorScaleKind.has_metadata?(:family, :pentatonic) # => false
990
+ #
991
+ # @example Array inclusion
992
+ # MajorScaleKind.has_metadata?(:character, :bright) # => true
993
+ def self.has_metadata?(key, value = nil)
994
+ if value.nil?
995
+ metadata.key?(key)
996
+ else
997
+ metadata[key] == value ||
998
+ (metadata[key].is_a?(Array) && metadata[key].include?(value))
999
+ end
1000
+ end
1001
+
253
1002
  private
254
1003
 
1004
+ # Computes intervals between consecutive scale degrees.
1005
+ # @return [Array<Integer>, nil] intervals or nil if not calculable
1006
+ # @api private
1007
+ def self.compute_intervals
1008
+ return nil unless respond_to?(:pitches) && pitches.size > 1
1009
+ pitch_values = pitches.map { |p| p[:pitch] }
1010
+ # Only compute within first octave
1011
+ first_octave = pitch_values.take_while { |p| p < 12 }
1012
+ first_octave.push(12) if first_octave.last != 12
1013
+ first_octave.each_cons(2).map { |a, b| b - a }
1014
+ end
1015
+
1016
+ # Computes symmetry type of the scale.
1017
+ # @return [Symbol, nil] :equal, :palindrome, :repeating, or nil
1018
+ # @api private
1019
+ def self.compute_symmetry
1020
+ return nil unless respond_to?(:pitches)
1021
+ intervals = compute_intervals
1022
+ return nil unless intervals && intervals.any?
1023
+
1024
+ # Check if intervals are all equal (e.g., whole tone: [2,2,2,2,2,2])
1025
+ return :equal if intervals.uniq.size == 1
1026
+
1027
+ # Check for palindrome pattern
1028
+ return :palindrome if intervals == intervals.reverse
1029
+
1030
+ # Check for repeating pattern
1031
+ (1..intervals.size / 2).each do |len|
1032
+ pattern = intervals.take(len)
1033
+ if intervals.each_slice(len).all? { |slice| slice == pattern || slice.size < len }
1034
+ return :repeating
1035
+ end
1036
+ end
1037
+
1038
+ nil
1039
+ end
1040
+
1041
+ public
1042
+
1043
+ # Creates internal index mapping function names to grade indices.
1044
+ #
1045
+ # @return [self]
1046
+ #
1047
+ # @api private
255
1048
  def self.create_grade_functions_index
256
1049
  @grade_names_index = {}
257
1050
  pitches.each_index do |i|
@@ -264,9 +1057,88 @@ module Musa
264
1057
  end
265
1058
  end
266
1059
 
1060
+ # Instantiated scale with specific root pitch.
1061
+ #
1062
+ # Scale represents a concrete scale (major, minor, etc.) rooted on a specific
1063
+ # pitch. It provides access to scale degrees, interval calculations, frequency
1064
+ # generation, and chord construction.
1065
+ #
1066
+ # ## Creation
1067
+ #
1068
+ # Scales are created via {ScaleKind}:
1069
+ #
1070
+ # tuning = Scales[:et12][440.0]
1071
+ # c_major = tuning.major[60] # Via convenience method
1072
+ # a_minor = tuning[:minor][69] # Via bracket notation
1073
+ #
1074
+ # ## Accessing Notes
1075
+ #
1076
+ # **By numeric grade** (0-based):
1077
+ #
1078
+ # scale[0] # First degree (tonic)
1079
+ # scale[1] # Second degree
1080
+ # scale[4] # Fifth degree
1081
+ #
1082
+ # **By function name** (dynamic methods):
1083
+ #
1084
+ # scale.tonic # First degree
1085
+ # scale.dominant # Fifth degree
1086
+ # scale.mediant # Third degree
1087
+ #
1088
+ # **By Roman numeral**:
1089
+ #
1090
+ # scale[:I] # First degree
1091
+ # scale[:V] # Fifth degree
1092
+ # scale[:IV] # Fourth degree
1093
+ #
1094
+ # **With accidentals** (sharp # or flat _):
1095
+ #
1096
+ # scale[:I#] # Raised tonic
1097
+ # scale[:V_] # Flatted dominant
1098
+ # scale['II##'] # Double-raised second
1099
+ #
1100
+ # ## Note Operations
1101
+ #
1102
+ # Each note is a {NoteInScale} instance with full capabilities:
1103
+ #
1104
+ # note = scale.tonic
1105
+ # note.pitch # MIDI pitch number
1106
+ # note.frequency # Frequency in Hz
1107
+ # note.chord # Build chord from note
1108
+ # note.up(:P5) # Navigate by interval
1109
+ # note.sharp # Raise by semitone
1110
+ #
1111
+ # ## Special Methods
1112
+ #
1113
+ # - **chromatic**: Access chromatic scale at same root
1114
+ # - **octave**: Transpose scale to different octave
1115
+ # - **note_of_pitch**: Find note for specific MIDI pitch
1116
+ #
1117
+ # @example Basic scale access
1118
+ # c_major = tuning.major[60]
1119
+ # c_major.tonic.pitch # => 60 (C)
1120
+ # c_major.dominant.pitch # => 67 (G)
1121
+ # c_major[:III].pitch # => 64 (E)
1122
+ #
1123
+ # @example Chromatic alterations
1124
+ # c_major[:I#].pitch # => 61 (C#)
1125
+ # c_major[:V_].pitch # => 66 (F#/Gb)
1126
+ #
1127
+ # @example Building chords
1128
+ # c_major.tonic.chord # C major triad
1129
+ # c_major.dominant.chord :seventh # G dominant 7th
1130
+ #
1131
+ # @see ScaleKind Scale type definition
1132
+ # @see NoteInScale Individual note in scale
267
1133
  class Scale
268
1134
  extend Forwardable
269
1135
 
1136
+ # Creates a scale instance.
1137
+ #
1138
+ # @param kind [ScaleKind] the scale kind
1139
+ # @param root_pitch [Integer] MIDI root pitch
1140
+ #
1141
+ # @api private
270
1142
  def initialize(kind, root_pitch:)
271
1143
  @notes_by_grade = {}
272
1144
  @notes_by_pitch = {}
@@ -284,28 +1156,95 @@ module Musa
284
1156
  freeze
285
1157
  end
286
1158
 
1159
+ # Delegates tuning access to kind.
287
1160
  def_delegators :@kind, :tuning
288
1161
 
289
- attr_reader :kind, :root_pitch
1162
+ # Scale kind (major, minor, etc.).
1163
+ # @return [ScaleKind]
1164
+ attr_reader :kind
1165
+
1166
+ # Root pitch (MIDI number).
1167
+ # @return [Integer]
1168
+ attr_reader :root_pitch
290
1169
 
1170
+ # Returns the root note (first degree).
1171
+ #
1172
+ # Equivalent to scale[0] or scale.tonic.
1173
+ #
1174
+ # @return [NoteInScale] root note
1175
+ #
1176
+ # @example
1177
+ # c_major.root.pitch # => 60
291
1178
  def root
292
1179
  self[0]
293
1180
  end
294
1181
 
1182
+ # Returns the chromatic scale at the same root.
1183
+ #
1184
+ # @return [Scale] chromatic scale rooted at same pitch
1185
+ #
1186
+ # @example
1187
+ # c_major.chromatic # Chromatic scale starting at C
295
1188
  def chromatic
296
1189
  @kind.tuning.chromatic[@root_pitch]
297
1190
  end
298
1191
 
1192
+ # Returns the scale rooted at absolute pitch 0.
1193
+ #
1194
+ # @return [Scale] scale of same kind at MIDI 0
1195
+ #
1196
+ # @example
1197
+ # c_major.absolut # Major scale at MIDI 0
299
1198
  def absolut
300
1199
  @kind[0]
301
1200
  end
302
1201
 
1202
+ # Transposes scale by octaves.
1203
+ #
1204
+ # @param octave [Integer] octave offset (positive = up, negative = down)
1205
+ # @return [Scale] transposed scale
1206
+ # @raise [ArgumentError] if octave is not integer
1207
+ #
1208
+ # @example
1209
+ # c_major.octave(1) # C major one octave higher
1210
+ # c_major.octave(-1) # C major one octave lower
303
1211
  def octave(octave)
304
1212
  raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
305
1213
 
306
1214
  @kind[@root_pitch + octave * @kind.class.grades]
307
1215
  end
308
1216
 
1217
+ # Accesses scale degree by grade, symbol, or function name.
1218
+ #
1219
+ # Supports multiple access patterns:
1220
+ # - **Integer**: Numeric grade (0-based)
1221
+ # - **Symbol/String**: Function name or Roman numeral
1222
+ # - **With accidentals**: Add '#' for sharp, '_' for flat
1223
+ #
1224
+ # Notes are cached—repeated access returns same instance.
1225
+ #
1226
+ # @param grade_or_symbol [Integer, Symbol, String] degree specifier
1227
+ # @return [NoteInScale] note at specified degree
1228
+ # @raise [ArgumentError] if grade_or_symbol is invalid type
1229
+ #
1230
+ # @example Numeric access
1231
+ # scale[0] # Tonic
1232
+ # scale[4] # Dominant (in major/minor)
1233
+ #
1234
+ # @example Function name access
1235
+ # scale[:tonic]
1236
+ # scale[:dominant]
1237
+ # scale[:mediant]
1238
+ #
1239
+ # @example Roman numeral access
1240
+ # scale[:I] # Tonic
1241
+ # scale[:V] # Dominant
1242
+ # scale[:IV] # Subdominant
1243
+ #
1244
+ # @example With accidentals
1245
+ # scale[:I#] # Raised tonic
1246
+ # scale[:V_] # Flatted dominant
1247
+ # scale['II##'] # Double-raised second
309
1248
  def [](grade_or_symbol)
310
1249
 
311
1250
  raise ArgumentError, "grade_or_symbol '#{grade_or_symbol}' should be a Integer, String or Symbol" unless grade_or_symbol.is_a?(Symbol) || grade_or_symbol.is_a?(String) || grade_or_symbol.is_a?(Integer)
@@ -330,6 +1269,12 @@ module Musa
330
1269
  @notes_by_grade[wide_grade].sharp(sharps)
331
1270
  end
332
1271
 
1272
+ # Converts grade specifier to numeric grade and accidentals.
1273
+ #
1274
+ # @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
1275
+ # @return [Array(Integer, Integer)] wide grade and accidentals count
1276
+ #
1277
+ # @api private
333
1278
  def grade_of(grade_or_string_or_symbol)
334
1279
  name, wide_grade, accidentals = parse_grade(grade_or_string_or_symbol)
335
1280
 
@@ -343,6 +1288,15 @@ module Musa
343
1288
  return octave * @kind.class.grades + grade, accidentals
344
1289
  end
345
1290
 
1291
+ # Parses grade string/symbol into components.
1292
+ #
1293
+ # Handles formats like "I#", ":V_", "7##", extracting function name,
1294
+ # numeric grade, and accidentals.
1295
+ #
1296
+ # @param neuma_grade [Integer, Symbol, String] grade to parse
1297
+ # @return [Array(Symbol, Integer, Integer)] name, wide_grade, accidentals
1298
+ #
1299
+ # @api private
346
1300
  def parse_grade(neuma_grade)
347
1301
  name = wide_grade = nil
348
1302
  accidentals = 0
@@ -371,6 +1325,24 @@ module Musa
371
1325
  return name, wide_grade, accidentals
372
1326
  end
373
1327
 
1328
+ # Finds note for a specific MIDI pitch.
1329
+ #
1330
+ # Searches for a note in the scale matching the given pitch. Options control
1331
+ # behavior when pitch is not in scale.
1332
+ #
1333
+ # @param pitch [Integer] MIDI pitch number
1334
+ # @param allow_chromatic [Boolean] if true, return chromatic note when not in scale
1335
+ # @param allow_nearest [Boolean] if true, return nearest scale note
1336
+ # @return [NoteInScale, nil] matching note or nil
1337
+ #
1338
+ # @example Diatonic note
1339
+ # c_major.note_of_pitch(64) # => E (in scale)
1340
+ #
1341
+ # @example Chromatic note
1342
+ # c_major.note_of_pitch(63, allow_chromatic: true) # => Eb (chromatic)
1343
+ #
1344
+ # @example Nearest note
1345
+ # c_major.note_of_pitch(63, allow_nearest: true) # => E or D (nearest)
374
1346
  def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
375
1347
  allow_chromatic ||= false
376
1348
  allow_nearest ||= false
@@ -407,16 +1379,107 @@ module Musa
407
1379
  note
408
1380
  end
409
1381
 
1382
+ # Returns semitone offset for a named interval.
1383
+ #
1384
+ # @param interval_name [Symbol] interval name (e.g., :M3, :P5)
1385
+ # @return [Integer] semitone offset
1386
+ #
1387
+ # @example
1388
+ # scale.offset_of_interval(:P5) # => 7
1389
+ # scale.offset_of_interval(:M3) # => 4
410
1390
  def offset_of_interval(interval_name)
411
1391
  @kind.tuning.offset_of_interval(interval_name)
412
1392
  end
413
1393
 
1394
+ # Checks if all chord pitches exist in this scale.
1395
+ #
1396
+ # Uses the chord's definition to verify that every pitch in the chord
1397
+ # can be found as a diatonic note in this scale.
1398
+ #
1399
+ # @param chord [Musa::Chords::Chord] the chord to check
1400
+ # @return [Boolean] true if all chord notes are in scale
1401
+ #
1402
+ # @example
1403
+ # c_major = Scales.et12[440.0].major[60]
1404
+ # g7 = c_major.dominant.chord :seventh
1405
+ # c_major.contains_chord?(g7) # => true
1406
+ #
1407
+ # cm = c_major.tonic.chord.with_quality(:minor)
1408
+ # c_major.contains_chord?(cm) # => false (Eb not in C major)
1409
+ #
1410
+ # @see #degree_of_chord
1411
+ # @see #chord_on
1412
+ def contains_chord?(chord)
1413
+ chord.chord_definition.in_scale?(self, chord_root_pitch: chord.root.pitch)
1414
+ end
1415
+
1416
+ # Returns the grade (0-based) where the chord root falls in this scale.
1417
+ #
1418
+ # @param chord [Musa::Chords::Chord] the chord to check
1419
+ # @return [Integer, nil] grade (0-based) or nil if chord not in scale
1420
+ #
1421
+ # @example
1422
+ # c_major = Scales.et12[440.0].major[60]
1423
+ # g_chord = c_major.dominant.chord
1424
+ # c_major.degree_of_chord(g_chord) # => 4 (V degree, 0-based)
1425
+ #
1426
+ # @see #contains_chord?
1427
+ def degree_of_chord(chord)
1428
+ return nil unless contains_chord?(chord)
1429
+
1430
+ note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
1431
+ note&.grade
1432
+ end
1433
+
1434
+ # Creates an equivalent chord with this scale as its context.
1435
+ #
1436
+ # Returns a new Chord object that represents the same chord but with
1437
+ # this scale as its harmonic context. The chord's voicing (move and
1438
+ # duplicate settings) is preserved.
1439
+ #
1440
+ # @param chord [Musa::Chords::Chord] the source chord
1441
+ # @return [Musa::Chords::Chord, nil] new chord with this scale, or nil if not contained
1442
+ #
1443
+ # @example
1444
+ # c_major = Scales.et12[440.0].major[60]
1445
+ # g7 = c_major.dominant.chord :seventh
1446
+ #
1447
+ # g_mixolydian = Scales.et12[440.0].mixolydian[67]
1448
+ # g7_in_mixolydian = g_mixolydian.chord_on(g7)
1449
+ # g7_in_mixolydian.scale # => G Mixolydian scale
1450
+ #
1451
+ # @see #contains_chord?
1452
+ # @see #degree_of_chord
1453
+ def chord_on(chord)
1454
+ return nil unless contains_chord?(chord)
1455
+
1456
+ root_note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
1457
+ return nil unless root_note
1458
+
1459
+ Musa::Chords::Chord.with_root(
1460
+ root_note,
1461
+ scale: self,
1462
+ name: chord.chord_definition.name,
1463
+ move: chord.move.empty? ? nil : chord.move,
1464
+ duplicate: chord.duplicate.empty? ? nil : chord.duplicate
1465
+ )
1466
+ end
1467
+
1468
+ # Checks scale equality.
1469
+ #
1470
+ # Scales are equal if they have same kind and root pitch.
1471
+ #
1472
+ # @param other [Scale]
1473
+ # @return [Boolean]
414
1474
  def ==(other)
415
1475
  self.class == other.class &&
416
1476
  @kind == other.kind &&
417
1477
  @root_pitch == other.root_pitch
418
1478
  end
419
1479
 
1480
+ # Returns string representation.
1481
+ #
1482
+ # @return [String]
420
1483
  def inspect
421
1484
  "<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
422
1485
  end
@@ -424,13 +1487,108 @@ module Musa
424
1487
  alias to_s inspect
425
1488
  end
426
1489
 
1490
+ # Note within a scale context.
1491
+ #
1492
+ # NoteInScale represents a specific note within a scale, providing rich musical
1493
+ # functionality including:
1494
+ # - Pitch and frequency information
1495
+ # - Interval navigation (up, down, by named intervals)
1496
+ # - Chromatic alterations (sharp, flat)
1497
+ # - Scale navigation (change scales while keeping pitch)
1498
+ # - Chord construction
1499
+ # - Octave transposition
1500
+ #
1501
+ # ## Creation
1502
+ #
1503
+ # Notes are created via scale access, not directly:
1504
+ #
1505
+ # scale = tuning.major[60]
1506
+ # note = scale.tonic # NoteInScale instance
1507
+ # note = scale[:V] # Another NoteInScale
1508
+ #
1509
+ # ## Basic Properties
1510
+ #
1511
+ # note.pitch # MIDI pitch number
1512
+ # note.grade # Scale degree (0-based)
1513
+ # note.octave # Octave relative to scale root
1514
+ # note.frequency # Frequency in Hz
1515
+ # note.functions # Function names for this degree
1516
+ #
1517
+ # ## Interval Navigation
1518
+ #
1519
+ # **Natural intervals** (diatonic, within scale):
1520
+ #
1521
+ # note.up(2) # Up 2 scale degrees
1522
+ # note.down(1) # Down 1 scale degree
1523
+ #
1524
+ # **Chromatic intervals** (by semitones or named intervals):
1525
+ #
1526
+ # note.up(:P5) # Up perfect fifth
1527
+ # note.up(7) # Up 7 semitones (if chromatic specified)
1528
+ # note.down(:M3) # Down major third
1529
+ #
1530
+ # ## Chromatic Alterations
1531
+ #
1532
+ # note.sharp # Raise by 1 semitone
1533
+ # note.sharp(2) # Raise by 2 semitones
1534
+ # note.flat # Lower by 1 semitone
1535
+ # note.flat(2) # Lower by 2 semitones
1536
+ #
1537
+ # ## Scale Navigation
1538
+ #
1539
+ # note.scale(:minor) # Same pitch in minor scale
1540
+ # note.major # Same pitch in major scale
1541
+ # note.chromatic # Same pitch in chromatic scale
1542
+ #
1543
+ # ## Chord Construction
1544
+ #
1545
+ # note.chord # Build triad
1546
+ # note.chord :seventh # Build seventh chord
1547
+ # note.chord quality: :minor # Build with features
1548
+ #
1549
+ # ## Background Scale Context
1550
+ #
1551
+ # Chromatic notes remember their diatonic context:
1552
+ #
1553
+ # c# = c_major.tonic.sharp # C# in C major context
1554
+ # c#.background_scale # => c_major
1555
+ # c#.background_note # => C (natural)
1556
+ # c#.background_sharps # => 1
1557
+ #
1558
+ # @example Basic usage
1559
+ # c_major = tuning.major[60]
1560
+ # tonic = c_major.tonic
1561
+ # tonic.pitch # => 60
1562
+ # tonic.frequency # => ~261.63 Hz
1563
+ #
1564
+ # @example Interval navigation
1565
+ # tonic.up(:P5).pitch # => 67 (G)
1566
+ # tonic.up(4, :natural).pitch # => 71 (4 scale degrees = B)
1567
+ #
1568
+ # @example Chromatic alterations
1569
+ # tonic.sharp.pitch # => 61 (C#)
1570
+ # tonic.flat.pitch # => 59 (B)
1571
+ #
1572
+ # @example Chord building
1573
+ # tonic.chord # C major triad
1574
+ # tonic.chord :seventh # C major 7th
1575
+ #
1576
+ # @see Scale Parent scale
1577
+ # @see Chord Chord construction
427
1578
  class NoteInScale
428
1579
 
429
- # @param scale [Scale]
430
- # @param grade []
431
- # @param octave [Integer]
432
- # @param pitch [Number] pitch of the note, based on MIDI note numbers. Can be Integer, Rational or Float to express fractions of a semitone
1580
+ # Creates a note within a scale.
433
1581
  #
1582
+ # @param scale [Scale] parent scale
1583
+ # @param grade [Integer] scale degree (0-based)
1584
+ # @param octave [Integer] octave relative to scale root
1585
+ # @param pitch [Numeric] MIDI pitch (Integer, Rational, or Float for microtones)
1586
+ # @param background_scale [Scale, nil] diatonic context for chromatic notes
1587
+ # @param background_grade [Integer, nil] diatonic grade for chromatic notes
1588
+ # @param background_octave [Integer, nil] diatonic octave for chromatic notes
1589
+ # @param background_sharps [Integer, nil] sharps/flats from diatonic note
1590
+ #
1591
+ # @api private
434
1592
  def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
435
1593
  @scale = scale
436
1594
  @grade = grade
@@ -449,12 +1607,44 @@ module Musa
449
1607
  end
450
1608
  end
451
1609
 
452
- attr_reader :grade, :pitch
1610
+ # Scale degree (0-based).
1611
+ # @return [Integer]
1612
+ attr_reader :grade
1613
+
1614
+ # MIDI pitch number.
1615
+ # @return [Numeric]
1616
+ attr_reader :pitch
453
1617
 
1618
+ # Returns function names for this scale degree.
1619
+ #
1620
+ # @return [Array<Symbol>] function symbols
1621
+ #
1622
+ # @example
1623
+ # c_major.tonic.functions # => [:I, :_1, :tonic, :first]
454
1624
  def functions
455
1625
  @scale.kind.class.pitches[grade][:functions]
456
1626
  end
457
1627
 
1628
+ # Transposes note or returns current octave.
1629
+ #
1630
+ # **Without argument**: Returns current octave relative to scale root.
1631
+ #
1632
+ # **With argument**: Returns note transposed by octave offset.
1633
+ #
1634
+ # @param octave [Integer, nil] octave offset (nil to query current)
1635
+ # @param absolute [Boolean] if true, ignore current octave
1636
+ # @return [Integer, NoteInScale] current octave or transposed note
1637
+ # @raise [ArgumentError] if octave is not integer
1638
+ #
1639
+ # @example Query octave
1640
+ # note.octave # => 0 (at scale root octave)
1641
+ #
1642
+ # @example Transpose relative
1643
+ # note.octave(1).pitch # Up one octave from current
1644
+ # note.octave(-1).pitch # Down one octave from current
1645
+ #
1646
+ # @example Transpose absolute
1647
+ # note.octave(2, absolute: true).pitch # At octave 2, regardless of current
458
1648
  def octave(octave = nil, absolute: false)
459
1649
  if octave.nil?
460
1650
  @octave
@@ -465,6 +1655,18 @@ module Musa
465
1655
  end
466
1656
  end
467
1657
 
1658
+ # Creates a copy with background scale context.
1659
+ #
1660
+ # Used internally when creating chromatic notes to remember their
1661
+ # diatonic context.
1662
+ #
1663
+ # @param scale [Scale] background diatonic scale
1664
+ # @param grade [Integer, nil] background grade
1665
+ # @param octave [Integer, nil] background octave
1666
+ # @param sharps [Integer, nil] accidentals from background note
1667
+ # @return [NoteInScale] new note with background context
1668
+ #
1669
+ # @api private
468
1670
  def with_background(scale:, grade: nil, octave: nil, sharps: nil)
469
1671
  NoteInScale.new(@scale, @grade, @octave, @pitch,
470
1672
  background_scale: scale,
@@ -473,18 +1675,55 @@ module Musa
473
1675
  background_sharps: sharps)
474
1676
  end
475
1677
 
1678
+ # Background diatonic scale (for chromatic notes).
1679
+ # @return [Scale, nil]
476
1680
  attr_reader :background_scale
477
1681
 
1682
+ # Returns the diatonic note this chromatic note is based on.
1683
+ #
1684
+ # @return [NoteInScale, nil] background note or nil
1685
+ #
1686
+ # @example
1687
+ # c# = c_major.tonic.sharp
1688
+ # c#.background_note.pitch # => 60 (C natural)
478
1689
  def background_note
479
1690
  @background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
480
1691
  end
481
1692
 
1693
+ # Sharps/flats from background note.
1694
+ # @return [Integer, nil]
482
1695
  attr_reader :background_sharps
483
1696
 
1697
+ # Returns wide grade (grade + octave * grades_per_octave).
1698
+ #
1699
+ # @return [Integer]
1700
+ #
1701
+ # @example
1702
+ # note.wide_grade # => 7 (second octave, first degree)
1703
+ #
1704
+ # @api private
484
1705
  def wide_grade
485
1706
  @grade + @octave * @scale.kind.class.grades
486
1707
  end
487
1708
 
1709
+ # Navigates upward by interval.
1710
+ #
1711
+ # Supports both natural (diatonic) and chromatic (semitone) intervals.
1712
+ #
1713
+ # - **Numeric interval + :natural**: Move by scale degrees
1714
+ # - **Symbol or numeric interval + :chromatic**: Move by semitones or named interval
1715
+ #
1716
+ # @param interval_name_or_interval [Symbol, Integer] interval
1717
+ # @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
1718
+ # @param sign [Integer] direction multiplier (internal use)
1719
+ # @return [NoteInScale] note at interval above
1720
+ #
1721
+ # @example Natural interval (scale degrees)
1722
+ # note.up(2, :natural) # Up 2 scale degrees
1723
+ #
1724
+ # @example Chromatic interval (semitones)
1725
+ # note.up(:P5) # Up perfect fifth (7 semitones)
1726
+ # note.up(7) # Up 7 semitones (if chromatic)
488
1727
  def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)
489
1728
 
490
1729
  sign ||= 1
@@ -525,24 +1764,78 @@ module Musa
525
1764
 
526
1765
  private :calculate_note_of_pitch
527
1766
 
1767
+ # Navigates downward by interval.
1768
+ #
1769
+ # Same as {#up} but in reverse direction.
1770
+ #
1771
+ # @param interval_name_or_interval [Symbol, Integer] interval
1772
+ # @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
1773
+ # @return [NoteInScale] note at interval below
1774
+ #
1775
+ # @example
1776
+ # note.down(2, :natural) # Down 2 scale degrees
1777
+ # note.down(:P5) # Down perfect fifth
528
1778
  def down(interval_name_or_interval, natural_or_chromatic = nil)
529
1779
  up(interval_name_or_interval, natural_or_chromatic, sign: -1)
530
1780
  end
531
1781
 
1782
+ # Raises note by semitones (adds sharps).
1783
+ #
1784
+ # @param count [Integer, nil] number of semitones (default 1)
1785
+ # @return [NoteInScale] raised note
1786
+ #
1787
+ # @example
1788
+ # note.sharp.pitch # Up 1 semitone
1789
+ # note.sharp(2).pitch # Up 2 semitones
532
1790
  def sharp(count = nil)
533
1791
  count ||= 1
534
1792
  calculate_note_of_pitch(@pitch, count)
535
1793
  end
536
1794
 
1795
+ # Lowers note by semitones (adds flats).
1796
+ #
1797
+ # @param count [Integer, nil] number of semitones (default 1)
1798
+ # @return [NoteInScale] lowered note
1799
+ #
1800
+ # @example
1801
+ # note.flat.pitch # Down 1 semitone
1802
+ # note.flat(2).pitch # Down 2 semitones
537
1803
  def flat(count = nil)
538
1804
  count ||= 1
539
1805
  sharp(-count)
540
1806
  end
541
1807
 
1808
+ # Calculates frequency in Hz.
1809
+ #
1810
+ # Uses the scale system's frequency calculation (equal temperament,
1811
+ # just intonation, etc.) and the tuning's A frequency.
1812
+ #
1813
+ # @return [Float] frequency in Hz
1814
+ #
1815
+ # @example
1816
+ # c_major.tonic.frequency # => ~261.63 Hz (middle C at A=440)
542
1817
  def frequency
543
- @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root)
1818
+ @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
544
1819
  end
545
1820
 
1821
+ # Changes scale while keeping pitch, or returns current scale.
1822
+ #
1823
+ # **Without argument**: Returns current scale.
1824
+ #
1825
+ # **With argument**: Returns note at same pitch in different scale kind.
1826
+ #
1827
+ # @param kind_id_or_kind [Symbol, ScaleKind, nil] scale kind or ID
1828
+ # @return [Scale, NoteInScale] current scale or note in new scale
1829
+ #
1830
+ # @example Query current scale
1831
+ # note.scale # => <Scale: kind = MajorScaleKind ...>
1832
+ #
1833
+ # @example Change to minor
1834
+ # note.scale(:minor) # Same pitch in minor scale
1835
+ #
1836
+ # @example Dynamic method
1837
+ # note.minor # Same as note.scale(:minor)
1838
+ # note.major # Same as note.scale(:major)
546
1839
  def scale(kind_id_or_kind = nil)
547
1840
  if kind_id_or_kind.nil?
548
1841
  @scale
@@ -555,10 +1848,52 @@ module Musa
555
1848
  end
556
1849
  end
557
1850
 
1851
+ # Finds this note in another scale.
1852
+ #
1853
+ # Searches for a note with the same pitch in the target scale.
1854
+ #
1855
+ # @param scale [Scale] target scale to search
1856
+ # @return [NoteInScale, nil] note in target scale or nil
1857
+ #
1858
+ # @example
1859
+ # c_major_tonic = c_major.tonic
1860
+ # c_minor = tuning.minor[60]
1861
+ # c_major_tonic.on(c_minor) # C in C minor scale
558
1862
  def on(scale)
559
1863
  scale.note_of_pitch @pitch
560
1864
  end
561
1865
 
1866
+ # Builds a chord rooted on this note.
1867
+ #
1868
+ # Creates a chord using this note as the root. Chord can be specified by:
1869
+ # - Feature values (:triad, :seventh, :major, :minor, etc.)
1870
+ # - Feature hash (quality:, size:)
1871
+ # - Chord definition name (not shown here, see Chord.with_root)
1872
+ #
1873
+ # If no features specified, defaults to major triad.
1874
+ #
1875
+ # @param feature_values [Array<Symbol>] feature values (size, quality, etc.)
1876
+ # @param allow_chromatic [Boolean] allow non-diatonic chord notes
1877
+ # @param move [Hash{Symbol => Integer}] initial octave moves
1878
+ # @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
1879
+ # @param features_hash [Hash] feature key-value pairs
1880
+ # @return [Chord] chord rooted on this note
1881
+ #
1882
+ # @example Default triad
1883
+ # note.chord # Major triad
1884
+ #
1885
+ # @example Specified size
1886
+ # note.chord :seventh # Seventh chord matching scale
1887
+ # note.chord :ninth # Ninth chord
1888
+ #
1889
+ # @example With features
1890
+ # note.chord quality: :minor, size: :seventh
1891
+ # note.chord :minor, :seventh # Same as above
1892
+ #
1893
+ # @example With voicing
1894
+ # note.chord :seventh, move: {root: -1}, duplicate: {fifth: 1}
1895
+ #
1896
+ # @see Chord Chord class
562
1897
  def chord(*feature_values,
563
1898
  allow_chromatic: nil,
564
1899
  move: nil,
@@ -575,6 +1910,12 @@ module Musa
575
1910
  **features)
576
1911
  end
577
1912
 
1913
+ # Checks note equality.
1914
+ #
1915
+ # Notes are equal if they have same scale, grade, octave, and pitch.
1916
+ #
1917
+ # @param other [NoteInScale]
1918
+ # @return [Boolean]
578
1919
  def ==(other)
579
1920
  self.class == other.class &&
580
1921
  @scale == other.scale &&
@@ -583,6 +1924,9 @@ module Musa
583
1924
  @pitch == other.pitch
584
1925
  end
585
1926
 
1927
+ # Returns string representation.
1928
+ #
1929
+ # @return [String]
586
1930
  def inspect
587
1931
  "<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
588
1932
  end