-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.py
313 lines (243 loc) · 11.1 KB
/
main.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
from scipy.fft import fft
from scipy.io import wavfile
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import find_peaks
from os.path import isfile
from coloredOutput import style
import os
class Extractor:
"""class used to extract a numpy array from a .wav file
input:
path to .wav
methods:
extract, returns the data as a np array
plot(), plots it uning the matplotlib
"""
def __init__(self, file):
file = file.strip("\"")
if isfile(file):
pass
else:
file = file.replace("\\", "/" ) #UNIX uses foreward slashes instead of backslashes
#TODO generate wavefile in designated folder
if file[-4:] != ".wav":
os.system(f'ffmpeg -i "{file}" "{file[:-4]}.wav"')
print(file)
self.file = file[:-4]+".wav"
print(self.file)
else:
self.file = file
def extract(self, channel = 0, sampleBeginning =0, sampleEnd = -1):
"""input: channel (mostly 1 or 0), sampleBeginning (seconds), sampleEnd (seconds)
output: the data of the channel using a numphy array """
fs, data = wavfile.read(self.file) #extract data
self.data = data
self.samplesPerSecond = fs
sampleBeginning, sampleEnd = int(sampleBeginning * self.samplesPerSecond), int(sampleEnd * self.samplesPerSecond)
self.data = self.data[:,channel][sampleBeginning:sampleEnd]
#throw exeption, when data is empty (all zeros)
if all(flag == 0 for flag in self.data):
raise LookupError(f"""the data is empty, the wavefile could not be read, maybe there is no music at that time?
Try to readjust the timeframe and try again.
here is the data: {self.data}""")
#when data is empty, raise exeption
if sampleBeginning == sampleEnd:
raise IndexError("starting and ending point are the same")
self.lenghth = len(self.data)
return self.data
def plot(self, channel = 0,sampleEnd = None, sampleBeginning = 0): #in seconds
""" input: channel (std = 0), sampleEnd in seconds, sampleBeginning in seconds
plots the music using matplotlib, the self.extract() method will have to be called first """
self.extract()
if sampleEnd == None:
sampleEnd = self.extract(channel = 0).__len__() // self.samplesPerSecond
x = np.arange(sampleBeginning, sampleEnd, 1/self.samplesPerSecond) #start, stop, step
#transform
sampleBeginning = int(sampleBeginning * self.samplesPerSecond)
sampleEnd = int(sampleEnd * self.samplesPerSecond)
# sample_length = sampleEnd - sampleBeginning
y = self.data[sampleBeginning :sampleEnd]
plt.plot(x,y)
plt.show()
class Transformator():
"""Fourrier transforms given data
inherits from Extractor
methods:
transform
find extrema
plot
fields:
fdata: contains x and y coordinates of transformed data
"""
def __init__(self):
pass
def transform(self, y, samplesPerSecond,frequencyBeginning = 300, frequencyEnd = 1000, **kwargs):
"""the fourrier transform gives a representation of the frequencies in the input array
fourrier transforms given array, returns (xf,yf)
slicing improves processing spead and memory usage
returns:
xf: x coordinate linspace
yf: fourrier transform of input array"""
N = y.__len__()
#Come back and increment this number after you attempt to optimize this resource-heavy function and fail: 3
yf = fft(y, overwrite_x=True, workers=1)
T = 1.0 / samplesPerSecond
frequencyBeginningIndex = int(frequencyBeginning*T*N)
frequencyEndIndex = int(frequencyEnd*T*N)
yf = yf[frequencyBeginningIndex:frequencyEndIndex]
N = yf.__len__()
xf = np.linspace(frequencyBeginning, frequencyEnd, N)
return xf, np.abs(yf)
def findextrema(self, xf, yf,distance = 5,**kwargs):
"""
returns: (xfPeaks, yfPeaks), dtype = np.array
"""
if all(flag == 0 for flag in yf):
return [0],[0]
peaks, _ = find_peaks(yf, distance) #indecies
xfPeaks = xf[peaks]
yfPeaks = yf[peaks]
self.peaks = xfPeaks, yfPeaks
return xfPeaks, yfPeaks
class Translator():
"""used to transform the extracted frequencies into Notes"""
def __init__(self):
pass
def translate(self, beginning, end):
#frequencyToNoteValue
pass
def findMainFrequencies(self, xUnsorted , yUnsorted, threshhold = 1/3, number = 6, **kwargs):
"""returns:
(x, y) of the last [number] datapoints
"""
xUnsorted , yUnsorted
# There are different ways to do a Quick Sort partition, this implements the
# Hoare partition scheme. Tony Hoare also created the Quick Sort algorithm.
def partition(y, x, low, high):
# We select the middle element to be the pivot. Some implementations select
# the first element or the last element. Sometimes the median value becomes
# the pivot, or a random one. There are many more strategies that can be
# chosen or created.
pivot = y[(low + high) // 2]
i = low - 1
j = high + 1
while True:
i += 1
while y[i] < pivot:
i += 1
j -= 1
while y[j] > pivot:
j -= 1
if i >= j:
return j
# If an element at i (on the left of the pivot) is larger than the
# element at j (on right right of the pivot), then swap them
y[i], y[j] = y[j], y[i]
x[i], x[j] = x[j], x[i]
def quick_sort(y,x):
# Create a helper function that will be called recursively
def _quick_sort(y, x, low, high):
if low < high:
# This is the index after the pivot, where our lists are split
split_index = partition(y,x, low, high)
_quick_sort(y,x, low, split_index)
_quick_sort(y,x, split_index + 1, high)
_quick_sort(y,x, 0, len(y) - 1)
quick_sort(yUnsorted,xUnsorted)
xSorted = list(np.flip(xUnsorted, -1))[:number]
ySorted = list(np.flip(yUnsorted, -1))[:number]
#if the peak is lower than 2/3 of the highest peak, or so low it could be noise, delete it
noise = 1000000
i = 0
while i < len(ySorted):
if ySorted[i] < ySorted[0]* threshhold or ySorted[i] <noise:
del ySorted[i]
del xSorted[i]
else:
i += 1
mainFrequencies = xSorted , ySorted #return the first [number] datatpoints
return mainFrequencies
def frequencyToNoteValue(self, frequency, fStartingNote = 440): #a=440 Hz
"""finds the note closest to the frequency"""
if frequency == 0:
return None
n = 12 * np.log2(frequency/fStartingNote) #see https://www.techlib.com/reference/musical_note_frequencies.htm
return int(round(n))
def removeRepetitions(self, data, removePartialRepetitions = True):
#doesnt remove last triad if it has been repeaded. not ctritcal, just something to keep in mind
#search for doubles in every triad
for i in range(len(data)):
j = 0
while j < len(data[i]) -1:
if data[i][j] == data[i][j+1]:
del data[i][j+1]
else:
j += 1
#search for triads beeing contained in previous triads
if removePartialRepetitions:
i = 0
while i < len(data) - 1:
if all([ j in data[i] for j in data[i+1]]):
del data[i+1]
else:
i += 1
#seach for double triads
i = 0
while i < len(data) -1:
if sorted(data[i]) == sorted(data[i+1]):
del data[i+1]
else:
i += 1
return data
def noteNames(self, data):
key = ["a", "b", "h", "c", "cis", "d", "dis", "e", "f", "fis", "g", "gis"]
return [ [key[i % len(key)] for i in sorted(triad)] for triad in data ]
class Main(Extractor, Translator, Transformator):
def __init__(self, file):
Extractor.__init__(self, file=file)
def _thread(self, data, threshhold, numberOfNotes):
"""
generator object
input a song or a part of it
used to implement a method for a ProcessPoolExecutor map(..)
this is a convenience wrapper that brings together the different superclass methods
returns:
notes in the form of [[triad], [triad]...]
"""
transform = self.transform( y=data,samplesPerSecond=self.samplesPerSecond, frequencyBeginning = 300, frequencyEnd = 1000)
mainFrequencies = self.findMainFrequencies(*self.findextrema(*transform, distance = 5), threshhold=threshhold, number=numberOfNotes )
for i in mainFrequencies[0]:
note = self.frequencyToNoteValue(i)
yield note
def split(self, splitLengthInSeconds, sampleBeginning, sampleEnd, channel = 0):
#TODO this is wrong
data = self.extract(channel, sampleBeginning, sampleEnd)
lenghthInSec = self.lenghth / self.samplesPerSecond
print(f"lenghth in sec: {lenghthInSec}")
numberOfSplits= int(round(lenghthInSec / splitLengthInSeconds))
print(f"numberOfSplits: {numberOfSplits}")
return np.array_split(data, indices_or_sections= numberOfSplits )
def thread(self, *args):
"""turns the _thread genrerator obj. into a list"""
#pylint: disable=no-value-for-parameter
return list(self._thread(*args))
def main(self, chanel, sampleBeginning, sampleEnd, frequencyBeginning, frequencyEnd, distance, number, threshhold, fStartingNote = 440):
self.values = self.extract(chanel, sampleBeginning, sampleEnd)
self.xvalues = np.arange(sampleBeginning, sampleEnd, 1/self.samplesPerSecond)
self.fvalues_xy = self.transform(self.values, frequencyBeginning, frequencyEnd)
self.extrema = self.findextrema(*self.fvalues_xy, distance)
self.mainFrequencies = self.findMainFrequencies(*self.extrema,threshhold, number)
self.notes = [list(map(lambda x: self.frequencyToNoteValue(x, fStartingNote), self.mainFrequencies[0]))]
return self.notes
if all((True, True, True == (True, True, True))):
raise RuntimeError(f"""
Broken Lines, broken strings,
Broken threads, broken springs,
Broken idols, broken heads,
People sleeping in broken beds,
Ain't no use jiving,
Ain't no use joking,
EVERYTHING IS BROKEN
for seriours: if you see this error, stop coding immediately, run and seek shelter in a nearby closet!
""")