forked from eegsynth/eegsynth
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgenerateclock.py
More file actions
executable file
·349 lines (286 loc) · 11.7 KB
/
Copy pathgenerateclock.py
File metadata and controls
executable file
·349 lines (286 loc) · 11.7 KB
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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/usr/bin/env python
# This software is part of the EEGsynth project, see <https://github.com/eegsynth/eegsynth>.
#
# Copyright (C) 2017-2022 EEGsynth project
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
import mido
from fuzzywuzzy import process
import numpy as np
import os
import sys
import threading
import time
if hasattr(sys, 'frozen'):
path = os.path.split(sys.executable)[0]
file = os.path.split(sys.executable)[-1]
name = os.path.splitext(file)[0]
elif __name__ == '__main__' and sys.argv[0] != '':
path = os.path.split(sys.argv[0])[0]
file = os.path.split(sys.argv[0])[-1]
name = os.path.splitext(file)[0]
elif __name__ == '__main__':
path = os.path.abspath('')
file = os.path.split(path)[-1] + '.py'
name = os.path.splitext(file)[0]
else:
path = os.path.split(__file__)[0]
file = os.path.split(__file__)[-1]
name = os.path.splitext(file)[0]
# eegsynth/lib contains shared modules
sys.path.insert(0, os.path.join(path, '../../lib'))
import EEGsynth
def find_nearest_value(list, value):
# find the value in the list that is the nearest to the desired value
return min(list, key=lambda x: abs(x - value))
class ClockThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.running = True
self.rate = 60 # the rate is in bpm, i.e. quarter notes per minute
def setRate(self, rate):
if rate != self.rate:
with lock:
self.rate = rate
def stop(self):
self.running = False
def run(self):
slip = 0
while self.running:
monitor.debug('clock beat')
start = time.time()
delay = 60 / self.rate # the rate is in bpm
delay -= slip # correct for the slip from the previous iteration
jiffy = delay / 24
for tick in range(24):
clock[tick].set()
clock[tick].clear()
if jiffy > 0:
time.sleep(jiffy)
# the actual time used in this loop will be slightly different than desired
# this will be corrected on the next iteration
slip = time.time() - start - delay
class MidiThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.running = True
self.enabled = False
def setEnabled(self, enabled):
self.enabled = enabled
def stop(self):
self.enables = False
self.running = False
def run(self):
msg = mido.Message('clock')
while self.running:
if self.enabled and midiport:
monitor.debug('midi beat')
for tick in clock:
tick.wait()
midiport.send(msg)
else:
time.sleep(patch.getfloat('general', 'delay'))
class RedisThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.running = True
self.enabled = False
self.ppqn = 1 # this determines how many messages are sent per quarter note
self.shift = 0 # this determines by how many ticks the Redis message is shifted
# it will send a message on the selected clock ticks
self.clock = [0]
self.key = "{}.note".format(patch.getstring('output', 'prefix'))
def setPpqn(self, ppqn):
if ppqn != self.ppqn:
with lock:
self.ppqn = ppqn
self.clock = np.mod(
np.arange(0, 24, 24 / self.ppqn) + self.shift, 24).astype(int)
monitor.info("redis select = " + str(self.clock))
def setShift(self, shift):
if shift != self.shift:
with lock:
self.shift = shift
self.clock = np.mod(
np.arange(0, 24, 24 / self.ppqn) + self.shift, 24).astype(int)
monitor.info("redis select = " + str(self.clock))
def setEnabled(self, enabled):
self.enabled = enabled
def stop(self):
self.enabled = False
self.running = False
def run(self):
while self.running:
if self.enabled:
monitor.debug('redis beat')
for tick in [clock[indx] for indx in self.clock]:
tick.wait()
patch.setvalue(self.key, 1.)
else:
time.sleep(patch.getfloat('general', 'delay'))
def _setup():
"""Initialize the module
This adds a set of global variables
"""
global patch, name, path, monitor
# configure and start the patch, this will parse the command-line arguments and the ini file
patch = EEGsynth.patch(name=name, path=path)
# this shows the splash screen and can be used to track parameters that have changed
monitor = EEGsynth.monitor(name=name, debug=patch.getint('general', 'debug', default=1))
# there should not be any local variables in this function, they should all be global
if len(locals()):
print("LOCALS: " + ", ".join(locals().keys()))
def _start():
"""Start the module
This uses the global variables from setup and adds a set of global variables
"""
global patch, name, path, monitor
global stepsize, scale_rate, offset_rate, scale_shift, offset_shift, scale_ppqn, offset_ppqn, lock, clock, i, clockthread, midithread, redisthread, midiport, previous_midi_play, previous_midi_start, previous_redis_play
# get the options from the configuration file
stepsize = patch.getfloat('general', 'delay')
# the scale and offset are used to map the Redis values to internal values
scale_rate = patch.getfloat('scale', 'rate')
offset_rate = patch.getfloat('offset', 'rate')
scale_shift = patch.getfloat('scale', 'shift')
offset_shift = patch.getfloat('offset', 'shift')
scale_ppqn = patch.getfloat('scale', 'ppqn')
offset_ppqn = patch.getfloat('offset', 'ppqn')
# this is to prevent two threads accesing a variable at the same time
lock = threading.Lock()
# this is to synchronize the clocks
clock = []
for i in range(0, 24):
clock.append(threading.Event())
# create and start the thread that manages the clock
clockthread = ClockThread()
clockthread.start()
# create and start the thread for the MIDI output
midithread = MidiThread()
midithread.start()
# create and start the thread for the Redis output
redisthread = RedisThread()
redisthread.start()
# the MIDI interface will only be started when needed
midiport = None
previous_midi_play = None
previous_midi_start = None
previous_redis_play = None
# there should not be any local variables in this function, they should all be global
if len(locals()):
print("LOCALS: " + ", ".join(locals().keys()))
def _loop_once():
"""Run the main loop once
This uses the global variables from setup and start, and adds a set of global variables
"""
global patch, name, path, monitor
global stepsize, scale_rate, offset_rate, scale_shift, offset_shift, scale_ppqn, offset_ppqn, lock, clock, i, clockthread, midithread, redisthread, midiport, previous_midi_play, previous_midi_start, previous_redis_play
global start, redis_play, midi_play, midi_start, rate, shift, ppqn, elapsed, naptime
redis_play = patch.getint('redis', 'play')
midi_play = patch.getint('midi', 'play')
midi_start = patch.getint('midi', 'start')
if previous_redis_play is None and redis_play is not None:
previous_redis_play = not(redis_play)
if previous_midi_play is None and midi_play is not None:
previous_midi_play = not(midi_play)
if previous_midi_start is None and midi_start is not None:
previous_midi_start = not(midi_start)
# the MIDI port should only be opened once, and only if needed
if midi_play and midiport == None:
mididevice = patch.getstring('midi', 'device')
mididevice = EEGsynth.trimquotes(mididevice)
mididevice = process.extractOne(mididevice, mido.get_output_names())[0] # select the closest match
try:
midiport = mido.open_output(mididevice)
monitor.success('Connected to MIDI output')
except:
raise RuntimeError("cannot connect to MIDI output")
# do something whenever the value changes
if redis_play and not previous_redis_play:
redisthread.setEnabled(True)
previous_redis_play = True
elif not redis_play and previous_redis_play:
redisthread.setEnabled(False)
previous_redis_play = False
# do something whenever the value changes
if midi_play and not previous_midi_play:
midithread.setEnabled(True)
previous_midi_play = True
elif not midi_play and previous_midi_play:
midithread.setEnabled(False)
previous_midi_play = False
# do something whenever the value changes
if midi_start and not previous_midi_start:
if midiport != None:
midiport.send(mido.Message('start'))
previous_midi_start = True
elif not midi_start and previous_midi_start:
if midiport != None:
midiport.send(mido.Message('stop'))
previous_midi_start = False
rate = patch.getfloat('input', 'rate', default=0)
rate = EEGsynth.rescale(rate, slope=scale_rate, offset=offset_rate)
rate = EEGsynth.limit(rate, 30., 240.)
shift = patch.getfloat('input', 'shift', default=0)
shift = EEGsynth.rescale(shift, slope=scale_shift, offset=offset_shift)
shift = int(shift)
ppqn = patch.getfloat('input', 'ppqn', default=0)
ppqn = EEGsynth.rescale(ppqn, slope=scale_ppqn, offset=offset_ppqn)
ppqn = find_nearest_value([1, 2, 3, 4, 6, 8, 12, 24], ppqn)
# show the parameters whose value has changed
monitor.update("redis_play", redis_play)
monitor.update("midi_play", midi_play)
monitor.update("midi_start", midi_start)
monitor.update("rate", rate)
monitor.update("shift", shift)
monitor.update("ppqn", ppqn)
# update the clock and redis
clockthread.setRate(rate)
redisthread.setShift(shift)
redisthread.setPpqn(ppqn)
def _loop_forever():
"""Run the main loop forever
"""
global monitor, stepsize, elapsed, naptime
while True:
# measure the time to correct for the slip
start = time.time()
monitor.loop()
_loop_once()
# correct for the slip
elapsed = time.time() - start
naptime = stepsize - elapsed
if naptime > 0:
monitor.trace("naptime = " + str(naptime))
time.sleep(naptime)
def _stop():
"""Stop and clean up on SystemExit, KeyboardInterrupt, RuntimeError
"""
global monitor, midithread, redisthread, clockthread
monitor.success('Closing threads')
# the thread that manages the clock should be stopped the last
midithread.stop()
midithread.join()
redisthread.stop()
redisthread.join()
clockthread.stop()
clockthread.join()
if __name__ == "__main__":
_setup()
_start()
try:
_loop_forever()
except (SystemExit, KeyboardInterrupt, RuntimeError):
_stop()
sys.exit()