Skip to content

Commit 8509362

Browse files
committed
WIP, spectrogram and histogram.
1 parent 42df4db commit 8509362

File tree

2 files changed

+209
-49
lines changed

2 files changed

+209
-49
lines changed

constants.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99
/___/
1010
1111
"""
12-
TEXT_CLEAN = "✂️ cut <filename> [...filenames] ::: Removes non-speech parts of the selected soundwaves"
13-
TEXT_HELP = "📜 help ::: Shows this menu."
14-
TEXT_LIST = "💿 list [like] ::: Lists all loaded wavefiles. Optionally, list all waves with [like] in their name"
15-
TEXT_LOAD = "📥 load <filename> [...filenames] ::: Loads each specified file from ./input/<filename>.wav"
16-
TEXT_PLOT = "📈 plot [waveform|spectogram|histogram] [...filenames] ::: Plots the selected wavefile on the selected type of graph. Multiple wavefiles may be plotted. If no file is specified, plots all loaded."
17-
TEXT_QUIT = "🚪 quit ::: Closes the application"
18-
TEXT_NOT_LOADED = "🤔 I couldn't find this sound wave, did you load it? "
12+
TEXT_CLEAN = "✂️ cut <filename> [...filenames] ::: Removes non-speech parts of the selected soundwaves."
13+
TEXT_HELP = "📜 help ::: Shows this menu."
14+
TEXT_LIST = "💿 list [like] ::: Lists all loaded wavefiles. Optionally, list all waves with [like] in their name."
15+
TEXT_LOAD = "📥 load <filename> [...filenames] ::: Loads each specified file from ./input/<filename>.wav."
16+
TEXT_PLOT = "📈 plot [-t <waveform|spectrogram|histogram>] [-w <window beginning timestamp in ms>-<window ending timestamp in ms>] [-f <none|hamming|hanning>] [...filenames] ::: Plots the selected wavefile on the selected type of graph. Multiple wavefiles may be plotted. If no file is specified, plots all loaded. If spectrogram or histogram specified, use -w to specify window length and -f to specify the window function."
17+
TEXT_QUIT = "🚪 quit ::: Closes the application."
18+
TEXT_GEN = "🎧 gen [name] [harmonics] [duration] ::: Generates a sound wave with the given name and number of harmonics, lasting [duration] ms. If no name provided, name will be generated. Harmonics number equals 10 by default. Duration equals 100 (ms) by default."
19+
TEXT_NOT_LOADED = "🤔 I couldn't find sound wave \"%s\", did you load it? "
1920
TEXT_INVALID_SYNTAX = "🤔 I couldn't understand that. Try this command:"
20-
TEXT_GENERATE = "🎧 gen [name] [harmonics] [duration] ::: Generates a sound wave with the given name and number of harmonics, lasting [duration] ms. If no name provided, name will be generated. Harmonics number equals 10 by default. Duration equals 100 (ms) by default."
21+
TEXT_INVALID_SYNTAX_PLOT_WINDOW_T = "🤔 Oops. If you specify -w, you need to enter a starting and ending timestamp, like so: -t 200-500. Ending timestamp must be larger than starting timestamp."
22+
TEXT_INVALID_SYNTAX_PLOT_WINDOW = "🤔 Oops, something went wrong. One or more sound waves specified are not available at the specified timestamp."
23+
TEXT_INVALID_SYNTAX_PLOT_WINDOW_F = "🤔 Oops, %s is not a valid window function. Choose \"none\", \"hamming\" or \"hanning\"."
24+
TEXT_INVALID_SYNTAX_PLOT_TYPE = "🤔 Oops, %s is not a valid plot type. Choose \"waveform\", \"spectrogram\" or \"histogram\"."
2125
TEXT_ERROR_WRITING = "❌ There was an error while saving the wave "
2226
TEXT_GENERATED = "💽 You're officially an artist. Here's your sound wave: "
2327
TEXT_PLOTTING = "📈 Plotting..."

main.py

Lines changed: 197 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -137,31 +137,97 @@ def load_wave(filename):
137137
return SoundWave(name=filename, wave=wav, values=vals)
138138

139139

140-
def plot_waves(sound_waves: List[SoundWave], type="waveform"):
140+
def plot_waves(
141+
sound_waves: List[SoundWave],
142+
plot_type="waveform",
143+
window_t=None,
144+
window_func: str = "none"):
141145
"""
142146
Plots all passed sound waves on a single plot with the given type.
143147
"""
144-
title = f"{type.capitalize()} plot of"
145-
146-
plt.ylabel("Amplitude")
147-
148-
plt.xlabel("Time")
148+
title = f"{plot_type.capitalize()} plot of"
149+
150+
if plot_type == "waveform":
151+
152+
plt.ylabel("Amplitude")
153+
plt.xlabel("Time")
154+
155+
for sw in sound_waves:
156+
title += f" {sw.name}.wav"
157+
_, noise_borders = sw.find_endpoints(500, 5000)
158+
time = np.linspace(0, len(sw.values) /
159+
sw.wave.getframerate(), num=len(sw.values))
160+
# TODO check if okay to simply plot different times
161+
plt.plot(time, sw.values, label=f"{sw.name}.wav")
162+
clr = np.random.rand(3,)
163+
if not sw.cleaned:
164+
for xc in noise_borders:
165+
plt.axvline(x=xc, color=clr)
166+
167+
plt.legend()
168+
plt.show()
169+
return
149170

150-
for sw in sound_waves:
151-
title += f" {sw.name}.wav"
152-
_, noise_borders = sw.find_endpoints(500, 5000)
153-
time = np.linspace(0, len(sw.values) /
154-
sw.wave.getframerate(), num=len(sw.values))
155-
# TODO check if okay to simply plot different times
156-
plt.plot(time, sw.values, label=f"{sw.name}.wav")
157-
clr = np.random.rand(3,)
158-
if not sw.cleaned:
159-
for xc in noise_borders:
160-
plt.axvline(x=xc, color=clr)
171+
if plot_type == "histogram":
172+
173+
plt.ylabel("Magnitude")
174+
plt.xlabel("Frequency")
175+
plt.xscale('log')
176+
plt.yscale('log')
177+
178+
if window_t == None:
179+
window_t = (0, 100)
180+
window_dur = (window_t[1] - window_t[0]) / 1000
181+
182+
for sw in sound_waves:
183+
184+
title += f" {sw.name}.wav"
185+
_, noise_borders = sw.find_endpoints(500, 5000)
186+
N = int(sw.wave.getframerate() * window_dur)
187+
f = sw.wave.getframerate() * np.arange(N / 2) / N
188+
# TODO check if okay to simply plot different freqs
189+
y = dft(sw, window_t=window_t, window_func=window_func)
190+
y = y + y.min()
191+
y = y / y.max() * 100
192+
# plt.plot(f, y, label=f"{sw.name}.wav")
193+
# TODO fix
194+
plt.bar(f, y, align="center", width=f[1]-f[0])
195+
196+
plt.legend()
197+
plt.show()
198+
return
161199

162-
plt.legend()
200+
if plot_type == "spectrogram":
201+
202+
plt.ylabel("Magnitude")
203+
plt.xlabel("Frequency")
204+
205+
if window_t == None:
206+
window_t = (0, 100)
207+
window_dur = (window_t[1] - window_t[0]) / 1000
208+
209+
for sw in sound_waves:
210+
211+
title += f" {sw.name}.wav"
212+
_, noise_borders = sw.find_endpoints(500, 5000)
213+
N = int(sw.wave.getframerate() * window_dur)
214+
f = sw.wave.getframerate() * np.arange(N / 2) / N
215+
T = window_dur
216+
# TODO check if okay to simply plot different freqs
217+
y = dft(sw, window_t=window_t, window_func=window_func)
218+
plt.imshow(y, origin="lower", cmap="viridis",
219+
extent=(0, T, 0, sw.wave.getframerate() / 2 / 1000))
220+
clr = np.random.rand(3,)
221+
if not sw.cleaned:
222+
for xc in noise_borders:
223+
plt.axvline(x=xc, color=clr)
224+
225+
plt.legend()
226+
plt.show()
227+
return
163228

164-
plt.show()
229+
raise ValueError(
230+
"Invalid plot_type passed to function plot_waves: " + plot_type)
165231

166232

167233
def list_waves(d, filter=None):
@@ -230,6 +296,43 @@ def generate_wave(name, n, t):
230296
return sw
231297

232298

299+
def dft(sw: SoundWave, window_t: int, window_func: str):
300+
"""
301+
Discrete Fourier transform. Window_t in ms.
302+
"""
303+
304+
if window_t == None:
305+
window_t = (0, len(sw.values))
306+
window_dur = (window_t[1] - window_t[0]) / 1000
307+
window_func = window_func.lower()
308+
309+
# Calculate number of samples
310+
N = int(sw.wave.getframerate() * window_dur)
311+
312+
# TODO check if we should be cutting this at all
313+
t_start = int(window_t[0] / 1000 * sw.wave.getframerate())
314+
t_end = t_start + N
315+
y = sw.values[t_start: t_end]
316+
317+
if len(y) == 0:
318+
raise ValueError()
319+
if window_func != "none":
320+
if window_func == "hamming":
321+
y = y * np.hamming(N)
322+
elif window_func == "hanning":
323+
y = y * np.hanning(N)
324+
else:
325+
return None
326+
327+
# Slash right side and normalize
328+
y_temp = np.fft.fft(y)[0:int(N / 2)] / N
329+
y_temp[1:] = 2*y_temp[1:]
330+
331+
# Calculate magnitude, remove complex part
332+
FFT_y = np.abs(y_temp)
333+
return FFT_y
334+
335+
233336
# Helper global dict of all loaded soundwaves.
234337
sound_waves: Dict[str, SoundWave] = {}
235338

@@ -271,7 +374,7 @@ def main():
271374
for f in cmd[1:]:
272375
sw = sound_waves.get(f, None)
273376
if sw == None:
274-
print(TEXT_NOT_LOADED + f)
377+
print(TEXT_NOT_LOADED % f)
275378
br = True
276379
continue
277380
to_clean.append(sw)
@@ -348,39 +451,92 @@ def main():
348451

349452
elif func == "plot":
350453

351-
plot_type = 'waveform'
454+
plot_type = "waveform"
455+
window_t = None
456+
window_f = "none"
457+
to_compare = []
352458

353-
i = 1
354-
if len(cmd) > 1:
355-
if cmd[1].lower() in ['waveform', 'spectogram', 'histogram']:
356-
plot_type = cmd[1].lower()
357-
i += 1
459+
# Fetch window t and function
460+
cmd_lowered = [x.lower() for x in cmd]
358461

359-
if len(cmd) == 1:
360-
to_compare = sound_waves.values()
361-
else:
362-
to_compare = []
363-
br = False
364-
for f in cmd[i:]:
365-
sw = sound_waves.get(f, None)
366-
if sw == None:
367-
print(TEXT_NOT_LOADED + f)
368-
br = True
369-
continue
370-
to_compare.append(sw)
371-
if br:
462+
prev = None
463+
err = False
464+
pot_missing = None
465+
arg_flags = ["-w", "-f", "-t"]
466+
for arg in cmd_lowered:
467+
468+
if arg == "plot":
372469
continue
373470

471+
# If special arg
472+
if prev in arg_flags:
473+
pot_missing = None
474+
475+
if prev == "-w":
476+
try:
477+
tmp = arg.split("-")
478+
timestamp_start = int(tmp[0])
479+
timestamp_end = int(tmp[1])
480+
if timestamp_start - timestamp_end >= 0:
481+
print(TEXT_INVALID_SYNTAX_PLOT_WINDOW_T)
482+
err = True
483+
break
484+
window_t = (timestamp_start, timestamp_end)
485+
except:
486+
print(TEXT_INVALID_SYNTAX_PLOT_WINDOW_T)
487+
err = True
488+
break
489+
490+
elif prev == "-f":
491+
if arg not in ["none", "hamming", "hanning"]:
492+
print(TEXT_INVALID_SYNTAX_PLOT_WINDOW_F % arg)
493+
err = True
494+
break
495+
window_f = arg
496+
497+
else:
498+
if arg not in ["waveform", "spectrogram", "histogram"]:
499+
print(TEXT_INVALID_SYNTAX_PLOT_TYPE % arg)
500+
err = True
501+
break
502+
plot_type = arg
503+
504+
# Simple file name
505+
else:
506+
if pot_missing != None:
507+
print(TEXT_NOT_LOADED % fn)
508+
err = True
509+
break
510+
511+
fn = cmd[cmd_lowered.index(arg)]
512+
sw = sound_waves.get(fn, None)
513+
if sw == None:
514+
pot_missing = fn
515+
else:
516+
to_compare.append(sw)
517+
518+
prev = arg
519+
520+
if err:
521+
continue
522+
523+
if len(to_compare) == 0:
524+
to_compare = sound_waves.values()
525+
374526
print(TEXT_PLOTTING)
375-
plot_waves(to_compare, type=plot_type)
527+
# try:
528+
plot_waves(to_compare, plot_type=plot_type,
529+
window_t=window_t, window_func=window_f)
530+
# except ValueError:
531+
# print(TEXT_INVALID_SYNTAX_PLOT_WINDOW)
376532

377533
elif func == "quit":
378534
quit()
379535

380536
else:
381537
print("\n=== Help ===\n")
382538
print(f"{TEXT_CLEAN}\n")
383-
print(f"{TEXT_GENERATE}\n")
539+
print(f"{TEXT_GEN}\n")
384540
print(f"{TEXT_HELP}\n")
385541
print(f"{TEXT_LIST}\n")
386542
print(f"{TEXT_LOAD}\n")

0 commit comments

Comments
 (0)