musa-dsl 0.26.5 → 0.26.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d9154434fb485380e87cf24b6efa8ebdbaee0a223a94c2ad1988ae808b0a691
4
- data.tar.gz: 6323750820c35c1ef1e8b2220d4f511bba9fafaf504dacadce14aba33ee647f2
3
+ metadata.gz: 33fe7c69b6b470b2563a5bb631744c7d39c7adb83c21fa612d630b212e63d99b
4
+ data.tar.gz: 18bd3611f80fcb47af81c4afc377f827bb18aa60702b491eb8e2d5edb4f70c16
5
5
  SHA512:
6
- metadata.gz: 91e4c3067db836ae8eac4f0242bb4ac1dd6ee48da8959bb4f06b8133e49065260bf316d04dc893c1e2e9cd7f1cbf57b882167868ca3f27072869f15de9962424
7
- data.tar.gz: d0df67a8d02ce6ab12764cfd2478de3322a3006ada484bbbe32cef0e885eac35be2d157fda16ee7453db7b314bb93f81cb46bf83409b218b6936e673c1e4dc2a
6
+ metadata.gz: 35c3e1131b02666ee0f7db1dc596946a9b472738499b50139aea90fa1ecd468ff900378ce862fe6e9b31958dd94fbd8e1209cb23894a800ec8a1c888acb1cdd0
7
+ data.tar.gz: 6b2c952e9388f77ceb296b008ed572420f0621a0d05ebb617cc7254d18b4b5e3cbd9c81dee87b22deecf864d953a99f1cf4353e40b0ce3fb072a67ef93baada1
@@ -1,21 +1,22 @@
1
1
  require_relative '../core-ext/smart-proc-binder'
2
2
  require_relative '../core-ext/with'
3
3
 
4
- # TODO hacer que pueda funcionar en tiempo real? le vas suministrando seeds y le vas diciendo qué opción has elegido (p.ej. para hacer un armonizador en tiempo real)
5
- # TODO esto mismo sería aplicable en otros generadores? variatio/darwin? generative-grammar? markov?
4
+ # TODO: hacer que pueda funcionar en tiempo real? le vas suministrando seeds y le vas diciendo qué opción has elegido (p.ej. para hacer un armonizador en tiempo real)
5
+ # TODO: esto mismo sería aplicable en otros generadores? variatio/darwin? generative-grammar? markov?
6
+ # TODO: optimizar la llamada a .with que internamente genera cada vez un SmartProcBinder; podría generarse sólo una vez por cada &block
6
7
 
7
8
  module Musa
8
- module Backboner
9
+ module Rules
9
10
  using Musa::Extension::Arrayfy
10
11
 
11
- class Backboner
12
+ class Rules
12
13
  include Musa::Extension::With
13
14
 
14
15
  def initialize(&block)
15
16
  @dsl = RulesEvalContext.new(&block)
16
17
  end
17
18
 
18
- def generate_possibilities(object, confirmed_node = nil, node = nil, grow_rules = nil)
19
+ def generate_possibilities(object, confirmed_node = nil, node = nil, grow_rules = nil, **parameters)
19
20
  node ||= Node.new
20
21
  grow_rules ||= @dsl._grow_rules
21
22
 
@@ -26,11 +27,15 @@ module Musa
26
27
  grow_rule = grow_rules.shift
27
28
 
28
29
  if grow_rule
29
- grow_rule.generate_possibilities(object, history).each do |new_object|
30
+ grow_rule.generate_possibilities(object, history, **parameters).each do |new_object|
30
31
  new_node = Node.new new_object, node
31
- new_node.mark_as_ended! if @dsl._ended? new_object
32
+ if @dsl._has_ending? && @dsl._ended?(new_object, history, **parameters) ||
33
+ !@dsl._has_ending? && grow_rules.empty?
32
34
 
33
- rejection = @dsl._cut_rules.find { |cut_rule| cut_rule.rejects?(new_object, history) }
35
+ new_node.mark_as_ended!
36
+ end
37
+
38
+ rejection = @dsl._cut_rules.find { |cut_rule| cut_rule.rejects?(new_object, history, **parameters) }
34
39
  # TODO: include rejection secondary reasons in rejection message
35
40
 
36
41
  new_node.reject! rejection if rejection
@@ -41,14 +46,14 @@ module Musa
41
46
 
42
47
  unless grow_rules.empty?
43
48
  node.children.each do |node|
44
- generate_possibilities node.object, confirmed_node, node, grow_rules unless node.rejected || node.ended?
49
+ generate_possibilities node.object, confirmed_node, node, grow_rules, **parameters unless node.rejected || node.ended?
45
50
  end
46
51
  end
47
52
 
48
53
  node
49
54
  end
50
55
 
