diff --git a/pizzactrl/fs_names.py b/pizzactrl/fs_names.py index 2f1f6d5..ddd593d 100644 --- a/pizzactrl/fs_names.py +++ b/pizzactrl/fs_names.py @@ -80,14 +80,15 @@ class StoryFile(FileHandle): FileHandle.__init__(self, name, FileType.STORY) -REC_NAME = RecFile('name.wav') -REC_MY_IBK = RecFile('my_ibk.wav') +REC_NAME = RecFile('name.mp3') +REC_MY_IBK = RecFile('my_ibk.mp3') REC_PORTRAIT = RecFile('portrait.jpg') -REC_CITY_NAME = RecFile('city_name.wav') -REC_CITY_DESC = RecFile('city_description.wav') -REC_CITY_SOUND = RecFile('city_sound.wav') +REC_CITY_NAME = RecFile('city_name.mp3') +REC_CITY_DESC = RecFile('city_description.mp3') +REC_CITY_SOUND = RecFile('city_sound.mp3') REC_DRAW_CITY = RecFile('city_video.h264') REC_CITY_PHOTO = RecFile('city_drawing.jpg') +REC_MERGED_VIDEO = RecFile('video.mp4') SFX_ERROR = SfxFile('error') SFX_ERROR_DE = SfxFile('error-de') diff --git a/pizzactrl/gpio_pins.py b/pizzactrl/gpio_pins.py index 040658f..1415b33 100644 --- a/pizzactrl/gpio_pins.py +++ b/pizzactrl/gpio_pins.py @@ -1,5 +1,7 @@ # GPIO pin definitions +BTN_START = 26 # "Start" button (begin the performance) TODO review + BTN_BACK_GPIO = 14 # "Back" button (user input) BTN_FORWARD_GPIO = 18 # "Forward" button (user input) diff --git a/pizzactrl/hal_serial.py b/pizzactrl/hal_serial.py index e30d8db..b347e0f 100644 --- a/pizzactrl/hal_serial.py +++ b/pizzactrl/hal_serial.py @@ -4,7 +4,8 @@ from time import sleep from enum import Enum from typing import Any, List -from scipy.io.wavfile import write as writewav +# from scipy.io.wavfile import write as writewav +import pydub import sounddevice as sd import soundfile as sf @@ -13,7 +14,7 @@ import numpy as np from . import gpio_pins from picamera import PiCamera -from gpiozero import Button, OutputDevice, PWMOutputDevice, PWMLED +from gpiozero import Button import serial @@ -53,6 +54,8 @@ class PizzaHAL: def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE): self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=None) + self.btn_start = Button(gpio_pins.BTN_START) + self.camera = None self.soundcache = {} @@ -120,7 +123,8 @@ def turn_off(hal: PizzaHAL): def wait_for_input(hal: PizzaHAL, go_callback: Any, - back_callback: Any, **kwargs): + back_callback: Any, to_callback: Any, + timeout=120, **kwargs): """ Blink leds on buttons. Wait until the user presses a button, then execute the appropriate callback @@ -128,12 +132,16 @@ def wait_for_input(hal: PizzaHAL, go_callback: Any, :param hal: The hardware abstraction object :param go_callback: called when button 'go' is pressed :param back_callback: called whan button 'back' is pressed + :param to_callback: called on timeout + :param timeout: inactivity timeout in seconds (default 120) """ - resp = hal.send_cmd(SerialCommands.USER_INTERACTION) + resp = hal.send_cmd(SerialCommands.USER_INTERACTION, timeout) if resp == 'B': go_callback(**kwargs) elif resp == 'R': back_callback(**kwargs) + else: + to_callback(**kwargs) @blocking @@ -200,8 +208,13 @@ def record_sound(hal: PizzaHAL, filename: Any, duration: int, myrecording = sd.rec(int(duration * AUDIO_REC_SR), samplerate=AUDIO_REC_SR, channels=2) + # TODO user interaction instead sd.wait() # Wait until recording is finished - writewav(str(filename), AUDIO_REC_SR, myrecording) + # TODO test + myrecording = np.int16(myrecording) + song = pydub.AudioSegment(myrecording.tobytes(), frame_rate=AUDIO_REC_SR, sample_width=2, channels=2) + song.export(str(filename), format="mp3", bitrate="320k") + # ALTERNATIVE writewav(str(filename), AUDIO_REC_SR, myrecording) if cache: hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR) diff --git a/pizzactrl/statemachine.py b/pizzactrl/statemachine.py index 2235e1d..1fc3b58 100644 --- a/pizzactrl/statemachine.py +++ b/pizzactrl/statemachine.py @@ -1,16 +1,15 @@ import logging import os.path -import threading from typing import Any from time import sleep from enum import Enum, auto +from subprocess import call -from pizzactrl import fs_names, sb_dummy, sb_de_alt +from pizzactrl import fs_names, sb_dummy from .storyboard import Activity - from .hal_serial import play_sound, take_photo, record_video, record_sound, turn_off, \ PizzaHAL, init_camera, init_sounds, wait_for_input, \ light_layer, backlight, move_vert, move_hor, rewind @@ -24,6 +23,7 @@ class State(Enum): IDLE_START = auto() PLAY = auto() PAUSE = auto() + REWIND = auto() IDLE_END = auto() SHUTDOWN = auto() ERROR = -1 @@ -54,7 +54,8 @@ class Statemachine: def __init__(self, story_de: Any=None, story_en: Any=None, - move: bool = False): + move: bool = False, + loop: bool = True): self.state = State.POWER_ON self.hal = PizzaHAL() self.story = None @@ -64,6 +65,7 @@ class Statemachine: self.lang = Language.NOT_SET self.move = move self.test = False + self.loop = loop def run(self): logger.debug(f'Run(state={self.state})') @@ -118,66 +120,50 @@ class Statemachine: # check scroll positions and rewind if necessary turn_off(self.hal) - #rewind(self.hal.motor_ud, self.hal.ud_sensor) - if not os.path.exists(fs_names.USB_STICK): logger.warning('USB-Stick not found.') self.state = State.ERROR return + # Callback for start when blue button is held + self.hal.btn_start.when_activated = self._start_or_rewind + # play a sound if everything is alright play_sound(self.hal, fs_names.SFX_POST_OK) - # Callback for start when blue button is held - # TODO add start button - #self.hal.btn_forward.when_deactivated = self._start - self.state = State.IDLE_START def _idle_start(self): """ - Device is armed. Wait for user to hold blue button to start + Device is armed. Wait for user to press start button """ pass - def _start(self): + def _start_or_rewind(self): """ - Start playback when blue button is held for 3s - """ - t = 0. - while (self.hal.btn_forward.inactive_time < 3.0) and \ - not self.hal.btn_forward.is_active: - t = self.hal.btn_forward.inactive_time + Callback function. - if t > 3.0: - self.hal.btn_forward.when_deactivated = None - if not self.hal.btn_back.is_active: - self.alt = True + If statemachine is in idle state, start playback when start + button is pressed (released). + + If statemachine is playing, trigger rewind and start fresh + """ + if self.state == State.IDLE_START: self.state = State.PLAY + return + if self.state == State.PLAY: + self.state = State.REWIND def _play(self): """ Run the storyboard """ logger.debug(f'play') - if not self.alt: - play_sound(self.hal, fs_names.SND_SELECT_LANG) - wait_for_input(self.hal, - self._lang_de, - self._lang_en) - - sleep(0.5) + if self.test: + self.story = sb_dummy.STORYBOARD else: - self.story = sb_de_alt.STORYBOARD - - try: - if self.story is None: - self.story = sb_dummy.STORYBOARD - except AttributeError: - pass - finally: - if self.test: - self.story = sb_dummy.STORYBOARD + # TODO reenable language selection + self.story = self.story_en for chapter in iter(self.story): logger.debug(f'playing chapter {chapter}') @@ -187,16 +173,17 @@ class Statemachine: if act.activity is Activity.WAIT_FOR_INPUT: wait_for_input(hal=self.hal, go_callback=chapter.mobilize, - back_callback=chapter.rewind) - elif act.activity is Activity.ADVANCE_UP: - if chapter.move and self.move: - logger.debug( - f'advance({self.hal.motor_ud}, ' - f'{self.hal.ud_sensor})') - advance(motor=self.hal.motor_ud, - sensor=self.hal.ud_sensor) - elif not self.move: - play_sound(self.hal, fs_names.StoryFile('stop')) + back_callback=chapter.rewind, + to_callback=self._start_or_rewind) + # elif act.activity is Activity.ADVANCE_UP: + # if chapter.move and self.move: + # logger.debug( + # f'advance({self.hal.motor_ud}, ' + # f'{self.hal.ud_sensor})') + # advance(motor=self.hal.motor_ud, + # sensor=self.hal.ud_sensor) + # elif not self.move: + # play_sound(self.hal, fs_names.StoryFile('stop')) else: try: { @@ -206,22 +193,35 @@ class Statemachine: Activity.TAKE_PHOTO: take_photo, Activity.LIGHT_LAYER: light_layer, Activity.LIGHT_BACK: backlight, - # Activity.ADVANCE_UP: noop - + Activity.ADVANCE_UP: move_vert, + Activity.ADVANCE_LEFT: move_hor }[act.activity](self.hal, **act.values) except KeyError: logger.exception('Caught KeyError, ignoring...') pass - self.state = State.IDLE_END + self.state = State.REWIND + + def _rewind(self): + """ + Rewind all scrolls, post-process videos + """ + # postprocessing + cmdstring = f'MP4Box -add {fs_names.REC_CITY_DESC} -add{fs_names.REC_DRAW_CITY} {fs_names.REC_MERGED_VIDEO}' + call([cmdstring], shell=True) + + if self.move: + rewind(self.hal) + + if self.loop: + self.state = State.IDLE_START + else: + self.state = State.IDLE_END def _idle_end(self): """ Initialize shutdown """ - if self.move: - rewind(self.hal.motor_ud, self.hal.ud_sensor) - self.state = State.SHUTDOWN def _shutdown(self): diff --git a/pizzactrl/storyboard.py b/pizzactrl/storyboard.py index ae9e7a3..2ddd151 100644 --- a/pizzactrl/storyboard.py +++ b/pizzactrl/storyboard.py @@ -7,7 +7,7 @@ class Activity(Enum): RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False} RECORD_VIDEO = {'duration': 0.0, 'filename': ''} TAKE_PHOTO = {'filename': ''} - ADVANCE_UP = {'speed': 0.3, 'direction': True} + ADVANCE_UP = {'speed': 0.3, 'direction': True, 'steps': 100} ADVANCE_LEFT = {'speed': 0.3, 'direction': True, 'steps': 180} # TODO set right number of steps LIGHT_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True} LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0} @@ -40,8 +40,6 @@ class Chapter: if self.pos >= len(self.activities): raise StopIteration act = self.activities[self.pos] - if act.activity is Activity.ADVANCE_UP: # TODO add ADVANCE_LEFT - self.move_ud += 1 self.pos += 1 return act @@ -50,7 +48,6 @@ class Chapter: def rewind(self, **kwargs): self.move = False - self.move_ud = 0 self.pos = 0 def mobilize(self, **kwargs): diff --git a/requirements.txt b/requirements.txt index 7ab7c9a..e918d00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ click sounddevice soundfile scipy -pyserial \ No newline at end of file +pyserial +pydub \ No newline at end of file diff --git a/setup.py b/setup.py index 1db18da..a157b54 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,8 @@ with open('pizzactrl/__init__.py', 'rb') as f: 'sounddevice', 'soundfile', 'scipy', - 'pyserial' + 'pyserial', + 'pydub' ], entry_points='''