commit cba3dd96d6379258965cab96d8fc0fe22e19911e Author: Mal Date: Tue Jun 16 12:27:53 2020 +0200 Init diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b64833a --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +DESTDIR := +ROOTDIR=/usr/share/pumpkin/ +SERVICE_PATH=/etc/systemd/system/ + +install: + pip install RPi.GPIO + mkdir -p $(DESTDIR)$(ROOTDIR) + mkdir -p $(DESTDIR)$(SERVICE_PATH) + cp __main__.py $(DESTDIR)$(ROOTDIR) + cp rgb.py $(DESTDIR)$(ROOTDIR) + cp waveloader.py $(DESTDIR)$(ROOTDIR) + cp config.py $(DESTDIR)$(ROOTDIR) + cp pumpkin.py $(DESTDIR)$(ROOTDIR) + cp pumpkin.service $(DESTDIR)$(SERVICE_PATH) + systemctl daemon-reload + +uninstall: + rm $(SERVICE_PATH)pumpkin.service + rm -R $(ROOTDIR) + diff --git a/README b/README new file mode 100644 index 0000000..5a1ef8a --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +RUN THE INSTALLER BY TYPING IN: +./installer + +IF SOME DEPENDENCIES ARE MISSING PLEASE INSTALL THEM WITH YOUR PACKAGE MANAGER. + +IF THE INSTALLER WAS SUCCESSFULL YOU HAVE TO START IT ONCE TO GET A DEFAULT CONFIG FILE: +sudo systemctl start pumpkin + +IT WILL CRASH BUT NOW YOU WILL HAVE A CUSTOMIZABLE CONFIG FILE. OPEN IT: +sudo nano /etc/pumpkin.conf + +TO MAKE IT START SUCCESSFULLY YOU HAVE TO GIVE THE ABSOLUTE PATH TO YOUR WAVE FOLDER: +AUDIO_PATH=/your/wave/folder + +THEN SPECIFY YOUR GPIOS FOR EVERY COLOR. IF THERE ARE MULTIPLE LEDS FOR ONE COLOR SEPARATE THEM WITH , LIKE THAT: +GPIOS_RED=11,12,13 + +NOW YOU HAVE A BASIC CONFIGURATION AND IT SHOULD RUN. START THE PUMPKIN AGAIN BY TYPING IN +sudo systemctl start pumpkin + +TO STOP IT TYPE +sudo systemctl stop pumpkin + +IF YOU WANT TO MAKE IT START AUTOMATICALLY AFTER BOOT WITHOUT LOGIN TYPE +sudo systemctl enable pumpkin + +TO DELETE THE AUTOSTART TYPE +sudo systemctl disable pumpkin + +IF YOU HAVE A FOG MACHINE YOU CAN ALSO SPECIFY ONE GPIO FOR THE EVAPORATOR AND ONE FOR THE PUMP: +GPIO_EVAPORATOR= +GPIO_PUMP= + +AND NOW HAVE FUN! diff --git a/__main__.py b/__main__.py new file mode 100755 index 0000000..3452837 --- /dev/null +++ b/__main__.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +from pumpkin import Pumpkin +from config import Config +from time import time +from time import sleep +from os import listdir +from os.path import isfile +from shutil import copy +from sys import argv +import atexit + + +def cleanup(): + pumpkin.cleanup() + + +CONFIG_PATH = '/etc/pumpkin.conf' + +# Make sure that a config file exists +if not isfile(CONFIG_PATH): + parts = argv[0].split('/') + pathDefaultConf = '/'.join(parts[:len(parts) - 1]) + '/pumpkin.conf' + + print(pathDefaultConf) + copy(pathDefaultConf, CONFIG_PATH) + +config = Config(CONFIG_PATH) + +AUDIO_PATH = config.getString('AUDIO_PATH') + +COLOR_RED = config.getInt('COLOR_RED') +COLOR_GREEN = config.getInt('COLOR_GREEN') +COLOR_BLUE = config.getInt('COLOR_BLUE') + +COLOR_BACKGROUND_RED = config.getInt('COLOR_BACKGROUND_RED') +COLOR_BACKGROUND_GREEN = config.getInt('COLOR_BACKGROUND_GREEN') +COLOR_BACKGROUND_BLUE = config.getInt('COLOR_BACKGROUND_BLUE') + +COLOR_FIRE_RED = config.getInt('COLOR_FIRE_RED') +COLOR_FIRE_GREEN = config.getInt('COLOR_FIRE_GREEN') +COLOR_FIRE_BLUE = config.getInt('COLOR_FIRE_BLUE') + +GPIOS_RED = config.getIntList('GPIOS_RED') +GPIOS_GREEN = config.getIntList('GPIOS_GREEN') +GPIOS_BLUE = config.getIntList('GPIOS_BLUE') +GPIO_TRIGGER = config.getInt('GPIO_TRIGGER') + +GPIO_EVAPORATOR = config.getInt('GPIO_EVAPORATOR') +GPIO_PUMP = config.getInt('GPIO_PUMP') +FOG_RUNTIME_MAX = config.getFloat('FOG_RUNTIME_MAX') + +GPIO_POWEROFF = config.getInt('GPIO_POWEROFF') +RETRIGGER_TIMEOUT = config.getFloat('RETRIGGER_TIMEOUT') + +PWM_FREQUENCY = config.getInt('PWM_FREQUENCY') + +pumpkin = Pumpkin(AUDIO_PATH, GPIOS_RED, GPIOS_GREEN, GPIOS_BLUE, GPIO_TRIGGER, PWM_FREQUENCY) +pumpkin.setAudioColor(COLOR_RED, COLOR_GREEN, COLOR_BLUE) +pumpkin.setBackgroundColor(COLOR_BACKGROUND_RED, COLOR_BACKGROUND_GREEN, COLOR_BACKGROUND_BLUE) +pumpkin.setFireColor(COLOR_FIRE_RED, COLOR_FIRE_GREEN, COLOR_FIRE_BLUE) +pumpkin.enableFog(GPIO_EVAPORATOR, GPIO_PUMP, FOG_RUNTIME_MAX) +pumpkin.enablePowerOff(GPIO_POWEROFF) + +atexit.register(cleanup) + +retriggerTimeout = RETRIGGER_TIMEOUT +t_after_trigger = time() - retriggerTimeout + +while True: + + try: + if pumpkin.isTriggered() and time() - t_after_trigger > retriggerTimeout: + pumpkin.speak() + t_after_trigger = time() + else: + pumpkin.lurk() + + sleep(0.5) + + except Exception as e: + print(e) + exit() diff --git a/config.py b/config.py new file mode 100644 index 0000000..eed0067 --- /dev/null +++ b/config.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +class Config: + def __init__(self, file): + self.COMMENT_MARK = '#' + + self.path = file + + try: + with open(file, 'r') as conf: + config = conf.read().split('\n') + except: + raise IOError('Couldn\'t read ' + file + '!') + + if not self.isValidFile(config): + raise Exception('Error: ' + file + ' is no valid config file!') + + self.values = {} + + for c in config: + if self.lineHasValue(c): + parts = c.split('=') + self.values[parts[0].strip()] = parts[1].strip() + + def getValue(self, key): + try: + return self.values[key] + except: + return None + + def getInt(self, key): + value = self.getValue(key) + + if value == None: + return None + + return int(value) + + def getFloat(self, key): + return float(self.getValue(key)) + + def getBool(self, key): + return bool(self.getValue(key)) + + def getString(self, key): + return self.getValue(key) + + def getList(self, key): + values = [] + + for v in self.getValue(key).split(','): + values.append(v.strip()) + + return values + + def getIntList(self, key): + values = [] + + for i in self.getList(key): + values.append(int(i)) + + return values + + def lineHasValue(self, line): + return not line.startswith(self.COMMENT_MARK) and len(line.replace(' ', '').replace('\n', '')) > 0 and line.count('=') == 1 + + def lineHasSyntaxError(self, line): + return not line.startswith(self.COMMENT_MARK) and len(line.replace(' ', '').replace('\n', '')) > 0 and line.count('=') != 1 + + def isValidFile(self, config): + count = 1 + for line in config: + if self.lineHasSyntaxError(line): + print(self.path + ' Line ' + str(count) + ': Invalid syntax!') + count += 1 + + return True diff --git a/configure b/configure new file mode 100755 index 0000000..72d6687 --- /dev/null +++ b/configure @@ -0,0 +1,29 @@ +#!/bin/bash + +function check_package() +{ + printf "\tChecking for ${1}... " + + ${1} --version &> /dev/null + + if [ ! $? == 0 ] + then + echo "Failed!" + echo "" + echo "Please install ${1}!" + exit 1 + fi + + echo "Found." +} + +echo "Checking for dependencies:" + +check_package python3 +check_package pip3 +check_package gcc +check_package make +check_package aplay + +echo "" +echo "Ready to install." diff --git a/installer b/installer new file mode 100755 index 0000000..683e19d --- /dev/null +++ b/installer @@ -0,0 +1,9 @@ +#!/bin/bash + +./configure + +if [ $? == 0 ] +then + echo "Installing..." + sudo make install +fi diff --git a/pumpkin.conf b/pumpkin.conf new file mode 100644 index 0000000..4b509dc --- /dev/null +++ b/pumpkin.conf @@ -0,0 +1,58 @@ +# The GPIO numbers are in board mode so you can count down on your +# Raspberry Pi to find the number: +# _____________ +# (1) (2) \ +# (3) (4) | +# (5) (6) | +# +# and so on... +# +# To get a view for the numbering install the python module gpiozero the following way: +# +# sudo pip install gpiozero +# +# Now if you type pinout you get a nice graphical overview for your pins. +# WARNING: Only use GPIOs that are NOT reserved for I²C, SPI or UART! + +PWM_FREQUENCY=60 + +# You can specify multiple GPIOS for one color separated by , +GPIOS_RED=11 +GPIOS_GREEN=12 +GPIOS_BLUE=13 + +# The path to your audio folder (must contain wave files!). +AUDIO_PATH=/path/to/audio/files + +# The color for sound visualization +COLOR_RED=0 +COLOR_GREEN=128 +COLOR_BLUE=255 + +# The color for silence parts of your audio visualization. +COLOR_BACKGROUND_RED=0 +COLOR_BACKGROUND_GREEN=0 +COLOR_BACKGROUND_BLUE=0 + +# The color for the flickering candle on standby. +COLOR_FIRE_RED=255 +COLOR_FIRE_GREEN=128 +COLOR_FIRE_BLUE=0 + +# The input pin for your motion sensor. +GPIO_TRIGGER=7 + +# The seconds the pumpkin waits to be triggered again after he has been triggered. +RETRIGGER_TIMEOUT=10 + +# The input pin for your evaporator activation. +GPIO_EVAPORATOR=0 + +# The input pin for your pump activation. +GPIO_PUMP=0 + +# The maximal runtime for pump and evaporator in seconds during speaking. +FOG_RUNTIME_MAX=3 + +# The input pin for a poweroff switch to power down the whole system. +GPIO_POWEROFF=0 diff --git a/pumpkin.py b/pumpkin.py new file mode 100644 index 0000000..780b0a8 --- /dev/null +++ b/pumpkin.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +import _thread +import atexit +import RPi.GPIO as GPIO +import os +from waveloader import WaveLoader +from rgb import RGBMatrix +from rgb import RGB +from subprocess import call +from time import time +from random import randint + + +class Pumpkin: + def __init__(self, soundpath, GPIOS_RED = [11], GPIOS_GREEN = [12], GPIOS_BLUE = [13], GPIO_TRIGGER = 15, PWM_FREQUENCY = 60): + self.soundfiles = [] + self.loadWaveFolder(soundpath) + + if len(self.soundfiles) == 0: + raise IOError('No audio files found in ' + soundpath + '!') + + GPIO.setmode(GPIO.BOARD) + + self.GPIOS_RED = GPIOS_RED + self.GPIOS_GREEN = GPIOS_GREEN + self.GPIOS_BLUE = GPIOS_BLUE + self.GPIO_TRIGGER = GPIO_TRIGGER + self.GPIO_EVAPORATOR = 0 + self.GPIO_PUMP = 0 + self.GPIO_POWEROFF = 0 + + self.fogRuntimeMax = 3 + + GPIO.setup(self.GPIO_TRIGGER, GPIO.IN) + + self.rgbControl = RGBMatrix(PWM_FREQUENCY) + + for r in self.GPIOS_RED: + self.rgbControl.addRed(r) + + for g in self.GPIOS_GREEN: + self.rgbControl.addGreen(g) + + for b in self.GPIOS_BLUE: + self.rgbControl.addBlue(b) + + self.color = RGB(255, 255, 255) + self.colorBackground = RGB(0, 0, 0) + self.colorFire = RGB(255, 55, 0) + + atexit.register(self.cleanup) + + def __del__(self): + self.cleanup() + + def cleanup(self): + GPIO.cleanup() + + def enableFog(self, GPIO_EVAPORATOR, GPIO_PUMP, runtimeSecondsMax = 3): + self.GPIO_EVAPORATOR = GPIO_EVAPORATOR + self.GPIO_PUMP = GPIO_PUMP + self.fogRuntimeMax = runtimeSecondsMax + + if self.GPIO_EVAPORATOR != 0: + GPIO.setup(self.GPIO_EVAPORATOR, GPIO.OUT) + + if self.GPIO_PUMP != 0: + GPIO.setup(self.GPIO_PUMP, GPIO.OUT) + + self.stopFog() + + def enablePowerOff(self, GPIO_POWEROFF): + self.GPIO_POWEROFF = GPIO_POWEROFF + + if self.GPIO_POWEROFF != 0: + self.powerSwitch = GPIO.setup(self.GPIO_POWEROFF, GPIO.IN) + + def loadWaveFolder(self, path): + if not path.endswith('/'): + path += '/' + + files = os.listdir(path) + + for f in files: + if f.endswith('.wav'): + self.soundfiles.append(path + f) + + def lurk(self): + if self.isTurnedOff(): + self.poweroff() + + self.rgbControl.candle(self.colorFire.r, self.colorFire.g, self.colorFire.b, 0.5) + + def speak(self): + self.startFog() + + soundfile = self.soundfiles[randint(0, len(self.soundfiles) - 1)] + + sound = WaveLoader(soundfile) + + duration = sound.frameCount / sound.frameRate + t_now = 0 + + player = _thread.start_new_thread(call, (['aplay', soundfile],)) + t_start = time() + + while t_now < duration: + t_now = time() - t_start + amplitude = sound.getCurrentLevelRealtime(t_now) + + if amplitude < 0.1: + red = self.colorBackground.r + green = self.colorBackground.g + blue = self.colorBackground.b + else: + amplitude = 1 + red = self.color.r * amplitude + green = self.color.g * amplitude + blue = self.color.b * amplitude + + self.rgbControl.setColor(red, green, blue) + + if t_now > self.fogRuntimeMax: + self.stopFog() + + self.stopFog() + + def startFog(self): + if self.GPIO_EVAPORATOR != 0: + GPIO.output(self.GPIO_EVAPORATOR, True) + + if self.GPIO_PUMP != 0: + GPIO.output(self.GPIO_PUMP, True) + + def stopFog(self): + if self.GPIO_EVAPORATOR != 0: + GPIO.output(self.GPIO_EVAPORATOR, False) + + if self.GPIO_PUMP != 0: + GPIO.output(self.GPIO_PUMP, False) + + def isTriggered(self): + return bool(GPIO.input(self.GPIO_TRIGGER)) + + def isTurnedOff(self): + if self.GPIO_POWEROFF == 0: + return False + + return bool(GPIO.input(self.GPIO_POWEROFF)) + + def poweroff(self): + call(['poweroff']) + + def setAudioColor(self, red, green, blue): + self.color = RGB(red, green, blue) + + def setBackgroundColor(self, red, green, blue): + self.colorBackground = RGB(red, green, blue) + + def setFireColor(self, red, green, blue): + self.colorFire = RGB(red, green, blue) + + def loadAudioFolder(self, path): + if os.path.isdir(path): + if not path.endswith('/'): + path += '/' + + for file in os.listdir(path): + if file.endswith('.wav'): + audioFiles.append(audioPath + file) + + @staticmethod + def findAudioFiles(): + audioFiles = [] + + for home in os.listdir('/home'): + audioPath = '/home/' + home + '/.config/pumpkin/audio/' + + if os.path.isdir(audioPath): + for file in os.listdir(audioPath): + audioFiles.append(audioPath + file) + + return audioFiles diff --git a/pumpkin.service b/pumpkin.service new file mode 100644 index 0000000..6c95062 --- /dev/null +++ b/pumpkin.service @@ -0,0 +1,5 @@ +[Service] +ExecStart=/usr/share/pumpkin/__main__.py + +[Install] +WantedBy=multi-user.target diff --git a/rgb.py b/rgb.py new file mode 100644 index 0000000..fff29d8 --- /dev/null +++ b/rgb.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 + +import RPi.GPIO as GPIO +from time import sleep +from time import time +from random import randint + + +class RGB: + def __init__(self, red, green, blue): + self.r = red + self.g = green + self.b = blue + + def getDutyCycles(self): + percent = RGB.rgbToPerc(self.r, self.g, self.b) + + return RGB(percent[0], percent[1], percent[2]) + + @staticmethod + def sanitizeColor(value): + if value < 0 or value > 255: + print('Value must be between 0 and 255! Fixing color value.') + return max(0, min(value, 255)) + + return value + + @staticmethod + def rgbToPerc(red, green, blue): + percent = 100 / 255 + + return (red * percent, green * percent, blue * percent) + + @staticmethod + def percToRGB(red, green, blue): + percent = 255 / 100 + + return (red * percent, green * percent, blue * percent) + + +class RGB_Control: + + def __init__(self, r, g, b, freq=60): + self.pins_allowed = (7, 11, 12, 13, 15, 16, 18, 19, 21, 22) + self.frequency = freq + + if r not in self.pins_allowed: + print("Fehler: r wurde ungültigem Pin zugewiesen!") + exit() + + if g not in self.pins_allowed: + print("Fehler: g wurde ungültigem Pin zugewiesen!") + exit() + + if b not in self.pins_allowed: + print("Fehler: b wurde ungültigem Pin zugewiesen!") + exit() + + try: + GPIO.setup(r, GPIO.OUT) + GPIO.setup(g, GPIO.OUT) + GPIO.setup(b, GPIO.OUT) + except: + GPIO.setmode(GPIO.BOARD) + + GPIO.setup(r, GPIO.OUT) + GPIO.setup(g, GPIO.OUT) + GPIO.setup(b, GPIO.OUT) + + self.r = GPIO.PWM(r, freq) + self.g = GPIO.PWM(g, freq) + self.b = GPIO.PWM(b, freq) + + self.r.start(0) + self.g.start(0) + self.b.start(0) + + self.r_alt = None + self.g_alt = None + self.b_alt = None + + self.intensity = 1 + + self.current_r = 0 + self.current_g = 0 + self.current_b = 0 + + + def __del__(self): + try: + GPIO.cleanup() + except: + pass + + def enableSecondLED(self, gpioRed, gpioGreen, gpioBlue): + GPIO.setup(gpioRed, GPIO.OUT) + GPIO.setup(gpioGreen, GPIO.OUT) + GPIO.setup(gpioBlue, GPIO.OUT) + + self.r_alt = GPIO.PWM(gpioRed, self.frequency) + self.g_alt = GPIO.PWM(gpioGreen, self.frequency) + self.b_alt = GPIO.PWM(gpioBlue, self.frequency) + + self.r_alt.start(0) + self.g_alt.start(0) + self.b_alt.start(0) + + def toggleSpectrum(self,fps=30,speed=1): + self.setColor(255,0,0) + + self.transition((255, 255, 0), duration = speed) + self.transition((0, 255, 0), duration = speed) + self.transition((0, 255, 255), duration = speed) + self.transition((0, 0, 255), duration = speed) + self.transition((255, 0, 255), duration = speed) + self.transition((255, 0, 0), duration = speed) + + + def flash(self, color = (255, 255, 255)): + color_start = (self.current_r, self.current_g, self.current_b) + self.setColor(color[0], color[1], color[2]) + sleep(1/30) + self.setColor(color_start[0], color_start[1], color_start[2]) + + + def pulse(self, from_color, to_color, freq=0.5): + duration = freq * 0.5 + + self.transition(from_color, duration=duration) + self.transition(to_color, duration=duration) + + + def fire(self, wind = 0.5, r = 255, g = 55, b = 0): + wind_power = 100 - (100 * wind) + + flicker = randint(int(wind_power), 100) * 0.01 + + color_start = (r * flicker, g * flicker, b * flicker) + + duration = randint(10, int(40 - wind * 30)) * 0.01 + + self.transition(color_start, duration = duration) + + def panic(self, times = 10, color = (255, 255, 255)): + for t in range(times): + self.flash(color = color) + sleep(1/30) + + def setColor(self, r, g, b): + perc = RGB.rgbToPerc(r, g, b) + + self.current_r = r + self.current_g = g + self.current_b = b + + self.r.ChangeDutyCycle(perc[0] * self.intensity) + self.g.ChangeDutyCycle(perc[1] * self.intensity) + self.b.ChangeDutyCycle(perc[2] * self.intensity) + + if self.r_alt != None: + self.r_alt.ChangeDutyCycle(perc[0] * self.intensity) + + if self.g_alt != None: + self.g_alt.ChangeDutyCycle(perc[1] * self.intensity) + + if self.b_alt != None: + self.b_alt.ChangeDutyCycle(perc[2] * self.intensity) + + def transition(self, color_finish, duration = 3, fps = 30): + start_r = self.current_r + start_g = self.current_g + start_b = self.current_b + + finish_r = color_finish[0] + finish_g = color_finish[1] + finish_b = color_finish[2] + + delta_r = finish_r - start_r + delta_g = finish_g - start_g + delta_b = finish_b - start_b + + + frames = duration * fps + frame_duration = 1 / fps + + # Step per frame for every color + stf_r = delta_r / frames + stf_g = delta_g / frames + stf_b = delta_b / frames + + self.setColor(self.current_r, self.current_g, self.current_b) + sleep(frame_duration) + + start_time = time() + + while time() - start_time < duration: + + self.current_r += stf_r + self.current_g += stf_g + self.current_b += stf_b + + if self.current_r > 255: + self.current_r = 255 + elif self.current_r < 0: + self.current_r = 0 + + if self.current_g > 255: + self.current_g = 255 + elif self.current_g < 0: + self.current_g = 0 + + if self.current_b > 255: + self.current_b = 255 + elif self.current_b < 0: + self.current_b = 0 + + self.setColor(self.current_r, self.current_g, self.current_b) + + sleep(frame_duration) + + +class RGBMatrix(RGB): + + def __init__(self, PWM_FREQUENCY = 60): + self.PWM_FREQUENCY = PWM_FREQUENCY + + self.gpiosRed = [] + self.gpiosGreen = [] + self.gpiosBlue = [] + + self.currentColor = RGB(0, 0, 0) + + GPIO.setmode(GPIO.BOARD) + + def __del__(self): + GPIO.cleanup() + + def setupLED(self, gpio): + GPIO.setup(gpio, GPIO.OUT) + led = GPIO.PWM(gpio, self.PWM_FREQUENCY) + led.start(0) + + return led + + def addRed(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().r) + self.gpiosRed.append(led) + + def addGreen(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().g) + self.gpiosGreen.append(led) + + def addBlue(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().b) + self.gpiosBlue.append(led) + + def setColor(self, red, green, blue): + rgb = RGB(red, green, blue) + + colorsDutyCycle = rgb.getDutyCycles() + + for red in self.gpiosRed: + red.ChangeDutyCycle(colorsDutyCycle.r) + + for green in self.gpiosGreen: + green.ChangeDutyCycle(colorsDutyCycle.g) + + for blue in self.gpiosBlue: + blue.ChangeDutyCycle(colorsDutyCycle.b) + + self.currentColor = RGB(rgb.r, rgb.g, rgb.b) + + def fadeToColor(self, red, green, blue, seconds, fps = 30): + timeStart = time() + timeEnd = timeStart + seconds + frameDuration = 1 / fps + + colorStart = self.currentColor + colorTarget = RGB(red, green, blue) + + deltaRed = colorTarget.r - colorStart.r + deltaGreen = colorTarget.g - colorStart.g + deltaBlue = colorTarget.b - colorStart.b + + while time() < timeEnd: + frameStart = time() + + progress = (time() - timeStart) / seconds + + self.setColor( + colorStart.r + deltaRed * progress, + colorStart.g + deltaGreen * progress, + colorStart.b + deltaBlue * progress + ) + + duration = time() - frameStart + + if duration < frameDuration: + sleep(frameDuration - duration) + + self.setColor(red, green, blue) + + def candle(self, red, green, blue, wind = 0.5): + darkenMax = 100 - wind * 100 + color = RGB(red, green, blue) + + intensity = randint(darkenMax, 100) / 100 + seconds = randint(darkenMax, 50) / 100 + + targetColor = RGB(color.r * intensity, color.g * intensity, color.b * intensity) + self.fadeToColor(targetColor.r, targetColor.g, targetColor.b, seconds) + + # Chance of 5% for flickering + flickering = randint(0, 100) < 5 + + if flickering: + intensityLight = 1 + intensityDark = 0.9 + times = randint(4, 10) + duration = randint(5, 30) / 100 + + colorLight = RGB(color.r * intensityLight, color.g * intensityLight, color.b * intensityLight) + colorDark = RGB(color.r * intensityDark, color.g * intensityDark, color.b * intensityDark) + + intensity = 1 + + for t in range(times): + progress = (1 / times) * t + intensity = abs(0.5 - progress) * 2 + colorDark = RGB( + color.r * intensityDark * intensity, + color.g * intensityDark * intensity, + color.b * intensityDark * intensity + ) + self.fadeToColor(colorLight.r, colorLight.g, colorLight.b, duration) + self.fadeToColor(colorDark.r, colorDark.g, colorDark.b, duration) + + def toggleSpectrum(self, seconds): + spectrum = [ + RGB(255, 0, 0), + RGB(255, 255, 0), + RGB(0, 255, 0), + RGB(0, 255, 255), + RGB(0, 0, 255), + RGB(255, 0, 255) + ] + + fadeDuration = seconds / len(spectrum) + + for color in spectrum: + self.fadeToColor(color.r, color.g, color.b, fadeDuration) + +def halloween(control): + + while True: + flash = randint(1, 100) + if flash >= 1 and flash <= 5: + t = randint(1, 3) + control.panic(times = t, color = (200, 255, 255)) + + control.fire(wind = 0.8) + + +def spectrum(control): + while True: + control.toggleSpectrum() + + +def fire(control, wind = 0.5): + while True: + control.fire(wind = wind) + + +if __name__ == "__main__": + try: + + control = RGB_Control(11, 12, 13) + + fire(control, wind = 0.9) + + GPIO.cleanup() + + except: + del control diff --git a/waveloader.py b/waveloader.py new file mode 100644 index 0000000..acea9b6 --- /dev/null +++ b/waveloader.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import wave + + +class WaveLoader: + def __init__(self, filename): + self.soundfile = wave.open(filename, 'rb') + + self.channels = self.soundfile.getnchannels() + self.sampleWidth = self.soundfile.getsampwidth() + self.frameCount = self.soundfile.getnframes() + self.frameRate = self.soundfile.getframerate() + self.filename = filename + self.amplitudeMax = 2 ** (self.sampleWidth * 8) + self.levelData = {} + self.amplitudeData = {} + + def __del__(self): + self.soundfile.close() + + def getCurrentLevelRealtime(self, seconds): + frameIndex = int(round(seconds * self.frameRate)) + + if frameIndex >= self.frameCount: + return 0 + + self.soundfile.setpos(frameIndex) + + level = int.from_bytes(self.soundfile.readframes(1)[0:self.sampleWidth], 'little') + + if level > self.amplitudeMax * 0.5: + level -= self.amplitudeMax + + return abs(level) / (self.amplitudeMax * 0.5) + + def getLevel(self, channel = 0): + if channel < self.channels: + return self.levelData[channel].copy() + + def getAmplitude(self, channel = 0): + if channel < self.channels: + return self.amplitudeData[channel].copy() + + def getLevelSum(self): + if self.channels == 1: + return self.levelData[0].copy() + + levelSum = [] + + for f in range(self.frameCount): + amplitudeSum = 0 + + for c in range(self.channels): + amplitudeSum += self.levelData[c][f] + + levelSum.append(amplitudeSum / self.channels) + + return levelSum + + def getCurrentLevel(self, seconds): + frameIndex = int(round(seconds * self.frameRate)) + + if frameIndex >= self.frameCount: + return 0 + + level = self.levelData[0][frameIndex] + return level + + level = 0 + + for c in range(self.channels): + level += self.levelData[c][frameIndex] + + return level / self.channels + + def loadData(self): + self.levelReset() + self.amplitudeReset() + soundfile = wave.open(self.filename, 'rb') + + for f in range(self.frameCount): + frame = soundfile.readframes(1) + + for c in range(self.channels): + byteOffset = c * self.sampleWidth + amplitude = int.from_bytes(frame[byteOffset:byteOffset + self.sampleWidth], 'little') + + if amplitude > self.amplitudeMax * 0.5: + amplitude -= self.amplitudeMax + + self.amplitudeData[c].append(amplitude / (self.amplitudeMax * 0.5)) + self.levelData[c].append(abs(amplitude) / (self.amplitudeMax * 0.5)) + + soundfile.close() + + def levelReset(self): + self.levelData = {} + + for c in range(self.channels): + self.levelData[c] = [] + + def amplitudeReset(self): + self.amplitudeData = {} + + for c in range(self.channels): + self.amplitudeData[c] = []