diff --git a/light9/collector/weblisteners.py b/light9/collector/weblisteners.py --- a/light9/collector/weblisteners.py +++ b/light9/collector/weblisteners.py @@ -39,8 +39,7 @@ class WebListeners(object): self.clients = [(c, t) for c, t in self.clients if c != client] log.info('delClient %s, %s left', client, len(self.clients)) - def outputAttrsSet(self, dev: DeviceUri, attrs: Dict[OutputAttr, Any], outputMap: Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, - DmxMessageIndex]]): + def outputAttrsSet(self, dev: DeviceUri, attrs: Dict[OutputAttr, Any], outputMap: Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, DmxMessageIndex]]): """called often- don't be slow""" self.pendingMessageForDev[dev] = (attrs, outputMap) # maybe put on a stack for flusher or something diff --git a/light9/effect/effecteval.py b/light9/effect/effecteval.py --- a/light9/effect/effecteval.py +++ b/light9/effect/effecteval.py @@ -20,10 +20,10 @@ log.info("reload effecteval") def literalColor(rnorm, gnorm, bnorm): - return Literal( - rgb_to_hex([int(rnorm * 255), - int(gnorm * 255), - int(bnorm * 255)])) + return Literal(rgb_to_hex(( + int(rnorm * 255), # + int(gnorm * 255), # + int(bnorm * 255)))) def literalColorHsv(h, s, v): @@ -82,9 +82,7 @@ class EffectEval(object): the effect code. """ # both callers need to apply note overrides - effectSettings = dict( - effectSettings - ) # we should make everything into nice float and Color objects too + effectSettings = dict(effectSettings) # we should make everything into nice float and Color objects too strength = float(effectSettings[L9['strength']]) if strength <= 0: @@ -93,10 +91,7 @@ class EffectEval(object): report = {} out: Dict[Tuple[URIRef, URIRef], Any] = {} # (dev, attr): value - out.update( - self.simpleOutputs.values( - self.effect, strength, - effectSettings.get(L9['colorScale'], None))) + out.update(self.simpleOutputs.values(self.effect, strength, effectSettings.get(L9['colorScale'], None))) if self.effect.startswith(L9['effect/']): tail = 'effect_' + self.effect[len(L9['effect/']):] @@ -112,9 +107,7 @@ class EffectEval(object): def effect_Curtain(effectSettings, strength, songTime, noteTime): - return {(L9['device/lowPattern%s' % n], L9['color']): - literalColor(strength, strength, strength) - for n in range(301, 308 + 1)} + return {(L9['device/lowPattern%s' % n], L9['color']): literalColor(strength, strength, strength) for n in range(301, 308 + 1)} def effect_animRainbow(effectSettings, strength, songTime, noteTime): @@ -124,10 +117,8 @@ def effect_animRainbow(effectSettings, s tr, tg, tb = hex_to_rgb(tint) for n in range(1, 5 + 1): scl = strength * nsin(songTime + n * .3)**3 - col = literalColor( - scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), - scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), - scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) + col = literalColor(scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), + scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) dev = L9['device/aura%s' % n] out.update({ @@ -136,10 +127,8 @@ def effect_animRainbow(effectSettings, s }) ang = songTime * 4 out.update({ - (dev, L9['rx']): - lerp(.27, .7, (n - 1) / 4) + .2 * math.sin(ang + n), - (dev, L9['ry']): - lerp(.46, .52, (n - 1) / 4) + .5 * math.cos(ang + n), + (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4) + .2 * math.sin(ang + n), + (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4) + .5 * math.cos(ang + n), }) return out @@ -160,10 +149,8 @@ def effect_auraSparkles(effectSettings, }) ang = songTime * 4 out.update({ - (dev, L9['rx']): - lerp(.27, .8, (n - 1) / 4) + .2 * math.sin(ang + n), - (dev, L9['ry']): - lerp(.46, .52, (n - 1) / 4) + .4 * math.cos(ang + n), + (dev, L9['rx']): lerp(.27, .8, (n - 1) / 4) + .2 * math.sin(ang + n), + (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4) + .4 * math.cos(ang + n), }) return out @@ -171,8 +158,7 @@ def effect_auraSparkles(effectSettings, def effect_qpan(effectSettings, strength, songTime, noteTime): dev = L9['device/q2'] dur = 4 - col = scale(scale('#ffffff', strength), - effectSettings.get(L9['colorScale']) or '#ffffff') + col = scale(scale('#ffffff', strength), effectSettings.get(L9['colorScale']) or '#ffffff') return { (dev, L9['color']): col, (dev, L9['focus']): 0.589, @@ -189,10 +175,8 @@ def effect_pulseRainbow(effectSettings, tr, tg, tb = hex_to_rgb(tint) for n in range(1, 5 + 1): scl = strength - col = literalColor( - scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), - scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), - scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) + col = literalColor(scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), + scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) dev = L9['device/aura%s' % n] out.update({ @@ -248,10 +232,8 @@ def effect_qsweep(effectSettings, streng (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5), }) out.update({ - (dev, L9['rx']): - lerp(.3, .8, nsin(songTime / period + n / 4)), - (dev, L9['ry']): - effectSettings.get(L9['ry'], .2), + (dev, L9['rx']): lerp(.3, .8, nsin(songTime / period + n / 4)), + (dev, L9['ry']): effectSettings.get(L9['ry'], .2), }) return out @@ -269,16 +251,12 @@ def effect_qsweepusa(effectSettings, str for n in range(1, 3 + 1): dev = L9['device/q%s' % n] out.update({ - (dev, L9['color']): - scale(colmap[n], effectSettings.get(L9['strength'], 1)), - (dev, L9['zoom']): - effectSettings.get(L9['zoom'], .5), + (dev, L9['color']): scale(colmap[n], effectSettings.get(L9['strength'], 1)), + (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5), }) out.update({ - (dev, L9['rx']): - lerp(.3, .8, nsin(songTime / period + n / 4)), - (dev, L9['ry']): - effectSettings.get(L9['ry'], .5), + (dev, L9['rx']): lerp(.3, .8, nsin(songTime / period + n / 4)), + (dev, L9['ry']): effectSettings.get(L9['ry'], .5), }) return out @@ -379,21 +357,18 @@ def effect_Strobe(effectSettings, streng offset = 0 f = (((songTime + offset) * rate) % 1.0) c = (f < duty) * strength - col = rgb_to_hex([int(c * 255), int(c * 255), int(c * 255)]) + col = rgb_to_hex((int(c * 255), int(c * 255), int(c * 255))) return {(L9['device/colorStrip'], L9['color']): Literal(col)} def effect_lightning(effectSettings, strength, songTime, noteTime): devs = [ - L9['device/veryLow1'], L9['device/veryLow2'], L9['device/veryLow3'], - L9['device/veryLow4'], L9['device/veryLow5'], L9['device/backlight1'], - L9['device/backlight2'], L9['device/backlight3'], - L9['device/backlight4'], L9['device/backlight5'], L9['device/down2'], - L9['device/down3'], L9['device/down4'], L9['device/hexLow3'], - L9['device/hexLow5'], L9['device/postL1'], L9['device/postR1'] + L9['device/veryLow1'], L9['device/veryLow2'], L9['device/veryLow3'], L9['device/veryLow4'], L9['device/veryLow5'], L9['device/backlight1'], + L9['device/backlight2'], L9['device/backlight3'], L9['device/backlight4'], L9['device/backlight5'], L9['device/down2'], L9['device/down3'], + L9['device/down4'], L9['device/hexLow3'], L9['device/hexLow5'], L9['device/postL1'], L9['device/postR1'] ] out = {} - col = rgb_to_hex([int(255 * strength)] * 3) + col = rgb_to_hex((int(255 * strength),) * 3) for i, dev in enumerate(devs): n = noise(songTime * 8 + i * 6.543) if n > .4: @@ -452,7 +427,7 @@ def effect_image(effectSettings, strengt out[(SKY[f'cycGreen{column}'], L9['brightness'])] = color[1] out[(SKY[f'cycBlue{column}'], L9['brightness'])] = color[2] else: - out[(dev, L9['color'])] = rgb_to_hex(map(_8bit, color)) + out[(dev, L9['color'])] = rgb_to_hex(tuple(map(_8bit, color))) return out @@ -527,13 +502,7 @@ def effect_parNoise(effectSettings, stre gamma = .6 for dev in [SKY['strip1'], SKY['strip2'], SKY['strip3']]: out[(dev, L9['color'])] = scale( - rgb_to_hex( - (_8bit(r * math.pow(max(.01, noise(speed * songTime)), gamma)), - _8bit(g * - math.pow(max(.01, noise(speed * songTime + 10)), gamma)), - _8bit( - b * - math.pow(max(.01, noise(speed * songTime + 20)), gamma)))), - strength) + rgb_to_hex((_8bit(r * math.pow(max(.01, noise(speed * songTime)), gamma)), _8bit(g * math.pow(max(.01, noise(speed * songTime + 10)), gamma)), + _8bit(b * math.pow(max(.01, noise(speed * songTime + 20)), gamma)))), strength) return out diff --git a/light9/effect/sequencer/eval_faders.py b/light9/effect/sequencer/eval_faders.py new file mode 100644 --- /dev/null +++ b/light9/effect/sequencer/eval_faders.py @@ -0,0 +1,81 @@ + +import asyncio +import logging +import time +from typing import Callable, Coroutine, List, cast +from light9.effect.sequencer.sequencer import Note + +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import URIRef + +from light9.effect import effecteval +from light9.effect.settings import DeviceSettings +from light9.effect.simple_outputs import SimpleOutputs +from light9.metrics import metrics +from light9.namespaces import L9, RDF +from light9.newtypes import NoteUri + +log = logging.getLogger('sequencer') +class FaderEval: + """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector + + The current faders become Notes in here, for more code reuse. + """ + def __init__(self, + graph: SyncedGraph, + sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]], + ): + self.graph = graph + self.sendToCollector = sendToCollector + + # Notes without times- always on + self.notes: List[Note] = [] + + self.simpleOutputs = SimpleOutputs(self.graph) + self.graph.addHandler(self.compileGraph) + self.lastLoopSucceeded = False + + # self.codeWatcher = CodeWatcher(onChange=self.onCodeChange) + log.info('startupdating task') + asyncio.create_task(self.startUpdating()) + + async def startUpdating(self): + await self.graph.addAsyncHandler(self.update) + log.info('startupdating task done') + + def onCodeChange(self): + log.debug('seq.onCodeChange') + self.graph.addHandler(self.compileGraph) + #self.updateLoop() + + @metrics('compile_graph_fader').time() + def compileGraph(self) -> None: + """rebuild our data from the graph""" + self.notes = [] + for fader in self.graph.subjects(RDF.type, L9['Fader']): + def compileFader() -> Note: + return self.compileFader(cast(URIRef, fader)) + + self.notes.append(compileFader()) + if self.notes: + asyncio.create_task(self.startUpdating()) + + + @metrics('compile_fader').time() + def compileFader(self, fader: URIRef) -> Note: + return Note(self.graph, NoteUri(cast(NoteUri, fader)), effecteval, + self.simpleOutputs, timed=False) + + @metrics('update_call_fader').time() + async def update(self): + settings = [] + for note in self.notes: + effectValue = self.graph.value(note.uri, L9['value']) + if effectValue is None: + log.info(f'skip note {note}, no :value') + continue + s, report = note.outputSettings(t=time.time(), strength=float(effectValue)) + settings.append(s) + devSettings = DeviceSettings.fromList(self.graph, settings) + with metrics('update_s3_send_fader').time(): # our measurement + sendSecs = await self.sendToCollector(devSettings) diff --git a/light9/effect/sequencer/sequencer.py b/light9/effect/sequencer/sequencer.py --- a/light9/effect/sequencer/sequencer.py +++ b/light9/effect/sequencer/sequencer.py @@ -158,7 +158,7 @@ class CodeWatcher(object): self.onChange() # in case we got an event at the start of the write - reactor.callLater(.1, go) + reactor.callLater(.1, go) # type: ignore class Sequencer(object): @@ -173,7 +173,7 @@ class Sequencer(object): metrics('update_loop_goal_fps').set(self.fps) metrics('update_loop_goal_latency').set(1 / self.fps) self.sendToCollector = sendToCollector - self.music = MusicTime(period=.2, pollCurvecalc=False) + self.music = MusicTime(period=.2) self.recentUpdateTimes: List[float] = [] self.lastStatLog = 0.0 @@ -278,67 +278,3 @@ class Sequencer(object): # (sometimes it's None, not sure why, and neither is mypy) #if isinstance(sendSecs, float): # metrics('update_s3_send_client').observe(sendSecs) - -class FaderEval: - """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector - - The current faders become Notes in here, for more code reuse. - """ - def __init__(self, - graph: SyncedGraph, - sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]], - ): - self.graph = graph - self.sendToCollector = sendToCollector - - # Notes without times- always on - self.notes: List[Note] = [] - - self.simpleOutputs = SimpleOutputs(self.graph) - self.graph.addHandler(self.compileGraph) - self.lastLoopSucceeded = False - - # self.codeWatcher = CodeWatcher(onChange=self.onCodeChange) - log.info('startupdating task') - asyncio.create_task(self.startUpdating()) - - async def startUpdating(self): - await self.graph.addAsyncHandler(self.update) - log.info('startupdating task done') - - def onCodeChange(self): - log.debug('seq.onCodeChange') - self.graph.addHandler(self.compileGraph) - #self.updateLoop() - - @metrics('compile_graph_fader').time() - def compileGraph(self) -> None: - """rebuild our data from the graph""" - self.notes = [] - for fader in self.graph.subjects(RDF.type, L9['Fader']): - def compileFader() -> Note: - return self.compileFader(cast(URIRef, fader)) - - self.notes.append(compileFader()) - if self.notes: - asyncio.create_task(self.startUpdating()) - - - @metrics('compile_fader').time() - def compileFader(self, fader: URIRef) -> Note: - return Note(self.graph, NoteUri(cast(NoteUri, fader)), effecteval, - self.simpleOutputs, timed=False) - - @metrics('update_call_fader').time() - async def update(self): - settings = [] - for note in self.notes: - effectValue = self.graph.value(note.uri, L9['value']) - if effectValue is None: - log.info(f'skip note {note}, no :value') - continue - s, report = note.outputSettings(t=time.time(), strength=float(effectValue)) - settings.append(s) - devSettings = DeviceSettings.fromList(self.graph, settings) - with metrics('update_s3_send_fader').time(): # our measurement - sendSecs = await self.sendToCollector(devSettings) diff --git a/light9/effect/sequencer/service.py b/light9/effect/sequencer/service.py --- a/light9/effect/sequencer/service.py +++ b/light9/effect/sequencer/service.py @@ -9,7 +9,8 @@ import time from light9 import networking from light9.collector.collector_client_asyncio import sendToCollector -from light9.effect.sequencer.sequencer import FaderEval, Sequencer, StateUpdate +from light9.effect.sequencer.eval_faders import FaderEval +from light9.effect.sequencer.sequencer import Sequencer, StateUpdate from light9.effect.settings import DeviceSettings from light9.metrics import metrics from light9.run_local import log diff --git a/light9/effect/simple_outputs.py b/light9/effect/simple_outputs.py --- a/light9/effect/simple_outputs.py +++ b/light9/effect/simple_outputs.py @@ -6,13 +6,16 @@ from rdflib import URIRef class SimpleOutputs(object): + """ + Watches graph for effects that are just fading output attrs. + Call `values` to get (dev,attr):value settings. + """ def __init__(self, graph): self.graph = graph # effect : [(dev, attr, value, isScaled)] - self.effectOutputs: Dict[URIRef, List[ - Tuple[URIRef, URIRef, Any, bool]]] = {} + self.effectOutputs: Dict[URIRef, List[Tuple[URIRef, URIRef, Any, bool]]] = {} self.graph.addHandler(self.updateEffectsFromGraph)