Skip to content

Commit

Permalink
Merge pull request #3 from gituser12981u2/develop
Browse files Browse the repository at this point in the history
updated to flake8 standards and fixed ci pipeline
  • Loading branch information
gituser12981u2 authored May 17, 2024
2 parents d1b8b0d + 29f40e0 commit 79e3f04
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 39 deletions.
21 changes: 15 additions & 6 deletions audio_visualizer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
from .visualizer import AudioVisualizer


def main():
"""Entry point for the audio visualizer command line interface."""
import argparse
parser = argparse.ArgumentParser(description="Terminal Audio Visualizer")
parser.add_argument('--mode', choices=['vertical', 'horizontal'], default='vertical', help="Choose visualization mode: vertical or horizontal")
parser.add_argument('--alpha', type=float, default=0.2, help="Smoothing factor for FFT; default is 0.2. Raise if you want it to be less charming")
parser.add_argument('--chunk', type=int, default=2048, help="Number of frames per buffer; default is 2048")
parser.add_argument('--rate', type=int, default=44100, help="Sampling rate; default is 44100")
parser.add_argument('--bar_count', type=int, default=75, help="Number of bars in the visualization; default is 75")
parser.add_argument('--mode', choices=['vertical', 'horizontal'],
default='vertical',
help="Choose visualization mode: vertical or horizontal") # noqa: E501
parser.add_argument('--alpha', type=float, default=0.2,
help="Smoothing factor for FFT; default is 0.2. Raise if you want it to be less charming") # noqa: E501
parser.add_argument('--chunk', type=int, default=2048,
help="Number of frames per buffer; default is 2048") # noqa: E501
parser.add_argument('--rate', type=int, default=44100,
help="Sampling rate; default is 44100")
parser.add_argument('--bar_count', type=int, default=75,
help="Number of bars in the visualization; default is 75") # noqa: E501
args = parser.parse_args()

visualizer = AudioVisualizer(mode=args.mode, alpha=args.alpha, chunk=args.chunk, rate=args.rate, bar_count=args.bar_count)
visualizer = AudioVisualizer(mode=args.mode, alpha=args.alpha,
chunk=args.chunk, rate=args.rate,
bar_count=args.bar_count)
visualizer.start()
94 changes: 63 additions & 31 deletions audio_visualizer/visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import time
import logging


class AudioVisualizer:
"""Audio Visualizer for terminal.
This class handles the visualization of audio data in the terminal using either a vertical or horizontal bar chart
This class handles the visualization of audio data in the terminal using
either a vertical or horizontal bar chart
Attributes:
mode (str): Visualization mode ('vertical' or 'horizontal').
Expand All @@ -23,9 +25,10 @@ class AudioVisualizer:
window (np.ndarray): Hamming window applied to audio data.
"""

