ramekin 0.0.5b
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 +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
|