musa-dsl 0.40.0 → 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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Gemfile +0 -1
  4. data/docs/subsystems/music.md +326 -15
  5. data/lib/musa-dsl/generative/darwin.rb +36 -1
  6. data/lib/musa-dsl/generative/generative-grammar.rb +28 -0
  7. data/lib/musa-dsl/generative/markov.rb +2 -0
  8. data/lib/musa-dsl/generative/rules.rb +54 -0
  9. data/lib/musa-dsl/generative/variatio.rb +69 -0
  10. data/lib/musa-dsl/midi/midi-recorder.rb +4 -0
  11. data/lib/musa-dsl/midi/midi-voices.rb +10 -0
  12. data/lib/musa-dsl/music/chords.rb +54 -9
  13. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -521
  14. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  15. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  16. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  17. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  18. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  19. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  20. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  21. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  22. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  23. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  24. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  25. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  26. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  27. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  28. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  29. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  30. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  31. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  32. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  33. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  34. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  35. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  36. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  37. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  38. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  39. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  40. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  41. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  42. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  43. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  44. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  45. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  46. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  47. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  48. data/lib/musa-dsl/music/scales.rb +427 -0
  49. data/lib/musa-dsl/series/buffer-serie.rb +6 -0
  50. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +23 -0
  51. data/lib/musa-dsl/series/quantizer-serie.rb +12 -0
  52. data/lib/musa-dsl/series/queue-serie.rb +13 -0
  53. data/lib/musa-dsl/version.rb +2 -1
  54. data/musa-dsl.gemspec +20 -15
  55. metadata +85 -22
@@ -149,6 +149,8 @@ module Musa
149
149
  # cut 'validate' { |obj| prune if invalid?(obj) }
150
150
  # ended_when { |obj| complete?(obj) }
151
151
  # end
152
+ #
153
+ # @return [void]
152
154
  def initialize(&block)
153
155
  @dsl = RulesEvalContext.new(&block)
154
156
  end
@@ -257,6 +259,7 @@ module Musa
257
259
  # @return [Array<CutRule>] cut rules
258
260
  attr_reader :_grow_rules, :_ended_when, :_cut_rules
259
261
 
262
+ # @return [void]
260
263
  # @api private
261
264
  def initialize(&block)
262
265
  @_grow_rules = []
@@ -296,10 +299,24 @@ module Musa
296
299
  self
297
300
  end
298
301
 
302
+ # Checks if end condition is defined.
303
+ #
304
+ # @return [Boolean] true if ended_when was called
305
+ #
306
+ # @api private
299
307
  def _has_ending?
300
308
  !@_ended_when.nil?
301
309
  end
302
310
 
311
+ # Evaluates end condition for object.
312
+ #
313
+ # @param object [Object] object to check
314
+ # @param history [Array] object history
315
+ # @param parameters [Hash] additional parameters
316
+ #
317
+ # @return [Boolean] true if object should end
318
+ #
319
+ # @api private
303
320
  def _ended?(object, history, **parameters)
304
321
  if @_ended_when
305
322
  with object, history, **parameters, &@_ended_when
@@ -311,11 +328,21 @@ module Musa
311
328
  class GrowRule
312
329
  attr_reader :name
313
330
 
331
+ # @param name [String] rule name
332
+ #
333
+ # @return [void]
314
334
  def initialize(name, &block)
315
335
  @name = name
316
336
  @block = block
317
337
  end
318
338
 
339
+ # Generates possible branched objects.
340
+ #
341
+ # @param object [Object] object to branch
342
+ # @param history [Array] object history
343
+ # @param parameters [Hash] additional parameters
344
+ #
345
+ # @return [Array<Object>] branched objects
319
346
  def generate_possibilities(object, history, **parameters)
320
347
  # TODO: optimize context using only one instance for all genereate_possibilities calls
321
348
  context = GrowRuleEvalContext.new
@@ -329,10 +356,16 @@ module Musa
329
356
 
330
357
  attr_reader :_branches
331
358
 
359
+ # @return [void]
332
360
  def initialize
333
361
  @_branches = []
334
362
  end
335
363
 
364
+ # Records a branched object.
365
+ #
366
+ # @param object [Object] branched object
367
+ #
368
+ # @return [self]
336
369
  def branch(object)
