Update to local copy that supports animations

Also some lint fixes, but not all because this is a big enough change.
This commit is contained in:
Trysdyn Black 2025-06-13 11:03:20 -07:00
parent 603fad46f9
commit 1e88c125f9
16 changed files with 179 additions and 99 deletions

View file

@ -1,12 +1,13 @@
import array
import config
import os
import pyaudio
import random
import typing
import tkinter
import typing
from tkinter import filedialog
import pyaudio
import config
# Hide Pygame "Hello world" stuff. Why does this require an envvar :/
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
@ -16,12 +17,13 @@ from pygame.locals import *
__author__ = "Trysdyn Black"
__license__ = "ACSL v1.4"
__version__ = "1.0.0"
__copyright__ = "Copyright 2022, Trysdyn Black"
__version__ = "1.1.0"
__copyright__ = "Copyright 2025, Trysdyn Black"
class Audio:
"""Audio subsystem that monitors a microphone and determines avatar state.
"""
Audio subsystem that monitors a microphone and determines avatar state.
Methods
-------
@ -32,6 +34,8 @@ class Audio:
def __init__(self, channels: int, rate: int, frames: int, threshold: int = 5000, smoothing: int = 0) -> None:
"""
Init.
Parameters
----------
channels : int
@ -60,7 +64,8 @@ class Audio:
self.this_smooth = 0
def get_state(self) -> bool:
"""Check audio buffer and indicate if the avatar's mouth should be open this frame.
"""
Check audio buffer and indicate if the avatar's mouth should be open this frame.
Returns
-------
@ -84,7 +89,8 @@ class Audio:
class Tube:
"""Encapsulates avatar and windowing logic, handles drawing.
"""
Encapsulates avatar and windowing logic, handles drawing.
Methods
-------
@ -104,10 +110,13 @@ class Tube:
blink_frames: int = 0,
shake_intensity: int = 0,
shake_delay: int = 0,
animation_delay: int = 100,
win_size: typing.Optional[tuple] = None,
bg_color: tuple = (0, 255, 0),
) -> None:
"""
Init.
Parameters
----------
image_closed : string
@ -134,6 +143,9 @@ class Tube:
shake_delay : int
How many frames to prevent shaking after completing a prior shake.
Defaults to 0.
animation_delay : int
How many frames to wait between animation frames.
Defaults to 10.
win_size : tuple(int, int) or None
Window size as a tuple of ints (width, height).
Defaults to None, which uses the image size of image_closed instead.
@ -141,6 +153,7 @@ class Tube:
Background color as a tuple of ints (red, green, blue).
Defaults to (0, 255, 0) for greenscreen green.
"""
self.frame_num = 0
self.open_frames = 0
self.blinked_frames = 0
@ -149,22 +162,38 @@ class Tube:
pygame.display.set_caption("pyngtube")
# Closed mouth image is mandatory
self.image_closed = pygame.image.load(image_closed).convert_alpha()
if not isinstance(image_closed, list):
image_closed = [image_closed]
self.image_closed = []
for i in image_closed:
self.image_closed.append(pygame.image.load(i).convert_alpha())
# Open mouth image is optional
if image_open:
self.image_open = pygame.image.load(image_open).convert_alpha()
if not isinstance(image_open, list):
image_open = [image_open]
self.image_open = []
for i in image_open:
self.image_open.append(pygame.image.load(i).convert_alpha())
else:
self.image_open = None
# Blinking images are optional
if image_blink_closed:
self.image_blink_closed = pygame.image.load(image_blink_closed).convert_alpha()
if not isinstance(image_blink_closed, list):
image_blink_closed = [image_blink_closed]
self.image_blink_closed = []
for i in image_blink_closed:
self.image_blink_closed.append(pygame.image.load(i).convert_alpha())
else:
self.image_blink_closed = None
if image_blink_open:
self.image_blink_open = pygame.image.load(image_blink_open).convert_alpha()
if not isinstance(image_blink_open, list):
image_blink_open = [image_blink_open]
self.image_blink_open = []
for i in image_blink_open:
self.image_blink_open.append(pygame.image.load(i).convert_alpha())
else:
self.image_blink_open = None
@ -172,22 +201,24 @@ class Tube:
self.blink_chance = blink_chance
self.blink_frames = blink_frames
self.animation_delay = animation_delay
# If we only have a closed-mouth image (why?) use it for open-mouth too
using_image_open = self.image_open if self.image_open else self.image_closed
using_image_open = self.image_open or self.image_closed
# State map for which image to use. (Mouth Opened?, Blinked?)
# If we don't have blinked images, we use the non-blinked images for those slots
self.state_map = {
(False, False): self.image_closed,
(True, False): using_image_open,
(False, True): self.image_blink_closed if self.image_blink_closed else self.image_closed,
(True, True): self.image_blink_open if self.image_blink_open else using_image_open,
(False, True): self.image_blink_closed or self.image_closed,
(True, True): self.image_blink_open or using_image_open,
}
# If we specified a window size, use it. If not use closed image size
# Then we pad by 10px to allow for shaking
if not win_size:
win_size = self.image_closed.get_size()
win_size = self.image_closed[0].get_size()
self.win_size = (
win_size[0] + (shake_intensity * 2),
@ -202,11 +233,12 @@ class Tube:
self.shake_delay = shake_delay
# Create our drawing buffer and initialize true display size
self.buf = pygame.surface.Surface(self.image_closed.get_size())
self.buf = pygame.surface.Surface(self.image_closed[0].get_size())
self.display = pygame.display.set_mode(self.win_size, pygame.RESIZABLE)
def should_blink(self) -> bool:
"""Check if we're currently blinking and check blink_chance to indicate if we should be blinking this frame.
"""
Check if we're currently blinking and check blink_chance to indicate if we should be blinking this frame.
Returns
-------
@ -228,8 +260,8 @@ class Tube:
return False
def update(self, opened: bool, blinked: bool) -> None:
"""Perform updates and drawing for one frame. This should be called once a frame and fed avatar state info on
open mouth and blinking. This handles all the other placement, style, and drawing functions from there.
"""
Perform updates and drawing for one frame.
Parameters
----------
@ -238,6 +270,12 @@ class Tube:
blinked : bool
true if the avatar's eyes should be closed this frame.
"""
self.frame_num += 1
frame = int(self.frame_num / self.animation_delay)
if frame >= len(self.state_map[(False, False)]):
self.frame_num = 0
frame = 0
# Blank with bg_color
self.display.fill(self.bg_color)
self.buf.fill(self.bg_color)
@ -248,7 +286,7 @@ class Tube:
i = self.shake_intensity
# Figure out which image we're drawing this frame using our state map
this_frame_image = self.state_map[(opened, blinked)]
this_frame_image = self.state_map[(opened, blinked)][frame]
# If the mic state is opened and an open mouth image exists, blit it
# to the drawing buffer. Otherwise use the closed mouth image.
@ -315,6 +353,7 @@ if __name__ == "__main__":
blink_frames=c.blink_frames,
bg_color=c.bg_color,
shake_delay=c.shake_delay,
animation_delay=c.animation_delay,
shake_intensity=c.shake_intensity,
)
rehash = False