51
- def apply(object_or_list, node = nil)
56
+ def apply(object_or_list, node = nil, **parameters)
52
57
  list = object_or_list.arrayfy.clone
53
58
 
54
59
  node ||= Node.new
@@ -56,7 +61,7 @@ module Musa
56
61
  seed = list.shift
57
62
 
58
63
  if seed
59
- result = generate_possibilities seed, node
64
+ result = generate_possibilities seed, node, **parameters
60
65
 
61
66
  fished = result.fish
62
67
 
@@ -64,7 +69,7 @@ module Musa
64
69
 
65
70
  fished.each do |object|
66
71
  subnode = node.add(object).mark_as_ended!
67
- apply list, subnode
72
+ apply list, subnode, **parameters
68
73
  end
69
74
  end
70
75
 
@@ -77,11 +82,12 @@ module Musa
77
82
  attr_reader :_grow_rules, :_ended_when, :_cut_rules
78
83
 
79
84
  def initialize(&block)
85
+ @_grow_rules = []
86
+ @_cut_rules = []
80
87
  with &block
81
88
  end
82
89
 
83
90
  def grow(name, &block)
84
- @_grow_rules ||= []
85
91
  @_grow_rules << GrowRule.new(name, &block)
86
92
  self
87
93
  end
@@ -92,13 +98,20 @@ module Musa
92
98
  end
93
99
 
94
100
  def cut(reason, &block)
95
- @_cut_rules ||= []
96
101
  @_cut_rules << CutRule.new(reason, &block)
97
102
  self
98
103
  end
99
104
 
100
- def _ended?(object)
101
- instance_exec object, &@_ended_when
105
+ def _has_ending?
106
+ !@_ended_when.nil?
107
+ end
108
+
109
+ def _ended?(object, history, **parameters)
110
+ if @_ended_when
111
+ with object, history, **parameters, &@_ended_when
112
+ else
113
+ false
114
+ end
102
115
  end
103
116
 
104
117
  class GrowRule
@@ -109,10 +122,10 @@ module Musa
109
122
  @block = block
110
123
  end
111
124
 
112
- def generate_possibilities(object, history)
125
+ def generate_possibilities(object, history, **parameters)
113
126
  # TODO: optimize context using only one instance for all genereate_possibilities calls
114
127
  context = GrowRuleEvalContext.new
115
- context.with object, history, &@block
128
+ context.with object, history, **parameters, &@block
116
129
 
117
130
  context._branches
118
131
  end
@@ -145,10 +158,10 @@ module Musa
145
158
  @block = block
146
159
  end
147
160
 
148
- def rejects?(object, history)
161
+ def rejects?(object, history, **parameters)
149
162
  # TODO: optimize context using only one instance for all rejects? checks
150
163
  context = CutRuleEvalContext.new
151
- context.with object, history, &@block
164
+ context.with object, history, **parameters, &@block
152
165
 
153
166
  reasons = context._secondary_reasons.collect { |_| ("#{@reason} (#{_})" if _) || @reason }
154
167
 
@@ -203,7 +216,7 @@ module Musa
203
216
  @children.each(&:update_rejection_by_children!)
204
217
 
205
218
  if !@children.empty? && !@children.find { |n| !n.rejected }
206
- reject! "Node rejected because all children are rejected"
219
+ reject! 'Node rejected because all children are rejected'
207
220
  end
208
221
 
209
222
  @ended = true
@@ -1,5 +1,5 @@
1
1
  require_relative 'generative/variatio'
2
2
  require_relative 'generative/darwin'
3
- require_relative 'generative/backboner'
3
+ require_relative 'generative/rules'
4
4
  require_relative 'generative/markov'
5
5
  require_relative 'generative/generative-grammar'
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Musa
2
4
  module Chords
3
5
  class ChordDefinition
@@ -6,7 +8,7 @@ module Musa
6
8
  end
7
9
 
8
10
  def self.register(name, offsets:, **features)
9
- definition = ChordDefinition.new(name, offsets: offsets, **features).freeze
11
+ definition = ChordDefinition.new(name, offsets: offsets, **features)
10
12
 
11
13
  @definitions ||= {}
12
14
  @definitions[definition.name] = definition
@@ -14,6 +16,9 @@ module Musa
14
16
  @features_by_value ||= {}
15
17
  definition.features.each { |k, v| @features_by_value[v] = k }
16
18
 
19
+ @feature_keys ||= Set[]
20
+ features.keys.each { |feature_name| @feature_keys << feature_name }
21
+
17
22
  self
18
23
  end
19
24
 
@@ -44,11 +49,16 @@ module Musa
44
49
  @features_by_value.keys
45
50
  end
46
51
 
52
+ def self.feature_keys
53
+ @feature_keys
54
+ end
55
+
47
56
  def initialize(name, offsets:, **features)
