Created
May 22, 2024 21:41
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screenshot:
![image](https://private-user-images.githubusercontent.com/385379/332970572-285c5ed3-a16e-4609-8836-69e699ef3605.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjE3NDQ0NDksIm5iZiI6MTcyMTc0NDE0OSwicGF0aCI6Ii8zODUzNzkvMzMyOTcwNTcyLTI4NWM1ZWQzLWExNmUtNDYwOS04ODM2LTY5ZTY5OWVmMzYwNS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNzIzJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDcyM1QxNDE1NDlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xMTIyYTRlM2MwYTlkZjJkMzgwZjk4NjYxYTEzNGZmNWQxYWZhNjhmN2NjYWQyYjYyOTVmMTk1NzcwMGY4MWQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9._i22tgkXPkGw6sjgqdwtFkTdU0H5eWYRTZ9bGQ0FI10)