-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathfibonaccistretch.py
518 lines (399 loc) · 22.3 KB
/
fibonaccistretch.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
"""
Fibonacci Stretch
by David Su http://usdivad.com/
A method of time-stretching an existing audio track such that its rhythmic
pulses become expanded or contracted along the Fibonacci sequence, using
Euclidean rhythms as the basis for modification.
"""
# ## Part 1 - Representing rhythm as symbolic data
# Standard libraries
import math
# External libraries
import IPython.display as ipd
import librosa
import librosa.display
import numpy as np
import matplotlib.pyplot as plt
# Fork of Brian House's implementation of Bjorklund's algorithm
# https://github.com/brianhouse/bjorklund
import bjorklund
# Generate clicks based on input rhythm bit array (e.g. [1,0,0,1,0,0,1,0])
def generate_rhythm_clicks(rhythm, click_interval=0.25, sr=44100):
step_length_samples = int(librosa.time_to_samples(click_interval, sr=sr))
rhythm_length_samples = step_length_samples * (len(rhythm))
# Generate click times
pulse_click_times, step_click_times = generate_rhythm_times(rhythm, click_interval)
# Generate pulse clicks
pulse_click_times = np.array([i * click_interval for i in range(len(rhythm))
if rhythm[i] != 0])
pulse_clicks = librosa.clicks(times=pulse_click_times, click_freq=2000.0, sr=sr, length=rhythm_length_samples)
# Generate step clicks
step_click_times = np.array([i * click_interval for i in range(len(rhythm))])
step_clicks = librosa.clicks(times=step_click_times, click_freq=1000.0, sr=sr, length=rhythm_length_samples)
step_clicks = np.hstack((step_clicks, np.zeros(step_length_samples, dtype="int"))) # add last step samples
# Add zeros to pulse clicks so that it's the same length as the step clicks signal
pulse_clicks = np.hstack((pulse_clicks, np.zeros(len(step_clicks)-len(pulse_clicks), dtype="int")))
# Ensure proper length
pulse_clicks = pulse_clicks[:rhythm_length_samples]
step_clicks = step_clicks[:rhythm_length_samples]
return (pulse_clicks, step_clicks)
# Generate times for a rhythm
def generate_rhythm_times(rhythm, interval):
pulse_times = np.array([float(i * interval) for i in range(len(rhythm)) if rhythm[i] != 0])
step_times = np.array([float(i * interval) for i in range(len(rhythm))])
return (pulse_times, step_times)
# Function to calculate pulse lengths based on rhythm patterns
def calculate_pulse_lengths(rhythm):
pulse_indices = np.array(([i for i,p in enumerate(rhythm) if p > 0]))
pulse_indices = np.hstack((pulse_indices, len(rhythm)))
pulse_lengths = np.array([pulse_indices[i+1] - pulse_indices[i] for i in range(len(pulse_indices) - 1)])
return pulse_lengths
# ## Part 2 - Fibonacci rhythms
# Calculate nth fibonacci number
def fibonacci(n):
if n == 0 or n == 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
# Find the index of a Fibonacci number
def find_fibonacci_index(n):
phi = (1 + math.sqrt(5)) / 2 # Golden ratio; 1.61803398875...
return int(math.log((n * math.sqrt(5)) + 0.5) / math.log(phi))
# Expand pulse lengths by factor of 1
def fibonacci_expand_pulse_lengths(pulse_lengths):
return fibonacci_scale_pulse_lengths(pulse_lengths, 1)
# Function to scale pulse lengths along the Fibonacci sequence
#
# Note that `scale_amount` determines the direction and magnitude of the scaling.
# If `scale_amount` > 0, it corresponds to a rhythmic expansion.
# If `scale_amount` < 0, it corresponds to a rhythmic contraction.
# If `scale_amount` == 0, the original scale is maintained and no changes are made.
def fibonacci_scale_pulse_lengths(pulse_lengths, scale_amount=0):
scaled_pulse_lengths = np.array([], dtype="int")
for pulse_length in pulse_lengths:
fib_i = find_fibonacci_index(pulse_length)
# if fib_i + scale_amount < 0:
# print("ERROR: Scale amount out of bounds")
# return pulse_lengths
scaled_pulse_length = fibonacci(max(fib_i + scale_amount, 0))
scaled_pulse_lengths = np.hstack((scaled_pulse_lengths, scaled_pulse_length))
return scaled_pulse_lengths
# Define the functions we'll use to scale rhythms along the Fibonacci sequence
def fibonacci_scale_rhythm(rhythm, scale_amount):
pulse_lengths = calculate_pulse_lengths(rhythm)
scaled_pulse_lengths = fibonacci_scale_pulse_lengths(pulse_lengths, scale_amount)
scaled_pulse_lengths = np.array([p for p in scaled_pulse_lengths if p > 0])
scaled_rhythm = pulse_lengths_to_rhythm(scaled_pulse_lengths)
return scaled_rhythm
def pulse_lengths_to_rhythm(pulse_lengths):
rhythm = np.array([], dtype="int")
for p in pulse_lengths:
pulse = np.zeros(p, dtype="int")
pulse[0] = 1
rhythm = np.hstack((rhythm, pulse))
return rhythm
# ## Part 3 - Mapping rhythm to audio
# Estimate tempo from an input signal
def estimate_tempo(y, sr, start_bpm=120.0):
# Estimate tempo
onset_env = librosa.onset.onset_strength(y, sr=sr) # TODO: Compare this with librosa.beat.beat_track
tempo = librosa.beat.tempo(y, sr=sr, onset_envelope=onset_env, start_bpm=start_bpm)
return float(tempo)
# Calculate beat times
def calculate_beat_times(y, sr, tempo):
# Calculate params based on input
T = len(y)/float(sr) # Total audio length in seconds
seconds_per_beat = 60.0/tempo
# Start beat at first onset rather than time 0
# TODO: Let this first onset also be user-supplied for more accurate results
beat_times = np.arange(detect_first_onset_time(y, sr), T, seconds_per_beat)
return beat_times
# Detect first onset
def detect_first_onset_time(y, sr, hop_length=1024):
onset_frames = librosa.onset.onset_detect(y, sr=sr, hop_length=hop_length)
onset_times = librosa.frames_to_time(onset_frames)
return onset_times[0]
# Calculate measure indices in samples
def calculate_measure_samples(y, beat_samples, beats_per_measure):
max_samples = len(y)
start_sample = beat_samples[0]
beat_interval = beat_samples[1] - beat_samples[0]
measure_interval = beat_interval * beats_per_measure
if measure_interval >= beat_interval:
return np.array(beat_samples[::beats_per_measure], dtype="int")
else:
beat_indices = np.indices([len(beat_samples)])[0]
measure_indices = np.indices([len(beat_samples)/beats_per_measure])[0]
return np.interp(measure_indices, beat_indices/beats_per_measure, beat_samples)
"""Generating clicks for tresillo rhythm at the proper tempo and start time,
to overlay onto an audio track"""
def generate_rhythm_overlay(rhythm, measure_samples, steps_per_measure, sr):
# Calculate click interval
measure_length = measure_samples[1]-measure_samples[0]
# click_tempo = tempo * (steps_per_measure/float(beats_per_measure))
# click_interval = 60.0/click_tempo
measure_length_seconds = librosa.samples_to_time(measure_length, sr=sr)
click_interval = measure_length_seconds / float(steps_per_measure)
# Generate click times for single measure
pulse_times_measure, step_times_measure = generate_rhythm_times(rhythm, click_interval)
# Generate clicks for single measure
pulse_clicks_measure, step_clicks_measure = generate_rhythm_clicks(rhythm, click_interval, sr=sr)
# Concatenate clicks and click times for all measures
pulse_times, step_times, pulse_clicks, step_clicks = np.array([]), np.array([]), np.array([]), np.array([])
for s in measure_samples:
t = float(librosa.samples_to_time(s, sr=sr))
pulse_clicks = np.hstack((pulse_clicks, pulse_clicks_measure))
step_clicks = np.hstack((step_clicks, step_clicks_measure))
pulse_times = np.hstack((pulse_times, pulse_times_measure + t))
step_times = np.hstack((step_times, step_times_measure + t))
# Offset clicks by first onset
pulse_clicks = np.hstack((np.zeros(measure_samples[0]), pulse_clicks))
step_clicks = np.hstack((np.zeros(measure_samples[0]), step_clicks))
return (pulse_times, step_times, pulse_clicks, step_clicks)
"""Visualizing and hearing the result"""
def overlay_rhythm_onto_audio(rhythm, audio_samples, measure_samples, sr=44100, plt_size=(16,4), click_colors={"measure": "r",
"pulse": "r",
"step": "r"}):
# Get overlay data
pulse_times, step_times, pulse_clicks, step_clicks = generate_rhythm_overlay(rhythm,
measure_samples,
len(rhythm),
sr)
measure_times = librosa.samples_to_time(measure_samples, sr=sr)
measure_clicks = librosa.clicks(times=measure_times, sr=sr, click_freq=3000.0, length=len(audio_samples))
# Calculate max length in samples
available_lengths = [len(audio_samples), len(measure_clicks), len(pulse_clicks), len(step_clicks)]
length_samples = min(available_lengths)
# Plot original waveform
plt.figure(figsize=plt_size)
librosa.display.waveplot(audio_samples, sr=sr, alpha=0.5)
# Plot rhythm clicks
plt.vlines(measure_times, -1, 1, color=click_colors["measure"])
plt.vlines(pulse_times, -0.5, 0.5, color=click_colors["pulse"])
plt.vlines(step_times, -0.25, 0.25, color=click_colors["step"], alpha=0.75)
# Play both clicks together with audio track
concatenated_audio_samples = ((audio_samples[:length_samples]*2.0)
+ (measure_clicks[:length_samples]*0.25)
+ (pulse_clicks[:length_samples]*0.25)
+ (step_clicks[:length_samples]*0.25))
audio_display = ipd.Audio(concatenated_audio_samples, rate=sr)
return audio_display
# ## Part 4 - Time-stretching audio
# Calculate ratios between pulses for two rhythm sequences
# NOTE: This assumes that both rhythm sequences have the same number of pulses! Extra pulses in the longer rhythm will be ignored
def calculate_pulse_ratios(original_rhythm, target_rhythm):
original_pulse_lengths = calculate_pulse_lengths(original_rhythm)
target_pulse_lengths = calculate_pulse_lengths(target_rhythm)
num_pulses = min(len(original_pulse_lengths), len(target_pulse_lengths))
pulse_ratios = np.array([original_pulse_lengths[i]/float(target_pulse_lengths[i]) for i in range(num_pulses)])
#if len(pulse_ratios) < len(original_pulse_lengths): # Add 0s to pulse ratios if there aren't enough
# pulse_ratios = np.hstack((pulse_ratios, np.zeros(len(original_pulse_lengths) - len(pulse_ratios))))
return pulse_ratios
# Modify a single measure
def modify_measure(data, original_rhythm, target_rhythm, stretch_method):
modified_data = np.array([])
# Define the rhythmic properties we'll use
original_num_samples = len(data)
original_num_steps = len(original_rhythm)
target_num_steps = len(target_rhythm)
# Get indices of steps for measure
original_step_interval = original_num_samples / float(original_num_steps)
original_step_indices = np.arange(0, original_num_samples, original_step_interval, dtype="int")
# Get only indices of pulses based on rhythm
original_pulse_indices = np.array([original_step_indices[i] for i in range(original_num_steps) if original_rhythm[i] > 0])
# Calculate pulse ratios
pulse_ratios = calculate_pulse_ratios(original_rhythm, target_rhythm)
# Calculate pulse lengths
original_pulse_lengths = calculate_pulse_lengths(original_rhythm)
target_pulse_lengths = calculate_pulse_lengths(target_rhythm)
# Concatenate time-stretched versions of rhythm's pulses
for i,p in enumerate(original_pulse_indices):
# Get pulse sample data; samples between current and next pulse, or if it's the final pulse,
# samples between pulse and end of audio
pulse_start = p
pulse_stop = len(data)-1
if i < len(original_pulse_indices)-1:
pulse_stop = original_pulse_indices[i+1]
pulse_samples = data[pulse_start:pulse_stop]
# Time-stretch this step based on ratio of old to new rhythm length
# TODO: Try out other methods of manipulation, such as using onset detection in addition to steps and pulses
if stretch_method == "timestretch":
pulse_samples = librosa.effects.time_stretch(pulse_samples, pulse_ratios[i])
elif stretch_method == "euclidean":
pulse_samples = euclidean_stretch(pulse_samples,
original_pulse_lengths[i],
target_pulse_lengths[min(i, len(target_pulse_lengths)-1)])
else:
print("ERROR: Invalid stretch method {}".format(stretch_method))
# Add the samples to our modified audio time series
modified_data = np.hstack((modified_data, pulse_samples))
# Time-stretch entire measure to maintain original measure length (so that it sounds more natural)
stretch_multiplier = len(modified_data)/float(len(data))
modified_data = librosa.effects.time_stretch(modified_data, stretch_multiplier)
return modified_data
# Modify an entire audio track; basically just loops through a track's measures
# and calls modify_measure() on each measure
def modify_track(data, measure_samples, original_rhythm, target_rhythm, stretch_method="timestretch"):
modified_track_data = np.zeros(measure_samples[0])
modified_measure_samples = np.array([], dtype="int")
for i, sample in enumerate(measure_samples[:-1]):
modified_measure_samples = np.hstack((modified_measure_samples, len(modified_track_data)))
measure_start = measure_samples[i]
measure_stop = measure_samples[i+1]
measure_data = data[measure_start:measure_stop]
modified_measure_data = modify_measure(measure_data, original_rhythm, target_rhythm, stretch_method)
modified_track_data = np.hstack((modified_track_data, modified_measure_data))
return (modified_track_data, modified_measure_samples)
# ## Part 5 - Euclidean stretch
# Euclid's algorithm to find greatest common divisor
def euclid(a, b):
m = max(a, b)
k = min(a, b)
if k==0:
return m
else:
return euclid(k, m%k)
# Euclidean stretch for modifying a single pulse (basically time-stretching subdivisions based on Euclidean rhythms)
def euclidean_stretch(pulse_samples, original_pulse_length, target_pulse_length):
target_pulse_samples = np.array([])
# Return empty samples array if target pulse length < 1
if target_pulse_length < 1:
return target_pulse_samples
# Ensure original pulse rhythm ("opr") has length equal to or less than target_pulse_length
# ... by using target pulse length
# original_pulse_length = min(original_pulse_length, target_pulse_length)
# ... by using divisors of original pulse length
# if original_pulse_length > target_pulse_length:
# # print("WARNING: original_pulse_length {} "
# # "is greater than target_pulse_length {}".format(original_pulse_length,
# # target_pulse_length))
# for i in range(1, original_pulse_length+1):
# opl_new = int(original_pulse_length / float(i))
# if opl_new <= target_pulse_length:
# original_pulse_length = opl_new
# # print("original_pulse_length is now {}".format(original_pulse_length))
# break
# ... by using lowest common multiple as target pulse length
if original_pulse_length > target_pulse_length:
# print("Target pulse length before: {}".format(target_pulse_length))
gcd = euclid(original_pulse_length, target_pulse_length)
lcm = (original_pulse_length*target_pulse_length) / gcd
target_pulse_length = lcm
# print("Target pulse length after: {}".format(target_pulse_length))
# original_pulse_length = target_pulse_length
opr = np.ones(original_pulse_length, dtype="int")
# Generate target pulse rhythm ("tpr")
tpr = bjorklund.bjorklund(pulses=original_pulse_length, steps=target_pulse_length)
tpr_pulse_lengths = calculate_pulse_lengths(tpr)
tpr_pulse_ratios = calculate_pulse_ratios(opr, tpr)
# Subdivide (i.e. segment) the pulse based on original pulse length
pulse_subdivision_step = int(len(pulse_samples) / float(original_pulse_length))
pulse_subdivision_indices = np.arange(0, len(pulse_samples), pulse_subdivision_step, dtype="int")
pulse_subdivision_indices = pulse_subdivision_indices[:original_pulse_length]
# Time-stretch each subdivision based on ratios
for i,si in enumerate(pulse_subdivision_indices):
subdivision_start = si
subdivision_stop = len(pulse_samples) - 1
if i < len(pulse_subdivision_indices)-1:
subdivision_stop = pulse_subdivision_indices[i+1]
pulse_subdivision_samples = pulse_samples[subdivision_start:subdivision_stop]
# Stretch the relevant subdivisions based on target pulse rhythm
pulse_subdivision_samples = librosa.effects.time_stretch(pulse_subdivision_samples, tpr_pulse_ratios[i])
# Concatenate phrase
target_pulse_samples = np.hstack((target_pulse_samples, pulse_subdivision_samples))
return target_pulse_samples
# ## Part 6 - Fibonacci stretch: implementation and examples
# An end-to-end implementation of Fibonacci stretch
# Example usage:
# fibonacci_stretch_track("data/imtheone_cropped_chance_60s.mp3",
# tempo=162,
# original_rhythm=np.array([1,0,0,0,0,1,0,0]),
# target_rhythm=np.array([1,0,0,0,0,1,0,0,0,0]),
# overlay_clicks=True)
def fibonacci_stretch_track(audio_filepath,
sr=44100,
original_rhythm=np.array([1,0,0,1,0,0,1,0], dtype="int"),
stretch_method="euclidean",
stretch_factor=1,
target_rhythm=None,
tempo=None,
beats_per_measure=4,
hop_length=1024,
overlay_clicks=False,
plt_size=(16,4),
render_track=True):
# Load input audio
y, sr = librosa.load(audio_filepath, sr=sr)
# Extract rhythm features from audio
if tempo is None:
tempo = estimate_tempo(y, sr)
beat_times = calculate_beat_times(y, sr, tempo)
beat_samples = librosa.time_to_samples(beat_times, sr=sr)
measure_samples = calculate_measure_samples(y, beat_samples, beats_per_measure)
# Generate target rhythm
if target_rhythm is None:
target_rhythm = fibonacci_scale_rhythm(original_rhythm, stretch_factor)
# Modify the track
y_modified, measure_samples_modified = modify_track(y, measure_samples,
original_rhythm, target_rhythm,
stretch_method="euclidean")
# Render the track and any plots
rendered_track = ipd.Audio(y_modified, rate=sr)
if overlay_clicks:
rendered_track = overlay_rhythm_onto_audio(target_rhythm, y_modified, measure_samples_modified, sr, plt_size=plt_size)
else:
plt.figure(figsize=plt_size)
librosa.display.waveplot(y_modified, sr=sr)
# Return rendered track...
if render_track:
return rendered_track
# ... or return modified track and measure samples
else:
return (y_modified, measure_samples_modified)
# ================================
# From other nbs
# Calculate stretch ratios for each original step, for use in real-time
def calculate_step_stretch_ratios(original_rhythm, target_rhythm):
# Original and target pulse lengths
original_pulse_lengths = list(calculate_pulse_lengths(original_rhythm))
target_pulse_lengths = list(calculate_pulse_lengths(target_rhythm))
# Pulse ratios
pulse_ratios = list(calculate_pulse_ratios(original_rhythm, target_rhythm))
if len(pulse_ratios) < len(original_pulse_lengths): # Add 0s to pulse ratios if there aren't enough
for _ in range(len(original_pulse_lengths) - len(pulse_ratios)):
pulse_ratios.append(0.0)
assert(len(pulse_ratios) == len(original_pulse_lengths))
# Format pulse ratios so there's one for each step
pulse_ratios_by_step = []
for i,pulse_length in enumerate(original_pulse_lengths):
for _ in range(pulse_length):
pulse_ratios_by_step.append(pulse_ratios[i])
#print("Pulse ratios by step: {}".format(pulse_ratios_by_step))
# Calculate stretch ratios for each original step
# Adapted from Euclidean stretch
step_stretch_ratios = []
for i in range(min(len(original_pulse_lengths), len(target_pulse_lengths))):
# Pulse lengths
opl = original_pulse_lengths[i]
tpl = target_pulse_lengths[i]
# Adjust target pulse length if it's too small
#if opl > tpl:
# tpl = opl
while opl > tpl:
tpl *= 2
# Use steps as original pulse rhythm ("opr")
opr = [1] * len(original_rhythm)
# Generate target pulse rhythm ("tpr") using Bjorklund's algorithm
tpr = bjorklund.bjorklund(pulses=opl, steps=tpl)
tpr_pulse_lengths = calculate_pulse_lengths(tpr)
tpr_pulse_ratios = calculate_pulse_ratios(opr, tpr)
# Scale the tpr pulse ratios by the corresponding ratio from pulse_ratios_by_step
tpr_pulse_ratios *= pulse_ratios_by_step[i]
step_stretch_ratios.extend(tpr_pulse_ratios)
#print("Step stretch ratios before multiplier: {}".format(step_stretch_ratios))
# Multiply by stretch multiplier to make sure the length is the same as original
stretch_multiplier = 1.0 / (sum(step_stretch_ratios) / len(step_stretch_ratios))
step_stretch_ratios = [r * stretch_multiplier for r in step_stretch_ratios]
assert(round(sum(step_stretch_ratios) / len(step_stretch_ratios), 5) == 1) # Make sure it's *close enough* to original length.
#print("Step stretch ratios after multiplier: {}".format(step_stretch_ratios))
return step_stretch_ratios