diff --git a/.vscode/launch.json b/.vscode/launch.json index 86378de..5b9ba6c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "program": "beamshow.py", "console": "integratedTerminal", "justMyCode": true, - "args": ["-w","-b","-a","Notepad"] + "args": ["-w","-b","-a","Notepad", "--fps"] } ] } \ No newline at end of file diff --git a/beamshow.py b/beamshow.py index 75bb58c..662f755 100644 --- a/beamshow.py +++ b/beamshow.py @@ -7,7 +7,7 @@ import sys from effects.effect import Effect from effects.presets import Presets from util.color import Colors -from util.audio import AudioProcess +from util.audio import AudioProcess, find_all_inputs def print_displays() -> None: @@ -18,6 +18,12 @@ def print_displays() -> None: sys.exit(0) +def print_inputs() -> None: + devs = find_all_inputs() + print("\n".join((x["name"] for x in devs))) + sys.exit(0) + + class Beamshow: def __init__( self, @@ -313,6 +319,9 @@ def app_main() -> None: argparser.add_argument( "--list-displays", action="store_true", help="Show available displays" ) + argparser.add_argument( + "--list-inputs", action="store_true", help="Show available audio inputs" + ) argparser.add_argument( "-w", "--window", action="store_true", help="Display in a window" ) @@ -348,6 +357,9 @@ def app_main() -> None: if args.list_displays: print_displays() + if args.list_displays: + print_inputs() + show = Beamshow( audio_device_name=args.audio, render3d=args.render3d, diff --git a/effects/effect.py b/effects/effect.py index a597d80..94bd33d 100644 --- a/effects/effect.py +++ b/effects/effect.py @@ -31,22 +31,15 @@ class MovingEffect(Effect): """ -ring chase two colors square -square two color - line scan vh starfield spawn chance rainbow circle rotate -spot row - -alles größer - fix double spot 4 spots """ diff --git a/effects/presets.py b/effects/presets.py index b1b32a2..fd324ff 100644 --- a/effects/presets.py +++ b/effects/presets.py @@ -3,9 +3,11 @@ import pygame as pg from typing import Iterator, List from random import choice, randint, randrange +from effects.chase_circle import ChaseCircle from effects.effect import Effect from effects.line import Lines +from effects.rectangle import Rectangle from util.color import ( color_darken, color_randomize, @@ -38,9 +40,10 @@ class Presets: def __init__(self, bounds: pg.Rect, beat_reactive: bool = False) -> None: self.bounds = bounds self.beat_reactive = beat_reactive + self.shuffles = list(iter(self)) * 2 def default(self) -> List[Effect]: - return self.StrobeLine() + return self.Rectangle() def __getitem__(self, idx: str) -> List[Effect]: return getattr(self, idx)() @@ -52,18 +55,24 @@ class Presets: return ( func for func in dir(self) - if callable(getattr(self, func)) and not func.startswith("__") + if callable(getattr(self, func)) + and not (func.startswith("__") or func == "randomize") ) def randomize(self) -> str: - return choice( - [ - func - for func in dir(self) - if callable(getattr(self, func)) - and not (func.startswith("__") or func == "randomize") - ] - ) + selected = choice(self.shuffles) + self.shuffles.remove(selected) + if not self.shuffles: + self.shuffles = list(iter(self)) * 2 + return selected + # return choice( + # [ + # func + # for func in dir(self) + # if callable(getattr(self, func)) + # and not (func.startswith("__") or func == "randomize") + # ] + # ) def DoubleSpotRandomColor(self) -> List[Effect]: return [ @@ -358,28 +367,28 @@ class Presets: mover=transform_bounce( bounds=self.bounds, velocity=(0.5, 1), - x_factor=(3, 3), + x_factor=(2, 2), y_factor=(0.5, 1), ), ) ] - def FallingLine(self) -> List[Effect]: - bounds = pg.rect.Rect(0, 0, self.bounds.width, 30) - return [ - Lines( - bounds=bounds, - vectors=(((0, 15), (self.bounds.width, 15), color_wheel()),), - thickness=30, - mover=transform_falling( - bounds, - acceleration=0.8, - initial_pos=(0, 0), - initial_velocity=0.01, - on_beat_reset=True, - ), - ), - ] + # def FallingLine(self) -> List[Effect]: + # bounds = pg.rect.Rect(0, 0, self.bounds.width, 30) + # return [ + # Lines( + # bounds=bounds, + # vectors=(((0, 15), (self.bounds.width, 15), color_wheel()),), + # thickness=30, + # mover=transform_falling( + # bounds, + # acceleration=0.8, + # initial_pos=(0, 0), + # initial_velocity=0.01, + # on_beat_reset=True, + # ), + # ), + # ] def StrobeLine(self) -> List[Effect]: thickness = 50 @@ -481,3 +490,108 @@ class Presets: ) for i in range(count) ] + + def StrobeRect(self) -> List[Effect]: + thickness = 50 + color_lr = color_wheel(hue=0, increase=1, repeat=4) + color_ud = color_wheel(hue=180, increase=1, repeat=4) + color_lr_s = color_strobe(color=color_lr, rate=(30, 120), flash_color=color_ud) + color_ud_s = color_strobe(color=color_ud, rate=(30, 120), flash_color=color_lr) + return [ + Lines( + bounds=self.bounds, + vectors=( + ( + (0, thickness // 2), + (self.bounds.width, thickness // 2), + color_ud_s, + ), + ( + (0, self.bounds.height - thickness // 2), + (self.bounds.width, self.bounds.height - thickness // 2), + color_ud_s, + ), + ), + thickness=thickness, + mover=transform_static((0, 0)), + ), + Lines( + bounds=self.bounds, + vectors=( + ( + (thickness // 2, 0), + (thickness // 2, self.bounds.height), + color_lr_s, + ), + ( + (self.bounds.width - thickness // 2, 0), + (self.bounds.width - thickness // 2, self.bounds.height), + color_lr_s, + ), + ), + thickness=thickness, + mover=transform_static((0, 0)), + ), + ] + + def ChaseCircle(self) -> List[Effect]: + return [ + ChaseCircle( + bounds=self.bounds, + colors=( + color_wheel(increase=(75 if self.beat_reactive else 0)), + color_wheel(hue=180, increase=(75 if self.beat_reactive else 0)), + ), + on_beat_color=self.beat_reactive, + size=int(self.bounds.height * 0.7), + rot_speed=5, + thickness=80, + size_factor=1, + mover=transform_bounce( + bounds=self.bounds, + velocity=(0.1, 0.5), + x_factor=(0.5, 1), + y_factor=(0.1, 0.3), + ), + ) + ] + + def ChaseCircleLine(self) -> List[Effect]: + return [ + ChaseCircle( + bounds=self.bounds, + colors=( + color_wheel(increase=(75 if self.beat_reactive else 0)), + color_wheel(hue=180, increase=(75 if self.beat_reactive else 0)), + ), + on_beat_color=self.beat_reactive, + size=int(self.bounds.height * 0.7), + rot_speed=-20, + thickness=80, + size_factor=0.2, + mover=transform_bounce( + bounds=self.bounds, + velocity=(0.1, 0.5), + x_factor=(0.5, 1), + y_factor=(0.1, 0.3), + ), + ) + ] + + def Rectangle(self) -> List[Effect]: + return [ + Rectangle( + bounds=self.bounds, + color=color_wheel(increase=(75 if self.beat_reactive else 1)), + size=int(self.bounds.height*0.9), + aspect=1.0, + rot_speed=0.5, + thickness=80, + mover=transform_bounce( + bounds=self.bounds, + velocity=(0.1, 0.5), + x_factor=(0.5, 1), + y_factor=(0.1, 0.3), + ), + ) + ] diff --git a/effects/rectangle.py b/effects/rectangle.py new file mode 100644 index 0000000..5c461e1 --- /dev/null +++ b/effects/rectangle.py @@ -0,0 +1,125 @@ +from effects.effect import MovingEffect +from typing import Any, Optional +from util import XYCoord +from util.color import Colors, ColorGenerator, color_fade, copy_color +import math +import pygame as pg + +from util.transform import PositionGenerator + + +def calc_x(R, T, D): + a = 1 + R**2 + b = 2 * T * (1 + R) + c = T**2 * (1 + R**2) - D**2 + + return max( + (-b + math.sqrt(b**2 - 4 * a * c)) / (2 * a), + (-b - math.sqrt(b**2 - 4 * a * c)) / (2 * a), + ) + + +class Rectangle(MovingEffect): + def __init__( + self, + bounds: pg.Rect, + color: ColorGenerator, + size: int, + aspect: float = 1.0, + rot_speed: float = 0.5, + thickness: int = 30, + mover: Optional[PositionGenerator] = None, + *groups: pg.sprite.Group, + ) -> None: + self.color_gen = color + self.color = copy_color(next(color)) + self.fader = color_fade( + initial_color=self.color, end_color=next(self.color_gen), duration=10 + ) + self.angle = 45.0 + self.rot_speed = rot_speed + self.thickness = thickness + + x = calc_x(aspect, thickness, size) + print(x) + print(size) + + self.rsize = (x, (x + 2 * thickness) * aspect - 2 * thickness) + print(self.rsize) + + self.rect_image = pg.Surface(self.rsize) + self.rect_image.fill(Colors.Black) + self.rect_image.set_colorkey(Colors.Black) + + image = pg.Surface((size, size)) + image.fill(Colors.Black) + image.set_colorkey(Colors.Black) + super().__init__( + image, + pg.Rect( + bounds.centerx - size // 2, + bounds.centery - size // 2, + size, + size, + ), + mover, + *groups, + ) + + self.update(is_beat=True) + + def update(self, *args: Any, **kwargs: Any) -> None: + # self.rect.center = self.mover.send((self.rect.size, kwargs["is_beat"])) + + self.image.fill(Colors.Blue) + self.rect_image.fill(Colors.Green) + + # rc = next(self.fader) + # pg.draw.rect( + # self.rect_image, + # rc, + # ((0, 0), self.rsize), + # 0, + # ) + # pg.draw.rect( + # self.rect_image, + # Colors.Black, + # ( + # self.thickness // 2, + # self.thickness // 2, + # self.rsize[0] - self.thickness, + # self.rsize[1] - self.thickness, + # ), + # 0, + # ) + + # if kwargs["is_beat"]: + # new_color = next(self.color_gen) + # self.fader = color_fade( + # initial_color=self.color, end_color=new_color, duration=10 + # ) + # self.color = copy_color(new_color) + # pg.draw.rect( + # self.rect_image, + # Colors.White, + # ( + # self.thickness, + # self.thickness, + # self.rsize[0] - self.thickness * 2, + # self.rsize[1] - self.thickness * 2, + # ), + # 0, + # ) + + r2 = pg.transform.rotate(self.rect_image, self.angle) + print(self.image.get_width()-r2.get_width()) + + self.image.blit( + r2, + ( + (self.image.get_width() - r2.get_width()) // 2, + (self.image.get_height() - r2.get_height()) // 2, + ), + ) + + # self.angle = (self.angle + self.rot_speed) % (360) diff --git a/effects/scanreticle.py b/effects/scanreticle.py index a995fdd..7d81350 100644 --- a/effects/scanreticle.py +++ b/effects/scanreticle.py @@ -18,7 +18,7 @@ class ScanReticle(MovingEffect): ) -> None: self.colors = colors self.bounds = bounds - self.rect_size = min(self.bounds.width // 6, self.bounds.height // 6) + self.rect_size = min(self.bounds.width // 3, self.bounds.height // 3) image = pg.Surface(self.bounds.size) image.fill(Colors.Black) diff --git a/requirements.txt b/requirements.txt index ba810ca..70d31a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pygame==2.1.3 soundcard==0.4.2 -pyaudiowpatch==0.2.12.5 \ No newline at end of file +pyaudiowpatch==0.2.12.5 +aubio==0.4.9 \ No newline at end of file diff --git a/util/audio.py b/util/audio.py index 4a8b4bb..8c3c458 100644 --- a/util/audio.py +++ b/util/audio.py @@ -1,4 +1,5 @@ import time +from typing import List import aubio import pyaudiowpatch as pa @@ -7,6 +8,17 @@ import numpy as np import threading +def find_all_inputs() -> List[dict]: + devices: List[dict] = [] + p = pa.PyAudio() + api_info = p.get_host_api_info_by_type(pa.paWASAPI) + for i in range(api_info["deviceCount"]): + dev_info = p.get_device_info_by_host_api_device_index(api_info["index"], i) + if dev_info["maxInputChannels"] > 0: + devices.append(dev_info) + return devices + + def find_audio_device(p: pa.PyAudio, api: int, name: str) -> dict: api_info = p.get_host_api_info_by_type(api) for i in range(api_info["deviceCount"]): diff --git a/util/color.py b/util/color.py index 8e3e379..581237f 100644 --- a/util/color.py +++ b/util/color.py @@ -66,29 +66,15 @@ def color_fadeout(initial_color: pg.Color, fade_speed: float) -> ColorGenerator: def color_fade( initial_color: pg.Color, end_color: pg.Color, duration: int ) -> ColorGenerator: - color = copy_color(initial_color) - h, s, l, a = color.hsla - h2, s2, l2, a2 = end_color.hsla - h_inc = (h2 - h) / duration - s_inc = (s2 - s) / duration - l_inc = (l2 - l) / duration - a_inc = (a2 - a) / duration - h_f = float(h) - s_f = float(s) - l_f = float(l) - a_f = float(a) - + inc = 1.0 / duration + factor = 0.0 + col1 = copy_color(initial_color) + col2 = copy_color(end_color) while True: - yield color - color.hsla = int(h_f), int(s_f), int(l_f), int(a_f) - if h_f < h2: - h_f += h_inc - if s_f < s2: - s_f += s_inc - if l_f < l2: - l_f += l_inc - if a_f < a2: - a_f += a_inc + yield col1.lerp(col2, factor) + factor += inc + if factor > 1: + factor = 1 def color_cycle(seq: Sequence[pg.Color]) -> ColorGenerator: