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
@@ -3,10 +3,142 @@ require_relative 'chord-definition'
3
3
 
4
4
  module Musa
5
5
  module Chords
6
+ # Instantiated chord with specific root and scale context.
7
+ #
8
+ # Chord represents an actual chord instance with a root note, scale context,
9
+ # and chord definition. It provides access to chord tones, voicing modifications,
10
+ # and navigation between related chords.
11
+ #
12
+ # ## Creation
13
+ #
14
+ # Chords are typically created from scale notes rather than directly:
15
+ #
16
+ # scale = Scales::Scales.default_system.default_tuning.major[60]
17
+ # chord = scale.tonic.chord # C major triad
18
+ # chord = scale.tonic.chord :seventh # C major seventh
19
+ # chord = scale.dominant.chord :ninth # G ninth chord
20
+ #
21
+ # ## Accessing Chord Tones
22
+ #
23
+ # Chord tones are accessed by their position name (root, third, fifth, etc.):
24
+ #
25
+ # chord.root # Returns NoteInScale for root
26
+ # chord.third # Returns NoteInScale for third
27
+ # chord.fifth # Returns NoteInScale for fifth
28
+ # chord.seventh # Returns NoteInScale for seventh (if exists)
29
+ #
30
+ # When notes are duplicated, use `all: true` to get all instances:
31
+ #
32
+ # chord.root(all: true) # Returns array of all root notes
33
+ #
34
+ # ## Features and Navigation
35
+ #
36
+ # Chords have features (quality, size) and can navigate to related chords:
37
+ #
38
+ # chord.features # => { quality: :major, size: :triad }
39
+ # chord.quality # => :major (dynamic method)
40
+ # chord.size # => :triad (dynamic method)
41
+ #
42
+ # chord.with_quality(:minor) # Change to minor
43
+ # chord.with_size(:seventh) # Add seventh
44
+ # chord.featuring(size: :ninth) # Change multiple features
45
+ #
46
+ # ## Voicing Modifications
47
+ #
48
+ # ### Move - Relocate specific chord tones to different octaves:
49
+ #
50
+ # chord.move(root: -1, seventh: 1)
51
+ # # Root down one octave, seventh up one octave
52
+ #
53
+ # ### Duplicate - Add copies of chord tones in other octaves:
54
+ #
55
+ # chord.duplicate(root: -2, third: [-1, 1])
56
+ # # Add root 2 octaves down, third 1 octave down and 1 up
57
+ #
58
+ # ### Octave - Transpose entire chord:
59
+ #
60
+ # chord.octave(-1) # Move entire chord down one octave
61
+ #
62
+ # ## Pitch Extraction
63
+ #
64
+ # chord.pitches # All pitches sorted by pitch
65
+ # chord.pitches(:root, :third) # Only specified chord tones
66
+ # chord.notes # Sorted ChordGradeNote structs
67
+ #
68
+ # ## Scale Context
69
+ #
70
+ # Chords maintain their scale context. When navigating to chords with
71
+ # non-diatonic notes (e.g., major to minor), the scale may become nil:
72
+ #
73
+ # major_chord = c_major.tonic.chord
74
+ # major_chord.scale # => C major scale
75
+ #
76
+ # minor_chord = major_chord.with_quality(:minor)
77
+ # minor_chord.scale # => nil (Eb not in C major)
78
+ #
79
+ # @example Basic triad creation
80
+ # scale = Scales::Scales.default_system.default_tuning.major[60]
81
+ # chord = scale.tonic.chord
82
+ # chord.root.pitch # => 60 (C)
83
+ # chord.third.pitch # => 64 (E)
84
+ # chord.fifth.pitch # => 67 (G)
85
+ #
86
+ # @example Seventh chord
87
+ # chord = scale.tonic.chord :seventh
88
+ # chord.seventh.pitch # => 71 (B)
89
+ #
90
+ # @example Voicing with move and duplicate
91
+ # scale = Scales::Scales.default_system.default_tuning.major[60]
92
+ # chord = scale.dominant.chord(:seventh)
93
+ # .move(root: -1, third: -1)
94
+ # .duplicate(fifth: [0, 1])
95
+ #
96
+ # @example Feature navigation
97
+ # scale = Scales::Scales.default_system.default_tuning.major[60]
98
+ # maj_triad = scale.tonic.chord
99
+ # min_triad = maj_triad.with_quality(:minor)
100
+ # maj_seventh = maj_triad.with_size(:seventh)
101
+ #
102
+ # @see ChordDefinition Chord template/definition
103
+ # @see NoteInScale Note within scale
104
+ # @see chord-definitions.rb Standard chord types
6
105
  class Chord
