diff --git a/AudioExplorer.py b/AudioExplorer.py new file mode 100644 index 0000000..581295d --- /dev/null +++ b/AudioExplorer.py @@ -0,0 +1,208 @@ +### AudioExplorer ### +# Rev. 1.0, 4.3.2023 +# Open, plot and play near any soundfile +# Dynamically change fidelity and samplerate +# +# Dependencies: +# matplotlib +# pydub (requires ffmpeg) +# numpy +# tkinter +# +# Potential improvements: +# Automatically mirror visual zoom (set with matplotlib toolbar) to start and end times + +import os +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) +from matplotlib.backend_bases import key_press_handler +from matplotlib.figure import Figure + +import numpy as np +import threading +import sys, getopt +import tkinter as tk +import tkinter.ttk as ttk +from pydub import AudioSegment +from pydub.playback import play +from time import sleep + +BITS_PER_BYTE = 8 +DEFAULT_STARTTIME = 0 +DEFAULT_ENDTIME = 12500 +DEFAULT_SAMPLERATE = 11025 +DEFAULT_FIDELITY = 8 # Bitvalue +DEFAULT_AUDIOFILENAME = "Maien.wav" + +def configurePlot(figure): +# plt.style.use("seaborn-v0_8-whitegrid") + leftAxes = figure.add_axes([0.15,0.07,0.8,0.4]) + leftAxes.set_xlabel("s") + leftAxes.set_ylabel("Wert") + rightAxes = figure.add_axes([0.15,0.55,0.8,0.4]) + rightAxes.set_xlabel("s") + rightAxes.set_ylabel("Wert") + +def plotSound(sound, figure): + sec = sound.duration_seconds + try: + (left,right) = sound.split_to_mono() + leftData = left.get_array_of_samples() + rightData = right.get_array_of_samples() + except ValueError: + leftData = sound.get_array_of_samples() + rightData = sound.get_array_of_samples() + + + dataCount = len(leftData) + + time = np.linspace(0,dataCount/sound.frame_rate,dataCount) + (leftAxes,rightAxes) = figure.axes + leftAxes.plot(time,leftData,alpha=0.8) + rightAxes.plot(time,rightData,alpha=0.8) + +def downsampleAndCompress(sound,targetSamplerate=44100,targetFidelity=16): + # sample_width is in bytes as no-one considered an odd number of bits to be useful + # well, we need it so targetFidelity is in bits. Expand sample_width to bits + fidelityRatio = 2**(sound.sample_width*BITS_PER_BYTE-targetFidelity) + + newSound = sound.set_frame_rate(targetSamplerate) + + try: + (newLeft,newRight) = newSound.split_to_mono() + except ValueError: + newLeft = newSound + newRight = newSound + + newLeftData = np.multiply(np.divide(newLeft.get_array_of_samples(),fidelityRatio).astype('h'),fidelityRatio).astype('h') + newRightData = np.multiply(np.divide(newRight.get_array_of_samples(),fidelityRatio).astype('h'),fidelityRatio).astype('h') + newLeft = AudioSegment(newLeftData.tobytes(),frame_rate=targetSamplerate,sample_width=sound.sample_width,channels=1) + newRight = AudioSegment(newRightData.tobytes(),frame_rate=targetSamplerate,sample_width=sound.sample_width,channels=1) + + newSound = AudioSegment.from_mono_audiosegments(newLeft,newRight) + return newSound + +def playSound(sound): + playThread = threading.Thread(target=play,args=(sound,)) + playThread.start() + +def fullUpdate(sound, samplerate, fidelity, startTime, endTime, figure): + global newSound + newSound = downsampleAndCompress(sound[startTime:endTime], samplerate, fidelity) + + (leftAxes,rightAxes) = figure.axes + leftAxes.clear() + rightAxes.clear() + + plotSound(sound[startTime:endTime],figure) + plotSound(newSound,figure) + + figure.canvas.flush_events() + figure.canvas.draw() + + return newSound + +def on_key_press(event): + print(f"key {event.key} pressed") + +def updateSlider(event): + sliderVariable = str(event.widget.cget("variable")) + sliderValue = event.widget.getvar(sliderVariable) + +def main(argv): + + soundFileName = DEFAULT_AUDIOFILENAME + + root = tk.Tk() + root.wm_title("AudioExplorer") + + # define tk-Variables to store the widget values + startTime = tk.IntVar() + startTime.set(DEFAULT_STARTTIME) + endTime = tk.IntVar() + endTime.set(DEFAULT_ENDTIME) + targetFidelity = tk.IntVar() + targetFidelity.set(DEFAULT_FIDELITY) + targetSamplerate = tk.IntVar() + targetSamplerate.set(DEFAULT_SAMPLERATE) + + try: + opts, args = getopt.getopt(argv,"hf:b:e:s:r:",["help","file=","begin=","end=","samplerate=","resolution="]) + except getopt.GetoptError: + print("AudioExplorer.py -f [-b ][-e ] [-s ] [-r ]") + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print("AudioExplorer.py -f [-b ][-e ] [-s ] [-r ]") + sys.exit() + elif opt in ('-f', '--file'): + soundFilePath = arg + directory,soundFileName = os.path.split(soundFilePath) + elif opt in ('-b', '--begin'): + startTime.set(int(arg)) + elif opt in ('-e','--end'): + endTime.set(int(arg)) + elif opt in ('-s','--samplerate'): + targetSamplerate.set(int(arg)) + elif opt in ('-r','--resolution'): + targetFidelity.set(int(arg)) + + + sound = AudioSegment.from_file(soundFileName) + maxLength = len(sound) + + startTime.set(0) + endTime.set(maxLength) + shortSound = sound[startTime.get():endTime.get()] + + figure = Figure(figsize=(14,5), dpi=100) + canvas = FigureCanvasTkAgg(figure, master=root) + canvas.draw() + canvas.get_tk_widget().grid(row=1) + + toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False) + toolbar.grid(row=1) + toolbar.update() + canvas.get_tk_widget().grid(row=5) + + canvas.mpl_connect("key_press_event", on_key_press) + + configurePlot(figure) + global newSound + newSound = fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure) + + #plotSound(sound[startTime.get():endTime.get()],figure) + + frm = ttk.Frame(root, padding=10) + frm.grid() + tk.Label(frm, text="Filename: " + soundFileName).grid(row=2,column=0) + + tk.Button(frm, text="Play", command=lambda: playSound(newSound)).grid(row=2,column=1) + tk.Button(frm, text="Quit", command=root.destroy).grid(row=2,column=2) + + tk.Label(frm, text="Fidelity [Bits]").grid(row=3,column=0) + fidelitySlider = tk.Scale(frm, from_=1, to=16, tickinterval=1, variable=targetFidelity, length=400, orient=tk.HORIZONTAL)#, command=lambda x: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + fidelitySlider.grid(row=3,column=1) + fidelitySlider.bind("", lambda event: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + + tk.Label(frm, text="Samplerate [Hz]").grid(row=4,column=0) + samplerateSlider = tk.Scale(frm, from_=100, to=44100, tickinterval=10000, variable=targetSamplerate, length=400, orient=tk.HORIZONTAL)#, command=lambda x: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + samplerateSlider.grid(row=4,column=1) + samplerateSlider.bind("", lambda event: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + + tk.Label(frm, text="Start [ms]").grid(row=5,column=0) + startSlider = tk.Scale(frm, from_=0, to=maxLength-1, tickinterval=50000, variable=startTime, length=400, orient=tk.HORIZONTAL)#, command=lambda x: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + startSlider.grid(row=5,column=1) + startSlider.bind("", lambda event: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + + tk.Label(frm, text="End [ms]").grid(row=6,column=0) + endSlider = tk.Scale(frm, from_=1, to=maxLength, tickinterval=50000, variable=endTime, length=400, orient=tk.HORIZONTAL)#, command=lambda x: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + endSlider.grid(row=6,column=1) + endSlider.bind("", lambda event: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure)) + + + root.mainloop() + + +if __name__ == "__main__": + main(sys.argv[1:]) \ No newline at end of file