337
370
  @_branches << object
338
371
  self
@@ -347,11 +380,21 @@ module Musa
347
380
  class CutRule
348
381
  attr_reader :reason
349
382
 
383
+ # @param reason [String] rejection reason
384
+ #
385
+ # @return [void]
350
386
  def initialize(reason, &block)
351
387
  @reason = reason
352
388
  @block = block
353
389
  end
354
390
 
391
+ # Checks if object should be rejected.
392
+ #
393
+ # @param object [Object] object to check
394
+ # @param history [Array] object history
395
+ # @param parameters [Hash] additional parameters
396
+ #
397
+ # @return [Array<String>, nil] rejection reasons or nil if not rejected
355
398
  def rejects?(object, history, **parameters)
356
399
  # TODO: optimize context using only one instance for all rejects? checks
357
400
  context = CutRuleEvalContext.new
@@ -367,10 +410,16 @@ module Musa
367
410
 
368
411
  attr_reader :_secondary_reasons
369
412
 
413
+ # @return [void]
370
414
  def initialize
371
415
  @_secondary_reasons = []
372
416
  end
373
417
 
418
+ # Marks object for pruning.
419
+ #
420
+ # @param secondary_reason [String, nil] additional reason detail
421
+ #
422
+ # @return [self]
374
423
  def prune(secondary_reason = nil)
375
424
  @_secondary_reasons << secondary_reason
376
425
  self
@@ -399,6 +448,11 @@ module Musa
399
448
  class Node
400
449
  attr_reader :parent, :children, :object, :rejected
401
450
 
451
+ # @param object [Object, nil] node object
452
+ # @param parent [Node, nil] parent node
453
+ #
454
+ # @return [void]
455
+ #
402
456
  # @api private
403
457
  def initialize(object = nil, parent = nil)
404
458
  @parent = parent
@@ -143,6 +143,8 @@ module Musa
143
143
  # field :y, [:a, :b]
144
144
  # constructor { |x:, y:| { x: x, y: y } }
145
145
  # end
146
+ #
147
+ # @return [void]
146
148
  def initialize(instance_name, &block)
147
149
  raise ArgumentError, 'instance_name should be a symbol' unless instance_name.is_a?(Symbol)
148
150
  raise ArgumentError, 'block is needed' unless block
@@ -259,6 +261,11 @@ module Musa
259
261
 
260
262
  private
261
263
 
264
+ # Generates evaluation tree for parameter calculation.
265
+ #
266
+ # @param fieldset [Fieldset] fieldset to process
267
+ #
268
+ # @return [A, nil] root node of evaluation tree
262
269
  def generate_eval_tree_A(fieldset)
263
270
  root = nil
264
271
  current = nil
@@ -279,6 +286,11 @@ module Musa
279
286
  root
280
287
  end
281
288
 
289
+ # Generates evaluation tree for attribute application.
290
+ #
291
+ # @param fieldset [Fieldset] fieldset to process
292
+ #
293
+ # @return [B] root node of attribute tree
282
294
  def generate_eval_tree_B(fieldset)
283
295
  affected_field_names = []
284
296
  inner = []
@@ -298,12 +310,19 @@ module Musa
298
310
  attr_reader :parameter_name, :options
299
311
  attr_accessor :inner
300
312
 
313
+ # @param parameter_name [Symbol] parameter name
314
+ # @param options [Array] option values
315
+ #
316
+ # @return [void]
301
317
  def initialize(parameter_name, options)
302
318
  @parameter_name = parameter_name
303
319
  @options = options
304
320
  @inner = nil
305
321
  end
306
322
 
323
+ # Calculates all parameter combinations.
324
+ #
325
+ # @return [Array<Hash>] parameter combination hashes
307
326
  def calc_parameters
308
327
  unless @calc_parameters
309
328
  if inner
@@ -320,16 +339,19 @@ module Musa
320
339
  private_constant :A
321
340
 
322
341
  class A1 < A
342
+ # @return [void]
323
343
  def initialize(parameter_name, options)
324
344
  super parameter_name, options
325
345
 
326
346
  @own_parameters = @options.collect { |option| { @parameter_name => option } }