48
- @name = name
49
- @features = features.clone.freeze
50
- @pitch_offsets = offsets.clone.freeze
51
- @pitch_names = offsets.collect { |k, v| [v, k] }.to_h
57
+ @name = name.freeze
58
+ @features = features.transform_values(&:dup).transform_values(&:freeze).freeze
59
+ @pitch_offsets = offsets.dup.freeze
60
+ @pitch_names = offsets.collect { |k, v| [v, k] }.to_h.freeze
61
+ freeze
52
62
  end
53
63
 
54
64
  attr_reader :name, :features, :pitch_offsets, :pitch_names
@@ -57,6 +67,10 @@ module Musa
57
67
  @pitch_offsets.values.collect { |offset| root_pitch + offset }
58
68
  end
59
69
 
70
+ def in_scale?(scale, chord_root_pitch:)
71
+ !pitches(chord_root_pitch).find { |chord_pitch| scale.note_of_pitch(chord_pitch).nil? }
72
+ end
73
+
60
74
  def named_pitches(elements_or_pitches, &block)
61
75
  pitches = elements_or_pitches.collect do |element_or_pitch|
62
76
  [if block_given?
@@ -93,7 +107,7 @@ module Musa
93
107
 
94
108
  alias to_s inspect
95
109
 
96
- protected
110
+ private
97
111
 
98
112
  def octave_reduce(pitches)
99
113
  pitches.collect { |p| p % 12 }
@@ -1,15 +1,23 @@
1
1
  require_relative 'chord-definition'
2
2
 
3
+ # TODO trasladar los acordes de https://en.wikipedia.org/wiki/Chord_notation
4
+
3
5
  Musa::Chords::ChordDefinition.register :maj, quality: :major, size: :triad, offsets: { root: 0, third: 4, fifth: 7 }
4
6
  Musa::Chords::ChordDefinition.register :min, quality: :minor, size: :triad, offsets: { root: 0, third: 3, fifth: 7 }
7
+ Musa::Chords::ChordDefinition.register :dim, quality: :diminished, size: :triad, offsets: { root: 0, third: 3, fifth: 3 }
8
+ Musa::Chords::ChordDefinition.register :aug, quality: :augmented, size: :triad, offsets: { root: 0, third: 4, fifth: 8 }
5
9
 
6
10
  Musa::Chords::ChordDefinition.register :maj7, quality: :major, size: :seventh, offsets: { root: 0, third: 4, fifth: 7, seventh: 11 }
7
- Musa::Chords::ChordDefinition.register :maj7, quality: :major, size: :seventh, dominant: :dominant , offsets: { root: 0, third: 4, fifth: 7, seventh: 10 }
8
-
9
11
  Musa::Chords::ChordDefinition.register :min7, quality: :minor, size: :seventh, offsets: { root: 0, third: 3, fifth: 7, seventh: 11 }
10
12
 
13
+ Musa::Chords::ChordDefinition.register :dom7, quality: :dominant, size: :seventh, offsets: { root: 0, third: 4, fifth: 7, seventh: 10 }
14
+
11
15
  Musa::Chords::ChordDefinition.register :maj9, quality: :major, size: :ninth, offsets: { root: 0, third: 4, fifth: 7, seventh: 11, ninth: 14 }
12
16
  Musa::Chords::ChordDefinition.register :min9, quality: :minor, size: :ninth, offsets: { root: 0, third: 3, fifth: 7, seventh: 11, ninth: 14 }
17
+ Musa::Chords::ChordDefinition.register :dom9, quality: :dominant, size: :ninth, offsets: { root: 0, third: 4, fifth: 7, seventh: 10, ninth: 14 }
13
18
 
14
19
  Musa::Chords::ChordDefinition.register :maj11, quality: :major, size: :eleventh, offsets: { root: 0, third: 4, fifth: 7, seventh: 11, ninth: 14, eleventh: 17 }
15
20
  Musa::Chords::ChordDefinition.register :min11, quality: :minor, size: :eleventh, offsets: { root: 0, third: 3, fifth: 7, seventh: 11, ninth: 14, eleventh: 17 }
21
+
22
+ Musa::Chords::ChordDefinition.register :maj13, quality: :major, size: :eleventh, offsets: { root: 0, third: 4, fifth: 7, seventh: 11, ninth: 14, eleventh: 17 }
23
+ Musa::Chords::ChordDefinition.register :min13, quality: :minor, size: :eleventh, offsets: { root: 0, third: 3, fifth: 7, seventh: 11, ninth: 14, eleventh: 17 }
@@ -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,6 +281,7 @@ module Musa
277
281
  end
278
282
  end
279
283
 
284
+ freeze
280
285
  end
281
286
 
282
287
  def_delegators :@kind, :a_tuning
@@ -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)
@@ -113,9 +113,11 @@ module Musa
113
113
  @client_threads.clear
