musa-dsl 0.26.5 → 0.26.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,319 +3,213 @@ require_relative 'chord-definition'
3
3
 
4
4
  module Musa
5
5
  module Chords
6
- using Musa::Extension::Arrayfy
7
-
8
6
  class Chord
9
- def initialize(name_or_notes_or_pitches = nil, # name | [notes] | [pitches]
10
- # definitory
11
- name: nil,
12
- root: nil, root_grade: nil,
13
- notes: nil, pitches: nil,
14
- features: nil,
15
- # target scale (or scale reference)
16
- scale: nil,
17
- allow_chromatic: nil,
18
- # operations
19
- inversion: nil, state: nil,
20
- position: nil,
21
- move: nil,
22
- duplicate: nil,
23
- add: nil,
24
- drop: nil,
25
- #
26
- _source: nil)
27
-
28
- # Preparing notes and pitches Arrays: they will we used to collect further notes and pitches
29
- #
30
- if notes
31
- notes = notes.collect do |n|
32
- case n
33
- when Scales::NoteInScale
34
- n
35
- when Numeric, Symbol
36
- scale[n]
7
+
8
+ using Musa::Extension::Arrayfy
9
+
10
+ def self.with_root(root_note_or_pitch_or_symbol, scale: nil, allow_chromatic: false, name: nil, move: nil, duplicate: nil, **features)
11
+ root =
12
+ case root_note_or_pitch_or_symbol
13
+ when Scales::NoteInScale
14
+ root_note_or_pitch_or_symbol
15
+ when Numeric
16
+ if scale
17
+ scale.note_of_pitch(root_note_or_pitch_or_symbol, allow_chromatic: allow_chromatic)
37
18
  else
38
- raise ArgumentError, "Can't recognize #{n} in notes list #{notes}"
19
+ scale = Musa::Scales::Scales.default_system.default_tuning[root_note_or_pitch_or_symbol].major
20
+ scale.note_of_pitch(root_note_or_pitch_or_symbol)
39
21
  end
22
+ when Symbol
23
+ raise ArgumentError, "Missing scale parameter to calculate root note for #{root_note_or_pitch_or_symbol}" unless scale
24
+
25
+ scale[root_note_or_pitch_or_symbol]
26
+ else
27
+ raise ArgumentError, "Unexpected #{root_note_or_pitch_or_symbol}"
40
28
  end
41
- end
42
29
 
43
- pitches = pitches.clone if pitches
30
+ scale ||= root.scale
44
31
 
45
- # Preparing root_pitch
46
- #
32
+ if name
33
+ raise ArgumentError, "Received name parameter with value #{name}: features parameter is not allowed" if features.any?
47
34
 
48
- root_pitch = nil
35
+ chord_definition = ChordDefinition[name]
49
36
 
50
- raise ArgumentError, "Duplicate parameter: root: #{root} and root_grade: #{root_grade}" if root && root_grade
37
+ elsif features.any?
38
+ chord_definition = Helper.find_definition_by_features(root.pitch, features, scale, allow_chromatic: allow_chromatic)
51
39
 
52
- allow_chromatic ||= scale.nil?
40
+ else
41
+ raise ArgumentError, "Don't know how to find a chord definition without name or features parameters"
42
+ end
53
43
 
54
- if root&.is_a?(Scales::NoteInScale)
55
- root_pitch = root.pitch
56
- scale ||= root.scale
44
+ unless chord_definition
45
+ raise ArgumentError,
46
+ "Unable to find chord definition for root #{root}" \
47
+ "#{" with name #{name}" if name}" \
48
+ "#{" with features #{features}" if features.any?}"
57
49
  end
58
50
 
59
- raise ArgumentError, "Don't know how to recognize root_grade #{root_grade}: scale is not provided" if root_grade && !scale
51
+ source_notes_map = Helper.compute_source_notes_map(root, chord_definition, scale)
52
+
53
+ Chord.new(root, scale, chord_definition, move, duplicate, source_notes_map)
54
+ end
60
55
 
61
- root_pitch = scale[root_grade].pitch if root_grade && scale
56
+ class Helper
57
+ def self.compute_source_notes_map(root, chord_definition, scale)
58
+ chord_definition.pitch_offsets.transform_values do |offset|
59
+ pitch = root.pitch + offset
60
+ [scale.note_of_pitch(pitch) || scale.chromatic.note_of_pitch(pitch)]
61
+ end.tap { |_| _.values.each(&:freeze) }.freeze
62
+ end
62
63
 
