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
|
@@ -3,10 +3,142 @@ require_relative 'chord-definition'
|
|
|
3
3
|
|
|
4
4
|
module Musa
|
|
5
5
|
module Chords
|
|
6
|
+
# Instantiated chord with specific root and scale context.
|
|
7
|
+
#
|
|
8
|
+
# Chord represents an actual chord instance with a root note, scale context,
|
|
9
|
+
# and chord definition. It provides access to chord tones, voicing modifications,
|
|
10
|
+
# and navigation between related chords.
|
|
11
|
+
#
|
|
12
|
+
# ## Creation
|
|
13
|
+
#
|
|
14
|
+
# Chords are typically created from scale notes rather than directly:
|
|
15
|
+
#
|
|
16
|
+
# scale = Scales::Scales.default_system.default_tuning.major[60]
|
|
17
|
+
# chord = scale.tonic.chord # C major triad
|
|
18
|
+
# chord = scale.tonic.chord :seventh # C major seventh
|
|
19
|
+
# chord = scale.dominant.chord :ninth # G ninth chord
|
|
20
|
+
#
|
|
21
|
+
# ## Accessing Chord Tones
|
|
22
|
+
#
|
|
23
|
+
# Chord tones are accessed by their position name (root, third, fifth, etc.):
|
|
24
|
+
#
|
|
25
|
+
# chord.root # Returns NoteInScale for root
|
|
26
|
+
# chord.third # Returns NoteInScale for third
|
|
27
|
+
# chord.fifth # Returns NoteInScale for fifth
|
|
28
|
+
# chord.seventh # Returns NoteInScale for seventh (if exists)
|
|
29
|
+
#
|
|
30
|
+
# When notes are duplicated, use `all: true` to get all instances:
|
|
31
|
+
#
|
|
32
|
+
# chord.root(all: true) # Returns array of all root notes
|
|
33
|
+
#
|
|
34
|
+
# ## Features and Navigation
|
|
35
|
+
#
|
|
36
|
+
# Chords have features (quality, size) and can navigate to related chords:
|
|
37
|
+
#
|
|
38
|
+
# chord.features # => { quality: :major, size: :triad }
|
|
39
|
+
# chord.quality # => :major (dynamic method)
|
|
40
|
+
# chord.size # => :triad (dynamic method)
|
|
41
|
+
#
|
|
42
|
+
# chord.with_quality(:minor) # Change to minor
|
|
43
|
+
# chord.with_size(:seventh) # Add seventh
|
|
44
|
+
# chord.featuring(size: :ninth) # Change multiple features
|
|
45
|
+
#
|
|
46
|
+
# ## Voicing Modifications
|
|
47
|
+
#
|
|
48
|
+
# ### Move - Relocate specific chord tones to different octaves:
|
|
49
|
+
#
|
|
50
|
+
# chord.move(root: -1, seventh: 1)
|
|
51
|
+
# # Root down one octave, seventh up one octave
|
|
52
|
+
#
|
|
53
|
+
# ### Duplicate - Add copies of chord tones in other octaves:
|
|
54
|
+
#
|
|
55
|
+
# chord.duplicate(root: -2, third: [-1, 1])
|
|
56
|
+
# # Add root 2 octaves down, third 1 octave down and 1 up
|
|
57
|
+
#
|
|
58
|
+
# ### Octave - Transpose entire chord:
|
|
59
|
+
#
|
|
60
|
+
# chord.octave(-1) # Move entire chord down one octave
|
|
61
|
+
#
|
|
62
|
+
# ## Pitch Extraction
|
|
63
|
+
#
|
|
64
|
+
# chord.pitches # All pitches sorted by pitch
|
|
65
|
+
# chord.pitches(:root, :third) # Only specified chord tones
|
|
66
|
+
# chord.notes # Sorted ChordGradeNote structs
|
|
67
|
+
#
|
|
68
|
+
# ## Scale Context
|
|
69
|
+
#
|
|
70
|
+
# Chords maintain their scale context. When navigating to chords with
|
|
71
|
+
# non-diatonic notes (e.g., major to minor), the scale may become nil:
|
|
72
|
+
#
|
|
73
|
+
# major_chord = c_major.tonic.chord
|
|
74
|
+
# major_chord.scale # => C major scale
|
|
75
|
+
#
|
|
76
|
+
# minor_chord = major_chord.with_quality(:minor)
|
|
77
|
+
# minor_chord.scale # => nil (Eb not in C major)
|
|
78
|
+
#
|
|
79
|
+
# @example Basic triad creation
|
|
80
|
+
# scale = Scales::Scales.default_system.default_tuning.major[60]
|
|
81
|
+
# chord = scale.tonic.chord
|
|
82
|
+
# chord.root.pitch # => 60 (C)
|
|
83
|
+
# chord.third.pitch # => 64 (E)
|
|
84
|
+
# chord.fifth.pitch # => 67 (G)
|
|
85
|
+
#
|
|
86
|
+
# @example Seventh chord
|
|
87
|
+
# chord = scale.tonic.chord :seventh
|
|
88
|
+
# chord.seventh.pitch # => 71 (B)
|
|
89
|
+
#
|
|
90
|
+
# @example Voicing with move and duplicate
|
|
91
|
+
# scale = Scales::Scales.default_system.default_tuning.major[60]
|
|
92
|
+
# chord = scale.dominant.chord(:seventh)
|
|
93
|
+
# .move(root: -1, third: -1)
|
|
94
|
+
# .duplicate(fifth: [0, 1])
|
|
95
|
+
#
|
|
96
|
+
# @example Feature navigation
|
|
97
|
+
# scale = Scales::Scales.default_system.default_tuning.major[60]
|
|
98
|
+
# maj_triad = scale.tonic.chord
|
|
99
|
+
# min_triad = maj_triad.with_quality(:minor)
|
|
100
|
+
# maj_seventh = maj_triad.with_size(:seventh)
|
|
101
|
+
#
|
|
102
|
+
# @see ChordDefinition Chord template/definition
|
|
103
|
+
# @see NoteInScale Note within scale
|
|
104
|
+
# @see chord-definitions.rb Standard chord types
|
|
6
105
|
class Chord
|
|
7
106
|
|
|
8
107
|
using Musa::Extension::Arrayfy
|
|
9
108
|
|
|
109
|
+
# Creates a chord with specified root.
|
|
110
|
+
#
|
|
111
|
+
# Factory method for creating chords by specifying the root note and either
|
|
112
|
+
# a chord definition name or features. The root can be a NoteInScale, pitch
|
|
113
|
+
# number, or scale degree symbol.
|
|
114
|
+
#
|
|
115
|
+
# @param root_note_or_pitch_or_symbol [NoteInScale, Integer, Symbol] chord root
|
|
116
|
+
# - NoteInScale: use note directly
|
|
117
|
+
# - Integer (MIDI pitch): find note in scale, or create C major if no scale
|
|
118
|
+
# - Symbol (scale degree): requires scale parameter (e.g., :tonic, :dominant)
|
|
119
|
+
# @param scale [Scale, nil] scale context for finding notes
|
|
120
|
+
# @param allow_chromatic [Boolean] allow non-diatonic notes
|
|
121
|
+
# @param name [Symbol, nil] chord definition name (:maj, :min7, etc.)
|
|
122
|
+
# @param move [Hash{Symbol => Integer}, nil] initial octave moves (e.g., `{root: -1}`)
|
|
123
|
+
# @param duplicate [Hash{Symbol => Integer, Array<Integer>}, nil] initial duplications
|
|
124
|
+
# @param features [Hash] chord features if not using name (quality:, size:, etc.)
|
|
125
|
+
# @return [Chord] new chord instance
|
|
126
|
+
#
|
|
127
|
+
# @example With note from scale
|
|
128
|
+
# Chord.with_root(scale.tonic, name: :maj7)
|
|
129
|
+
#
|
|
130
|
+
# @example With MIDI pitch and scale
|
|
131
|
+
# Chord.with_root(60, scale: c_major, name: :min)
|
|
132
|
+
#
|
|
133
|
+
# @example With scale degree
|
|
134
|
+
# Chord.with_root(:dominant, scale: c_major, quality: :dominant, size: :seventh)
|
|
135
|
+
#
|
|
136
|
+
# @example With features instead of name
|
|
137
|
+
# Chord.with_root(60, scale: c_major, quality: :major, size: :triad)
|
|
138
|
+
#
|
|
139
|
+
# @example With voicing parameters
|
|
140
|
+
# Chord.with_root(60, scale: c_major, name: :maj7,
|
|
141
|
+
# move: {root: -1}, duplicate: {fifth: 1})
|
|
10
142
|
def self.with_root(root_note_or_pitch_or_symbol, scale: nil, allow_chromatic: false, name: nil, move: nil, duplicate: nil, **features)
|
|
11
143
|
root =
|
|
12
144
|
case root_note_or_pitch_or_symbol
|
|
@@ -53,7 +185,21 @@ module Musa
|
|
|
53
185
|
Chord.new(root, scale, chord_definition, move, duplicate, source_notes_map)
|
|
54
186
|
end
|
|
55
187
|
|
|
188
|
+
# Internal helper methods for chord construction.
|
|
189
|
+
#
|
|
190
|
+
# @api private
|
|
56
191
|
class Helper
|
|
192
|
+
# Computes the source notes map for a chord.
|
|
193
|
+
#
|
|
194
|
+
# Maps each chord position (root, third, fifth, etc.) to its corresponding
|
|
195
|
+
# note in the scale or chromatic scale.
|
|
196
|
+
#
|
|
197
|
+
# @param root [NoteInScale] chord root note
|
|
198
|
+
# @param chord_definition [ChordDefinition] chord structure
|
|
199
|
+
# @param scale [Scale] scale context
|
|
200
|
+
# @return [Hash{Symbol => Array<NoteInScale>}] position to notes mapping
|
|
201
|
+
#
|
|
202
|
+
# @api private
|
|
57
203
|
def self.compute_source_notes_map(root, chord_definition, scale)
|
|
58
204
|
chord_definition.pitch_offsets.transform_values do |offset|
|
|
59
205
|
pitch = root.pitch + offset
|
|
@@ -61,6 +207,18 @@ module Musa
|
|
|
61
207
|
end.tap { |_| _.values.each(&:freeze) }.freeze
|
|
62
208
|
end
|
|
63
209
|
|
|
210
|
+
# Finds a chord definition matching features and scale constraints.
|
|
211
|
+
#
|
|
212
|
+
# Searches for chord definitions with specified features, filtering out
|
|
213
|
+
# those that don't fit in the scale unless allow_chromatic is true.
|
|
214
|
+
#
|
|
215
|
+
# @param root_pitch [Integer] MIDI pitch of chord root
|
|
216
|
+
# @param features [Hash] desired chord features (quality:, size:, etc.)
|
|
217
|
+
# @param scale [Scale] scale context for diatonic filtering
|
|
218
|
+
# @param allow_chromatic [Boolean] allow non-diatonic chords
|
|
219
|
+
# @return [ChordDefinition, nil] matching definition or nil
|
|
220
|
+
#
|
|
221
|
+
# @api private
|
|
64
222
|
def self.find_definition_by_features(root_pitch, features, scale, allow_chromatic:)
|
|
65
223
|
featured_chord_definitions = ChordDefinition.find_by_features(**features)
|
|
66
224
|
|
|
@@ -76,10 +234,33 @@ module Musa
|
|
|
76
234
|
|
|
77
235
|
private_constant :Helper
|
|
78
236
|
|
|
237
|
+
# Container for chord tone with its position name.
|
|
238
|
+
#
|
|
239
|
+
# Associates a chord position (grade) with its corresponding note.
|
|
240
|
+
# Used internally for sorting and organizing chord notes.
|
|
241
|
+
#
|
|
242
|
+
# @!attribute grade
|
|
243
|
+
# @return [Symbol] position name (:root, :third, :fifth, etc.)
|
|
244
|
+
# @!attribute note
|
|
245
|
+
# @return [NoteInScale] the note at this position
|
|
246
|
+
#
|
|
247
|
+
# @api private
|
|
79
248
|
ChordGradeNote = Struct.new(:grade, :note, keyword_init: true)
|
|
80
249
|
|
|
81
250
|
private_constant :ChordGradeNote
|
|
82
251
|
|
|
252
|
+
# Creates a chord (private constructor).
|
|
253
|
+
#
|
|
254
|
+
# Use {with_root} or create chords from scale notes instead.
|
|
255
|
+
#
|
|
256
|
+
# @param root [NoteInScale] chord root note
|
|
257
|
+
# @param scale [Scale, nil] scale context (nil if chromatic notes present)
|
|
258
|
+
# @param chord_definition [ChordDefinition] chord structure
|
|
259
|
+
# @param move [Hash, nil] octave moves for positions
|
|
260
|
+
# @param duplicate [Hash, nil] octave duplications for positions
|
|
261
|
+
# @param source_notes_map [Hash] position to notes mapping
|
|
262
|
+
#
|
|
263
|
+
# @api private
|
|
83
264
|
private def initialize(root, scale, chord_definition, move, duplicate, source_notes_map)
|
|
84
265
|
@root = root
|
|
85
266
|
@scale = scale
|
|
@@ -131,21 +312,82 @@ module Musa
|
|
|
131
312
|
end
|
|
132
313
|
end
|
|
133
314
|
|
|
134
|
-
|
|
135
|
-
|
|
315
|
+
# Scale context (nil if chord contains non-diatonic notes).
|
|
316
|
+
# @return [Scale, nil]
|
|
317
|
+
attr_reader :scale
|
|
318
|
+
|
|
319
|
+
# Chord definition template.
|
|
320
|
+
# @return [ChordDefinition]
|
|
321
|
+
attr_reader :chord_definition
|
|
322
|
+
|
|
323
|
+
# Octave moves applied to positions.
|
|
324
|
+
# @return [Hash{Symbol => Integer}]
|
|
325
|
+
attr_reader :move
|
|
326
|
+
|
|
327
|
+
# Octave duplications applied to positions.
|
|
328
|
+
# @return [Hash{Symbol => Integer, Array<Integer>}]
|
|
329
|
+
attr_reader :duplicate
|
|
330
|
+
|
|
331
|
+
# Returns chord notes sorted by pitch.
|
|
332
|
+
#
|
|
333
|
+
# @return [Array<ChordGradeNote>] sorted array of grade-note pairs
|
|
334
|
+
#
|
|
335
|
+
# @example
|
|
336
|
+
# chord.notes.each do |chord_grade_note|
|
|
337
|
+
# puts "#{chord_grade_note.grade}: #{chord_grade_note.note.pitch}"
|
|
338
|
+
# end
|
|
136
339
|
def notes
|
|
137
340
|
@sorted_notes
|
|
138
341
|
end
|
|
139
342
|
|
|
343
|
+
# Returns MIDI pitches of chord notes.
|
|
344
|
+
#
|
|
345
|
+
# Without arguments, returns all pitches sorted from low to high.
|
|
346
|
+
# With grade arguments, returns only pitches for those positions.
|
|
347
|
+
#
|
|
348
|
+
# @param grades [Array<Symbol>] optional position names to filter
|
|
349
|
+
# @return [Array<Integer>] MIDI pitches sorted by pitch
|
|
350
|
+
#
|
|
351
|
+
# @example All pitches
|
|
352
|
+
# chord.pitches # => [60, 64, 67]
|
|
353
|
+
#
|
|
354
|
+
# @example Specific positions
|
|
355
|
+
# chord.pitches(:root, :third) # => [60, 64]
|
|
140
356
|
def pitches(*grades)
|
|
141
357
|
grades = @notes_map.keys if grades.empty?
|
|
142
358
|
@sorted_notes.select { |_| grades.include?(_.grade) }.collect { |_| _.note.pitch }
|
|
143
359
|
end
|
|
144
360
|
|
|
361
|
+
# Returns chord features.
|
|
362
|
+
#
|
|
363
|
+
# @return [Hash{Symbol => Symbol}] features hash (quality:, size:, etc.)
|
|
364
|
+
#
|
|
365
|
+
# @example
|
|
366
|
+
# chord.features # => { quality: :major, size: :triad }
|
|
145
367
|
def features
|
|
146
368
|
@chord_definition.features
|
|
147
369
|
end
|
|
148
370
|
|
|
371
|
+
# Creates new chord with modified features.
|
|
372
|
+
#
|
|
373
|
+
# Returns a new chord with the same root but different features.
|
|
374
|
+
# Features can be specified as values (converted to feature hash) or
|
|
375
|
+
# as keyword arguments.
|
|
376
|
+
#
|
|
377
|
+
# @param values [Array<Symbol>] feature values to change
|
|
378
|
+
# @param allow_chromatic [Boolean] allow non-diatonic result
|
|
379
|
+
# @param hash [Hash] feature key-value pairs to change
|
|
380
|
+
# @return [Chord] new chord with modified features
|
|
381
|
+
# @raise [ArgumentError] if no matching chord definition found
|
|
382
|
+
#
|
|
383
|
+
# @example Change size
|
|
384
|
+
# chord.featuring(size: :seventh)
|
|
385
|
+
#
|
|
386
|
+
# @example Change quality
|
|
387
|
+
# chord.featuring(quality: :minor)
|
|
388
|
+
#
|
|
389
|
+
# @example Change multiple features
|
|
390
|
+
# chord.featuring(quality: :dominant, size: :ninth)
|
|
149
391
|
def featuring(*values, allow_chromatic: false, **hash)
|
|
150
392
|
# create a new list of features based on current features but
|
|
151
393
|
# replacing the values for the new ones and adding the new features
|
|
@@ -166,6 +408,19 @@ module Musa
|
|
|
166
408
|
source_notes_map)
|
|
167
409
|
end
|
|
168
410
|
|
|
411
|
+
# Transposes entire chord to a different octave.
|
|
412
|
+
#
|
|
413
|
+
# Moves all chord notes by the specified octave offset, preserving
|
|
414
|
+
# internal voicing structure (moves and duplications).
|
|
415
|
+
#
|
|
416
|
+
# @param octave [Integer] octave offset (positive = up, negative = down)
|
|
417
|
+
# @return [Chord] new chord in different octave
|
|
418
|
+
#
|
|
419
|
+
# @example Move chord down one octave
|
|
420
|
+
# chord.octave(-1)
|
|
421
|
+
#
|
|
422
|
+
# @example Move chord up two octaves
|
|
423
|
+
# chord.octave(2)
|
|
169
424
|
def octave(octave)
|
|
170
425
|
source_notes_map = @source_notes_map.transform_values do |notes|
|
|
171
426
|
notes.collect { |note| note.octave(octave) }.freeze
|
|
@@ -174,26 +429,77 @@ module Musa
|
|
|
174
429
|
Chord.new(@root.octave(octave), @scale, chord_definition, @move, @duplicate, source_notes_map)
|
|
175
430
|
end
|
|
176
431
|
|
|
432
|
+
# Creates new chord with positions moved to different octaves.
|
|
433
|
+
#
|
|
434
|
+
# Relocates specific chord positions to different octaves while keeping
|
|
435
|
+
# other positions unchanged. Multiple positions can be moved at once.
|
|
436
|
+
# Merges with existing moves.
|
|
437
|
+
#
|
|
438
|
+
# @param octaves [Hash{Symbol => Integer}] position to octave offset mapping
|
|
439
|
+
# @return [Chord] new chord with moved positions
|
|
440
|
+
#
|
|
441
|
+
# @example Move root down, seventh up
|
|
442
|
+
# chord.move(root: -1, seventh: 1)
|
|
443
|
+
#
|
|
444
|
+
# @example Drop voicing (move third and seventh down)
|
|
445
|
+
# chord.move(third: -1, seventh: -1)
|
|
177
446
|
def move(**octaves)
|
|
178
447
|
Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
|
|
179
448
|
end
|
|
180
449
|
|
|
450
|
+
# Creates new chord with positions duplicated in other octaves.
|
|
451
|
+
#
|
|
452
|
+
# Adds copies of specific chord positions in different octaves.
|
|
453
|
+
# Original positions remain at their current octave.
|
|
454
|
+
# Merges with existing duplications.
|
|
455
|
+
#
|
|
456
|
+
# @param octaves [Hash{Symbol => Integer, Array<Integer>}] position to octave(s)
|
|
457
|
+
# @return [Chord] new chord with duplicated positions
|
|
458
|
+
#
|
|
459
|
+
# @example Duplicate root two octaves down
|
|
460
|
+
# chord.duplicate(root: -2)
|
|
461
|
+
#
|
|
462
|
+
# @example Duplicate third in multiple octaves
|
|
463
|
+
# chord.duplicate(third: [-1, 1])
|
|
464
|
+
#
|
|
465
|
+
# @example Duplicate multiple positions
|
|
466
|
+
# chord.duplicate(root: -1, fifth: 1)
|
|
181
467
|
def duplicate(**octaves)
|
|
182
468
|
Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
|
|
183
469
|
end
|
|
184
470
|
|
|
471
|
+
# Checks chord equality.
|
|
472
|
+
#
|
|
473
|
+
# Chords are equal if they have the same notes and chord definition.
|
|
474
|
+
#
|
|
475
|
+
# @param other [Chord] chord to compare
|
|
476
|
+
# @return [Boolean] true if chords are equal
|
|
185
477
|
def ==(other)
|
|
186
478
|
self.class == other.class &&
|
|
187
479
|
@sorted_notes == other.notes &&
|
|
188
480
|
@chord_definition == other.chord_definition
|
|
189
481
|
end
|
|
190
482
|
|
|
483
|
+
# Returns string representation.
|
|
484
|
+
#
|
|
485
|
+
# @return [String]
|
|
191
486
|
def inspect
|
|
192
487
|
"<Chord #{@name} root #{@root} notes #{@sorted_notes.collect { |_| "#{_.grade}=#{_.note.grade}|#{_.note.pitch} "} }>"
|
|
193
488
|
end
|
|
194
489
|
|
|
195
490
|
alias to_s inspect
|
|
196
491
|
|
|
492
|
+
# Applies move and duplicate operations to notes map.
|
|
493
|
+
#
|
|
494
|
+
# Computes the final notes map after applying octave moves and duplications
|
|
495
|
+
# to the source notes.
|
|
496
|
+
#
|
|
497
|
+
# @param notes_map [Hash] source notes map
|
|
498
|
+
# @param moved [Hash, nil] octave moves for positions
|
|
499
|
+
# @param duplicated [Hash, nil] octave duplications for positions
|
|
500
|
+
# @return [Hash{Symbol => Array<NoteInScale>}] final notes map
|
|
501
|
+
#
|
|
502
|
+
# @api private
|
|
197
503
|
private def compute_moved_and_duplicated(notes_map, moved, duplicated)
|
|
198
504
|
notes_map = notes_map.transform_values(&:dup)
|
|
199
505
|
|