def __init__(self, mode='vertical', alpha=0.2, chunk=2048, rate=44100, bar_count=75):
def __init__(self, mode='vertical', alpha=0.2, chunk=2048, rate=44100,
bar_count=75):
"""Initializes the AudioVisualizer with given parameters.
Args:
mode (str): Visualization mode ('vertical' or 'horizontal').
alpha (float): Smoothing factor for FFT.
Expand All @@ -40,9 +43,11 @@ def __init__(self, mode='vertical', alpha=0.2, chunk=2048, rate=44100, bar_count
self.audio = pyaudio.PyAudio()
self.stream = None
try:
self.stream = self.audio.open(format=self.FORMAT, channels=self.CHANNELS,
rate=self.RATE, input=True,
frames_per_buffer=self.CHUNK)
self.stream = self.audio.open(format=self.FORMAT,
channels=self.CHANNELS,
rate=self.RATE,
input=True,
frames_per_buffer=self.CHUNK)
except Exception as e:
logging.error(f"Failed to open stream: {e}")
self.smoothed_fft = np.zeros(self.CHUNK // 2)
Expand All @@ -54,8 +59,8 @@ def __init__(self, mode='vertical', alpha=0.2, chunk=2048, rate=44100, bar_count

def start(self):
"""Starts the audio visualization process.
Continuously reads audio data from the stream and updates the terminal visualization until interrupted.
Continuously reads audio data from the stream and updates the terminal
visualization until interrupted.
"""
try:
if self.mode == 'vertical':
Expand All @@ -70,25 +75,34 @@ def start(self):
self.cleanup()

def visualize_vertical(self):
"""Visualizes audio data in a vertical bar chart--from left to right."""
"""Visualizes audio data in a vertical bar chart
--from left to right."""
while True:
data = np.frombuffer(self.stream.read(self.CHUNK), dtype=np.int16)
# Apply window function
windowed_data = data * self.window
fft = np.abs(np.fft.fft(windowed_data).real)
fft = fft[:int(len(fft)/2)] # keep only the first half

# Smoothing factor
self.smoothed_fft = self.alpha * self.smoothed_fft + (1 - self.alpha) * fft

max_fft = np.max(self.smoothed_fft) if np.max(self.smoothed_fft) > 0 else 1 # Avoid division by zero
self.smoothed_fft = (
self.alpha * self.smoothed_fft
+ (1 - self.alpha) * fft
)

max_fft = (
np.max(self.smoothed_fft) if np.max(self.smoothed_fft)
> 0 else 1 # Avoid division by zero
)

# Clear the screen
os.system('cls' if os.name == 'nt' else 'clear')

# Visualization logic
indices = np.logspace(0, np.log10(len(self.smoothed_fft)), num=self.bar_count + 1, endpoint=True, base=10).astype(int) - 1
indices = np.unique(np.clip(indices, 0, len(self.smoothed_fft)-1)) # Ensure unique indices and within bounds
indices = np.logspace(0, np.log10(len(self.smoothed_fft)),
num=self.bar_count + 1, endpoint=True,
base=10).astype(int) - 1
# Ensure unique indices and within bounds
indices = np.unique(np.clip(indices, 0, len(self.smoothed_fft)-1))

for i in range(len(indices) - 1):
bar_values = self.smoothed_fft[indices[i]:indices[i+1]]
Expand All @@ -100,40 +114,58 @@ def visualize_vertical(self):
bar_value = 0

# Calculate the number of characters to print
num_chars = int(np.sqrt(bar_value / max_fft) * 50) if not np.isnan(bar_value) else 0
print('█' * num_chars) # Normalize and apply sqrt to enhance visibility
num_chars = (int(np.sqrt(bar_value / max_fft) * 50)
# Normalize and apply sqrt to enhance visibility
if not np.isnan(bar_value) else 0
)
print('█' * num_chars)

time.sleep(0.07) # control frame rate

def visualize_horizontal(self):
"""Visualizes audio data in a horizontal bar chart--from bottom to top."""
"""Visualizes audio data in a horizontal bar chart
--from bottom to top."""
# Determine the size of the terminal window
rows, columns = os.get_terminal_size()
max_bar_height = rows - 2

# Visualization logic
bar_count = columns # Number of bars to match the width of the terminal window
bar_count = columns # Num bars the width of the terminal
bar_heights = np.zeros(bar_count, dtype=int)

while True:
# Get the audio data and apply the FFT
data = np.frombuffer(self.stream.read(self.CHUNK), dtype=np.int16)
fft = np.abs(np.fft.fft(data * np.hamming(self.CHUNK)).real)[:self.CHUNK // 2]

self.smoothed_fft = self.alpha * self.smoothed_fft + (1 - self.alpha) * fft # Apply smoothing
max_fft = np.max(self.smoothed_fft) if np.max(self.smoothed_fft) > 0 else 1 # Normalize

fft = np.abs(np.fft.fft(
data * np.hamming(self.CHUNK)).real)[:self.CHUNK // 2]

self.smoothed_fft = (self.alpha * self.smoothed_fft
+ (1 - self.alpha) * fft) # Apply smoothing
max_fft = (np.max(self.smoothed_fft)
if np.max(self.smoothed_fft) > 0 else 1) # Normalize

# Calculate the height of each bar according to the FFT results
log_scale_index = np.logspace(0, np.log10(len(self.smoothed_fft)), num=bar_count, endpoint=True, base=10).astype(int) - 1
log_scale_index = np.unique(np.clip(log_scale_index, 0, len(self.smoothed_fft)-1)) # Ensure unique indices and within bounds
log_scale_index = np.logspace(0, np.log10(len(self.smoothed_fft)),
num=bar_count, endpoint=True,
base=10).astype(int) - 1
log_scale_index = np.unique(np.clip(log_scale_index, 0,
# unique indices in bounds
len(self.smoothed_fft)-1))

for i in range(len(log_scale_index) - 1):
bar_values = self.smoothed_fft[log_scale_index[i]:log_scale_index[i+1]]
bar_height = int(np.mean(bar_values) / max_fft * max_bar_height) if bar_values.size > 0 else 0
bar_values = self.smoothed_fft[
log_scale_index[i]:log_scale_index[i+1]
]
bar_height = (
int(np.mean(bar_values) / max_fft * max_bar_height)
if bar_values.size > 0 else 0
)

bar_heights[i] = bar_height

# Clear the frame buffer
frame_buffer = "\n".join("".join("█" if bar_heights[col] >= row else " " for col in range(bar_count - 1)) for row in range(max_bar_height - 1, -1, -1))
frame_buffer = "\n".join("".join("█" if bar_heights[col] >= row else " " # noqa: E501
for col in range(bar_count - 1)) for row in range(max_bar_height - 1, -1, -1)) # noqa: E501

# Clear the terminal and print the entire frame at once
os.system('cls' if os.name == 'nt' else 'clear')
Expand Down
14 changes: 12 additions & 2 deletions tests/test_visualizer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import unittest
import numpy as np
from unittest.mock import patch, MagicMock
from audio_visualizer.visualizer import AudioVisualizer


class TestAudioVisualizer(unittest.TestCase):
def setUp(self):
@patch('audio_visualizer.visualizer.pyaudio.PyAudio')
def setUp(self, MockPyAudio):
mock_stream = MagicMock()
mock_pyaudio_instance = MockPyAudio.return_vale
mock_pyaudio_instance.open.return_value = mock_stream

self.visualizer = AudioVisualizer(mode='vertical')
self.visualizer.stream = mock_stream

def test_initialization(self):
self.assertEqual(self.visualizer.mode, 'vertical')
Expand All @@ -23,10 +31,12 @@ def test_cleanup(self):
self.fail(f"cleanup method raised an exception: {e}")

def test_fft_calculation(self):
data = np.random.randint(-32768, 32767, self.visualizer.CHUNK).astype(np.int16)
data = (np.random.randint(-32768, 32767, self.visualizer.CHUNK)
.astype(np.int16))
windowed_data = data * self.visualizer.window
fft = np.abs(np.fft.fft(windowed_data).real)
self.assertEqual(len(fft), self.visualizer.CHUNK)


if __name__ == '__main__':
unittest.main()

0 comments on commit 79e3f04

Please sign in to comment.