7
106
 
8
107
  using Musa::Extension::Arrayfy
9
108
 
109
+ # Creates a chord with specified root.
110
+ #
111
+ # Factory method for creating chords by specifying the root note and either
112
+ # a chord definition name or features. The root can be a NoteInScale, pitch
113
+ # number, or scale degree symbol.
114
+ #
115
+ # @param root_note_or_pitch_or_symbol [NoteInScale, Integer, Symbol] chord root
116
+ # - NoteInScale: use note directly
117
+ # - Integer (MIDI pitch): find note in scale, or create C major if no scale
118
+ # - Symbol (scale degree): requires scale parameter (e.g., :tonic, :dominant)
119
+ # @param scale [Scale, nil] scale context for finding notes
120
+ # @param allow_chromatic [Boolean] allow non-diatonic notes
121
+ # @param name [Symbol, nil] chord definition name (:maj, :min7, etc.)
122
+ # @param move [Hash{Symbol => Integer}, nil] initial octave moves (e.g., `{root: -1}`)
123
+ # @param duplicate [Hash{Symbol => Integer, Array<Integer>}, nil] initial duplications
124
+ # @param features [Hash] chord features if not using name (quality:, size:, etc.)
125
+ # @return [Chord] new chord instance
126
+ #
127
+ # @example With note from scale
128
+ # Chord.with_root(scale.tonic, name: :maj7)
129
+ #
130
+ # @example With MIDI pitch and scale
131
+ # Chord.with_root(60, scale: c_major, name: :min)
132
+ #
133
+ # @example With scale degree
134
+ # Chord.with_root(:dominant, scale: c_major, quality: :dominant, size: :seventh)
135
+ #
136
+ # @example With features instead of name
137
+ # Chord.with_root(60, scale: c_major, quality: :major, size: :triad)
138
+ #
139
+ # @example With voicing parameters
140
+ # Chord.with_root(60, scale: c_major, name: :maj7,
141
+ # move: {root: -1}, duplicate: {fifth: 1})
10
142
  def self.with_root(root_note_or_pitch_or_symbol, scale: nil, allow_chromatic: false, name: nil, move: nil, duplicate: nil, **features)
11
143
  root =
12
144
  case root_note_or_pitch_or_symbol
@@ -53,7 +185,21 @@ module Musa
53
185
  Chord.new(root, scale, chord_definition, move, duplicate, source_notes_map)
54
186
  end
55
187
 
188
+ # Internal helper methods for chord construction.
189
+ #
190
+ # @api private
56
191
  class Helper
192
+ # Computes the source notes map for a chord.
193
+ #
194
+ # Maps each chord position (root, third, fifth, etc.) to its corresponding
195
+ # note in the scale or chromatic scale.
196
+ #
197
+ # @param root [NoteInScale] chord root note
198
+ # @param chord_definition [ChordDefinition] chord structure
199
+ # @param scale [Scale] scale context
200
+ # @return [Hash{Symbol => Array<NoteInScale>}] position to notes mapping
201
+ #
202
+ # @api private
57
203
  def self.compute_source_notes_map(root, chord_definition, scale)
58
204
  chord_definition.pitch_offsets.transform_values do |offset|
59
205
  pitch = root.pitch + offset
@@ -61,6 +207,18 @@ module Musa
61
207
  end.tap { |_| _.values.each(&:freeze) }.freeze
