musa-dsl 0.30.2 → 0.40.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -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,122 @@ 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
28
149
  end
29
150
 
151
+ # Abstract base class for musical scale systems.
152
+ #
153
+ # ScaleSystem defines the foundation of a tuning system, including:
154
+ #
155
+ # - Number of notes per octave
156
+ # - Available intervals
157
+ # - Frequency calculation method
158
+ # - Registered scale kinds (major, minor, etc.)
159
+ #
160
+ # ## Subclass Requirements
161
+ #
162
+ # Subclasses must implement:
163
+ #
164
+ # - {.id}: Unique symbol identifier
165
+ # - {.notes_in_octave}: Number of notes in an octave
166
+ # - {.part_of_tone_size}: Size of smallest pitch unit (for sharps/flats)
167
+ # - {.intervals}: Hash of named intervals to semitone offsets
168
+ # - {.frequency_of_pitch}: Pitch to frequency conversion
169
+ #
170
+ # Optionally override:
171
+ #
172
+ # - {.default_a_frequency}: Reference A frequency (defaults to 440.0 Hz)
173
+ #
174
+ # ## Usage
175
+ #
176
+ # ScaleSystem is accessed via {Scales} module, not instantiated directly:
177
+ #
178
+ # system = Scales[:et12] # Get system
179
+ # tuning = system[440.0] # Get tuning
180
+ # scale = tuning.major[60] # Get scale
181
+ #
182
+ # @abstract Subclass and implement abstract methods
183
+ # @see EquallyTempered12ToneScaleSystem Concrete 12-tone implementation
184
+ # @see ScaleSystemTuning Tuning with specific A frequency
30
185
  class ScaleSystem
31
- # @abstract Subclass is expected to implement names
32
- # @!method id
33
- # @return [Symbol] the id of the ScaleSystem as a symbol
186
+ # Returns the unique identifier for this scale system.
187
+ #
188
+ # @abstract Subclass must implement
189
+ # @return [Symbol] the scale system ID (e.g., :et12)
190
+ # @raise [RuntimeError] if not implemented in subclass
34
191
  #
192
+ # @example
193
+ # EquallyTempered12ToneScaleSystem.id # => :et12
35
194
  def self.id
36
195
  raise 'Method not implemented. Should be implemented in subclass.'
37
196
  end
38
197
 
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
198
+ # Returns the number of notes in one octave.
42
199
  #
200
+ # @abstract Subclass must implement
201
+ # @return [Integer] notes per octave (e.g., 12 for chromatic)
202
+ # @raise [RuntimeError] if not implemented in subclass
203
+ #
204
+ # @example
205
+ # EquallyTempered12ToneScaleSystem.notes_in_octave # => 12
43
206
  def self.notes_in_octave
44
207
  raise 'Method not implemented. Should be implemented in subclass.'
45
208
  end
46
209
 
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
210
+ # Returns the size of the smallest pitch unit.
211
+ #
212
+ # Used for calculating sharp (#) and flat (♭) alterations.
213
+ # In equal temperament, this is 1 semitone.
50
214
  #
215
+ # @abstract Subclass must implement
216
+ # @return [Integer] smallest unit size
217
+ # @raise [RuntimeError] if not implemented in subclass
218
+ #
219
+ # @example
220
+ # EquallyTempered12ToneScaleSystem.part_of_tone_size # => 1
51
221
  def self.part_of_tone_size
52
222
  raise 'Method not implemented. Should be implemented in subclass.'
53
223
  end
54
224
 
55
- # @abstract Subclass is expected to implement intervals
56
- # @!method intervals
57
- # @return [Hash] the intervals of the ScaleSystem as { name: semitones#, ... }
225
+ # Returns available intervals as name-to-offset mapping.
226
+ #
227
+ # Intervals are named using standard music theory notation:
228
+ #
229
+ # - **P** (Perfect): P1, P4, P5, P8
230
+ # - **M** (Major): M2, M3, M6, M7
231
+ # - **m** (minor): m2, m3, m6, m7
232
+ # - **TT**: Tritone
58
233
  #
234
+ # @abstract Subclass must implement
235
+ # @return [Hash{Symbol => Integer}] interval names to semitone offsets
236
+ # @raise [RuntimeError] if not implemented in subclass
237
+ #
238
+ # @example
239
+ # intervals[:M3] # => 4 (major third = 4 semitones)
240
+ # intervals[:P5] # => 7 (perfect fifth = 7 semitones)
241
+ # intervals[:m7] # => 10 (minor seventh = 10 semitones)
59
242
  def self.intervals
60
243
  # TODO: implementar intérvalos sinónimos (p.ej, m3 = A2)
61
244
  # TODO: implementar identificación de intérvalos, teniendo en cuenta no sólo los semitonos sino los grados de separación
@@ -63,25 +246,54 @@ module Musa
63
246
  raise 'Method not implemented. Should be implemented in subclass.'
64
247
  end
65
248
 
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
249
+ # Calculates frequency for a given pitch.
250
+ #
251
+ # Converts MIDI pitch numbers to frequencies in Hz. The calculation method
252
+ # depends on the tuning system (equal temperament, just intonation, etc.).
72
253
  #
254
+ # @abstract Subclass must implement
255
+ # @param pitch [Numeric] MIDI pitch number (60 = middle C, 69 = A440)
256
+ # @param root_pitch [Numeric] root pitch of scale (for non-equal temperaments)
257
+ # @param a_frequency [Numeric] reference A frequency in Hz
258
+ # @return [Float] frequency in Hz
259
+ # @raise [RuntimeError] if not implemented in subclass
260
+ #
261
+ # @example Equal temperament
262
+ # # A440 (MIDI 69)
263
+ # frequency_of_pitch(69, 60, 440.0) # => 440.0
264
+ #
265
+ # # Middle C (MIDI 60)
266
+ # frequency_of_pitch(60, 60, 440.0) # => ~261.63 Hz
73
267
  def self.frequency_of_pitch(pitch, root_pitch, a_frequency)
74
268
  raise 'Method not implemented. Should be implemented in subclass.'
75
269
  end
76
270
 
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
271
+ # Returns the default A frequency.
80
272
  #
273
+ # @return [Float] default A frequency in Hz (440.0 standard concert pitch)
274
+ #
275
+ # @example
276
+ # ScaleSystem.default_a_frequency # => 440.0
81
277
  def self.default_a_frequency
82
278
  440.0
83
279
  end
84
280
 
281
+ # Creates or retrieves a tuning for this scale system.
282
+ #
283
+ # Returns a {ScaleSystemTuning} instance for the specified A frequency.
284
+ # Tunings are cached—repeated calls with same frequency return same instance.
285
+ #
286
+ # @param a_frequency [Numeric] reference A frequency in Hz
287
+ # @return [ScaleSystemTuning] tuning instance
288
+ #
289
+ # @example Standard pitch
290
+ # tuning = ScaleSystem[440.0]
291
+ #
292
+ # @example Baroque pitch
293
+ # baroque = ScaleSystem[415.0]
294
+ #
295
+ # @example Modern high pitch
296
+ # modern = ScaleSystem[442.0]
85
297
  def self.[](a_frequency)
86
298
  a_frequency = a_frequency.to_f
87
299
 
@@ -91,14 +303,34 @@ module Musa
91
303
  @a_tunings[a_frequency]
92
304
  end
93
305
 
306
+ # Returns semitone offset for a named interval.
307
+ #
308
+ # @param name [Symbol] interval name (e.g., :M3, :P5)
309
+ # @return [Integer] semitone offset
310
+ #
311
+ # @example
312
+ # offset_of_interval(:P5) # => 7
94
313
  def self.offset_of_interval(name)
95
314
  intervals[name]
96
315
  end
97
316
 
317
+ # Returns the default tuning (A=440Hz).
318
+ #
319
+ # @return [ScaleSystemTuning] default tuning instance
320
+ #
321
+ # @example
322
+ # tuning = ScaleSystem.default_tuning
98
323
  def self.default_tuning
99
324
  self[default_a_frequency]
100
325
  end
101
326
 
327
+ # Registers a scale kind (major, minor, etc.) with this system.
328
+ #
329
+ # @param scale_kind_class [Class] ScaleKind subclass to register
330
+ # @return [self]
331
+ #
332
+ # @example
333
+ # EquallyTempered12ToneScaleSystem.register MajorScaleKind
102
334
  def self.register(scale_kind_class)
103
335
  @scale_kind_classes ||= {}
104
336
  @scale_kind_classes[scale_kind_class.id] = scale_kind_class
@@ -108,31 +340,90 @@ module Musa
108
340
  self
109
341
  end
110
342
 
343
+ # Retrieves a registered scale kind by ID.
344
+ #
345
+ # @param id [Symbol] scale kind identifier
346
+ # @return [Class] ScaleKind subclass
347
+ # @raise [KeyError] if not found
111
348
  def self.scale_kind_class(id)
112
349
  raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
113
350
 
114
351
  @scale_kind_classes[id]
115
352
  end
116
353
 
354
+ # Checks if a scale kind is registered.
355
+ #
356
+ # @param id [Symbol] scale kind identifier
357
+ # @return [Boolean]
117
358
  def self.scale_kind_class?(id)
118
359
  @scale_kind_classes.key? id
119
360
  end
120
361
 
362
+ # Returns all registered scale kinds.
363
+ #
364
+ # @return [Hash{Symbol => Class}] scale kind classes
121
365
  def self.scale_kind_classes
122
366
  @scale_kind_classes
123
367
  end
124
368
 
369
+ # Returns the chromatic scale kind class.
370
+ #
371
+ # @return [Class] chromatic ScaleKind subclass
372
+ # @raise [RuntimeError] if chromatic scale not defined
125
373
  def self.chromatic_class
126
374
  raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
127
375
 
128
376
  @chromatic_scale_kind_class
129
377
  end
130
378
 
379
+ # Compares scale systems for equality.
380
+ #
381
+ # @param other [ScaleSystem]
382
+ # @return [Boolean]
131
383
  def ==(other)
132
384
  self.class == other.class
133
385
  end
134
386
  end
135
387
 
388
+ # Scale system with specific A frequency tuning.
389
+ #
390
+ # ScaleSystemTuning combines a {ScaleSystem} with a specific reference A frequency,
391
+ # providing access to scale kinds (major, minor, chromatic, etc.) tuned to that
392
+ # frequency.
393
+ #
394
+ # ## Usage
395
+ #
396
+ # Tunings are created via {ScaleSystem.[]}:
397
+ #
398
+ # tuning = Scales[:et12][440.0] # Standard pitch
399
+ # baroque = Scales[:et12][415.0] # Baroque pitch
400
+ #
401
+ # ## Accessing Scales
402
+ #
403
+ # **By symbol**:
404
+ #
405
+ # tuning[:major][60] # C major scale
406
+ #
407
+ # **By method name**:
408
+ #
409
+ # tuning.major[60] # C major scale
410
+ # tuning.minor[69] # A minor scale
411
+ #
412
+ # **Chromatic scale**:
413
+ #
414
+ # tuning.chromatic[60] # C chromatic scale
415
+ #
416
+ # @example Standard usage
417
+ # tuning = Scales::Scales.default_system.default_tuning
418
+ # c_major = tuning.major[60]
419
+ # a_minor = tuning.minor[69]
420
+ #
421
+ # @example Historical pitch
422
+ # baroque = Scales[:et12][415.0]
423
+ # scale = baroque.major[60] # C major at A=415Hz
424
+ #
425
+ # @see ScaleSystem Parent scale system
426
+ # @see ScaleKind Scale types (major, minor, etc.)
136
427
  class ScaleSystemTuning
137
428
  extend Forwardable
138
429
 
@@ -181,70 +472,214 @@ module Musa
181
472
  alias to_s inspect
182
473
  end
183
474
 
475
+ # Abstract base class for scale types (major, minor, chromatic, etc.).
476
+ #
477
+ # ScaleKind defines a type of scale (major, minor, chromatic, etc.) independent
478
+ # of root pitch or tuning. It specifies:
479
+ #
480
+ # - Scale degrees and their pitch offsets
481
+ # - Function names for each degree (tonic, dominant, etc.)
482
+ # - Number of grades per octave
483
+ # - Whether the scale is chromatic (contains all pitches)
484
+ #
485
+ # ## Subclass Requirements
486
+ #
487
+ # Subclasses must implement:
488
+ #
489
+ # - {.id}: Unique symbol identifier (:major, :minor, :chromatic, etc.)
490
+ # - {.pitches}: Array defining scale structure
491
+ # - {.chromatic?}: Whether this is the chromatic scale (default: false)
492
+ # - {.grades}: Number of grades per octave (if different from pitches.length)
493
+ #
494
+ # ## Pitch Structure
495
+ #
496
+ # The {.pitches} array defines the scale structure:
497
+ #
498
+ # [{ functions: [:I, :tonic, :_1], pitch: 0 },
499
+ # { functions: [:II, :supertonic, :_2], pitch: 2 },
500
+ # ...]
501
+ #
502
+ # - **functions**: Array of symbols that can access this degree
503
+ # - **pitch**: Semitone offset from root
504
+ #
505
+ # ## Dynamic Method Creation
506
+ #
507
+ # Each scale instance gets methods for all registered scale kinds:
508
+ #
509
+ # note.major # Get major scale rooted on this note
510
+ # note.minor # Get minor scale rooted on this note
511
+ #
512
+ # ## Usage
513
+ #
514
+ # ScaleKind instances are accessed via tuning:
515
+ #
516
+ # tuning = Scales[:et12][440.0]
517
+ # major_kind = tuning[:major] # ScaleKind instance
518
+ # c_major = major_kind[60] # Scale instance
519
+ #
520
+ # Or directly via convenience methods:
521
+ #
522
+ # c_major = tuning.major[60]
523
+ #
524
+ # @abstract Subclass and implement abstract methods
525
+ # @see MajorScaleKind Concrete major scale implementation
526
+ # @see MinorNaturalScaleKind Concrete minor scale implementation
527
+ # @see ChromaticScaleKind Concrete chromatic scale implementation
528
+ # @see Scale Instantiated scale with root pitch
184
529
  class ScaleKind
530
+ # Creates a scale kind instance.
531
+ #
532
+ # @param tuning [ScaleSystemTuning] the tuning context
533
+ #
534
+ # @api private
185
535
  def initialize(tuning)
186
536
  @tuning = tuning
187
537
  @scales = {}
188
538
  end
189
539
 
540
+ # The tuning context.
541
+ # @return [ScaleSystemTuning]
190
542
  attr_reader :tuning
191
543
 
544
+ # Creates or retrieves a scale rooted on specific pitch.
545
+ #
546
+ # Scales are cached—repeated calls with same pitch return same instance.
547
+ #
548
+ # @param root_pitch [Integer] MIDI root pitch (60 = middle C)
549
+ # @return [Scale] scale instance
550
+ #
551
+ # @example
552
+ # major_kind = tuning[:major]
553
+ # c_major = major_kind[60] # C major
554
+ # g_major = major_kind[67] # G major
192
555
  def [](root_pitch)
193
556
  @scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
194
557
  @scales[root_pitch]
195
558
  end
196
559
 
560
+ # Returns scale with default root (middle C, MIDI 60).
561
+ #
562
+ # @return [Scale] scale rooted on middle C
563
+ #
564
+ # @example
565
+ # tuning.major.default_root # C major
197
566
  def default_root
198
567
  self[60]
199
568
  end
200
569
 
570
+ # Returns scale with absolute root (MIDI 0).
571
+ #
572
+ # @return [Scale] scale rooted on MIDI 0
573
+ #
574
+ # @example
575
+ # tuning.major.absolut # Scale rooted at MIDI 0
201
576
  def absolut
202
577
  self[0]
203
578
  end
204
579
 
580
+ # Checks scale kind equality.
581
+ #
582
+ # @param other [ScaleKind]
583
+ # @return [Boolean]
205
584
  def ==(other)
206
585
  self.class == other.class && @tuning == other.tuning
207
586
  end
208
587
 
588
+ # Returns string representation.
589
+ #
590
+ # @return [String]
209
591
  def inspect
210
592
  "<#{self.class.name}: tuning = #{@tuning}>"
211
593
  end
212
594
 
213
595
  alias to_s inspect
214
596
 
215
- # @abstract Subclass is expected to implement id
216
- # @!method id
217
- # @return [Symbol] the id of the ScaleKind as a symbol
597
+ # Returns the unique identifier for this scale kind.
598
+ #
599
+ # @abstract Subclass must implement
600
+ # @return [Symbol] scale kind ID (e.g., :major, :minor, :chromatic)
601
+ # @raise [RuntimeError] if not implemented in subclass
602
+ #
603
+ # @example
604
+ # MajorScaleKind.id # => :major
218
605
  def self.id
219
606
  raise 'Method not implemented. Should be implemented in subclass.'
220
607
  end
221
608
 
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> }, ... ]
609
+ # Returns the pitch structure definition.
610
+ #
611
+ # Defines the scale degrees and their pitch offsets from the root.
612
+ # Each entry specifies function names and semitone offset.
613
+ #
614
+ # @abstract Subclass must implement
615
+ # @return [Array<Hash>] array of pitch definitions with:
616
+ # - **:functions** [Array<Symbol>]: function names for this degree
617
+ # - **:pitch** [Integer]: semitone offset from root
618
+ # @raise [RuntimeError] if not implemented in subclass
619
+ #
620
+ # @example Major scale structure (partial)
621
+ # [{ functions: [:I, :tonic, :_1], pitch: 0 },
622
+ # { functions: [:II, :supertonic, :_2], pitch: 2 },
623
+ # { functions: [:III, :mediant, :_3], pitch: 4 },
624
+ # ...]
225
625
  def self.pitches
