Skip to content

Instantly share code, notes, and snippets.

@severin-lemaignan
Created May 22, 2024 21:41
Show Gist options
  • Save severin-lemaignan/f7444443b4b6e405118a594354652a8c to your computer and use it in GitHub Desktop.
Save severin-lemaignan/f7444443b4b6e405118a594354652a8c to your computer and use it in GitHub Desktop.
Animated wavy circle that respond to voice (uses Skia, OpenCV, pyaudio, numpy)
import skia
import numpy as np
import cv2
import math
import time
import pyaudio
import struct
# Define the canvas size
width, height = 800, 800
# Create a bitmap and a canvas
bitmap = skia.Bitmap()
bitmap.setInfo(skia.ImageInfo.Make(width, height, skia.ColorType.kBGRA_8888_ColorType, skia.AlphaType.kOpaque_AlphaType))
bitmap.allocPixels()
canvas = skia.Canvas(bitmap)
# Define the circle parameters
center_x, center_y = width // 2, height // 2
base_radius = 300
num_segments = 64
angle_step = (2 * math.pi) / num_segments
nb_octaves = 7
# Define the animation parameters
frequency = 0.3 # Number of cycles per second
amplitude = 20 # Maximum offset in pixels
cv2.namedWindow('Blue Circle')
# Clear the canvas with black color
bg_paint = skia.Paint(Color=skia.ColorBLACK)
# Draw the circle (64 segments)
light_blue = skia.Color(113, 176, 210, 255)
fg_paint = skia.Paint(Color=light_blue)
fg_paint.setAntiAlias(True)
fg_paint.setStyle(skia.Paint.kStroke_Style)
# Audio setup using pyaudio
FORMAT = pyaudio.paInt16 # 16-bit resolution
CHANNELS = 1 # 1 channel
RATE = 16000
CHUNK = 1024 # Number of samples per frame
audio = pyaudio.PyAudio()
# Start streaming audio input
stream = audio.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=True,
frames_per_buffer=CHUNK)
# Function to get audio data and compute FFT
def get_fft_data():
data = stream.read(CHUNK, exception_on_overflow=False)
data_int = np.frombuffer(data, dtype=np.int16)
fft_data = np.fft.fft(data_int)
fft_data = np.abs(fft_data[200:200+num_segments*2:2]) # Take the first `num_segments` data points
#fft_data = np.fft.fft(data_int, n=num_segments+1)
#fft_data = np.abs(fft_data[1:]) # Take the first `num_segments` data points
return fft_data
fft_offsets = np.zeros(num_segments)
while True:
canvas.drawRect(skia.Rect.MakeWH(width, height), bg_paint)
fft_data = get_fft_data()
#max_fft = np.max(fft_data)
max_fft = 1000000.
last_fft_offsets = (fft_data / max_fft)
max_fft_val = np.max(last_fft_offsets)
volume_lvl = min(10, round(max_fft_val * 3))
fft_offsets += last_fft_offsets * amplitude * 3
fft_offsets *= 0.8
current_time = time.time()
# Precompute offsets for each segment
octaves = [
[amplitude * math.sin(2 * math.pi * frequency * current_time + (i*octave) * angle_step * (1 - 2*(octave % 2))) for i in range(num_segments)]
for octave in range(3,3+nb_octaves)
]
for idx, offsets in enumerate(octaves):
fg_paint.setStrokeWidth(idx-1)
fg_paint.setMaskFilter(skia.MaskFilter.MakeBlur(skia.kNormal_BlurStyle, idx*2+3 + ((volume_lvl // 4) * 2) ))
for i in range(num_segments):
angle = i * angle_step
radius = base_radius + offsets[i] + fft_offsets[i]
x0 = center_x + radius * math.cos(angle)
y0 = center_y + radius * math.sin(angle)
angle_next = (i + 1) * angle_step
radius_next = base_radius + offsets[(i + 1) % num_segments] + fft_offsets[(i + 1) % num_segments] # Use modulo for circular indexing
x1 = center_x + radius_next * math.cos(angle_next)
y1 = center_y + radius_next * math.sin(angle_next)
canvas.drawLine(x0, y0, x1, y1, fg_paint)
# Convert Skia bitmap to NumPy array
image = skia.Image.MakeFromBitmap(bitmap)
image_data = image.toarray()
# Display the image using OpenCV
cv2.imshow('Blue Circle', image_data)
if cv2.waitKey(30) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
@severin-lemaignan
Copy link
Author

Screenshot:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment