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.
@@ -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