226
626
  raise 'Method not implemented. Should be implemented in subclass.'
227
627
  end
228
628
 
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.
629
+ # Indicates whether this is the chromatic scale.
630
+ #
631
+ # Only one scale kind per system should return true. The chromatic scale
632
+ # contains all notes in the scale system and is used as a fallback for
633
+ # non-diatonic notes.
634
+ #
635
+ # @return [Boolean] true if chromatic scale (default: false)
636
+ #
637
+ # @example
638
+ # ChromaticScaleKind.chromatic? # => true
639
+ # MajorScaleKind.chromatic? # => false
232
640
  def self.chromatic?
233
641
  false
234
642
  end
235
643
 
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
644
+ # Returns the number of grades per octave.
645
+ #
646
+ # For scales defining extended harmony (8th, 9th, etc.), this returns
647
+ # the number of diatonic degrees within one octave. Defaults to the
648
+ # number of pitch definitions.
649
+ #
650
+ # @return [Integer] number of grades per octave
651
+ #
652
+ # @example
653
+ # MajorScaleKind.grades # => 7 (not 13, even with extended degrees)
239
654
  def self.grades
240
655
  pitches.length
241
656
  end
242
657
 
658
+ # Returns grade index for a function symbol.
659
+ #
660
+ # @param symbol [Symbol] function name (e.g., :tonic, :dominant, :V)
661
+ # @return [Integer, nil] grade index or nil if not found
662
+ #
663
+ # @example
664
+ # MajorScaleKind.grade_of_function(:tonic) # => 0
665
+ # MajorScaleKind.grade_of_function(:dominant) # => 4
666
+ # MajorScaleKind.grade_of_function(:V) # => 4
667
+ #
668
+ # @api private
243
669
  def self.grade_of_function(symbol)
244
670
  create_grade_functions_index unless @grade_names_index
245
671
  @grade_names_index[symbol]
246
672
  end
247
673
 
674
+ # Returns all function symbols for accessing scale degrees.
675
+ #
676
+ # @return [Array<Symbol>] all function names
677
+ #
678
+ # @example
679
+ # MajorScaleKind.grades_functions
680
+ # # => [:I, :_1, :tonic, :first, :II, :_2, :supertonic, :second, ...]
681
+ #
682
+ # @api private
248
683
  def self.grades_functions
249
684
  create_grade_functions_index unless @grade_names_index
250
685
  @grade_names_index.keys
@@ -252,6 +687,11 @@ module Musa
252
687
 
253
688
  private
254
689
 
690
+ # Creates internal index mapping function names to grade indices.
691
+ #
692
+ # @return [self]
693
+ #
694
+ # @api private
255
695
  def self.create_grade_functions_index
256
696
  @grade_names_index = {}
257
697
  pitches.each_index do |i|
@@ -264,9 +704,88 @@ module Musa
264
704
  end
265
705
  end
266
706
 
707
+ # Instantiated scale with specific root pitch.
708
+ #
709
+ # Scale represents a concrete scale (major, minor, etc.) rooted on a specific
710
+ # pitch. It provides access to scale degrees, interval calculations, frequency
711
+ # generation, and chord construction.
712
+ #
713
+ # ## Creation
714
+ #
715
+ # Scales are created via {ScaleKind}:
716
+ #
717
+ # tuning = Scales[:et12][440.0]
718
+ # c_major = tuning.major[60] # Via convenience method
719
+ # a_minor = tuning[:minor][69] # Via bracket notation
720
+ #
721
+ # ## Accessing Notes
722
+ #
723
+ # **By numeric grade** (0-based):
724
+ #
725
+ # scale[0] # First degree (tonic)
726
+ # scale[1] # Second degree
727
+ # scale[4] # Fifth degree
728
+ #
729
+ # **By function name** (dynamic methods):
730
+ #
731
+ # scale.tonic # First degree
732
+ # scale.dominant # Fifth degree
733
+ # scale.mediant # Third degree
734
+ #
735
+ # **By Roman numeral**:
736
+ #
737
+ # scale[:I] # First degree
738
+ # scale[:V] # Fifth degree
739
+ # scale[:IV] # Fourth degree
740
+ #
741
+ # **With accidentals** (sharp # or flat _):
742
+ #
743
+ # scale[:I#] # Raised tonic
744
+ # scale[:V_] # Flatted dominant
745
+ # scale['II##'] # Double-raised second
746
+ #
747
+ # ## Note Operations
748
+ #
749
+ # Each note is a {NoteInScale} instance with full capabilities:
750
+ #
751
+ # note = scale.tonic
752
+ # note.pitch # MIDI pitch number
753
+ # note.frequency # Frequency in Hz
754
+ # note.chord # Build chord from note
755
+ # note.up(:P5) # Navigate by interval
756
+ # note.sharp # Raise by semitone
757
+ #
758
+ # ## Special Methods
759
+ #
760
+ # - **chromatic**: Access chromatic scale at same root
761
+ # - **octave**: Transpose scale to different octave
762
+ # - **note_of_pitch**: Find note for specific MIDI pitch
763
+ #
764
+ # @example Basic scale access
765
+ # c_major = tuning.major[60]
766
+ # c_major.tonic.pitch # => 60 (C)
767
+ # c_major.dominant.pitch # => 67 (G)
768
+ # c_major[:III].pitch # => 64 (E)
769
+ #
770
+ # @example Chromatic alterations
771
+ # c_major[:I#].pitch # => 61 (C#)
772
+ # c_major[:V_].pitch # => 66 (F#/Gb)
773
+ #
774
+ # @example Building chords
775
+ # c_major.tonic.chord # C major triad
776
+ # c_major.dominant.chord :seventh # G dominant 7th
777
+ #
778
+ # @see ScaleKind Scale type definition
779
+ # @see NoteInScale Individual note in scale
267
780
  class Scale
