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,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
|