62
208
  end
63
209
 
210
+ # Finds a chord definition matching features and scale constraints.
211
+ #
212
+ # Searches for chord definitions with specified features, filtering out
213
+ # those that don't fit in the scale unless allow_chromatic is true.
214
+ #
215
+ # @param root_pitch [Integer] MIDI pitch of chord root
216
+ # @param features [Hash] desired chord features (quality:, size:, etc.)
217
+ # @param scale [Scale] scale context for diatonic filtering
218
+ # @param allow_chromatic [Boolean] allow non-diatonic chords
219
+ # @return [ChordDefinition, nil] matching definition or nil
220
+ #
221
+ # @api private
64
222
  def self.find_definition_by_features(root_pitch, features, scale, allow_chromatic:)
65
223
  featured_chord_definitions = ChordDefinition.find_by_features(**features)
66
224
 
@@ -76,10 +234,33 @@ module Musa
76
234
 
77
235
  private_constant :Helper
78
236
 
237
+ # Container for chord tone with its position name.
238
+ #
239
+ # Associates a chord position (grade) with its corresponding note.
240
+ # Used internally for sorting and organizing chord notes.
241
+ #
242
+ # @!attribute grade
243
+ # @return [Symbol] position name (:root, :third, :fifth, etc.)
244
+ # @!attribute note
245
+ # @return [NoteInScale] the note at this position
246
+ #
247
+ # @api private
79
248
  ChordGradeNote = Struct.new(:grade, :note, keyword_init: true)
80
249
 
81
250
  private_constant :ChordGradeNote
82
251
 
252
+ # Creates a chord (private constructor).
253
+ #
254
+ # Use {with_root} or create chords from scale notes instead.
255
+ #
256
+ # @param root [NoteInScale] chord root note
257
+ # @param scale [Scale, nil] scale context (nil if chromatic notes present)
258
+ # @param chord_definition [ChordDefinition] chord structure
259
+ # @param move [Hash, nil] octave moves for positions
260
+ # @param duplicate [Hash, nil] octave duplications for positions
261
+ # @param source_notes_map [Hash] position to notes mapping
262
+ #
263
+ # @api private
83
264
  private def initialize(root, scale, chord_definition, move, duplicate, source_notes_map)
84
265
  @root = root
85
266
  @scale = scale
@@ -131,21 +312,82 @@ module Musa
131
312
  end
132
313
  end
133
314
 
134
- attr_reader :scale, :chord_definition, :move, :duplicate
135
-
315
+ # Scale context (nil if chord contains non-diatonic notes).
316
+ # @return [Scale, nil]
317
+ attr_reader :scale
318
+
319
+ # Chord definition template.
320
+ # @return [ChordDefinition]
321
+ attr_reader :chord_definition
322
+
323
+ # Octave moves applied to positions.
324
+ # @return [Hash{Symbol => Integer}]
325
+ attr_reader :move
326
+
327
+ # Octave duplications applied to positions.
328
+ # @return [Hash{Symbol => Integer, Array<Integer>}]
329
+ attr_reader :duplicate
330
+
331
+ # Returns chord notes sorted by pitch.
332
+ #
333
+ # @return [Array<ChordGradeNote>] sorted array of grade-note pairs
334
+ #
335
+ # @example
336
+ # chord.notes.each do |chord_grade_note|
337
+ # puts "#{chord_grade_note.grade}: #{chord_grade_note.note.pitch}"
338
+ # end
136
339
  def notes
137
340
  @sorted_notes
138
341
  end
139
342
 
343
+ # Returns MIDI pitches of chord notes.
344
+ #
345
+ # Without arguments, returns all pitches sorted from low to high.
346
+ # With grade arguments, returns only pitches for those positions.
347
+ #
348
+ # @param grades [Array<Symbol>] optional position names to filter
349
+ # @return [Array<Integer>] MIDI pitches sorted by pitch
350
+ #
351
+ # @example All pitches
352
+ # chord.pitches # => [60, 64, 67]
353
+ #
354
+ # @example Specific positions
355
+ # chord.pitches(:root, :third) # => [60, 64]
140
356
  def pitches(*grades)
