#!/usr/bin/python3 import argparse import sounddevice import soundfile import pyglet from pyglet.window import key import factory import display import yaml import time import dbus 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 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: 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 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() return True def stop(self): pyglet.clock.unschedule(self.update) 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()) 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: 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['items']) setlist = factory.MakeSet(config['set'], items) preshow = factory.MakeSet(config['preshow'], items) if 'defaults' in config: defaults = config['defaults'] else: defaults = {} return {'items' : items, 'setlist' : setlist, 'preshow' : preshow} 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') 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) 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()