63
- # Parse name_or_notes_or_pitches to name, notes, pitches
64
- #
65
- #
66
- case name_or_notes_or_pitches
67
- when Symbol
68
- raise ArgumentError, "Duplicate parameter #{name_or_notes_or_pitches} and name: #{name}" if name
69
-
70
- name = name_or_notes_or_pitches
71
-
72
- when Array
73
- name_or_notes_or_pitches.each do |note_or_pitch|
74
- case note_or_pitch
75
- when Scales::NoteInScale
76
- notes ||= [] << note_or_pitch
77
- when Numeric
78
- if scale
79
- notes ||= [] << scale[note_or_pitch]
80
- else
81
- pitches ||= [] << note_or_pitch
82
- end
83
- when Symbol
84
- raise ArgumentError, "Don't know how to recognize #{note_or_pitch} in parameter list #{name_or_notes_or_pitches}: it's a symbol but the scale is not provided" unless scale
85
-
86
- notes ||= [] << scale[note_or_pitch]
87
- else
88
- raise ArgumentError, "Can't recognize #{note_or_pitch} in parameter list #{name_or_notes_or_pitches}"
64
+ def self.find_definition_by_features(root_pitch, features, scale, allow_chromatic:)
65
+ featured_chord_definitions = ChordDefinition.find_by_features(**features)
66
+
67
+ unless allow_chromatic
68
+ featured_chord_definitions.reject! do |chord_definition|
69
+ chord_definition.pitches(root_pitch).find { |chord_pitch| scale.note_of_pitch(chord_pitch).nil? }
89
70
  end
90
71
  end
91
72
 
92
- when nil
93
- # nothing happens
94
- else
95
- raise ArgumentError, "Can't recognize #{name_or_notes_or_pitches}"
73
+ featured_chord_definitions.first
96
74
  end
75
+ end
97
76
 
98
- # Eval definitory atributes
99
- #
100
-
101
- @notes = if _source.nil?
102
- compute_notes(name, root_pitch, scale, notes, pitches, features, allow_chromatic)
103
- else
104
- compute_notes_from_source(_source, name, root_pitch, scale, notes, pitches, features, allow_chromatic)
105
- end
77
+ private_constant :Helper
106
78
 
107
- # Eval adding / droping operations
108
- #
79
+ ChordGradeNote = Struct.new(:grade, :note, keyword_init: true)
109
80
 
110
- add&.each do |to_add|
111
- case to_add
112
- when NoteInScale
113
- @notes << to_add
114
- when Numeric # pitch increment
115
- pitch = root_pitch + to_add
116
- @notes << scale.note_of_pitch(pitch) || scale.chromatic.note_of_pitch(pitch)
117
- when Symbol # interval name
118
- pitch = root_pitch + scale.offset_of_interval(to_add)
119
- @notes << scale.note_of_pitch(pitch)
120
- else
121
- raise ArgumentError, "Can't recognize element to add #{to_add}"
122
- end
123
- end
81
+ private_constant :ChordGradeNote
124
82
 
125
- # TODO: Missing chord operations: drop, inversion, state, position
126
- #
127
- raise NotImplementedError, 'Missing chord operations: drop, inversion, state, position' if drop || inversion || state || position
83
+ private def initialize(root, scale, chord_definition, move, duplicate, source_notes_map)
84
+ @root = root
85
+ @scale = scale
86
+ @chord_definition = chord_definition
87
+ @move = move.dup.freeze || {}
88
+ # TODO: ojo esto implica que sólo se puede duplicar una vez cada grado! permitir múltiples?
89
+ @duplicate = duplicate.dup.freeze || {}
90
+ @source_notes_map = source_notes_map.dup.freeze
91
+ @notes_map = compute_moved_and_duplicated(source_notes_map, move, duplicate)
128
92
 
129
- # Eval voice increment operations
93
+ # Calculate sorted notes: from lower to higher notes
130
94
  #
131
-
132
- if move
133
- raise ArgumentError, 'move: expected a Hash' unless move.is_a?(Hash)
134
-
135
- move.each do |position, octave|
136
- @notes[position][0] = @notes[position][0].octave(octave)
95
+ @sorted_notes = []
96
+ @notes_map.each_pair do |name, array_of_notes|
97
+ array_of_notes.each do |note|
98
+ @sorted_notes << ChordGradeNote.new(grade: name, note: note).freeze
137
99
  end
138
100
  end
139
101
 