268
781
  extend Forwardable
269
782
 
783
+ # Creates a scale instance.
784
+ #
785
+ # @param kind [ScaleKind] the scale kind
786
+ # @param root_pitch [Integer] MIDI root pitch
787
+ #
788
+ # @api private
270
789
  def initialize(kind, root_pitch:)
271
790
  @notes_by_grade = {}
272
791
  @notes_by_pitch = {}
@@ -284,28 +803,95 @@ module Musa
284
803
  freeze
285
804
  end
286
805
 
806
+ # Delegates tuning access to kind.
287
807
  def_delegators :@kind, :tuning
288
808
 
289
- attr_reader :kind, :root_pitch
809
+ # Scale kind (major, minor, etc.).
810
+ # @return [ScaleKind]
811
+ attr_reader :kind
812
+
813
+ # Root pitch (MIDI number).
814
+ # @return [Integer]
815
+ attr_reader :root_pitch
290
816
 
817
+ # Returns the root note (first degree).
818
+ #
819
+ # Equivalent to scale[0] or scale.tonic.
820
+ #
821
+ # @return [NoteInScale] root note
822
+ #
823
+ # @example
824
+ # c_major.root.pitch # => 60
291
825
  def root
292
826
  self[0]
293
827
  end
294
828
 
829
+ # Returns the chromatic scale at the same root.
830
+ #
831
+ # @return [Scale] chromatic scale rooted at same pitch
832
+ #
833
+ # @example
834
+ # c_major.chromatic # Chromatic scale starting at C
295
835
  def chromatic
296
836
  @kind.tuning.chromatic[@root_pitch]
297
837
  end
298
838
 
839
+ # Returns the scale rooted at absolute pitch 0.
840
+ #
841
+ # @return [Scale] scale of same kind at MIDI 0
842
+ #
843
+ # @example
844
+ # c_major.absolut # Major scale at MIDI 0
299
845
  def absolut
300
846
  @kind[0]
301
847
  end
302
848
 
849
+ # Transposes scale by octaves.
850
+ #
851
+ # @param octave [Integer] octave offset (positive = up, negative = down)
852
+ # @return [Scale] transposed scale
853
+ # @raise [ArgumentError] if octave is not integer
854
+ #
855
+ # @example
856
+ # c_major.octave(1) # C major one octave higher
857
+ # c_major.octave(-1) # C major one octave lower
303
858
  def octave(octave)
304
859
  raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
305
860
 
306
861
  @kind[@root_pitch + octave * @kind.class.grades]
307
862
  end
308
863
 
864
+ # Accesses scale degree by grade, symbol, or function name.
865
+ #
866
+ # Supports multiple access patterns:
867
+ # - **Integer**: Numeric grade (0-based)
868
+ # - **Symbol/String**: Function name or Roman numeral
869
+ # - **With accidentals**: Add '#' for sharp, '_' for flat
870
+ #
871
+ # Notes are cached—repeated access returns same instance.
872
+ #
873
+ # @param grade_or_symbol [Integer, Symbol, String] degree specifier
874
+ # @return [NoteInScale] note at specified degree
875
+ # @raise [ArgumentError] if grade_or_symbol is invalid type
876
+ #
877
+ # @example Numeric access
878
+ # scale[0] # Tonic
879
+ # scale[4] # Dominant (in major/minor)
880
+ #
881
+ # @example Function name access
882
+ # scale[:tonic]
883
+ # scale[:dominant]
884
+ # scale[:mediant]
885
+ #
886
+ # @example Roman numeral access
887
+ # scale[:I] # Tonic
888
+ # scale[:V] # Dominant
889
+ # scale[:IV] # Subdominant
890
+ #
891
+ # @example With accidentals
892
+ # scale[:I#] # Raised tonic
893
+ # scale[:V_] # Flatted dominant
894
+ # scale['II##'] # Double-raised second
309
895
  def [](grade_or_symbol)
310
896
 
311
897
  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 +916,12 @@ module Musa
330
916
  @notes_by_grade[wide_grade].sharp(sharps)
331
917
  end
332
918
 
919
+ # Converts grade specifier to numeric grade and accidentals.
920
+ #
921
+ # @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
922
+ # @return [Array(Integer, Integer)] wide grade and accidentals count
923
+ #
924
+ # @api private
333
925
  def grade_of(grade_or_string_or_symbol)
334
926
  name, wide_grade, accidentals = parse_grade(grade_or_string_or_symbol)
335
927
 
@@ -343,6 +935,15 @@ module Musa
343
935
  return octave * @kind.class.grades + grade, accidentals
344
936
  end
345
937
 
938
+ # Parses grade string/symbol into components.
939
+ #
940
+ # Handles formats like "I#", ":V_", "7##", extracting function name,
941
+ # numeric grade, and accidentals.
942
+ #
943
+ # @param neuma_grade [Integer, Symbol, String] grade to parse
944
+ # @return [Array(Symbol, Integer, Integer)] name, wide_grade, accidentals
945
+ #
946
+ # @api private
346
947
  def parse_grade(neuma_grade)
347
948
  name = wide_grade = nil
348
949
  accidentals = 0
@@ -371,6 +972,24 @@ module Musa
371
972
  return name, wide_grade, accidentals
372
973
  end
373
974
 
975
+ # Finds note for a specific MIDI pitch.
976
+ #
977
+ # Searches for a note in the scale matching the given pitch. Options control
978
+ # behavior when pitch is not in scale.
979
+ #
980
+ # @param pitch [Integer] MIDI pitch number
981
+ # @param allow_chromatic [Boolean] if true, return chromatic note when not in scale
982
+ # @param allow_nearest [Boolean] if true, return nearest scale note
983
+ # @return [NoteInScale, nil] matching note or nil
984
+ #
985
+ # @example Diatonic note
986
+ # c_major.note_of_pitch(64) # => E (in scale)
987
+ #
988
+ # @example Chromatic note
989
+ # c_major.note_of_pitch(63, allow_chromatic: true) # => Eb (chromatic)
990
+ #
991
+ # @example Nearest note
992
+ # c_major.note_of_pitch(63, allow_nearest: true) # => E or D (nearest)
374
993
  def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
