ramekin 0.0.5b
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +277 -0
- data/gembin/ramekin +16 -0
- data/lib/ramekin/amk_runner/sample_groups.rb +65 -0
- data/lib/ramekin/amk_runner.rb +123 -0
- data/lib/ramekin/amk_setup.rb +116 -0
- data/lib/ramekin/channel_separator.rb +40 -0
- data/lib/ramekin/cli.rb +367 -0
- data/lib/ramekin/config.rb +126 -0
- data/lib/ramekin/element.rb +24 -0
- data/lib/ramekin/errors.rb +57 -0
- data/lib/ramekin/legato.rb +142 -0
- data/lib/ramekin/macros.rb +101 -0
- data/lib/ramekin/meta.rb +227 -0
- data/lib/ramekin/note_aggregator.rb +252 -0
- data/lib/ramekin/processor.rb +78 -0
- data/lib/ramekin/renderer.rb +288 -0
- data/lib/ramekin/sample_pack.rb +296 -0
- data/lib/ramekin/spc_player.rb +122 -0
- data/lib/ramekin/tokenizer.rb +287 -0
- data/lib/ramekin/util.rb +120 -0
- data/lib/ramekin/volume.rb +16 -0
- data/lib/ramekin.rb +19 -0
- metadata +122 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class MacroDefinition < Element
|
3
|
+
attr_reader :token, :elements
|
4
|
+
def initialize(token, elements=[])
|
5
|
+
@token = token
|
6
|
+
@elements = elements
|
7
|
+
end
|
8
|
+
|
9
|
+
def name
|
10
|
+
@token.value
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"macro:#{name}=[#{@elements.map(&:first).map(&:repr_basic).join(',')}]"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MacroExpander < Processor
|
19
|
+
def initialize
|
20
|
+
@definitions = Hash.new { |h, k| h[k] = [] }
|
21
|
+
@defines = {}
|
22
|
+
@ifdef_depth = 0
|
23
|
+
@skip_depth = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(&b)
|
27
|
+
@level = 0
|
28
|
+
parse_chunk(nil) do |el, stack|
|
29
|
+
el.macro_stack = stack
|
30
|
+
yield el
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_chunk(expected_end=nil, &b)
|
35
|
+
@level += 1
|
36
|
+
|
37
|
+
each do |el|
|
38
|
+
if @skip_depth && @ifdef_depth >= @skip_depth
|
39
|
+
if Token === el && el.type == :endif
|
40
|
+
@ifdef_depth -= 1
|
41
|
+
|
42
|
+
if @ifdef_depth < @ifdef_depth
|
43
|
+
@skip_depth = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
el.macro_stack = @stack.dup
|
51
|
+
next yield el, [] unless Token === el
|
52
|
+
|
53
|
+
if el.type == expected_end
|
54
|
+
break
|
55
|
+
end
|
56
|
+
|
57
|
+
case el.type
|
58
|
+
when :macro
|
59
|
+
definition = MacroDefinition.new(el)
|
60
|
+
@definitions[el.value] = definition
|
61
|
+
parse_chunk :endmacro do |sub, stack|
|
62
|
+
definition.elements << [sub, stack + [el]]
|
63
|
+
end
|
64
|
+
|
65
|
+
# yield definition, []
|
66
|
+
when :macrocall
|
67
|
+
@definitions[el.value].elements.each do |sub, stack|
|
68
|
+
yield sub.dup, stack
|
69
|
+
end
|
70
|
+
when :endmacro
|
71
|
+
error! 'endmacro token outside of a macro definition'
|
72
|
+
when :define
|
73
|
+
# TODO: value defines (needs lexer support)
|
74
|
+
@defines[el.value] = 1
|
75
|
+
when :ifdef
|
76
|
+
@ifdef_depth += 1
|
77
|
+
unless @defines.key?(el.value)
|
78
|
+
@skip_depth = @ifdef_depth
|
79
|
+
end
|
80
|
+
when :ifndef
|
81
|
+
@ifdef_depth += 1
|
82
|
+
if @defines.key?(el.value)
|
83
|
+
@skip_depth = @ifdef_depth
|
84
|
+
end
|
85
|
+
when :endif
|
86
|
+
@ifdef_depth -= 1
|
87
|
+
else
|
88
|
+
yield el, []
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue StopIteration
|
92
|
+
if @level > 1
|
93
|
+
error! "unclosed replacement: missing double quote" if @level > 1
|
94
|
+
end
|
95
|
+
# pass
|
96
|
+
ensure
|
97
|
+
@level -= 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
data/lib/ramekin/meta.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Meta
|
3
|
+
include Error::Helpers
|
4
|
+
|
5
|
+
attr_reader :title
|
6
|
+
attr_reader :game
|
7
|
+
attr_reader :author
|
8
|
+
attr_reader :comment
|
9
|
+
attr_reader :readme
|
10
|
+
attr_reader :amk
|
11
|
+
attr_reader :instruments
|
12
|
+
attr_reader :sample_groups
|
13
|
+
attr_reader :options
|
14
|
+
attr_reader :volume
|
15
|
+
attr_reader :tempo
|
16
|
+
attr_reader :echo
|
17
|
+
def initialize(elements)
|
18
|
+
@elements = elements
|
19
|
+
@instruments = []
|
20
|
+
@sample_groups = []
|
21
|
+
@options = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse
|
25
|
+
loop do
|
26
|
+
break if @elements.empty?
|
27
|
+
next!
|
28
|
+
|
29
|
+
case @current.type
|
30
|
+
when :directive then parse_directive
|
31
|
+
when :amk then @amk = @current
|
32
|
+
when :option then @options[@current.value] = true
|
33
|
+
when :w then @volume = @current
|
34
|
+
when :t, :bpm then @tempo = @current
|
35
|
+
else
|
36
|
+
error! 'invalid token in header'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@sample_groups << 'default' if @sample_groups.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_directive
|
44
|
+
@current_directive = @current
|
45
|
+
case @current.value
|
46
|
+
when 'title' then @title = expect_arg(:string)
|
47
|
+
when 'game' then @game = expect_arg(:string)
|
48
|
+
when 'author' then @author = expect_arg(:string)
|
49
|
+
when 'comment' then @comment = expect_arg(:string)
|
50
|
+
when 'readme' then @readme = expect_arg(:string)
|
51
|
+
when 'pack' then
|
52
|
+
pack_name = expect_arg(:string)
|
53
|
+
@current_pack = SamplePack.find(pack_name.value) \
|
54
|
+
or error!("can't find sample pack #{pack_name.value.inspect}, try running `ramekin package update`")
|
55
|
+
when 'instrument'
|
56
|
+
name, path = expect_args(:instrument, :string)
|
57
|
+
extensions = []
|
58
|
+
while (el = check_arg(:adsr, :tuning, :o))
|
59
|
+
extensions << el
|
60
|
+
end
|
61
|
+
|
62
|
+
return unless @current_pack
|
63
|
+
@instruments << Instrument.new(@current_pack, @current, name, path, extensions)
|
64
|
+
when 'default', 'optimized' then @sample_groups << @current
|
65
|
+
|
66
|
+
# TODO: real echo syntax
|
67
|
+
when 'echo' then @echo = expect_args(*([:hex] * 8))
|
68
|
+
else
|
69
|
+
error! 'invalid directive in header'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def expect_args(*types)
|
74
|
+
types.map do |type|
|
75
|
+
next!
|
76
|
+
unless @current.type == type
|
77
|
+
error! "expected #{type} in ##{@current_directive.value} directive"
|
78
|
+
end
|
79
|
+
|
80
|
+
@current
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def expect_arg(type)
|
85
|
+
expect_args(type).first
|
86
|
+
end
|
87
|
+
|
88
|
+
def check_arg(*types)
|
89
|
+
if peek && types.include?(peek.type)
|
90
|
+
next!
|
91
|
+
@current
|
92
|
+
else
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def next!
|
98
|
+
if @peek
|
99
|
+
@current = @peek
|
100
|
+
@peek = nil
|
101
|
+
return @current
|
102
|
+
end
|
103
|
+
|
104
|
+
@current = @elements.shift
|
105
|
+
end
|
106
|
+
|
107
|
+
def peek
|
108
|
+
@peek ||= @elements.shift
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# TODO: customization options for instrument
|
113
|
+
class Instrument < Element
|
114
|
+
attr_reader :pack, :directive, :name, :path
|
115
|
+
def initialize(pack, directive, name, path, extensions)
|
116
|
+
@pack = pack
|
117
|
+
@directive = directive
|
118
|
+
@name = name
|
119
|
+
@path = path
|
120
|
+
@extensions = extensions
|
121
|
+
end
|
122
|
+
|
123
|
+
include Error::Helpers
|
124
|
+
# @override
|
125
|
+
def default_error_location
|
126
|
+
@directive
|
127
|
+
end
|
128
|
+
|
129
|
+
def inspect
|
130
|
+
"inst:@#{name.value} #{@extensions.inspect}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def sample_name
|
134
|
+
@sample_name ||= File.basename(sample_path)
|
135
|
+
end
|
136
|
+
|
137
|
+
def sample_path
|
138
|
+
@pack.find(@path.value)
|
139
|
+
end
|
140
|
+
|
141
|
+
def pack_adsr
|
142
|
+
return if pack_hexes.nil?
|
143
|
+
|
144
|
+
hex1, hex2, _ = pack_hexes
|
145
|
+
h1 = hex1.to_i(16)
|
146
|
+
h2 = hex2.to_i(16)
|
147
|
+
|
148
|
+
# unset the first bit, as it is unused
|
149
|
+
h1 &= ~0x80
|
150
|
+
|
151
|
+
decay = (7 - (h1 >> 4))
|
152
|
+
attack = (15 - (h1 & 0b1111))
|
153
|
+
sustain = h2 >> 5
|
154
|
+
release = (31 - (h2 & 0b11111))
|
155
|
+
|
156
|
+
[attack, decay, sustain, release]
|
157
|
+
end
|
158
|
+
|
159
|
+
def ext_adsr
|
160
|
+
adsr = @extensions.select { |e| e.type == :adsr }.last
|
161
|
+
adsr && adsr.value.split(',').map(&:to_i)
|
162
|
+
end
|
163
|
+
|
164
|
+
def adsr
|
165
|
+
@adsr ||= ext_adsr || pack_adsr \
|
166
|
+
or error! "no adsr configured for #{name}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def tuning
|
170
|
+
@tuning ||= ext_tuning || pack_tuning \
|
171
|
+
or error! "no tuning configured for #{name}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def pack_tuning
|
175
|
+
_, _, _, d, e = pack_hexes
|
176
|
+
[d, e]
|
177
|
+
end
|
178
|
+
|
179
|
+
def ext_tuning
|
180
|
+
tuning = @extensions.select { |e| e.type == :tuning }.last
|
181
|
+
tuning && tuning.value.scan(/../)
|
182
|
+
end
|
183
|
+
|
184
|
+
def pack_hexes
|
185
|
+
# TODO: alts
|
186
|
+
@pack_hexes ||= @pack.tunings_for(sample_name).first
|
187
|
+
end
|
188
|
+
|
189
|
+
def pack_gain
|
190
|
+
_, _, g, _, _ = pack_hexes
|
191
|
+
return g
|
192
|
+
end
|
193
|
+
|
194
|
+
def ext_gain
|
195
|
+
gain = @extensions.select { |e| e.type == :gain }.last
|
196
|
+
gain && gain.value
|
197
|
+
end
|
198
|
+
|
199
|
+
def gain
|
200
|
+
@gain ||= ext_gain || pack_gain \
|
201
|
+
or error! "no gain configured for #{name}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def hexes
|
205
|
+
a, d, s, r = self.adsr
|
206
|
+
t1, t2 = self.tuning
|
207
|
+
g = self.gain
|
208
|
+
|
209
|
+
adsr1 = ((7 - d)*16 | 0x80) + (15 - a)
|
210
|
+
adsr2 = (s*32 + (31-r))
|
211
|
+
|
212
|
+
[adsr1.to_s(16), adsr2.to_s(16), g, t1, t2]
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_amk
|
216
|
+
"#{File.basename(sample_name).inspect} #{hexes.map { |h| "$#{h}" }.join(' ')}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def octave
|
220
|
+
@extensions.reverse_each do |ext|
|
221
|
+
return ext.value.to_i if Token === ext && ext.type == :o
|
222
|
+
end
|
223
|
+
|
224
|
+
4
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class NoteEvent < Element
|
3
|
+
include Error::Helpers
|
4
|
+
def default_error_location
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :note, :extensions, :octave
|
9
|
+
def initialize(note, octave, triplets, extensions=[])
|
10
|
+
@note = note
|
11
|
+
@octave = octave
|
12
|
+
@triplets = triplets
|
13
|
+
@extensions = extensions
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
@note.start
|
18
|
+
end
|
19
|
+
|
20
|
+
def fin
|
21
|
+
(@extensions.last || @note).fin
|
22
|
+
end
|
23
|
+
|
24
|
+
def octave_num
|
25
|
+
case @octave
|
26
|
+
when Token then @octave.value.to_i
|
27
|
+
else @octave.to_i
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def note_name
|
32
|
+
return 'r' if rest?
|
33
|
+
return '^' if tie?
|
34
|
+
@note.value
|
35
|
+
end
|
36
|
+
|
37
|
+
def rest?
|
38
|
+
@note.type == :r
|
39
|
+
end
|
40
|
+
|
41
|
+
def tie?
|
42
|
+
@note.type == :native_tie
|
43
|
+
end
|
44
|
+
|
45
|
+
KNOWN_LENGTHS = {}.tap do |out|
|
46
|
+
ticks = 192
|
47
|
+
length = 1
|
48
|
+
|
49
|
+
loop do
|
50
|
+
out[ticks] = length.to_s
|
51
|
+
|
52
|
+
break unless ticks % 2 == 0
|
53
|
+
|
54
|
+
out[ticks * 3 / 2] = "#{length}."
|
55
|
+
length *= 2
|
56
|
+
ticks /= 2
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def length_amk
|
61
|
+
# we use l16
|
62
|
+
return '' if ticks == 12
|
63
|
+
KNOWN_LENGTHS.fetch(ticks) { "=#{ticks}" }
|
64
|
+
end
|
65
|
+
|
66
|
+
def octave_amk(current_octave=nil)
|
67
|
+
return '' if rest? || tie?
|
68
|
+
|
69
|
+
return "o#{octave_num}" if current_octave.nil?
|
70
|
+
|
71
|
+
case octave_num
|
72
|
+
when current_octave
|
73
|
+
""
|
74
|
+
when current_octave + 1
|
75
|
+
">"
|
76
|
+
when current_octave - 1
|
77
|
+
"<"
|
78
|
+
else
|
79
|
+
"o#{octave_num}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_amk(current_octave=nil)
|
84
|
+
return "#{octave_amk(current_octave)}#{note_name}#{length_amk}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def repr
|
88
|
+
"note:#{to_amk}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_length
|
92
|
+
@note.meta[:l]
|
93
|
+
end
|
94
|
+
|
95
|
+
def ticks
|
96
|
+
if @extensions.empty? && default_length.nil?
|
97
|
+
error! "no length and no default specified with l", el: @note
|
98
|
+
end
|
99
|
+
|
100
|
+
exts = @extensions
|
101
|
+
exts = [default_length] if exts.empty?
|
102
|
+
|
103
|
+
base = exts.map do |ext|
|
104
|
+
ext = ext.value if Token === ext
|
105
|
+
|
106
|
+
case ext
|
107
|
+
when /\A=(\d+)\z/ then $1.to_i
|
108
|
+
when '0' then 192 * 2
|
109
|
+
when /\A(\d+)\z/ then 192 / $1.to_i
|
110
|
+
when /\A(\d+)[.]\z/ then 192 * 3 / ($1.to_i * 2)
|
111
|
+
else
|
112
|
+
error! "unknown length specifier #{ext}", el: ext
|
113
|
+
end
|
114
|
+
end.sum
|
115
|
+
|
116
|
+
base = (base * 2) / 3 if @triplets
|
117
|
+
|
118
|
+
base
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
repr
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class CombinedRestEvent < NoteEvent
|
127
|
+
def initialize(rests)
|
128
|
+
@rests = rests
|
129
|
+
error! "empty CombinedRestEvent" if rests.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
rests.first.start
|
134
|
+
end
|
135
|
+
|
136
|
+
def fin
|
137
|
+
rests.last.fin
|
138
|
+
end
|
139
|
+
|
140
|
+
def rest?
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
def ticks
|
145
|
+
@rests.map(&:ticks).sum
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class ScanForL < Processor
|
150
|
+
def call(&b)
|
151
|
+
@current_l = nil
|
152
|
+
|
153
|
+
each do |el|
|
154
|
+
case el.type
|
155
|
+
when :l
|
156
|
+
@current_l = el
|
157
|
+
when :note, :r, :native_tie
|
158
|
+
el.meta[:l] = @current_l
|
159
|
+
yield el
|
160
|
+
else
|
161
|
+
yield el
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class NoteAggregator < Processor
|
168
|
+
def initialize
|
169
|
+
@current_note = nil
|
170
|
+
end
|
171
|
+
|
172
|
+
def flush!(&b)
|
173
|
+
if @current_note
|
174
|
+
yield @current_note
|
175
|
+
@current_note = nil
|
176
|
+
end
|
177
|
+
|
178
|
+
buffer.each(&b)
|
179
|
+
buffer.clear
|
180
|
+
end
|
181
|
+
|
182
|
+
def call(&b)
|
183
|
+
return enum_for(:call) unless block_given?
|
184
|
+
|
185
|
+
current_l = nil
|
186
|
+
current_octave = 4
|
187
|
+
@triplets = false
|
188
|
+
|
189
|
+
each do |el|
|
190
|
+
next yield el unless Token === el
|
191
|
+
|
192
|
+
case el.type
|
193
|
+
when :lbrace
|
194
|
+
@triplets = true
|
195
|
+
yield el
|
196
|
+
when :rbrace
|
197
|
+
@triplets = false
|
198
|
+
yield el
|
199
|
+
when :instrument
|
200
|
+
inst = el.meta[:inst]
|
201
|
+
|
202
|
+
current_octave = inst ? inst.octave : 4
|
203
|
+
buffer << el
|
204
|
+
when :l
|
205
|
+
current_l = el
|
206
|
+
when :note, :r, :native_tie
|
207
|
+
flush!(&b)
|
208
|
+
@current_note = NoteEvent.new(el, current_octave, @triplets)
|
209
|
+
when :o
|
210
|
+
current_octave = el.value.to_i
|
211
|
+
when :octave
|
212
|
+
current_octave += (el.value == '<' ? -1 : 1)
|
213
|
+
when :tie
|
214
|
+
# handle, for example, r^8
|
215
|
+
if @current_note.extensions.empty?
|
216
|
+
@current_note.extensions << @current_note.default_length
|
217
|
+
end
|
218
|
+
|
219
|
+
next
|
220
|
+
when :length
|
221
|
+
@current_note.extensions << el
|
222
|
+
else
|
223
|
+
buffer << el
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
flush!(&b)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class RestAggregator < Processor
|
232
|
+
def flush!(&b)
|
233
|
+
return if buffer.empty?
|
234
|
+
yield CombinedRestEvent.new(buffer)
|
235
|
+
@buffer = []
|
236
|
+
end
|
237
|
+
|
238
|
+
def call(&b)
|
239
|
+
each do |evt|
|
240
|
+
if NoteEvent === evt && evt.rest?
|
241
|
+
buffer << evt
|
242
|
+
next
|
243
|
+
end
|
244
|
+
|
245
|
+
flush!(&b)
|
246
|
+
yield evt
|
247
|
+
end
|
248
|
+
|
249
|
+
flush!(&b)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Processor
|
3
|
+
attr_accessor :stream
|
4
|
+
|
5
|
+
include Error::Helpers
|
6
|
+
|
7
|
+
def buffer
|
8
|
+
@buffer ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def flush!(&b)
|
12
|
+
return if buffer.empty?
|
13
|
+
buffer.each(&b)
|
14
|
+
buffer.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(enum, *a, &b)
|
18
|
+
return enum_for(:call, enum, *a) unless block_given?
|
19
|
+
|
20
|
+
enum = enum.each if enum.is_a?(Array)
|
21
|
+
|
22
|
+
inst = new(*a)
|
23
|
+
inst.stream = enum
|
24
|
+
inst.call(&b)
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
loop do
|
29
|
+
next! or break
|
30
|
+
yield @current
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def next!
|
35
|
+
if @peek
|
36
|
+
@current, @peek = @peek, nil
|
37
|
+
return @current
|
38
|
+
end
|
39
|
+
|
40
|
+
@current = @stream.next
|
41
|
+
end
|
42
|
+
|
43
|
+
def peek
|
44
|
+
@peek ||= @stream.next
|
45
|
+
rescue StopIteration
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.compose(*processors)
|
50
|
+
Chain.new(processors)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Chain < Processor
|
55
|
+
def initialize(processors)
|
56
|
+
@processors = processors
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(stream, &b)
|
60
|
+
out = stream
|
61
|
+
|
62
|
+
@processors.each do |p|
|
63
|
+
out = p.call(out)
|
64
|
+
end
|
65
|
+
|
66
|
+
out
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Inspector < Processor
|
71
|
+
def call(&b)
|
72
|
+
each do |el|
|
73
|
+
# p el
|
74
|
+
yield el
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|