140
- if duplicate
141
- raise ArgumentError, 'duplicate: expected a Hash' unless duplicate.is_a?(Hash)
102
+ @sorted_notes.sort_by! { |chord_grade_note| chord_grade_note.note.pitch }
103
+ @sorted_notes.freeze
142
104
 
143
- duplicate.each do |position, octave|
144
- octave.arrayfy.each do |octave|
145
- @notes[position] << @notes[position][0].octave(octave)
105
+ # Add getters for grades
106
+ #
107
+ @notes_map.each_key do |chord_grade_name|
108
+ define_singleton_method chord_grade_name do |all: false|
109
+ if all
110
+ @notes_map[chord_grade_name]
111
+ else
112
+ @notes_map[chord_grade_name].first
146
113
  end
147
114
  end
148
115
  end
149
116
 
150
- # Identify chord
117
+ # Add getters for the features values
151
118
  #
119
+ @chord_definition.features.each_key do |feature_name|
120
+ define_singleton_method feature_name do
121
+ @chord_definition.features[feature_name]
122
+ end
123
+ end
152
124
 
153
- @notes.freeze
154
-
155
- @chord_definition = ChordDefinition.find_by_pitches(@notes.values.flatten(1).collect(&:pitch))
156
-
157
- ChordDefinition.feature_values.each do |name|
158
- define_singleton_method name do
159
- featuring(name)
125
+ # Add navigation methods to other chords based on changing a feature
126
+ #
127
+ ChordDefinition.feature_keys.each do |feature_name|
128
+ define_singleton_method "with_#{feature_name}".to_sym do |feature_value, allow_chromatic: true|
129
+ featuring(allow_chromatic: allow_chromatic, **{ feature_name => feature_value })
160
130
  end
161
131
  end
162
132
  end
163
133
 
164
- attr_reader :notes, :chord_definition
134
+ attr_reader :scale, :chord_definition, :move, :duplicate
165
135
 
166
- def name(name = nil)
167
- if name.nil?
168
- @chord_definition&.name
169
- else
170
- Chord.new(_source: self, name: name)
171
- end
136
+ def notes
137
+ @sorted_notes
172
138
  end
173
139
 
174
- def features
175
- @chord_definition&.features
140
+ def pitches(*grades)
141
+ grades = @notes_map.keys if grades.empty?
142
+ @sorted_notes.select { |_| grades.include?(_.grade) }.collect { |_| _.note.pitch }
176
143
  end
177
144
 
178
- def featuring(*values, allow_chromatic: nil, **hash)
179
- features = @chord_definition.features.dup if @chord_definition
180
- features ||= {}
145
+ def features
146
+ @chord_definition.features
147
+ end
181
148
 
149
+ def featuring(*values, allow_chromatic: false, **hash)
150
+ # create a new list of features based on current features but
151
+ # replacing the values for the new ones and adding the new features
152
+ #
153
+ features = @chord_definition.features.dup
182
154
  ChordDefinition.features_from(values, hash).each { |k, v| features[k] = v }
183
155
 
184
- Chord.new(_source: self, allow_chromatic: allow_chromatic, features: features)
185
- end
156
+ chord_definition = Helper.find_definition_by_features(@root.pitch, features, @scale, allow_chromatic: allow_chromatic)
186
157
 
187
- def root(root = nil)
188
- if root.nil?
189
- @notes[:root]
190
- else
191
- Chord.new(_source: self, root: root)
192
- end
193
- end
194
-
195
- def [](position)
196
- case position
197
- when Numeric
198
- @notes.values[position]
199
- when Symbol
200
- @notes[position]
201
- end
202
- end
158
+ raise ArgumentError, "Unable to find a chord definition for #{features}" unless chord_definition
203
159
 
204
- def move(**octaves)
205
- Chord.new(_source: self, move: octaves)
206
- end
160
+ source_notes_map = Helper.compute_source_notes_map(@root, chord_definition, @scale)
207
161
 
208
- def duplicate(**octaves)
209
- Chord.new(_source: self, duplicate: octaves)
162
+ Chord.new(@root,
163
+ (@scale if chord_definition.in_scale?(@scale, chord_root_pitch: @root.pitch)),
164
+ chord_definition,
165
+ @move, @duplicate,
166
+ source_notes_map)
210
167
  end
211
168
 
212
- def scale
213
- scales = @notes.values.flatten(1).collect(&:scale).uniq
214
- scales.first if scales.size == 1
215
- end
169
+ def octave(octave)
170
+ source_notes_map = @source_notes_map.transform_values do |notes|
171
+ notes.collect { |note| note.octave(octave) }.freeze
172
+ end.freeze
216
173
 
