"""
Audio Frequency Spectrum Visualizer (Tkinter + matplotlib)
- Select a WAV file
- Shows waveform (top) and animated FFT spectrum bars (bottom)
- Works with mono or stereo WAV files
- Uses scipy.io.wavfile to read WAV
- Animation scrolls through the audio buffer, computing FFT per frame to simulate live visualization
Limitations:
- Only WAV supported out of the box. For MP3, use pydub to convert to raw samples (instructions below).
- No audio playback included by default (keeps dependencies minimal). See comments for how to add playback.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from scipy.io import wavfile
import os
# -----------------------
# Visualization settings
# -----------------------
FRAME_SIZE = 2048 # samples per frame for FFT (power of two recommended)
HOP_SIZE = 1024 # step between frames (overlap)
SPECTRUM_BINS = 80 # number of bars to display
SAMPLE_REDUCE = 1 # reduce sampling rate by factor (keep 1 unless memory issue)
# -----------------------
# Helper utilities
# -----------------------
def load_wav_file(path):
"""Read WAV file and return (rate, samples as float32 mono)."""
rate, data = wavfile.read(path)
# normalize and convert to float32 in range [-1,1]
if data.dtype == np.int16:
data = data.astype(np.float32) / 32768.0
elif data.dtype == np.int32:
data = data.astype(np.float32) / 2147483648.0
elif data.dtype == np.uint8:
data = (data.astype(np.float32) - 128) / 128.0
else:
data = data.astype(np.float32)
# if stereo, convert to mono by averaging channels
if data.ndim == 2:
data = data.mean(axis=1)
# optionally downsample (by integer factor)
if SAMPLE_REDUCE > 1:
data = data[::SAMPLE_REDUCE]
rate = rate // SAMPLE_REDUCE
return rate, data
def compute_spectrum(frame, rate, n_bins=SPECTRUM_BINS):
"""
Compute FFT amplitude spectrum for a frame (1D array).
Returns frequency centers and amplitudes (log-scaled).
"""
# apply a window to reduce spectral leakage
window = np.hanning(len(frame))
frame_windowed = frame * window
# FFT
fft = np.fft.rfft(frame_windowed, n=FRAME_SIZE)
mags = np.abs(fft)
# convert to dB scale
mags_db = 20 * np.log10(mags + 1e-6)
freqs = np.fft.rfftfreq(FRAME_SIZE, d=1.0 / rate)
# reduce to n_bins by averaging contiguous bands (log spaced bins could be better)
# we'll use linear bins for simplicity
bins = np.array_split(np.arange(len(freqs)), n_bins)
bfreqs = []
bampl = []
for b in bins:
if len(b) == 0:
bfreqs.append(0)
bampl.append(-120.0)
continue
bfreqs.append(freqs[b].mean())
bampl.append(mags_db[b].mean())
# normalize amplitudes to 0..1 for plotting bars
a = np.array(bampl)
a = (a - a.min()) / (np.maximum(a.max() - a.min(), 1e-6))
return np.array(bfreqs), a
# -----------------------
# Main App
# -----------------------
class SpectrumVisualizerApp:
def __init__(self, root):
self.root = root
self.root.title("Audio Frequency Spectrum Visualizer")
self.root.geometry("1000x650")
# Top controls
ctrl = ttk.Frame(root)
ctrl.pack(side=tk.TOP, fill=tk.X, padx=8, pady=6)
self.path_var = tk.StringVar()
ttk.Label(ctrl, text="Audio File:").pack(side=tk.LEFT)
self.path_entry = ttk.Entry(ctrl, textvariable=self.path_var, width=60)
self.path_entry.pack(side=tk.LEFT, padx=6)
ttk.Button(ctrl, text="Browse", command=self.browse_file).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="Load", command=self.load_file).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="Start", command=self.start).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="Pause/Resume", command=self.toggle_pause).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="Stop", command=self.stop).pack(side=tk.LEFT, padx=4)
# Status
self.status_var = tk.StringVar(value="No file loaded")
ttk.Label(root, textvariable=self.status_var).pack(side=tk.TOP, anchor="w", padx=8)
# Matplotlib figure with two subplots (waveform + spectrum)
self.fig, (self.ax_wave, self.ax_spec) = plt.subplots(2, 1, figsize=(9, 6), gridspec_kw={'height_ratios':[1,1]})
plt.tight_layout(pad=3.0)
self.canvas = FigureCanvasTkAgg(self.fig, master=root)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# controls / internal state
self.sr = None
self.samples = None
self.num_frames = 0
self.current_frame_idx = 0
self.paused = True
self.ani = None
# waveform plot placeholders
self.wave_x = None
self.wave_plot = None
# spectrum bars
self.bar_container = None
self.freq_centers = None
# initialize plots
self.setup_empty_plots()
def setup_empty_plots(self):
self.ax_wave.clear()
self.ax_wave.set_title("Waveform (time domain)")
self.ax_wave.set_xlabel("Time (s)")
self.ax_wave.set_ylabel("Amplitude")
self.ax_spec.clear()
self.ax_spec.set_title("Frequency Spectrum (animated)")
self.ax_spec.set_xlabel("Frequency (Hz)")
self.ax_spec.set_ylabel("Normalized amplitude")
self.canvas.draw_idle()
def browse_file(self):
p = filedialog.askopenfilename(filetypes=[("WAV files", "*.wav"), ("All files", "*.*")])
if p:
self.path_var.set(p)
def load_file(self):
path = self.path_var.get().strip()
if not path or not os.path.exists(path):
messagebox.showerror("Error", "Please choose a valid WAV file.")
return
try:
self.sr, self.samples = load_wav_file(path)
except Exception as e:
messagebox.showerror("Error", f"Failed to read WAV: {e}")
return
# compute frames count
self.num_frames = max(1, (len(self.samples) - FRAME_SIZE) // HOP_SIZE + 1)
self.current_frame_idx = 0
self.status_var.set(f"Loaded: {os.path.basename(path)} | SR={self.sr} Hz | Samples={len(self.samples)} | Frames={self.num_frames}")
# draw full waveform
t = np.arange(len(self.samples)) / float(self.sr)
self.ax_wave.clear()
self.ax_wave.plot(t, self.samples, color='gray', linewidth=0.5)
self.ax_wave.set_xlim(t.min(), t.max())
self.ax_wave.set_ylim(-1.0, 1.0)
self.ax_wave.set_title("Waveform (time domain)")
self.ax_wave.set_xlabel("Time (s)")
self.ax_wave.set_ylabel("Amplitude")
# prepare spectrum bar placeholders using first frame
frame0 = self.samples[:FRAME_SIZE]
freqs, amps = compute_spectrum(frame0, self.sr, n_bins=SPECTRUM_BINS)
self.freq_centers = freqs
x = np.arange(len(amps))
self.ax_spec.clear()
self.bar_container = self.ax_spec.bar(x, amps, align='center', alpha=0.8)
self.ax_spec.set_xticks(x[::max(1,len(x)//10)])
# show frequency labels at a few ticks
tick_idx = np.linspace(0, len(freqs)-1, min(10, len(freqs))).astype(int)
tick_labels = [f"{int(freqs[i])}Hz" for i in tick_idx]
self.ax_spec.set_xticks(tick_idx)
self.ax_spec.set_xticklabels(tick_labels, rotation=45)
self.ax_spec.set_ylim(0, 1.02)
self.ax_spec.set_title("Frequency Spectrum (animated)")
# vertical line on waveform to show current frame
self.wave_marker = self.ax_wave.axvline(0, color='red', linewidth=1.0)
self.canvas.draw_idle()
def start(self):
if self.samples is None:
messagebox.showwarning("No file", "Load a WAV file first.")
return
if self.ani:
# reset animation
self.ani.event_source.stop()
self.ani = None
self.paused = False
# Create animation that updates every ~30 ms
self.ani = FuncAnimation(self.fig, self.update_frame, interval=30, blit=False)
self.canvas.draw_idle()
self.status_var.set("Playing visualization... (simulation)")
def toggle_pause(self):
if self.ani is None:
return
if self.paused:
self.paused = False
self.status_var.set("Resumed")
else:
self.paused = True
self.status_var.set("Paused")
def stop(self):
if self.ani:
self.ani.event_source.stop()
self.ani = None
self.current_frame_idx = 0
self.paused = True
self.status_var.set("Stopped")
# reset waveform marker
if hasattr(self, 'wave_marker') and self.wave_marker:
self.wave_marker.set_xdata(0)
self.canvas.draw_idle()
def update_frame(self, *args):
if self.samples is None or self.paused:
return
# compute frame start/end
start = int(self.current_frame_idx * HOP_SIZE)
end = start + FRAME_SIZE
if end > len(self.samples):
# loop to start for continuous visual demo
self.current_frame_idx = 0
start = 0
end = FRAME_SIZE
frame = self.samples[start:end]
# update waveform marker
t_pos = (start + FRAME_SIZE/2) / float(self.sr)
self.wave_marker.set_xdata(t_pos)
# compute spectrum
freqs, amps = compute_spectrum(frame, self.sr, n_bins=SPECTRUM_BINS)
# update bars
for rect, h in zip(self.bar_container, amps):
rect.set_height(h)
# optional: change bar colors based on amplitude
for rect, h in zip(self.bar_container, amps):
rect.set_color(plt.cm.viridis(h))
self.current_frame_idx += 1
if self.current_frame_idx >= self.num_frames:
self.current_frame_idx = 0 # loop
self.canvas.draw_idle()
# -----------------------
# Run the app
# -----------------------
def main():
root = tk.Tk()
app = SpectrumVisualizerApp(root)
root.mainloop()
if __name__ == "__main__":
main()
No comments:
Post a Comment