cyclotone 0.1.0
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/LICENSE.txt +21 -0
- data/README.md +121 -0
- data/Rakefile +12 -0
- data/cyclotone.gemspec +29 -0
- data/exe/cyclotone +12 -0
- data/lib/cyclotone/backends/midi_backend.rb +114 -0
- data/lib/cyclotone/backends/midi_file_backend.rb +178 -0
- data/lib/cyclotone/backends/midi_message_support.rb +80 -0
- data/lib/cyclotone/backends/osc_backend.rb +117 -0
- data/lib/cyclotone/controls.rb +142 -0
- data/lib/cyclotone/dsl.rb +141 -0
- data/lib/cyclotone/errors.rb +27 -0
- data/lib/cyclotone/euclidean.rb +65 -0
- data/lib/cyclotone/event.rb +65 -0
- data/lib/cyclotone/harmony.rb +159 -0
- data/lib/cyclotone/mini_notation/ast.rb +199 -0
- data/lib/cyclotone/mini_notation/compiler.rb +115 -0
- data/lib/cyclotone/mini_notation/parser.rb +350 -0
- data/lib/cyclotone/oscillators.rb +131 -0
- data/lib/cyclotone/pattern.rb +361 -0
- data/lib/cyclotone/scheduler.rb +168 -0
- data/lib/cyclotone/state.rb +49 -0
- data/lib/cyclotone/stream.rb +185 -0
- data/lib/cyclotone/support/deterministic.rb +42 -0
- data/lib/cyclotone/time_span.rb +99 -0
- data/lib/cyclotone/transforms/accumulation.rb +45 -0
- data/lib/cyclotone/transforms/alteration.rb +173 -0
- data/lib/cyclotone/transforms/concatenation.rb +15 -0
- data/lib/cyclotone/transforms/condition.rb +63 -0
- data/lib/cyclotone/transforms/sample.rb +82 -0
- data/lib/cyclotone/transforms/time.rb +93 -0
- data/lib/cyclotone/transition.rb +204 -0
- data/lib/cyclotone/version.rb +5 -0
- data/lib/cyclotone.rb +32 -0
- metadata +79 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cyclotone
|
|
4
|
+
module MiniNotation
|
|
5
|
+
module AST
|
|
6
|
+
class Node
|
|
7
|
+
def ==(other)
|
|
8
|
+
other.is_a?(self.class) && to_h == other.to_h
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
alias eql? ==
|
|
12
|
+
|
|
13
|
+
def hash
|
|
14
|
+
[self.class, to_h].hash
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Atom < Node
|
|
19
|
+
attr_reader :value, :sample
|
|
20
|
+
|
|
21
|
+
def initialize(value:, sample: nil)
|
|
22
|
+
@value = value
|
|
23
|
+
@sample = sample
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with_sample(sample_number)
|
|
28
|
+
self.class.new(value: value, sample: sample_number)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{ value: value, sample: sample }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Rest < Node
|
|
37
|
+
def to_h
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Sequence < Node
|
|
43
|
+
attr_reader :elements
|
|
44
|
+
|
|
45
|
+
def initialize(elements:)
|
|
46
|
+
@elements = elements.freeze
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
{ elements: elements }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Stack < Node
|
|
56
|
+
attr_reader :patterns
|
|
57
|
+
|
|
58
|
+
def initialize(patterns:)
|
|
59
|
+
@patterns = patterns.freeze
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_h
|
|
64
|
+
{ patterns: patterns }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Alternating < Node
|
|
69
|
+
attr_reader :patterns
|
|
70
|
+
|
|
71
|
+
def initialize(patterns:)
|
|
72
|
+
@patterns = patterns.freeze
|
|
73
|
+
freeze
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{ patterns: patterns }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Repeat < Node
|
|
82
|
+
attr_reader :pattern, :count
|
|
83
|
+
|
|
84
|
+
def initialize(pattern:, count:)
|
|
85
|
+
@pattern = pattern
|
|
86
|
+
@count = count.to_i
|
|
87
|
+
freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_h
|
|
91
|
+
{ pattern: pattern, count: count }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Replicate < Node
|
|
96
|
+
attr_reader :pattern, :count
|
|
97
|
+
|
|
98
|
+
def initialize(pattern:, count:)
|
|
99
|
+
@pattern = pattern
|
|
100
|
+
@count = count.to_i
|
|
101
|
+
freeze
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_h
|
|
105
|
+
{ pattern: pattern, count: count }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Slow < Node
|
|
110
|
+
attr_reader :pattern, :amount
|
|
111
|
+
|
|
112
|
+
def initialize(pattern:, amount:)
|
|
113
|
+
@pattern = pattern
|
|
114
|
+
@amount = amount
|
|
115
|
+
freeze
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def to_h
|
|
119
|
+
{ pattern: pattern, amount: amount }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Elongate < Node
|
|
124
|
+
attr_reader :pattern, :amount
|
|
125
|
+
|
|
126
|
+
def initialize(pattern:, amount:)
|
|
127
|
+
@pattern = pattern
|
|
128
|
+
@amount = amount
|
|
129
|
+
freeze
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def increment(step = 1)
|
|
133
|
+
self.class.new(pattern: pattern, amount: amount + step)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_h
|
|
137
|
+
{ pattern: pattern, amount: amount }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class Degrade < Node
|
|
142
|
+
attr_reader :pattern, :probability
|
|
143
|
+
|
|
144
|
+
def initialize(pattern:, probability:)
|
|
145
|
+
@pattern = pattern
|
|
146
|
+
@probability = probability
|
|
147
|
+
freeze
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def to_h
|
|
151
|
+
{ pattern: pattern, probability: probability }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class Choice < Node
|
|
156
|
+
attr_reader :patterns
|
|
157
|
+
|
|
158
|
+
def initialize(patterns:)
|
|
159
|
+
@patterns = patterns.freeze
|
|
160
|
+
freeze
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def to_h
|
|
164
|
+
{ patterns: patterns }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class Euclidean < Node
|
|
169
|
+
attr_reader :pattern, :pulses, :steps, :rotation
|
|
170
|
+
|
|
171
|
+
def initialize(pattern:, pulses:, steps:, rotation: 0)
|
|
172
|
+
@pattern = pattern
|
|
173
|
+
@pulses = pulses.to_i
|
|
174
|
+
@steps = steps.to_i
|
|
175
|
+
@rotation = rotation.to_i
|
|
176
|
+
freeze
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def to_h
|
|
180
|
+
{ pattern: pattern, pulses: pulses, steps: steps, rotation: rotation }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class Polymetric < Node
|
|
185
|
+
attr_reader :patterns, :steps
|
|
186
|
+
|
|
187
|
+
def initialize(patterns:, steps: nil)
|
|
188
|
+
@patterns = patterns.freeze
|
|
189
|
+
@steps = steps&.to_i
|
|
190
|
+
freeze
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def to_h
|
|
194
|
+
{ patterns: patterns, steps: steps }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cyclotone
|
|
4
|
+
module MiniNotation
|
|
5
|
+
class Compiler
|
|
6
|
+
def compile(node)
|
|
7
|
+
case node
|
|
8
|
+
when AST::Atom
|
|
9
|
+
compile_atom(node)
|
|
10
|
+
when AST::Rest
|
|
11
|
+
Pattern.silence
|
|
12
|
+
when AST::Sequence
|
|
13
|
+
compile_sequence(node)
|
|
14
|
+
when AST::Stack
|
|
15
|
+
Pattern.stack(node.patterns.map { |pattern| compile(pattern) })
|
|
16
|
+
when AST::Alternating
|
|
17
|
+
compile_alternating(node)
|
|
18
|
+
when AST::Repeat
|
|
19
|
+
compile(node.pattern).fast(node.count)
|
|
20
|
+
when AST::Replicate
|
|
21
|
+
Pattern.timecat(Array.new(node.count) { [1, compile(node.pattern)] })
|
|
22
|
+
when AST::Slow
|
|
23
|
+
compile(node.pattern).slow(node.amount)
|
|
24
|
+
when AST::Elongate
|
|
25
|
+
compile(node.pattern).slow(node.amount)
|
|
26
|
+
when AST::Degrade
|
|
27
|
+
compile(node.pattern).degrade_by(node.probability)
|
|
28
|
+
when AST::Choice
|
|
29
|
+
Pattern.randcat(node.patterns.map { |pattern| compile(pattern) })
|
|
30
|
+
when AST::Euclidean
|
|
31
|
+
compile_euclidean(node)
|
|
32
|
+
when AST::Polymetric
|
|
33
|
+
compile_polymetric(node)
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "unsupported AST node #{node.class}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def compile_atom(node)
|
|
42
|
+
if node.sample
|
|
43
|
+
Pattern.pure({ s: node.value, n: node.sample })
|
|
44
|
+
else
|
|
45
|
+
Pattern.pure(node.value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def compile_sequence(node)
|
|
50
|
+
Pattern.timecat(node.elements.map { |element| compile_weighted(element) })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def compile_weighted(node)
|
|
54
|
+
if node.is_a?(AST::Elongate)
|
|
55
|
+
[node.amount, compile(node.pattern)]
|
|
56
|
+
else
|
|
57
|
+
[1, compile(node)]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def compile_alternating(node)
|
|
62
|
+
patterns = node.patterns.map { |pattern| compile(pattern) }
|
|
63
|
+
|
|
64
|
+
Pattern.new do |span|
|
|
65
|
+
cycle = span.cycle_number
|
|
66
|
+
patterns[cycle % patterns.length].query_span(span)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compile_euclidean(node)
|
|
71
|
+
gates = Euclidean.generate(node.pulses, node.steps, node.rotation)
|
|
72
|
+
Pattern.timecat(
|
|
73
|
+
gates.map do |gate|
|
|
74
|
+
[1, gate ? compile(node.pattern) : Pattern.silence]
|
|
75
|
+
end
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def compile_polymetric(node)
|
|
80
|
+
base_steps = (node.steps || step_count(node.patterns.first)).to_i
|
|
81
|
+
base_steps = 1 if base_steps <= 0
|
|
82
|
+
|
|
83
|
+
Pattern.stack(
|
|
84
|
+
node.patterns.map do |pattern|
|
|
85
|
+
pattern_steps = [step_count(pattern), 1].max
|
|
86
|
+
compile(pattern).slow(Rational(pattern_steps, base_steps))
|
|
87
|
+
end
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def step_count(node)
|
|
92
|
+
case node
|
|
93
|
+
when AST::Atom, AST::Rest
|
|
94
|
+
1
|
|
95
|
+
when AST::Sequence
|
|
96
|
+
node.elements.sum { |element| step_count(element) }
|
|
97
|
+
when AST::Stack, AST::Alternating, AST::Choice
|
|
98
|
+
node.patterns.map { |pattern| step_count(pattern) }.max || 1
|
|
99
|
+
when AST::Repeat, AST::Replicate
|
|
100
|
+
step_count(node.pattern) * node.count
|
|
101
|
+
when AST::Slow, AST::Degrade
|
|
102
|
+
step_count(node.pattern)
|
|
103
|
+
when AST::Elongate
|
|
104
|
+
step_count(node.pattern) * node.amount.to_i
|
|
105
|
+
when AST::Euclidean
|
|
106
|
+
[node.steps, 1].max * step_count(node.pattern)
|
|
107
|
+
when AST::Polymetric
|
|
108
|
+
node.steps || step_count(node.patterns.first)
|
|
109
|
+
else
|
|
110
|
+
1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cyclotone
|
|
4
|
+
module MiniNotation
|
|
5
|
+
class Parser
|
|
6
|
+
Token = Struct.new(:type, :value, :line, :column, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
SINGLE_CHAR_TOKENS = {
|
|
9
|
+
"[" => :lbracket,
|
|
10
|
+
"]" => :rbracket,
|
|
11
|
+
"{" => :lbrace,
|
|
12
|
+
"}" => :rbrace,
|
|
13
|
+
"(" => :lparen,
|
|
14
|
+
")" => :rparen,
|
|
15
|
+
"," => :comma,
|
|
16
|
+
"." => :dot,
|
|
17
|
+
"~" => :tilde,
|
|
18
|
+
"*" => :star,
|
|
19
|
+
"/" => :slash,
|
|
20
|
+
"!" => :bang,
|
|
21
|
+
"_" => :underscore,
|
|
22
|
+
"@" => :at,
|
|
23
|
+
"?" => :question,
|
|
24
|
+
"|" => :pipe,
|
|
25
|
+
":" => :colon,
|
|
26
|
+
"%" => :percent
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def parse(input)
|
|
30
|
+
@tokens = tokenize(input.to_s)
|
|
31
|
+
@index = 0
|
|
32
|
+
|
|
33
|
+
skip_spaces
|
|
34
|
+
raise ParseError.new("input is empty", line: 1, column: 1) if current.type == :eof
|
|
35
|
+
|
|
36
|
+
ast = parse_stack(terminators: [:eof])
|
|
37
|
+
skip_spaces
|
|
38
|
+
expect(:eof)
|
|
39
|
+
ast
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def tokenize(input)
|
|
45
|
+
tokens = []
|
|
46
|
+
line = 1
|
|
47
|
+
column = 1
|
|
48
|
+
index = 0
|
|
49
|
+
|
|
50
|
+
while index < input.length
|
|
51
|
+
char = input[index]
|
|
52
|
+
|
|
53
|
+
if char.match?(/\s/)
|
|
54
|
+
start_column = column
|
|
55
|
+
while index < input.length && input[index].match?(/\s/)
|
|
56
|
+
if input[index] == "\n"
|
|
57
|
+
line += 1
|
|
58
|
+
column = 1
|
|
59
|
+
else
|
|
60
|
+
column += 1
|
|
61
|
+
end
|
|
62
|
+
index += 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
tokens << Token.new(type: :space, value: " ", line: line, column: start_column)
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if char == "<"
|
|
70
|
+
if input[index + 1] == ">"
|
|
71
|
+
tokens << Token.new(type: :choice_gap, value: "<>", line: line, column: column)
|
|
72
|
+
index += 2
|
|
73
|
+
column += 2
|
|
74
|
+
else
|
|
75
|
+
tokens << Token.new(type: :langle, value: char, line: line, column: column)
|
|
76
|
+
index += 1
|
|
77
|
+
column += 1
|
|
78
|
+
end
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if char == ">"
|
|
83
|
+
tokens << Token.new(type: :rangle, value: char, line: line, column: column)
|
|
84
|
+
index += 1
|
|
85
|
+
column += 1
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if SINGLE_CHAR_TOKENS.key?(char)
|
|
90
|
+
tokens << Token.new(type: SINGLE_CHAR_TOKENS[char], value: char, line: line, column: column)
|
|
91
|
+
index += 1
|
|
92
|
+
column += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if char.match?(/[0-9]/)
|
|
97
|
+
start_index = index
|
|
98
|
+
start_column = column
|
|
99
|
+
while index < input.length && input[index].match?(/[0-9.]/)
|
|
100
|
+
index += 1
|
|
101
|
+
column += 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
tokens << Token.new(type: :number, value: input[start_index...index], line: line, column: start_column)
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
start_index = index
|
|
109
|
+
start_column = column
|
|
110
|
+
|
|
111
|
+
while index < input.length
|
|
112
|
+
current_char = input[index]
|
|
113
|
+
break if current_char.match?(/\s/) || SINGLE_CHAR_TOKENS.key?(current_char) || %w[< >].include?(current_char)
|
|
114
|
+
|
|
115
|
+
index += 1
|
|
116
|
+
column += 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
value = input[start_index...index]
|
|
120
|
+
tokens << Token.new(type: :word, value: value, line: line, column: start_column)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
tokens << Token.new(type: :eof, value: nil, line: line, column: column)
|
|
124
|
+
tokens
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_stack(terminators:)
|
|
128
|
+
patterns = [parse_choice(terminators: terminators + [:comma])]
|
|
129
|
+
skip_spaces
|
|
130
|
+
|
|
131
|
+
while accept(:comma)
|
|
132
|
+
skip_spaces
|
|
133
|
+
patterns << parse_choice(terminators: terminators + [:comma])
|
|
134
|
+
skip_spaces
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return patterns.first if patterns.length == 1
|
|
138
|
+
|
|
139
|
+
AST::Stack.new(patterns: patterns)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_choice(terminators:)
|
|
143
|
+
patterns = [parse_sequence(terminators: terminators + [:pipe])]
|
|
144
|
+
skip_spaces
|
|
145
|
+
|
|
146
|
+
while accept(:pipe)
|
|
147
|
+
skip_spaces
|
|
148
|
+
patterns << parse_sequence(terminators: terminators + [:pipe])
|
|
149
|
+
skip_spaces
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return patterns.first if patterns.length == 1
|
|
153
|
+
|
|
154
|
+
AST::Choice.new(patterns: patterns)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_sequence(terminators:)
|
|
158
|
+
groups = []
|
|
159
|
+
current_group = []
|
|
160
|
+
|
|
161
|
+
loop do
|
|
162
|
+
skip_spaces
|
|
163
|
+
break if terminators.include?(current.type)
|
|
164
|
+
|
|
165
|
+
if accept(:dot)
|
|
166
|
+
groups << build_group(current_group)
|
|
167
|
+
current_group = []
|
|
168
|
+
next
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if accept(:underscore)
|
|
172
|
+
raise parse_error("unexpected underscore") if current_group.empty?
|
|
173
|
+
|
|
174
|
+
current_group[-1] = if current_group[-1].is_a?(AST::Elongate)
|
|
175
|
+
current_group[-1].increment
|
|
176
|
+
else
|
|
177
|
+
AST::Elongate.new(pattern: current_group[-1], amount: 2)
|
|
178
|
+
end
|
|
179
|
+
next
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
current_group << parse_term
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
groups << build_group(current_group) unless current_group.empty?
|
|
186
|
+
raise parse_error("expected a pattern") if groups.empty?
|
|
187
|
+
|
|
188
|
+
return groups.first if groups.length == 1
|
|
189
|
+
|
|
190
|
+
AST::Sequence.new(elements: groups)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_term
|
|
194
|
+
node = parse_primary
|
|
195
|
+
|
|
196
|
+
loop do
|
|
197
|
+
skip_spaces
|
|
198
|
+
|
|
199
|
+
node = case current.type
|
|
200
|
+
when :star
|
|
201
|
+
advance
|
|
202
|
+
AST::Repeat.new(pattern: node, count: parse_integer)
|
|
203
|
+
when :bang
|
|
204
|
+
advance
|
|
205
|
+
AST::Replicate.new(pattern: node, count: parse_integer)
|
|
206
|
+
when :slash
|
|
207
|
+
advance
|
|
208
|
+
AST::Slow.new(pattern: node, amount: parse_number)
|
|
209
|
+
when :at
|
|
210
|
+
advance
|
|
211
|
+
AST::Elongate.new(pattern: node, amount: parse_number)
|
|
212
|
+
when :question
|
|
213
|
+
advance
|
|
214
|
+
probability = current.type == :number ? parse_number : 0.5
|
|
215
|
+
AST::Degrade.new(pattern: node, probability: probability)
|
|
216
|
+
when :colon
|
|
217
|
+
advance
|
|
218
|
+
sample = parse_integer
|
|
219
|
+
unless node.is_a?(AST::Atom)
|
|
220
|
+
raise parse_error("sample suffix can only be applied to atoms")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
node.with_sample(sample)
|
|
224
|
+
when :lparen
|
|
225
|
+
parse_euclidean(node)
|
|
226
|
+
else
|
|
227
|
+
break
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
node
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def parse_primary
|
|
235
|
+
token = current
|
|
236
|
+
|
|
237
|
+
case token.type
|
|
238
|
+
when :word
|
|
239
|
+
advance
|
|
240
|
+
AST::Atom.new(value: token.value)
|
|
241
|
+
when :number
|
|
242
|
+
advance
|
|
243
|
+
numeric_value = token.value.include?(".") ? token.value.to_f : token.value.to_i
|
|
244
|
+
AST::Atom.new(value: numeric_value)
|
|
245
|
+
when :tilde
|
|
246
|
+
advance
|
|
247
|
+
AST::Rest.new
|
|
248
|
+
when :lbracket
|
|
249
|
+
advance
|
|
250
|
+
node = parse_stack(terminators: [:rbracket])
|
|
251
|
+
expect(:rbracket)
|
|
252
|
+
node
|
|
253
|
+
when :langle
|
|
254
|
+
advance
|
|
255
|
+
node = parse_sequence(terminators: [:rangle])
|
|
256
|
+
expect(:rangle)
|
|
257
|
+
AST::Alternating.new(patterns: unwrap(node))
|
|
258
|
+
when :lbrace
|
|
259
|
+
parse_polymetric
|
|
260
|
+
else
|
|
261
|
+
raise parse_error("unexpected token #{token.type}")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def parse_euclidean(node)
|
|
266
|
+
expect(:lparen)
|
|
267
|
+
pulses = parse_integer
|
|
268
|
+
expect(:comma)
|
|
269
|
+
steps = parse_integer
|
|
270
|
+
rotation = 0
|
|
271
|
+
|
|
272
|
+
if accept(:comma)
|
|
273
|
+
rotation = parse_integer
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
expect(:rparen)
|
|
277
|
+
AST::Euclidean.new(pattern: node, pulses: pulses, steps: steps, rotation: rotation)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def parse_polymetric
|
|
281
|
+
expect(:lbrace)
|
|
282
|
+
patterns = [parse_sequence(terminators: [:comma, :rbrace])]
|
|
283
|
+
|
|
284
|
+
while accept(:comma)
|
|
285
|
+
patterns << parse_sequence(terminators: [:comma, :rbrace])
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
expect(:rbrace)
|
|
289
|
+
steps = accept(:percent) ? parse_integer : nil
|
|
290
|
+
|
|
291
|
+
AST::Polymetric.new(patterns: patterns, steps: steps)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def parse_number
|
|
295
|
+
token = expect(:number)
|
|
296
|
+
|
|
297
|
+
token.value.include?(".") ? token.value.to_f : token.value.to_i
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def parse_integer
|
|
301
|
+
expect(:number).value.to_i
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def build_group(elements)
|
|
305
|
+
raise parse_error("empty group") if elements.empty?
|
|
306
|
+
|
|
307
|
+
return elements.first if elements.length == 1
|
|
308
|
+
|
|
309
|
+
AST::Sequence.new(elements: elements)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def unwrap(node)
|
|
313
|
+
return node.elements if node.is_a?(AST::Sequence)
|
|
314
|
+
|
|
315
|
+
[node]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def current
|
|
319
|
+
@tokens[@index]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def advance
|
|
323
|
+
token = current
|
|
324
|
+
@index += 1
|
|
325
|
+
token
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def accept(type)
|
|
329
|
+
return false unless current.type == type
|
|
330
|
+
|
|
331
|
+
advance
|
|
332
|
+
true
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def expect(type)
|
|
336
|
+
return advance if current.type == type
|
|
337
|
+
|
|
338
|
+
raise parse_error("expected #{type}, got #{current.type}")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def skip_spaces
|
|
342
|
+
advance while current.type == :space
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def parse_error(message)
|
|
346
|
+
ParseError.new(message, line: current.line, column: current.column)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|