375
994
  allow_chromatic ||= false
376
995
  allow_nearest ||= false
@@ -407,16 +1026,33 @@ module Musa
407
1026
  note
408
1027
  end
409
1028
 
1029
+ # Returns semitone offset for a named interval.
1030
+ #
1031
+ # @param interval_name [Symbol] interval name (e.g., :M3, :P5)
1032
+ # @return [Integer] semitone offset
1033
+ #
1034
+ # @example
1035
+ # scale.offset_of_interval(:P5) # => 7
1036
+ # scale.offset_of_interval(:M3) # => 4
410
1037
  def offset_of_interval(interval_name)
411
1038
  @kind.tuning.offset_of_interval(interval_name)
412
1039
  end
413
1040
 
1041
+ # Checks scale equality.
1042
+ #
1043
+ # Scales are equal if they have same kind and root pitch.
1044
+ #
1045
+ # @param other [Scale]
1046
+ # @return [Boolean]
414
1047
  def ==(other)
415
1048
  self.class == other.class &&
416
1049
  @kind == other.kind &&
417
1050
  @root_pitch == other.root_pitch
418
1051
  end
419
1052
 
1053
+ # Returns string representation.
1054
+ #
1055
+ # @return [String]
420
1056
  def inspect
421
1057
  "<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
422
1058
  end
@@ -424,13 +1060,108 @@ module Musa
424
1060
  alias to_s inspect
425
1061
  end
426
1062
 
1063
+ # Note within a scale context.
1064
+ #
1065
+ # NoteInScale represents a specific note within a scale, providing rich musical
1066
+ # functionality including:
1067
+ # - Pitch and frequency information
1068
+ # - Interval navigation (up, down, by named intervals)
1069
+ # - Chromatic alterations (sharp, flat)
1070
+ # - Scale navigation (change scales while keeping pitch)
1071
+ # - Chord construction
1072
+ # - Octave transposition
1073
+ #
1074
+ # ## Creation
1075
+ #
1076
+ # Notes are created via scale access, not directly:
1077
+ #
1078
+ # scale = tuning.major[60]
1079
+ # note = scale.tonic # NoteInScale instance
1080
+ # note = scale[:V] # Another NoteInScale
1081
+ #
1082
+ # ## Basic Properties
1083
+ #
1084
+ # note.pitch # MIDI pitch number
1085
+ # note.grade # Scale degree (0-based)
1086
+ # note.octave # Octave relative to scale root
1087
+ # note.frequency # Frequency in Hz
1088
+ # note.functions # Function names for this degree
1089
+ #
1090
+ # ## Interval Navigation
1091
+ #
1092
+ # **Natural intervals** (diatonic, within scale):
1093
+ #
1094
+ # note.up(2) # Up 2 scale degrees
1095
+ # note.down(1) # Down 1 scale degree
1096
+ #
1097
+ # **Chromatic intervals** (by semitones or named intervals):
1098
+ #
1099
+ # note.up(:P5) # Up perfect fifth
1100
+ # note.up(7) # Up 7 semitones (if chromatic specified)
1101
+ # note.down(:M3) # Down major third
1102
+ #
1103
+ # ## Chromatic Alterations
1104
+ #
1105
+ # note.sharp # Raise by 1 semitone
1106
+ # note.sharp(2) # Raise by 2 semitones
1107
+ # note.flat # Lower by 1 semitone
1108
+ # note.flat(2) # Lower by 2 semitones
1109
+ #
1110
+ # ## Scale Navigation
1111
+ #
1112
+ # note.scale(:minor) # Same pitch in minor scale
1113
+ # note.major # Same pitch in major scale
1114
+ # note.chromatic # Same pitch in chromatic scale
1115
+ #
1116
+ # ## Chord Construction
1117
+ #
1118
+ # note.chord # Build triad
1119
+ # note.chord :seventh # Build seventh chord
1120
+ # note.chord quality: :minor # Build with features
1121
+ #
1122
+ # ## Background Scale Context
1123
+ #
1124
+ # Chromatic notes remember their diatonic context:
1125
+ #
1126
+ # c# = c_major.tonic.sharp # C# in C major context
1127
+ # c#.background_scale # => c_major
1128
+ # c#.background_note # => C (natural)
1129
+ # c#.background_sharps # => 1
1130
+ #
1131
+ # @example Basic usage
1132
+ # c_major = tuning.major[60]
1133
+ # tonic = c_major.tonic
1134
+ # tonic.pitch # => 60
1135
+ # tonic.frequency # => ~261.63 Hz
1136
+ #
1137
+ # @example Interval navigation
1138
+ # tonic.up(:P5).pitch # => 67 (G)
1139
+ # tonic.up(4, :natural).pitch # => 71 (4 scale degrees = B)
1140
+ #
1141
+ # @example Chromatic alterations
1142
+ # tonic.sharp.pitch # => 61 (C#)
1143
+ # tonic.flat.pitch # => 59 (B)
1144
+ #
1145
+ # @example Chord building
1146
+ # tonic.chord # C major triad
1147
+ # tonic.chord :seventh # C major 7th
1148
+ #
1149
+ # @see Scale Parent scale
1150
+ # @see Chord Chord construction
427
1151
  class NoteInScale
428
1152
 
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
1153
+ # Creates a note within a scale.
433
1154
  #
