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
@@ -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,77 @@ 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.
433
+ #
434
+ # Relocates specific chord positions to different octaves while keeping
435
+ # other positions unchanged. Multiple positions can be moved at once.
436
+ # Merges with existing moves.
437
+ #
438
+ # @param octaves [Hash{Symbol => Integer}] position to octave offset mapping
439
+ # @return [Chord] new chord with moved positions
440
+ #
441
+ # @example Move root down, seventh up
442
+ # chord.move(root: -1, seventh: 1)
443
+ #
444
+ # @example Drop voicing (move third and seventh down)
445
+ # chord.move(third: -1, seventh: -1)
177
446
  def move(**octaves)
178
447
  Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
179
448
  end
180
449
 
450
+ # Creates new chord with positions duplicated in other octaves.
451
+ #
452
+ # Adds copies of specific chord positions in different octaves.
453
+ # Original positions remain at their current octave.
454
+ # Merges with existing duplications.
455
+ #
456
+ # @param octaves [Hash{Symbol => Integer, Array<Integer>}] position to octave(s)
457
+ # @return [Chord] new chord with duplicated positions
458
+ #
459
+ # @example Duplicate root two octaves down
460
+ # chord.duplicate(root: -2)
461
+ #
462
+ # @example Duplicate third in multiple octaves
463
+ # chord.duplicate(third: [-1, 1])
464
+ #
465
+ # @example Duplicate multiple positions
466
+ # chord.duplicate(root: -1, fifth: 1)
181
467
  def duplicate(**octaves)
182
468
  Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
183
469
  end
184
470
 
471
+ # Checks chord equality.
472
+ #
473
+ # Chords are equal if they have the same notes and chord definition.
474
+ #
475
+ # @param other [Chord] chord to compare
476
+ # @return [Boolean] true if chords are equal
185
477
  def ==(other)
186
478
  self.class == other.class &&
187
479
  @sorted_notes == other.notes &&
188
480
  @chord_definition == other.chord_definition
189
481
  end
190
482
 
483
+ # Returns string representation.
484
+ #
485
+ # @return [String]
191
486
  def inspect
192
487
  "<Chord #{@name} root #{@root} notes #{@sorted_notes.collect { |_| "#{_.grade}=#{_.note.grade}|#{_.note.pitch} "} }>"
193
488
  end
194
489
 
195
490
  alias to_s inspect
196
491
 
492
+ # Applies move and duplicate operations to notes map.
493
+ #
494
+ # Computes the final notes map after applying octave moves and duplications
495
+ # to the source notes.
496
+ #
497
+ # @param notes_map [Hash] source notes map
498
+ # @param moved [Hash, nil] octave moves for positions
499
+ # @param duplicated [Hash, nil] octave duplications for positions
500
+ # @return [Hash{Symbol => Array<NoteInScale>}] final notes map
501
+ #
502
+ # @api private
197
503
  private def compute_moved_and_duplicated(notes_map, moved, duplicated)
198
504
  notes_map = notes_map.transform_values(&:dup)
199
505