summaryrefslogtreecommitdiff
path: root/render.py
blob: 833b5f533cd68f3a8485578b4ce4d850dc1252df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python3

import queue
import time
import threading

from .audio import DefaultAudioPlayer, AudioPlayer
from .output import LightingOutput
from .workspace import SHOW, CHASER, Advanceable 

class FunctionQueue:
    def after(self, t: float, f: callable):
        """Run the given function after t milliseconds."""
        self.queue.put((time.monotonic() + t/1000, f))

    def start(self):
        """Run until the queue is empty."""
        while not self.queue.empty():
            t, f = self.queue.get()
            time.sleep(max(0, time.monotonic() - t))
            f()

    def __init__(self):
        self.queue = queue.SimpleQueue()

class Renderer:
    """Basic renderer for functions.

    Supports live-rendering Chasers and Shows.

    Instances of this class are NOT thread-safe, with the exception of the advance() method, 
    which may be called from other threads.
    """
    def start(self):
        if self.start_time != -1:
            raise ValueError("Already running")
        self.f.after(0, self.render_step)
        self.f.start()
        self.nx = None
        self.data = None
        self.vals = {}

    def render_step(self):
        """Output the current step and render the next one."""
        if self.nx not in (None, -1):
            self.fq.after((max(self.minnx, self.nx), self.render_step))
        elif self.nx is None:
            self.start_time = time.monotonic()

        self.lo.set_values(tuple(self.values.items()))

        with self.data_lock:
            t = 1000*(int((time.monotonic() - self.start_time)/1000 + 1) + self.nx)
            vals, acues, self.nx, self.data = self.f.render(t)
            for c, v in vals:
                self.values[c] = v

    def advance(self):
        """Advance the function, if possible.

        It is not an error to call this function when dealing with non-Advanceable toplevel 
        functions; this will just do nothing.
        """
        with self.data_lock:
            if self.start_time == -1:
                raise ValueError("Cannot advance a function that has not been started!")
            if issubclass(type(self.f), Advanceable):
                t = 1000*int(self.monotonic() - self.start_time)
                self.data = self.f.advance(self.data, time.monotonic() - self.start_time)
                *_, self.data = self.f.render(t)

    def __init__(self, f, lo:LightingOutput, ao: AudioPlayer=DefaultAudioPlayer, minnx=-1):
        if f.type not in (SHOW, CHASER):
            raise ValueError("Only Shows and Chasers may be used as toplevel functions")
        self.start_time = -1
        self.f = f
        self.fq = FunctionQueue()
        self.minnx = minnx
        self.nx = None
        self.data = None
        self.data_lock = threading.Lock() 
        self.values = {c: 0 for c in self.f.scope}
        self.lo = lo
        self.ao = ao
        self.aplayers = {}