musa-dsl 0.41.0 → 0.42.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +15 -1
  4. data/docs/README.md +1 -0
  5. data/docs/subsystems/datasets.md +75 -0
  6. data/docs/subsystems/generative.md +92 -6
  7. data/docs/subsystems/music.md +33 -14
  8. data/docs/subsystems/transport.md +26 -0
  9. data/lib/musa-dsl/datasets/dataset.rb +2 -0
  10. data/lib/musa-dsl/datasets/gdv.rb +3 -3
  11. data/lib/musa-dsl/datasets/p.rb +1 -1
  12. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +4 -2
  13. data/lib/musa-dsl/datasets/score.rb +3 -1
  14. data/lib/musa-dsl/generative/generative-grammar.rb +3 -1
  15. data/lib/musa-dsl/generative/markov.rb +1 -1
  16. data/lib/musa-dsl/midi/midi-voices.rb +3 -1
  17. data/lib/musa-dsl/music/chord-definition.rb +7 -5
  18. data/lib/musa-dsl/music/chord-definitions.rb +37 -0
  19. data/lib/musa-dsl/music/chords.rb +69 -47
  20. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +1 -1
  21. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +1 -1
  22. data/lib/musa-dsl/music/scales.rb +219 -107
  23. data/lib/musa-dsl/musicxml/builder/note.rb +31 -92
  24. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +33 -94
  25. data/lib/musa-dsl/musicxml/builder/rest.rb +30 -91
  26. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +31 -91
  27. data/lib/musa-dsl/neumas/array-to-neumas.rb +1 -1
  28. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +2 -2
  29. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +367 -3
  30. data/lib/musa-dsl/series/base-series.rb +250 -240
  31. data/lib/musa-dsl/series/buffer-serie.rb +10 -5
  32. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +6 -3
  33. data/lib/musa-dsl/series/main-serie-constructors.rb +19 -15
  34. data/lib/musa-dsl/series/main-serie-operations.rb +74 -29
  35. data/lib/musa-dsl/series/proxy-serie.rb +5 -1
  36. data/lib/musa-dsl/series/quantizer-serie.rb +4 -2
  37. data/lib/musa-dsl/series/queue-serie.rb +2 -1
  38. data/lib/musa-dsl/series/series-composer.rb +5 -2
  39. data/lib/musa-dsl/series/timed-serie.rb +8 -4
  40. data/lib/musa-dsl/transport/timer-clock.rb +4 -2
  41. data/lib/musa-dsl/transport/timer.rb +27 -4
  42. data/lib/musa-dsl/version.rb +1 -2
  43. data/musa-dsl.gemspec +0 -2
  44. metadata +1 -1
@@ -104,7 +104,7 @@ module Musa
104
104
  # Makes the scale system available via symbol lookup and dynamic method.
105
105
  # Optionally marks it as the default system.
106
106
  #
107
- # @param scale_system [Class] the ScaleSystem subclass to register
107
+ # @param scale_system [Class<ScaleSystem>] the ScaleSystem subclass to register
108
108
  # @param default [Boolean] whether to set as default system
109
109
  # @return [self]
110
110
  #
@@ -126,20 +126,24 @@ module Musa
126
126
  # Retrieves a registered scale system by ID.
127
127
  #
128
128
  # @param id [Symbol] the scale system identifier
129
- # @return [Class] the ScaleSystem subclass
129
+ # @return [Class<ScaleSystem>] the ScaleSystem subclass
130
130
  # @raise [KeyError] if scale system not found
131
131
  #
132
132
  # @example
133
133
  # Scales[:et12] # => EquallyTempered12ToneScaleSystem
134
- def self.[](id)
134
+ def self.get(id)
135
135
  raise KeyError, "Scale system :#{id} not found" unless @scale_systems.key?(id)
136
136
 
137
137
  @scale_systems[id]
138
138
  end
139
139
 
140
+ class << self
141
+ alias_method :[], :get
142
+ end
143
+
140
144
  # Returns the default scale system.
141
145
  #
142
- # @return [Class] the default ScaleSystem subclass
146
+ # @return [Class<ScaleSystem>] the default ScaleSystem subclass
143
147
  #
144
148
  # @example
145
149
  # Scales.default_system # => EquallyTempered12ToneScaleSystem
