MathPhysTools/AudioExplorer.py

208 lines
7.6 KiB
Python

### 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 <soundfile> [-b <begintime in ms>][-e <endtime in ms>] [-s <samplerate in Hz>] [-r <bits of resolution>]")
sys.exit(2)
for opt, arg in opts:
if opt == '-h':
print("AudioExplorer.py -f <soundfile> [-b <begintime in ms>][-e <endtime in ms>] [-s <samplerate in Hz>] [-r <bits of resolution>]")
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("<ButtonRelease-1>", 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("<ButtonRelease-1>", 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("<ButtonRelease-1>", 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("<ButtonRelease-1>", lambda event: fullUpdate(shortSound,targetSamplerate.get(),targetFidelity.get(),startTime.get(), endTime.get(), figure))
root.mainloop()
if __name__ == "__main__":
main(sys.argv[1:])