#!/usr/bin/python3 import argparse import sounddevice import soundfile import pyglet from pyglet.window import key import factory import display import dmx import yaml import time import dbus from ola.ClientWrapper import ClientWrapper from ola.DMXConstants import (DMX_MIN_SLOT_VALUE, DMX_MAX_SLOT_VALUE, DMX_UNIVERSE_SIZE) class EventRunner: def __init__(self, gigconfig): self._gigconfig = gigconfig self._running = False self._selected = None self._playing = None self._active = [] self._currentitem = None for item in self._gigconfig['items']: self._gigconfig['items'][item].set_eventrunner(self) def set_audiencecontent(self, audiencecontent): for item in self._gigconfig['items']: self._gigconfig['items'][item].set_audiencecontent(audiencecontent) def get_config(self, section): return self._gigconfig[section] def select(self, pos): self._selected = pos self._playing = None self.activate('select') def play(self): self._playing = self._selected self._selected = None self.activate('start') def advance(self): if self._playing is not None: return self.activate() elif self._selected is not None: if self._next_button_is_prestart: return self.activate() self.play() return True return False def activate(self, marker=None): newitem = self.get_item() if self._currentitem != newitem: selected = self._selected self.stop() newitem.reset() self._currentitem = newitem if marker is None: marker = 'select' elif marker != 'select': return False self._selected = selected else: self.stop(full=False) now = time.time() if not self._currentitem.activate(marker, now): return False for event in self._currentitem._events: if event.at is not None and not event.scheduled: pyglet.clock.schedule_once(self.update, event.at - now, event) print("Scheduled event {} at {} ({} from now)".format(event, event.at, event.at - now)) event.scheduled = True self._next_button = self._currentitem.get_next_button() self._next_button_is_prestart = self._currentitem.is_button_prestart(self._next_button) return True def stop(self, full=True): print("Unscheduling events") pyglet.clock.unschedule(self.update) if not full: return self._playing = None self._selected = None for event in self._active: if event is not None: event.stop() self._active = [] if self._currentitem is not None: self._currentitem.reset() def update(self, dt, event): event.start() self._active.append(event) def notify(self, *args, **kwargs): for event in self._active: if event is not None: event.notify(*args, **kwargs) def finished(self, event): # We might get called back here from within a notify or other such construct for i, e in enumerate(self._active): if e is event: self._active[i] = None def pause(self): pass def get_item(self, pos=None): if pos is None: pos = self._playing if pos is None: pos = self._selected if pos is None: return None item = self._gigconfig['setlist'][pos] return self._gigconfig['items'][item] def status(self): if self._playing is not None: status = 'Playing {}'.format(self.get_item().get_title()) if self._next_button is not None: button = "Marker {}".format(self._next_button) else: button = "Select next" elif self._selected is not None: status = 'Selected {}'.format(self.get_item().get_title()) if (self._next_button is not None) and self._next_button_is_prestart: button = "Marker {}".format(self._next_button) else: button = "Play" else: status = 'Waiting' button = "Select" return { 'selected' : self._selected, 'playing' : self._playing, 'running' : self._running, 'status' : status, 'button' : button, } class MainWindow(pyglet.window.Window): def __init__(self, gigconfig, eventrunner, audiencecontent, *args, **kwargs): super().__init__(*args, **kwargs) self._gigconfig = gigconfig self._eventrunner = eventrunner self._audiencecontent = audiencecontent self._audiencecontent.register(self) self._cursorpos = 0 self._playpos = None self._selectpos = None self.on_resize(self.width, self.height) self.update_cursor(0) def on_resize(self, width, height): self._scale = self.height / 600 self.batch = pyglet.graphics.Batch() self.status = pyglet.text.Label('Status bar', font_name='Ariel', font_size=18 * self._scale, x=self.width//2, y=0, anchor_x='left', anchor_y='bottom', batch=self.batch) self.next_action = pyglet.text.Label('Next:', font_name='Ariel', font_size=18 * self._scale, x=self.width//2, y=40 * self._scale, anchor_x='left', anchor_y='bottom', batch=self.batch) self._spacing = 40 * self._scale self._cursorspread = 300 * self._scale self._cursorwidth = 15 * self._scale self._cursorheight = 20 * self._scale y_increment = self._spacing y_counter = self.height - y_increment # They're really buttons, but... self.buttons = [] for item in self._gigconfig['setlist']: self.buttons.append(pyglet.text.Label(self._gigconfig['items'][item].get_title(), font_name='Ariel', font_size=14 * self._scale, width=self._cursorspread, x=self.width//4, y=y_counter, anchor_x='center', anchor_y='center', batch=self.batch)) y_counter -= y_increment self._cursor1x = self.width//4 - self._cursorspread/2 self._cursor2x = self.width//4 + self._cursorspread/2 self._cursory = self.height//2 self.cursor1 = pyglet.shapes.Triangle(self._cursor1x, self._cursory, self._cursor1x-self._cursorwidth, self._cursory-self._cursorheight/2, self._cursor1x-self._cursorwidth, self._cursory+self._cursorheight/2, (255, 255, 255, 255), batch=self.batch) self.cursor2 = pyglet.shapes.Triangle(self._cursor2x, self._cursory, self._cursor2x+self._cursorwidth, self._cursory-self._cursorheight/2, self._cursor2x+self._cursorwidth, self._cursory+self._cursorheight/2, (255, 255, 255, 255), batch=self.batch) self.update_objects() self.audience_update() def audience_update(self): self.switch_to() self._audiencecontent.create(self, self.width // 2, self.height // 2, x=self.width//2, y=self.height//4) def update_objects(self): status = self._eventrunner.status() self._selectpos = status['selected'] self._playpos = status['playing'] self.set_status(status['status']) self.set_next_action(status['button']) self.update_cursor() def update_cursor(self, pos=None): if pos is not None: self._cursorpos = pos y_increment = self._spacing y_counter = self.height // 2 + y_increment * self._cursorpos for i, button in enumerate(self.buttons): button.y = y_counter if i == self._playpos: button.color = (0, 255, 0, 255) elif i == self._selectpos: button.color = (255, 255, 0, 255) else: button.color = (255, 255, 255, 255) y_counter -= y_increment def select(self): if self._playpos == self._cursorpos: if not self._eventrunner.advance(): if self._cursorpos < len(self.buttons) - 1: self.update_cursor(self._cursorpos + 1) self._eventrunner.select(self._cursorpos) elif self._selectpos == self._cursorpos: if not self._eventrunner.advance(): self._eventrunner.play() else: self._eventrunner.select(self._cursorpos) self.update_objects() def set_status(self, text): self.status.text = text def set_next_action(self, text): self.next_action.text = "Next: {}".format(text) def action_up(self): if self._cursorpos > 0: self.update_cursor(self._cursorpos - 1) def action_down(self): if self._cursorpos < len(self.buttons) - 1: self.update_cursor(self._cursorpos + 1) def action_select(self): self.select() def action_stop(self): if self._playpos is not None: self._eventrunner.stop() elif self._selectpos is not None: self._eventrunner.stop() else: self.action_up() self.update_objects() def on_key_press(self, symbol, modifiers): if symbol == key.UP: self.action_up() elif symbol == key.DOWN: self.action_down() elif symbol == key.ENTER: self.action_select() elif symbol == key.SPACE: self.action_stop() else: super().on_key_press(symbol, modifiers) def on_draw(self): self.clear() self.batch.draw() self.audiencebatch.draw() class AudienceContent: def __init__(self, gigconfig, eventrunner): self._gigconfig = gigconfig self._eventrunner = eventrunner self._objects = [] self._eventrunner.set_audiencecontent(self) self._display = display.DisplayContent() def register(self, obj): self._objects.append(obj) def update(self): for obj in self._objects: obj.audience_update() def create(self, obj, width, height, x=0, y=0): self._display.create(obj, width, height, x, y) def new_content(self, obj): self._display = obj self.update() class AudienceWindow(pyglet.window.Window): def __init__(self, audiencecontent, *args, **kwargs): super().__init__(*args, **kwargs) self._audiencecontent = audiencecontent self._audiencecontent.register(self) self._audiencecontent.create(self, self.width, self.height) def audience_update(self): self.switch_to() self._audiencecontent.create(self, self.width, self.height) def on_resize(self, width, height): print("New size is: {} x {}".format(width, height)) self._audiencecontent.create(self, width, height) def on_draw(self): self.clear() self.audiencebatch.draw() def process_config(config): items = factory.MakeItems(config.get('items')) setlist = factory.MakeSet(config.get('set'), items) defaults = config.get('defaults') fixture_types = factory.MakeFixtureTypes(config.get('fixture_types')) fixtures = factory.MakeFixtures(config.get('fixtures'), fixture_types) groups = factory.MakeGroups(config.get('groups'), fixtures) scenes = factory.MakeScenes(config.get('scenes'), fixtures, groups) return { 'items' : items, 'setlist' : setlist, 'defaults' : defaults, 'fixture_types' : fixture_types, 'fixtures' : fixtures, 'groups' : groups, 'scenes' : scenes, } def load_config(filename): with open(filename, encoding='utf8') as file: y = yaml.safe_load(file) config = process_config(y) return config def playback(filename): data, fs = soundfile.read(filename) #sounddevice.play(data, fs, device='alsa,hw:CARD=QU16') sounddevice.play(data, fs, device='QU-16 alsa', mapping=[21,22]) sounddevice.wait() def main(): parser = argparse.ArgumentParser(prog='gig.py', description='Gig showrunning') parser.add_argument('filename') parser.add_argument('-w', '--window', action='store_true', help='Run in a window') parser.add_argument('-u', '--universe', default=0, type=int, help='DMX universe') args = parser.parse_args() config = load_config(args.filename) display = pyglet.display.get_display() screens = display.get_screens() # print(screens) mainscreen = screens[0] if len(screens) > 1: audiencescreen = screens[1] else: audiencescreen = None print(mainscreen) print(audiencescreen) eventrunner = EventRunner(gigconfig=config) audiencecontent = AudienceContent(gigconfig=config, eventrunner=eventrunner) try: wrapper = ClientWrapper() DMX_DATA_SIZE = DMX_UNIVERSE_SIZE dmx_object = dmx.DMX(args.universe, wrapper, DMX_DATA_SIZE) except Exception: dmx_object = None fixtureoutput = dmx.FixtureOutput(fixtures=[config['fixtures'][x] for x in config['fixtures']], dmx=dmx_object) pyglet.clock.schedule_interval(fixtureoutput.tick, 1/30.0) if audiencescreen is not None: window2 = AudienceWindow(audiencecontent=audiencecontent, fullscreen=False, screen=audiencescreen, resizable=True) if not args.window: window2.set_location(audiencescreen.x, audiencescreen.y) window2.set_size(audiencescreen.width, audiencescreen.height) window2.set_mouse_position(audiencescreen.x, audiencescreen.y) window2.set_fullscreen(True) #window2.set_location(audiencescreen.x, audiencescreen.y) #event_logger = pyglet.window.event.WindowEventLogger() #window2.push_handlers(event_logger) window = MainWindow(gigconfig=config, eventrunner=eventrunner, audiencecontent=audiencecontent, fullscreen=False, screen=mainscreen, resizable=True) if not args.window: window.set_location(mainscreen.x, mainscreen.y) window.set_size(mainscreen.width, mainscreen.height) window.set_mouse_position(mainscreen.x, mainscreen.y) window.set_fullscreen(True) #window.set_location(mainscreen.x, mainscreen.y) try: # Let's set up some stuff to inhibit the screensaver bus = dbus.SessionBus() saver = bus.get_object('org.freedesktop.ScreenSaver', '/ScreenSaver') saver_interface = dbus.Interface(saver, dbus_interface='org.freedesktop.ScreenSaver') # now we can inhibit the screensaver cookie=saver_interface.Inhibit("gig.py", "Don't blank the screen during the gig") except Exception: pass pyglet.app.run() if __name__ == "__main__": main()