@@ -319,7 +323,7 @@ module Musa
319
323
  #
320
324
  # @example Modern high pitch
321
325
  # modern = ScaleSystem[442.0]
322
- def self.[](a_frequency)
326
+ def self.get(a_frequency)
323
327
  a_frequency = a_frequency.to_f
324
328
 
325
329
  @a_tunings ||= {}
@@ -328,6 +332,10 @@ module Musa
328
332
  @a_tunings[a_frequency]
329
333
  end
330
334
 
335
+ class << self
336
+ alias_method :[], :get
337
+ end
338
+
331
339
  # Returns semitone offset for a named interval.
332
340
  #
333
341
  # @param name [Symbol] interval name (e.g., :M3, :P5)
@@ -351,7 +359,7 @@ module Musa
351
359
 
352
360
  # Registers a scale kind (major, minor, etc.) with this system.
353
361
  #
354
- # @param scale_kind_class [Class] ScaleKind subclass to register
362
+ # @param scale_kind_class [Class<ScaleKind>] ScaleKind subclass to register
355
363
  # @return [self]
356
364
  #
357
365
  # @example
@@ -368,7 +376,7 @@ module Musa
368
376
  # Retrieves a registered scale kind by ID.
369
377
  #
370
378
  # @param id [Symbol] scale kind identifier
371
- # @return [Class] ScaleKind subclass
379
+ # @return [Class<ScaleKind>] ScaleKind subclass
372
380
  # @raise [KeyError] if not found
373
381
  def self.scale_kind_class(id)
374
382
  raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
@@ -393,7 +401,7 @@ module Musa
393
401
 
394
402
  # Returns the chromatic scale kind class.
395
403
  #
396
- # @return [Class] chromatic ScaleKind subclass
404
+ # @return [Class<ScaleKind>] chromatic ScaleKind subclass
397
405
  # @raise [RuntimeError] if chromatic scale not defined
398
406
  def self.chromatic_class
399
407
  raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
@@ -452,6 +460,12 @@ module Musa
452
460
  class ScaleSystemTuning
453
461
  extend Forwardable
454
462
 
463
+ # Creates a tuning instance for a scale system.
464
+ #
465
+ # @param scale_system [Class<ScaleSystem>] the ScaleSystem subclass
466
+ # @param a_frequency [Numeric] reference A frequency in Hz
467
+ #
468
+ # @api private
455
469
  def initialize(scale_system, a_frequency)
456
470
  @scale_system = scale_system
457
471
  @a_frequency = a_frequency
@@ -468,18 +482,75 @@ module Musa
468
482
 
469
483
  # TODO: allow scales not based in octaves but in other intervals (like fifths or other ratios). Possibly based on intervals definition of ScaleSystem plus a "generator interval" attribute
470
484
 
485
+ # @!method notes_in_octave
486
+ # Returns the number of notes in one octave.
487
+ # Delegated from {ScaleSystem.notes_in_octave}.
488
+ # @return [Integer] notes per octave (e.g., 12 for chromatic)
489
+
490
+ # @!method offset_of_interval(name)
491
+ # Returns semitone offset for a named interval.
492
+ # Delegated from {ScaleSystem.offset_of_interval}.
493
+ # @param name [Symbol] interval name (e.g., :M3, :P5)
494
+ # @return [Integer] semitone offset
495
+
471
496
  def_delegators :@scale_system, :notes_in_octave, :offset_of_interval
472
497
 
473
- attr_reader :a_frequency, :scale_system
498
+ # Reference A frequency in Hz.
499
+ # @return [Float]
500
+ attr_reader :a_frequency
501
+
502
+ # The parent scale system.
503
+ # @return [Class<ScaleSystem>] ScaleSystem subclass
504
+ attr_reader :scale_system
474
505
 
475
- def [](scale_kind_class_id)
506
+ # Retrieves a scale kind by ID.
507
+ #
508
+ # Creates and caches {ScaleKind} instances for efficient reuse.
509
+ #
510
+ # @param scale_kind_class_id [Symbol] scale kind identifier (e.g., :major, :minor)
511
+ # @return [ScaleKind] scale kind instance
512
+ # @raise [KeyError] if scale kind not found
513
+ #
514
+ # @example
515
+ # tuning[:major][60] # C major scale
516
+ # tuning[:minor][69] # A minor scale
517
+ #
518
+ # @see ScaleKind
519
+ def get(scale_kind_class_id)
476
520
  @scale_kinds[scale_kind_class_id] ||= @scale_system.scale_kind_class(scale_kind_class_id).new self