327
347
  end
328
348
 
349
+ # @return [Array<Hash>] own parameter combinations
329
350
  def calc_own_parameters
330
351
  @own_parameters
331
352
  end
332
353
 
354
+ # @return [String] string representation
333
355
  def inspect
334
356
  "A1 name: #{@parameter_name}, options: #{@options}, inner: #{@inner || 'nil'}"
335
357
  end
@@ -340,6 +362,11 @@ module Musa
340
362
  private_constant :A1
341
363
 
342
364
  class A2 < A
365
+ # @param parameter_name [Symbol] parameter name
366
+ # @param options [Array] option values
367
+ # @param subcomponent [A] nested tree
368
+ #
369
+ # @return [void]
343
370
  def initialize(parameter_name, options, subcomponent)
344
371
  super parameter_name, options
345
372
 
@@ -361,10 +388,12 @@ module Musa
361
388
  @own_parameters = result
362
389
  end
363
390
 
391
+ # @return [Array<Hash>] own parameter combinations
364
392
  def calc_own_parameters
365
393
  @own_parameters
366
394
  end
367
395
 
396
+ # @return [String] string representation
368
397
  def inspect
369
398
  "A2 name: #{@parameter_name}, options: #{@options}, subcomponent: #{@subcomponent}, inner: #{@inner || 'nil'}"
370
399
  end
@@ -383,6 +412,13 @@ module Musa
383
412
  class B
384
413
  attr_reader :parameter_name, :options, :affected_field_names, :blocks, :inner
385
414
 
415
+ # @param parameter_name [Symbol] parameter name
416
+ # @param options [Array] option values
417
+ # @param affected_field_names [Array<Symbol>] field names affected
418
+ # @param inner [Array<B>] nested B nodes
419
+ # @param blocks [Array<Proc>] with_attributes blocks
420
+ #
421
+ # @return [void]
386
422
  def initialize(parameter_name, options, affected_field_names, inner, blocks)
387
423
  @parameter_name = parameter_name
388
424
  @options = options
@@ -392,6 +428,12 @@ module Musa
392
428
  @procedures = blocks.collect { |proc| Musa::Extension::SmartProcBinder::SmartProcBinder.new proc }
393
429
  end
394
430
 
431
+ # Runs attribute application for this node.
432
+ #
433
+ # @param parameters_with_depth [Hash] parameters with nesting depth
434
+ # @param parent_parameters [Hash, nil] parent context parameters
435
+ #
436
+ # @return [void]
395
437
  def run(parameters_with_depth, parent_parameters = nil)
396
438
  parent_parameters ||= {}
397
439
 
@@ -417,6 +459,7 @@ module Musa
417
459
  end
418
460
  end
419
461
 
462
+ # @return [String] string representation
420
463
  def inspect
421
464
  "B name: #{@parameter_name}, options: #{@options}, affected_field_names: #{@affected_field_names}, blocks_size: #{@blocks.size}, inner: #{@inner}"
422
465
  end
@@ -435,6 +478,10 @@ module Musa
435
478
  # @return [Fieldset] defined fieldset
436
479
  attr_reader :_fieldset
437
480
 
481
+ # @param name [Symbol] fieldset name
482
+ # @param options [Array, Range, nil] fieldset options
483
+ #
484
+ # @return [void]
438
485
  # @api private
439
486
  def initialize(name, options = nil, &block)
440
487
  @_fieldset = Fieldset.new name, options.arrayfy.explode_ranges
@@ -446,6 +493,8 @@ module Musa
446
493
  #
447
494
  # @param name [Symbol] field name
448
495
  # @param options [Array, Range, nil] field option values
496
+ #
497
+ # @return [void]
449
498
  # @api private
450
499
  def field(name, options = nil)
451
500
  @_fieldset.components << Field.new(name, options.arrayfy.explode_ranges)
@@ -455,7 +504,10 @@ module Musa
455
504
  #
456
505
  # @param name [Symbol] fieldset name
457
506
  # @param options [Array, Range, nil] fieldset option values
507
+ #
458
508
  # @yield fieldset DSL block
509
+ #
510
+ # @return [void]
459
511
  # @api private
460
512
  def fieldset(name, options = nil, &block)
461
513
  fieldset_context = FieldsetContext.new name, options, &block
@@ -465,6 +517,8 @@ module Musa
465
517
  # Adds attribute modification block.
466
518
  #
467
519
  # @yield attribute modification block
520
+ #
521
+ # @return [void]
468
522
  # @api private
469
523
  def with_attributes(&block)
470
524
  @_fieldset.with_attributes << block
@@ -481,6 +535,7 @@ module Musa
481
535
  # @return [Proc, nil] finalize block
482
536
  attr_reader :_constructor, :_finalize
483
537
 
538
+ # @return [void]
484
539
  # @api private
485
540
  def initialize(&block)
486
541
  @_constructor = nil
@@ -492,6 +547,8 @@ module Musa
492
547
  # Defines object constructor.
493
548
  #
494
549
  # @yield constructor block receiving field values
550
+ #
551
+ # @return [void]
495
552
  # @api private
496
553
  def constructor(&block)
497
554
  @_constructor = block
@@ -500,6 +557,8 @@ module Musa
500
557
  # Defines finalize block.
501
558
  #
502
559
  # @yield finalize block receiving completed object
560
+ #
561
+ # @return [void]
503
562
  # @api private
504
563
  def finalize(&block)
505
564
  @_finalize = block
@@ -512,6 +571,7 @@ module Musa
512
571
  attr_reader :name
513
572
  attr_accessor :options
514
573
 
574
+ # @return [String] string representation
515
575
  def inspect
516
576
  "Field #{@name} options: #{@options}"
517
577
  end
@@ -520,6 +580,10 @@ module Musa
520
580
 
521
581
  private
522
582
 
583
+ # @param name [Symbol] field name
584
+ # @param options [Array] option values
585
+ #
586
+ # @return [void]
523
587
  def initialize(name, options)
524
588
  @name = name
525
589
  @options = options
@@ -532,6 +596,7 @@ module Musa
532
596
  attr_reader :name, :with_attributes, :components
533
597
  attr_accessor :options
534
598
 
599
+ # @return [String] string representation
535
600
  def inspect
536
601
  "Fieldset #{@name} options: #{@options} components: #{@components}"
537
602
  end
@@ -540,6 +605,10 @@ module Musa
540
605
 
541
606
  private
542
607
 
608
+ # @param name [Symbol] fieldset name
609
+ # @param options [Array, nil] option values
610
+ #
611
+ # @return [void]
543
612
  def initialize(name, options)
544
613
  @name = name
545
614
  @options = options || [nil]
@@ -79,6 +79,8 @@ module Musa
79
79
  # @see https://github.com/javier-sy/midi-events MIDIEvents documentation
80
80
  class MIDIRecorder
81
81
  # @param sequencer [Musa::Sequencer::Sequencer] provides the musical position for each recorded message.
82
+ #
83
+ # @return [void]
82
84
  def initialize(sequencer)
83
85
  @sequencer = sequencer
84
86
  @midi_parser = MIDIParser.new
@@ -179,6 +181,8 @@ module Musa
179
181
  #
180
182
  # @param position [Rational] sequencer position when captured.
181
183
  # @param message [MIDIEvents::Event] parsed MIDI event.
184
+ #
185
+ # @return [void]
182
186
  def initialize(position, message)
183
187
  @position = position
184
188
  @message = message
@@ -97,6 +97,8 @@ module Musa
97
97
  # @param output [#puts, nil] anything responding to `puts` that accepts `MIDIEvents::Event`s (typically a MIDICommunications output).
98
98
  # @param channels [Array<Numeric>, Range, Numeric] list of MIDI channels to control. Ranges are expanded automatically.
99
99
  # @param do_log [Boolean] enables info level logs per emitted message.
100
+ #
101
+ # @return [void]
100
102
  def initialize(sequencer:, output:, channels:, do_log: nil)
101
103
  do_log ||= false
102
104
 
@@ -200,6 +202,8 @@ module Musa
200
202
  # @param output [#puts, nil]
201
203
  # @param channel [Integer] MIDI channel number (0-15).
202
204
  # @param name [String, nil] human friendly identifier.
