This repository has been archived by the owner on Aug 29, 2018. It is now read-only.
/
fabric.py
740 lines (573 loc) · 22.1 KB
/
fabric.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
#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
""" ding dong :: www.hecanjog.com :: (cc) by-nc-sa
"""
import wave
import audioop
import math
import random
import struct
import string
import time
import hashlib
from datetime import datetime
audio_params = [2, 2, 44100, 0, "NONE", "not_compressed"]
snddir = ''
dsp_grain = 64
env_min = 2
cycle_count = 0
thetime = 0
seedint = 0
seedstep = 0
seedhash = ''
def lget(list, index, default=True):
""" Safely return a selected element from a list and handle IndexErrors """
try:
return list[index]
except IndexError:
if default == True:
return list[-1]
else:
return list[0]
def interleave(list_one, list_two):
""" Combine two lists by interleaving their elements """
# find the length of the longest list
if len(list_one) > len(list_two):
big_list = len(list_one)
elif len(list_two) > len(list_one):
big_list = len(list_two)
else:
if randint(0, 1) == 0:
big_list = len(list_one)
else:
big_list = len(list_two)
combined_lists = []
# loop over it and insert alternating items
for index in range(big_list):
if index <= len(list_one) - 1:
combined_lists.append(list_one[index])
if index <= len(list_two) - 1:
combined_lists.append(list_two[index])
return combined_lists
def packet_shuffle(list, packet_size):
""" Shuffle a subset of list items in place.
Takes a list, splits it into sub-lists of size N
and shuffles those sub-lists. Returns flattened list.
"""
if packet_size >= 3 and packet_size <= len(list):
lists = list_split(list, packet_size)
shuffled_lists = []
for sublist in lists:
shuffled_lists.append(randshuffle(sublist))
big_list = []
for shuffled_list in shuffled_lists:
big_list.extend(shuffled_list)
return big_list
def list_split(list, packet_size):
""" Split a list into groups of size N """
trigs = []
for i in range(len(list)):
if i % int(packet_size) == 0:
trigs.append(i)
newlist = []
for trig_index, trig in enumerate(trigs):
if trig_index < len(trigs) - 1:
packets = []
for packet_bit in range(packet_size):
packets.append(list[packet_bit + trig])
newlist.append(packets)
return newlist
def rotate(list, start):
""" Rotate a list by a given offset """
if start > len(list) - 1:
start = len(list) - 1
return list[start:] + list[:start]
def timer(cmd='start'):
""" Counts elapsed time between start and stop events.
Useful for tracking render time. """
global thetime
if cmd == 'start':
thetime = time.time()
print 'Started render at timestamp', thetime
return thetime
elif cmd == 'stop':
thetime = time.time() - thetime
themin = int(thetime) / 60
thesec = thetime - (themin * 60)
print 'Render time:', themin, 'min', thesec, 'sec'
return thetime
def transpose(audio_string, amount):
""" Transpose an audio fragment by a given amount.
1.0 is unchanged, 0.5 is half speed, 2.0 is twice speed, etc """
amount = 1.0 / float(amount)
audio_string = audioop.ratecv(audio_string, audio_params[1], audio_params[0], audio_params[2], int(audio_params[2] * amount), None)
return audio_string[0]
def byte_string(number):
""" Return integer encoded as bytes formatted for wave data """
return struct.pack("<h", number)
def tone(length=44100, freq=440, wavetype='sine2pi', amp=1.0, blocksize=0):
cyclelen = htf(freq * 0.99)
numcycles = length / cyclelen
if blocksize > 0:
numblocks = numcycles / blocksize
if numcycles % blocksize > 0:
numblocks += 1
cycles = ''.join([blocksize * cycle(freq * rand(0.99, 1.0), wavetype, amp) for i in range(numblocks)])
else:
cycles = numcycles * cycle(freq * rand(0.99, 1.0), wavetype, amp)
return cycles
def chirp(numcycles, lfreq, hfreq, length=0, reps=1, etype=False):
# Sweep from low freq to high freq
freqs = wavetable('line', numcycles, hfreq, lfreq)
freqs = [cycle(f) for f in freqs]
out = ''.join(freqs)
# Envelope
if etype is not False:
out = env(out, etype, True)
# Add padding
if length > 0:
out = pad(out, 0, length - flen(out))
# Multiply
out = out * reps
return out
def noise(length):
return ''.join([byte_string(randint(-32768, 32767)) for i in range(length * audio_params[0])])
def cycle(freq, wavetype='sine2pi', amp=1.0):
wavecycle = wavetable(wavetype, htf(freq))
return ''.join([byte_string(cap(amp * s * 32767, 32767, -32768)) * audio_params[0] for s in wavecycle])
def scale(low_target, high_target, low, high, pos):
pos = float(pos - low) / float(high - low)
return pos * float(high_target - low_target) + low_target
def cap(num, max, min=0):
if num < min:
num = min
elif num > max:
num = max
return num
def seed(theseed=False):
global seedint
global seedhash
if theseed == False:
theseed = cycle(440)
h = hashlib.sha1(theseed)
seedhash = h.digest()
seedint = int(''.join([str(ord(c)) for c in list(seedhash)]))
return seedint
def stepseed():
global seedint
global seedstep
h = hashlib.sha1(str(seedint))
seedint = int(''.join([str(ord(c)) for c in list(h.digest())]))
seedstep = seedstep + 1
return seedint
def randint(lowbound=0, highbound=1):
global seedint
if seedint > 0:
return int(rand() * (highbound - lowbound) + lowbound)
else:
return random.randint(lowbound, highbound)
def rand(lowbound=0, highbound=1):
global seedint
if seedint > 0:
return ((stepseed() / 100.0**20) % 1.0) * (highbound - lowbound) + lowbound
else:
return random.random() * (highbound - lowbound) + lowbound
def randchoose(items):
return items[randint(0, len(items)-1)]
def randshuffle(input):
items = input[:]
shuffled = []
for i in range(len(items)):
if len(items) > 0:
item = randchoose(items)
shuffled.append(item)
items.remove(item)
return shuffled
def breakpoint(values, size=512):
""" Takes a list of values, or a pair of wavetable types and values,
and builds an interpolated list of points between each value using
the wavetable type. Default table type is linear. """
# we need at least a start and end point
if len(values) < 2:
values = [ 0.0, ['line', 1.0] ]
# Handle some small size cases
if size == 0:
log('WARNING: breakpoint size 0')
log('values: '+str(values))
log('')
return []
elif size < 4 and size > 0:
log('WARNING: small breakpoint, size ' + str(size))
log('values: '+str(values))
log('')
return [values[0] for i in range(size)]
# Need at least one destination value per point computed
if size < len(values):
values = values[:size]
# Each value produces a group of intermediate points
groups = []
# The size of each group of intermediate points is divded evenly into the target
# size, ignoring the first value and accounting for uneven divisions.
gsize = size / (len(values)-1)
gsizespill = size % (len(values)-1)
# Pretend the first loop shifts the last endval to the startval
try:
if len(values[0]) > 1:
endval = values[0][1]
except TypeError:
endval = values[0]
values.pop(0)
# To build the list of points, loop through each value
for i, v in enumerate(values):
try:
if len(v) > 1:
wtype = v[0]
startval = endval
endval = v[1]
if len(v) == 3:
gsize = gsize * v[2]
except TypeError:
wtype = 'line'
startval = endval
endval = v
# Pad last group with leftover points
if v == values[-1]:
gsize += gsizespill
groups.extend(wavetable(wtype, gsize, endval, startval))
return groups
def wavetable(wtype="sine", size=512, highval=1.0, lowval=0.0):
wtable = []
wave_types = ["sine", "gauss", "cos", "line", "saw", "impulse", "phasor", "sine2pi", "cos2pi", "vary", "flat"]
if wtype == "random":
wtype = wave_types[randint(0, len(wave_types) - 1)]
if wtype == "sine":
wtable = [math.sin(i * math.pi) * (highval - lowval) + lowval for i in frange(size, 1.0, 0.0)]
elif wtype == "gauss":
def gauss(x):
# From: http://johndcook.com/python_phi.html
# Prolly doing it wrong!
a1 = 0.254829592
a2 = -0.284496736
a3 = 1.421413741
a4 = -1.453152027
a5 = 1.061405429
p = 0.3275911
sign = 1
if x < 0:
sign = -1
x = abs(x)/math.sqrt(2.0)
t = 1.0/(1.0 + p * x)
y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * math.exp(-x * x)
return abs(abs(sign * y) - 1.0)
wtable = [gauss(i) * (highval - lowval) + lowval for i in frange(size, 2.0, -2.0)]
elif wtype == "sine2pi":
wtable = [math.sin(i * math.pi * 2) * (highval - lowval) + lowval for i in frange(size, 1.0, 0.0)]
elif wtype == "cos2pi":
wtable = [math.cos(i * math.pi * 2) * (highval - lowval) + lowval for i in frange(size, 1.0, 0.0)]
elif wtype == "cos":
wtable = [math.cos(i * math.pi) * (highval - lowval) + lowval for i in frange(size, 1.0, 0.0)]
elif wtype == "tri":
# Inverse triangle wave, because I'm a dummy. It's late, so it goes.
wtable = [math.fabs(i) for i in frange(size, highval, lowval - highval)] # Only really a triangle wave when centered on zero
elif wtype == "saw" or wtype == "line":
wtable = [i for i in frange(size, highval, lowval)]
elif wtype == "phasor":
wtable = wavetable("line", size, highval, lowval)
list.reverse(wtable)
elif wtype == "impulse":
wtable = [float(randint(-1, 1)) for i in range(size / randint(2, 12))]
wtable.extend([0.0 for i in range(size - len(wtable))])
elif wtype == "vary":
if size < 32:
bsize = size
else:
bsize = size / randint(2, 16)
btable = [ [wave_types[randint(0, len(wave_types)-1)], rand(lowval, highval)] for i in range(bsize) ]
if len(btable) > 0:
btable[0] = lowval
else:
btable = [lowval]
wtable = breakpoint(btable, size)
elif wtype == "flat":
wtable = [highval for i in range(size)]
return wtable
def frange(steps, highval=1.0, lowval=0.0):
if steps == 1:
return [lowval]
return [ (i / float(steps-1)) * (highval - lowval) + lowval for i in range(steps)]
def alias(audio_string, passthru = 0, envelope = 'random', split_size = 0):
if passthru > 0:
return audio_string
if envelope == 'flat':
envelope = False
if split_size == 0:
split_size = dsp_grain / randint(1, dsp_grain)
packets = split(audio_string, split_size)
packets = [p*2 for i, p in enumerate(packets) if i % 2]
out = ''.join(packets)
if envelope:
out = env(out, envelope)
return out
def log(message, mode="a"):
logfile = open("tmplog.txt", mode)
logfile.write(str(message) + "\n")
return logfile.close()
def fill(string, length, chans=2):
if flen(string) < length:
repeats = length / flen(string) + 1
string = string * repeats
return cut(string, 0, length)
def mix(layers, leftalign=True, boost=2.0):
""" mixes N stereo audio strings """
attenuation = 1.0 / len(layers)
attenuation *= boost
layers.sort(key = len)
output_length = flen(layers[-1])
out = pad('', output_length, 0)
for layer in layers:
padding = output_length - flen(layer)
if leftalign:
layer = pad(layer, 0, padding)
else:
layer = pad(layer, padding, 0)
layer = audioop.mul(layer, audio_params[1], attenuation)
if len(layer) != ftc(output_length) or len(out) != ftc(output_length):
dif = int(math.fabs(len(layer) - len(out)))
log('unequal'+str(dif))
if len(out) < len(layer):
layer = layer[:len(layer) - dif]
else:
out = out[:len(out) - dif]
out = audioop.add(out, layer, audio_params[1])
return out
def flen(string):
# string length in frames
return len(string) / (audio_params[1] + audio_params[0])
def pad(string, start, end):
# start and end given in samples, as usual
# eg lengths of silence to pad at start and end
zero = struct.pack('<h', 0)
zero = zero[0:1] * audio_params[1] * audio_params[0] # will we ever have a width > 2? donno.
return "%s%s%s" % ((start * zero), string, (end * zero))
def iscrossing(first, second):
""" Detects zero crossing between two mono frames """
if len(first) == 2 and len(second) == 2:
first = struct.unpack("<h", first)
second = struct.unpack("<h", second)
if first[0] > 0 and second[0] < 0:
return True
elif first[0] < 0 and second[0] > 0:
return True
elif first[0] == 0 and second[0] != 0:
return False
elif first[0] != 0 and second[0] == 0:
return True
return False
def amp(string, scale):
return audioop.mul(string, audio_params[1], scale)
def prob(item_dictionary):
weighted_list = []
for item, weight in item_dictionary.iteritems():
for i in range(weight):
weighted_list.append(item)
return randchoose(weighted_list)
def stf(s):
ms = s * 1000.0
return mstf(ms)
def mstf(ms):
frames_in_ms = audio_params[2] / 1000.0
frames = ms * frames_in_ms
return int(frames)
def ftms(frames):
ms = frames / float(audio_params[2])
return ms * 1000
def fts(frames):
s = frames / float(audio_params[2])
return s
def ftc(frames):
frames = int(frames)
frames *= audio_params[1] # byte width
frames *= audio_params[0] # num chans
return frames
def htf(hz):
""" hz to frames """
if hz > 0:
frames = audio_params[2] / float(hz)
else:
frames = 1 # 0hz is okay...
return int(frames)
def timestamp_filename():
""" Generate a datetime string to add to filenames """
current_time = str(datetime.time(datetime.now()))
current_time = current_time.split(':')
current_seconds = current_time[2].split('.')
current_time = current_time[0] + '.' + current_time[1] + '.' + current_seconds[0]
current_date = str(datetime.date(datetime.now()))
return current_date + "_" + current_time
def write(audio_string, filename, timestamp = True, dirname="renders"):
""" Write audio data to renders directory with the Python wave module """
if timestamp == True:
filename = dirname + '/' + filename + '-' + timestamp_filename() + '.wav'
else:
filename = dirname + '/' + filename + '.wav'
wavfile = wave.open(filename, "w")
wavfile.setparams(audio_params)
wavfile.writeframes(audio_string)
wavfile.close()
return filename
def read(filename):
""" Read a 44.1k / 16bit WAV file from disk with the Python wave module.
Mono files are converted to stereo automatically. """
filename = snddir + filename
print 'loading', filename
file = wave.open(filename, "r")
file_frames = file.readframes(file.getnframes())
snd = Sound()
# check for mono files
if file.getnchannels() == 1:
file_frames = audioop.tostereo(file_frames, file.getsampwidth(), 0.5, 0.5)
snd.params = file.getparams()
snd.params = (2, snd.params[1], snd.params[2], snd.params[3], snd.params[4], snd.params[5])
else:
snd.params = file.getparams()
snd.data = file_frames
return snd
def insert_into(haystack, needle, position):
# split string at position index
hay = cut(haystack, 0, position)
stack = cut(haystack, position, flen(haystack) - position)
return "%s%s%s" % (hay, needle, stack)
def replace_into(haystack, needle, position):
hayend = position * audio_params[1] * audio_params[0]
stackstart = hayend - (flen(needle) * audio_params[1] * audio_params[0])
return "%s%s%s" % (haystack[:hayend], needle, haystack[stackstart:])
def cut(string, start, length):
# start and length are both given in frames (aka samples)za
if start + length > flen(string):
log('No cut for you!')
log('in len: '+str(flen(string))+'start: '+str(start)+' length: '+str(length))
length = int(length) * audio_params[1] * audio_params[0]
start = int(start) * audio_params[1] * audio_params[0]
return string[start : start + length]
def mixstereo(chans):
""" mix a list of two mono sounds into a stereo sound """
chans[0] = audioop.tostereo(chans[0], audio_params[1], 1, 0)
chans[1] = audioop.tostereo(chans[1], audio_params[1], 0, 1)
return mix(chans)
def splitmono(string):
""" split a stereo sound into a list of mono sounds """
left = audioop.tomono(string, audio_params[1], 1, 0)
right = audioop.tomono(string, audio_params[1], 0, 1)
return [left, right]
def split(string, size, chans=2):
""" split a sound into chunks of size N in frames, or by zero crossings """
if size == 0:
if chans == 2:
# split into mono files
tracks = splitmono(string)
for i, track in enumerate(tracks):
tracks[i] = split(track, 0, 1)
return tracks
elif chans == 1:
frames = split(string, 1, 1)
chunk, chunks = [], []
for i, frame in enumerate(frames):
try:
if chunk == []:
chunk += [ frame ]
elif iscrossing(frame, frames[i+1]) == False and chunk != []:
chunk += [ frame ]
elif iscrossing(frame, frames[i+1]) == True and chunk != []:
chunk += [ frame ]
chunks += [ ''.join(chunk) ]
chunk = []
except IndexError:
chunk += [ frame ]
chunks += [ ''.join(chunk) ]
return chunks
elif size > 0:
frames = int(size) * audio_params[1] * chans
return [string[frames * count : (frames * count) + frames] for count in range(len(string) / frames)]
def vsplit(input, minsize, maxsize):
# min/max size is in frames...
output = []
pos = 0
for chunk in range(flen(input) / minsize):
chunksize = randint(minsize, maxsize)
if pos + chunksize < flen(input) - chunksize:
output.append(cut(input, pos, chunksize))
pos += chunksize
return output
def bpm2ms(bpm):
return 60000.0 / float(bpm)
def bpm2frames(bpm):
return int((bpm2ms(bpm) / 1000.0) * audio_params[2])
def pantamp(pan_pos):
# Translate the pan position into a tuple size two of left amp and right amp
if pan_pos > 0.5:
pan_pos -= 0.5
pan_pos *= 2.0
pan_pos = 1.0 - pan_pos
pan_pos = (pan_pos, 1.0)
elif pan_pos < 0.5:
pan_pos *= 2.0
pan_pos = (1.0, pan_pos)
else:
pan_pos = (1.0, 1.0)
return pan_pos
def pan(slice, pan_pos=0.5, amp=1.0):
amps = pantamp(pan_pos)
lslice = audioop.tomono(slice, audio_params[1], 1, 0)
lslice = audioop.tostereo(lslice, audio_params[1], amps[0], 0)
rslice = audioop.tomono(slice, audio_params[1], 0, 1)
rslice = audioop.tostereo(rslice, audio_params[1], 0, amps[1])
slice = audioop.add(lslice, rslice, audio_params[1])
return audioop.mul(slice, audio_params[1], amp)
def env(audio_string, wavetable_type="sine", fullres=False):
# Very short envelopes are possible...
if flen(audio_string) < dsp_grain * 4 or fullres == True:
packets = split(audio_string, 1)
else:
packets = split(audio_string, dsp_grain)
wtable = wavetable(wavetable_type, len(packets))
packets = [audioop.mul(packet, audio_params[1], wtable[i]) for i, packet in enumerate(packets)]
return ''.join(packets)
def panenv(sound, ptype='line', etype='sine', panlow=0.0, panhigh=1.0, envlow=0.0, envhigh=1.0):
packets = split(sound, dsp_grain)
ptable = wavetable(ptype, len(packets), panlow, panhigh)
etable = wavetable(etype, len(packets), envlow, envhigh)
packets = [pan(p, ptable[i], etable[i]) for i, p in enumerate(packets)]
return ''.join(packets)
def pulsar(sound, freq=(1.0, 1.01, 'random'), amp=(0.0, 1.0, 'random'), pan_pos=0.5):
slices = split(sound, dsp_grain)
# Set pulsaret parameters
out_params = {
'amp': (amp[0], amp[1], wavetable(amp[2], len(slices))),
'freq': (freq[0], freq[1], wavetable(freq[2], len(slices))),
'pan_pos': pan_pos,
}
# Process each dsp_grain length packet with subpulse() - packets returned can be of variable size
slices = [pulsaret(slice, out_params, i) for i, slice in enumerate(slices)]
# Join packets into pulse and return the audio string
return ''.join(slices)
def pulsaret(slice, params, index):
amp = ((params['amp'][1] - params['amp'][0]) * params['amp'][2][index]) + params['amp'][0]
slice = pan(slice, params['pan_pos'], amp)
freq_width = params['freq'][2][index] * (params['freq'][1] - params['freq'][0]) + params['freq'][0]
target_rate = int(audio_params[2] * (1.0 / float(freq_width)))
if target_rate == audio_params[2]:
return slice
else:
slice = audioop.ratecv(slice, audio_params[0], audio_params[1], audio_params[2], cap(target_rate, 2147483647, dsp_grain), None)
return slice[0]
def fnoise(sound, coverage):
target_frames = int(flen(sound) * coverage)
for i in range(target_frames):
p = randint(0, flen(sound) - 1)
f = cut(sound, p, 1)
sound = replace_into(sound, f, randint(0, flen(sound) - 1))
return sound
class Sound:
def __init__(self):
data = ''
params = ''