477
521
  end
478
522
 
523
+ alias_method :[], :get
524
+
525
+ # Returns the chromatic scale kind.
526
+ #
527
+ # Provides access to the chromatic scale, which contains all pitches
528
+ # in the scale system. Used as fallback for non-diatonic notes.
529
+ #
530
+ # @return [ScaleKind] chromatic scale kind instance
531
+ #
532
+ # @example
533
+ # tuning.chromatic[60] # Chromatic scale at C
534
+ #
535
+ # @see ScaleSystem.chromatic_class
479
536
  def chromatic
480
537
  @chromatic_scale_kind
481
538
  end
482
539
 
540
+ # Calculates frequency for a MIDI pitch.
541
+ #
542
+ # Delegates to the scale system's frequency calculation using
543
+ # this tuning's A frequency as reference.
544
+ #
545
+ # @param pitch [Numeric] MIDI pitch number (60 = middle C)
546
+ # @param root [Numeric] root pitch of the scale (for non-equal temperaments)
547
+ # @return [Float] frequency in Hz
548
+ #
549
+ # @example
550
+ # tuning.frequency_of_pitch(69, 60) # => 440.0 (A4)
551
+ # tuning.frequency_of_pitch(60, 60) # => ~261.63 (C4)
552
+ #
553
+ # @see ScaleSystem.frequency_of_pitch
483
554
  def frequency_of_pitch(pitch, root)
484
555
  @scale_system.frequency_of_pitch(pitch, root, @a_frequency)
485
556
  end
@@ -492,7 +563,7 @@ module Musa
492
563
  #
493
564
  # @param metadata_criteria [Hash] metadata key-value pairs to match
494
565
  # @yield [kind_class] optional block for custom filtering
495
- # @yieldparam kind_class [Class] the ScaleKind subclass
566
+ # @yieldparam kind_class [Class<ScaleKind>] the ScaleKind subclass
496
567
  # @yieldreturn [Boolean] true to include this scale kind
497
568
  # @return [Array<ScaleKind>] matching scale kind instances
498
569
  #
@@ -543,27 +614,33 @@ module Musa
543
614
  # @example Search G7 in greek mode scales
544
615
  # tuning = Scales.et12[440.0]
545
616
  # g7 = tuning.major[60].dominant.chord :seventh
546
- # tuning.chords_of(g7, family: :greek_modes)
617
+ # tuning.search_chord_in_scales(g7, family: :greek_modes)
547
618
  #
548
619
  # @example Search with brightness filter
549
- # tuning.chords_of(g7, brightness: -1..1)
620
+ # tuning.search_chord_in_scales(g7, brightness: -1..1)
550
621
  #
551
622
  # @example Search in all scale types
552
- # tuning.chords_of(g7)
623
+ # tuning.search_chord_in_scales(g7)
553
624
  #
554
- # @see ScaleKind#scales_containing
555
- # @see Musa::Chords::Chord#in_scales
556
- def chords_of(chord, roots: nil, **metadata_criteria)
625
+ # @see ScaleKind#find_chord_in_scales
626
+ # @see Musa::Chords::Chord#search_in_scales
627
+ def search_chord_in_scales(chord, roots: nil, **metadata_criteria)
557
628
  roots ||= 0...notes_in_octave
558
629
  kinds = filtered_scale_kind_ids(**metadata_criteria)
559
630
 
560
631
  kinds.flat_map do |kind_id|
561
- self[kind_id].scales_containing(chord, roots: roots)
632
+ self[kind_id].find_chord_in_scales(chord, roots: roots)
562
633
  end
563
634
  end
564
635
 
565
636
  private
566
637
 
638
+ # Returns scale kind IDs filtered by metadata criteria.
639
+ #
640
+ # @param metadata_criteria [Hash] key-value pairs to match
641
+ # @return [Array<Symbol>] matching scale kind IDs
642
+ #
643
+ # @api private
567
644
  def filtered_scale_kind_ids(**metadata_criteria)