141
357
  grades = @notes_map.keys if grades.empty?
142
358
  @sorted_notes.select { |_| grades.include?(_.grade) }.collect { |_| _.note.pitch }
143
359
  end
144
360
 
361
+ # Returns chord features.
362
+ #
363
+ # @return [Hash{Symbol => Symbol}] features hash (quality:, size:, etc.)
364
+ #
365
+ # @example
366
+ # chord.features # => { quality: :major, size: :triad }
145
367
  def features
146
368
  @chord_definition.features
147
369
  end
148
370
 
371
+ # Creates new chord with modified features.
372
+ #
373
+ # Returns a new chord with the same root but different features.
374
+ # Features can be specified as values (converted to feature hash) or
375
+ # as keyword arguments.
376
+ #
377
+ # @param values [Array<Symbol>] feature values to change
378
+ # @param allow_chromatic [Boolean] allow non-diatonic result
379
+ # @param hash [Hash] feature key-value pairs to change
380
+ # @return [Chord] new chord with modified features
381
+ # @raise [ArgumentError] if no matching chord definition found
382
+ #
383
+ # @example Change size
384
+ # chord.featuring(size: :seventh)
385
+ #
386
+ # @example Change quality
387
+ # chord.featuring(quality: :minor)
388
+ #
389
+ # @example Change multiple features
390
+ # chord.featuring(quality: :dominant, size: :ninth)
149
391
  def featuring(*values, allow_chromatic: false, **hash)
150
392
  # create a new list of features based on current features but
151
393
  # replacing the values for the new ones and adding the new features
@@ -166,6 +408,19 @@ module Musa
166
408
  source_notes_map)
167
409
  end
168
410
 
411
+ # Transposes entire chord to a different octave.
412
+ #
413
+ # Moves all chord notes by the specified octave offset, preserving
414
+ # internal voicing structure (moves and duplications).
415
+ #
416
+ # @param octave [Integer] octave offset (positive = up, negative = down)
417
+ # @return [Chord] new chord in different octave
418
+ #
419
+ # @example Move chord down one octave
420
+ # chord.octave(-1)
421
+ #
422
+ # @example Move chord up two octaves
423
+ # chord.octave(2)
169
424
  def octave(octave)
170
425
  source_notes_map = @source_notes_map.transform_values do |notes|
171
426
  notes.collect { |note| note.octave(octave) }.freeze
@@ -174,26 +429,122 @@ module Musa
174
429
  Chord.new(@root.octave(octave), @scale, chord_definition, @move, @duplicate, source_notes_map)
175
430
  end
176
431
 
432
+ # Creates new chord with positions moved to different octaves, or returns current move settings.
433
+ #
434
+ # When called with arguments, relocates specific chord positions to different
435
+ # octaves while keeping other positions unchanged. Multiple positions can be
436
+ # moved at once. Merges with existing moves.
437
+ #
438
+ # When called without arguments, returns the current move settings hash.
439
+ #
440
+ # @param octaves [Hash{Symbol => Integer}] position to octave offset mapping
441
+ # @return [Chord, Hash] new chord with moved positions, or current move settings if no arguments
442
+ #
443
+ # @example Move root down, seventh up
444
+ # chord.move(root: -1, seventh: 1)
445
+ #
446
+ # @example Drop voicing (move third and seventh down)
447
+ # chord.move(third: -1, seventh: -1)
448
+ #
449
+ # @example Get current move settings
450
+ # chord.move # => { root: -1 }
177
451
  def move(**octaves)
452
+ return @move if octaves.empty?
453
+
178
454
  Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
179
455
  end
180
456
 
