-
Notifications
You must be signed in to change notification settings - Fork 86
/
arp.py
250 lines (196 loc) · 8.78 KB
/
arp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/usr/bin/env python3
"""Arpeggiator & ascending/descending scale generator
Outputs ascending/descending quantized CV, with additional outputs
a perfect fifth up/down (following the circle of fifths)
"""
from europi import *
from europi_script import EuroPiScript
from experimental.quantizer import CommonScales, SEMITONE_LABELS, VOLTS_PER_OCTAVE, VOLTS_PER_SEMITONE, SEMITONES_PER_OCTAVE
from experimental.random_extras import shuffle
import random
MODE_ASCENDING = 0
MODE_DESCENDING = 1
MODE_RANDOM = 2
class Arpeggio:
def __init__(self, scale, mode):
"""Create an arpeggio using the notes from the given scale
@param scale A quantizer scales whose notes will be used
@param mode One of ascending, descending, or random
"""
self.mode = mode
self.change_scale(scale)
def change_scale(self, scale):
"""Change the current scale
@param scale The new quantizer scale we want to use
"""
self.semitones = [
i for i in range(len(scale.notes)) if scale[i]
]
if self.mode == MODE_RANDOM:
# shuffle so we have a chance to choose the last note first
# see @next_node
shuffle(self.semitones)
def next_note(self):
"""Get the next note that should be played
@return The semitone as an integer from 0-12
"""
if self.mode == MODE_ASCENDING:
semitone = self.semitones.pop(0)
self.semitones.append(semitone)
elif self.mode == MODE_DESCENDING:
semitone = self.semitones.pop(-1)
self.semitones.insert(0, semitone)
else:
# never choose the _last_ note in the array
# move the chosen note to the end of the list to avoid repeats
n = random.randint(0, len(self.semitones)-2)
semitone = self.semitones.pop(n)
self.semitones.append(n)
return semitone
def __len__(self):
return len(self.semitones)
class Arpeggiator(EuroPiScript):
def __init__(self):
super().__init__()
## Indicates if the GUI needs to be refreshed
self.ui_dirty = True
## The available scales to be played
self.scales = [
CommonScales.Chromatic,
CommonScales.NatMajor,
CommonScales.HarMajor,
CommonScales.Major135,
CommonScales.Major1356,
CommonScales.Major1357,
CommonScales.NatMinor,
CommonScales.HarMinor,
CommonScales.Minor135,
CommonScales.Minor1356,
CommonScales.Minor1357,
CommonScales.MajorBlues,
CommonScales.MinorBlues,
CommonScales.WholeTone,
CommonScales.Pentatonic,
CommonScales.Dominant7
]
## The active scale within @self.scales
self.current_scale_index = 0
## Have we received an external trigger to be processed?
self.trigger_recvd = False
## Should we change the scale on the next trigger?
self.scale_changed = False
## What octave range are we playing (minimum 1)
self.n_octaves = 1
## What is the current octave we're playing (range [0, n_octaves))
self.current_octave = 0
## What is the root note of our scale (semitone, default C=0)
self.root = 0
## What is the lowest octave we play
self.root_octave = 0
@din.handler
def on_din():
self.trigger_recvd = True
@b1.handler
def on_b1_press():
self.current_scale_index = (self.current_scale_index - 1) % len(self.scales)
self.scale_changed = True
self.ui_dirty = True
@b2.handler
def on_b2_press():
self.current_scale_index = (self.current_scale_index + 1) % len(self.scales)
self.scale_changed = True
self.ui_dirty = True
self.load()
self.set_scale()
def save(self):
state = {
"scale": self.current_scale_index
}
self.save_state_json(state)
def load(self):
settings = self.load_state_json()
self.current_scale_index = settings.get("scale", 0)
def set_scale(self):
# keep track of the number of notes we've played; once we play enough we may need to
# go up/down an octave
self.n_notes_played = 0
# the current root octave for ascending arpeggios
self.current_octave = 0
scale = self.scales[self.current_scale_index]
self.arps = [
Arpeggio(scale, MODE_ASCENDING), # ascending notes, ascending octaves
Arpeggio(scale, MODE_ASCENDING), # ascending notes, descending octaves
Arpeggio(scale, MODE_RANDOM), # random notes, ascending octaves
Arpeggio(scale, MODE_DESCENDING), # descending notes, ascending octaves
Arpeggio(scale, MODE_DESCENDING), # descending notes, descending octaves
Arpeggio(scale, MODE_RANDOM) # random notes, random octaves
]
def tick(self):
# apply the new scale in-sync with the incoming triggers so we don't get out-of-phase changes
if self.scale_changed:
self.scale_changed = False
self.set_scale()
self.save()
# apply the output voltages; each one is slightly unique
# CV1: ascending arpeggio, ascending octaves
volts = (self.root_octave + self.current_octave) * VOLTS_PER_OCTAVE + (self.root + self.arps[0].next_note()) * VOLTS_PER_SEMITONE
cv1.voltage(volts)
# CV2: ascending arpeggio, descending octaves
volts = (self.root_octave + self.n_octaves - self.current_octave - 1) * VOLTS_PER_OCTAVE + (self.root + self.arps[1].next_note()) * VOLTS_PER_SEMITONE
cv2.voltage(volts)
# CV3: random arpeggio, ascending octaves
volts = (self.root_octave + self.current_octave) * VOLTS_PER_OCTAVE + (self.root + self.arps[2].next_note()) * VOLTS_PER_SEMITONE
cv3.voltage(volts)
# CV4: descending arpeggio, ascending octaves
volts = (self.root_octave + self.current_octave) * VOLTS_PER_OCTAVE + (self.root + self.arps[3].next_note()) * VOLTS_PER_SEMITONE
cv4.voltage(volts)
# CV5: descending arpeggio, descending octaves
volts = (self.root_octave + self.n_octaves - self.current_octave - 1) * VOLTS_PER_OCTAVE + (self.root + self.arps[4].next_note()) * VOLTS_PER_SEMITONE
cv5.voltage(volts)
# CV6: random arpeggio, random octave
volts = random.randint(self.root_octave, self.root_octave + self.n_octaves) * VOLTS_PER_OCTAVE + (self.root + self.arps[5].next_note()) * VOLTS_PER_SEMITONE
cv6.voltage(volts)
# Increment the note & octave counter
self.n_notes_played += 1
if self.n_notes_played >= len(self.arps[0]):
self.current_octave += 1
self.n_notes_played = 0
if self.current_octave >= self.n_octaves:
self.current_octave = 0
def main(self):
prev_root_octave = self.root_octave
prev_n_octaves = self.n_octaves
prev_root = self.root
while True:
# Update the desired root, octave, and octave range according to the analogue inputs
self.root_octave = int(k1.percent() * 5)
self.n_octaves = int(k2.percent() * 5) + 1
self.root = int(ain.read_voltage() / VOLTS_PER_SEMITONE) % SEMITONES_PER_OCTAVE
# Update the CV outputs if we've received a clock signal
if self.trigger_recvd:
self.trigger_recvd = False
self.tick()
# Re-render the OLED if needed
# This should only occur if we've pressed a button or the analogue inputs have changed
self.ui_dirty = (self.ui_dirty or
prev_root_octave != self.root_octave or
prev_n_octaves != self.n_octaves or
prev_root != self.root
)
if self.ui_dirty:
self.ui_dirty = False
oled.fill(0)
# OLED displays something like:
# +--------------+
# | F#1-3 | <- Root note, root octave, highest octave (adjust: AIN, K1, K2)
# | Min 1356 | <- Current scale/arpeggio selection (adjust: B1/B2)
# +--------------+
oled.centre_text(f"""{SEMITONE_LABELS[self.root]}{self.root_octave}-{self.root_octave + self.n_octaves - 1}
{self.scales[self.current_scale_index]}""")
oled.show()
# Keep track of previous analogue settings so we can update the GUI again if needed
prev_root_octave = self.root_octave
prev_n_octaves = self.n_octaves
prev_root = self.root
if __name__ == "__main__":
Arpeggiator().main()