568
645
  kinds = @scale_system.scale_kind_classes.keys
569
646
 
@@ -575,6 +652,13 @@ module Musa
575
652
  end
576
653
  end
577
654
 
655
+ # Checks if a scale kind class matches the given criteria.
656
+ #
657
+ # @param kind_class [Class<ScaleKind>] ScaleKind subclass to check
658
+ # @param criteria [Hash] metadata key-value pairs to match
659
+ # @return [Boolean] true if all criteria match
660
+ #
661
+ # @api private
578
662
  def matches_metadata?(kind_class, criteria)
579
663
  criteria.all? do |key, value|
580
664
  actual = kind_class.metadata[key]
@@ -591,12 +675,21 @@ module Musa
591
675
 
592
676
  public
593
677
 
678
+ # Checks tuning equality.
679
+ #
680
+ # Tunings are equal if they have the same scale system and A frequency.
681
+ #
682
+ # @param other [ScaleSystemTuning] the tuning to compare
683
+ # @return [Boolean] true if equal
594
684
  def ==(other)
595
685
  self.class == other.class &&
596
686
  @scale_system == other.scale_system &&
597
687
  @a_frequency == other.a_frequency
598
688
  end
599
689
 
690
+ # Returns string representation.
691
+ #
692
+ # @return [String] human-readable description
600
693
  def inspect
601
694
  "<ScaleSystemTuning: scale_system = #{@scale_system} a_frequency = #{@a_frequency}>"
602
695
  end
@@ -684,11 +777,13 @@ module Musa
684
777
  # major_kind = tuning[:major]
685
778
  # c_major = major_kind[60] # C major
686
779
  # g_major = major_kind[67] # G major
687
- def [](root_pitch)
780
+ def get(root_pitch)
688
781
  @scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
689
782
  @scales[root_pitch]
690
783
  end
691
784
 
785
+ alias_method :[], :get
786
+
692
787
  # Returns scale with default root (middle C, MIDI 60).
693
788
  #
694
789
  # @return [Scale] scale rooted on middle C
@@ -722,19 +817,19 @@ module Musa
722
817
  # @example Find G major triad in all major scales
723
818
  # tuning = Scales.et12[440.0]
724
819
  # g_triad = tuning.major[60].dominant.chord
725
- # tuning.major.scales_containing(g_triad)
820
+ # tuning.major.find_chord_in_scales(g_triad)
726
821
  # # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]
727
822
  #
728
- # @see Scale#chord_on
729
- # @see ScaleSystemTuning#chords_in_scales
730
- def scales_containing(chord, roots: nil)
823
+ # @see Musa::Chords::Chord#as_chord_in_scale
824
+ # @see ScaleSystemTuning#search_chord_in_scales
825
+ def find_chord_in_scales(chord, roots: nil)
731
826
  roots ||= 0...tuning.notes_in_octave
732
827
  base_pitch = chord.root.pitch % tuning.notes_in_octave
733
828
 
734
829
  roots.filter_map do |root_offset|
735
830
  root_pitch = base_pitch + root_offset
736
831
  scale = self[root_pitch]
737
- scale.chord_on(chord)
832
+ chord.as_chord_in_scale(scale)
738
833
  end
739
834
  end
740
835
 
@@ -1091,9 +1186,9 @@ module Musa
1091
1186
  # scale[:V] # Fifth degree
1092
1187
  # scale[:IV] # Fourth degree
1093
1188
  #
1094
- # **With accidentals** (sharp # or flat _):
1189
+ # **With accidentals** (sharp # or flat _). Use strings for #:
1095
1190
  #
1096
- # scale[:I#] # Raised tonic
1191
+ # scale['I#'] # Raised tonic
1097
1192
  # scale[:V_] # Flatted dominant
1098
1193
  # scale['II##'] # Double-raised second
1099
1194
  #
@@ -1120,8 +1215,8 @@ module Musa
1120
1215
  # c_major.dominant.pitch # => 67 (G)
1121
1216
  # c_major[:III].pitch # => 64 (E)
1122
1217
  #
1123
- # @example Chromatic alterations
1124
- # c_major[:I#].pitch # => 61 (C#)
1218
+ # @example Chromatic alterations (use strings for #)
1219
+ # c_major['I#'].pitch # => 61 (C#)
1125
1220
  # c_major[:V_].pitch # => 66 (F#/Gb)
