view blender/time_sync/time_from_graph.py @ 2458:0e27ba33118c default tip

better blender<->asco playback cooperation. still no play support in blender; only seek
author drewp@bigasterisk.com
date Tue, 20 May 2025 16:25:06 -0700
parents d94480bfb179
children
line wrap: on
line source

import asyncio
import threading
import time
from typing import Coroutine

from rdfdb.patch import Patch
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
from rdflib import Literal

from light9 import networking, showconfig
from light9.ascoltami.graph_context import ascoltamiContext
from light9.ascoltami.play_state import AscoPlayState
from light9.namespaces import L9
from light9.newtypes import decimalLiteral
from light9.run_local import log
from light9.typedgraph import typedValue

from ..asyncio_thread import startLoopInThread
from .blender_time import (
    BlenderTime,
    PausedGotoTime,
    PlayingGotoTime,
    SceneLoaded,
    _TimeEvent,
)


class Sync:
    """asco is the authority on playback status. Sync maintains a copy of the state"""
    lock = threading.Lock()

    # this is edited ONLY by bg thread
    ascoPlayState: AscoPlayState

    def __init__(self):
        # main thread

        # one mutable instance; modified by bg thread
        self.ascoPlayState = AscoPlayState(None, None, False, False, 1.0)
        self.blenderTime = BlenderTime(self.onBlenderEvent, self.ascoPlayState)
        self.blenderTime.start()

        self.ctx = ascoltamiContext(showconfig.showUri())
        log.debug('🚋3 startLoopInThread')
        self._loop = startLoopInThread(self.connectGraph())
        log.info('🚋10 Sync initd')

    def onBlenderEvent(self, event: _TimeEvent):
        # main thread
        match event:
            case SceneLoaded():
                self.blenderTime.setRange(self.ascoPlayState.duration)
                self.blenderTime.setCurrentTime(self.ascoPlayState.getCurrentSongTime() or 0.0)
            case PausedGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, False))
            case PlayingGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, True))

    def runInBackgroundLoop(self, f: Coroutine):
        # main thread
        asyncio.run_coroutine_threadsafe(f, self._loop)

    async def connectGraph(self):
        # bg thread
        log.info('🚋11 start SyncedGraph')
        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.ascoPlayState.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime'])
            self.ascoPlayState.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime'])
            self.ascoPlayState.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0
            self.blenderTime.durationDirty = True  # todo: called too often
            self.ascoPlayState.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False
            self.ascoPlayState.endOfSong = typedValue(bool | None, self.graph, asco, L9['endOfSong']) or False
            log.info(f'🍇 syncFromGraph {self.ascoPlayState=}')
            self.blenderTime.curFrameDirty = True

    async def setInGraph(self, t: float, isBlenderPlaying: bool):
        # bg thread
        log.debug(f'set graph time to {t:.2f} {isBlenderPlaying=}')
        p = Patch()
        if isBlenderPlaying:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(True)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(round(time.time() - t, 1))))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], None))
        else:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(False)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], None))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(round(t, 1))))

        if p.isEmpty():
            return
        await self.graph.patch(p)