-
Notifications
You must be signed in to change notification settings - Fork 87
/
Copy pathegressus_melodiam.py
973 lines (830 loc) · 39.3 KB
/
egressus_melodiam.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
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
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
from europi import *
import machine
from time import ticks_diff, ticks_ms
from random import uniform
from europi_script import EuroPiScript
from europi_config import EuroPiConfig
import gc
import math
import framebuf
"""
Egressus Melodiam (Stepped Melody)
author: Nik Ansell (github.com/gamecat69)
date: 19-Feb-24
labels: clocked lfo, sequencer, CV, randomness
Known Issues:
- When the clock rate or output division changes significantly this creates temporary wave shape discontinuities
- Input clocks > 150BPM can cause waveshape glitches. Workaround this by dividing the clock first if working at 150BPM or higher.
- Some input clocks < 16ms can be occasionally missed causing glitchy waves to he output or CV pattern steps to be missed.
Workaround this issue using longer clock pulse durations or 50% duty cycle square waves.
- Performance is affected when using the controls - the poor pico has trouble keeping up with the not-yet optimized code
Possible future work:
- Med [UX]: When in unclocked mode the pattern length cannot be changed - change UI? Or maybe restrict pattern length to 1 (LFO mode only in unclocked mode?)
- Low [FEATURE]: Find a use for ain? Change unClocked clock time?
- High [PERF]: See if performance can be optimized to work with clock inputs < 16ms
Programming notes:
Outputs -> CVPattern Banks -> CV Patterns data structure:
self.cvPatternBanks[idx][self.CvPattern][step]
cvPatternBank[idx] - Output: Each list item is a reference to an output
[maxCvPatterns] - Cv Pattern Banks: Each list item is a reference to a CV Pattern
[maxStepLength] - Cv Patterns: Each list item is a CV value per clock step
Slew / interpolation generator structures:
CV shapes are created using interpolation formulas that fill in the gaps between CV step values in a CV pattern.
Each interpolation formula is generated by an associated slew shape function.
There are both linear and non-linear interpolation functions that create various smooth and no-so-smooth shapes.
Each slew shape function generates an array of values (samples) between two given points.
Samples are output on the CV outputs based on the list of pre-computed interpolated values (sample buffers)
A value from the sample buffer is retrieved using a Python generator function (self.slewGenerator()).
One instance of the generator function for each each slew shape exists in a list: self.slewGeneratorObjects[]
A new sample buffer (array of interpolated values) is created at each clock step.
The number of samples required in each sample buffer (one for each output) is calculated at each clock step or if
the clock rate or output division changes.
The timing and associated hanlding of input clock interrupts (clocks to din) is not perfect - python/micropython is not
known to be great at sub-second timing!
Therefore, at the end of each clock step the algorithm tunes itself to work around sample buffer overruns or underruns.
If there is a buffer underrun (not enough samples in the buffer), the previous output voltage (sample) is used until the algorithm
catches back up with itself.
slewGeneratorObjects[] contains 6 object (one for each output)
Each slewGeneratorObjects[] object is a reference to a copy of the self.slewGenerator() function.
The self.slewGenerator() function receives a buffer filled with samples and yields one sample each time it is called using next()
In order to maintain the best balance of smooth waves and Rpi pico memory usage, an algorithm is used to vary the
sample rate automatically based on the selected output division.
This causes the sample rate to be at minium when there is a slow clock and high output division.
Conversely, when there is a fast clock and low output division a higher sample rate is used to avoid unwanted steps
often caused by under-samping a wave form.
"""
# Minimum allowed time between incoming 16th note clocks. Smaller values are too fast for poor ole micropython to keep up
MIN_CLOCK_TIME_MS = 50
# Maximum allowed time between incoming 16th note clocks. This is 2 BPM, larger value causes memory issues unless the next two vals are halfed
MAX_CLOCK_TIME_MS = 3750
# Max sample rate and clock divisor to keep required buffer sizes within memory limits
MAX_SAMPLE_RATE = 32
MAX_OUTPUT_DENOMINATOR = 8
MIN_MS_BETWEEN_SAVES = 2000
# Calculate maximum sample buffer size required
SLEW_BUFFER_SIZE_IN_SAMPLES = int(
(MAX_CLOCK_TIME_MS / 1000) * MAX_SAMPLE_RATE * MAX_OUTPUT_DENOMINATOR
)
# Reduce knob hysteresis using this value - Mutable Instruments style
KNOB_CHANGE_TOLERANCE = 0.999
# Set the maximum CV voltage using a global config value
# Important: Needs firmware v0.12.1 or higher
MAX_CV_VOLTAGE = europi_config.MAX_OUTPUT_VOLTAGE
MAX_STEP_LENGTH = 32
# Diff between incoming clocks are stored in the FiFo buffer and averaged
# Averaging over 5 values seems to deal with wonky clocks quite well
CLOCK_DIFF_BUFFER_LEN = 5
# If the clock rate changes more than this, trigger a recalculation.
# Avoids wonky waves when wonky clocks are used.
MIN_CLOCK_CHANGE_DETECTION_MS = 100
# Slightly quicker way to get integers from boolean values
BOOL_DICT = {False: 0, True: 1}
# Wave shape bit arrays
WAVE_SHAPE_IMGS = [
bytearray(
b"\xfe\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x82\x10\x83\xf0"
), # stepUpStepDown
bytearray(
b"\x00\x00\x06\x00\x05\x00\t\x00\t\x00\x10\x80\x10\x80 @ @@ @ \x80\x10"
), # linspace (tri)
bytearray(b"0\x00(\x10D\x10D\x10D D\x10\x82 \x82 \x82 \x82 \x81@\x01\xc0"), # smooth (sine)
bytearray(
b"\x04\x00\x04\x00\x06\x00\x06\x00\n\x00\t\x00\t\x00\x10\x80 \x80 @@ \x80\x10"
), # expUpexpDown
bytearray(
b'\x0c\x00\x12\x00\x12\x00"\x00"\x00A\x00A\x00@\x80@\x80\x80@\x80 \x80\x10'
), # sharkTooth
bytearray(
b"\x04\x00\x05\x00\x04\x80\x08\x80\x08@\x08@\x10 \x10 \x10 @\x10\x80\x00"
), # sharkToothReverse
bytearray(
b"\x03\xf0\x0c\x100\x10 \x10@\x10@\x10@\x10\x80\x10\x80\x10\x80\x10\x80\x10\x80\x10"
), # logUpStepDown
bytearray(
b"\xff\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80@\x80@\x80 \x80\x10"
), # stepUpExpDown
]
class EgressusMelodiam(EuroPiScript):
def __init__(self):
# Initialize variables
self.newClockToProcess = False
self.clockStep = 0
self.stepPerOutput = [0, 0, 0, 0, 0, 0]
self.nextStepPerOutput = [0, 0, 0, 0, 0, 0]
self.CvPattern = 0
self.resetTimeout = MAX_CLOCK_TIME_MS
self.screenRefreshNeeded = True
self.showNewPatternIndicator = False
self.showNewPatternIndicatorClockStep = 0
self.numCvPatterns = 1 # Leave at 1 due to memory limitations
self.maxCvPatterns = 1 # Leave at 1 due to memory limitations
self.slewArray = []
self.lastClockTime = 0
self.lastSlewVoltageOutputTime = [0, 0, 0, 0, 0, 0]
self.slewGeneratorObjects = [
self.slewGenerator([0]),
self.slewGenerator([0]),
self.slewGenerator([0]),
self.slewGenerator([0]),
self.slewGenerator([0]),
self.slewGenerator([0]),
]
self.slewShapes = [
self.stepUpStepDown,
self.linspace,
self.smooth,
self.expUpexpDown,
self.sharkTooth,
self.sharkToothReverse,
self.logUpStepDown,
self.stepUpExpDown,
]
self.voltageExtremes = [0, MAX_CV_VOLTAGE]
self.outputVoltageFlipFlops = [
True,
True,
True,
True,
True,
True,
] # Flipflops between self.VoltageExtremes for LFO mode
self.selectedOutput = 0
self.lastK1Reading = 0
self.currentK1Reading = 0
self.lastK2Reading = 0
self.currentK2Reading = 0
self.running = False
self.bufferUnderrunCounter = [0, 0, 0, 0, 0, 0]
self.bufferOverrunSamples = [0, 0, 0, 0, 0, 0]
self.samplesPerSec = [0, 0, 0, 0, 0, 0]
self.msBetweenSamples = [0, 0, 0, 0, 0, 0]
self.unClockedMode = False
self.lastClockTime = ticks_ms()
self.lastSaveState = ticks_ms()
self.pendingSaveState = False
self.previousOutputVoltage = [0, 0, 0, 0, 0, 0]
self.slewBufferSampleNum = [0, 0, 0, 0, 0, 0]
self.slewBufferPosition = [0, 0, 0, 0, 0, 0]
self.bufferSampleOffsets = [0, 0, 0, 0, 0, 0]
self.squareOutputs = [0, 0, 0, 0, 0, 0]
self.loadState()
# pre-create slew buffers to avoid memory allocation errors
self.initSlewBuffers()
# Initialize inputClockDiffs using previous self.msBetweenClocks from loadState()
self.inputClockDiffs = []
# Init clock diff buffer with the default or saved value
for n in range(CLOCK_DIFF_BUFFER_LEN):
self.inputClockDiffs.append(self.msBetweenClocks)
self.averageMsBetweenClocks = self.average(self.inputClockDiffs)
# Clock rate or output division changed, recalculate optimal sample rate
self.calculateOptimalSampleRate()
# -----------------------------
# Interupt Handling functions
# -----------------------------
@din.handler
def clockTrigger():
"""Triggered on each rising edge into digital input. Sets running flag to true.
Sets a flag to tell main() to process the clock step. Ignored in unclocked mode."""
self.running = True
if not self.unClockedMode:
self.newClockToProcess = True
@b1.handler_falling
def b1Pressed():
"""Triggered when B1 is pressed and released"""
if (
ticks_diff(ticks_ms(), b1.last_pressed()) > 2000
and ticks_diff(ticks_ms(), b1.last_pressed()) < 5000
):
# long press generate new CV pattern
self.generateNewRandomCVPattern(new=False, activePatternOnly=True)
self.showNewPatternIndicator = True
self.screenRefreshNeeded = True
self.showNewPatternIndicatorClockStep = self.clockStep
self.pendingSaveState = True
#self.saveState()
else:
# short press change slew mode
self.outputSlewModes[self.selectedOutput] = (
self.outputSlewModes[self.selectedOutput] + 1
) % len(self.slewShapes)
self.screenRefreshNeeded = True
self.pendingSaveState = True
#self.saveState()
@b2.handler_falling
def b2Pressed():
"""Triggered when B2 is pressed and released"""
if (
ticks_diff(ticks_ms(), b2.last_pressed()) > 2000
and ticks_diff(ticks_ms(), b2.last_pressed()) < 5000
):
# long press change to unclocked mode
self.unClockedMode = not self.unClockedMode
if self.unClockedMode:
self.running = True
self.pendingSaveState = True
#self.saveState()
# Update previous knob values to avoid them changing when the mode changes
self.lastK1Reading = self.currentK1Reading
self.lastK2Reading = self.currentK2Reading
self.screenRefreshNeeded = True
else:
# short press change selected output
self.selectedOutput = (self.selectedOutput + 1) % 6
self.screenRefreshNeeded = True
self.pendingSaveState = True
#self.saveState()
def calculateOptimalSampleRate(self):
"""Calculate optimal sample rate for smooth CV output while using minimal memory"""
for idx in range(len(cvs)):
self.samplesPerSec[idx] = int(
min(2 *
(MAX_SAMPLE_RATE / self.outputDivisions[idx])
* (MAX_CLOCK_TIME_MS / self.averageMsBetweenClocks),
MAX_SAMPLE_RATE,
)
)
self.msBetweenSamples[idx] = int(1000 / self.samplesPerSec[idx])
def initSlewBuffers(self):
"""Create slew buffers and fill with zeros"""
self.slewBuffers = []
for n in range(6): # for each output 0-5
self.slewBuffers.append([]) # add new empty list to the buffer list
for m in range(SLEW_BUFFER_SIZE_IN_SAMPLES):
self.slewBuffers[n].append(0)
def average(self, list):
"""Pythonic mean average function"""
myList = list.copy()
return sum(myList) / len(myList)
def initCvPatternBanks(self):
"""Initialize CV pattern banks"""
# Init CV pattern banks, one for each output
self.cvPatternBanks = [[], [], [], [], [], []]
for n in range(self.maxCvPatterns):
self.generateNewRandomCVPattern(self)
return self.cvPatternBanks
def generateNewRandomCVPattern(self, new=True, activePatternOnly=False):
"""Generate new CV pattern for existing bank or create a new bank
@param new If true, create a new pattern/overwrite the existing one. Otherwise re-use the existing pattern
@param activePatternOnly If true, generate pattern for selected output. Otherwise generate pattern for all outputs
@return True if the pattern(s) were successfully generated, otherwise False
"""
# Note: This function is capable of working with multiple pattern banks
# However, due to current memory limitations only one pattern bank is used
try:
gc.collect()
if new:
# new flag provided, create new list
if activePatternOnly:
self.cvPatternBanks[self.selectedOutput].append(
self.generateRandomPattern(MAX_STEP_LENGTH, 0, MAX_CV_VOLTAGE)
)
else:
for pattern in self.cvPatternBanks:
pattern.append(
self.generateRandomPattern(MAX_STEP_LENGTH, 0, MAX_CV_VOLTAGE)
)
else:
# Update existing list
if activePatternOnly:
self.cvPatternBanks[self.selectedOutput][self.CvPattern] = (
self.generateRandomPattern(MAX_STEP_LENGTH, 0, MAX_CV_VOLTAGE)
)
else:
for pattern in self.cvPatternBanks:
pattern[self.CvPattern] = self.generateRandomPattern(
MAX_STEP_LENGTH, 0, MAX_CV_VOLTAGE
)
return True
except Exception:
return False
def generateRandomPattern(self, length, min, max):
"""Generate a random pattern of a desired length containing values between min and max
@param length The length of the desired pattern
@param min The minimum value for pattern elements (inclusive)
@param max The maximum value for pattern elements (inclusive)
@return The generated pattern
"""
self.t = []
for i in range(0, length):
self.t.append(round(uniform(min, max), 3))
return self.t
def main(self):
"""Entry point - main loop. See inline comments for more info"""
while True:
self.updateScreen()
self.getK1Value()
self.getOutputDivision()
if self.newClockToProcess:
# Get the time difference since the last clockTime
newDiffBetweenClocks = min(MAX_CLOCK_TIME_MS, ticks_ms() - self.lastClockTime)
self.lastClockTime = ticks_ms()
# Add time diff between clocks to inputClockDiffs Fifo list, skipping the first clock as we have no reference
if self.clockStep > 0:
self.inputClockDiffs[self.clockStep % CLOCK_DIFF_BUFFER_LEN] = (
newDiffBetweenClocks
)
# Clock rate change detection
if (
self.clockStep >= CLOCK_DIFF_BUFFER_LEN
and abs(newDiffBetweenClocks - self.averageMsBetweenClocks)
> MIN_CLOCK_CHANGE_DETECTION_MS
):
# Update average ms between clocks
self.averageMsBetweenClocks = self.average(self.inputClockDiffs)
# Clock rate or output division changed, recalculate optimal sample rate
self.calculateOptimalSampleRate()
self.handleClockStep()
# Incremenent the clock step
self.clockStep += 1
self.newClockToProcess = False
# Cycle through outputs, process when needed
for idx in range(len(cvs)):
if (
ticks_diff(ticks_ms(), self.lastSlewVoltageOutputTime[idx])
>= self.msBetweenSamples[idx]
and self.running
):
try:
# Do we have a sample in the buffer?
if self.slewBufferPosition[idx] < self.slewBufferSampleNum[idx]:
# Yes, we have a sample, output voltage to match the sample, reset underrun counter and advance position in buffer
# If a square interpolation mode (0) a precalculated value is used
# as the generator function for square waves was really buggy
if self.outputSlewModes[idx] == 0:
v = self.squareOutputs[idx]
else:
v = next(self.slewGeneratorObjects[idx])
cvs[idx].voltage(v)
self.previousOutputVoltage[idx] = v
self.bufferUnderrunCounter[idx] = 0
else:
# We do not have a sample - buffer under run
# Output the previous voltage to keep things as smooth as possible
cvs[idx].voltage(self.previousOutputVoltage[idx])
self.bufferUnderrunCounter[idx] += 1
# Advance the position in the sample/slew buffer
self.slewBufferPosition[idx] += 1
# Update the last sample output time
self.lastSlewVoltageOutputTime[idx] = ticks_ms()
except StopIteration:
continue
# Save state
if self.pendingSaveState and ticks_diff(ticks_ms(), self.lastSaveState) >= MIN_MS_BETWEEN_SAVES:
self.saveState()
self.pendingSaveState = False
# If we are not being clocked, trigger a clock after the configured clock time
if (
self.unClockedMode
and ticks_diff(ticks_ms(), self.lastClockTime) >= self.averageMsBetweenClocks
):
self.running = True
self.lastClockTime = ticks_ms()
self.handleClockStep()
self.clockStep += 1
# If I have been running, then stopped for longer than resetTimeout, reset all steps to 0
if (
not self.unClockedMode
and self.clockStep != 0
and ticks_diff(ticks_ms(), din.last_triggered()) > self.resetTimeout
):
for idx in range(len(cvs)):
self.stepPerOutput[idx] = 0
# Update screen with the upcoming CV pattern
self.screenRefreshNeeded = True
self.pendingSaveState = True
#self.saveState()
self.running = False
for cv in cvs:
cv.off()
self.bufferUnderrunCounter = [0, 0, 0, 0, 0, 0]
self.bufferOverrunSamples = [0, 0, 0, 0, 0, 0]
self.slewBufferPosition = [0, 0, 0, 0, 0, 0]
self.bufferSampleOffsets = [0, 0, 0, 0, 0, 0]
def handleClockStep(self):
"""Advances step and generates new slew voltages to next value in CV pattern"""
# Cycle through outputs and generate slew for each
for idx in range(len(cvs)):
# If the clockstep is a division of the output division
if self.clockStep % (self.outputDivisions[idx]) == 0:
# flip the flip flop value for LFO mode
self.outputVoltageFlipFlops[idx] = not self.outputVoltageFlipFlops[idx]
# Catch buffer over-runs by detecting that not all samples were used in the last cycle
if self.clockStep > CLOCK_DIFF_BUFFER_LEN and not self.unClockedMode:
self.bufferOverrunSamples[idx] = int(
self.slewBufferPosition[idx] - self.slewBufferSampleNum[idx]
)
self.bufferSampleOffsets[idx] = (
self.bufferSampleOffsets[idx] - self.bufferOverrunSamples[idx]
)
else:
self.bufferSampleOffsets[idx] = 0
# Set the target number of samples for the next cycle, factoring in any previous overruns
# Calculate the number of samples needed until the next clock
self.slewBufferSampleNum[idx] = min(
SLEW_BUFFER_SIZE_IN_SAMPLES,
int(
(
(self.averageMsBetweenClocks / 1000)
* self.outputDivisions[idx]
* self.samplesPerSec[idx]
)
- self.bufferSampleOffsets[idx]
),
)
# If length is one, cycle between high and low voltages (traditional LFO)
# Each output uses a its configured slew shape
if self.patternLength == 1:
# If square transition, set next output value to be one of the voltage extremes (flipping each time)
if self.outputSlewModes[idx] == 0:
self.squareOutputs[idx] = self.voltageExtremes[
BOOL_DICT[self.outputVoltageFlipFlops[idx]]
]
else:
self.slewArray = self.slewShapes[self.outputSlewModes[idx]](
self.voltageExtremes[BOOL_DICT[self.outputVoltageFlipFlops[idx]]],
self.voltageExtremes[BOOL_DICT[not self.outputVoltageFlipFlops[idx]]],
self.slewBufferSampleNum[idx],
self.slewBuffers[idx],
)
else:
# If square transition, just output the CV value in the pattern associated with the current step
if self.outputSlewModes[idx] == 0:
self.squareOutputs[idx] = self.cvPatternBanks[idx][self.CvPattern][
self.stepPerOutput[idx]
]
else:
self.slewArray = self.slewShapes[self.outputSlewModes[idx]](
self.cvPatternBanks[idx][self.CvPattern][self.stepPerOutput[idx]],
self.cvPatternBanks[idx][self.CvPattern][self.nextStepPerOutput[idx]],
self.slewBufferSampleNum[idx],
self.slewBuffers[idx],
)
# Update the function object reference to the generator function, passing it the latest slewArray sample buffer
self.slewGeneratorObjects[idx] = self.slewGenerator(self.slewArray)
# Go back to the start of the buffer
self.slewBufferPosition[idx] = 0
# Calculate next steps (indexs in CV patterns)
self.stepPerOutput[idx] = ((self.stepPerOutput[idx] + 1)) % self.patternLength
self.nextStepPerOutput[idx] = ((self.stepPerOutput[idx] + 1)) % self.patternLength
# Hide the shreaded visual indicator after 2 clock steps
if self.clockStep > self.showNewPatternIndicatorClockStep + 2:
self.showNewPatternIndicator = False
def getK1Value(self):
"""Get the k1 value, update params if changed"""
self.currentK1Reading = k1.read_position(100) + 1
if abs(self.currentK1Reading - self.lastK1Reading) > KNOB_CHANGE_TOLERANCE:
# knob has moved
if self.unClockedMode:
# Set clock speed based on k1 value. This calc creates knob increments of 75ms
self.averageMsBetweenClocks = (
self.currentK1Reading * (MAX_CLOCK_TIME_MS / MIN_CLOCK_TIME_MS) / 2
)
# clock rate or output division changed, calculate optimal sample rate
self.calculateOptimalSampleRate()
else:
# Set pattern length
self.patternLength = int((MAX_STEP_LENGTH / 100) * (self.currentK1Reading - 1)) + 1
# Something changed, update screen and save state
self.pendingSaveState = True
#self.saveState()
self.screenRefreshNeeded = True
self.lastK1Reading = self.currentK1Reading
def getOutputDivision(self):
"""Get the output division from k2"""
self.currentK2Reading = k2.read_position(MAX_OUTPUT_DENOMINATOR) + 1
if self.currentK2Reading != self.lastK2Reading:
self.outputDivisions[self.selectedOutput] = k2.read_position(MAX_OUTPUT_DENOMINATOR) + 1
self.screenRefreshNeeded = True
self.lastK2Reading = self.currentK2Reading
# clock rate or output division changed, calculate optimal sample rate
self.calculateOptimalSampleRate()
self.pendingSaveState = True
#self.saveState()
def saveState(self):
"""Save working vars to a save state file"""
self.state = {
"cvPatternBanks": self.cvPatternBanks,
"CvPattern": self.CvPattern,
"outputSlewModes": self.outputSlewModes,
"outputDivisions": self.outputDivisions,
"patternLength": self.patternLength,
"msBetweenClocks": self.msBetweenClocks,
"unClockedMode": self.unClockedMode,
}
self.save_state_json(self.state)
self.lastSaveState = ticks_ms()
def loadState(self):
"""Load a previously saved state, or initialize working vars, then save"""
self.state = self.load_state_json()
self.cvPatternBanks = self.state.get("cvPatternBanks", [])
self.CvPattern = self.state.get("CvPattern", 0)
self.outputSlewModes = self.state.get("outputSlewModes", [0, 0, 0, 0, 0, 0])
self.outputDivisions = self.state.get("outputDivisions", [1, 2, 4, 1, 2, 4])
self.patternLength = self.state.get("patternLength", 8)
self.msBetweenClocks = self.state.get("msBetweenClocks", 976)
self.unClockedMode = self.state.get("unClockedMode", False)
if len(self.cvPatternBanks) == 0:
self.initCvPatternBanks()
self.pendingSaveState = True
#self.saveState()
# Let the rest of the script know how many pattern banks we have
self.numCvPatterns = len(self.cvPatternBanks[0])
def drawWave(self):
"""UI wave visualizations"""
fb = framebuf.FrameBuffer(
WAVE_SHAPE_IMGS[self.outputSlewModes[self.selectedOutput]], 12, 12, framebuf.MONO_HLSB
)
oled.blit(fb, 0, 20)
def updateScreen(self):
"""Update the screen only if something has changed. oled.show() hogs the processor and causes latency."""
# Only update if something has changed
if not self.screenRefreshNeeded:
return
# Clear screen
oled.fill(0)
# Selected output
oled.fill_rect(108, 0, 20, 9, 1)
oled.text(f"{self.selectedOutput + 1}", 115, 1, 0)
# Show division for selected output
number = self.outputDivisions[self.selectedOutput]
x = 111 if number >= 10 else 115
oled.text(f"{number}", x, 12, 1)
# Show wave for selected output
self.drawWave()
if self.unClockedMode:
if self.averageMsBetweenClocks != 0:
oled.text(f"{int(self.averageMsBetweenClocks * 2)} ms", 31, 1, 1)
else:
# Draw pattern length
row1 = ""
row2 = ""
row3 = ""
row4 = ""
if self.patternLength > 24:
# draw two rows
row1 = "........"
row2 = "........"
row3 = "........"
for i in range(24, self.patternLength):
row4 = row4 + "."
elif self.patternLength > 16:
row1 = "........"
row2 = "........"
for i in range(16, self.patternLength):
row3 = row3 + "."
elif self.patternLength > 8:
row1 = "........"
for i in range(8, self.patternLength):
row2 = row2 + "."
else:
# draw one row
for i in range(self.patternLength):
row1 = row1 + "."
xStart = 27
oled.text(row1, xStart, 0, 1)
oled.text(row2, xStart, 6, 1)
oled.text(row3, xStart, 12, 1)
oled.text(row4, xStart, 18, 1)
# Draw a visual cue for when a long button press has been detected
# and a new random pattern is being generated
if self.showNewPatternIndicator:
fb = framebuf.FrameBuffer(
bytearray(b"\x0f\x000\x80N`Q \x94\xa0\xaa\x90\xa9P\xa5@Z\x80H\x803\x00\x0c\x00"),
12,
12,
framebuf.MONO_HLSB,
)
oled.blit(fb, 0, 0)
oled.show()
# -----------------------------
# Slew functions
# -----------------------------
def stepUpStepDown(self, start, stop, num, buffer):
"""Produces step up, step down
@param start Starting value
@param stop Target value
@param num Number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
if self.patternLength == 1: # LFO Mode, make sure we complete a full cycle
for i in range(num / 2):
buffer[c] = start
c += 1
for i in range(num / 2):
buffer[c] = stop
c += 1
else:
for i in range(num - 1):
buffer[c] = stop
c += 1
return buffer
def linspace(self, start, stop, num, buffer):
"""Produces a linear transition
@param start Starting value
@param stop Target value
@param num Number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
num = max(1, num) # avoid divide by zero
diff = (float(stop) - start) / (num)
for i in range(num):
val = (diff * i) + start
buffer[c] = val
c += 1
return buffer
def logUpStepDown(self, start, stop, num, buffer):
"""Produces a log up/step down transition
@param start Starting value
@param stop Target value
@param num Number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
if self.patternLength == 1: # LFO Mode, make sure we complete a full cycle
for i in range(num / 2):
i = max(i, 1)
x = 1 - ((stop - float(start)) / (i)) + (stop - 1)
buffer[c] = x
c += 1
for i in range(num / 2):
buffer[c] = stop
c += 1
else:
if stop >= start:
for i in range(num):
i = max(i, 1)
x = 1 - ((stop - float(start)) / (i)) + (stop - 1)
buffer[c] = x
c += 1
else:
for i in range(num):
buffer[c] = stop
c += 1
return buffer
def stepUpExpDown(self, start, stop, num, buffer):
"""Produces a step up, exponential down transition
@param start Starting value
@param stop Target value
@param num Number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
if stop <= start:
for i in range(num):
i = max(i, 1)
x = 1 - ((stop - float(start)) / (i)) + (stop - 1)
buffer[c] = x
c += 1
else:
for i in range(num):
buffer[c] = stop
c += 1
return buffer
def smooth(self, start, stop, num, buffer):
"""Produces smooth curve using half a cosine wave
@param start Starting value
@param stop Target value
@param num The number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
freqHz = 0.5 # We want to complete half a cycle
amplitude = abs(
(stop - start) / 2
) # amplitude is half of the diff between start and stop (this is peak to peak)
if start <= stop:
# Starting position is 90 degrees (cos) at 'start' volts
startOffset = num
amplitudeOffset = start
else:
# Starting position is 0 degrees (cos) at 'stop' volts
startOffset = 0
amplitudeOffset = stop
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
return buffer
def expUpexpDown(self, start, stop, num, buffer):
"""Produces pointy exponential wave using a quarter cosine up and a quarter cosine down
@param start Starting value
@param stop Target value
@param num The number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
freqHz = 0.25 # We want to complete quarter of a cycle
amplitude = abs(
(stop - start)
) # amplitude is half of the diff between start and stop (this is peak to peak)
if start <= stop:
startOffset = num * 2
amplitudeOffset = start
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
else:
startOffset = num
amplitudeOffset = stop
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
return buffer
def sharkTooth(self, start, stop, num, buffer):
"""Produces a sharktooth wave with an approximate log curve up and approximate
exponential curve down
@param start Starting value
@param stop Target value
@param num The number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
freqHz = 0.25 # We want to complete quarter of a cycle
amplitude = abs(
(stop - start)
) # amplitude is half of the diff between start and stop (this is peak to peak)
if start <= stop:
startOffset = num * 3
amplitudeOffset = start - amplitude
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
else:
startOffset = num
amplitudeOffset = stop
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
return buffer
def sharkToothReverse(self, start, stop, num, buffer):
"""Produces a reverse sharktooth wave with an approximate exponential curve up and approximate
log curve down
@param start Starting value
@param stop Target value
@param num The number of samples required
@param buffer Pointer to fill with samples
@return The edited buffer
"""
c = 0
freqHz = 0.25 # We want to complete quarter of a cycle
amplitude = abs(
(stop - start)
) # amplitude is half of the diff between start and stop (this is peak to peak)
if start <= stop:
startOffset = num * 2
amplitudeOffset = start
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
else:
startOffset = 0
amplitudeOffset = 1 - (amplitude - stop + 1)
for i in range(num):
i += startOffset
val = amplitude + float(
amplitude * math.cos(2 * math.pi * freqHz * i / float(num))
)
buffer[c] = round(val + amplitudeOffset, 4)
c += 1
return buffer
def slewGenerator(self, arr):
"""Generator function that returns the next slew sample from the specified list
@param The list of samples to choose from
"""
for s in range(len(arr)):
yield arr[s]
if __name__ == "__main__":
dm = EgressusMelodiam()
dm.main()