217
- # Converts the chord to a specific scale with the notes in the chord
218
- def as_scale
174
+ Chord.new(@root.octave(octave), @scale, chord_definition, @move, @duplicate, source_notes_map)
219
175
  end
220
176
 
221
- def project_on_all(*scales, allow_chromatic: nil)
222
- # TODO add match to other chords... what does it means?
223
- allow_chromatic ||= false
224
-
225
- note_sets = {}
226
- scales.each do |scale|
227
- note_sets[scale] = if allow_chromatic
228
- @notes.values.flatten(1).collect { |n| n.on(scale) || n.on(scale.chromatic) }
229
- else
230
- @notes.values.flatten(1).collect { |n| n.on(scale) }
231
- end
232
- end
233
-
234
- note_sets_in_scale = note_sets.values.reject { |notes| notes.include?(nil) }
235
- note_sets_in_scale.collect { |notes| Chord.new(notes: notes) }
177
+ def move(**octaves)
178
+ Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
236
179
  end
237
180
 
238
- def project_on(*scales, allow_chromatic: nil)
239
- allow_chromatic ||= false
240
- project_on_all(*scales, allow_chromatic: allow_chromatic).first
181
+ def duplicate(**octaves)
182
+ Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
241
183
  end
242
184
 
243
185
  def ==(other)
244
- self.class == other.class && @notes == other.notes
186
+ self.class == other.class &&
187
+ @sorted_notes == other.notes &&
188
+ @chord_definition == other.chord_definition
245
189
  end
246
190
 
247
191
  def inspect
248
- "<Chord: notes = #{@notes}>"
192
+ "<Chord #{@name} root #{@root} notes #{@sorted_notes.collect { |_| "#{_.grade}=#{_.note.grade}|#{_.note.pitch} "} }>"
249
193
  end
250
194
 
251
195
  alias to_s inspect
252
196
 
253
- private
254
-
255
- def compute_notes(name, root_pitch, scale, notes, pitches, features, allow_chromatic)
256
- if name && root_pitch && scale && !(notes || pitches || features)
257
-
258
- chord_definition = ChordDefinition[name]
259
-
260
- raise ArgumentError, "Unrecognized #{name} chord" unless chord_definition
261
-
262
- chord_definition.pitch_offsets.transform_values do |offset|
263
- pitch = root_pitch + offset
264
- [scale.note_of_pitch(pitch) || scale.chromatic.note_of_pitch(pitch)]
265
- end
266
-
267
- elsif root_pitch && features && scale && !(name || notes || pitches)
197
+ private def compute_moved_and_duplicated(notes_map, moved, duplicated)
198
+ notes_map = notes_map.transform_values(&:dup)
268
199
 
269
- chord_definitions = ChordDefinition.find_by_features(**features)
270
-
271
- unless allow_chromatic
272
- chord_definitions.reject! do |chord_definition|
273
- chord_definition.pitches(root_pitch).find { |chord_pitch| scale.note_of_pitch(chord_pitch).nil? }
274
- end
275
- end
276
-
277
- selected = chord_definitions.first
278
-
279
- unless selected
280
- raise ArgumentError, "Don't know how to create a chord with root pitch #{root_pitch}"\
281
- " and features #{features} based on scale #{scale.kind.class} with root on #{scale.root}: "\
282
- " no suitable definition found (allow_chromatic is #{allow_chromatic})"
283
- end
200
+ moved&.each do |position, octave|
201
+ notes_map[position][0] = notes_map[position][0].octave(octave)
202
+ end
284
203
 
285
- selected.pitch_offsets.transform_values do |offset|
286
- pitch = root_pitch + offset
287
- [scale.note_of_pitch(pitch) || scale.chromatic.note_of_pitch(pitch)]
204
+ duplicated&.each do |position, octave|
205
+ octave.arrayfy.each do |octave|
206
+ notes_map[position] << notes_map[position][0].octave(octave)
288
207
  end
289
-
290
- elsif (notes || pitches && scale) && !(name || root_pitch || features)
291
-
292
- notes ||= []
293
-
294
- notes += pitches.collect { |p| scale.note_of_pitch(p) } if pitches
295
-
296
- chord_definition = ChordDefinition.find_by_pitches(notes.collect(&:pitch))
297
-
298
- raise "Can't find a chord definition for pitches #{pitches} on scale #{scale.kind.id} based on #{scale.root}" unless chord_definition
299
-
300
- chord_definition.named_pitches(notes, &:pitch)
301
- else
302
- pattern = { name: name, root: root_pitch, scale: scale, notes: notes, pitches: pitches, features: features, allow_chromatic: allow_chromatic }
303
- raise ArgumentError, "Can't understand chord definition pattern #{pattern}"
304
208
  end
