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,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Controls
5
+ CONTROL_DEFS = {
6
+ s: { type: :string, aliases: [:sound] },
7
+ n: { type: :integer },
8
+ speed: { type: :float, default: 1.0 },
9
+ begin: { type: :float, aliases: [:sample_begin] },
10
+ end: { type: :float, aliases: [:sample_end] },
11
+ pan: { type: :float, default: 0.5 },
12
+ gain: { type: :float, default: 1.0 },
13
+ amp: { type: :float, default: 1.0 },
14
+ cut: { type: :integer },
15
+ unit: { type: :string },
16
+ accelerate: { type: :float },
17
+ legato: { type: :float },
18
+ attack: { type: :float, aliases: [:att] },
19
+ hold: { type: :float },
20
+ release: { type: :float, aliases: [:rel] },
21
+ cutoff: { type: :float, aliases: [:lpf] },
22
+ resonance: { type: :float, aliases: [:lpq] },
23
+ hcutoff: { type: :float, aliases: [:hpf] },
24
+ hresonance: { type: :float, aliases: [:hpq] },
25
+ bandf: { type: :float, aliases: [:bpf] },
26
+ bandq: { type: :float, aliases: [:bpq] },
27
+ djf: { type: :float },
28
+ vowel: { type: :string },
29
+ delay: { type: :float },
30
+ delaytime: { type: :float, aliases: [:delayt] },
31
+ delayfeedback: { type: :float, aliases: [:delayfb] },
32
+ lock: { type: :integer },
33
+ dry: { type: :float },
34
+ room: { type: :float },
35
+ size: { type: :float, aliases: [:sz] },
36
+ distort: { type: :float },
37
+ triode: { type: :float },
38
+ shape: { type: :float },
39
+ squiz: { type: :float },
40
+ crush: { type: :float },
41
+ coarse: { type: :float },
42
+ tremolorate: { type: :float, aliases: [:tremr] },
43
+ tremolodepth: { type: :float, aliases: [:tremdp] },
44
+ phaserrate: { type: :float, aliases: [:phasr] },
45
+ phaserdepth: { type: :float, aliases: [:phasdp] },
46
+ leslie: { type: :float },
47
+ lrate: { type: :float },
48
+ lsize: { type: :float },
49
+ octer: { type: :float },
50
+ octersub: { type: :float },
51
+ octersubsub: { type: :float },
52
+ fshift: { type: :float },
53
+ fshiftnote: { type: :float },
54
+ fshiftphase: { type: :float },
55
+ ring: { type: :float },
56
+ ringf: { type: :float },
57
+ ringdf: { type: :float },
58
+ note: { type: :integer },
59
+ velocity: { type: :integer, default: 100 },
60
+ sustain: { type: :float, default: 1.0 },
61
+ channel: { type: :integer, default: 0 },
62
+ cc: { type: :hash }
63
+ }.freeze
64
+
65
+ ALIASES = CONTROL_DEFS.each_with_object({}) do |(name, options), mapping|
66
+ mapping[name] = name
67
+ Array(options[:aliases]).each { |alias_name| mapping[alias_name] = name }
68
+ end.freeze
69
+
70
+ module_function
71
+
72
+ CONTROL_DEFS.each_key do |name|
73
+ define_method(name) do |pattern_or_value|
74
+ factory(name, pattern_or_value)
75
+ end
76
+
77
+ Array(CONTROL_DEFS[name][:aliases]).each do |alias_name|
78
+ define_method(alias_name) do |pattern_or_value|
79
+ factory(name, pattern_or_value)
80
+ end
81
+ end
82
+ end
83
+
84
+ def control(name, pattern_or_value)
85
+ factory(name, pattern_or_value)
86
+ end
87
+
88
+ def factory(name, pattern_or_value)
89
+ canonical_name = canonical(name)
90
+ pattern = coerce_pattern(pattern_or_value)
91
+
92
+ pattern.fmap do |value|
93
+ wrap_value(canonical_name, value)
94
+ end
95
+ end
96
+
97
+ def coerce_pattern(pattern_or_value)
98
+ return pattern_or_value if pattern_or_value.is_a?(Pattern)
99
+ return Pattern.mn(pattern_or_value) if pattern_or_value.is_a?(String)
100
+
101
+ Pattern.pure(pattern_or_value)
102
+ end
103
+
104
+ def wrap_value(control_name, value)
105
+ if value.is_a?(Hash)
106
+ if control_name == :s && (value.key?(:s) || value.key?(:n))
107
+ value.merge(s: value[:s] || value[:value])
108
+ elsif control_name == :note && value.key?(:note)
109
+ value
110
+ else
111
+ value.merge(control_name => value[control_name] || value[:value] || value)
112
+ end
113
+ else
114
+ { control_name => value }
115
+ end
116
+ end
117
+
118
+ def canonical(name)
119
+ ALIASES.fetch(name.to_sym) { raise InvalidControlError, "unknown control #{name}" }
120
+ end
121
+ end
122
+ end
123
+
124
+ module Cyclotone
125
+ class Pattern
126
+ Controls::CONTROL_DEFS.each_key do |name|
127
+ define_method(name) do |pattern_or_value|
128
+ merge(Controls.factory(name, pattern_or_value))
129
+ end
130
+
131
+ Array(Controls::CONTROL_DEFS[name][:aliases]).each do |alias_name|
132
+ define_method(alias_name) do |pattern_or_value|
133
+ merge(Controls.factory(name, pattern_or_value))
134
+ end
135
+ end
136
+ end
137
+
138
+ def control(name, pattern_or_value)
139
+ merge(Controls.factory(name, pattern_or_value))
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module DSL
5
+ Controls::ALIASES.each_key do |name|
6
+ define_method(name) do |pattern_or_value = nil|
7
+ Controls.public_send(name, pattern_or_value)
8
+ end
9
+ end
10
+
11
+ %i[sine cosine tri saw isaw square rand irand perlin range smooth].each do |name|
12
+ define_method(name) do |*args|
13
+ Oscillators.public_send(name, *args)
14
+ end
15
+ end
16
+
17
+ def stream
18
+ Stream.instance
19
+ end
20
+
21
+ (1..16).each do |index|
22
+ define_method(:"d#{index}") do |pattern = nil, &block|
23
+ target_pattern = block ? block.call : pattern
24
+ stream.d(index, target_pattern)
25
+ end
26
+ end
27
+
28
+ def p(name, pattern = nil, &block)
29
+ target_pattern = block ? block.call : pattern
30
+ stream.p(name, target_pattern)
31
+ end
32
+
33
+ def hush
34
+ stream.hush
35
+ end
36
+
37
+ def setcps(value)
38
+ stream.setcps(value)
39
+ end
40
+
41
+ def reset_cycles
42
+ stream.reset_cycles
43
+ end
44
+
45
+ def set_cycle(value)
46
+ stream.set_cycle(value)
47
+ end
48
+
49
+ def trigger
50
+ stream.trigger
51
+ end
52
+
53
+ def qtrigger
54
+ stream.qtrigger
55
+ end
56
+
57
+ def mtrigger(period)
58
+ stream.mtrigger(period)
59
+ end
60
+
61
+ def xfade(id, pattern)
62
+ stream.xfade(id, pattern)
63
+ end
64
+
65
+ def xfade_in(id, cycles, pattern)
66
+ stream.xfade_in(id, cycles, pattern)
67
+ end
68
+
69
+ def clutch(id, pattern)
70
+ stream.clutch(id, pattern)
71
+ end
72
+
73
+ def clutch_in(id, cycles, pattern)
74
+ stream.clutch_in(id, cycles, pattern)
75
+ end
76
+
77
+ def interpolate(id, pattern)
78
+ stream.interpolate(id, pattern)
79
+ end
80
+
81
+ def interpolate_in(id, cycles, pattern)
82
+ stream.interpolate_in(id, cycles, pattern)
83
+ end
84
+
85
+ def jump(id, pattern)
86
+ stream.jump(id, pattern)
87
+ end
88
+
89
+ def jump_in(id, cycles, pattern)
90
+ stream.jump_in(id, cycles, pattern)
91
+ end
92
+
93
+ def anticipate(id, pattern)
94
+ stream.anticipate(id, pattern)
95
+ end
96
+
97
+ def solo(id)
98
+ stream.solo(id)
99
+ end
100
+
101
+ def unsolo(id)
102
+ stream.unsolo(id)
103
+ end
104
+
105
+ def mute(id)
106
+ stream.mute(id)
107
+ end
108
+
109
+ def unmute(id)
110
+ stream.unmute(id)
111
+ end
112
+
113
+ def fade_in(cycles)
114
+ stream.fade_in(cycles)
115
+ end
116
+
117
+ def fade_out(cycles)
118
+ stream.fade_out(cycles)
119
+ end
120
+
121
+ def start
122
+ stream.start
123
+ end
124
+
125
+ def stop
126
+ stream.stop
127
+ end
128
+
129
+ def chord(name, root: 0)
130
+ Harmony.chord(name, root: root)
131
+ end
132
+
133
+ def scale(name, pattern, root: 0)
134
+ Harmony.scale(name, pattern, root: root)
135
+ end
136
+
137
+ def running?
138
+ stream.running?
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ class Error < StandardError; end
5
+
6
+ class ParseError < Error
7
+ attr_reader :line, :column
8
+
9
+ def initialize(message, line: nil, column: nil)
10
+ @line = line
11
+ @column = column
12
+
13
+ super(format_message(message))
14
+ end
15
+
16
+ private
17
+
18
+ def format_message(message)
19
+ return message unless line && column
20
+
21
+ "#{message} at line #{line}, column #{column}"
22
+ end
23
+ end
24
+
25
+ class ConnectionError < Error; end
26
+ class InvalidControlError < Error; end
27
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Euclidean
5
+ module_function
6
+
7
+ def generate(pulses, steps, rotation = 0)
8
+ normalized_pulses = pulses.to_i
9
+ normalized_steps = steps.to_i
10
+
11
+ raise ArgumentError, "steps must be positive" if normalized_steps <= 0
12
+ raise ArgumentError, "pulses must be between 0 and steps" if normalized_pulses.negative? || normalized_pulses > normalized_steps
13
+
14
+ pattern = bjorklund(normalized_pulses, normalized_steps)
15
+ pattern = pattern.rotate(pattern.index(true) || 0)
16
+ rotate(pattern, rotation)
17
+ end
18
+
19
+ def bjorklund(pulses, steps)
20
+ return Array.new(steps, false) if pulses.zero?
21
+ return Array.new(steps, true) if pulses == steps
22
+
23
+ counts = []
24
+ remainders = [pulses]
25
+ divisor = steps - pulses
26
+ level = 0
27
+
28
+ while remainders[level] > 1
29
+ counts << (divisor / remainders[level])
30
+ remainders << (divisor % remainders[level])
31
+ divisor = remainders[level]
32
+ level += 1
33
+ end
34
+
35
+ counts << divisor
36
+
37
+ pattern = []
38
+ build(level, counts, remainders, pattern)
39
+ pattern.take(steps)
40
+ end
41
+ private_class_method :bjorklund
42
+
43
+ def build(level, counts, remainders, pattern)
44
+ case level
45
+ when -1
46
+ pattern << false
47
+ when -2
48
+ pattern << true
49
+ else
50
+ counts[level].times { build(level - 1, counts, remainders, pattern) }
51
+ build(level - 2, counts, remainders, pattern) unless remainders[level].zero?
52
+ end
53
+ end
54
+ private_class_method :build
55
+
56
+ def rotate(pattern, rotation)
57
+ return pattern if pattern.empty?
58
+
59
+ normalized_rotation = rotation.to_i % pattern.length
60
+
61
+ pattern.rotate(normalized_rotation)
62
+ end
63
+ private_class_method :rotate
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ class Event
5
+ attr_reader :whole, :part, :value
6
+
7
+ def initialize(whole:, part:, value:)
8
+ @whole = whole
9
+ @part = part
10
+ @value = value
11
+ freeze
12
+ end
13
+
14
+ def onset
15
+ whole&.start
16
+ end
17
+
18
+ def offset
19
+ whole&.stop
20
+ end
21
+
22
+ def triggered?
23
+ return false unless onset
24
+
25
+ part.includes?(onset)
26
+ end
27
+
28
+ def duration
29
+ whole&.duration
30
+ end
31
+
32
+ def has_whole?
33
+ !whole.nil?
34
+ end
35
+
36
+ def active_span
37
+ whole || part
38
+ end
39
+
40
+ def covers_time?(time)
41
+ active_span.includes?(time)
42
+ end
43
+
44
+ def with_value(new_value)
45
+ self.class.new(whole: whole, part: part, value: new_value)
46
+ end
47
+
48
+ def with_span(new_whole: whole, new_part: part)
49
+ self.class.new(whole: new_whole, part: new_part, value: value)
50
+ end
51
+
52
+ def ==(other)
53
+ other.is_a?(self.class) &&
54
+ whole == other.whole &&
55
+ part == other.part &&
56
+ value == other.value
57
+ end
58
+
59
+ alias eql? ==
60
+
61
+ def hash
62
+ [self.class, whole, part, value].hash
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Harmony
5
+ NOTE_OFFSETS = {
6
+ "c" => 0, "cs" => 1, "db" => 1, "d" => 2, "ds" => 3, "eb" => 3, "e" => 4,
7
+ "f" => 5, "fs" => 6, "gb" => 6, "g" => 7, "gs" => 8, "ab" => 8, "a" => 9,
8
+ "as" => 10, "bb" => 10, "b" => 11
9
+ }.freeze
10
+
11
+ SCALES = {
12
+ major: [0, 2, 4, 5, 7, 9, 11],
13
+ minor: [0, 2, 3, 5, 7, 8, 10],
14
+ dorian: [0, 2, 3, 5, 7, 9, 10],
15
+ phrygian: [0, 1, 3, 5, 7, 8, 10],
16
+ lydian: [0, 2, 4, 6, 7, 9, 11],
17
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
18
+ locrian: [0, 1, 3, 5, 6, 8, 10],
19
+ harmonic_minor: [0, 2, 3, 5, 7, 8, 11],
20
+ melodic_minor: [0, 2, 3, 5, 7, 9, 11],
21
+ whole_tone: [0, 2, 4, 6, 8, 10],
22
+ chromatic: (0..11).to_a,
23
+ pentatonic_major: [0, 2, 4, 7, 9],
24
+ pentatonic_minor: [0, 3, 5, 7, 10],
25
+ blues: [0, 3, 5, 6, 7, 10],
26
+ egyptian: [0, 2, 5, 7, 10],
27
+ hirajoshi: [0, 2, 3, 7, 8],
28
+ iwato: [0, 1, 5, 6, 10],
29
+ enigmatic: [0, 1, 4, 6, 8, 10, 11],
30
+ neapolitan_major: [0, 1, 3, 5, 7, 9, 11],
31
+ neapolitan_minor: [0, 1, 3, 5, 7, 8, 11]
32
+ }.freeze
33
+
34
+ CHORDS = {
35
+ major: [0, 4, 7],
36
+ minor: [0, 3, 7],
37
+ diminished: [0, 3, 6],
38
+ augmented: [0, 4, 8],
39
+ sus2: [0, 2, 7],
40
+ sus4: [0, 5, 7],
41
+ major7: [0, 4, 7, 11],
42
+ minor7: [0, 3, 7, 10],
43
+ dominant7: [0, 4, 7, 10]
44
+ }.freeze
45
+
46
+ module_function
47
+
48
+ def scale(name, pattern, root: 0)
49
+ intervals = SCALES.fetch(name.to_sym)
50
+ root_note = note_number(root)
51
+
52
+ Pattern.ensure_pattern(pattern).fmap do |value|
53
+ apply_scale(intervals, root_note, value)
54
+ end
55
+ end
56
+
57
+ def chord(name, root: 0)
58
+ root_note = note_number(root)
59
+ notes = CHORDS.fetch(name.to_sym) { raise ArgumentError, "unknown chord #{name}" }.map do |interval|
60
+ root_note + interval
61
+ end
62
+
63
+ Pattern.pure(notes)
64
+ end
65
+
66
+ def arpeggiate(pattern, mode: :up)
67
+ Pattern.ensure_pattern(pattern).flat_map_events do |event|
68
+ notes = extract_notes(event.value)
69
+ next [event] if notes.empty?
70
+
71
+ ordered = order_notes(notes, mode)
72
+ segment_length = event.part.duration / ordered.length
73
+
74
+ ordered.each_with_index.map do |note, index|
75
+ part = TimeSpan.new(
76
+ event.part.start + (segment_length * index),
77
+ event.part.start + (segment_length * (index + 1))
78
+ )
79
+
80
+ Event.new(whole: part, part: part, value: note)
81
+ end
82
+ end
83
+ end
84
+
85
+ def note_number(value)
86
+ return value.to_i if value.is_a?(Numeric)
87
+
88
+ normalized = value.to_s.strip.downcase
89
+ match = normalized.match(/\A([a-g](?:s|b)?)(-?\d+)\z/)
90
+ return normalized.to_i if match.nil?
91
+
92
+ NOTE_OFFSETS.fetch(match[1]) + ((match[2].to_i + 1) * 12)
93
+ end
94
+
95
+ def apply_scale(intervals, root_note, value)
96
+ if value.is_a?(Hash) && value.key?(:note)
97
+ value.merge(note: map_degree(intervals, root_note, value[:note]))
98
+ else
99
+ map_degree(intervals, root_note, value)
100
+ end
101
+ end
102
+ private_class_method :apply_scale
103
+
104
+ def map_degree(intervals, root_note, value)
105
+ degree = note_number(value)
106
+ octave, index = degree.divmod(intervals.length)
107
+ root_note + intervals[index] + (octave * 12)
108
+ end
109
+ private_class_method :map_degree
110
+
111
+ def extract_notes(value)
112
+ return value[:note] if value.is_a?(Hash) && value[:note].is_a?(Array)
113
+ return value if value.is_a?(Array)
114
+
115
+ []
116
+ end
117
+ private_class_method :extract_notes
118
+
119
+ def order_notes(notes, mode)
120
+ case mode.to_sym
121
+ when :down
122
+ notes.reverse
123
+ when :updown
124
+ notes + notes[1...-1].reverse
125
+ when :converge
126
+ left = notes.each_slice(2).map(&:first)
127
+ right = notes.each_slice(2).map(&:last).compact.reverse
128
+ left.zip(right).flatten.compact
129
+ else
130
+ notes
131
+ end
132
+ end
133
+ private_class_method :order_notes
134
+ end
135
+ end
136
+
137
+ module Cyclotone
138
+ class Pattern
139
+ def up(semitones)
140
+ fmap do |value|
141
+ if value.is_a?(Hash) && value.key?(:note)
142
+ value.merge(note: value[:note] + semitones.to_i)
143
+ else
144
+ value + semitones.to_i
145
+ end
146
+ end
147
+ end
148
+
149
+ def scale(name, root: 0)
150
+ Harmony.scale(name, self, root: root)
151
+ end
152
+
153
+ def arp(mode = :up)
154
+ Harmony.arpeggiate(self, mode: mode)
155
+ end
156
+
157
+ alias arpeggiate arp
158
+ end
159
+ end