musa-dsl 0.14.32 → 0.21.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/Gemfile +0 -1
- data/README.md +5 -1
- data/lib/musa-dsl.rb +54 -11
- data/lib/musa-dsl/core-ext.rb +7 -13
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +15 -23
- data/lib/musa-dsl/core-ext/arrayfy.rb +30 -12
- data/lib/musa-dsl/core-ext/attribute-builder.rb +194 -0
- data/lib/musa-dsl/core-ext/deep-copy.rb +185 -0
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +44 -40
- data/lib/musa-dsl/core-ext/inspect-nice.rb +40 -22
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +108 -0
- data/lib/musa-dsl/core-ext/with.rb +26 -0
- data/lib/musa-dsl/datasets.rb +8 -3
- data/lib/musa-dsl/datasets/dataset.rb +3 -0
- data/lib/musa-dsl/datasets/delta-d.rb +12 -0
- data/lib/musa-dsl/datasets/e.rb +61 -0
- data/lib/musa-dsl/datasets/gdv.rb +51 -411
- data/lib/musa-dsl/datasets/gdvd.rb +179 -0
- data/lib/musa-dsl/datasets/helper.rb +41 -0
- data/lib/musa-dsl/datasets/p.rb +68 -0
- data/lib/musa-dsl/datasets/packed-v.rb +19 -0
- data/lib/musa-dsl/datasets/pdv.rb +22 -15
- data/lib/musa-dsl/datasets/ps.rb +113 -0
- data/lib/musa-dsl/datasets/score.rb +210 -0
- data/lib/musa-dsl/datasets/score/queriable.rb +48 -0
- data/lib/musa-dsl/datasets/score/render.rb +31 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +160 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +51 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +153 -0
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +158 -0
- data/lib/musa-dsl/datasets/v.rb +23 -0
- data/lib/musa-dsl/generative.rb +5 -5
- data/lib/musa-dsl/generative/backboner.rb +274 -0
- data/lib/musa-dsl/generative/darwin.rb +102 -96
- data/lib/musa-dsl/generative/generative-grammar.rb +182 -187
- data/lib/musa-dsl/generative/markov.rb +56 -53
- data/lib/musa-dsl/generative/variatio.rb +234 -222
- data/lib/musa-dsl/logger.rb +1 -0
- data/lib/musa-dsl/logger/logger.rb +32 -0
- data/lib/musa-dsl/matrix.rb +1 -0
- data/lib/musa-dsl/matrix/matrix.rb +210 -0
- data/lib/musa-dsl/midi.rb +2 -2
- data/lib/musa-dsl/midi/midi-recorder.rb +54 -52
- data/lib/musa-dsl/midi/midi-voices.rb +187 -182
- data/lib/musa-dsl/music.rb +5 -5
- data/lib/musa-dsl/music/chord-definition.rb +54 -50
- data/lib/musa-dsl/music/chord-definitions.rb +13 -9
- data/lib/musa-dsl/music/chords.rb +236 -238
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +187 -183
- data/lib/musa-dsl/music/scales.rb +331 -332
- data/lib/musa-dsl/musicxml.rb +1 -0
- data/lib/musa-dsl/musicxml/builder/attributes.rb +155 -0
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +45 -0
- data/lib/musa-dsl/musicxml/builder/direction.rb +322 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +90 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +137 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +152 -0
- data/lib/musa-dsl/musicxml/builder/note.rb +577 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +44 -0
- data/lib/musa-dsl/musicxml/builder/part.rb +67 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +126 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +117 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +120 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +43 -0
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +112 -0
- data/lib/musa-dsl/neumalang.rb +1 -1
- data/lib/musa-dsl/neumalang/datatypes.citrus +79 -0
- data/lib/musa-dsl/neumalang/neuma.citrus +165 -0
- data/lib/musa-dsl/neumalang/neumalang.citrus +32 -242
- data/lib/musa-dsl/neumalang/neumalang.rb +373 -142
- data/lib/musa-dsl/neumalang/process.citrus +21 -0
- data/lib/musa-dsl/neumalang/terminals.citrus +67 -0
- data/lib/musa-dsl/neumalang/vectors.citrus +23 -0
- data/lib/musa-dsl/neumas.rb +5 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +34 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +63 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +57 -0
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +15 -0
- data/lib/musa-dsl/neumas/neumas.rb +37 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +34 -0
- data/lib/musa-dsl/repl.rb +1 -1
- data/lib/musa-dsl/repl/repl.rb +122 -110
- data/lib/musa-dsl/sequencer.rb +1 -1
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +163 -136
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +301 -286
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +550 -321
- data/lib/musa-dsl/sequencer/base-sequencer-public.rb +198 -176
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +75 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +75 -0
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +105 -85
- data/lib/musa-dsl/sequencer/timeslots.rb +34 -0
- data/lib/musa-dsl/series.rb +1 -1
- data/lib/musa-dsl/{core-ext → series}/array-to-serie.rb +1 -1
- data/lib/musa-dsl/series/base-series.rb +171 -168
- data/lib/musa-dsl/series/hash-serie-splitter.rb +134 -132
- data/lib/musa-dsl/series/holder-serie.rb +1 -1
- data/lib/musa-dsl/series/main-serie-constructors.rb +6 -1
- data/lib/musa-dsl/series/main-serie-operations.rb +807 -797
- data/lib/musa-dsl/series/proxy-serie.rb +6 -6
- data/lib/musa-dsl/series/queue-serie.rb +5 -5
- data/lib/musa-dsl/series/series.rb +2 -0
- data/lib/musa-dsl/transcription.rb +4 -0
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +227 -0
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +36 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +17 -0
- data/lib/musa-dsl/transcription/transcription.rb +55 -0
- data/lib/musa-dsl/transport.rb +6 -6
- data/lib/musa-dsl/transport/clock.rb +26 -26
- data/lib/musa-dsl/transport/dummy-clock.rb +32 -30
- data/lib/musa-dsl/transport/external-tick-clock.rb +21 -20
- data/lib/musa-dsl/transport/input-midi-clock.rb +89 -80
- data/lib/musa-dsl/transport/timer-clock.rb +72 -71
- data/lib/musa-dsl/transport/timer.rb +28 -26
- data/lib/musa-dsl/transport/transport.rb +111 -93
- data/musa-dsl.gemspec +3 -3
- metadata +73 -24
- data/lib/musa-dsl/core-ext/array-apply-get.rb +0 -18
- data/lib/musa-dsl/core-ext/array-to-neumas.rb +0 -28
- data/lib/musa-dsl/core-ext/as-context-run.rb +0 -44
- data/lib/musa-dsl/core-ext/duplicate.rb +0 -134
- data/lib/musa-dsl/core-ext/key-parameters-procedure-binder.rb +0 -85
- data/lib/musa-dsl/core-ext/proc-nice.rb +0 -13
- data/lib/musa-dsl/core-ext/send-nice.rb +0 -21
- data/lib/musa-dsl/core-ext/string-to-neumas.rb +0 -27
- data/lib/musa-dsl/datasets/gdv-decorators.rb +0 -221
- data/lib/musa-dsl/generative/rules.rb +0 -282
- data/lib/musa-dsl/neuma.rb +0 -1
- data/lib/musa-dsl/neuma/neuma.rb +0 -181
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'logger/logger'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Musa; module Logger
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(sequencer: nil, position_format: nil)
|
6
|
+
super STDERR, level: WARN
|
7
|
+
|
8
|
+
@sequencer = sequencer
|
9
|
+
@position_format = position_format || 3.3
|
10
|
+
|
11
|
+
|
12
|
+
self.formatter = proc do |severity, time, progname, msg|
|
13
|
+
level = "[#{severity}] " unless severity == 'DEBUG'
|
14
|
+
|
15
|
+
if msg
|
16
|
+
position = if @sequencer
|
17
|
+
integer_digits = @position_format.to_i
|
18
|
+
decimal_digits = ((@position_format - integer_digits) * 10).round
|
19
|
+
|
20
|
+
"%#{integer_digits + decimal_digits + 1}s: " % ("%.#{decimal_digits}f" % sequencer.position.to_f)
|
21
|
+
end
|
22
|
+
|
23
|
+
progname = "[#{progname}] " if progname
|
24
|
+
|
25
|
+
"#{position}#{level}#{progname}#{msg}\n"
|
26
|
+
else
|
27
|
+
"\n"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end; end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'matrix/matrix'
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
require_relative '../datasets/p'
|
4
|
+
require_relative '../sequencer'
|
5
|
+
|
6
|
+
module Musa
|
7
|
+
module Matrix
|
8
|
+
## TODO should exist this module?
|
9
|
+
end
|
10
|
+
|
11
|
+
module Extension
|
12
|
+
module Matrix
|
13
|
+
refine Array do
|
14
|
+
def indexes_of_values
|
15
|
+
indexes = {}
|
16
|
+
|
17
|
+
size.times do |i|
|
18
|
+
indexes[self[i]] ||= []
|
19
|
+
indexes[self[i]] << i
|
20
|
+
end
|
21
|
+
|
22
|
+
indexes
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_p(time_dimension, keep_time: nil)
|
26
|
+
condensed_matrices.collect { |m| m.to_p(time_dimension, keep_time: keep_time) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def condensed_matrices
|
30
|
+
condensed = []
|
31
|
+
|
32
|
+
each do |other|
|
33
|
+
if condensed.empty?
|
34
|
+
condensed << other
|
35
|
+
else
|
36
|
+
done = false
|
37
|
+
condensed.each do |matrix|
|
38
|
+
if matrix._rows.first == other._rows.first
|
39
|
+
other._rows.shift
|
40
|
+
matrix._rows.prepend other._rows.shift until other._rows.empty?
|
41
|
+
done = true
|
42
|
+
|
43
|
+
elsif matrix._rows.first == other._rows.last
|
44
|
+
other._rows.pop
|
45
|
+
matrix._rows.prepend other._rows.pop until other._rows.empty?
|
46
|
+
done = true
|
47
|
+
|
48
|
+
elsif matrix._rows.last == other._rows.first
|
49
|
+
other._rows.shift
|
50
|
+
matrix._rows.append other._rows.shift until other._rows.empty?
|
51
|
+
done = true
|
52
|
+
|
53
|
+
elsif matrix._rows.last == other._rows.last
|
54
|
+
other._rows.pop
|
55
|
+
matrix._rows.append other._rows.pop until other._rows.empty?
|
56
|
+
done = true
|
57
|
+
end
|
58
|
+
|
59
|
+
break if done
|
60
|
+
end
|
61
|
+
condensed << other unless done
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
condensed
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
refine ::Matrix do
|
70
|
+
include Musa::Datasets
|
71
|
+
|
72
|
+
def to_p(time_dimension, keep_time: nil)
|
73
|
+
|
74
|
+
decompose(self.to_a, time_dimension).collect do |points|
|
75
|
+
line = []
|
76
|
+
|
77
|
+
start_point = points[0]
|
78
|
+
start_time = start_point[time_dimension]
|
79
|
+
|
80
|
+
line << start_point.tap { |_| _.delete_at(time_dimension) unless keep_time; _ }.extend(Datasets::V)
|
81
|
+
|
82
|
+
(1..points.size-1).each do |i|
|
83
|
+
end_point = points[i]
|
84
|
+
|
85
|
+
end_time = end_point[time_dimension]
|
86
|
+
|
87
|
+
line << end_time - start_time
|
88
|
+
line << end_point.tap { |_| _.delete_at(time_dimension) unless keep_time; _ }.extend(Datasets::V)
|
89
|
+
|
90
|
+
start_time = end_time
|
91
|
+
end
|
92
|
+
|
93
|
+
line.extend(Datasets::P)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_score(time_dimension,
|
98
|
+
mapper: nil,
|
99
|
+
score: nil,
|
100
|
+
position: nil,
|
101
|
+
sequencer: nil,
|
102
|
+
beats_per_bar: nil, ticks_per_beat: nil,
|
103
|
+
right_open: nil,
|
104
|
+
do_log: nil,
|
105
|
+
&block)
|
106
|
+
|
107
|
+
raise ArgumentError,
|
108
|
+
"'beats_per_bar' and 'ticks_per_beat' parameters should be both nil or both have values" \
|
109
|
+
unless beats_per_bar && ticks_per_beat ||
|
110
|
+
beats_per_bar.nil? && ticks_per_beat.nil?
|
111
|
+
|
112
|
+
raise ArgumentError,
|
113
|
+
"'sequencer' parameter should not be used when 'beats_per_bar' and 'ticks_per_beat' parameters are used" \
|
114
|
+
if sequencer && beats_per_bar
|
115
|
+
|
116
|
+
raise ArgumentError,
|
117
|
+
"'time_dimension' parameter should be an index number if no 'mapping' is provided" \
|
118
|
+
unless time_dimension.is_a?(Symbol) && mapper&.include?(time_dimension) ||
|
119
|
+
time_dimension.is_a?(Integer) && (0..self.column_size-1).include(time_dimension)
|
120
|
+
|
121
|
+
time_dimension = mapper&.find_index(time_dimension) if time_dimension.is_a?(Symbol)
|
122
|
+
|
123
|
+
mapper = mapper.clone.tap { |_| _.delete_at(time_dimension) } if mapper
|
124
|
+
|
125
|
+
run_sequencer = sequencer.nil?
|
126
|
+
|
127
|
+
score ||= Musa::Datasets::Score.new
|
128
|
+
|
129
|
+
sequencer ||= Sequencer::Sequencer.new(beats_per_bar, ticks_per_beat, do_log: do_log)
|
130
|
+
|
131
|
+
right_open = right_open.clone.tap { |_| _.delete_at(time_dimension) } if right_open&.is_a?(Array)
|
132
|
+
|
133
|
+
sequencer.at(position || 1r) do |_|
|
134
|
+
to_p(time_dimension).each do |p|
|
135
|
+
p.to_score(score: score,
|
136
|
+
mapper: mapper,
|
137
|
+
sequencer: _,
|
138
|
+
position: _.position,
|
139
|
+
right_open: right_open,
|
140
|
+
do_log: do_log,
|
141
|
+
&block)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
if run_sequencer
|
146
|
+
sequencer.run
|
147
|
+
score
|
148
|
+
else
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def _rows
|
154
|
+
@rows
|
155
|
+
end
|
156
|
+
|
157
|
+
private def decompose(array, time_dimension)
|
158
|
+
x_dim = array.collect { |v| v[time_dimension] }
|
159
|
+
x_dim_values_indexes = x_dim.indexes_of_values
|
160
|
+
|
161
|
+
used_indexes = Set[]
|
162
|
+
|
163
|
+
directional_segments = []
|
164
|
+
|
165
|
+
x_dim_values_indexes.keys.sort.each do |value|
|
166
|
+
x_dim_values_indexes[value].each do |index|
|
167
|
+
# hacia un lado
|
168
|
+
|
169
|
+
unless used_indexes.include?(index)
|
170
|
+
i = index
|
171
|
+
xx = array[i][time_dimension]
|
172
|
+
|
173
|
+
a = []
|
174
|
+
|
175
|
+
while i >= 0 && array[i][time_dimension] >= xx
|
176
|
+
used_indexes << i
|
177
|
+
a << array[i]
|
178
|
+
|
179
|
+
xx = array[i][time_dimension]
|
180
|
+
i -= 1
|
181
|
+
end
|
182
|
+
|
183
|
+
directional_segments << a if a.size > 1
|
184
|
+
|
185
|
+
# y hacia el otro
|
186
|
+
|
187
|
+
i = index
|
188
|
+
xx = array[i][time_dimension]
|
189
|
+
|
190
|
+
b = []
|
191
|
+
|
192
|
+
while i < array.size && array[i][time_dimension] >= xx
|
193
|
+
used_indexes << i
|
194
|
+
b << array[i]
|
195
|
+
|
196
|
+
xx = array[i][time_dimension]
|
197
|
+
i += 1
|
198
|
+
end
|
199
|
+
|
200
|
+
directional_segments << b if b.size > 1
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
return directional_segments
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
data/lib/musa-dsl/midi.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative 'midi/midi-voices'
|
2
|
+
require_relative 'midi/midi-recorder'
|
@@ -1,83 +1,85 @@
|
|
1
1
|
require 'nibbler'
|
2
2
|
|
3
3
|
module Musa
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module MIDIRecorder
|
5
|
+
class MIDIRecorder
|
6
|
+
def initialize(sequencer)
|
7
|
+
@sequencer = sequencer
|
8
|
+
@nibbler = Nibbler.new
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
clear
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def clear
|
14
|
+
@messages = []
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def record(midi_bytes)
|
18
|
+
m = @nibbler.parse midi_bytes
|
19
|
+
m = [m] unless m.is_a? Array
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
m.each do |mm|
|
22
|
+
@messages << Message.new(@sequencer.position, mm)
|
23
|
+
end
|
22
24
|
end
|
23
|
-
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
def raw
|
27
|
+
@messages
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
def transcription
|
31
|
+
note_on = {}
|
32
|
+
last_note = {}
|
32
33
|
|
33
|
-
|
34
|
+
notes = []
|
34
35
|
|
35
|
-
|
36
|
-
|
36
|
+
@messages.each do |m|
|
37
|
+
mm = m.message
|
37
38
|
|
38
|
-
|
39
|
+
if mm.is_a?(MIDIMessage::NoteOn)
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
if last_note[mm.channel]
|
42
|
+
notes << { position: last_note[mm.channel], channel: mm.channel, pitch: :silence, duration: m.position - last_note[mm.channel] }
|
43
|
+
last_note.delete mm.channel
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
+
note = { position: m.position, channel: mm.channel, pitch: mm.note, velocity: mm.velocity }
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
note_on[mm.channel] ||= {}
|
49
|
+
note_on[mm.channel][mm.note] = note
|
49
50
|
|
50
|
-
|
51
|
+
notes << note
|
51
52
|
|
52
|
-
|
53
|
+
elsif mm.is_a?(MIDIMessage::NoteOff)
|
53
54
|
|
54
|
-
|
55
|
+
note_on[mm.channel] ||= {}
|
55
56
|
|
56
|
-
|
57
|
+
note = note_on[mm.channel][mm.note]
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
if note
|
60
|
+
note_on[mm.channel].delete mm.note
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
note[:duration] = m.position - note[:position]
|
63
|
+
note[:velocity_off] = mm.velocity
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
+
last_note[mm.channel] = m.position
|
67
|
+
end
|
66
68
|
end
|
67
|
-
end
|
68
69
|
|
69
|
-
|
70
|
-
|
70
|
+
notes
|
71
|
+
end
|
71
72
|
|
72
|
-
|
73
|
-
|
73
|
+
class Message
|
74
|
+
attr_accessor :position, :message
|
74
75
|
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
def initialize(position, message)
|
77
|
+
@position = position
|
78
|
+
@message = message
|
79
|
+
end
|
78
80
|
end
|
79
|
-
end
|
80
81
|
|
81
|
-
|
82
|
+
private_constant :Message
|
83
|
+
end
|
82
84
|
end
|
83
85
|
end
|
@@ -1,274 +1,279 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'midi-message'
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
require_relative '../core-ext/arrayfy'
|
5
|
+
require_relative '../core-ext/array-explode-ranges'
|
6
|
+
|
7
|
+
using Musa::Extension::Arrayfy
|
8
|
+
using Musa::Extension::ExplodeRanges
|
6
9
|
|
7
10
|
module Musa
|
8
|
-
|
9
|
-
|
11
|
+
module MIDIVoices
|
12
|
+
class MIDIVoices
|
13
|
+
attr_accessor :log
|
10
14
|
|
11
|
-
|
12
|
-
|
15
|
+
def initialize(sequencer:, output:, channels:, do_log: nil)
|
16
|
+
do_log ||= false
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
@sequencer = sequencer
|
19
|
+
@output = output
|
20
|
+
@channels = channels.arrayfy.explode_ranges
|
21
|
+
@do_log = do_log
|
18
22
|
|
19
|
-
|
20
|
-
|
23
|
+
reset
|
24
|
+
end
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
def reset
|
27
|
+
@voices = @channels.collect { |channel| MIDIVoice.new sequencer: @sequencer, output: @output, channel: channel, log: @do_log }.freeze
|
28
|
+
end
|
25
29
|
|
26
|
-
|
30
|
+
attr_reader :voices
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def fast_forward=(enabled)
|
33
|
+
@voices.each { |voice| voice.fast_forward = enabled }
|
34
|
+
end
|
31
35
|
|
32
|
-
|
33
|
-
|
36
|
+
def panic(reset: nil)
|
37
|
+
reset ||= false
|
34
38
|
|
35
|
-
|
39
|
+
@voices.each(&:all_notes_off)
|
36
40
|
|
37
|
-
|
41
|
+
@output.puts MIDIMessage::SystemRealtime.new(0xff) if reset
|
42
|
+
end
|
38
43
|
end
|
39
|
-
end
|
40
44
|
|
41
|
-
|
45
|
+
private
|
42
46
|
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
class MIDIVoice
|
48
|
+
attr_accessor :name, :do_log
|
49
|
+
attr_reader :sequencer, :output, :channel, :active_pitches, :tick_duration
|
46
50
|
|
47
|
-
|
48
|
-
|
51
|
+
def initialize(sequencer:, output:, channel:, name: nil, log: nil)
|
52
|
+
log ||= false
|
49
53
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
@sequencer = sequencer
|
55
|
+
@output = output
|
56
|
+
@channel = channel
|
57
|
+
@name = name
|
58
|
+
@do_log = log
|
55
59
|
|
56
|
-
|
60
|
+
@tick_duration = Rational(1, @sequencer.ticks_per_bar)
|
57
61
|
|
58
|
-
|
62
|
+
@controllers_control = ControllersControl.new(@output, @channel)
|
59
63
|
|
60
|
-
|
61
|
-
|
64
|
+
@active_pitches = []
|
65
|
+
fill_active_pitches @active_pitches
|
62
66
|
|
63
|
-
|
67
|
+
log 'Warning: voice without output' unless @output
|
64
68
|
|
65
|
-
|
66
|
-
|
69
|
+
self
|
70
|
+
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
+
def fast_forward=(enabled)
|
73
|
+
if @fast_forward && !enabled
|
74
|
+
(0..127).each do |pitch|
|
75
|
+
@output.puts MIDIMessage::NoteOn.new(@channel, pitch, @active_pitches[pitch][:velocity]) unless @active_pitches[pitch][:note_controls].empty?
|
76
|
+
end
|
72
77
|
end
|
73
|
-
end
|
74
78
|
|
75
|
-
|
76
|
-
|
79
|
+
@fast_forward = enabled
|
80
|
+
end
|
77
81
|
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
def fast_forward?
|
83
|
+
@fast_forward
|
84
|
+
end
|
81
85
|
|
82
|
-
|
83
|
-
|
86
|
+
def note(pitchvalue = nil, pitch: nil, velocity: nil, duration: nil, duration_offset: nil, note_duration: nil, velocity_off: nil)
|
87
|
+
pitch ||= pitchvalue
|
84
88
|
|
85
|
-
|
86
|
-
|
89
|
+
if pitch
|
90
|
+
velocity ||= 63
|
87
91
|
|
88
|
-
|
89
|
-
|
92
|
+
duration_offset ||= -@tick_duration
|
93
|
+
note_duration ||= [0, duration + duration_offset].max
|
90
94
|
|
91
|
-
|
95
|
+
velocity_off ||= 63
|
92
96
|
|
93
|
-
|
97
|
+
NoteControl.new(self, pitch: pitch, velocity: velocity, duration: note_duration, velocity_off: velocity_off).note_on
|
98
|
+
end
|
94
99
|
end
|
95
|
-
end
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
|
101
|
+
def controller
|
102
|
+
@controllers_control
|
103
|
+
end
|
100
104
|
|
101
|
-
|
102
|
-
|
103
|
-
|
105
|
+
def sustain_pedal=(value)
|
106
|
+
@controllers_control[:sustain_pedal] = value
|
107
|
+
end
|
104
108
|
|
105
|
-
|
106
|
-
|
107
|
-
|
109
|
+
def sustain_pedal
|
110
|
+
@controllers_control[:sustain_pedal]
|
111
|
+
end
|
108
112
|
|
109
|
-
|
110
|
-
|
111
|
-
|
113
|
+
def all_notes_off
|
114
|
+
@active_pitches.clear
|
115
|
+
fill_active_pitches @active_pitches
|
112
116
|
|
113
|
-
|
114
|
-
|
117
|
+
@output.puts MIDIMessage::ChannelMessage.new(0xb, @channel, 0x7b, 0)
|
118
|
+
end
|
115
119
|
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
def log(msg)
|
121
|
+
@sequencer.log "voice #{name || @channel}: #{msg}" if @do_log
|
122
|
+
end
|
119
123
|
|
120
|
-
|
121
|
-
|
124
|
+
def to_s
|
125
|
+
"voice #{@name} output: #{@output} channel: #{@channel}"
|
122
126
|
end
|
123
127
|
|
124
|
-
|
128
|
+
private
|
125
129
|
|
126
|
-
|
127
|
-
|
128
|
-
|
130
|
+
def fill_active_pitches(pitches)
|
131
|
+
(0..127).each do |pitch|
|
132
|
+
pitches[pitch] = { note_controls: Set[], velocity: 0 }
|
133
|
+
end
|
129
134
|
end
|
130
|
-
end
|
131
135
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
+
class ControllersControl
|
137
|
+
def initialize(output, channel)
|
138
|
+
@output = output
|
139
|
+
@channel = channel
|
136
140
|
|
137
|
-
|
138
|
-
|
139
|
-
|
141
|
+
@controller_map = { sustain_pedal: 0x40 }
|
142
|
+
@controller = []
|
143
|
+
end
|
140
144
|
|
141
|
-
|
142
|
-
|
143
|
-
|
145
|
+
def []=(controller_number_or_symbol, value)
|
146
|
+
number = number_of(controller_number_or_symbol)
|
147
|
+
value ||= 0
|
144
148
|
|
145
|
-
|
146
|
-
|
147
|
-
|
149
|
+
@controller[number] = [[0, value].max, 0xff].min
|
150
|
+
@output.puts MIDIMessage::ChannelMessage.new(0xb, @channel, number, @controller[number])
|
151
|
+
end
|
148
152
|
|
149
|
-
|
150
|
-
|
151
|
-
|
153
|
+
def [](controller_number_or_symbol)
|
154
|
+
@controller[number_of(controller_number_or_symbol)]
|
155
|
+
end
|
152
156
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
157
|
+
def number_of(controller_number_or_symbol)
|
158
|
+
case controller_number_or_symbol
|
159
|
+
when Numeric
|
160
|
+
controller_number_or_symbol.to_i
|
161
|
+
when Symbol
|
162
|
+
@controller_map[controller_number_or_symbol]
|
163
|
+
else
|
164
|
+
raise ArgumentError, "#{controller_number_or_symbol} is not a Numeric nor a Symbol. Only MIDI controller numbers are allowed"
|
165
|
+
end
|
161
166
|
end
|
162
167
|
end
|
163
|
-
end
|
164
168
|
|
165
|
-
|
169
|
+
private_constant :ControllersControl
|
166
170
|
|
167
|
-
|
168
|
-
|
171
|
+
class NoteControl
|
172
|
+
attr_reader :start_position, :end_position
|
169
173
|
|
170
|
-
|
171
|
-
|
174
|
+
def initialize(voice, pitch:, velocity: nil, duration: nil, velocity_off: nil)
|
175
|
+
raise ArgumentError, "MIDIVoice: note duration should be nil or Numeric: #{duration} (#{duration.class})" unless duration.nil? || duration.is_a?(Numeric)
|
172
176
|
|
173
|
-
|
177
|
+
@voice = voice
|
174
178
|
|
175
|
-
|
179
|
+
@pitch = pitch.arrayfy.explode_ranges
|
176
180
|
|
177
|
-
|
178
|
-
|
181
|
+
@velocity = velocity.arrayfy.explode_ranges
|
182
|
+
@velocity_off = velocity_off.arrayfy.explode_ranges
|
179
183
|
|
180
|
-
|
184
|
+
@duration = duration
|
181
185
|
|
182
|
-
|
183
|
-
|
186
|
+
@do_on_stop = []
|
187
|
+
@do_after = []
|
184
188
|
|
185
|
-
|
186
|
-
|
189
|
+
@start_position = @end_position = nil
|
190
|
+
end
|
187
191
|
|
188
|
-
|
189
|
-
|
190
|
-
|
192
|
+
def note_on
|
193
|
+
@start_position = @voice.sequencer.position
|
194
|
+
@end_position = nil
|
191
195
|
|
192
|
-
|
193
|
-
|
194
|
-
|
196
|
+
@pitch.each_index do |i|
|
197
|
+
pitch = @pitch[i]
|
198
|
+
velocity = @velocity[i % @velocity.size]
|
195
199
|
|
196
|
-
|
197
|
-
|
198
|
-
|
200
|
+
if !silence?(pitch)
|
201
|
+
@voice.active_pitches[pitch][:note_controls] << self
|
202
|
+
@voice.active_pitches[pitch][:velocity] = velocity
|
199
203
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
204
|
+
msg = MIDIMessage::NoteOn.new(@voice.channel, pitch, velocity)
|
205
|
+
@voice.log "#{msg.verbose_name} velocity: #{velocity} duration: #{@duration}"
|
206
|
+
@voice.output.puts msg if @voice.output && !@voice.fast_forward?
|
207
|
+
else
|
208
|
+
@voice.log "silence duration: #{@duration}"
|
209
|
+
end
|
205
210
|
end
|
206
|
-
end
|
207
211
|
|
208
|
-
|
212
|
+
return self unless @duration
|
213
|
+
|
214
|
+
this = self
|
215
|
+
@voice.sequencer.wait @duration do
|
216
|
+
this.note_off velocity: @velocity_off
|
217
|
+
end
|
209
218
|
|
210
|
-
|
211
|
-
@voice.sequencer.wait @duration do
|
212
|
-
this.note_off velocity: @velocity_off
|
219
|
+
self
|
213
220
|
end
|
214
221
|
|
215
|
-
|
216
|
-
|
222
|
+
def note_off(velocity: nil)
|
223
|
+
velocity ||= @velocity_off
|
217
224
|
|
218
|
-
|
219
|
-
velocity ||= @velocity_off
|
225
|
+
velocity = velocity.arrayfy.explode_ranges
|
220
226
|
|
221
|
-
|
227
|
+
@pitch.each_index do |i|
|
228
|
+
pitch = @pitch[i]
|
229
|
+
velocity_off = velocity[i % velocity.size]
|
222
230
|
|
223
|
-
|
224
|
-
pitch = @pitch[i]
|
225
|
-
velocity_off = velocity[i % velocity.size]
|
231
|
+
next if silence?(pitch)
|
226
232
|
|
227
|
-
|
233
|
+
@voice.active_pitches[pitch][:note_controls].delete self
|
228
234
|
|
229
|
-
|
235
|
+
next unless @voice.active_pitches[pitch][:note_controls].empty?
|
236
|
+
|
237
|
+
msg = MIDIMessage::NoteOff.new(@voice.channel, pitch, velocity_off)
|
238
|
+
@voice.log msg.verbose_name.to_s
|
239
|
+
@voice.output.puts msg if @voice.output && !@voice.fast_forward?
|
240
|
+
end
|
230
241
|
|
231
|
-
|
242
|
+
@end_position = @voice.sequencer.position
|
232
243
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
end
|
244
|
+
@do_on_stop.each do |do_on_stop|
|
245
|
+
@voice.sequencer.wait 0, &do_on_stop
|
246
|
+
end
|
237
247
|
|
238
|
-
|
248
|
+
@do_after.each do |do_after|
|
249
|
+
@voice.sequencer.wait @voice.tick_duration + do_after[:bars], &do_after[:block]
|
250
|
+
end
|
239
251
|
|
240
|
-
|
241
|
-
@voice.sequencer.wait 0, &do_on_stop
|
252
|
+
nil
|
242
253
|
end
|
243
254
|
|
244
|
-
|
245
|
-
@
|
255
|
+
def active?
|
256
|
+
@start_position && !@end_position
|
246
257
|
end
|
247
258
|
|
248
|
-
|
249
|
-
|
259
|
+
def on_stop(&block)
|
260
|
+
@do_on_stop << block
|
261
|
+
nil
|
262
|
+
end
|
250
263
|
|
251
|
-
|
252
|
-
|
253
|
-
|
264
|
+
def after(bars = 0, &block)
|
265
|
+
@do_after << { bars: bars.rationalize, block: block }
|
266
|
+
nil
|
267
|
+
end
|
254
268
|
|
255
|
-
|
256
|
-
@do_on_stop << block
|
257
|
-
nil
|
258
|
-
end
|
269
|
+
private
|
259
270
|
|
260
|
-
|
261
|
-
|
262
|
-
|
271
|
+
def silence?(pitch)
|
272
|
+
pitch.nil? || pitch == :silence
|
273
|
+
end
|
263
274
|
end
|
264
275
|
|
265
|
-
|
266
|
-
|
267
|
-
def silence?(pitch)
|
268
|
-
pitch.nil? || pitch == :silence
|
269
|
-
end
|
276
|
+
private_constant :NoteControl
|
270
277
|
end
|
271
|
-
|
272
|
-
private_constant :NoteControl
|
273
278
|
end
|
274
279
|
end
|