musa-dsl 0.26.5 → 0.26.6

Sign up to get free protection for your applications and to get access to all the features.
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