1126
1221
  #
1127
1222
  # @example Building chords
@@ -1156,7 +1251,16 @@ module Musa
1156
1251
  freeze
1157
1252
  end
1158
1253
 
1159
- # Delegates tuning access to kind.
1254
+ # @!method tuning
1255
+ # Returns the tuning system associated with this scale.
1256
+ #
1257
+ # Delegated from ScaleKind#tuning.
1258
+ #
1259
+ # @return [ScaleSystemTuning] the tuning system used by this scale
1260
+ #
1261
+ # @example
1262
+ # scale = Scales.et12[440.0].major[60]
1263
+ # scale.tuning # => ScaleSystemTuning for 12-TET at A=440Hz
1160
1264
  def_delegators :@kind, :tuning
1161
1265
 
1162
1266
  # Scale kind (major, minor, etc.).
@@ -1241,11 +1345,11 @@ module Musa
1241
1345
  # scale[:V] # Dominant
1242
1346
  # scale[:IV] # Subdominant
1243
1347
  #
1244
- # @example With accidentals
1245
- # scale[:I#] # Raised tonic
1348
+ # @example With accidentals (use strings for #)
1349
+ # scale['I#'] # Raised tonic
1246
1350
  # scale[:V_] # Flatted dominant
1247
1351
  # scale['II##'] # Double-raised second
1248
- def [](grade_or_symbol)
1352
+ def get(grade_or_symbol)
1249
1353
 
1250
1354
  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)
1251
1355
 
@@ -1269,6 +1373,8 @@ module Musa
1269
1373
  @notes_by_grade[wide_grade].sharp(sharps)
1270
1374
  end
1271
1375
 
1376
+ alias_method :[], :get
1377
+
1272
1378
  # Converts grade specifier to numeric grade and accidentals.
1273
1379
  #
1274
1380
  # @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
@@ -1431,38 +1537,45 @@ module Musa
1431
1537
  note&.grade
1432
1538
  end
1433
1539
 
1434
- # Creates an equivalent chord with this scale as its context.
1540
+ # Creates a chord rooted on the specified scale degree.
1435
1541
  #
1436
- # Returns a new Chord object that represents the same chord but with
1437
- # this scale as its harmonic context. The chord's voicing (move and
1438
- # duplicate settings) is preserved.
1542
+ # This is a convenience method that combines scale note access with
1543
+ # chord creation. It's equivalent to `scale[grade].chord(...)`.
1439
1544
  #
1440
- # @param chord [Musa::Chords::Chord] the source chord
1441
- # @return [Musa::Chords::Chord, nil] new chord with this scale, or nil if not contained
1545
+ # @param grade [Integer, Symbol, String] scale degree (0-based numeric, function name like :tonic, or Roman numeral like :V)
1546
+ # @param feature_values [Array<Symbol>] chord feature values (:seventh, :major, etc.)
1547
+ # @param allow_chromatic [Boolean] allow non-diatonic chord notes
1548
+ # @param move [Hash{Symbol => Integer}] initial octave moves for chord tones
1549
+ # @param duplicate [Hash{Symbol => Integer, Array}] initial duplications
1550
+ # @param features_hash [Hash] additional feature key-value pairs
1551
+ # @return [Chords::Chord] chord rooted on the specified degree
1442
1552
  #
1443
- # @example
1444
- # c_major = Scales.et12[440.0].major[60]
1445
- # g7 = c_major.dominant.chord :seventh
1553
+ # @example Create triads
1554
+ # scale.chord_on(0) # Tonic triad (I)
1555
+ # scale.chord_on(:dominant) # Dominant triad (V)
1556
+ # scale.chord_on(:IV) # Subdominant triad
1446
1557
  #
1447
- # g_mixolydian = Scales.et12[440.0].mixolydian[67]
1448
- # g7_in_mixolydian = g_mixolydian.chord_on(g7)
1449
- # g7_in_mixolydian.scale # => G Mixolydian scale
1558
+ # @example Create extended chords
1559
+ # scale.chord_on(4, :seventh) # V7
1560
+ # scale.chord_on(:dominant, :ninth) # V9
1561
+ # scale.chord_on(0, :seventh, :major) # Imaj7
1450
1562
  #