114
114
  end
115
115
 
116
- def puts(message)
116
+ def puts(*messages)
117
117
  if @connection
118
- send output: @connection, content: message&.to_s
118
+ messages.each do |message|
119
+ send output: @connection, content: message&.to_s
120
+ end
119
121
  else
120
122
  @logger.warn('REPL') { "trying to print a message in Atom client but the client is not connected. Ignoring message \'#{message} \'." }
121
123
  end
@@ -45,7 +45,12 @@ module Musa::Sequencer
45
45
  case operation[:current_operation]
46
46
  when :none
47
47
  when :block
48
- __play_eval.block_procedure_binder.call operation[:current_parameter], control: control
48
+ # duplicating parameters as direct object value (operation[:current_parameter])
49
+ # and key_passed parameters (**operation[:current_parameter])
50
+ #
51
+ __play_eval.block_procedure_binder.call operation[:current_parameter],
52
+ **operation[:current_parameter],
53
+ control: control
49
54
 
50
55
  when :event
51
56
  control._launch operation[:current_event],
@@ -41,7 +41,7 @@ module Musa
41
41
  do_error_log: do_error_log,
42
42
  log_position_format: log_position_format
43
43
 
44
- # dsl_context_class ||= DSLContext
44
+ dsl_context_class ||= DSLContext
45
45
 
46
46
  @dsl = dsl_context_class.new @sequencer, keep_block_context: keep_block_context
47
47
 
@@ -21,10 +21,12 @@ module Musa
21
21
  @logger.debug! if do_log
22
22
  end
23
23
 
24
+ @time_table = []
24
25
  @midi_parser = MIDIParser.new
25
26
  end
26
27
 
27
28
  attr_reader :input
29
+ attr_reader :time_table
28
30
 
29
31
  def input=(input_midi_port)
30
32
  @input = input_midi_port
@@ -121,6 +123,7 @@ module Musa
121
123
  case m.name
122
124
  when 'Start'
123
125
  process_start
126
+ @time_table.clear
124
127
 
125
128
  when 'Stop'
126
129
  @logger.debug('InputMidiClock') { 'processing Stop...' }
@@ -135,7 +138,15 @@ module Musa
135
138
  @logger.debug('InputMidiClock') { 'processing Continue... done' }
136
139
 
137
140
  when 'Clock'
138
- yield if block_given? && @started
141
+ if block_given? && @started
142
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
143
+ yield
144
+ finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
145
+
146
+ duration = finish_time - start_time
147
+ @time_table[duration] ||= 0
148
+ @time_table[duration] += 1
149
+ end
139
150
 
140
151
  when 'Song Position Pointer'
141
152
  new_position_in_midi_beats = m.data[0] & 0x7F | ((m.data[1] & 0x7F) << 7)
data/lib/musa-dsl.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Musa
2
- VERSION = '0.26.5'.freeze
2
+ VERSION = '0.26.6.wip'.freeze
3
3
  end
4
4
 
5
5
  require_relative 'musa-dsl/core-ext'
@@ -54,7 +54,7 @@ module Musa::All
54
54
 
55
55
  include Musa::Darwin
56
56
  include Musa::Markov
57
- include Musa::Backboner
57
+ include Musa::Rules
58
58
  include Musa::Variatio
59
59
 
60
60
  include Musa::MIDIRecorder
data/musa-dsl.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'musa-dsl'
3
- s.version = '0.26.5'
4
- s.date = '2022-02-25'
3
+ s.version = '0.26.6'
4
+ s.date = '2022-02-26'
5
5
  s.summary = 'A simple Ruby DSL for making complex music'
6
6
  s.description = 'Musa-DSL: A Ruby framework and DSL for algorithmic sound and musical thinking and composition'
7
7
  s.authors = ['Javier Sánchez Yeste']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: musa-dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.5
4
+ version: 0.26.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Sánchez Yeste
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-25 00:00:00.000000000 Z
11
+ date: 2022-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -161,10 +161,10 @@ files:
161
161
  - lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb
162
162
  - lib/musa-dsl/datasets/v.rb
163
163
  - lib/musa-dsl/generative.rb
164
- - lib/musa-dsl/generative/backboner.rb
165
164
  - lib/musa-dsl/generative/darwin.rb
166
165
  - lib/musa-dsl/generative/generative-grammar.rb
167
166
  - lib/musa-dsl/generative/markov.rb
167
+ - lib/musa-dsl/generative/rules.rb
168
168
  - lib/musa-dsl/generative/variatio.rb
169
169
  - lib/musa-dsl/logger.rb
170
170
  - lib/musa-dsl/logger/logger.rb