Going through TODOs for SIBI.

Implemented, untested:
- start button
- record sounds as mp3 (using pydub)
- video and audio converted to mp4
- added new state REWIND
This commit is contained in:
jpunkt 2021-11-01 17:00:40 +01:00
parent 16165b61bd
commit 711454a06c
7 changed files with 86 additions and 71 deletions

View file

@ -80,14 +80,15 @@ class StoryFile(FileHandle):
FileHandle.__init__(self, name, FileType.STORY) FileHandle.__init__(self, name, FileType.STORY)
REC_NAME = RecFile('name.wav') REC_NAME = RecFile('name.mp3')
REC_MY_IBK = RecFile('my_ibk.wav') REC_MY_IBK = RecFile('my_ibk.mp3')
REC_PORTRAIT = RecFile('portrait.jpg') REC_PORTRAIT = RecFile('portrait.jpg')
REC_CITY_NAME = RecFile('city_name.wav') REC_CITY_NAME = RecFile('city_name.mp3')
REC_CITY_DESC = RecFile('city_description.wav') REC_CITY_DESC = RecFile('city_description.mp3')
REC_CITY_SOUND = RecFile('city_sound.wav') REC_CITY_SOUND = RecFile('city_sound.mp3')
REC_DRAW_CITY = RecFile('city_video.h264') REC_DRAW_CITY = RecFile('city_video.h264')
REC_CITY_PHOTO = RecFile('city_drawing.jpg') REC_CITY_PHOTO = RecFile('city_drawing.jpg')
REC_MERGED_VIDEO = RecFile('video.mp4')
SFX_ERROR = SfxFile('error') SFX_ERROR = SfxFile('error')
SFX_ERROR_DE = SfxFile('error-de') SFX_ERROR_DE = SfxFile('error-de')

View file

@ -1,5 +1,7 @@
# GPIO pin definitions # GPIO pin definitions
BTN_START = 26 # "Start" button (begin the performance) TODO review
BTN_BACK_GPIO = 14 # "Back" button (user input) BTN_BACK_GPIO = 14 # "Back" button (user input)
BTN_FORWARD_GPIO = 18 # "Forward" button (user input) BTN_FORWARD_GPIO = 18 # "Forward" button (user input)

View file