205
+ #
206
+ # @return [void]
203
207
  def initialize(sequencer:, output:, channel:, name: nil, do_log: nil)
204
208
  do_log ||= false
205
209
 
@@ -337,6 +341,8 @@ module Musa
337
341
  class ControllersControl
338
342
  # @param output [#puts] MIDI output.
339
343
  # @param channel [Integer] MIDI channel number.
344
+ #
345
+ # @return [void]
340
346
  def initialize(output, channel)
341
347
  @output = output
342
348
  @channel = channel
@@ -368,6 +374,8 @@ module Musa
368
374
  #
369
375
  # @param controller_number_or_symbol [Integer, Symbol] CC number or well-known alias (see +@controller_map+).
370
376
  # @param value [Integer] byte value that will be clamped to 0-127.
377
+ #
378
+ # @return [Integer] clamped value
371
379
  def []=(controller_number_or_symbol, value)
372
380
  number = number_of(controller_number_or_symbol)
373
381
  value ||= 0
@@ -429,6 +437,8 @@ module Musa
429
437
  # @param velocity [Numeric, Array<Numeric>] on velocity (can be per-note).
430
438
  # @param duration [Numeric, nil] duration in bars or nil for infinite.
431
439
  # @param velocity_off [Numeric, Array<Numeric>] release velocity.
440
+ #
441
+ # @return [void]
432
442
  def initialize(voice, pitch:, velocity: nil, duration: nil, velocity_off: nil)
433
443
  raise ArgumentError, "MIDIVoice: note duration should be nil or Numeric: #{duration} (#{duration.class})" unless duration.nil? || duration.is_a?(Numeric)
434
444
 
@@ -429,32 +429,41 @@ module Musa
429
429
  Chord.new(@root.octave(octave), @scale, chord_definition, @move, @duplicate, source_notes_map)
430
430
  end
431
431
 
432
- # Creates new chord with positions moved to different octaves.
432
+ # Creates new chord with positions moved to different octaves, or returns current move settings.
433
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.
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.
437
439
  #
438
440
  # @param octaves [Hash{Symbol => Integer}] position to octave offset mapping
439
- # @return [Chord] new chord with moved positions
441
+ # @return [Chord, Hash] new chord with moved positions, or current move settings if no arguments
440
442
  #
441
443
  # @example Move root down, seventh up
442
444
  # chord.move(root: -1, seventh: 1)
443
445
  #
444
446
  # @example Drop voicing (move third and seventh down)
445
447
  # chord.move(third: -1, seventh: -1)
448
+ #
449
+ # @example Get current move settings
450
+ # chord.move # => { root: -1 }
446
451
  def move(**octaves)
452
+ return @move if octaves.empty?
453
+
447
454
  Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
448
455
  end
449
456
 
450
- # Creates new chord with positions duplicated in other octaves.
457
+ # Creates new chord with positions duplicated in other octaves, or returns current duplicate settings.
451
458
  #
452
- # Adds copies of specific chord positions in different octaves.
453
- # Original positions remain at their current octave.
459
+ # When called with arguments, adds copies of specific chord positions in
460
+ # different octaves. Original positions remain at their current octave.
454
461
  # Merges with existing duplications.
455
462
  #
463
+ # When called without arguments, returns the current duplicate settings hash.
464
+ #
456
465
  # @param octaves [Hash{Symbol => Integer, Array<Integer>}] position to octave(s)
457
- # @return [Chord] new chord with duplicated positions
466
+ # @return [Chord, Hash] new chord with duplicated positions, or current duplicate settings if no arguments
458
467
  #
459
468
  # @example Duplicate root two octaves down
460
469
  # chord.duplicate(root: -2)
@@ -464,10 +473,46 @@ module Musa
464
473
  #
465
474
  # @example Duplicate multiple positions
466
475
  # chord.duplicate(root: -1, fifth: 1)
476
+ #
477
+ # @example Get current duplicate settings
478
+ # chord.duplicate # => { root: -1 }
467
479
  def duplicate(**octaves)
480
+ return @duplicate if octaves.empty?
481
+
468
482
  Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
469
483
  end
470
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
+
471
516
  # Checks chord equality.
472
517
  #
473
518
  # Chords are equal if they have the same notes and chord definition.