musa-dsl 0.30.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- 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 +233 -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 +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -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 +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -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 +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -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 +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -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 +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -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 +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- metadata +87 -8
|
@@ -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,122 @@ 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
|
|
28
149
|
end
|
|
29
150
|
|
|
151
|
+
# Abstract base class for musical scale systems.
|
|
152
|
+
#
|
|
153
|
+
# ScaleSystem defines the foundation of a tuning system, including:
|
|
154
|
+
#
|
|
155
|
+
# - Number of notes per octave
|
|
156
|
+
# - Available intervals
|
|
157
|
+
# - Frequency calculation method
|
|
158
|
+
# - Registered scale kinds (major, minor, etc.)
|
|
159
|
+
#
|
|
160
|
+
# ## Subclass Requirements
|
|
161
|
+
#
|
|
162
|
+
# Subclasses must implement:
|
|
163
|
+
#
|
|
164
|
+
# - {.id}: Unique symbol identifier
|
|
165
|
+
# - {.notes_in_octave}: Number of notes in an octave
|
|
166
|
+
# - {.part_of_tone_size}: Size of smallest pitch unit (for sharps/flats)
|
|
167
|
+
# - {.intervals}: Hash of named intervals to semitone offsets
|
|
168
|
+
# - {.frequency_of_pitch}: Pitch to frequency conversion
|
|
169
|
+
#
|
|
170
|
+
# Optionally override:
|
|
171
|
+
#
|
|
172
|
+
# - {.default_a_frequency}: Reference A frequency (defaults to 440.0 Hz)
|
|
173
|
+
#
|
|
174
|
+
# ## Usage
|
|
175
|
+
#
|
|
176
|
+
# ScaleSystem is accessed via {Scales} module, not instantiated directly:
|
|
177
|
+
#
|
|
178
|
+
# system = Scales[:et12] # Get system
|
|
179
|
+
# tuning = system[440.0] # Get tuning
|
|
180
|
+
# scale = tuning.major[60] # Get scale
|
|
181
|
+
#
|
|
182
|
+
# @abstract Subclass and implement abstract methods
|
|
183
|
+
# @see EquallyTempered12ToneScaleSystem Concrete 12-tone implementation
|
|
184
|
+
# @see ScaleSystemTuning Tuning with specific A frequency
|
|
30
185
|
class ScaleSystem
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# @
|
|
186
|
+
# Returns the unique identifier for this scale system.
|
|
187
|
+
#
|
|
188
|
+
# @abstract Subclass must implement
|
|
189
|
+
# @return [Symbol] the scale system ID (e.g., :et12)
|
|
190
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
34
191
|
#
|
|
192
|
+
# @example
|
|
193
|
+
# EquallyTempered12ToneScaleSystem.id # => :et12
|
|
35
194
|
def self.id
|
|
36
195
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
37
196
|
end
|
|
38
197
|
|
|
39
|
-
#
|
|
40
|
-
# @!method notes_in_octave
|
|
41
|
-
# @return [Integer] the number of notes in one octave in the ScaleSystem
|
|
198
|
+
# Returns the number of notes in one octave.
|
|
42
199
|
#
|
|
200
|
+
# @abstract Subclass must implement
|
|
201
|
+
# @return [Integer] notes per octave (e.g., 12 for chromatic)
|
|
202
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
203
|
+
#
|
|
204
|
+
# @example
|
|
205
|
+
# EquallyTempered12ToneScaleSystem.notes_in_octave # => 12
|
|
43
206
|
def self.notes_in_octave
|
|
44
207
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
45
208
|
end
|
|
46
209
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
210
|
+
# Returns the size of the smallest pitch unit.
|
|
211
|
+
#
|
|
212
|
+
# Used for calculating sharp (#) and flat (♭) alterations.
|
|
213
|
+
# In equal temperament, this is 1 semitone.
|
|
50
214
|
#
|
|
215
|
+
# @abstract Subclass must implement
|
|
216
|
+
# @return [Integer] smallest unit size
|
|
217
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# EquallyTempered12ToneScaleSystem.part_of_tone_size # => 1
|
|
51
221
|
def self.part_of_tone_size
|
|
52
222
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
53
223
|
end
|
|
54
224
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
225
|
+
# Returns available intervals as name-to-offset mapping.
|
|
226
|
+
#
|
|
227
|
+
# Intervals are named using standard music theory notation:
|
|
228
|
+
#
|
|
229
|
+
# - **P** (Perfect): P1, P4, P5, P8
|
|
230
|
+
# - **M** (Major): M2, M3, M6, M7
|
|
231
|
+
# - **m** (minor): m2, m3, m6, m7
|
|
232
|
+
# - **TT**: Tritone
|
|
58
233
|
#
|
|
234
|
+
# @abstract Subclass must implement
|
|
235
|
+
# @return [Hash{Symbol => Integer}] interval names to semitone offsets
|
|
236
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
237
|
+
#
|
|
238
|
+
# @example
|
|
239
|
+
# intervals[:M3] # => 4 (major third = 4 semitones)
|
|
240
|
+
# intervals[:P5] # => 7 (perfect fifth = 7 semitones)
|
|
241
|
+
# intervals[:m7] # => 10 (minor seventh = 10 semitones)
|
|
59
242
|
def self.intervals
|
|
60
243
|
# TODO: implementar intérvalos sinónimos (p.ej, m3 = A2)
|
|
61
244
|
# TODO: implementar identificación de intérvalos, teniendo en cuenta no sólo los semitonos sino los grados de separación
|
|
@@ -63,25 +246,54 @@ module Musa
|
|
|
63
246
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
64
247
|
end
|
|
65
248
|
|
|
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
|
|
249
|
+
# Calculates frequency for a given pitch.
|
|
250
|
+
#
|
|
251
|
+
# Converts MIDI pitch numbers to frequencies in Hz. The calculation method
|
|
252
|
+
# depends on the tuning system (equal temperament, just intonation, etc.).
|
|
72
253
|
#
|
|
254
|
+
# @abstract Subclass must implement
|
|
255
|
+
# @param pitch [Numeric] MIDI pitch number (60 = middle C, 69 = A440)
|
|
256
|
+
# @param root_pitch [Numeric] root pitch of scale (for non-equal temperaments)
|
|
257
|
+
# @param a_frequency [Numeric] reference A frequency in Hz
|
|
258
|
+
# @return [Float] frequency in Hz
|
|
259
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
260
|
+
#
|
|
261
|
+
# @example Equal temperament
|
|
262
|
+
# # A440 (MIDI 69)
|
|
263
|
+
# frequency_of_pitch(69, 60, 440.0) # => 440.0
|
|
264
|
+
#
|
|
265
|
+
# # Middle C (MIDI 60)
|
|
266
|
+
# frequency_of_pitch(60, 60, 440.0) # => ~261.63 Hz
|
|
73
267
|
def self.frequency_of_pitch(pitch, root_pitch, a_frequency)
|
|
74
268
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
75
269
|
end
|
|
76
270
|
|
|
77
|
-
#
|
|
78
|
-
# @!method default_a_frequency
|
|
79
|
-
# @return [Number] the frequency A by default
|
|
271
|
+
# Returns the default A frequency.
|
|
80
272
|
#
|
|
273
|
+
# @return [Float] default A frequency in Hz (440.0 standard concert pitch)
|
|
274
|
+
#
|
|
275
|
+
# @example
|
|
276
|
+
# ScaleSystem.default_a_frequency # => 440.0
|
|
81
277
|
def self.default_a_frequency
|
|
82
278
|
440.0
|
|
83
279
|
end
|
|
84
280
|
|
|
281
|
+
# Creates or retrieves a tuning for this scale system.
|
|
282
|
+
#
|
|
283
|
+
# Returns a {ScaleSystemTuning} instance for the specified A frequency.
|
|
284
|
+
# Tunings are cached—repeated calls with same frequency return same instance.
|
|
285
|
+
#
|
|
286
|
+
# @param a_frequency [Numeric] reference A frequency in Hz
|
|
287
|
+
# @return [ScaleSystemTuning] tuning instance
|
|
288
|
+
#
|
|
289
|
+
# @example Standard pitch
|
|
290
|
+
# tuning = ScaleSystem[440.0]
|
|
291
|
+
#
|
|
292
|
+
# @example Baroque pitch
|
|
293
|
+
# baroque = ScaleSystem[415.0]
|
|
294
|
+
#
|
|
295
|
+
# @example Modern high pitch
|
|
296
|
+
# modern = ScaleSystem[442.0]
|
|
85
297
|
def self.[](a_frequency)
|
|
86
298
|
a_frequency = a_frequency.to_f
|
|
87
299
|
|
|
@@ -91,14 +303,34 @@ module Musa
|
|
|
91
303
|
@a_tunings[a_frequency]
|
|
92
304
|
end
|
|
93
305
|
|
|
306
|
+
# Returns semitone offset for a named interval.
|
|
307
|
+
#
|
|
308
|
+
# @param name [Symbol] interval name (e.g., :M3, :P5)
|
|
309
|
+
# @return [Integer] semitone offset
|
|
310
|
+
#
|
|
311
|
+
# @example
|
|
312
|
+
# offset_of_interval(:P5) # => 7
|
|
94
313
|
def self.offset_of_interval(name)
|
|
95
314
|
intervals[name]
|
|
96
315
|
end
|
|
97
316
|
|
|
317
|
+
# Returns the default tuning (A=440Hz).
|
|
318
|
+
#
|
|
319
|
+
# @return [ScaleSystemTuning] default tuning instance
|
|
320
|
+
#
|
|
321
|
+
# @example
|
|
322
|
+
# tuning = ScaleSystem.default_tuning
|
|
98
323
|
def self.default_tuning
|
|
99
324
|
self[default_a_frequency]
|
|
100
325
|
end
|
|
101
326
|
|
|
327
|
+
# Registers a scale kind (major, minor, etc.) with this system.
|
|
328
|
+
#
|
|
329
|
+
# @param scale_kind_class [Class] ScaleKind subclass to register
|
|
330
|
+
# @return [self]
|
|
331
|
+
#
|
|
332
|
+
# @example
|
|
333
|
+
# EquallyTempered12ToneScaleSystem.register MajorScaleKind
|
|
102
334
|
def self.register(scale_kind_class)
|
|
103
335
|
@scale_kind_classes ||= {}
|
|
104
336
|
@scale_kind_classes[scale_kind_class.id] = scale_kind_class
|
|
@@ -108,31 +340,90 @@ module Musa
|
|
|
108
340
|
self
|
|
109
341
|
end
|
|
110
342
|
|
|
343
|
+
# Retrieves a registered scale kind by ID.
|
|
344
|
+
#
|
|
345
|
+
# @param id [Symbol] scale kind identifier
|
|
346
|
+
# @return [Class] ScaleKind subclass
|
|
347
|
+
# @raise [KeyError] if not found
|
|
111
348
|
def self.scale_kind_class(id)
|
|
112
349
|
raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
|
|
113
350
|
|
|
114
351
|
@scale_kind_classes[id]
|
|
115
352
|
end
|
|
116
353
|
|
|
354
|
+
# Checks if a scale kind is registered.
|
|
355
|
+
#
|
|
356
|
+
# @param id [Symbol] scale kind identifier
|
|
357
|
+
# @return [Boolean]
|
|
117
358
|
def self.scale_kind_class?(id)
|
|
118
359
|
@scale_kind_classes.key? id
|
|
119
360
|
end
|
|
120
361
|
|
|
362
|
+
# Returns all registered scale kinds.
|
|
363
|
+
#
|
|
364
|
+
# @return [Hash{Symbol => Class}] scale kind classes
|
|
121
365
|
def self.scale_kind_classes
|
|
122
366
|
@scale_kind_classes
|
|
123
367
|
end
|
|
124
368
|
|
|
369
|
+
# Returns the chromatic scale kind class.
|
|
370
|
+
#
|
|
371
|
+
# @return [Class] chromatic ScaleKind subclass
|
|
372
|
+
# @raise [RuntimeError] if chromatic scale not defined
|
|
125
373
|
def self.chromatic_class
|
|
126
374
|
raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
|
|
127
375
|
|
|
128
376
|
@chromatic_scale_kind_class
|
|
129
377
|
end
|
|
130
378
|
|
|
379
|
+
# Compares scale systems for equality.
|
|
380
|
+
#
|
|
381
|
+
# @param other [ScaleSystem]
|
|
382
|
+
# @return [Boolean]
|
|
131
383
|
def ==(other)
|
|
132
384
|
self.class == other.class
|
|
133
385
|
end
|
|
134
386
|
end
|
|
135
387
|
|
|
388
|
+
# Scale system with specific A frequency tuning.
|
|
389
|
+
#
|
|
390
|
+
# ScaleSystemTuning combines a {ScaleSystem} with a specific reference A frequency,
|
|
391
|
+
# providing access to scale kinds (major, minor, chromatic, etc.) tuned to that
|
|
392
|
+
# frequency.
|
|
393
|
+
#
|
|
394
|
+
# ## Usage
|
|
395
|
+
#
|
|
396
|
+
# Tunings are created via {ScaleSystem.[]}:
|
|
397
|
+
#
|
|
398
|
+
# tuning = Scales[:et12][440.0] # Standard pitch
|
|
399
|
+
# baroque = Scales[:et12][415.0] # Baroque pitch
|
|
400
|
+
#
|
|
401
|
+
# ## Accessing Scales
|
|
402
|
+
#
|
|
403
|
+
# **By symbol**:
|
|
404
|
+
#
|
|
405
|
+
# tuning[:major][60] # C major scale
|
|
406
|
+
#
|
|
407
|
+
# **By method name**:
|
|
408
|
+
#
|
|
409
|
+
# tuning.major[60] # C major scale
|
|
410
|
+
# tuning.minor[69] # A minor scale
|
|
411
|
+
#
|
|
412
|
+
# **Chromatic scale**:
|
|
413
|
+
#
|
|
414
|
+
# tuning.chromatic[60] # C chromatic scale
|
|
415
|
+
#
|
|
416
|
+
# @example Standard usage
|
|
417
|
+
# tuning = Scales::Scales.default_system.default_tuning
|
|
418
|
+
# c_major = tuning.major[60]
|
|
419
|
+
# a_minor = tuning.minor[69]
|
|
420
|
+
#
|
|
421
|
+
# @example Historical pitch
|
|
422
|
+
# baroque = Scales[:et12][415.0]
|
|
423
|
+
# scale = baroque.major[60] # C major at A=415Hz
|
|
424
|
+
#
|
|
425
|
+
# @see ScaleSystem Parent scale system
|
|
426
|
+
# @see ScaleKind Scale types (major, minor, etc.)
|
|
136
427
|
class ScaleSystemTuning
|
|
137
428
|
extend Forwardable
|
|
138
429
|
|
|
@@ -181,70 +472,214 @@ module Musa
|
|
|
181
472
|
alias to_s inspect
|
|
182
473
|
end
|
|
183
474
|
|
|
475
|
+
# Abstract base class for scale types (major, minor, chromatic, etc.).
|
|
476
|
+
#
|
|
477
|
+
# ScaleKind defines a type of scale (major, minor, chromatic, etc.) independent
|
|
478
|
+
# of root pitch or tuning. It specifies:
|
|
479
|
+
#
|
|
480
|
+
# - Scale degrees and their pitch offsets
|
|
481
|
+
# - Function names for each degree (tonic, dominant, etc.)
|
|
482
|
+
# - Number of grades per octave
|
|
483
|
+
# - Whether the scale is chromatic (contains all pitches)
|
|
484
|
+
#
|
|
485
|
+
# ## Subclass Requirements
|
|
486
|
+
#
|
|
487
|
+
# Subclasses must implement:
|
|
488
|
+
#
|
|
489
|
+
# - {.id}: Unique symbol identifier (:major, :minor, :chromatic, etc.)
|
|
490
|
+
# - {.pitches}: Array defining scale structure
|
|
491
|
+
# - {.chromatic?}: Whether this is the chromatic scale (default: false)
|
|
492
|
+
# - {.grades}: Number of grades per octave (if different from pitches.length)
|
|
493
|
+
#
|
|
494
|
+
# ## Pitch Structure
|
|
495
|
+
#
|
|
496
|
+
# The {.pitches} array defines the scale structure:
|
|
497
|
+
#
|
|
498
|
+
# [{ functions: [:I, :tonic, :_1], pitch: 0 },
|
|
499
|
+
# { functions: [:II, :supertonic, :_2], pitch: 2 },
|
|
500
|
+
# ...]
|
|
501
|
+
#
|
|
502
|
+
# - **functions**: Array of symbols that can access this degree
|
|
503
|
+
# - **pitch**: Semitone offset from root
|
|
504
|
+
#
|
|
505
|
+
# ## Dynamic Method Creation
|
|
506
|
+
#
|
|
507
|
+
# Each scale instance gets methods for all registered scale kinds:
|
|
508
|
+
#
|
|
509
|
+
# note.major # Get major scale rooted on this note
|
|
510
|
+
# note.minor # Get minor scale rooted on this note
|
|
511
|
+
#
|
|
512
|
+
# ## Usage
|
|
513
|
+
#
|
|
514
|
+
# ScaleKind instances are accessed via tuning:
|
|
515
|
+
#
|
|
516
|
+
# tuning = Scales[:et12][440.0]
|
|
517
|
+
# major_kind = tuning[:major] # ScaleKind instance
|
|
518
|
+
# c_major = major_kind[60] # Scale instance
|
|
519
|
+
#
|
|
520
|
+
# Or directly via convenience methods:
|
|
521
|
+
#
|
|
522
|
+
# c_major = tuning.major[60]
|
|
523
|
+
#
|
|
524
|
+
# @abstract Subclass and implement abstract methods
|
|
525
|
+
# @see MajorScaleKind Concrete major scale implementation
|
|
526
|
+
# @see MinorNaturalScaleKind Concrete minor scale implementation
|
|
527
|
+
# @see ChromaticScaleKind Concrete chromatic scale implementation
|
|
528
|
+
# @see Scale Instantiated scale with root pitch
|
|
184
529
|
class ScaleKind
|
|
530
|
+
# Creates a scale kind instance.
|
|
531
|
+
#
|
|
532
|
+
# @param tuning [ScaleSystemTuning] the tuning context
|
|
533
|
+
#
|
|
534
|
+
# @api private
|
|
185
535
|
def initialize(tuning)
|
|
186
536
|
@tuning = tuning
|
|
187
537
|
@scales = {}
|
|
188
538
|
end
|
|
189
539
|
|
|
540
|
+
# The tuning context.
|
|
541
|
+
# @return [ScaleSystemTuning]
|
|
190
542
|
attr_reader :tuning
|
|
191
543
|
|
|
544
|
+
# Creates or retrieves a scale rooted on specific pitch.
|
|
545
|
+
#
|
|
546
|
+
# Scales are cached—repeated calls with same pitch return same instance.
|
|
547
|
+
#
|
|
548
|
+
# @param root_pitch [Integer] MIDI root pitch (60 = middle C)
|
|
549
|
+
# @return [Scale] scale instance
|
|
550
|
+
#
|
|
551
|
+
# @example
|
|
552
|
+
# major_kind = tuning[:major]
|
|
553
|
+
# c_major = major_kind[60] # C major
|
|
554
|
+
# g_major = major_kind[67] # G major
|
|
192
555
|
def [](root_pitch)
|
|
193
556
|
@scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
|
|
194
557
|
@scales[root_pitch]
|
|
195
558
|
end
|
|
196
559
|
|
|
560
|
+
# Returns scale with default root (middle C, MIDI 60).
|
|
561
|
+
#
|
|
562
|
+
# @return [Scale] scale rooted on middle C
|
|
563
|
+
#
|
|
564
|
+
# @example
|
|
565
|
+
# tuning.major.default_root # C major
|
|
197
566
|
def default_root
|
|
198
567
|
self[60]
|
|
199
568
|
end
|
|
200
569
|
|
|
570
|
+
# Returns scale with absolute root (MIDI 0).
|
|
571
|
+
#
|
|
572
|
+
# @return [Scale] scale rooted on MIDI 0
|
|
573
|
+
#
|
|
574
|
+
# @example
|
|
575
|
+
# tuning.major.absolut # Scale rooted at MIDI 0
|
|
201
576
|
def absolut
|
|
202
577
|
self[0]
|
|
203
578
|
end
|
|
204
579
|
|
|
580
|
+
# Checks scale kind equality.
|
|
581
|
+
#
|
|
582
|
+
# @param other [ScaleKind]
|
|
583
|
+
# @return [Boolean]
|
|
205
584
|
def ==(other)
|
|
206
585
|
self.class == other.class && @tuning == other.tuning
|
|
207
586
|
end
|
|
208
587
|
|
|
588
|
+
# Returns string representation.
|
|
589
|
+
#
|
|
590
|
+
# @return [String]
|
|
209
591
|
def inspect
|
|
210
592
|
"<#{self.class.name}: tuning = #{@tuning}>"
|
|
211
593
|
end
|
|
212
594
|
|
|
213
595
|
alias to_s inspect
|
|
214
596
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
# @
|
|
597
|
+
# Returns the unique identifier for this scale kind.
|
|
598
|
+
#
|
|
599
|
+
# @abstract Subclass must implement
|
|
600
|
+
# @return [Symbol] scale kind ID (e.g., :major, :minor, :chromatic)
|
|
601
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
602
|
+
#
|
|
603
|
+
# @example
|
|
604
|
+
# MajorScaleKind.id # => :major
|
|
218
605
|
def self.id
|
|
219
606
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
220
607
|
end
|
|
221
608
|
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
609
|
+
# Returns the pitch structure definition.
|
|
610
|
+
#
|
|
611
|
+
# Defines the scale degrees and their pitch offsets from the root.
|
|
612
|
+
# Each entry specifies function names and semitone offset.
|
|
613
|
+
#
|
|
614
|
+
# @abstract Subclass must implement
|
|
615
|
+
# @return [Array<Hash>] array of pitch definitions with:
|
|
616
|
+
# - **:functions** [Array<Symbol>]: function names for this degree
|
|
617
|
+
# - **:pitch** [Integer]: semitone offset from root
|
|
618
|
+
# @raise [RuntimeError] if not implemented in subclass
|
|
619
|
+
#
|
|
620
|
+
# @example Major scale structure (partial)
|
|
621
|
+
# [{ functions: [:I, :tonic, :_1], pitch: 0 },
|
|
622
|
+
# { functions: [:II, :supertonic, :_2], pitch: 2 },
|
|
623
|
+
# { functions: [:III, :mediant, :_3], pitch: 4 },
|
|
624
|
+
# ...]
|
|
225
625
|
def self.pitches
|
|
226
626
|
raise 'Method not implemented. Should be implemented in subclass.'
|
|
227
627
|
end
|
|
228
628
|
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
629
|
+
# Indicates whether this is the chromatic scale.
|
|
630
|
+
#
|
|
631
|
+
# Only one scale kind per system should return true. The chromatic scale
|
|
632
|
+
# contains all notes in the scale system and is used as a fallback for
|
|
633
|
+
# non-diatonic notes.
|
|
634
|
+
#
|
|
635
|
+
# @return [Boolean] true if chromatic scale (default: false)
|
|
636
|
+
#
|
|
637
|
+
# @example
|
|
638
|
+
# ChromaticScaleKind.chromatic? # => true
|
|
639
|
+
# MajorScaleKind.chromatic? # => false
|
|
232
640
|
def self.chromatic?
|
|
233
641
|
false
|
|
234
642
|
end
|
|
235
643
|
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
644
|
+
# Returns the number of grades per octave.
|
|
645
|
+
#
|
|
646
|
+
# For scales defining extended harmony (8th, 9th, etc.), this returns
|
|
647
|
+
# the number of diatonic degrees within one octave. Defaults to the
|
|
648
|
+
# number of pitch definitions.
|
|
649
|
+
#
|
|
650
|
+
# @return [Integer] number of grades per octave
|
|
651
|
+
#
|
|
652
|
+
# @example
|
|
653
|
+
# MajorScaleKind.grades # => 7 (not 13, even with extended degrees)
|
|
239
654
|
def self.grades
|
|
240
655
|
pitches.length
|
|
241
656
|
end
|
|
242
657
|
|
|
658
|
+
# Returns grade index for a function symbol.
|
|
659
|
+
#
|
|
660
|
+
# @param symbol [Symbol] function name (e.g., :tonic, :dominant, :V)
|
|
661
|
+
# @return [Integer, nil] grade index or nil if not found
|
|
662
|
+
#
|
|
663
|
+
# @example
|
|
664
|
+
# MajorScaleKind.grade_of_function(:tonic) # => 0
|
|
665
|
+
# MajorScaleKind.grade_of_function(:dominant) # => 4
|
|
666
|
+
# MajorScaleKind.grade_of_function(:V) # => 4
|
|
667
|
+
#
|
|
668
|
+
# @api private
|
|
243
669
|
def self.grade_of_function(symbol)
|
|
244
670
|
create_grade_functions_index unless @grade_names_index
|
|
245
671
|
@grade_names_index[symbol]
|
|
246
672
|
end
|
|
247
673
|
|
|
674
|
+
# Returns all function symbols for accessing scale degrees.
|
|
675
|
+
#
|
|
676
|
+
# @return [Array<Symbol>] all function names
|
|
677
|
+
#
|
|
678
|
+
# @example
|
|
679
|
+
# MajorScaleKind.grades_functions
|
|
680
|
+
# # => [:I, :_1, :tonic, :first, :II, :_2, :supertonic, :second, ...]
|
|
681
|
+
#
|
|
682
|
+
# @api private
|
|
248
683
|
def self.grades_functions
|
|
249
684
|
create_grade_functions_index unless @grade_names_index
|
|
250
685
|
@grade_names_index.keys
|
|
@@ -252,6 +687,11 @@ module Musa
|
|
|
252
687
|
|
|
253
688
|
private
|
|
254
689
|
|
|
690
|
+
# Creates internal index mapping function names to grade indices.
|
|
691
|
+
#
|
|
692
|
+
# @return [self]
|
|
693
|
+
#
|
|
694
|
+
# @api private
|
|
255
695
|
def self.create_grade_functions_index
|
|
256
696
|
@grade_names_index = {}
|
|
257
697
|
pitches.each_index do |i|
|
|
@@ -264,9 +704,88 @@ module Musa
|
|
|
264
704
|
end
|
|
265
705
|
end
|
|
266
706
|
|
|
707
|
+
# Instantiated scale with specific root pitch.
|
|
708
|
+
#
|
|
709
|
+
# Scale represents a concrete scale (major, minor, etc.) rooted on a specific
|
|
710
|
+
# pitch. It provides access to scale degrees, interval calculations, frequency
|
|
711
|
+
# generation, and chord construction.
|
|
712
|
+
#
|
|
713
|
+
# ## Creation
|
|
714
|
+
#
|
|
715
|
+
# Scales are created via {ScaleKind}:
|
|
716
|
+
#
|
|
717
|
+
# tuning = Scales[:et12][440.0]
|
|
718
|
+
# c_major = tuning.major[60] # Via convenience method
|
|
719
|
+
# a_minor = tuning[:minor][69] # Via bracket notation
|
|
720
|
+
#
|
|
721
|
+
# ## Accessing Notes
|
|
722
|
+
#
|
|
723
|
+
# **By numeric grade** (0-based):
|
|
724
|
+
#
|
|
725
|
+
# scale[0] # First degree (tonic)
|
|
726
|
+
# scale[1] # Second degree
|
|
727
|
+
# scale[4] # Fifth degree
|
|
728
|
+
#
|
|
729
|
+
# **By function name** (dynamic methods):
|
|
730
|
+
#
|
|
731
|
+
# scale.tonic # First degree
|
|
732
|
+
# scale.dominant # Fifth degree
|
|
733
|
+
# scale.mediant # Third degree
|
|
734
|
+
#
|
|
735
|
+
# **By Roman numeral**:
|
|
736
|
+
#
|
|
737
|
+
# scale[:I] # First degree
|
|
738
|
+
# scale[:V] # Fifth degree
|
|
739
|
+
# scale[:IV] # Fourth degree
|
|
740
|
+
#
|
|
741
|
+
# **With accidentals** (sharp # or flat _):
|
|
742
|
+
#
|
|
743
|
+
# scale[:I#] # Raised tonic
|
|
744
|
+
# scale[:V_] # Flatted dominant
|
|
745
|
+
# scale['II##'] # Double-raised second
|
|
746
|
+
#
|
|
747
|
+
# ## Note Operations
|
|
748
|
+
#
|
|
749
|
+
# Each note is a {NoteInScale} instance with full capabilities:
|
|
750
|
+
#
|
|
751
|
+
# note = scale.tonic
|
|
752
|
+
# note.pitch # MIDI pitch number
|
|
753
|
+
# note.frequency # Frequency in Hz
|
|
754
|
+
# note.chord # Build chord from note
|
|
755
|
+
# note.up(:P5) # Navigate by interval
|
|
756
|
+
# note.sharp # Raise by semitone
|
|
757
|
+
#
|
|
758
|
+
# ## Special Methods
|
|
759
|
+
#
|
|
760
|
+
# - **chromatic**: Access chromatic scale at same root
|
|
761
|
+
# - **octave**: Transpose scale to different octave
|
|
762
|
+
# - **note_of_pitch**: Find note for specific MIDI pitch
|
|
763
|
+
#
|
|
764
|
+
# @example Basic scale access
|
|
765
|
+
# c_major = tuning.major[60]
|
|
766
|
+
# c_major.tonic.pitch # => 60 (C)
|
|
767
|
+
# c_major.dominant.pitch # => 67 (G)
|
|
768
|
+
# c_major[:III].pitch # => 64 (E)
|
|
769
|
+
#
|
|
770
|
+
# @example Chromatic alterations
|
|
771
|
+
# c_major[:I#].pitch # => 61 (C#)
|
|
772
|
+
# c_major[:V_].pitch # => 66 (F#/Gb)
|
|
773
|
+
#
|
|
774
|
+
# @example Building chords
|
|
775
|
+
# c_major.tonic.chord # C major triad
|
|
776
|
+
# c_major.dominant.chord :seventh # G dominant 7th
|
|
777
|
+
#
|
|
778
|
+
# @see ScaleKind Scale type definition
|
|
779
|
+
# @see NoteInScale Individual note in scale
|
|
267
780
|
class Scale
|
|
268
781
|
extend Forwardable
|
|
269
782
|
|
|
783
|
+
# Creates a scale instance.
|
|
784
|
+
#
|
|
785
|
+
# @param kind [ScaleKind] the scale kind
|
|
786
|
+
# @param root_pitch [Integer] MIDI root pitch
|
|
787
|
+
#
|
|
788
|
+
# @api private
|
|
270
789
|
def initialize(kind, root_pitch:)
|
|
271
790
|
@notes_by_grade = {}
|
|
272
791
|
@notes_by_pitch = {}
|
|
@@ -284,28 +803,95 @@ module Musa
|
|
|
284
803
|
freeze
|
|
285
804
|
end
|
|
286
805
|
|
|
806
|
+
# Delegates tuning access to kind.
|
|
287
807
|
def_delegators :@kind, :tuning
|
|
288
808
|
|
|
289
|
-
|
|
809
|
+
# Scale kind (major, minor, etc.).
|
|
810
|
+
# @return [ScaleKind]
|
|
811
|
+
attr_reader :kind
|
|
812
|
+
|
|
813
|
+
# Root pitch (MIDI number).
|
|
814
|
+
# @return [Integer]
|
|
815
|
+
attr_reader :root_pitch
|
|
290
816
|
|
|
817
|
+
# Returns the root note (first degree).
|
|
818
|
+
#
|
|
819
|
+
# Equivalent to scale[0] or scale.tonic.
|
|
820
|
+
#
|
|
821
|
+
# @return [NoteInScale] root note
|
|
822
|
+
#
|
|
823
|
+
# @example
|
|
824
|
+
# c_major.root.pitch # => 60
|
|
291
825
|
def root
|
|
292
826
|
self[0]
|
|
293
827
|
end
|
|
294
828
|
|
|
829
|
+
# Returns the chromatic scale at the same root.
|
|
830
|
+
#
|
|
831
|
+
# @return [Scale] chromatic scale rooted at same pitch
|
|
832
|
+
#
|
|
833
|
+
# @example
|
|
834
|
+
# c_major.chromatic # Chromatic scale starting at C
|
|
295
835
|
def chromatic
|
|
296
836
|
@kind.tuning.chromatic[@root_pitch]
|
|
297
837
|
end
|
|
298
838
|
|
|
839
|
+
# Returns the scale rooted at absolute pitch 0.
|
|
840
|
+
#
|
|
841
|
+
# @return [Scale] scale of same kind at MIDI 0
|
|
842
|
+
#
|
|
843
|
+
# @example
|
|
844
|
+
# c_major.absolut # Major scale at MIDI 0
|
|
299
845
|
def absolut
|
|
300
846
|
@kind[0]
|
|
301
847
|
end
|
|
302
848
|
|
|
849
|
+
# Transposes scale by octaves.
|
|
850
|
+
#
|
|
851
|
+
# @param octave [Integer] octave offset (positive = up, negative = down)
|
|
852
|
+
# @return [Scale] transposed scale
|
|
853
|
+
# @raise [ArgumentError] if octave is not integer
|
|
854
|
+
#
|
|
855
|
+
# @example
|
|
856
|
+
# c_major.octave(1) # C major one octave higher
|
|
857
|
+
# c_major.octave(-1) # C major one octave lower
|
|
303
858
|
def octave(octave)
|
|
304
859
|
raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
|
|
305
860
|
|
|
306
861
|
@kind[@root_pitch + octave * @kind.class.grades]
|
|
307
862
|
end
|
|
308
863
|
|
|
864
|
+
# Accesses scale degree by grade, symbol, or function name.
|
|
865
|
+
#
|
|
866
|
+
# Supports multiple access patterns:
|
|
867
|
+
# - **Integer**: Numeric grade (0-based)
|
|
868
|
+
# - **Symbol/String**: Function name or Roman numeral
|
|
869
|
+
# - **With accidentals**: Add '#' for sharp, '_' for flat
|
|
870
|
+
#
|
|
871
|
+
# Notes are cached—repeated access returns same instance.
|
|
872
|
+
#
|
|
873
|
+
# @param grade_or_symbol [Integer, Symbol, String] degree specifier
|
|
874
|
+
# @return [NoteInScale] note at specified degree
|
|
875
|
+
# @raise [ArgumentError] if grade_or_symbol is invalid type
|
|
876
|
+
#
|
|
877
|
+
# @example Numeric access
|
|
878
|
+
# scale[0] # Tonic
|
|
879
|
+
# scale[4] # Dominant (in major/minor)
|
|
880
|
+
#
|
|
881
|
+
# @example Function name access
|
|
882
|
+
# scale[:tonic]
|
|
883
|
+
# scale[:dominant]
|
|
884
|
+
# scale[:mediant]
|
|
885
|
+
#
|
|
886
|
+
# @example Roman numeral access
|
|
887
|
+
# scale[:I] # Tonic
|
|
888
|
+
# scale[:V] # Dominant
|
|
889
|
+
# scale[:IV] # Subdominant
|
|
890
|
+
#
|
|
891
|
+
# @example With accidentals
|
|
892
|
+
# scale[:I#] # Raised tonic
|
|
893
|
+
# scale[:V_] # Flatted dominant
|
|
894
|
+
# scale['II##'] # Double-raised second
|
|
309
895
|
def [](grade_or_symbol)
|
|
310
896
|
|
|
311
897
|
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 +916,12 @@ module Musa
|
|
|
330
916
|
@notes_by_grade[wide_grade].sharp(sharps)
|
|
331
917
|
end
|
|
332
918
|
|
|
919
|
+
# Converts grade specifier to numeric grade and accidentals.
|
|
920
|
+
#
|
|
921
|
+
# @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
|
|
922
|
+
# @return [Array(Integer, Integer)] wide grade and accidentals count
|
|
923
|
+
#
|
|
924
|
+
# @api private
|
|
333
925
|
def grade_of(grade_or_string_or_symbol)
|
|
334
926
|
name, wide_grade, accidentals = parse_grade(grade_or_string_or_symbol)
|
|
335
927
|
|
|
@@ -343,6 +935,15 @@ module Musa
|
|
|
343
935
|
return octave * @kind.class.grades + grade, accidentals
|
|
344
936
|
end
|
|
345
937
|
|
|
938
|
+
# Parses grade string/symbol into components.
|
|
939
|
+
#
|
|
940
|
+
# Handles formats like "I#", ":V_", "7##", extracting function name,
|
|
941
|
+
# numeric grade, and accidentals.
|
|
942
|
+
#
|
|
943
|
+
# @param neuma_grade [Integer, Symbol, String] grade to parse
|
|
944
|
+
# @return [Array(Symbol, Integer, Integer)] name, wide_grade, accidentals
|
|
945
|
+
#
|
|
946
|
+
# @api private
|
|
346
947
|
def parse_grade(neuma_grade)
|
|
347
948
|
name = wide_grade = nil
|
|
348
949
|
accidentals = 0
|
|
@@ -371,6 +972,24 @@ module Musa
|
|
|
371
972
|
return name, wide_grade, accidentals
|
|
372
973
|
end
|
|
373
974
|
|
|
975
|
+
# Finds note for a specific MIDI pitch.
|
|
976
|
+
#
|
|
977
|
+
# Searches for a note in the scale matching the given pitch. Options control
|
|
978
|
+
# behavior when pitch is not in scale.
|
|
979
|
+
#
|
|
980
|
+
# @param pitch [Integer] MIDI pitch number
|
|
981
|
+
# @param allow_chromatic [Boolean] if true, return chromatic note when not in scale
|
|
982
|
+
# @param allow_nearest [Boolean] if true, return nearest scale note
|
|
983
|
+
# @return [NoteInScale, nil] matching note or nil
|
|
984
|
+
#
|
|
985
|
+
# @example Diatonic note
|
|
986
|
+
# c_major.note_of_pitch(64) # => E (in scale)
|
|
987
|
+
#
|
|
988
|
+
# @example Chromatic note
|
|
989
|
+
# c_major.note_of_pitch(63, allow_chromatic: true) # => Eb (chromatic)
|
|
990
|
+
#
|
|
991
|
+
# @example Nearest note
|
|
992
|
+
# c_major.note_of_pitch(63, allow_nearest: true) # => E or D (nearest)
|
|
374
993
|
def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
|
|
375
994
|
allow_chromatic ||= false
|
|
376
995
|
allow_nearest ||= false
|
|
@@ -407,16 +1026,33 @@ module Musa
|
|
|
407
1026
|
note
|
|
408
1027
|
end
|
|
409
1028
|
|
|
1029
|
+
# Returns semitone offset for a named interval.
|
|
1030
|
+
#
|
|
1031
|
+
# @param interval_name [Symbol] interval name (e.g., :M3, :P5)
|
|
1032
|
+
# @return [Integer] semitone offset
|
|
1033
|
+
#
|
|
1034
|
+
# @example
|
|
1035
|
+
# scale.offset_of_interval(:P5) # => 7
|
|
1036
|
+
# scale.offset_of_interval(:M3) # => 4
|
|
410
1037
|
def offset_of_interval(interval_name)
|
|
411
1038
|
@kind.tuning.offset_of_interval(interval_name)
|
|
412
1039
|
end
|
|
413
1040
|
|
|
1041
|
+
# Checks scale equality.
|
|
1042
|
+
#
|
|
1043
|
+
# Scales are equal if they have same kind and root pitch.
|
|
1044
|
+
#
|
|
1045
|
+
# @param other [Scale]
|
|
1046
|
+
# @return [Boolean]
|
|
414
1047
|
def ==(other)
|
|
415
1048
|
self.class == other.class &&
|
|
416
1049
|
@kind == other.kind &&
|
|
417
1050
|
@root_pitch == other.root_pitch
|
|
418
1051
|
end
|
|
419
1052
|
|
|
1053
|
+
# Returns string representation.
|
|
1054
|
+
#
|
|
1055
|
+
# @return [String]
|
|
420
1056
|
def inspect
|
|
421
1057
|
"<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
|
|
422
1058
|
end
|
|
@@ -424,13 +1060,108 @@ module Musa
|
|
|
424
1060
|
alias to_s inspect
|
|
425
1061
|
end
|
|
426
1062
|
|
|
1063
|
+
# Note within a scale context.
|
|
1064
|
+
#
|
|
1065
|
+
# NoteInScale represents a specific note within a scale, providing rich musical
|
|
1066
|
+
# functionality including:
|
|
1067
|
+
# - Pitch and frequency information
|
|
1068
|
+
# - Interval navigation (up, down, by named intervals)
|
|
1069
|
+
# - Chromatic alterations (sharp, flat)
|
|
1070
|
+
# - Scale navigation (change scales while keeping pitch)
|
|
1071
|
+
# - Chord construction
|
|
1072
|
+
# - Octave transposition
|
|
1073
|
+
#
|
|
1074
|
+
# ## Creation
|
|
1075
|
+
#
|
|
1076
|
+
# Notes are created via scale access, not directly:
|
|
1077
|
+
#
|
|
1078
|
+
# scale = tuning.major[60]
|
|
1079
|
+
# note = scale.tonic # NoteInScale instance
|
|
1080
|
+
# note = scale[:V] # Another NoteInScale
|
|
1081
|
+
#
|
|
1082
|
+
# ## Basic Properties
|
|
1083
|
+
#
|
|
1084
|
+
# note.pitch # MIDI pitch number
|
|
1085
|
+
# note.grade # Scale degree (0-based)
|
|
1086
|
+
# note.octave # Octave relative to scale root
|
|
1087
|
+
# note.frequency # Frequency in Hz
|
|
1088
|
+
# note.functions # Function names for this degree
|
|
1089
|
+
#
|
|
1090
|
+
# ## Interval Navigation
|
|
1091
|
+
#
|
|
1092
|
+
# **Natural intervals** (diatonic, within scale):
|
|
1093
|
+
#
|
|
1094
|
+
# note.up(2) # Up 2 scale degrees
|
|
1095
|
+
# note.down(1) # Down 1 scale degree
|
|
1096
|
+
#
|
|
1097
|
+
# **Chromatic intervals** (by semitones or named intervals):
|
|
1098
|
+
#
|
|
1099
|
+
# note.up(:P5) # Up perfect fifth
|
|
1100
|
+
# note.up(7) # Up 7 semitones (if chromatic specified)
|
|
1101
|
+
# note.down(:M3) # Down major third
|
|
1102
|
+
#
|
|
1103
|
+
# ## Chromatic Alterations
|
|
1104
|
+
#
|
|
1105
|
+
# note.sharp # Raise by 1 semitone
|
|
1106
|
+
# note.sharp(2) # Raise by 2 semitones
|
|
1107
|
+
# note.flat # Lower by 1 semitone
|
|
1108
|
+
# note.flat(2) # Lower by 2 semitones
|
|
1109
|
+
#
|
|
1110
|
+
# ## Scale Navigation
|
|
1111
|
+
#
|
|
1112
|
+
# note.scale(:minor) # Same pitch in minor scale
|
|
1113
|
+
# note.major # Same pitch in major scale
|
|
1114
|
+
# note.chromatic # Same pitch in chromatic scale
|
|
1115
|
+
#
|
|
1116
|
+
# ## Chord Construction
|
|
1117
|
+
#
|
|
1118
|
+
# note.chord # Build triad
|
|
1119
|
+
# note.chord :seventh # Build seventh chord
|
|
1120
|
+
# note.chord quality: :minor # Build with features
|
|
1121
|
+
#
|
|
1122
|
+
# ## Background Scale Context
|
|
1123
|
+
#
|
|
1124
|
+
# Chromatic notes remember their diatonic context:
|
|
1125
|
+
#
|
|
1126
|
+
# c# = c_major.tonic.sharp # C# in C major context
|
|
1127
|
+
# c#.background_scale # => c_major
|
|
1128
|
+
# c#.background_note # => C (natural)
|
|
1129
|
+
# c#.background_sharps # => 1
|
|
1130
|
+
#
|
|
1131
|
+
# @example Basic usage
|
|
1132
|
+
# c_major = tuning.major[60]
|
|
1133
|
+
# tonic = c_major.tonic
|
|
1134
|
+
# tonic.pitch # => 60
|
|
1135
|
+
# tonic.frequency # => ~261.63 Hz
|
|
1136
|
+
#
|
|
1137
|
+
# @example Interval navigation
|
|
1138
|
+
# tonic.up(:P5).pitch # => 67 (G)
|
|
1139
|
+
# tonic.up(4, :natural).pitch # => 71 (4 scale degrees = B)
|
|
1140
|
+
#
|
|
1141
|
+
# @example Chromatic alterations
|
|
1142
|
+
# tonic.sharp.pitch # => 61 (C#)
|
|
1143
|
+
# tonic.flat.pitch # => 59 (B)
|
|
1144
|
+
#
|
|
1145
|
+
# @example Chord building
|
|
1146
|
+
# tonic.chord # C major triad
|
|
1147
|
+
# tonic.chord :seventh # C major 7th
|
|
1148
|
+
#
|
|
1149
|
+
# @see Scale Parent scale
|
|
1150
|
+
# @see Chord Chord construction
|
|
427
1151
|
class NoteInScale
|
|
428
1152
|
|
|
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
|
|
1153
|
+
# Creates a note within a scale.
|
|
433
1154
|
#
|
|
1155
|
+
# @param scale [Scale] parent scale
|
|
1156
|
+
# @param grade [Integer] scale degree (0-based)
|
|
1157
|
+
# @param octave [Integer] octave relative to scale root
|
|
1158
|
+
# @param pitch [Numeric] MIDI pitch (Integer, Rational, or Float for microtones)
|
|
1159
|
+
# @param background_scale [Scale, nil] diatonic context for chromatic notes
|
|
1160
|
+
# @param background_grade [Integer, nil] diatonic grade for chromatic notes
|
|
1161
|
+
# @param background_octave [Integer, nil] diatonic octave for chromatic notes
|
|
1162
|
+
# @param background_sharps [Integer, nil] sharps/flats from diatonic note
|
|
1163
|
+
#
|
|
1164
|
+
# @api private
|
|
434
1165
|
def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
|
|
435
1166
|
@scale = scale
|
|
436
1167
|
@grade = grade
|
|
@@ -449,12 +1180,44 @@ module Musa
|
|
|
449
1180
|
end
|
|
450
1181
|
end
|
|
451
1182
|
|
|
452
|
-
|
|
1183
|
+
# Scale degree (0-based).
|
|
1184
|
+
# @return [Integer]
|
|
1185
|
+
attr_reader :grade
|
|
1186
|
+
|
|
1187
|
+
# MIDI pitch number.
|
|
1188
|
+
# @return [Numeric]
|
|
1189
|
+
attr_reader :pitch
|
|
453
1190
|
|
|
1191
|
+
# Returns function names for this scale degree.
|
|
1192
|
+
#
|
|
1193
|
+
# @return [Array<Symbol>] function symbols
|
|
1194
|
+
#
|
|
1195
|
+
# @example
|
|
1196
|
+
# c_major.tonic.functions # => [:I, :_1, :tonic, :first]
|
|
454
1197
|
def functions
|
|
455
1198
|
@scale.kind.class.pitches[grade][:functions]
|
|
456
1199
|
end
|
|
457
1200
|
|
|
1201
|
+
# Transposes note or returns current octave.
|
|
1202
|
+
#
|
|
1203
|
+
# **Without argument**: Returns current octave relative to scale root.
|
|
1204
|
+
#
|
|
1205
|
+
# **With argument**: Returns note transposed by octave offset.
|
|
1206
|
+
#
|
|
1207
|
+
# @param octave [Integer, nil] octave offset (nil to query current)
|
|
1208
|
+
# @param absolute [Boolean] if true, ignore current octave
|
|
1209
|
+
# @return [Integer, NoteInScale] current octave or transposed note
|
|
1210
|
+
# @raise [ArgumentError] if octave is not integer
|
|
1211
|
+
#
|
|
1212
|
+
# @example Query octave
|
|
1213
|
+
# note.octave # => 0 (at scale root octave)
|
|
1214
|
+
#
|
|
1215
|
+
# @example Transpose relative
|
|
1216
|
+
# note.octave(1).pitch # Up one octave from current
|
|
1217
|
+
# note.octave(-1).pitch # Down one octave from current
|
|
1218
|
+
#
|
|
1219
|
+
# @example Transpose absolute
|
|
1220
|
+
# note.octave(2, absolute: true).pitch # At octave 2, regardless of current
|
|
458
1221
|
def octave(octave = nil, absolute: false)
|
|
459
1222
|
if octave.nil?
|
|
460
1223
|
@octave
|
|
@@ -465,6 +1228,18 @@ module Musa
|
|
|
465
1228
|
end
|
|
466
1229
|
end
|
|
467
1230
|
|
|
1231
|
+
# Creates a copy with background scale context.
|
|
1232
|
+
#
|
|
1233
|
+
# Used internally when creating chromatic notes to remember their
|
|
1234
|
+
# diatonic context.
|
|
1235
|
+
#
|
|
1236
|
+
# @param scale [Scale] background diatonic scale
|
|
1237
|
+
# @param grade [Integer, nil] background grade
|
|
1238
|
+
# @param octave [Integer, nil] background octave
|
|
1239
|
+
# @param sharps [Integer, nil] accidentals from background note
|
|
1240
|
+
# @return [NoteInScale] new note with background context
|
|
1241
|
+
#
|
|
1242
|
+
# @api private
|
|
468
1243
|
def with_background(scale:, grade: nil, octave: nil, sharps: nil)
|
|
469
1244
|
NoteInScale.new(@scale, @grade, @octave, @pitch,
|
|
470
1245
|
background_scale: scale,
|
|
@@ -473,18 +1248,55 @@ module Musa
|
|
|
473
1248
|
background_sharps: sharps)
|
|
474
1249
|
end
|
|
475
1250
|
|
|
1251
|
+
# Background diatonic scale (for chromatic notes).
|
|
1252
|
+
# @return [Scale, nil]
|
|
476
1253
|
attr_reader :background_scale
|
|
477
1254
|
|
|
1255
|
+
# Returns the diatonic note this chromatic note is based on.
|
|
1256
|
+
#
|
|
1257
|
+
# @return [NoteInScale, nil] background note or nil
|
|
1258
|
+
#
|
|
1259
|
+
# @example
|
|
1260
|
+
# c# = c_major.tonic.sharp
|
|
1261
|
+
# c#.background_note.pitch # => 60 (C natural)
|
|
478
1262
|
def background_note
|
|
479
1263
|
@background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
|
|
480
1264
|
end
|
|
481
1265
|
|
|
1266
|
+
# Sharps/flats from background note.
|
|
1267
|
+
# @return [Integer, nil]
|
|
482
1268
|
attr_reader :background_sharps
|
|
483
1269
|
|
|
1270
|
+
# Returns wide grade (grade + octave * grades_per_octave).
|
|
1271
|
+
#
|
|
1272
|
+
# @return [Integer]
|
|
1273
|
+
#
|
|
1274
|
+
# @example
|
|
1275
|
+
# note.wide_grade # => 7 (second octave, first degree)
|
|
1276
|
+
#
|
|
1277
|
+
# @api private
|
|
484
1278
|
def wide_grade
|
|
485
1279
|
@grade + @octave * @scale.kind.class.grades
|
|
486
1280
|
end
|
|
487
1281
|
|
|
1282
|
+
# Navigates upward by interval.
|
|
1283
|
+
#
|
|
1284
|
+
# Supports both natural (diatonic) and chromatic (semitone) intervals.
|
|
1285
|
+
#
|
|
1286
|
+
# - **Numeric interval + :natural**: Move by scale degrees
|
|
1287
|
+
# - **Symbol or numeric interval + :chromatic**: Move by semitones or named interval
|
|
1288
|
+
#
|
|
1289
|
+
# @param interval_name_or_interval [Symbol, Integer] interval
|
|
1290
|
+
# @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
|
|
1291
|
+
# @param sign [Integer] direction multiplier (internal use)
|
|
1292
|
+
# @return [NoteInScale] note at interval above
|
|
1293
|
+
#
|
|
1294
|
+
# @example Natural interval (scale degrees)
|
|
1295
|
+
# note.up(2, :natural) # Up 2 scale degrees
|
|
1296
|
+
#
|
|
1297
|
+
# @example Chromatic interval (semitones)
|
|
1298
|
+
# note.up(:P5) # Up perfect fifth (7 semitones)
|
|
1299
|
+
# note.up(7) # Up 7 semitones (if chromatic)
|
|
488
1300
|
def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)
|
|
489
1301
|
|
|
490
1302
|
sign ||= 1
|
|
@@ -525,24 +1337,78 @@ module Musa
|
|
|
525
1337
|
|
|
526
1338
|
private :calculate_note_of_pitch
|
|
527
1339
|
|
|
1340
|
+
# Navigates downward by interval.
|
|
1341
|
+
#
|
|
1342
|
+
# Same as {#up} but in reverse direction.
|
|
1343
|
+
#
|
|
1344
|
+
# @param interval_name_or_interval [Symbol, Integer] interval
|
|
1345
|
+
# @param natural_or_chromatic [Symbol, nil] :natural or :chromatic
|
|
1346
|
+
# @return [NoteInScale] note at interval below
|
|
1347
|
+
#
|
|
1348
|
+
# @example
|
|
1349
|
+
# note.down(2, :natural) # Down 2 scale degrees
|
|
1350
|
+
# note.down(:P5) # Down perfect fifth
|
|
528
1351
|
def down(interval_name_or_interval, natural_or_chromatic = nil)
|
|
529
1352
|
up(interval_name_or_interval, natural_or_chromatic, sign: -1)
|
|
530
1353
|
end
|
|
531
1354
|
|
|
1355
|
+
# Raises note by semitones (adds sharps).
|
|
1356
|
+
#
|
|
1357
|
+
# @param count [Integer, nil] number of semitones (default 1)
|
|
1358
|
+
# @return [NoteInScale] raised note
|
|
1359
|
+
#
|
|
1360
|
+
# @example
|
|
1361
|
+
# note.sharp.pitch # Up 1 semitone
|
|
1362
|
+
# note.sharp(2).pitch # Up 2 semitones
|
|
532
1363
|
def sharp(count = nil)
|
|
533
1364
|
count ||= 1
|
|
534
1365
|
calculate_note_of_pitch(@pitch, count)
|
|
535
1366
|
end
|
|
536
1367
|
|
|
1368
|
+
# Lowers note by semitones (adds flats).
|
|
1369
|
+
#
|
|
1370
|
+
# @param count [Integer, nil] number of semitones (default 1)
|
|
1371
|
+
# @return [NoteInScale] lowered note
|
|
1372
|
+
#
|
|
1373
|
+
# @example
|
|
1374
|
+
# note.flat.pitch # Down 1 semitone
|
|
1375
|
+
# note.flat(2).pitch # Down 2 semitones
|
|
537
1376
|
def flat(count = nil)
|
|
538
1377
|
count ||= 1
|
|
539
1378
|
sharp(-count)
|
|
540
1379
|
end
|
|
541
1380
|
|
|
1381
|
+
# Calculates frequency in Hz.
|
|
1382
|
+
#
|
|
1383
|
+
# Uses the scale system's frequency calculation (equal temperament,
|
|
1384
|
+
# just intonation, etc.) and the tuning's A frequency.
|
|
1385
|
+
#
|
|
1386
|
+
# @return [Float] frequency in Hz
|
|
1387
|
+
#
|
|
1388
|
+
# @example
|
|
1389
|
+
# c_major.tonic.frequency # => ~261.63 Hz (middle C at A=440)
|
|
542
1390
|
def frequency
|
|
543
|
-
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.
|
|
1391
|
+
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
|
|
544
1392
|
end
|
|
545
1393
|
|
|
1394
|
+
# Changes scale while keeping pitch, or returns current scale.
|
|
1395
|
+
#
|
|
1396
|
+
# **Without argument**: Returns current scale.
|
|
1397
|
+
#
|
|
1398
|
+
# **With argument**: Returns note at same pitch in different scale kind.
|
|
1399
|
+
#
|
|
1400
|
+
# @param kind_id_or_kind [Symbol, ScaleKind, nil] scale kind or ID
|
|
1401
|
+
# @return [Scale, NoteInScale] current scale or note in new scale
|
|
1402
|
+
#
|
|
1403
|
+
# @example Query current scale
|
|
1404
|
+
# note.scale # => <Scale: kind = MajorScaleKind ...>
|
|
1405
|
+
#
|
|
1406
|
+
# @example Change to minor
|
|
1407
|
+
# note.scale(:minor) # Same pitch in minor scale
|
|
1408
|
+
#
|
|
1409
|
+
# @example Dynamic method
|
|
1410
|
+
# note.minor # Same as note.scale(:minor)
|
|
1411
|
+
# note.major # Same as note.scale(:major)
|
|
546
1412
|
def scale(kind_id_or_kind = nil)
|
|
547
1413
|
if kind_id_or_kind.nil?
|
|
548
1414
|
@scale
|
|
@@ -555,10 +1421,52 @@ module Musa
|
|
|
555
1421
|
end
|
|
556
1422
|
end
|
|
557
1423
|
|
|
1424
|
+
# Finds this note in another scale.
|
|
1425
|
+
#
|
|
1426
|
+
# Searches for a note with the same pitch in the target scale.
|
|
1427
|
+
#
|
|
1428
|
+
# @param scale [Scale] target scale to search
|
|
1429
|
+
# @return [NoteInScale, nil] note in target scale or nil
|
|
1430
|
+
#
|
|
1431
|
+
# @example
|
|
1432
|
+
# c_major_tonic = c_major.tonic
|
|
1433
|
+
# c_minor = tuning.minor[60]
|
|
1434
|
+
# c_major_tonic.on(c_minor) # C in C minor scale
|
|
558
1435
|
def on(scale)
|
|
559
1436
|
scale.note_of_pitch @pitch
|
|
560
1437
|
end
|
|
561
1438
|
|
|
1439
|
+
# Builds a chord rooted on this note.
|
|
1440
|
+
#
|
|
1441
|
+
# Creates a chord using this note as the root. Chord can be specified by:
|
|
1442
|
+
# - Feature values (:triad, :seventh, :major, :minor, etc.)
|
|
1443
|
+
# - Feature hash (quality:, size:)
|
|
1444
|
+
# - Chord definition name (not shown here, see Chord.with_root)
|
|
1445
|
+
#
|
|
1446
|
+
# If no features specified, defaults to major triad.
|
|
1447
|
+
#
|
|
1448
|
+
# @param feature_values [Array<Symbol>] feature values (size, quality, etc.)
|
|
1449
|
+
# @param allow_chromatic [Boolean] allow non-diatonic chord notes
|
|
1450
|
+
# @param move [Hash{Symbol => Integer}] initial octave moves
|
|
1451
|
+
# @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
|
|
1452
|
+
# @param features_hash [Hash] feature key-value pairs
|
|
1453
|
+
# @return [Chord] chord rooted on this note
|
|
1454
|
+
#
|
|
1455
|
+
# @example Default triad
|
|
1456
|
+
# note.chord # Major triad
|
|
1457
|
+
#
|
|
1458
|
+
# @example Specified size
|
|
1459
|
+
# note.chord :seventh # Seventh chord matching scale
|
|
1460
|
+
# note.chord :ninth # Ninth chord
|
|
1461
|
+
#
|
|
1462
|
+
# @example With features
|
|
1463
|
+
# note.chord quality: :minor, size: :seventh
|
|
1464
|
+
# note.chord :minor, :seventh # Same as above
|
|
1465
|
+
#
|
|
1466
|
+
# @example With voicing
|
|
1467
|
+
# note.chord :seventh, move: {root: -1}, duplicate: {fifth: 1}
|
|
1468
|
+
#
|
|
1469
|
+
# @see Chord Chord class
|
|
562
1470
|
def chord(*feature_values,
|
|
563
1471
|
allow_chromatic: nil,
|
|
564
1472
|
move: nil,
|
|
@@ -575,6 +1483,12 @@ module Musa
|
|
|
575
1483
|
**features)
|
|
576
1484
|
end
|
|
577
1485
|
|
|
1486
|
+
# Checks note equality.
|
|
1487
|
+
#
|
|
1488
|
+
# Notes are equal if they have same scale, grade, octave, and pitch.
|
|
1489
|
+
#
|
|
1490
|
+
# @param other [NoteInScale]
|
|
1491
|
+
# @return [Boolean]
|
|
578
1492
|
def ==(other)
|
|
579
1493
|
self.class == other.class &&
|
|
580
1494
|
@scale == other.scale &&
|
|
@@ -583,6 +1497,9 @@ module Musa
|
|
|
583
1497
|
@pitch == other.pitch
|
|
584
1498
|
end
|
|
585
1499
|
|
|
1500
|
+
# Returns string representation.
|
|
1501
|
+
#
|
|
1502
|
+
# @return [String]
|
|
586
1503
|
def inspect
|
|
587
1504
|
"<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
|
|
588
1505
|
end
|