1155
+ # @param scale [Scale] parent scale
1156
+ # @param grade [Integer] scale degree (0-based)
1157
+ # @param octave [Integer] octave relative to scale root
1158
+ # @param pitch [Numeric] MIDI pitch (Integer, Rational, or Float for microtones)
1159
+ # @param background_scale [Scale, nil] diatonic context for chromatic notes
1160
+ # @param background_grade [Integer, nil] diatonic grade for chromatic notes
1161
+ # @param background_octave [Integer, nil] diatonic octave for chromatic notes
1162
+ # @param background_sharps [Integer, nil] sharps/flats from diatonic note
1163
+ #
1164
+ # @api private
434
1165
  def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
435
1166
  @scale = scale
436
1167
  @grade = grade
@@ -449,12 +1180,44 @@ module Musa
449
1180
  end
450
1181
  end
451
1182
 
452
- attr_reader :grade, :pitch
1183
+ # Scale degree (0-based).
1184
+ # @return [Integer]
1185
+ attr_reader :grade
1186
+
1187
+ # MIDI pitch number.
1188
+ # @return [Numeric]
1189
+ attr_reader :pitch
453
1190
 
1191
+ # Returns function names for this scale degree.
1192
+ #
1193
+ # @return [Array<Symbol>] function symbols
1194
+ #
1195
+ # @example
1196
+ # c_major.tonic.functions # => [:I, :_1, :tonic, :first]
454
1197
  def functions
455
1198
  @scale.kind.class.pitches[grade][:functions]
456
1199
  end
457
1200
 
1201
+ # Transposes note or returns current octave.
1202
+ #
1203
+ # **Without argument**: Returns current octave relative to scale root.
1204
+ #
1205
+ # **With argument**: Returns note transposed by octave offset.
1206
+ #
1207
+ # @param octave [Integer, nil] octave offset (nil to query current)
1208
+ # @param absolute [Boolean] if true, ignore current octave
1209
+ # @return [Integer, NoteInScale] current octave or transposed note
1210
+ # @raise [ArgumentError] if octave is not integer
1211
+ #
1212
+ # @example Query octave
1213
+ # note.octave # => 0 (at scale root octave)
1214
+ #
1215
+ # @example Transpose relative
1216
+ # note.octave(1).pitch # Up one octave from current
1217
+ # note.octave(-1).pitch # Down one octave from current
1218
+ #
1219
+ # @example Transpose absolute
1220
+ # note.octave(2, absolute: true).pitch # At octave 2, regardless of current
458
1221
  def octave(octave = nil, absolute: false)
459
1222
  if octave.nil?
460
1223
  @octave
@@ -465,6 +1228,18 @@ module Musa
465
1228
  end
466
1229
  end
467
1230
 
1231
+ # Creates a copy with background scale context.
1232
+ #
1233
+ # Used internally when creating chromatic notes to remember their
1234
+ # diatonic context.
1235
+ #
1236
+ # @param scale [Scale] background diatonic scale
1237
+ # @param grade [Integer, nil] background grade
1238
+ # @param octave [Integer, nil] background octave
1239
+ # @param sharps [Integer, nil] accidentals from background note
1240
+ # @return [NoteInScale] new note with background context
1241
+ #
1242
+ # @api private
468
1243
  def with_background(scale:, grade: nil, octave: nil, sharps: nil)
469
1244
  NoteInScale.new(@scale, @grade, @octave, @pitch,
470
1245
  background_scale: scale,
@@ -473,18 +1248,55 @@ module Musa
473
1248
  background_sharps: sharps)
474
1249
  end
475
1250
 
1251
+ # Background diatonic scale (for chromatic notes).
1252
+ # @return [Scale, nil]
476
1253
  attr_reader :background_scale
477
1254
 
1255
+ # Returns the diatonic note this chromatic note is based on.
1256
+ #
1257
+ # @return [NoteInScale, nil] background note or nil
1258
+ #
1259
+ # @example
1260
+ # c# = c_major.tonic.sharp
1261
+ # c#.background_note.pitch # => 60 (C natural)
478
1262
  def background_note
479
1263
  @background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
480
1264
  end
481
1265
 
1266
+ # Sharps/flats from background note.
1267
+ # @return [Integer, nil]
482
1268
  attr_reader :background_sharps
483
1269
 
1270
+ # Returns wide grade (grade + octave * grades_per_octave).
1271
+ #
1272
+ # @return [Integer]
1273
+ #
1274
+ # @example
1275
+ # note.wide_grade # => 7 (second octave, first degree)
1276
+ #
1277
+ # @api private
484
1278
  def wide_grade
485
1279
  @grade + @octave * @scale.kind.class.grades
486
1280
  end
487
1281
 
1282
+ # Navigates upward by interval.
1283
+ #
1284
+ # Supports both natural (diatonic) and chromatic (semitone) intervals.
1285
+ #
1286
+ # - **Numeric interval + :natural**: Move by scale degrees
1287
+ # - **Symbol or numeric interval + :chromatic**: Move by semitones or named interval
1288
+ #
1289
+ # @param interval_name_or_interval [Symbol, Integer] interval
1290
+ # @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
1291
+ # @param sign [Integer] direction multiplier (internal use)
1292
+ # @return [NoteInScale] note at interval above
1293
+ #
1294
+ # @example Natural interval (scale degrees)
1295
+ # note.up(2, :natural) # Up 2 scale degrees
1296
+ #
1297
+ # @example Chromatic interval (semitones)
1298
+ # note.up(:P5) # Up perfect fifth (7 semitones)
1299
+ # note.up(7) # Up 7 semitones (if chromatic)
488
1300
  def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)
489
1301
 
490
1302
  sign ||= 1
@@ -525,24 +1337,78 @@ module Musa
525
1337
 
526
1338
  private :calculate_note_of_pitch
527
1339
 
1340
+ # Navigates downward by interval.
1341
+ #
1342
+ # Same as {#up} but in reverse direction.
1343
+ #
1344
+ # @param interval_name_or_interval [Symbol, Integer] interval
1345
+ # @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
1346
+ # @return [NoteInScale] note at interval below
1347
+ #
1348
+ # @example
1349
+ # note.down(2, :natural) # Down 2 scale degrees
1350
+ # note.down(:P5) # Down perfect fifth
528
1351
  def down(interval_name_or_interval, natural_or_chromatic = nil)
