From 8a53ecaa6db3bf9a2a84d44235dc90d85ae447d5 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 15 Feb 2026 17:12:23 -0700 Subject: [PATCH] push changes --- audio.py | 125 ++++++++++++++++++++++++++++++++++ gif.py | 42 ++++++++++++ main.py | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ test.py | 44 ++++++++++++ text.py | 71 +++++++++++++++++++ 5 files changed, 484 insertions(+) create mode 100644 audio.py create mode 100644 gif.py create mode 100755 main.py create mode 100644 test.py create mode 100644 text.py diff --git a/audio.py b/audio.py new file mode 100644 index 0000000..d689c43 --- /dev/null +++ b/audio.py @@ -0,0 +1,125 @@ +import pyaudio +import struct +import math +import time +import numpy as np + +from PIL import Image, ImageDraw + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + +width = 192 +height = 64 + +geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=5, + rotation=piomatter.Orientation.Normal, n_planes=6, n_temporal_planes=0) + +canvas = Image.new('RGB', (width, height), (0, 0, 0)) +draw = ImageDraw.Draw(canvas) + +framebuffer = np.asarray(canvas) + 0 # Make a mutable copy +matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.AdafruitMatrixBonnet, + framebuffer=framebuffer, + geometry=geometry) + +p = pyaudio.PyAudio() +info = p.get_host_api_info_by_index(0) +numdevices = info.get('deviceCount') + +for i in range(0, numdevices): + if (p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0: + print("Input Device id ", i, " - ", p.get_device_info_by_host_api_device_index(0, i).get('name')) + +print(p.get_device_info_by_index(0)['defaultSampleRate']) + +CHUNK = 1024 +FORMAT = pyaudio.paInt16 +CHANNELS = 2 +RATE = 44100 + +stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) + +def rms( data ): + count = len(data)/2 + format = "%dh"%(count) + shorts = struct.unpack( format, data ) + sum_squares = 0.0 + for sample in shorts: + n = sample * (1.0/32768) + sum_squares += n*n + return math.sqrt( sum_squares / count ) + + +run = True + +max_vol = 0 + +while run: + # print(rms(stream.read(CHUNK, exception_on_overflow = False))) + + print(max_vol) + + + + buffer = stream.read(CHUNK, exception_on_overflow = False) + waveform = np.frombuffer(buffer, dtype=np.int16) + + fft_complex = np.fft.fft(waveform, n=int(CHUNK*2)) + + max_val = math.sqrt(max(v.real * v.real + v.imag * v.imag for v in fft_complex)) + + if (max_val > max_vol): + max_vol = max_val + + # factor out the scale multiply. + + + draw.rectangle((0,0,width,height), fill=0x000000) + + for i in range(192): + + scale_value = (height / max_vol) * (1 + (i/100)) + + freq = i * 5 + + if (i < 2): + scale_value = (height / max_vol) * 0.9 + + if (i < 192): + + freq = i + + target = int(freq) + + # print(target) + + + v = fft_complex[target] + + dist = math.sqrt(v.real * v.real + v.imag * v.imag) + + mapped_dist = dist * scale_value + + if (max_vol < 500000): + mapped_dist = 1 + + draw.rectangle((i, height-mapped_dist, i, height), fill=0x880000) + + """ + for i,v in enumerate(fft_complex): + dist = math.sqrt(v.real * v.real + v.imag * v.imag) + mapped_dist = dist * scale_value + + draw.rectangle((i, 0, i, mapped_dist), fill=0x880000) + """ + + run=True + + max_vol = max_vol * 0.99 + + framebuffer[:] = np.asarray(canvas) + matrix.show() + + time.sleep(0.01) + diff --git a/gif.py b/gif.py new file mode 100644 index 0000000..ea7c33c --- /dev/null +++ b/gif.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +""" +Display an animated gif + +Run like this: + +$ python play_gif.py + +The animated gif is played repeatedly until interrupted with ctrl-c. +""" + +import time + +import numpy as np +import PIL.Image as Image + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + +width = 192 +height = 64 + +gif_file = "jake2.gif" + +canvas = Image.new('RGB', (width, height), (0, 0, 0)) +geometry = piomatter.Geometry(width=width, height=height, + n_addr_lines=5, rotation=piomatter.Orientation.Normal, n_planes=6, n_temporal_planes=0) +framebuffer = np.asarray(canvas) + 0 # Make a mutable copy +matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.AdafruitMatrixBonnet, + framebuffer=framebuffer, + geometry=geometry) + +with Image.open(gif_file) as img: + print(f"frames: {img.n_frames}") + while True: + for i in range(img.n_frames): + img.seek(i) + img2 = img.resize((192,64)) + canvas.paste(img2, (0,0)) + framebuffer[:] = np.asarray(canvas) + matrix.show() + time.sleep(0.05) diff --git a/main.py b/main.py new file mode 100755 index 0000000..e7ec615 --- /dev/null +++ b/main.py @@ -0,0 +1,202 @@ +#!/home/taco/venvs/visualizer/bin/python + +import pyaudio +import struct +import math +import time +import numpy as np + +from PIL import Image, ImageDraw + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + +# LED panel width +width = 192 + +# LED panel height +height = 64 + +# Top gradient color +color_1 = "#0000ff" + +# Bottom gradient color +color_2 = "#ff0000" + +# Should we mirror left and right channels? +mirror = False + +# Should we force draw a single pixel line? +zero_db_line = True + +# how long to hold maximum volume in seconds? +max_db_hold_time = 1 + +# how long between steps should we sleep? +delay = 0.01 + +# minimum maximum volume +minimum_max_volume = 4000000 + +# PyAudio config +CHUNK = 1024 +FORMAT = pyaudio.paInt16 +CHANNELS = 2 +RATE = 44100 + +geometry = piomatter.Geometry( + width=width, + height=height, + n_addr_lines=5, + rotation=piomatter.Orientation.Normal, + n_planes=7, + n_temporal_planes=1 +) + +canvas = Image.new('RGB', (width, height), (0, 0, 0)) +draw = ImageDraw.Draw(canvas) + +framebuffer = np.asarray(canvas) + 0 # Make a mutable copy +matrix = piomatter.PioMatter( + colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.AdafruitMatrixBonnet, + framebuffer=framebuffer, + geometry=geometry +) + +p = pyaudio.PyAudio() + +stream = p.open( + format=FORMAT, + channels=CHANNELS, + rate=RATE, + input=True, + frames_per_buffer=CHUNK +) + +run = True + +max_vol = minimum_max_volume +target_max_vol = minimum_max_volume + +left_channel = np.zeros(int(width)) +hf_left_channel = np.zeros(int(width/2)) +right_channel = np.zeros(int(width)) +hf_right_channel = np.zeros(int(width/2)) + + + +def generate_gradient(colour1: str, colour2: str, width: int, height: int) -> Image: + base = Image.new('RGB', (width, height), colour1) + top = Image.new('RGB', (width, height), colour2) + mask = Image.new('L', (width, height)) + mask_data = [] + for y in range(height): + mask_data.extend([int(255 * (y / height))] * width) + mask.putdata(mask_data) + base.paste(top, (0, 0), mask) + return base + +def clamp(minimum, maximum, value): + if (value > maximum): + return maximum + + if (value < minimum): + return minimum + + return value + +def mathCurve(step): + result = math.sin((step/40) + sin_offset)*4 + math.cos((step/30) + sin_offset*3)*4 + math.sin((step/35) + sin_offset*5)*4 + return math.floor(height/2 + result + 0.5) + +sin_offset = 0 + +db_hold_time = 0 + +gradient = generate_gradient(color_1, color_2, width, height) + +while run: + + left_channel = left_channel * 0.9 + right_channel = right_channel * 0.9 + + buffer = stream.read(CHUNK, exception_on_overflow = False) + waveform = np.frombuffer(buffer, dtype=np.int16) + + waveform = np.reshape(waveform, (CHUNK, 2)) + + #fft_complex_left = np.fft.fft(waveform[:, 0], n=int(CHUNK))[:width] + #fft_complex_right = np.fft.fft(waveform[:, 1], n=int(CHUNK))[:width] + + fft_complex_left = np.fft.fft(waveform[:, 0], n=int(CHUNK)) + fft_complex_right = np.fft.fft(waveform[:, 1], n=int(CHUNK)) + + max_val_left = math.sqrt(max(v.real * v.real + v.imag * v.imag for v in fft_complex_left)) + max_val_right = math.sqrt(max(v.real * v.real + v.imag * v.imag for v in fft_complex_right)) + + max_val = max(max_val_left, max_val_right) + + if (max_val > target_max_vol): + target_max_vol = max_val + db_hold_time = time.time() + max_db_hold_time + + if (max_vol < target_max_vol): + max_vol = max_vol + target_max_vol*0.1 + + canvas.paste(gradient) + + def calcDist(step, fft, fft_hist): + scale_value = (height / max_vol) * (1 + (step/100)) + + if (step < 2): + scale_value = (height / max_vol) * 0.9 + + use_value = fft_hist[step] + + v = fft[step] + + dist = math.sqrt(v.real * v.real + v.imag * v.imag) + + if dist > use_value: + fft_hist[step] = dist + use_value = dist + + return (use_value * scale_value) + + # LEFT FR + for i in range(0, int(width)): + + mapped_dist = calcDist(i, fft_complex_left, left_channel)/2 + + midpoint = mathCurve(i) + + draw.rectangle((i, 0, i, clamp(1, midpoint, midpoint - mapped_dist)), fill=0x000) + + # RIGHT FR + for i in range(0, int(width)): + mapped_dist = calcDist(i, fft_complex_right, right_channel)/2 + + horizontal_position = (width - 1) - i + if (mirror): + horizontal_position = i + + midpoint = mathCurve(horizontal_position) + + vertical_addition = 0 + if (zero_db_line): + vertical_addition = 1 + + draw.rectangle((horizontal_position, clamp(1, height, midpoint + vertical_addition + mapped_dist), horizontal_position, height), fill=0x000) + + if (max_vol > minimum_max_volume and db_hold_time < time.time()): + max_vol = target_max_vol = max_vol * 0.99 + + if (max_vol < minimum_max_volume): + max_vol = minimum_max_volume + + framebuffer[:] = np.asarray(canvas) + matrix.show() + + sin_offset = sin_offset + 0.01 + + time.sleep(delay) diff --git a/test.py b/test.py new file mode 100644 index 0000000..3ba52b1 --- /dev/null +++ b/test.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +Display a simple test pattern of 3 shapes on a single 64x32 matrix panel. + +Run like this: + +$ python simpletest.py + +""" + +import numpy as np +from PIL import Image, ImageDraw + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + +width = 192 +height = 64 + +geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=5, + rotation=piomatter.Orientation.Normal, n_planes=6, n_temporal_planes=4) + +canvas = Image.new('RGB', (width, height), (0, 0, 0)) +draw = ImageDraw.Draw(canvas) + +framebuffer = np.asarray(canvas) + 0 # Make a mutable copy +matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.AdafruitMatrixBonnet, + framebuffer=framebuffer, + geometry=geometry) + +# draw.rectangle((0, 0, 192, 64), fill=0x008800) +# draw.circle((18, 6), 4, fill=0x880000) +# draw.polygon([(28, 2), (32, 10), (24, 10)], fill=0x000088) + +draw.rectangle((0,0,1,64), fill=0x008800) +draw.rectangle((1,0,1,64), fill=0x880000) + +framebuffer[:] = np.asarray(canvas) +matrix.show() + +input("Press enter to exit") diff --git a/text.py b/text.py new file mode 100644 index 0000000..185b2d1 --- /dev/null +++ b/text.py @@ -0,0 +1,71 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +Display quote from the Adafruit quotes API as text scrolling across the +matrices. + +Requires the requests library to be installed. + +Run like this: + +$ python quote_scroller.py + +""" + +import numpy as np +import requests +from PIL import Image, ImageDraw, ImageFont + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + +# 128px for 2x1 matrices. Change to 64 if you're using a single matrix. +total_width = 192 +total_height = 64 + +bottom_half_shift_compensation = 1 + +font_color = (0, 128, 128) + +# Load the font +font = ImageFont.truetype("LindenHill-webfont.ttf", 40) + +quote_resp = requests.get("https://www.adafruit.com/api/quotes.php").json() + +text = f'{quote_resp[0]["text"]} - {quote_resp[0]["author"]}' +#text = "Sometimes you just want to use hardcoded strings. - Unknown" + +x, y, text_width, text_height = font.getbbox(text) + +full_txt_img = Image.new("RGB", (int(text_width) + 6, int(text_height) + 6), (0, 0, 0)) +draw = ImageDraw.Draw(full_txt_img) +draw.text((3, 3), text, font=font, fill=font_color) +full_txt_img.save("quote.png") + +single_frame_img = Image.new("RGB", (total_width, total_height), (0, 0, 0)) + +geometry = piomatter.Geometry(width=total_width, height=total_height, + n_addr_lines=5, rotation=piomatter.Orientation.Normal) +framebuffer = np.asarray(single_frame_img) + 0 # Make a mutable copy + +matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.AdafruitMatrixBonnet, + framebuffer=framebuffer, + geometry=geometry) + +print("Ctrl-C to exit") +while True: + for x_pixel in range(-total_width-1,full_txt_img.width): + if bottom_half_shift_compensation == 0: + # full paste + single_frame_img.paste(full_txt_img.crop((x_pixel, 0, x_pixel + total_width, total_height)), (0, 0)) + + else: + # top half + single_frame_img.paste(full_txt_img.crop((x_pixel, 0, x_pixel + total_width, total_height//2)), (0, 0)) + # bottom half shift compensation + single_frame_img.paste(full_txt_img.crop((x_pixel, total_height//2, x_pixel + total_width, total_height)), (bottom_half_shift_compensation, total_height//2)) + + framebuffer[:] = np.asarray(single_frame_img) + matrix.show()