### 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:])