305
- end
306
209
 
307
- def compute_notes_from_source(source, name, root_pitch, scale, notes, pitches, features, allow_chromatic)
308
- if !(name || root_pitch || scale || notes || pitches || features)
309
- source.notes
310
-
311
- elsif features && !(name || root_pitch || scale || notes || pitches)
312
- compute_notes(nil, source.root.first.pitch, source.root.first.scale, nil, nil, features, allow_chromatic)
313
-
314
- else
315
- pattern = { name: name, root: root_pitch, scale: scale, notes: notes, pitches: pitches, features: features, allow_chromatic: allow_chromatic }
316
- raise ArgumentError, "Can't understand chord definition pattern #{pattern}"
317
- end
210
+ notes_map.tap { |_| _.values.each(&:freeze) }.freeze
318
211
  end
319
212
  end
320
213
  end
321
214
  end
215
+
@@ -70,31 +70,31 @@ module Musa
70
70
  class MajorScaleKind < ScaleKind
71
71
  class << self
72
72
  @@pitches =
73
- [{ functions: %i[I _1 tonic],
73
+ [{ functions: %i[I _1 tonic first],
74
74
  pitch: 0 },
75
- { functions: %i[II _2 supertonic],
75
+ { functions: %i[II _2 supertonic second],
76
76
  pitch: 2 },
77
- { functions: %i[III _3 mediant],
77
+ { functions: %i[III _3 mediant third],
78
78
  pitch: 4 },
79
- { functions: %i[IV _4 subdominant],
79
+ { functions: %i[IV _4 subdominant fourth],
80
80
  pitch: 5 },
81
- { functions: %i[V _5 dominant],
81
+ { functions: %i[V _5 dominant fifth],
82
82
  pitch: 7 },
83
- { functions: %i[VI _6 submediant relative relative_minor],
83
+ { functions: %i[VI _6 submediant relative relative_minor sixth],
84
84
  pitch: 9 },
85
- { functions: %i[VII _7 leading],
85
+ { functions: %i[VII _7 leading seventh],
86
86
  pitch: 11 },
87
- { functions: %i[VIII _8],
87
+ { functions: %i[VIII _8 eighth],
88
88
  pitch: 12 },
89
- { functions: %i[IX _9],
89
+ { functions: %i[IX _9 ninth],
90
90
  pitch: 12 + 2 },
91
- { functions: %i[X _10],
91
+ { functions: %i[X _10 tenth],
92
92
  pitch: 12 + 4 },
93
- { functions: %i[XI _11],
93
+ { functions: %i[XI _11 eleventh],
94
94
  pitch: 12 + 5 },
95
- { functions: %i[XII _12],
95
+ { functions: %i[XII _12 twelfth],
96
96
  pitch: 12 + 7 },
97
- { functions: %i[XIII _13],
97
+ { functions: %i[XIII _13 thirteenth],
98
98
  pitch: 12 + 9 }].freeze
99
99
 
100
100
  def pitches
@@ -113,34 +113,34 @@ module Musa
113
113
  EquallyTempered12ToneScaleSystem.register MajorScaleKind
114
114
  end
115
115
 
116
- class MinorScaleKind < ScaleKind
116
+ class MinorNaturalScaleKind < ScaleKind
117
117
  class << self
118
118
  @@pitches =
119
- [{ functions: %i[i _1 tonic],
119
+ [{ functions: %i[i _1 tonic first],
120
120
  pitch: 0 },
121
- { functions: %i[ii _2 supertonic],
121
+ { functions: %i[ii _2 supertonic second],
122
122
  pitch: 2 },
123
- { functions: %i[iii _3 mediant relative relative_major],
123
+ { functions: %i[iii _3 mediant relative relative_major third],
124
124
  pitch: 3 },
125
- { functions: %i[iv _4 subdominant],
125
+ { functions: %i[iv _4 subdominant fourth],
126
126
  pitch: 5 },
127
- { functions: %i[v _5 dominant],
127
+ { functions: %i[v _5 dominant fifth],
128
128
  pitch: 7 },
129
- { functions: %i[vi _6 submediant],
129
+ { functions: %i[vi _6 submediant sixth],
130
130
  pitch: 8 },
131
- { functions: %i[vii _7],
131
+ { functions: %i[vii _7 seventh],
132
132
  pitch: 10 },
133
- { functions: %i[viii _8],
133
+ { functions: %i[viii _8 eighth],
134
134
  pitch: 12 },
135
- { functions: %i[ix _9],
135
+ { functions: %i[ix _9 ninth],
136
136
  pitch: 12 + 2 },
137
- { functions: %i[x _10],
137
+ { functions: %i[x _10 tenth],
138
138
  pitch: 12 + 3 },
139
- { functions: %i[xi _11],
139
+ { functions: %i[xi _11 eleventh],
140
140
  pitch: 12 + 5 },
141
- { functions: %i[xii _12],
141
+ { functions: %i[xii _12 twelfth],
142
142
  pitch: 12 + 7 },
143
- { functions: %i[xiii _13],
143
+ { functions: %i[xiii _13 thirteenth],
144
144
  pitch: 12 + 8 }].freeze
145
145
 
146
146
  def pitches
@@ -156,7 +156,7 @@ module Musa
156
156
  end
157
157
  end
158
158
 
159
- EquallyTempered12ToneScaleSystem.register MinorScaleKind
159
+ EquallyTempered12ToneScaleSystem.register MinorNaturalScaleKind
160
160
  end
161
161
 
162
162
  class MinorHarmonicScaleKind < ScaleKind
@@ -1,3 +1,5 @@
1
+ require_relative 'chords'
2
+
1
3
  module Musa
2
4
  module Scales
3
5
  module Scales
@@ -180,8 +182,6 @@ module Musa
180
182
  end
181
183
 
182
184
  class ScaleKind
183
- extend Forwardable
184
-
185
185
  def initialize(tuning)
186
186
  @tuning = tuning
187
187
  @scales = {}
@@ -194,6 +194,10 @@ module Musa
194
194
  @scales[root_pitch]
195
195
  end
196
196
 
197
+ def default_root
198
+ self[60]
199
+ end
200
+
197
201
  def absolut
198
202
  self[0]
199
203
  end
@@ -277,9 +281,10 @@ module Musa
277
281
  end
278
282
  end
279
283
 
284
+ freeze
280
285
  end
281
286
 
282
- def_delegators :@kind, :a_tuning
287
+ def_delegators :@kind, :tuning
283
288
 
284
289
  attr_reader :kind, :root_pitch
285
290
 
@@ -406,10 +411,6 @@ module Musa
406
411
  @kind.tuning.offset_of_interval(interval_name)
407
412
  end
408
413
 
409
- def chord_of(*grades_or_symbols)
410
- Chord.new(notes: grades_or_symbols.collect { |g| self[g] })
411
- end
412
-
413
414
  def ==(other)
414
415
  self.class == other.class &&
415
416
  @kind == other.kind &&
@@ -454,13 +455,13 @@ module Musa
454
455
  @scale.kind.class.pitches[grade][:functions]
455
456
  end
456
457
 
457
- def octave(octave = nil)
458
+ def octave(octave = nil, absolute: false)
458
459
  if octave.nil?
459
460
  @octave
460
461
  else
461
462
  raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
462
463
 
463
- @scale[@grade + (@octave + octave) * @scale.kind.class.grades]
464
+ @scale[@grade + ((absolute ? 0 : @octave) + octave) * @scale.kind.class.grades]
464
465
  end
465
466
  end
466
467
 
@@ -558,11 +559,20 @@ module Musa
558
559
  scale.note_of_pitch @pitch
559
560
  end
560
561
 
561
- def chord(*feature_values, allow_chromatic: nil, **features_hash)
562
+ def chord(*feature_values,
563
+ allow_chromatic: nil,
564
+ move: nil,
565
+ duplicate: nil,
566
+ **features_hash)
567
+
562
568
  features = { size: :triad } if feature_values.empty? && features_hash.empty?
563
- features ||= ChordDefinition.features_from(feature_values, features_hash)
569
+ features ||= Musa::Chords::ChordDefinition.features_from(feature_values, features_hash)
564
570
 
565
- Musa::Chords::Chord.new(root: self, allow_chromatic: allow_chromatic, features: features)
571
+ Musa::Chords::Chord.with_root(self,
572
+ allow_chromatic: allow_chromatic,
573
+ move: move,
574
+ duplicate: duplicate,
575
+ **features)
566
576
  end
567
577
 
568
578
  def ==(other)