457
+ # Creates new chord with positions duplicated in other octaves, or returns current duplicate settings.
458
+ #
459
+ # When called with arguments, adds copies of specific chord positions in
460
+ # different octaves. Original positions remain at their current octave.
461
+ # Merges with existing duplications.
462
+ #
463
+ # When called without arguments, returns the current duplicate settings hash.
464
+ #
465
+ # @param octaves [Hash{Symbol => Integer, Array<Integer>}] position to octave(s)
466
+ # @return [Chord, Hash] new chord with duplicated positions, or current duplicate settings if no arguments
467
+ #
468
+ # @example Duplicate root two octaves down
469
+ # chord.duplicate(root: -2)
470
+ #
471
+ # @example Duplicate third in multiple octaves
472
+ # chord.duplicate(third: [-1, 1])
473
+ #
474
+ # @example Duplicate multiple positions
475
+ # chord.duplicate(root: -1, fifth: 1)
476
+ #
477
+ # @example Get current duplicate settings
478
+ # chord.duplicate # => { root: -1 }
181
479
  def duplicate(**octaves)
480
+ return @duplicate if octaves.empty?
481
+
182
482
  Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
183
483
  end
184
484
 
485
+ # Finds this chord in other scales.
486
+ #
487
+ # Searches through scale kinds matching the given metadata criteria to find
488
+ # all scales that contain this chord. Returns new chord instances, each with
489
+ # its containing scale as context.
490
+ #
491
+ # @param roots [Range, Array, nil] pitch offsets to search (default: full octave)
492
+ # @param metadata [Hash] metadata filters for scale kinds (family:, brightness:, etc.)
493
+ # @return [Array<Chord>] this chord in different scale contexts
494
+ #
495
+ # @example Find G major triad in diatonic scales
496
+ # g_triad = c_major.dominant.chord
497
+ # g_triad.in_scales(family: :diatonic)
498
+ #
499
+ # @example Find chord in scales with specific brightness
500
+ # g7.in_scales(brightness: -1..1)
501
+ #
502
+ # @example Iterate over results
503
+ # g7.in_scales(family: :greek_modes).each do |chord|
504
+ # scale = chord.scale
505
+ # degree = scale.degree_of_chord(chord)
506
+ # puts "#{scale.kind.class.id} on #{scale.root_pitch}: degree #{degree}"
507
+ # end
508
+ #
509
+ # @see Musa::Scales::Scale#chord_on
510
+ # @see Musa::Scales::ScaleSystemTuning#chords_of
511
+ def in_scales(roots: nil, **metadata)
512
+ tuning = @scale&.kind&.tuning || @root.scale.kind.tuning
513
+ tuning.chords_of(self, roots: roots, **metadata)
514
+ end
515
+
516
+ # Checks chord equality.
517
+ #
518
+ # Chords are equal if they have the same notes and chord definition.
519
+ #
520
+ # @param other [Chord] chord to compare
521
+ # @return [Boolean] true if chords are equal
185
522
  def ==(other)
186
523
  self.class == other.class &&
187
524
  @sorted_notes == other.notes &&
188
525
  @chord_definition == other.chord_definition
189
526
  end
190
527
 
528
+ # Returns string representation.
529
+ #
530
+ # @return [String]
191
531
  def inspect
192
532
  "<Chord #{@name} root #{@root} notes #{@sorted_notes.collect { |_| "#{_.grade}=#{_.note.grade}|#{_.note.pitch} "} }>"
193
533
  end
194
534
 
195
535
  alias to_s inspect
196
536
 
537
+ # Applies move and duplicate operations to notes map.
538
+ #
539
+ # Computes the final notes map after applying octave moves and duplications
540
+ # to the source notes.
541
+ #
542
+ # @param notes_map [Hash] source notes map
543
+ # @param moved [Hash, nil] octave moves for positions
544
+ # @param duplicated [Hash, nil] octave duplications for positions
545
+ # @return [Hash{Symbol => Array<NoteInScale>}] final notes map
546
+ #
547
+ # @api private
197
548
  private def compute_moved_and_duplicated(notes_map, moved, duplicated)
198
549
  notes_map = notes_map.transform_values(&:dup)
199
550