@ -4,7 +4,8 @@ from time import sleep
from enum import Enum from enum import Enum
from typing import Any, List 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 sounddevice as sd
import soundfile as sf import soundfile as sf
@ -13,7 +14,7 @@ import numpy as np
from . import gpio_pins from . import gpio_pins
from picamera import PiCamera from picamera import PiCamera
from gpiozero import Button, OutputDevice, PWMOutputDevice, PWMLED from gpiozero import Button
import serial import serial
@ -53,6 +54,8 @@ class PizzaHAL:
def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE): def __init__(self, serialdev: str = SERIAL_DEV, baudrate: int = SERIAL_BAUDRATE):
self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=None) self.serialcon = serial.Serial(serialdev, baudrate=baudrate, timeout=None)
self.btn_start = Button(gpio_pins.BTN_START)
self.camera = None self.camera = None
self.soundcache = {} self.soundcache = {}
@ -120,7 +123,8 @@ def turn_off(hal: PizzaHAL):
def wait_for_input(hal: PizzaHAL, go_callback: Any, 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 Blink leds on buttons. Wait until the user presses a button, then execute
the appropriate callback the appropriate callback
@ -128,12 +132,16 @@ def wait_for_input(hal: PizzaHAL, go_callback: Any,
:param hal: The hardware abstraction object :param hal: The hardware abstraction object
:param go_callback: called when button 'go' is pressed :param go_callback: called when button 'go' is pressed
:param back_callback: called whan button 'back' 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': if resp == 'B':
go_callback(**kwargs) go_callback(**kwargs)
elif resp == 'R': elif resp == 'R':
back_callback(**kwargs) back_callback(**kwargs)
else:
to_callback(**kwargs)
@blocking @blocking
@ -200,8 +208,13 @@ def record_sound(hal: PizzaHAL, filename: Any, duration: int,
myrecording = sd.rec(int(duration * AUDIO_REC_SR), myrecording = sd.rec(int(duration * AUDIO_REC_SR),
samplerate=AUDIO_REC_SR, samplerate=AUDIO_REC_SR,
channels=2) channels=2)
# TODO user interaction instead
sd.wait() # Wait until recording is finished 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: if cache:
hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR) hal.soundcache[str(filename)] = (myrecording, AUDIO_REC_SR)

View file

@ -1,16 +1,15 @@
import logging import logging
import os.path import os.path
import threading
from typing import Any from typing import Any
from time import sleep from time import sleep
from enum import Enum, auto 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 .storyboard import Activity
from .hal_serial import play_sound, take_photo, record_video, record_sound, turn_off, \ from .hal_serial import play_sound, take_photo, record_video, record_sound, turn_off, \
PizzaHAL, init_camera, init_sounds, wait_for_input, \ PizzaHAL, init_camera, init_sounds, wait_for_input, \
light_layer, backlight, move_vert, move_hor, rewind light_layer, backlight, move_vert, move_hor, rewind
@ -24,6 +23,7 @@ class State(Enum):
IDLE_START = auto() IDLE_START = auto()
PLAY = auto() PLAY = auto()
PAUSE = auto() PAUSE = auto()
REWIND = auto()
IDLE_END = auto() IDLE_END = auto()
SHUTDOWN = auto() SHUTDOWN = auto()
ERROR = -1 ERROR = -1
@ -54,7 +54,8 @@ class Statemachine:
def __init__(self, def __init__(self,
story_de: Any=None, story_de: Any=None,
story_en: Any=None, story_en: Any=None,
move: bool = False): move: bool = False,
loop: bool = True):
self.state = State.POWER_ON self.state = State.POWER_ON
self.hal = PizzaHAL() self.hal = PizzaHAL()
self.story = None self.story = None
@ -64,6 +65,7 @@ class Statemachine:
self.lang = Language.NOT_SET self.lang = Language.NOT_SET
self.move = move self.move = move
self.test = False self.test = False
self.loop = loop
def run(self): def run(self):
logger.debug(f'Run(state={self.state})') logger.debug(f'Run(state={self.state})')
@ -118,66 +120,50 @@ class Statemachine:
# check scroll positions and rewind if necessary # check scroll positions and rewind if necessary
turn_off(self.hal) turn_off(self.hal)
#rewind(self.hal.motor_ud, self.hal.ud_sensor)
if not os.path.exists(fs_names.USB_STICK): if not os.path.exists(fs_names.USB_STICK):
logger.warning('USB-Stick not found.') logger.warning('USB-Stick not found.')
self.state = State.ERROR self.state = State.ERROR
return 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 a sound if everything is alright
play_sound(self.hal, fs_names.SFX_POST_OK) 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 self.state = State.IDLE_START
def _idle_start(self): 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 pass
def _start(self): def _start_or_rewind(self):
""" """
Start playback when blue button is held for 3s Callback function.
"""
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
if t > 3.0: If statemachine is in idle state, start playback when start
self.hal.btn_forward.when_deactivated = None button is pressed (released).
if not self.hal.btn_back.is_active:
self.alt = True If statemachine is playing, trigger rewind and start fresh
"""
if self.state == State.IDLE_START:
self.state = State.PLAY self.state = State.PLAY
return
if self.state == State.PLAY:
self.state = State.REWIND
def _play(self): def _play(self):
""" """
Run the storyboard Run the storyboard
""" """
logger.debug(f'play') 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)
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: if self.test:
self.story = sb_dummy.STORYBOARD self.story = sb_dummy.STORYBOARD
else:
# TODO reenable language selection
self.story = self.story_en
for chapter in iter(self.story): for chapter in iter(self.story):
logger.debug(f'playing chapter {chapter}') logger.debug(f'playing chapter {chapter}')
@ -187,16 +173,17 @@ class Statemachine:
if act.activity is Activity.WAIT_FOR_INPUT: if act.activity is Activity.WAIT_FOR_INPUT:
wait_for_input(hal=self.hal, wait_for_input(hal=self.hal,
go_callback=chapter.mobilize, go_callback=chapter.mobilize,
back_callback=chapter.rewind) back_callback=chapter.rewind,
elif act.activity is Activity.ADVANCE_UP: to_callback=self._start_or_rewind)
if chapter.move and self.move: # elif act.activity is Activity.ADVANCE_UP:
logger.debug( # if chapter.move and self.move:
f'advance({self.hal.motor_ud}, ' # logger.debug(
f'{self.hal.ud_sensor})') # f'advance({self.hal.motor_ud}, '
advance(motor=self.hal.motor_ud, # f'{self.hal.ud_sensor})')
sensor=self.hal.ud_sensor) # advance(motor=self.hal.motor_ud,
elif not self.move: # sensor=self.hal.ud_sensor)
play_sound(self.hal, fs_names.StoryFile('stop')) # elif not self.move:
# play_sound(self.hal, fs_names.StoryFile('stop'))
else: else:
try: try:
{ {
@ -206,22 +193,35 @@ class Statemachine:
Activity.TAKE_PHOTO: take_photo, Activity.TAKE_PHOTO: take_photo,
Activity.LIGHT_LAYER: light_layer, Activity.LIGHT_LAYER: light_layer,
Activity.LIGHT_BACK: backlight, Activity.LIGHT_BACK: backlight,
# Activity.ADVANCE_UP: noop Activity.ADVANCE_UP: move_vert,
Activity.ADVANCE_LEFT: move_hor
}[act.activity](self.hal, **act.values) }[act.activity](self.hal, **act.values)
except KeyError: except KeyError:
logger.exception('Caught KeyError, ignoring...') logger.exception('Caught KeyError, ignoring...')
pass pass
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 self.state = State.IDLE_END
def _idle_end(self): def _idle_end(self):
""" """
Initialize shutdown Initialize shutdown
""" """
if self.move:
rewind(self.hal.motor_ud, self.hal.ud_sensor)
self.state = State.SHUTDOWN self.state = State.SHUTDOWN
def _shutdown(self): def _shutdown(self):

View file

@ -7,7 +7,7 @@ class Activity(Enum):
RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False} RECORD_SOUND = {'duration': 0.0, 'filename': '', 'cache': False}
RECORD_VIDEO = {'duration': 0.0, 'filename': ''} RECORD_VIDEO = {'duration': 0.0, 'filename': ''}
TAKE_PHOTO = {'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 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_LAYER = {'intensity': 1.0, 'fade': 0.0, 'layer': True}
LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0} LIGHT_BACK = {'intensity': 1.0, 'fade': 0.0}
@ -40,8 +40,6 @@ class Chapter:
if self.pos >= len(self.activities): if self.pos >= len(self.activities):
raise StopIteration raise StopIteration
act = self.activities[self.pos] act = self.activities[self.pos]
if act.activity is Activity.ADVANCE_UP: # TODO add ADVANCE_LEFT
self.move_ud += 1
self.pos += 1 self.pos += 1
return act return act
@ -50,7 +48,6 @@ class Chapter:
def rewind(self, **kwargs): def rewind(self, **kwargs):
self.move = False self.move = False
self.move_ud = 0
self.pos = 0 self.pos = 0
def mobilize(self, **kwargs): def mobilize(self, **kwargs):

View file

@ -6,3 +6,4 @@ sounddevice
soundfile soundfile
scipy scipy
pyserial pyserial
pydub

View file

@ -30,7 +30,8 @@ with open('pizzactrl/__init__.py', 'rb') as f:
'sounddevice', 'sounddevice',
'soundfile', 'soundfile',
'scipy', 'scipy',
'pyserial' 'pyserial',
'pydub'
], ],
entry_points=''' entry_points='''