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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +544 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- metadata +158 -16
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
require_relative 'chords'
|
|
2
2
|
|
|
3
3
|
module Musa
|
|
4
|
+
# Musical scale system framework.
|
|
5
|
+
#
|
|
6
|
+
# The Scales module provides a comprehensive framework for working with musical scales,
|
|
7
|
+
# supporting multiple scale systems (equal temperament, just intonation, etc.), scale
|
|
8
|
+
# types (major, minor, chromatic, etc.), and musical operations (transposition, interval
|
|
9
|
+
# calculation, frequency generation, etc.).
|
|
10
|
+
#
|
|
11
|
+
# ## Architecture
|
|
12
|
+
#
|
|
13
|
+
# The framework has a hierarchical structure:
|
|
14
|
+
#
|
|
15
|
+
# 1. **ScaleSystem**: Defines the tuning system (e.g., 12-tone equal temperament)
|
|
16
|
+
# 2. **ScaleSystemTuning**: A scale system with specific A frequency (e.g., A=440Hz)
|
|
17
|
+
# 3. **ScaleKind**: Type of scale (major, minor, chromatic, etc.)
|
|
18
|
+
# 4. **Scale**: A scale kind rooted on a specific pitch (e.g., C major, A minor)
|
|
19
|
+
# 5. **NoteInScale**: A specific note within a scale
|
|
20
|
+
#
|
|
21
|
+
# ## Basic Usage
|
|
22
|
+
#
|
|
23
|
+
# # Access the default system (12-tone equal temperament at A=440Hz)
|
|
24
|
+
# tuning = Scales::Scales.default_system.default_tuning
|
|
25
|
+
#
|
|
26
|
+
# # Get a C major scale (root pitch 60 = middle C)
|
|
27
|
+
# c_major = tuning.major[60]
|
|
28
|
+
#
|
|
29
|
+
# # Access notes by grade or function
|
|
30
|
+
# c_major[0] # => Tonic (C)
|
|
31
|
+
# c_major.tonic # => Tonic (C)
|
|
32
|
+
# c_major.dominant # => Dominant (G)
|
|
33
|
+
# c_major[:V] # => Dominant (G)
|
|
34
|
+
#
|
|
35
|
+
# ## Advanced Features
|
|
36
|
+
#
|
|
37
|
+
# - **Multiple tuning systems**: Support for different A frequencies
|
|
38
|
+
# - **Interval calculations**: Named intervals (M3, P5, etc.) and numeric offsets
|
|
39
|
+
# - **Chromatic operations**: Sharp, flat, and chromatic movements
|
|
40
|
+
# - **Scale navigation**: Move between related scales
|
|
41
|
+
# - **Frequency calculation**: Convert pitches to frequencies
|
|
42
|
+
# - **Chord generation**: Build chords from scale degrees
|
|
43
|
+
#
|
|
44
|
+
# @see Scales Module for registering scale systems
|
|
45
|
+
# @see ScaleSystem Abstract base for scale systems
|
|
46
|
+
# @see EquallyTempered12ToneScaleSystem The default 12-tone equal temperament system
|
|
4
47
|
module Scales
|
|
48
|
+
# Scale system registry.
|
|
49
|
+
#
|
|
50
|
+
# The Scales module provides a central registry for scale systems, allowing access
|
|
51
|
+
# by symbol ID or method name.
|
|
52
|
+
#
|
|
53
|
+
# ## Registration
|
|
54
|
+
#
|
|
55
|
+
# Scale systems register themselves using {register}:
|
|
56
|
+
#
|
|
57
|
+
# Scales.register EquallyTempered12ToneScaleSystem, default: true
|
|
58
|
+
#
|
|
59
|
+
# ## Access Methods
|
|
60
|
+
#
|
|
61
|
+
# **By symbol**:
|
|
62
|
+
#
|
|
63
|
+
# Scales[:et12] # => EquallyTempered12ToneScaleSystem
|
|
64
|
+
# Scales[:et12][440.0] # => ScaleSystemTuning with A=440Hz
|
|
65
|
+
#
|
|
66
|
+
# **By method name**:
|
|
67
|
+
#
|
|
68
|
+
# Scales.et12 # => EquallyTempered12ToneScaleSystem
|
|
69
|
+
# Scales.et12[440.0] # => ScaleSystemTuning with A=440Hz
|
|
70
|
+
#
|
|
71
|
+
# **Default system**:
|
|
72
|
+
#
|
|
73
|
+
# Scales.default_system # => The default scale system
|
|
74
|
+
# Scales.default_system.default_tuning # => Default tuning (A=440Hz)
|
|
75
|
+
#
|
|
76
|
+
# @example Accessing scale systems
|
|
77
|
+
# # Get system by symbol
|
|
78
|
+
# system = Scales::Scales[:et12]
|
|
79
|
+
#
|
|
80
|
+
# # Get system by method
|
|
81
|
+
# system = Scales::Scales.et12
|
|
82
|
+
#
|
|
83
|
+
# # Get default system
|
|
84
|
+
# system = Scales::Scales.default_system
|
|
85
|
+
#
|
|
86
|
+
# @example Working with tunings
|
|
87
|
+
# # Get tuning with A=440Hz (default)
|
|
88
|
+
# tuning = Scales::Scales[:et12][440.0]
|
|
89
|
+
#
|
|
90
|
+
# # Get tuning with baroque pitch A=415Hz
|
|
91
|
+
# baroque = Scales::Scales[:et12][415.0]
|
|
92
|
+
#
|
|
93
|
+
# @example Building scales
|
|
94
|
+
# tuning = Scales::Scales.default_system.default_tuning
|
|
95
|
+
#
|
|
96
|
+
# # C major scale
|
|
97
|
+
# c_major = tuning.major[60]
|
|
98
|
+
#
|
|
99
|
+
# # A minor scale
|
|
100
|
+
# a_minor = tuning.minor[69]
|
|
5
101
|
module Scales
|
|
102
|
+
# Registers a scale system.
|
|
103
|
+
#
|
|
104
|
+
# Makes the scale system available via symbol lookup and dynamic method.
|
|
105
|
+
# Optionally marks it as the default system.
|
|
106
|
+
#
|
|
107
|
+
# @param scale_system [Class] the ScaleSystem subclass to register
|
|
108
|
+
# @param default [Boolean] whether to set as default system
|
|
109
|
+
# @return [self]
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# Scales.register EquallyTempered12ToneScaleSystem, default: true
|
|
6
113
|
def self.register(scale_system, default: nil)
|
|
7
114
|
@scale_systems ||= {}
|
|
8
115
|
@scale_systems[scale_system.id] = scale_system
|
|
@@ -16,46 +123,147 @@ module Musa
|
|
|
16
123
|
self
|
|
17
124
|
end
|
|
18
125
|
|
|
126
|
+
# Retrieves a registered scale system by ID.
|
|
127
|
+
#
|
|
128
|
+
# @param id [Symbol] the scale system identifier
|
|
129
|
+
# @return [Class] the ScaleSystem subclass
|
|
130
|
+
# @raise [KeyError] if scale system not found
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# Scales[:et12] # => EquallyTempered12ToneScaleSystem
|
|
19
134
|
def self.[](id)
|
|
20
135
|
raise KeyError, "Scale system :#{id} not found" unless @scale_systems.key?(id)
|
|
21
136
|
|
|
22
137
|
@scale_systems[id]
|
|
23
138
|
end
|
|
24
139
|
|
|
140
|
+
# Returns the default scale system.
|
|
141
|
+
#
|
|
142
|
+
# @return [Class] the default ScaleSystem subclass
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# Scales.default_system # => EquallyTempered12ToneScaleSystem
|
|
25
146
|
def self.default_system
|
|
26
147
|
@default_scale_system
|
|
27
148
|
end
|
|
149
|
+
|
|
150
|
+
# Convenience method to extend metadata for a scale kind by ID.
|
|
151
|
+
#
|
|
152
|
+
# Finds the ScaleKind class by its ID symbol and adds custom metadata to it.
|
|
153
|
+
# This is a shortcut for accessing the class directly and calling extend_metadata.
|
|
154
|
+
#
|
|
155
|
+
# @param scale_kind_id [Symbol] the scale kind identifier (e.g., :major, :dorian)
|
|
156
|
+
# @param metadata [Hash] key-value pairs to add as custom metadata
|
|
157
|
+
# @return [Hash] the updated custom_metadata hash
|
|
158
|
+
# @raise [KeyError] if scale kind not found
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# Scales.extend_metadata(:major, my_tag: :favorite)
|
|
162
|
+
# Scales.extend_metadata(:dorian, mood: :nostalgic, suitable_for: [:jazz])
|
|
163
|
+
#
|
|
164
|
+
# @see ScaleKind.extend_metadata
|
|
165
|
+
def self.extend_metadata(scale_kind_id, **metadata)
|
|
166
|
+
system = default_system
|
|
167
|
+
raise KeyError, "No default scale system registered" unless system
|
|
168
|
+
|
|
169
|
+
klass = system.scale_kind_class(scale_kind_id)
|
|
170
|
+
raise KeyError, "Scale kind :#{scale_kind_id} not found" unless klass
|
|
171
|
+
|
|
172
|
+
klass.extend_metadata(**metadata)
|
|
173
|
+
end
|
|
28
174
|
end
|
|
29
175
|
|
|
176
|
+
# Abstract base class for musical scale systems.
|
|
177
|
+
#
|
|
178
|
+
# ScaleSystem defines the foundation of a tuning system, including:
|
|
179
|
+
#
|
|
180
|
+
# - Number of notes per octave
|
|
181
|
+
# - Available intervals
|
|
182
|
+
# - Frequency calculation method
|
|
183
|
+
# - Registered scale kinds (major, minor, etc.)
|
|
184
|
+
#
|
|
185
|
+
# ## Subclass Requirements
|
|
186
|
+
#
|
|
187
|
+
# Subclasses must implement:
|
|
188
|
+
#
|
|
189
|
+
# - {.id}: Unique symbol identifier
|
|
190
|
+
# - {.notes_in_octave}: Number of notes in an octave
|
|
191
|
+
# - {.part_of_tone_size}: Size of smallest pitch unit (for sharps/flats)
|
|
192
|
+
# - {.intervals}: Hash of named intervals to semitone offsets
|
|
193
|
+
# - {.frequency_of_pitch}: Pitch to frequency conversion
|
|
194
|
+
#
|
|
195
|
+
# Optionally override:
|
|
196
|
+
#
|
|
197
|
+
# - {.default_a_frequency}: Reference A frequency (defaults to 440.0 Hz)
|
|
198
|
+
#
|
|
199
|
+
# ## Usage
|
|
200
|
+
#
|
|
201
|
+
# ScaleSystem is accessed via {Scales} module, not instantiated directly:
|
|
202
|
+
#
|
|
203
|
+
# system = Scales[:et12] # Get system
|
|
204
|
+
# tuning = system[440.0] # Get tuning
|
|
205
|
+
# scale = tuning.major[60] # Get scale
|
|
206
|
+
#
|
|
207
|
+
# @abstract Subclass and implement abstract methods
|
|
208
|
+
# @see EquallyTempered12ToneScaleSystem Concrete 12-tone implementation
|
|
209
|
+
# @see ScaleSystemTuning Tuning with specific A frequency
|
|
30
210
|
class ScaleSystem
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# @
|
|
211
|
+
# Returns the unique identifier for this scale system.
|
|
212
|
+
#
|
|
213
|
+
# @abstract Subclass must implement
|
|
214
|
+
# @return [Symbol] the scale system ID (e.g., :et12)
|
|
215
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
34
216
|
#
|
|
217
|
+
# @example
|
|
218
|
+
# EquallyTempered12ToneScaleSystem.id # => :et12
|
|
35
219
|
def self.id
|
|
36
220
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
37
221
|
end
|
|
38
222
|
|
|
39
|
-
#
|
|
40
|
-
# @!method notes_in_octave
|
|
41
|
-
# @return [Integer] the number of notes in one octave in the ScaleSystem
|
|
223
|
+
# Returns the number of notes in one octave.
|
|
42
224
|
#
|
|
225
|
+
# @abstract Subclass must implement
|
|
226
|
+
# @return [Integer] notes per octave (e.g., 12 for chromatic)
|
|
227
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
228
|
+
#
|
|
229
|
+
# @example
|
|
230
|
+
# EquallyTempered12ToneScaleSystem.notes_in_octave # => 12
|
|
43
231
|
def self.notes_in_octave
|
|
44
232
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
45
233
|
end
|
|
46
234
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
235
|
+
# Returns the size of the smallest pitch unit.
|
|
236
|
+
#
|
|
237
|
+
# Used for calculating sharp (#) and flat (♭) alterations.
|
|
238
|
+
# In equal temperament, this is 1 semitone.
|
|
50
239
|
#
|
|
240
|
+
# @abstract Subclass must implement
|
|
241
|
+
# @return [Integer] smallest unit size
|
|
242
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
243
|
+
#
|
|
244
|
+
# @example
|
|
245
|
+
# EquallyTempered12ToneScaleSystem.part_of_tone_size # => 1
|
|
51
246
|
def self.part_of_tone_size
|
|
52
247
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
53
248
|
end
|
|
54
249
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
250
|
+
# Returns available intervals as name-to-offset mapping.
|
|
251
|
+
#
|
|
252
|
+
# Intervals are named using standard music theory notation:
|
|
253
|
+
#
|
|
254
|
+
# - **P** (Perfect): P1, P4, P5, P8
|
|
255
|
+
# - **M** (Major): M2, M3, M6, M7
|
|
256
|
+
# - **m** (minor): m2, m3, m6, m7
|
|
257
|
+
# - **TT**: Tritone
|
|
58
258
|
#
|
|
259
|
+
# @abstract Subclass must implement
|
|
260
|
+
# @return [Hash{Symbol => Integer}] interval names to semitone offsets
|
|
261
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# intervals[:M3] # => 4 (major third = 4 semitones)
|
|
265
|
+
# intervals[:P5] # => 7 (perfect fifth = 7 semitones)
|
|
266
|
+
# intervals[:m7] # => 10 (minor seventh = 10 semitones)
|
|
59
267
|
def self.intervals
|
|
60
268
|
# TODO: implementar intérvalos sinónimos (p.ej, m3 = A2)
|
|
61
269
|
# TODO: implementar identificación de intérvalos, teniendo en cuenta no sólo los semitonos sino los grados de separación
|
|
@@ -63,25 +271,54 @@ module Musa
|
|
|
63
271
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
64
272
|
end
|
|
65
273
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# @param a_frequency [Number] The reference frequency of the mid A note
|
|
71
|
-
# @return [Number] the frequency of the fundamental tone of the pitch
|
|
274
|
+
# Calculates frequency for a given pitch.
|
|
275
|
+
#
|
|
276
|
+
# Converts MIDI pitch numbers to frequencies in Hz. The calculation method
|
|
277
|
+
# depends on the tuning system (equal temperament, just intonation, etc.).
|
|
72
278
|
#
|
|
279
|
+
# @abstract Subclass must implement
|
|
280
|
+
# @param pitch [Numeric] MIDI pitch number (60 = middle C, 69 = A440)
|
|
281
|
+
# @param root_pitch [Numeric] root pitch of scale (for non-equal temperaments)
|
|
282
|
+
# @param a_frequency [Numeric] reference A frequency in Hz
|
|
283
|
+
# @return [Float] frequency in Hz
|
|
284
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
285
|
+
#
|
|
286
|
+
# @example Equal temperament
|
|
287
|
+
# # A440 (MIDI 69)
|
|
288
|
+
# frequency_of_pitch(69, 60, 440.0) # => 440.0
|
|
289
|
+
#
|
|
290
|
+
# # Middle C (MIDI 60)
|
|
291
|
+
# frequency_of_pitch(60, 60, 440.0) # => ~261.63 Hz
|
|
73
292
|
def self.frequency_of_pitch(pitch, root_pitch, a_frequency)
|
|
74
293
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
75
294
|
end
|
|
76
295
|
|
|
77
|
-
#
|
|
78
|
-
# @!method default_a_frequency
|
|
79
|
-
# @return [Number] the frequency A by default
|
|
296
|
+
# Returns the default A frequency.
|
|
80
297
|
#
|
|
298
|
+
# @return [Float] default A frequency in Hz (440.0 standard concert pitch)
|
|
299
|
+
#
|
|
300
|
+
# @example
|
|
301
|
+
# ScaleSystem.default_a_frequency # => 440.0
|
|
81
302
|
def self.default_a_frequency
|
|
82
303
|
440.0
|
|
83
304
|
end
|
|
84
305
|
|
|
306
|
+
# Creates or retrieves a tuning for this scale system.
|
|
307
|
+
#
|
|
308
|
+
# Returns a {ScaleSystemTuning} instance for the specified A frequency.
|
|
309
|
+
# Tunings are cached—repeated calls with same frequency return same instance.
|
|
310
|
+
#
|
|
311
|
+
# @param a_frequency [Numeric] reference A frequency in Hz
|
|
312
|
+
# @return [ScaleSystemTuning] tuning instance
|
|
313
|
+
#
|
|
314
|
+
# @example Standard pitch
|
|
315
|
+
# tuning = ScaleSystem[440.0]
|
|
316
|
+
#
|
|
317
|
+
# @example Baroque pitch
|
|
318
|
+
# baroque = ScaleSystem[415.0]
|
|
319
|
+
#
|
|
320
|
+
# @example Modern high pitch
|
|
321
|
+
# modern = ScaleSystem[442.0]
|
|
85
322
|
def self.[](a_frequency)
|
|
86
323
|
a_frequency = a_frequency.to_f
|
|
87
324
|
|
|
@@ -91,14 +328,34 @@ module Musa
|
|
|
91
328
|
@a_tunings[a_frequency]
|
|
92
329
|
end
|
|
93
330
|
|
|
331
|
+
# Returns semitone offset for a named interval.
|
|
332
|
+
#
|
|
333
|
+
# @param name [Symbol] interval name (e.g., :M3, :P5)
|
|
334
|
+
# @return [Integer] semitone offset
|
|
335
|
+
#
|
|
336
|
+
# @example
|
|
337
|
+
# offset_of_interval(:P5) # => 7
|
|
94
338
|
def self.offset_of_interval(name)
|
|
95
339
|
intervals[name]
|
|
96
340
|
end
|
|
97
341
|
|
|
342
|
+
# Returns the default tuning (A=440Hz).
|
|
343
|
+
#
|
|
344
|
+
# @return [ScaleSystemTuning] default tuning instance
|
|
345
|
+
#
|
|
346
|
+
# @example
|
|
347
|
+
# tuning = ScaleSystem.default_tuning
|
|
98
348
|
def self.default_tuning
|
|
99
349
|
self[default_a_frequency]
|
|
100
350
|
end
|
|
101
351
|
|
|
352
|
+
# Registers a scale kind (major, minor, etc.) with this system.
|
|
353
|
+
#
|
|
354
|
+
# @param scale_kind_class [Class] ScaleKind subclass to register
|
|
355
|
+
# @return [self]
|
|
356
|
+
#
|
|
357
|
+
# @example
|
|
358
|
+
# EquallyTempered12ToneScaleSystem.register MajorScaleKind
|
|
102
359
|
def self.register(scale_kind_class)
|
|
103
360
|
@scale_kind_classes ||= {}
|
|
104
361
|
@scale_kind_classes[scale_kind_class.id] = scale_kind_class
|
|
@@ -108,31 +365,90 @@ module Musa
|
|
|
108
365
|
self
|
|
109
366
|
end
|
|
110
367
|
|
|
368
|
+
# Retrieves a registered scale kind by ID.
|
|
369
|
+
#
|
|
370
|
+
# @param id [Symbol] scale kind identifier
|
|
371
|
+
# @return [Class] ScaleKind subclass
|
|
372
|
+
# @raise [KeyError] if not found
|
|
111
373
|
def self.scale_kind_class(id)
|
|
112
374
|
raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
|
|
113
375
|
|
|
114
376
|
@scale_kind_classes[id]
|
|
115
377
|
end
|
|
116
378
|
|
|
379
|
+
# Checks if a scale kind is registered.
|
|
380
|
+
#
|
|
381
|
+
# @param id [Symbol] scale kind identifier
|
|
382
|
+
# @return [Boolean]
|
|
117
383
|
def self.scale_kind_class?(id)
|
|
118
384
|
@scale_kind_classes.key? id
|
|
119
385
|
end
|
|
120
386
|
|
|
387
|
+
# Returns all registered scale kinds.
|
|
388
|
+
#
|
|
389
|
+
# @return [Hash{Symbol => Class}] scale kind classes
|
|
121
390
|
def self.scale_kind_classes
|
|
122
391
|
@scale_kind_classes
|
|
123
392
|
end
|
|
124
393
|
|
|
394
|
+
# Returns the chromatic scale kind class.
|
|
395
|
+
#
|
|
396
|
+
# @return [Class] chromatic ScaleKind subclass
|
|
397
|
+
# @raise [RuntimeError] if chromatic scale not defined
|
|
125
398
|
def self.chromatic_class
|
|
126
399
|
raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
|
|
127
400
|
|
|
128
401
|
@chromatic_scale_kind_class
|
|
129
402
|
end
|
|
130
403
|
|
|
404
|
+
# Compares scale systems for equality.
|
|
405
|
+
#
|
|
406
|
+
# @param other [ScaleSystem]
|
|
407
|
+
# @return [Boolean]
|
|
131
408
|
def ==(other)
|
|
132
409
|
self.class == other.class
|
|
133
410
|
end
|
|
134
411
|
end
|
|
135
412
|
|
|
413
|
+
# Scale system with specific A frequency tuning.
|
|
414
|
+
#
|
|
415
|
+
# ScaleSystemTuning combines a {ScaleSystem} with a specific reference A frequency,
|
|
416
|
+
# providing access to scale kinds (major, minor, chromatic, etc.) tuned to that
|
|
417
|
+
# frequency.
|
|
418
|
+
#
|
|
419
|
+
# ## Usage
|
|
420
|
+
#
|
|
421
|
+
# Tunings are created via {ScaleSystem.[]}:
|
|
422
|
+
#
|
|
423
|
+
# tuning = Scales[:et12][440.0] # Standard pitch
|
|
424
|
+
# baroque = Scales[:et12][415.0] # Baroque pitch
|
|
425
|
+
#
|
|
426
|
+
# ## Accessing Scales
|
|
427
|
+
#
|
|
428
|
+
# **By symbol**:
|
|
429
|
+
#
|
|
430
|
+
# tuning[:major][60] # C major scale
|
|
431
|
+
#
|
|
432
|
+
# **By method name**:
|
|
433
|
+
#
|
|
434
|
+
# tuning.major[60] # C major scale
|
|
435
|
+
# tuning.minor[69] # A minor scale
|
|
436
|
+
#
|
|
437
|
+
# **Chromatic scale**:
|
|
438
|
+
#
|
|
439
|
+
# tuning.chromatic[60] # C chromatic scale
|
|
440
|
+
#
|
|
441
|
+
# @example Standard usage
|
|
442
|
+
# tuning = Scales::Scales.default_system.default_tuning
|
|
443
|
+
# c_major = tuning.major[60]
|
|
444
|
+
# a_minor = tuning.minor[69]
|
|
445
|
+
#
|
|
446
|
+
# @example Historical pitch
|
|
447
|
+
# baroque = Scales[:et12][415.0]
|
|
448
|
+
# scale = baroque.major[60] # C major at A=415Hz
|
|
449
|
+
#
|
|
450
|
+
# @see ScaleSystem Parent scale system
|
|
451
|
+
# @see ScaleKind Scale types (major, minor, etc.)
|
|
136
452
|
class ScaleSystemTuning
|
|
137
453
|
extend Forwardable
|
|
138
454
|
|
|
@@ -168,6 +484,113 @@ module Musa
|
|
|
168
484
|
@scale_system.frequency_of_pitch(pitch, root, @a_frequency)
|
|
169
485
|
end
|
|
170
486
|
|
|
487
|
+
# Returns scale kinds matching the given metadata criteria.
|
|
488
|
+
#
|
|
489
|
+
# Without arguments, returns all registered scale kinds.
|
|
490
|
+
# With keyword arguments, filters by metadata values.
|
|
491
|
+
# With a block, filters using custom predicate on ScaleKind class.
|
|
492
|
+
#
|
|
493
|
+
# @param metadata_criteria [Hash] metadata key-value pairs to match
|
|
494
|
+
# @yield [kind_class] optional block for custom filtering
|
|
495
|
+
# @yieldparam kind_class [Class] the ScaleKind subclass
|
|
496
|
+
# @yieldreturn [Boolean] true to include this scale kind
|
|
497
|
+
# @return [Array<ScaleKind>] matching scale kind instances
|
|
498
|
+
#
|
|
499
|
+
# @example All scale kinds
|
|
500
|
+
# tuning.scale_kinds
|
|
501
|
+
# # => [major_kind, minor_kind, dorian_kind, ...]
|
|
502
|
+
#
|
|
503
|
+
# @example Filter by metadata
|
|
504
|
+
# tuning.scale_kinds(family: :diatonic)
|
|
505
|
+
# tuning.scale_kinds(brightness: -1..1)
|
|
506
|
+
# tuning.scale_kinds(character: :jazz)
|
|
507
|
+
#
|
|
508
|
+
# @example Filter with block
|
|
509
|
+
# tuning.scale_kinds { |klass| klass.intrinsic_metadata[:has_leading_tone] }
|
|
510
|
+
#
|
|
511
|
+
# @example Combined
|
|
512
|
+
# tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness] < 0 }
|
|
513
|
+
#
|
|
514
|
+
# @see ScaleKind.metadata
|
|
515
|
+
# @see ScaleKind.has_metadata?
|
|
516
|
+
def scale_kinds(**metadata_criteria, &block)
|
|
517
|
+
result = @scale_system.scale_kind_classes.keys.map { |id| self[id] }
|
|
518
|
+
|
|
519
|
+
unless metadata_criteria.empty?
|
|
520
|
+
result = result.select do |kind|
|
|
521
|
+
matches_metadata?(kind.class, metadata_criteria)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if block
|
|
526
|
+
result = result.select { |kind| block.call(kind.class) }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
result
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Searches for a chord across multiple scale types.
|
|
533
|
+
#
|
|
534
|
+
# Iterates through the specified scale kinds and pitch roots to find
|
|
535
|
+
# all scales that contain the given chord. Returns chords with their
|
|
536
|
+
# containing scale as context.
|
|
537
|
+
#
|
|
538
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
539
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
540
|
+
# @param metadata_criteria [Hash] metadata filters for scale kinds
|
|
541
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
542
|
+
#
|
|
543
|
+
# @example Search G7 in greek mode scales
|
|
544
|
+
# tuning = Scales.et12[440.0]
|
|
545
|
+
# g7 = tuning.major[60].dominant.chord :seventh
|
|
546
|
+
# tuning.chords_of(g7, family: :greek_modes)
|
|
547
|
+
#
|
|
548
|
+
# @example Search with brightness filter
|
|
549
|
+
# tuning.chords_of(g7, brightness: -1..1)
|
|
550
|
+
#
|
|
551
|
+
# @example Search in all scale types
|
|
552
|
+
# tuning.chords_of(g7)
|
|
553
|
+
#
|
|
554
|
+
# @see ScaleKind#scales_containing
|
|
555
|
+
# @see Musa::Chords::Chord#in_scales
|
|
556
|
+
def chords_of(chord, roots: nil, **metadata_criteria)
|
|
557
|
+
roots ||= 0...notes_in_octave
|
|
558
|
+
kinds = filtered_scale_kind_ids(**metadata_criteria)
|
|
559
|
+
|
|
560
|
+
kinds.flat_map do |kind_id|
|
|
561
|
+
self[kind_id].scales_containing(chord, roots: roots)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
private
|
|
566
|
+
|
|
567
|
+
def filtered_scale_kind_ids(**metadata_criteria)
|
|
568
|
+
kinds = @scale_system.scale_kind_classes.keys
|
|
569
|
+
|
|
570
|
+
return kinds if metadata_criteria.empty?
|
|
571
|
+
|
|
572
|
+
kinds.select do |kind_id|
|
|
573
|
+
kind_class = @scale_system.scale_kind_class(kind_id)
|
|
574
|
+
matches_metadata?(kind_class, metadata_criteria)
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def matches_metadata?(kind_class, criteria)
|
|
579
|
+
criteria.all? do |key, value|
|
|
580
|
+
actual = kind_class.metadata[key]
|
|
581
|
+
case value
|
|
582
|
+
when Range
|
|
583
|
+
actual.is_a?(Numeric) && value.include?(actual)
|
|
584
|
+
when Array
|
|
585
|
+
value.any? { |v| actual == v || (actual.is_a?(Array) && actual.include?(v)) }
|
|
586
|
+
else
|
|
587
|
+
actual == value || (actual.is_a?(Array) && actual.include?(value))
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
public
|
|
593
|
+
|
|
171
594
|
def ==(other)
|
|
172
595
|
self.class == other.class &&
|
|
173
596
|
@scale_system == other.scale_system &&
|
|
@@ -181,77 +604,447 @@ module Musa
|
|
|
181
604
|
alias to_s inspect
|
|
182
605
|
end
|
|
183
606
|
|
|
607
|
+
# Abstract base class for scale types (major, minor, chromatic, etc.).
|
|
608
|
+
#
|
|
609
|
+
# ScaleKind defines a type of scale (major, minor, chromatic, etc.) independent
|
|
610
|
+
# of root pitch or tuning. It specifies:
|
|
611
|
+
#
|
|
612
|
+
# - Scale degrees and their pitch offsets
|
|
613
|
+
# - Function names for each degree (tonic, dominant, etc.)
|
|
614
|
+
# - Number of grades per octave
|
|
615
|
+
# - Whether the scale is chromatic (contains all pitches)
|
|
616
|
+
#
|
|
617
|
+
# ## Subclass Requirements
|
|
618
|
+
#
|
|
619
|
+
# Subclasses must implement:
|
|
620
|
+
#
|
|
621
|
+
# - {.id}: Unique symbol identifier (:major, :minor, :chromatic, etc.)
|
|
622
|
+
# - {.pitches}: Array defining scale structure
|
|
623
|
+
# - {.chromatic?}: Whether this is the chromatic scale (default: false)
|
|
624
|
+
# - {.grades}: Number of grades per octave (if different from pitches.length)
|
|
625
|
+
#
|
|
626
|
+
# ## Pitch Structure
|
|
627
|
+
#
|
|
628
|
+
# The {.pitches} array defines the scale structure:
|
|
629
|
+
#
|
|
630
|
+
# [{ functions: [:I, :tonic, :_1], pitch: 0 },
|
|
631
|
+
# { functions: [:II, :supertonic, :_2], pitch: 2 },
|
|
632
|
+
# ...]
|
|
633
|
+
#
|
|
634
|
+
# - **functions**: Array of symbols that can access this degree
|
|
635
|
+
# - **pitch**: Semitone offset from root
|
|
636
|
+
#
|
|
637
|
+
# ## Dynamic Method Creation
|
|
638
|
+
#
|
|
639
|
+
# Each scale instance gets methods for all registered scale kinds:
|
|
640
|
+
#
|
|
641
|
+
# note.major # Get major scale rooted on this note
|
|
642
|
+
# note.minor # Get minor scale rooted on this note
|
|
643
|
+
#
|
|
644
|
+
# ## Usage
|
|
645
|
+
#
|
|
646
|
+
# ScaleKind instances are accessed via tuning:
|
|
647
|
+
#
|
|
648
|
+
# tuning = Scales[:et12][440.0]
|
|
649
|
+
# major_kind = tuning[:major] # ScaleKind instance
|
|
650
|
+
# c_major = major_kind[60] # Scale instance
|
|
651
|
+
#
|
|
652
|
+
# Or directly via convenience methods:
|
|
653
|
+
#
|
|
654
|
+
# c_major = tuning.major[60]
|
|
655
|
+
#
|
|
656
|
+
# @abstract Subclass and implement abstract methods
|
|
657
|
+
# @see MajorScaleKind Concrete major scale implementation
|
|
658
|
+
# @see MinorNaturalScaleKind Concrete minor scale implementation
|
|
659
|
+
# @see ChromaticScaleKind Concrete chromatic scale implementation
|
|
660
|
+
# @see Scale Instantiated scale with root pitch
|
|
184
661
|
class ScaleKind
|
|
662
|
+
# Creates a scale kind instance.
|
|
663
|
+
#
|
|
664
|
+
# @param tuning [ScaleSystemTuning] the tuning context
|
|
665
|
+
#
|
|
666
|
+
# @api private
|
|
185
667
|
def initialize(tuning)
|
|
186
668
|
@tuning = tuning
|
|
187
669
|
@scales = {}
|
|
188
670
|
end
|
|
189
671
|
|
|
672
|
+
# The tuning context.
|
|
673
|
+
# @return [ScaleSystemTuning]
|
|
190
674
|
attr_reader :tuning
|
|
191
675
|
|
|
676
|
+
# Creates or retrieves a scale rooted on specific pitch.
|
|
677
|
+
#
|
|
678
|
+
# Scales are cached—repeated calls with same pitch return same instance.
|
|
679
|
+
#
|
|
680
|
+
# @param root_pitch [Integer] MIDI root pitch (60 = middle C)
|
|
681
|
+
# @return [Scale] scale instance
|
|
682
|
+
#
|
|
683
|
+
# @example
|
|
684
|
+
# major_kind = tuning[:major]
|
|
685
|
+
# c_major = major_kind[60] # C major
|
|
686
|
+
# g_major = major_kind[67] # G major
|
|
192
687
|
def [](root_pitch)
|
|
193
688
|
@scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
|
|
194
689
|
@scales[root_pitch]
|
|
195
690
|
end
|
|
196
691
|
|
|
692
|
+
# Returns scale with default root (middle C, MIDI 60).
|
|
693
|
+
#
|
|
694
|
+
# @return [Scale] scale rooted on middle C
|
|
695
|
+
#
|
|
696
|
+
# @example
|
|
697
|
+
# tuning.major.default_root # C major
|
|
197
698
|
def default_root
|
|
198
699
|
self[60]
|
|
199
700
|
end
|
|
200
701
|
|
|
702
|
+
# Returns scale with absolute root (MIDI 0).
|
|
703
|
+
#
|
|
704
|
+
# @return [Scale] scale rooted on MIDI 0
|
|
705
|
+
#
|
|
706
|
+
# @example
|
|
707
|
+
# tuning.major.absolut # Scale rooted at MIDI 0
|
|
201
708
|
def absolut
|
|
202
709
|
self[0]
|
|
203
710
|
end
|
|
204
711
|
|
|
712
|
+
# Finds all scales of this kind that contain the given chord.
|
|
713
|
+
#
|
|
714
|
+
# Searches through scales rooted on different pitches to find which ones
|
|
715
|
+
# contain all the notes of the given chord. Returns chords with their
|
|
716
|
+
# containing scale as context.
|
|
717
|
+
#
|
|
718
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
719
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
720
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
721
|
+
#
|
|
722
|
+
# @example Find G major triad in all major scales
|
|
723
|
+
# tuning = Scales.et12[440.0]
|
|
724
|
+
# g_triad = tuning.major[60].dominant.chord
|
|
725
|
+
# tuning.major.scales_containing(g_triad)
|
|
726
|
+
# # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]
|
|
727
|
+
#
|
|
728
|
+
# @see Scale#chord_on
|
|
729
|
+
# @see ScaleSystemTuning#chords_in_scales
|
|
730
|
+
def scales_containing(chord, roots: nil)
|
|
731
|
+
roots ||= 0...tuning.notes_in_octave
|
|
732
|
+
base_pitch = chord.root.pitch % tuning.notes_in_octave
|
|
733
|
+
|
|
734
|
+
roots.filter_map do |root_offset|
|
|
735
|
+
root_pitch = base_pitch + root_offset
|
|
736
|
+
scale = self[root_pitch]
|
|
737
|
+
scale.chord_on(chord)
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Checks scale kind equality.
|
|
742
|
+
#
|
|
743
|
+
# @param other [ScaleKind]
|
|
744
|
+
# @return [Boolean]
|
|
205
745
|
def ==(other)
|
|
206
746
|
self.class == other.class && @tuning == other.tuning
|
|
207
747
|
end
|
|
208
748
|
|
|
749
|
+
# Returns string representation.
|
|
750
|
+
#
|
|
751
|
+
# @return [String]
|
|
209
752
|
def inspect
|
|
210
753
|
"<#{self.class.name}: tuning = #{@tuning}>"
|
|
211
754
|
end
|
|
212
755
|
|
|
213
756
|
alias to_s inspect
|
|
214
757
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
# @
|
|
758
|
+
# Returns the unique identifier for this scale kind.
|
|
759
|
+
#
|
|
760
|
+
# @abstract Subclass must implement
|
|
761
|
+
# @return [Symbol] scale kind ID (e.g., :major, :minor, :chromatic)
|
|
762
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
763
|
+
#
|
|
764
|
+
# @example
|
|
765
|
+
# MajorScaleKind.id # => :major
|
|
218
766
|
def self.id
|
|
219
767
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
220
768
|
end
|
|
221
769
|
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
770
|
+
# Returns the pitch structure definition.
|
|
771
|
+
#
|
|
772
|
+
# Defines the scale degrees and their pitch offsets from the root.
|
|
773
|
+
# Each entry specifies function names and semitone offset.
|
|
774
|
+
#
|
|
775
|
+
# @abstract Subclass must implement
|
|
776
|
+
# @return [Array<Hash>] array of pitch definitions with:
|
|
777
|
+
# - **:functions** [Array<Symbol>]: function names for this degree
|
|
778
|
+
# - **:pitch** [Integer]: semitone offset from root
|
|
779
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
780
|
+
#
|
|
781
|
+
# @example Major scale structure (partial)
|
|
782
|
+
# [{ functions: [:I, :tonic, :_1], pitch: 0 },
|
|
783
|
+
# { functions: [:II, :supertonic, :_2], pitch: 2 },
|
|
784
|
+
# { functions: [:III, :mediant, :_3], pitch: 4 },
|
|
785
|
+
# ...]
|
|
225
786
|
def self.pitches
|
|
226
787
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
227
788
|
end
|
|
228
789
|
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
790
|
+
# Indicates whether this is the chromatic scale.
|
|
791
|
+
#
|
|
792
|
+
# Only one scale kind per system should return true. The chromatic scale
|
|
793
|
+
# contains all notes in the scale system and is used as a fallback for
|
|
794
|
+
# non-diatonic notes.
|
|
795
|
+
#
|
|
796
|
+
# @return [Boolean] true if chromatic scale (default: false)
|
|
797
|
+
#
|
|
798
|
+
# @example
|
|
799
|
+
# ChromaticScaleKind.chromatic? # => true
|
|
800
|
+
# MajorScaleKind.chromatic? # => false
|
|
232
801
|
def self.chromatic?
|
|
233
802
|
false
|
|
234
803
|
end
|
|
235
804
|
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
805
|
+
# Returns the number of grades per octave.
|
|
806
|
+
#
|
|
807
|
+
# For scales defining extended harmony (8th, 9th, etc.), this returns
|
|
808
|
+
# the number of diatonic degrees within one octave. Defaults to the
|
|
809
|
+
# number of pitch definitions.
|
|
810
|
+
#
|
|
811
|
+
# @return [Integer] number of grades per octave
|
|
812
|
+
#
|
|
813
|
+
# @example
|
|
814
|
+
# MajorScaleKind.grades # => 7 (not 13, even with extended degrees)
|
|
239
815
|
def self.grades
|
|
240
816
|
pitches.length
|
|
241
817
|
end
|
|
242
818
|
|
|
819
|
+
# Returns grade index for a function symbol.
|
|
820
|
+
#
|
|
821
|
+
# @param symbol [Symbol] function name (e.g., :tonic, :dominant, :V)
|
|
822
|
+
# @return [Integer, nil] grade index or nil if not found
|
|
823
|
+
#
|
|
824
|
+
# @example
|
|
825
|
+
# MajorScaleKind.grade_of_function(:tonic) # => 0
|
|
826
|
+
# MajorScaleKind.grade_of_function(:dominant) # => 4
|
|
827
|
+
# MajorScaleKind.grade_of_function(:V) # => 4
|
|
828
|
+
#
|
|
829
|
+
# @api private
|
|
243
830
|
def self.grade_of_function(symbol)
|
|
244
831
|
create_grade_functions_index unless @grade_names_index
|
|
245
832
|
@grade_names_index[symbol]
|
|
246
833
|
end
|
|
247
834
|
|
|
835
|
+
# Returns all function symbols for accessing scale degrees.
|
|
836
|
+
#
|
|
837
|
+
# @return [Array<Symbol>] all function names
|
|
838
|
+
#
|
|
839
|
+
# @example
|
|
840
|
+
# MajorScaleKind.grades_functions
|
|
841
|
+
# # => [:I, :_1, :tonic, :first, :II, :_2, :supertonic, :second, ...]
|
|
842
|
+
#
|
|
843
|
+
# @api private
|
|
248
844
|
def self.grades_functions
|
|
249
845
|
create_grade_functions_index unless @grade_names_index
|
|
250
846
|
@grade_names_index.keys
|
|
251
847
|
end
|
|
252
848
|
|
|
849
|
+
# Returns intrinsic metadata derived from scale structure.
|
|
850
|
+
#
|
|
851
|
+
# This metadata is automatically calculated from the scale's pitch
|
|
852
|
+
# structure and cannot be modified. It includes:
|
|
853
|
+
#
|
|
854
|
+
# - **:id**: Scale kind identifier
|
|
855
|
+
# - **:grades**: Number of diatonic degrees
|
|
856
|
+
# - **:pitches**: Array of pitch offsets from root
|
|
857
|
+
# - **:intervals**: Intervals between consecutive degrees
|
|
858
|
+
# - **:has_leading_tone**: Whether scale has pitch 11 (semitone below octave)
|
|
859
|
+
# - **:has_tritone**: Whether scale contains tritone (pitch 6)
|
|
860
|
+
# - **:symmetric**: Type of symmetry if any (:equal, :palindrome, :repeating)
|
|
861
|
+
#
|
|
862
|
+
# @return [Hash] intrinsic metadata derived from structure
|
|
863
|
+
#
|
|
864
|
+
# @example
|
|
865
|
+
# MajorScaleKind.intrinsic_metadata
|
|
866
|
+
# # => { id: :major, grades: 7, pitches: [0, 2, 4, 5, 7, 9, 11],
|
|
867
|
+
# # intervals: [2, 2, 1, 2, 2, 2], has_leading_tone: true,
|
|
868
|
+
# # has_tritone: true, symmetric: nil }
|
|
869
|
+
def self.intrinsic_metadata
|
|
870
|
+
result = {}
|
|
871
|
+
result[:id] = id if respond_to?(:id)
|
|
872
|
+
result[:grades] = grades if respond_to?(:grades)
|
|
873
|
+
if respond_to?(:pitches)
|
|
874
|
+
result[:pitches] = pitches.map { |p| p[:pitch] }
|
|
875
|
+
result[:intervals] = compute_intervals
|
|
876
|
+
result[:has_leading_tone] = pitches.any? { |p| p[:pitch] == 11 }
|
|
877
|
+
result[:has_tritone] = pitches.any? { |p| p[:pitch] == 6 }
|
|
878
|
+
result[:symmetric] = compute_symmetry
|
|
879
|
+
end
|
|
880
|
+
result.compact
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Returns base metadata defined by the musa-dsl library.
|
|
884
|
+
#
|
|
885
|
+
# This metadata is defined in each ScaleKind subclass using the
|
|
886
|
+
# `@base_metadata` class instance variable. It typically includes:
|
|
887
|
+
#
|
|
888
|
+
# - **:family**: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
|
|
889
|
+
# - **:brightness**: Relative brightness (-3 to +3, major = 0)
|
|
890
|
+
# - **:character**: Array of descriptive tags
|
|
891
|
+
# - **:parent**: Parent scale and degree for modes
|
|
892
|
+
#
|
|
893
|
+
# @return [Hash] library-defined metadata
|
|
894
|
+
#
|
|
895
|
+
# @example
|
|
896
|
+
# MajorScaleKind.base_metadata
|
|
897
|
+
# # => { family: :diatonic, brightness: 0, character: [:bright, :stable] }
|
|
898
|
+
def self.base_metadata
|
|
899
|
+
@base_metadata || {}
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Returns custom metadata added by users at runtime.
|
|
903
|
+
#
|
|
904
|
+
# This metadata is added via {.extend_metadata} and can be cleared
|
|
905
|
+
# with {.reset_custom_metadata}. Takes precedence over base_metadata.
|
|
906
|
+
#
|
|
907
|
+
# @return [Hash] user-defined metadata
|
|
908
|
+
#
|
|
909
|
+
# @example
|
|
910
|
+
# MajorScaleKind.extend_metadata(my_tag: :favorite)
|
|
911
|
+
# MajorScaleKind.custom_metadata # => { my_tag: :favorite }
|
|
912
|
+
def self.custom_metadata
|
|
913
|
+
@custom_metadata || {}
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Adds custom metadata to this scale kind.
|
|
917
|
+
#
|
|
918
|
+
# Custom metadata takes precedence over base_metadata when queried
|
|
919
|
+
# via {.metadata}. Multiple calls merge metadata together.
|
|
920
|
+
#
|
|
921
|
+
# @param metadata [Hash] key-value pairs to add
|
|
922
|
+
# @return [Hash] the updated custom_metadata hash (frozen)
|
|
923
|
+
#
|
|
924
|
+
# @example
|
|
925
|
+
# MajorScaleKind.extend_metadata(my_mood: :happy, rating: 5)
|
|
926
|
+
# MajorScaleKind.extend_metadata(suitable_for: [:pop, :classical])
|
|
927
|
+
# MajorScaleKind.custom_metadata
|
|
928
|
+
# # => { my_mood: :happy, rating: 5, suitable_for: [:pop, :classical] }
|
|
929
|
+
def self.extend_metadata(**metadata)
|
|
930
|
+
@custom_metadata ||= {}
|
|
931
|
+
@custom_metadata = @custom_metadata.merge(metadata).freeze
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Clears all custom metadata from this scale kind.
|
|
935
|
+
#
|
|
936
|
+
# @return [nil]
|
|
937
|
+
#
|
|
938
|
+
# @example
|
|
939
|
+
# MajorScaleKind.extend_metadata(temp: :data)
|
|
940
|
+
# MajorScaleKind.reset_custom_metadata
|
|
941
|
+
# MajorScaleKind.custom_metadata # => {}
|
|
942
|
+
def self.reset_custom_metadata
|
|
943
|
+
@custom_metadata = nil
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# Returns combined metadata from all three layers.
|
|
947
|
+
#
|
|
948
|
+
# Layers are merged with later layers taking precedence:
|
|
949
|
+
# intrinsic_metadata < base_metadata < custom_metadata
|
|
950
|
+
#
|
|
951
|
+
# @return [Hash] combined metadata from all layers
|
|
952
|
+
#
|
|
953
|
+
# @example
|
|
954
|
+
# MajorScaleKind.metadata
|
|
955
|
+
# # => { id: :major, grades: 7, pitches: [...], family: :diatonic, ... }
|
|
956
|
+
def self.metadata
|
|
957
|
+
intrinsic_metadata
|
|
958
|
+
.merge(base_metadata)
|
|
959
|
+
.merge(custom_metadata)
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Returns a specific metadata value.
|
|
963
|
+
#
|
|
964
|
+
# @param key [Symbol] the metadata key
|
|
965
|
+
# @return [Object, nil] the value or nil if not found
|
|
966
|
+
#
|
|
967
|
+
# @example
|
|
968
|
+
# MajorScaleKind.metadata_value(:family) # => :diatonic
|
|
969
|
+
def self.metadata_value(key)
|
|
970
|
+
metadata[key]
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Checks whether metadata contains a key or key-value match.
|
|
974
|
+
#
|
|
975
|
+
# When called with just a key, checks for key existence.
|
|
976
|
+
# When called with key and value, checks for exact match or
|
|
977
|
+
# array inclusion (if metadata value is an array).
|
|
978
|
+
#
|
|
979
|
+
# @param key [Symbol] the metadata key
|
|
980
|
+
# @param value [Object, nil] optional value to match
|
|
981
|
+
# @return [Boolean] whether the condition is satisfied
|
|
982
|
+
#
|
|
983
|
+
# @example Key existence
|
|
984
|
+
# MajorScaleKind.has_metadata?(:family) # => true
|
|
985
|
+
# MajorScaleKind.has_metadata?(:nonexistent) # => false
|
|
986
|
+
#
|
|
987
|
+
# @example Value matching
|
|
988
|
+
# MajorScaleKind.has_metadata?(:family, :diatonic) # => true
|
|
989
|
+
# MajorScaleKind.has_metadata?(:family, :pentatonic) # => false
|
|
990
|
+
#
|
|
991
|
+
# @example Array inclusion
|
|
992
|
+
# MajorScaleKind.has_metadata?(:character, :bright) # => true
|
|
993
|
+
def self.has_metadata?(key, value = nil)
|
|
994
|
+
if value.nil?
|
|
995
|
+
metadata.key?(key)
|
|
996
|
+
else
|
|
997
|
+
metadata[key] == value ||
|
|
998
|
+
(metadata[key].is_a?(Array) && metadata[key].include?(value))
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
|
|
253
1002
|
private
|
|
254
1003
|
|
|
1004
|
+
# Computes intervals between consecutive scale degrees.
|
|
1005
|
+
# @return [Array<Integer>, nil] intervals or nil if not calculable
|
|
1006
|
+
# @api private
|
|
1007
|
+
def self.compute_intervals
|
|
1008
|
+
return nil unless respond_to?(:pitches) && pitches.size > 1
|
|
1009
|
+
pitch_values = pitches.map { |p| p[:pitch] }
|
|
1010
|
+
# Only compute within first octave
|
|
1011
|
+
first_octave = pitch_values.take_while { |p| p < 12 }
|
|
1012
|
+
first_octave.push(12) if first_octave.last != 12
|
|
1013
|
+
first_octave.each_cons(2).map { |a, b| b - a }
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# Computes symmetry type of the scale.
|
|
1017
|
+
# @return [Symbol, nil] :equal, :palindrome, :repeating, or nil
|
|
1018
|
+
# @api private
|
|
1019
|
+
def self.compute_symmetry
|
|
1020
|
+
return nil unless respond_to?(:pitches)
|
|
1021
|
+
intervals = compute_intervals
|
|
1022
|
+
return nil unless intervals && intervals.any?
|
|
1023
|
+
|
|
1024
|
+
# Check if intervals are all equal (e.g., whole tone: [2,2,2,2,2,2])
|
|
1025
|
+
return :equal if intervals.uniq.size == 1
|
|
1026
|
+
|
|
1027
|
+
# Check for palindrome pattern
|
|
1028
|
+
return :palindrome if intervals == intervals.reverse
|
|
1029
|
+
|
|
1030
|
+
# Check for repeating pattern
|
|
1031
|
+
(1..intervals.size / 2).each do |len|
|
|
1032
|
+
pattern = intervals.take(len)
|
|
1033
|
+
if intervals.each_slice(len).all? { |slice| slice == pattern || slice.size < len }
|
|
1034
|
+
return :repeating
|
|
1035
|
+
end
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
nil
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
public
|
|
1042
|
+
|
|
1043
|
+
# Creates internal index mapping function names to grade indices.
|
|
1044
|
+
#
|
|
1045
|
+
# @return [self]
|
|
1046
|
+
#
|
|
1047
|
+
# @api private
|
|
255
1048
|
def self.create_grade_functions_index
|
|
256
1049
|
@grade_names_index = {}
|
|
257
1050
|
pitches.each_index do |i|
|
|
@@ -264,9 +1057,88 @@ module Musa
|
|
|
264
1057
|
end
|
|
265
1058
|
end
|
|
266
1059
|
|
|
1060
|
+
# Instantiated scale with specific root pitch.
|
|
1061
|
+
#
|
|
1062
|
+
# Scale represents a concrete scale (major, minor, etc.) rooted on a specific
|
|
1063
|
+
# pitch. It provides access to scale degrees, interval calculations, frequency
|
|
1064
|
+
# generation, and chord construction.
|
|
1065
|
+
#
|
|
1066
|
+
# ## Creation
|
|
1067
|
+
#
|
|
1068
|
+
# Scales are created via {ScaleKind}:
|
|
1069
|
+
#
|
|
1070
|
+
# tuning = Scales[:et12][440.0]
|
|
1071
|
+
# c_major = tuning.major[60] # Via convenience method
|
|
1072
|
+
# a_minor = tuning[:minor][69] # Via bracket notation
|
|
1073
|
+
#
|
|
1074
|
+
# ## Accessing Notes
|
|
1075
|
+
#
|
|
1076
|
+
# **By numeric grade** (0-based):
|
|
1077
|
+
#
|
|
1078
|
+
# scale[0] # First degree (tonic)
|
|
1079
|
+
# scale[1] # Second degree
|
|
1080
|
+
# scale[4] # Fifth degree
|
|
1081
|
+
#
|
|
1082
|
+
# **By function name** (dynamic methods):
|
|
1083
|
+
#
|
|
1084
|
+
# scale.tonic # First degree
|
|
1085
|
+
# scale.dominant # Fifth degree
|
|
1086
|
+
# scale.mediant # Third degree
|
|
1087
|
+
#
|
|
1088
|
+
# **By Roman numeral**:
|
|
1089
|
+
#
|
|
1090
|
+
# scale[:I] # First degree
|
|
1091
|
+
# scale[:V] # Fifth degree
|
|
1092
|
+
# scale[:IV] # Fourth degree
|
|
1093
|
+
#
|
|
1094
|
+
# **With accidentals** (sharp # or flat _):
|
|
1095
|
+
#
|
|
1096
|
+
# scale[:I#] # Raised tonic
|
|
1097
|
+
# scale[:V_] # Flatted dominant
|
|
1098
|
+
# scale['II##'] # Double-raised second
|
|
1099
|
+
#
|
|
1100
|
+
# ## Note Operations
|
|
1101
|
+
#
|
|
1102
|
+
# Each note is a {NoteInScale} instance with full capabilities:
|
|
1103
|
+
#
|
|
1104
|
+
# note = scale.tonic
|
|
1105
|
+
# note.pitch # MIDI pitch number
|
|
1106
|
+
# note.frequency # Frequency in Hz
|
|
1107
|
+
# note.chord # Build chord from note
|
|
1108
|
+
# note.up(:P5) # Navigate by interval
|
|
1109
|
+
# note.sharp # Raise by semitone
|
|
1110
|
+
#
|
|
1111
|
+
# ## Special Methods
|
|
1112
|
+
#
|
|
1113
|
+
# - **chromatic**: Access chromatic scale at same root
|
|
1114
|
+
# - **octave**: Transpose scale to different octave
|
|
1115
|
+
# - **note_of_pitch**: Find note for specific MIDI pitch
|
|
1116
|
+
#
|
|
1117
|
+
# @example Basic scale access
|
|
1118
|
+
# c_major = tuning.major[60]
|
|
1119
|
+
# c_major.tonic.pitch # => 60 (C)
|
|
1120
|
+
# c_major.dominant.pitch # => 67 (G)
|
|
1121
|
+
# c_major[:III].pitch # => 64 (E)
|
|
1122
|
+
#
|
|
1123
|
+
# @example Chromatic alterations
|
|
1124
|
+
# c_major[:I#].pitch # => 61 (C#)
|
|
1125
|
+
# c_major[:V_].pitch # => 66 (F#/Gb)
|
|
1126
|
+
#
|
|
1127
|
+
# @example Building chords
|
|
1128
|
+
# c_major.tonic.chord # C major triad
|
|
1129
|
+
# c_major.dominant.chord :seventh # G dominant 7th
|
|
1130
|
+
#
|
|
1131
|
+
# @see ScaleKind Scale type definition
|
|
1132
|
+
# @see NoteInScale Individual note in scale
|
|
267
1133
|
class Scale
|
|
268
1134
|
extend Forwardable
|
|
269
1135
|
|
|
1136
|
+
# Creates a scale instance.
|
|
1137
|
+
#
|
|
1138
|
+
# @param kind [ScaleKind] the scale kind
|
|
1139
|
+
# @param root_pitch [Integer] MIDI root pitch
|
|
1140
|
+
#
|
|
1141
|
+
# @api private
|
|
270
1142
|
def initialize(kind, root_pitch:)
|
|
271
1143
|
@notes_by_grade = {}
|
|
272
1144
|
@notes_by_pitch = {}
|
|
@@ -284,28 +1156,95 @@ module Musa
|
|
|
284
1156
|
freeze
|
|
285
1157
|
end
|
|
286
1158
|
|
|
1159
|
+
# Delegates tuning access to kind.
|
|
287
1160
|
def_delegators :@kind, :tuning
|
|
288
1161
|
|
|
289
|
-
|
|
1162
|
+
# Scale kind (major, minor, etc.).
|
|
1163
|
+
# @return [ScaleKind]
|
|
1164
|
+
attr_reader :kind
|
|
1165
|
+
|
|
1166
|
+
# Root pitch (MIDI number).
|
|
1167
|
+
# @return [Integer]
|
|
1168
|
+
attr_reader :root_pitch
|
|
290
1169
|
|
|
1170
|
+
# Returns the root note (first degree).
|
|
1171
|
+
#
|
|
1172
|
+
# Equivalent to scale[0] or scale.tonic.
|
|
1173
|
+
#
|
|
1174
|
+
# @return [NoteInScale] root note
|
|
1175
|
+
#
|
|
1176
|
+
# @example
|
|
1177
|
+
# c_major.root.pitch # => 60
|
|
291
1178
|
def root
|
|
292
1179
|
self[0]
|
|
293
1180
|
end
|
|
294
1181
|
|
|
1182
|
+
# Returns the chromatic scale at the same root.
|
|
1183
|
+
#
|
|
1184
|
+
# @return [Scale] chromatic scale rooted at same pitch
|
|
1185
|
+
#
|
|
1186
|
+
# @example
|
|
1187
|
+
# c_major.chromatic # Chromatic scale starting at C
|
|
295
1188
|
def chromatic
|
|
296
1189
|
@kind.tuning.chromatic[@root_pitch]
|
|
297
1190
|
end
|
|
298
1191
|
|
|
1192
|
+
# Returns the scale rooted at absolute pitch 0.
|
|
1193
|
+
#
|
|
1194
|
+
# @return [Scale] scale of same kind at MIDI 0
|
|
1195
|
+
#
|
|
1196
|
+
# @example
|
|
1197
|
+
# c_major.absolut # Major scale at MIDI 0
|
|
299
1198
|
def absolut
|
|
300
1199
|
@kind[0]
|
|
301
1200
|
end
|
|
302
1201
|
|
|
1202
|
+
# Transposes scale by octaves.
|
|
1203
|
+
#
|
|
1204
|
+
# @param octave [Integer] octave offset (positive = up, negative = down)
|
|
1205
|
+
# @return [Scale] transposed scale
|
|
1206
|
+
# @raise [ArgumentError] if octave is not integer
|
|
1207
|
+
#
|
|
1208
|
+
# @example
|
|
1209
|
+
# c_major.octave(1) # C major one octave higher
|
|
1210
|
+
# c_major.octave(-1) # C major one octave lower
|
|
303
1211
|
def octave(octave)
|
|
304
1212
|
raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
|
|
305
1213
|
|
|
306
1214
|
@kind[@root_pitch + octave * @kind.class.grades]
|
|
307
1215
|
end
|
|
308
1216
|
|
|
1217
|
+
# Accesses scale degree by grade, symbol, or function name.
|
|
1218
|
+
#
|
|
1219
|
+
# Supports multiple access patterns:
|
|
1220
|
+
# - **Integer**: Numeric grade (0-based)
|
|
1221
|
+
# - **Symbol/String**: Function name or Roman numeral
|
|
1222
|
+
# - **With accidentals**: Add '#' for sharp, '_' for flat
|
|
1223
|
+
#
|
|
1224
|
+
# Notes are cached—repeated access returns same instance.
|
|
1225
|
+
#
|
|
1226
|
+
# @param grade_or_symbol [Integer, Symbol, String] degree specifier
|
|
1227
|
+
# @return [NoteInScale] note at specified degree
|
|
1228
|
+
# @raise [ArgumentError] if grade_or_symbol is invalid type
|
|
1229
|
+
#
|
|
1230
|
+
# @example Numeric access
|
|
1231
|
+
# scale[0] # Tonic
|
|
1232
|
+
# scale[4] # Dominant (in major/minor)
|
|
1233
|
+
#
|
|
1234
|
+
# @example Function name access
|
|
1235
|
+
# scale[:tonic]
|
|
1236
|
+
# scale[:dominant]
|
|
1237
|
+
# scale[:mediant]
|
|
1238
|
+
#
|
|
1239
|
+
# @example Roman numeral access
|
|
1240
|
+
# scale[:I] # Tonic
|
|
1241
|
+
# scale[:V] # Dominant
|
|
1242
|
+
# scale[:IV] # Subdominant
|
|
1243
|
+
#
|
|
1244
|
+
# @example With accidentals
|
|
1245
|
+
# scale[:I#] # Raised tonic
|
|
1246
|
+
# scale[:V_] # Flatted dominant
|
|
1247
|
+
# scale['II##'] # Double-raised second
|
|
309
1248
|
def [](grade_or_symbol)
|
|
310
1249
|
|
|
311
1250
|
raise ArgumentError, "grade_or_symbol '#{grade_or_symbol}' should be a Integer, String or Symbol" unless grade_or_symbol.is_a?(Symbol) || grade_or_symbol.is_a?(String) || grade_or_symbol.is_a?(Integer)
|
|
@@ -330,6 +1269,12 @@ module Musa
|
|
|
330
1269
|
@notes_by_grade[wide_grade].sharp(sharps)
|
|
331
1270
|
end
|
|
332
1271
|
|
|
1272
|
+
# Converts grade specifier to numeric grade and accidentals.
|
|
1273
|
+
#
|
|
1274
|
+
# @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
|
|
1275
|
+
# @return [Array(Integer, Integer)] wide grade and accidentals count
|
|
1276
|
+
#
|
|
1277
|
+
# @api private
|
|
333
1278
|
def grade_of(grade_or_string_or_symbol)
|
|
334
1279
|
name, wide_grade, accidentals = parse_grade(grade_or_string_or_symbol)
|
|
335
1280
|
|
|
@@ -343,6 +1288,15 @@ module Musa
|
|
|
343
1288
|
return octave * @kind.class.grades + grade, accidentals
|
|
344
1289
|
end
|
|
345
1290
|
|
|
1291
|
+
# Parses grade string/symbol into components.
|
|
1292
|
+
#
|
|
1293
|
+
# Handles formats like "I#", ":V_", "7##", extracting function name,
|
|
1294
|
+
# numeric grade, and accidentals.
|
|
1295
|
+
#
|
|
1296
|
+
# @param neuma_grade [Integer, Symbol, String] grade to parse
|
|
1297
|
+
# @return [Array(Symbol, Integer, Integer)] name, wide_grade, accidentals
|
|
1298
|
+
#
|
|
1299
|
+
# @api private
|
|
346
1300
|
def parse_grade(neuma_grade)
|
|
347
1301
|
name = wide_grade = nil
|
|
348
1302
|
accidentals = 0
|
|
@@ -371,6 +1325,24 @@ module Musa
|
|
|
371
1325
|
return name, wide_grade, accidentals
|
|
372
1326
|
end
|
|
373
1327
|
|
|
1328
|
+
# Finds note for a specific MIDI pitch.
|
|
1329
|
+
#
|
|
1330
|
+
# Searches for a note in the scale matching the given pitch. Options control
|
|
1331
|
+
# behavior when pitch is not in scale.
|
|
1332
|
+
#
|
|
1333
|
+
# @param pitch [Integer] MIDI pitch number
|
|
1334
|
+
# @param allow_chromatic [Boolean] if true, return chromatic note when not in scale
|
|
1335
|
+
# @param allow_nearest [Boolean] if true, return nearest scale note
|
|
1336
|
+
# @return [NoteInScale, nil] matching note or nil
|
|
1337
|
+
#
|
|
1338
|
+
# @example Diatonic note
|
|
1339
|
+
# c_major.note_of_pitch(64) # => E (in scale)
|
|
1340
|
+
#
|
|
1341
|
+
# @example Chromatic note
|
|
1342
|
+
# c_major.note_of_pitch(63, allow_chromatic: true) # => Eb (chromatic)
|
|
1343
|
+
#
|
|
1344
|
+
# @example Nearest note
|
|
1345
|
+
# c_major.note_of_pitch(63, allow_nearest: true) # => E or D (nearest)
|
|
374
1346
|
def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
|
|
375
1347
|
allow_chromatic ||= false
|
|
376
1348
|
allow_nearest ||= false
|
|
@@ -407,16 +1379,107 @@ module Musa
|
|
|
407
1379
|
note
|
|
408
1380
|
end
|
|
409
1381
|
|
|
1382
|
+
# Returns semitone offset for a named interval.
|
|
1383
|
+
#
|
|
1384
|
+
# @param interval_name [Symbol] interval name (e.g., :M3, :P5)
|
|
1385
|
+
# @return [Integer] semitone offset
|
|
1386
|
+
#
|
|
1387
|
+
# @example
|
|
1388
|
+
# scale.offset_of_interval(:P5) # => 7
|
|
1389
|
+
# scale.offset_of_interval(:M3) # => 4
|
|
410
1390
|
def offset_of_interval(interval_name)
|
|
411
1391
|
@kind.tuning.offset_of_interval(interval_name)
|
|
412
1392
|
end
|
|
413
1393
|
|
|
1394
|
+
# Checks if all chord pitches exist in this scale.
|
|
1395
|
+
#
|
|
1396
|
+
# Uses the chord's definition to verify that every pitch in the chord
|
|
1397
|
+
# can be found as a diatonic note in this scale.
|
|
1398
|
+
#
|
|
1399
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1400
|
+
# @return [Boolean] true if all chord notes are in scale
|
|
1401
|
+
#
|
|
1402
|
+
# @example
|
|
1403
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1404
|
+
# g7 = c_major.dominant.chord :seventh
|
|
1405
|
+
# c_major.contains_chord?(g7) # => true
|
|
1406
|
+
#
|
|
1407
|
+
# cm = c_major.tonic.chord.with_quality(:minor)
|
|
1408
|
+
# c_major.contains_chord?(cm) # => false (Eb not in C major)
|
|
1409
|
+
#
|
|
1410
|
+
# @see #degree_of_chord
|
|
1411
|
+
# @see #chord_on
|
|
1412
|
+
def contains_chord?(chord)
|
|
1413
|
+
chord.chord_definition.in_scale?(self, chord_root_pitch: chord.root.pitch)
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
# Returns the grade (0-based) where the chord root falls in this scale.
|
|
1417
|
+
#
|
|
1418
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1419
|
+
# @return [Integer, nil] grade (0-based) or nil if chord not in scale
|
|
1420
|
+
#
|
|
1421
|
+
# @example
|
|
1422
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1423
|
+
# g_chord = c_major.dominant.chord
|
|
1424
|
+
# c_major.degree_of_chord(g_chord) # => 4 (V degree, 0-based)
|
|
1425
|
+
#
|
|
1426
|
+
# @see #contains_chord?
|
|
1427
|
+
def degree_of_chord(chord)
|
|
1428
|
+
return nil unless contains_chord?(chord)
|
|
1429
|
+
|
|
1430
|
+
note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
|
|
1431
|
+
note&.grade
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
# Creates an equivalent chord with this scale as its context.
|
|
1435
|
+
#
|
|
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.
|
|
1439
|
+
#
|
|
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
|
|
1442
|
+
#
|
|
1443
|
+
# @example
|
|
1444
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1445
|
+
# g7 = c_major.dominant.chord :seventh
|
|
1446
|
+
#
|
|
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
|
|
1450
|
+
#
|
|
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
|
+
)
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
# Checks scale equality.
|
|
1469
|
+
#
|
|
1470
|
+
# Scales are equal if they have same kind and root pitch.
|
|
1471
|
+
#
|
|
1472
|
+
# @param other [Scale]
|
|
1473
|
+
# @return [Boolean]
|
|
414
1474
|
def ==(other)
|
|
415
1475
|
self.class == other.class &&
|
|
416
1476
|
@kind == other.kind &&
|
|
417
1477
|
@root_pitch == other.root_pitch
|
|
418
1478
|
end
|
|
419
1479
|
|
|
1480
|
+
# Returns string representation.
|
|
1481
|
+
#
|
|
1482
|
+
# @return [String]
|
|
420
1483
|
def inspect
|
|
421
1484
|
"<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
|
|
422
1485
|
end
|
|
@@ -424,13 +1487,108 @@ module Musa
|
|
|
424
1487
|
alias to_s inspect
|
|
425
1488
|
end
|
|
426
1489
|
|
|
1490
|
+
# Note within a scale context.
|
|
1491
|
+
#
|
|
1492
|
+
# NoteInScale represents a specific note within a scale, providing rich musical
|
|
1493
|
+
# functionality including:
|
|
1494
|
+
# - Pitch and frequency information
|
|
1495
|
+
# - Interval navigation (up, down, by named intervals)
|
|
1496
|
+
# - Chromatic alterations (sharp, flat)
|
|
1497
|
+
# - Scale navigation (change scales while keeping pitch)
|
|
1498
|
+
# - Chord construction
|
|
1499
|
+
# - Octave transposition
|
|
1500
|
+
#
|
|
1501
|
+
# ## Creation
|
|
1502
|
+
#
|
|
1503
|
+
# Notes are created via scale access, not directly:
|
|
1504
|
+
#
|
|
1505
|
+
# scale = tuning.major[60]
|
|
1506
|
+
# note = scale.tonic # NoteInScale instance
|
|
1507
|
+
# note = scale[:V] # Another NoteInScale
|
|
1508
|
+
#
|
|
1509
|
+
# ## Basic Properties
|
|
1510
|
+
#
|
|
1511
|
+
# note.pitch # MIDI pitch number
|
|
1512
|
+
# note.grade # Scale degree (0-based)
|
|
1513
|
+
# note.octave # Octave relative to scale root
|
|
1514
|
+
# note.frequency # Frequency in Hz
|
|
1515
|
+
# note.functions # Function names for this degree
|
|
1516
|
+
#
|
|
1517
|
+
# ## Interval Navigation
|
|
1518
|
+
#
|
|
1519
|
+
# **Natural intervals** (diatonic, within scale):
|
|
1520
|
+
#
|
|
1521
|
+
# note.up(2) # Up 2 scale degrees
|
|
1522
|
+
# note.down(1) # Down 1 scale degree
|
|
1523
|
+
#
|
|
1524
|
+
# **Chromatic intervals** (by semitones or named intervals):
|
|
1525
|
+
#
|
|
1526
|
+
# note.up(:P5) # Up perfect fifth
|
|
1527
|
+
# note.up(7) # Up 7 semitones (if chromatic specified)
|
|
1528
|
+
# note.down(:M3) # Down major third
|
|
1529
|
+
#
|
|
1530
|
+
# ## Chromatic Alterations
|
|
1531
|
+
#
|
|
1532
|
+
# note.sharp # Raise by 1 semitone
|
|
1533
|
+
# note.sharp(2) # Raise by 2 semitones
|
|
1534
|
+
# note.flat # Lower by 1 semitone
|
|
1535
|
+
# note.flat(2) # Lower by 2 semitones
|
|
1536
|
+
#
|
|
1537
|
+
# ## Scale Navigation
|
|
1538
|
+
#
|
|
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
|
|
1542
|
+
#
|
|
1543
|
+
# ## Chord Construction
|
|
1544
|
+
#
|
|
1545
|
+
# note.chord # Build triad
|
|
1546
|
+
# note.chord :seventh # Build seventh chord
|
|
1547
|
+
# note.chord quality: :minor # Build with features
|
|
1548
|
+
#
|
|
1549
|
+
# ## Background Scale Context
|
|
1550
|
+
#
|
|
1551
|
+
# Chromatic notes remember their diatonic context:
|
|
1552
|
+
#
|
|
1553
|
+
# c# = c_major.tonic.sharp # C# in C major context
|
|
1554
|
+
# c#.background_scale # => c_major
|
|
1555
|
+
# c#.background_note # => C (natural)
|
|
1556
|
+
# c#.background_sharps # => 1
|
|
1557
|
+
#
|
|
1558
|
+
# @example Basic usage
|
|
1559
|
+
# c_major = tuning.major[60]
|
|
1560
|
+
# tonic = c_major.tonic
|
|
1561
|
+
# tonic.pitch # => 60
|
|
1562
|
+
# tonic.frequency # => ~261.63 Hz
|
|
1563
|
+
#
|
|
1564
|
+
# @example Interval navigation
|
|
1565
|
+
# tonic.up(:P5).pitch # => 67 (G)
|
|
1566
|
+
# tonic.up(4, :natural).pitch # => 71 (4 scale degrees = B)
|
|
1567
|
+
#
|
|
1568
|
+
# @example Chromatic alterations
|
|
1569
|
+
# tonic.sharp.pitch # => 61 (C#)
|
|
1570
|
+
# tonic.flat.pitch # => 59 (B)
|
|
1571
|
+
#
|
|
1572
|
+
# @example Chord building
|
|
1573
|
+
# tonic.chord # C major triad
|
|
1574
|
+
# tonic.chord :seventh # C major 7th
|
|
1575
|
+
#
|
|
1576
|
+
# @see Scale Parent scale
|
|
1577
|
+
# @see Chord Chord construction
|
|
427
1578
|
class NoteInScale
|
|
428
1579
|
|
|
429
|
-
#
|
|
430
|
-
# @param grade []
|
|
431
|
-
# @param octave [Integer]
|
|
432
|
-
# @param pitch [Number] pitch of the note, based on MIDI note numbers. Can be Integer, Rational or Float to express fractions of a semitone
|
|
1580
|
+
# Creates a note within a scale.
|
|
433
1581
|
#
|
|
1582
|
+
# @param scale [Scale] parent scale
|
|
1583
|
+
# @param grade [Integer] scale degree (0-based)
|
|
1584
|
+
# @param octave [Integer] octave relative to scale root
|
|
1585
|
+
# @param pitch [Numeric] MIDI pitch (Integer, Rational, or Float for microtones)
|
|
1586
|
+
# @param background_scale [Scale, nil] diatonic context for chromatic notes
|
|
1587
|
+
# @param background_grade [Integer, nil] diatonic grade for chromatic notes
|
|
1588
|
+
# @param background_octave [Integer, nil] diatonic octave for chromatic notes
|
|
1589
|
+
# @param background_sharps [Integer, nil] sharps/flats from diatonic note
|
|
1590
|
+
#
|
|
1591
|
+
# @api private
|
|
434
1592
|
def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
|
|
435
1593
|
@scale = scale
|
|
436
1594
|
@grade = grade
|
|
@@ -449,12 +1607,44 @@ module Musa
|
|
|
449
1607
|
end
|
|
450
1608
|
end
|
|
451
1609
|
|
|
452
|
-
|
|
1610
|
+
# Scale degree (0-based).
|
|
1611
|
+
# @return [Integer]
|
|
1612
|
+
attr_reader :grade
|
|
1613
|
+
|
|
1614
|
+
# MIDI pitch number.
|
|
1615
|
+
# @return [Numeric]
|
|
1616
|
+
attr_reader :pitch
|
|
453
1617
|
|
|
1618
|
+
# Returns function names for this scale degree.
|
|
1619
|
+
#
|
|
1620
|
+
# @return [Array<Symbol>] function symbols
|
|
1621
|
+
#
|
|
1622
|
+
# @example
|
|
1623
|
+
# c_major.tonic.functions # => [:I, :_1, :tonic, :first]
|
|
454
1624
|
def functions
|
|
455
1625
|
@scale.kind.class.pitches[grade][:functions]
|
|
456
1626
|
end
|
|
457
1627
|
|
|
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.
|
|
1633
|
+
#
|
|
1634
|
+
# @param octave [Integer, nil] octave offset (nil to query current)
|
|
1635
|
+
# @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)
|
|
1641
|
+
#
|
|
1642
|
+
# @example Transpose relative
|
|
1643
|
+
# note.octave(1).pitch # Up one octave from current
|
|
1644
|
+
# note.octave(-1).pitch # Down one octave from current
|
|
1645
|
+
#
|
|
1646
|
+
# @example Transpose absolute
|
|
1647
|
+
# note.octave(2, absolute: true).pitch # At octave 2, regardless of current
|
|
458
1648
|
def octave(octave = nil, absolute: false)
|
|
459
1649
|
if octave.nil?
|
|
460
1650
|
@octave
|
|
@@ -465,6 +1655,18 @@ module Musa
|
|
|
465
1655
|
end
|
|
466
1656
|
end
|
|
467
1657
|
|
|
1658
|
+
# Creates a copy with background scale context.
|
|
1659
|
+
#
|
|
1660
|
+
# Used internally when creating chromatic notes to remember their
|
|
1661
|
+
# diatonic context.
|
|
1662
|
+
#
|
|
1663
|
+
# @param scale [Scale] background diatonic scale
|
|
1664
|
+
# @param grade [Integer, nil] background grade
|
|
1665
|
+
# @param octave [Integer, nil] background octave
|
|
1666
|
+
# @param sharps [Integer, nil] accidentals from background note
|
|
1667
|
+
# @return [NoteInScale] new note with background context
|
|
1668
|
+
#
|
|
1669
|
+
# @api private
|
|
468
1670
|
def with_background(scale:, grade: nil, octave: nil, sharps: nil)
|
|
469
1671
|
NoteInScale.new(@scale, @grade, @octave, @pitch,
|
|
470
1672
|
background_scale: scale,
|
|
@@ -473,18 +1675,55 @@ module Musa
|
|
|
473
1675
|
background_sharps: sharps)
|
|
474
1676
|
end
|
|
475
1677
|
|
|
1678
|
+
# Background diatonic scale (for chromatic notes).
|
|
1679
|
+
# @return [Scale, nil]
|
|
476
1680
|
attr_reader :background_scale
|
|
477
1681
|
|
|
1682
|
+
# Returns the diatonic note this chromatic note is based on.
|
|
1683
|
+
#
|
|
1684
|
+
# @return [NoteInScale, nil] background note or nil
|
|
1685
|
+
#
|
|
1686
|
+
# @example
|
|
1687
|
+
# c# = c_major.tonic.sharp
|
|
1688
|
+
# c#.background_note.pitch # => 60 (C natural)
|
|
478
1689
|
def background_note
|
|
479
1690
|
@background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
|
|
480
1691
|
end
|
|
481
1692
|
|
|
1693
|
+
# Sharps/flats from background note.
|
|
1694
|
+
# @return [Integer, nil]
|
|
482
1695
|
attr_reader :background_sharps
|
|
483
1696
|
|
|
1697
|
+
# Returns wide grade (grade + octave * grades_per_octave).
|
|
1698
|
+
#
|
|
1699
|
+
# @return [Integer]
|
|
1700
|
+
#
|
|
1701
|
+
# @example
|
|
1702
|
+
# note.wide_grade # => 7 (second octave, first degree)
|
|
1703
|
+
#
|
|
1704
|
+
# @api private
|
|
484
1705
|
def wide_grade
|
|
485
1706
|
@grade + @octave * @scale.kind.class.grades
|
|
486
1707
|
end
|
|
487
1708
|
|
|
1709
|
+
# Navigates upward by interval.
|
|
1710
|
+
#
|
|
1711
|
+
# Supports both natural (diatonic) and chromatic (semitone) intervals.
|
|
1712
|
+
#
|
|
1713
|
+
# - **Numeric interval + :natural**: Move by scale degrees
|
|
1714
|
+
# - **Symbol or numeric interval + :chromatic**: Move by semitones or named interval
|
|
1715
|
+
#
|
|
1716
|
+
# @param interval_name_or_interval [Symbol, Integer] interval
|
|
1717
|
+
# @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
|
|
1718
|
+
# @param sign [Integer] direction multiplier (internal use)
|
|
1719
|
+
# @return [NoteInScale] note at interval above
|
|
1720
|
+
#
|
|
1721
|
+
# @example Natural interval (scale degrees)
|
|
1722
|
+
# note.up(2, :natural) # Up 2 scale degrees
|
|
1723
|
+
#
|
|
1724
|
+
# @example Chromatic interval (semitones)
|
|
1725
|
+
# note.up(:P5) # Up perfect fifth (7 semitones)
|
|
1726
|
+
# note.up(7) # Up 7 semitones (if chromatic)
|
|
488
1727
|
def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)
|
|
489
1728
|
|
|
490
1729
|
sign ||= 1
|
|
@@ -525,24 +1764,78 @@ module Musa
|
|
|
525
1764
|
|
|
526
1765
|
private :calculate_note_of_pitch
|
|
527
1766
|
|
|
1767
|
+
# Navigates downward by interval.
|
|
1768
|
+
#
|
|
1769
|
+
# Same as {#up} but in reverse direction.
|
|
1770
|
+
#
|
|
1771
|
+
# @param interval_name_or_interval [Symbol, Integer] interval
|
|
1772
|
+
# @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
|
|
1773
|
+
# @return [NoteInScale] note at interval below
|
|
1774
|
+
#
|
|
1775
|
+
# @example
|
|
1776
|
+
# note.down(2, :natural) # Down 2 scale degrees
|
|
1777
|
+
# note.down(:P5) # Down perfect fifth
|
|
528
1778
|
def down(interval_name_or_interval, natural_or_chromatic = nil)
|
|
529
1779
|
up(interval_name_or_interval, natural_or_chromatic, sign: -1)
|
|
530
1780
|
end
|
|
531
1781
|
|
|
1782
|
+
# Raises note by semitones (adds sharps).
|
|
1783
|
+
#
|
|
1784
|
+
# @param count [Integer, nil] number of semitones (default 1)
|
|
1785
|
+
# @return [NoteInScale] raised note
|
|
1786
|
+
#
|
|
1787
|
+
# @example
|
|
1788
|
+
# note.sharp.pitch # Up 1 semitone
|
|
1789
|
+
# note.sharp(2).pitch # Up 2 semitones
|
|
532
1790
|
def sharp(count = nil)
|
|
533
1791
|
count ||= 1
|
|
534
1792
|
calculate_note_of_pitch(@pitch, count)
|
|
535
1793
|
end
|
|
536
1794
|
|
|
1795
|
+
# Lowers note by semitones (adds flats).
|
|
1796
|
+
#
|
|
1797
|
+
# @param count [Integer, nil] number of semitones (default 1)
|
|
1798
|
+
# @return [NoteInScale] lowered note
|
|
1799
|
+
#
|
|
1800
|
+
# @example
|
|
1801
|
+
# note.flat.pitch # Down 1 semitone
|
|
1802
|
+
# note.flat(2).pitch # Down 2 semitones
|
|
537
1803
|
def flat(count = nil)
|
|
538
1804
|
count ||= 1
|
|
539
1805
|
sharp(-count)
|
|
540
1806
|
end
|
|
541
1807
|
|
|
1808
|
+
# Calculates frequency in Hz.
|
|
1809
|
+
#
|
|
1810
|
+
# Uses the scale system's frequency calculation (equal temperament,
|
|
1811
|
+
# just intonation, etc.) and the tuning's A frequency.
|
|
1812
|
+
#
|
|
1813
|
+
# @return [Float] frequency in Hz
|
|
1814
|
+
#
|
|
1815
|
+
# @example
|
|
1816
|
+
# c_major.tonic.frequency # => ~261.63 Hz (middle C at A=440)
|
|
542
1817
|
def frequency
|
|
543
|
-
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.
|
|
1818
|
+
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
|
|
544
1819
|
end
|
|
545
1820
|
|
|
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.
|
|
1826
|
+
#
|
|
1827
|
+
# @param kind_id_or_kind [Symbol, ScaleKind, nil] scale kind or ID
|
|
1828
|
+
# @return [Scale, NoteInScale] current scale or note in new scale
|
|
1829
|
+
#
|
|
1830
|
+
# @example Query current scale
|
|
1831
|
+
# note.scale # => <Scale: kind = MajorScaleKind ...>
|
|
1832
|
+
#
|
|
1833
|
+
# @example Change to minor
|
|
1834
|
+
# note.scale(:minor) # Same pitch in minor scale
|
|
1835
|
+
#
|
|
1836
|
+
# @example Dynamic method
|
|
1837
|
+
# note.minor # Same as note.scale(:minor)
|
|
1838
|
+
# note.major # Same as note.scale(:major)
|
|
546
1839
|
def scale(kind_id_or_kind = nil)
|
|
547
1840
|
if kind_id_or_kind.nil?
|
|
548
1841
|
@scale
|
|
@@ -555,10 +1848,52 @@ module Musa
|
|
|
555
1848
|
end
|
|
556
1849
|
end
|
|
557
1850
|
|
|
1851
|
+
# Finds this note in another scale.
|
|
1852
|
+
#
|
|
1853
|
+
# Searches for a note with the same pitch in the target scale.
|
|
1854
|
+
#
|
|
1855
|
+
# @param scale [Scale] target scale to search
|
|
1856
|
+
# @return [NoteInScale, nil] note in target scale or nil
|
|
1857
|
+
#
|
|
1858
|
+
# @example
|
|
1859
|
+
# c_major_tonic = c_major.tonic
|
|
1860
|
+
# c_minor = tuning.minor[60]
|
|
1861
|
+
# c_major_tonic.on(c_minor) # C in C minor scale
|
|
558
1862
|
def on(scale)
|
|
559
1863
|
scale.note_of_pitch @pitch
|
|
560
1864
|
end
|
|
561
1865
|
|
|
1866
|
+
# Builds a chord rooted on this note.
|
|
1867
|
+
#
|
|
1868
|
+
# Creates a chord using this note as the root. Chord can be specified by:
|
|
1869
|
+
# - Feature values (:triad, :seventh, :major, :minor, etc.)
|
|
1870
|
+
# - Feature hash (quality:, size:)
|
|
1871
|
+
# - Chord definition name (not shown here, see Chord.with_root)
|
|
1872
|
+
#
|
|
1873
|
+
# If no features specified, defaults to major triad.
|
|
1874
|
+
#
|
|
1875
|
+
# @param feature_values [Array<Symbol>] feature values (size, quality, etc.)
|
|
1876
|
+
# @param allow_chromatic [Boolean] allow non-diatonic chord notes
|
|
1877
|
+
# @param move [Hash{Symbol => Integer}] initial octave moves
|
|
1878
|
+
# @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
|
|
1879
|
+
# @param features_hash [Hash] feature key-value pairs
|
|
1880
|
+
# @return [Chord] chord rooted on this note
|
|
1881
|
+
#
|
|
1882
|
+
# @example Default triad
|
|
1883
|
+
# note.chord # Major triad
|
|
1884
|
+
#
|
|
1885
|
+
# @example Specified size
|
|
1886
|
+
# note.chord :seventh # Seventh chord matching scale
|
|
1887
|
+
# note.chord :ninth # Ninth chord
|
|
1888
|
+
#
|
|
1889
|
+
# @example With features
|
|
1890
|
+
# note.chord quality: :minor, size: :seventh
|
|
1891
|
+
# note.chord :minor, :seventh # Same as above
|
|
1892
|
+
#
|
|
1893
|
+
# @example With voicing
|
|
1894
|
+
# note.chord :seventh, move: {root: -1}, duplicate: {fifth: 1}
|
|
1895
|
+
#
|
|
1896
|
+
# @see Chord Chord class
|
|
562
1897
|
def chord(*feature_values,
|
|
563
1898
|
allow_chromatic: nil,
|
|
564
1899
|
move: nil,
|
|
@@ -575,6 +1910,12 @@ module Musa
|
|
|
575
1910
|
**features)
|
|
576
1911
|
end
|
|
577
1912
|
|
|
1913
|
+
# Checks note equality.
|
|
1914
|
+
#
|
|
1915
|
+
# Notes are equal if they have same scale, grade, octave, and pitch.
|
|
1916
|
+
#
|
|
1917
|
+
# @param other [NoteInScale]
|
|
1918
|
+
# @return [Boolean]
|
|
578
1919
|
def ==(other)
|
|
579
1920
|
self.class == other.class &&
|
|
580
1921
|
@scale == other.scale &&
|
|
@@ -583,6 +1924,9 @@ module Musa
|
|
|
583
1924
|
@pitch == other.pitch
|
|
584
1925
|
end
|
|
585
1926
|
|
|
1927
|
+
# Returns string representation.
|
|
1928
|
+
#
|
|
1929
|
+
# @return [String]
|
|
586
1930
|
def inspect
|
|
587
1931
|
"<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
|
|
588
1932
|
end
|