1451
- # @see #contains_chord?
1452
- # @see #degree_of_chord
1453
- def chord_on(chord)
1454
- return nil unless contains_chord?(chord)
1455
-
1456
- root_note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
1457
- return nil unless root_note
1458
-
1459
- Musa::Chords::Chord.with_root(
1460
- root_note,
1461
- scale: self,
1462
- name: chord.chord_definition.name,
1463
- move: chord.move.empty? ? nil : chord.move,
1464
- duplicate: chord.duplicate.empty? ? nil : chord.duplicate
1465
- )
1563
+ # @example With voicing
1564
+ # scale.chord_on(:I, :seventh, move: {root: -1})
1565
+ # scale.chord_on(0, :triad, duplicate: {root: 1})
1566
+ #
1567
+ # @see NoteInScale#chord
1568
+ # @see #get
1569
+ def chord_on(grade, *feature_values,
1570
+ allow_chromatic: nil,
1571
+ move: nil,
1572
+ duplicate: nil,
1573
+ **features_hash)
1574
+ self[grade].chord(*feature_values,
1575
+ allow_chromatic: allow_chromatic,
1576
+ move: move,
1577
+ duplicate: duplicate,
1578
+ **features_hash)
1466
1579
  end
1467
1580
 
1468
1581
  # Checks scale equality.
@@ -1536,9 +1649,10 @@ module Musa
1536
1649
  #
1537
1650
  # ## Scale Navigation
1538
1651
  #
1539
- # note.scale(:minor) # Same pitch in minor scale
1540
- # note.major # Same pitch in major scale
1541
- # note.chromatic # Same pitch in chromatic scale
1652
+ # note.scale # Parent scale this note belongs to
1653
+ # note.as_root_of(:minor) # New minor scale with this pitch as root
1654
+ # note.minor # Same as note.as_root_of(:minor)
1655
+ # note.chromatic # Same as note.as_root_of(:chromatic)
1542
1656
  #
1543
1657
  # ## Chord Construction
1544
1658
  #
@@ -1602,7 +1716,7 @@ module Musa
1602
1716
 
1603
1717
  @scale.kind.tuning.scale_system.scale_kind_classes.each_key do |name|
1604
1718
  define_singleton_method name do
1605
- scale(name)
1719
+ as_root_of(name)
1606
1720
  end
1607
1721
  end
1608
1722
  end
@@ -1625,34 +1739,27 @@ module Musa
1625
1739
  @scale.kind.class.pitches[grade][:functions]
1626
1740
  end
1627
1741
 
1628
- # Transposes note or returns current octave.
1629
- #
1630
- # **Without argument**: Returns current octave relative to scale root.
1631
- #
1632
- # **With argument**: Returns note transposed by octave offset.
1742
+ # Current octave relative to scale root.
1743
+ # @return [Integer]
1744
+ attr_reader :octave
1745
+
1746
+ # Returns note transposed by octave offset.
1633
1747
  #
1634
- # @param octave [Integer, nil] octave offset (nil to query current)
1748
+ # @param offset [Integer] octave offset (positive = up, negative = down)
1635
1749
  # @param absolute [Boolean] if true, ignore current octave
1636
- # @return [Integer, NoteInScale] current octave or transposed note
1637
- # @raise [ArgumentError] if octave is not integer
1638
- #
1639
- # @example Query octave
1640
- # note.octave # => 0 (at scale root octave)
1750
+ # @return [NoteInScale] transposed note
1751
+ # @raise [ArgumentError] if offset is not integer
1641
1752
  #
1642
1753
  # @example Transpose relative
1643
- # note.octave(1).pitch # Up one octave from current
1644
- # note.octave(-1).pitch # Down one octave from current
1754
+ # note.at_octave(1).pitch # Up one octave from current
1755
+ # note.at_octave(-1).pitch # Down one octave from current
1645
1756
  #
1646
1757
  # @example Transpose absolute