529
1352
  up(interval_name_or_interval, natural_or_chromatic, sign: -1)
530
1353
  end
531
1354
 
1355
+ # Raises note by semitones (adds sharps).
1356
+ #
1357
+ # @param count [Integer, nil] number of semitones (default 1)
1358
+ # @return [NoteInScale] raised note
1359
+ #
1360
+ # @example
1361
+ # note.sharp.pitch # Up 1 semitone
1362
+ # note.sharp(2).pitch # Up 2 semitones
532
1363
  def sharp(count = nil)
533
1364
  count ||= 1
534
1365
  calculate_note_of_pitch(@pitch, count)
535
1366
  end
536
1367
 
1368
+ # Lowers note by semitones (adds flats).
1369
+ #
1370
+ # @param count [Integer, nil] number of semitones (default 1)
1371
+ # @return [NoteInScale] lowered note
1372
+ #
1373
+ # @example
1374
+ # note.flat.pitch # Down 1 semitone
1375
+ # note.flat(2).pitch # Down 2 semitones
537
1376
  def flat(count = nil)
538
1377
  count ||= 1
539
1378
  sharp(-count)
540
1379
  end
541
1380
 
1381
+ # Calculates frequency in Hz.
1382
+ #
1383
+ # Uses the scale system's frequency calculation (equal temperament,
1384
+ # just intonation, etc.) and the tuning's A frequency.
1385
+ #
1386
+ # @return [Float] frequency in Hz
1387
+ #
1388
+ # @example
1389
+ # c_major.tonic.frequency # => ~261.63 Hz (middle C at A=440)
542
1390
  def frequency
543
- @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root)
1391
+ @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
544
1392
  end
545
1393
 
1394
+ # Changes scale while keeping pitch, or returns current scale.
1395
+ #
1396
+ # **Without argument**: Returns current scale.
1397
+ #
1398
+ # **With argument**: Returns note at same pitch in different scale kind.
1399
+ #
1400
+ # @param kind_id_or_kind [Symbol, ScaleKind, nil] scale kind or ID
1401
+ # @return [Scale, NoteInScale] current scale or note in new scale
1402
+ #
1403
+ # @example Query current scale
1404
+ # note.scale # => <Scale: kind = MajorScaleKind ...>
1405
+ #
1406
+ # @example Change to minor
1407
+ # note.scale(:minor) # Same pitch in minor scale
1408
+ #
1409
+ # @example Dynamic method
1410
+ # note.minor # Same as note.scale(:minor)
1411
+ # note.major # Same as note.scale(:major)
546
1412
  def scale(kind_id_or_kind = nil)
547
1413
  if kind_id_or_kind.nil?
548
1414
  @scale
@@ -555,10 +1421,52 @@ module Musa
555
1421
  end
556
1422
  end
557
1423
 
1424
+ # Finds this note in another scale.
1425
+ #
1426
+ # Searches for a note with the same pitch in the target scale.
1427
+ #
1428
+ # @param scale [Scale] target scale to search
1429
+ # @return [NoteInScale, nil] note in target scale or nil
1430
+ #
1431
+ # @example
1432
+ # c_major_tonic = c_major.tonic
1433
+ # c_minor = tuning.minor[60]
1434
+ # c_major_tonic.on(c_minor) # C in C minor scale
558
1435
  def on(scale)
559
1436
  scale.note_of_pitch @pitch
560
1437
  end
561
1438
 
1439
+ # Builds a chord rooted on this note.
1440
+ #
1441
+ # Creates a chord using this note as the root. Chord can be specified by:
1442
+ # - Feature values (:triad, :seventh, :major, :minor, etc.)
1443
+ # - Feature hash (quality:, size:)
1444
+ # - Chord definition name (not shown here, see Chord.with_root)
1445
+ #
1446
+ # If no features specified, defaults to major triad.
1447
+ #
1448
+ # @param feature_values [Array<Symbol>] feature values (size, quality, etc.)
1449
+ # @param allow_chromatic [Boolean] allow non-diatonic chord notes
1450
+ # @param move [Hash{Symbol => Integer}] initial octave moves
1451
+ # @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
1452
+ # @param features_hash [Hash] feature key-value pairs
1453
+ # @return [Chord] chord rooted on this note
1454
+ #
1455
+ # @example Default triad
1456
+ # note.chord # Major triad
1457
+ #
1458
+ # @example Specified size
1459
+ # note.chord :seventh # Seventh chord matching scale
1460
+ # note.chord :ninth # Ninth chord
1461
+ #
1462
+ # @example With features
1463
+ # note.chord quality: :minor, size: :seventh
1464
+ # note.chord :minor, :seventh # Same as above
1465
+ #
1466
+ # @example With voicing
1467
+ # note.chord :seventh, move: {root: -1}, duplicate: {fifth: 1}
1468
+ #
1469
+ # @see Chord Chord class
562
1470
  def chord(*feature_values,
563
1471
  allow_chromatic: nil,
564
1472
  move: nil,
@@ -575,6 +1483,12 @@ module Musa
575
1483
  **features)
576
1484
  end
577
1485
 
1486
+ # Checks note equality.
1487
+ #
1488
+ # Notes are equal if they have same scale, grade, octave, and pitch.
1489
+ #
1490
+ # @param other [NoteInScale]
1491
+ # @return [Boolean]
578
1492
  def ==(other)
579
1493
  self.class == other.class &&
580
1494
  @scale == other.scale &&
@@ -583,6 +1497,9 @@ module Musa
583
1497
  @pitch == other.pitch
584
1498
  end
585
1499
 
1500
+ # Returns string representation.
1501
+ #
1502
+ # @return [String]
586
1503
  def inspect
587
1504
  "<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
588
1505
  end