208 lines
7.6 KiB
Python
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:]) |