1647
- # note.octave(2, absolute: true).pitch # At octave 2, regardless of current
1648
- def octave(octave = nil, absolute: false)
1649
- if octave.nil?
1650
- @octave
1651
- else
1652
- raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
1758
+ # note.at_octave(2, absolute: true).pitch # At octave 2, regardless of current
1759
+ def at_octave(offset, absolute: false)
1760
+ raise ArgumentError, "#{offset} is not integer" unless offset == offset.to_i
1653
1761
 
1654
- @scale[@grade + ((absolute ? 0 : @octave) + octave) * @scale.kind.class.grades]
1655
- end
1762
+ @scale[@grade + ((absolute ? 0 : @octave) + offset) * @scale.kind.class.grades]
1656
1763
  end
1657
1764
 
1658
1765
  # Creates a copy with background scale context.
@@ -1747,6 +1854,13 @@ module Musa
1747
1854
  end
1748
1855
  end
1749
1856
 
1857
+ # Calculates note at given pitch offset from current note.
1858
+ #
1859
+ # @param in_scale_pitch [Numeric] base pitch
1860
+ # @param sharps [Integer] number of semitones to add (negative for flats)
1861
+ # @return [NoteInScale] resulting note
1862
+ #
1863
+ # @api private
1750
1864
  def calculate_note_of_pitch(in_scale_pitch, sharps)
1751
1865
  pitch = in_scale_pitch + sharps * @scale.kind.tuning.scale_system.part_of_tone_size
1752
1866
 
@@ -1818,33 +1932,31 @@ module Musa
1818
1932
  @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
1819
1933
  end
1820
1934
 
1821
- # Changes scale while keeping pitch, or returns current scale.
1822
- #
1823
- # **Without argument**: Returns current scale.
1824
- #
1825
- # **With argument**: Returns note at same pitch in different scale kind.
1935
+ # Parent scale this note belongs to.
1936
+ # @return [Scale]
1937
+ attr_reader :scale
1938
+
1939
+ # Creates a new scale with this note's pitch as the root.
1826
1940
  #
1827
- # @param kind_id_or_kind [Symbol, ScaleKind, nil] scale kind or ID
1828
- # @return [Scale, NoteInScale] current scale or note in new scale
1941
+ # @param kind_id_or_kind [Symbol, ScaleKind] scale kind or ID
1942
+ # @return [Scale] new scale rooted at this pitch
1829
1943
  #
1830
- # @example Query current scale
1831
- # note.scale # => <Scale: kind = MajorScaleKind ...>
1944
+ # @example Create minor scale from a note
1945
+ # e = c_major[64] # E in C major
1946
+ # e_minor = e.as_root_of(:minor) # E minor scale
1832
1947
  #
1833
- # @example Change to minor
1834
- # note.scale(:minor) # Same pitch in minor scale
1948
+ # @example With ScaleKind object
1949
+ # minor_kind = tuning[:minor]
1950
+ # e_minor = e.as_root_of(minor_kind)
1835
1951
  #
1836
- # @example Dynamic method
1837
- # note.minor # Same as note.scale(:minor)
1838
- # note.major # Same as note.scale(:major)
1839
- def scale(kind_id_or_kind = nil)
1840
- if kind_id_or_kind.nil?
1841
- @scale
1952
+ # @example Dynamic method (equivalent)
1953
+ # note.minor # Same as note.as_root_of(:minor)
1954
+ # note.major # Same as note.as_root_of(:major)
1955
+ def as_root_of(kind_id_or_kind)
1956
+ if kind_id_or_kind.is_a? ScaleKind
1957
+ kind_id_or_kind[@pitch]
1842
1958
  else
1843
- if kind_id_or_kind.is_a? ScaleKind
1844
- kind_id_or_kind[@pitch]
1845
- else
1846
- @scale.kind.tuning[kind_id_or_kind][@pitch]
1847
- end
1959
+ @scale.kind.tuning[kind_id_or_kind][@pitch]
1848
1960
  end
1849
1961
  end
1850
1962
 
@@ -1877,7 +1989,7 @@ module Musa
1877
1989
  # @param move [Hash{Symbol => Integer}] initial octave moves
1878
1990
  # @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
1879
1991
  # @param features_hash [Hash] feature key-value pairs
1880
- # @return [Chord] chord rooted on this note
1992
+ # @return [Chords::Chord] chord rooted on this note
1881
1993
  #
1882
1994
  # @example Default triad
1883
1995
  # note.chord # Major triad