# HG changeset patch # User drewp@bigasterisk.com # Date 2024-05-28 22:35:12 # Node ID b8a408caf1157fc400b24afabe7a0dc96eeaaf7e # Parent d1946cb32121b36cbb5ea86b9998670b7f9b5ab0 start blender sync diff --git a/src/light9/blender/__init__.py b/src/light9/blender/__init__.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/__init__.py @@ -0,0 +1,37 @@ +# ln -s ./ ~/.config/blender/4.2/scripts/addons/light9_sync/ + +#~/own/tool/blender $LIGHT9_SHOW/song1.blend --addons light9_sync +import logging +import sys + +from . import light_control, time_sync + +bl_info = { + "name": "light9_sync", + "description": "light9 sync", + "version": (0, 0, 1), + "blender": (4, 2, 0), + "category": "Object", +} + +modules = (time_sync, light_control) + + +def register(): + logging.getLogger('autodepgraphapi').setLevel(logging.INFO) + logging.getLogger('syncedgraph').setLevel(logging.INFO) + for p in [ + '/home/drewp/.local/share/pdm/venvs/light9-zxQNOBNq-3.11/lib/python3.11/site-packages', + '/home/drewp/projects/light9/src/', + '/my/proj/rdfdb', + ]: + if p not in sys.path: + sys.path.append(p) + + for module in modules: + module.register() + + +def unregister(): + for module in reversed(modules): + module.unregister() diff --git a/src/light9/blender/asyncio_thread.py b/src/light9/blender/asyncio_thread.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/asyncio_thread.py @@ -0,0 +1,16 @@ +import asyncio +from threading import Thread +from typing import Coroutine + + +def startLoopInThread(task: Coroutine) -> asyncio.AbstractEventLoop: + + def start_background_loop(loop: asyncio.AbstractEventLoop) -> None: + asyncio.set_event_loop(loop) + loop.run_forever() + + loop = asyncio.new_event_loop() + t = Thread(target=start_background_loop, args=(loop,), daemon=True) + t.start() + asyncio.run_coroutine_threadsafe(task, loop) + return loop diff --git a/src/light9/blender/light_control/__init__.py b/src/light9/blender/light_control/__init__.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/light_control/__init__.py @@ -0,0 +1,28 @@ +""" +watch blender lights, output to real lights +""" + +import time +from typing import cast + +import bpy # type: ignore +from bpy.app.handlers import persistent + +sender: object = None + + +def register(): + global sender + from .send_to_collector import Sender + + sender = Sender() + + # @persistent + # def fcp(scene): + # cast(Sender, sender).on_frame_change_post(scene) + + # bpy.app.handlers.frame_change_post.append(fcp) + + +def unregister(): + raise NotImplementedError diff --git a/src/light9/blender/light_control/send_to_collector.py b/src/light9/blender/light_control/send_to_collector.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/light_control/send_to_collector.py @@ -0,0 +1,7 @@ +class Sender: + + def blenderTime(self) -> float: + return -55 + + def on_frame_change_post(self, scene, *args): + print('ofcp',scene,args) diff --git a/src/light9/blender/time_sync/__init__.py b/src/light9/blender/time_sync/__init__.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/time_sync/__init__.py @@ -0,0 +1,16 @@ +""" +sets blender time to track ascoltami time; +blender-side time changes are sent back to ascoltami +""" + +sync: object = None + + +def register(): + global sync + from .time_from_graph import Sync + sync = Sync() + + +def unregister(): + raise NotImplementedError diff --git a/src/light9/blender/time_sync/time_from_graph.py b/src/light9/blender/time_sync/time_from_graph.py new file mode 100644 --- /dev/null +++ b/src/light9/blender/time_sync/time_from_graph.py @@ -0,0 +1,100 @@ +import threading +import time + +import bpy # type: ignore +from bpy.app.handlers import persistent # type: ignore +from light9.typedgraph import typedValue +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import URIRef + +from light9 import networking +from light9.blender.asyncio_thread import startLoopInThread +from light9.namespaces import L9 +from light9.run_local import log + + +def clamp(lo, hi, x): + return max(lo, min(hi, x)) + + +def floatValue(graph: SyncedGraph, s: URIRef, p: URIRef) -> float | None: + return typedValue(float | None, graph, s, p) + + +UPDATE_PERIOD = 1 / 20 + + +class Sync: + lock = threading.Lock() + duration: float = 1 + wallStartTime: float | None = None + pausedSongTime: float | None = None + playing=False + latestTime: float + + def __init__(self): + # main thread + self.lastSetFrame = -1 + self.lastGraphFrame = -1 + self.frameSetBy = 'init' + startLoopInThread(self.task()) + bpy.app.timers.register(self.update) + bpy.app.handlers.frame_change_post.append(self.on_frame_change_post) + + ## read from graph + + async def task(self): + # bg thread with asyncio loop + self.graph = SyncedGraph(networking.rdfdb.url, "time_sync") + self.graph.addHandler(self.syncFromGraph) + + def syncFromGraph(self): + # bg thread + with self.lock: + asco = L9['ascoltami'] + self.wallStartTime = floatValue(self.graph, asco, L9['wallStartTime']) + self.pausedSongTime = floatValue(self.graph, asco, L9['pausedSongTime']) + self.duration = floatValue(self.graph, asco, L9['duration']) or 1.0 + self.playing = typedValue(bool, self.graph, asco, L9['playing']) + + def currentTime(self) -> float | None: + if self.wallStartTime is not None: + return time.time() - self.wallStartTime + if self.pausedSongTime is not None: + return self.pausedSongTime + log.warn('no time data') + return None + + ## graph time -> blender time + + @persistent + def update(self): + # main thread? wherever blender runs timers + if self.playing: + with self.lock: + t = self.currentTime() + if t is not None: + self.setBlenderTime(t, self.duration) + return UPDATE_PERIOD + + def setBlenderTime(self, t: float, duration: float): + scene = bpy.context.scene + fps = scene.render.fps + scene.frame_start = 0 + scene.frame_end = int(duration * fps) + fr = int(clamp(t, 0, duration) * fps) + self.lastSetFrame = fr + scene.frame_set(fr) + + ## blender time -> graph time + + @persistent + def on_frame_change_post(self, scene, deps): + if scene.frame_current != self.lastSetFrame: + # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration) + self.lastSetFrame = scene.frame_current + t = scene.frame_current / scene.render.fps + self.setInGraph(t) + + def setInGraph(self, t: float): + log.warning(f'todo: set graph to {t}') \ No newline at end of file