comparison blender/time_sync/blender_time.py @ 2457:d94480bfb179

more work on blender time sync. Might be working, aside from blender play-button
author drewp@bigasterisk.com
date Tue, 20 May 2025 13:48:07 -0700
parents 2d454737a916
children
comparison
equal deleted inserted replaced
2456:917bc2eaf4f4 2457:d94480bfb179
3 3
4 import bpy 4 import bpy
5 from bpy.app.handlers import persistent 5 from bpy.app.handlers import persistent
6 6
7 from light9.run_local import log 7 from light9.run_local import log
8 from light9.ascoltami.play_state import AscoPlayState
8 9
9 UPDATE_PERIOD = 1 / 20 10 UPDATE_PERIOD = 1 / 20
10 11
11 12
12 def clamp(lo, hi, x): 13 def clamp(lo, hi, x):
29 @dataclass 30 @dataclass
30 class PlayingGotoTime(_TimeEvent): 31 class PlayingGotoTime(_TimeEvent):
31 t: float 32 t: float
32 33
33 34
35 @dataclass
36 class BlenderPlayState:
37 isAnimationPlaying: bool
38 isScrubbing: bool
39
40
34 class BlenderTime: 41 class BlenderTime:
35 """all methods to run in main thread""" 42 """all methods to run in main thread"""
36 43
37 def __init__(self, onEvent: Callable[[_TimeEvent], None]): 44 def __init__(self, onEvent: Callable[[_TimeEvent], None], ascoltamiPlayStateRO: AscoPlayState):
45 self.ascoltamiPlayStateRO = ascoltamiPlayStateRO
38 self._onEvent = onEvent 46 self._onEvent = onEvent
39 self.lastSetFrame = -1 47 self.lastSetFrame = -1
48 self.lastPlayState = BlenderPlayState(False, False)
49 self.loaded = False
50 self.curFrameDirty = True
51 self.durationDirty = True
40 52
41 def start(self): 53 def start(self):
42 bpy.app.handlers.load_post.append(self._on_load_post) 54 bpy.app.handlers.load_post.append(self._on_load_post)
43 bpy.app.handlers.frame_change_post.append(self._on_frame_change_post) 55 bpy.app.handlers.frame_change_post.append(self._on_frame_change_post)
44 56
45 # need persistent because `blender --addons ...` seems to start addon 57 # need persistent because `blender --addons ...` seems to start addon
46 # before loading scene. 58 # before loading scene.
47 bpy.app.timers.register(self.update, persistent=True) 59 bpy.app.timers.register(self._update, persistent=True)
48 60
49 self.emitEvent(SceneLoaded()) 61 self._emitEvent(SceneLoaded())
50 62
51 def setSceneDuration(self, duration: float): 63 def setSceneDuration(self, duration: float):
64 if not self.loaded:
65 return
52 self.duration = duration 66 self.duration = duration
53 scene = bpy.context.scene 67 scene = bpy.context.scene
54 fps = scene.render.fps 68 fps = scene.render.fps
55 scene.frame_start = 0 69 scene.frame_start = 0
56 scene.frame_end = int(duration * fps) 70 scene.frame_end = int(duration * fps)
57 71
58 def frameDuration(self): 72 def frameDuration(self):
73 """press Home on the timeline to frame the whole range"""
59 return 74 return
60 # todo: need to be in screen context or something 75 # todo: need to be in screen context or something
61 context_override = bpy.context.copy() 76 context_override = bpy.context.copy()
62 # context_override["selected_objects"] = list(context.scene.objects) 77 # context_override["selected_objects"] = list(context.scene.objects)
63 with bpy.context.temp_override(**context_override): 78 with bpy.context.temp_override(**context_override):
64 bpy.ops.action.view_all() 79 bpy.ops.action.view_all()
65 80
66 def setCurrentTime(self, t: float): 81 def setCurrentTime(self, t: float):
82 if not self.loaded:
83 return
67 scene = bpy.context.scene 84 scene = bpy.context.scene
68 fps = scene.render.fps 85 fps = scene.render.fps
69 fr = int(clamp(t, 0, self.duration) * fps) 86 fr = int(clamp(t * fps, 0, scene.frame_end))
70 self.lastSetFrame = fr 87 self.lastSetFrame = fr
71 scene.frame_set(fr) 88 scene.frame_set(fr)
72 89
73 def setBlenderTime(self, t: float, duration: float): 90 def setRange(self, duration: float):
74 log.info(f'set blender time to {t:.2f}')
75 self.setSceneDuration(duration) 91 self.setSceneDuration(duration)
76 self.frameDuration() 92 self.frameDuration()
77 self.setCurrentTime(t)
78 93
79 def emitEvent(self, event: _TimeEvent): 94 def _emitEvent(self, event: _TimeEvent):
80 log.info(f'🌹 emitEvent {event}') 95 # log.info(f'🌹 emitEvent {event}')
81 self._onEvent(event) 96 self._onEvent(event)
82 97
83 @persistent 98 @persistent
84 def update(self): 99 def _update(self):
85 if 0: 100 if self.curFrameDirty or self.ascoltamiPlayStateRO.playing:
86 if self.playing: 101 self._followAscoTime()
87 with self.lock: 102 self.curFrameDirty=False
88 t = self.currentTime() 103 if self.durationDirty:
89 if t is not None: 104 if (d := self.ascoltamiPlayStateRO.duration) is not None:
90 self.blenderTime.setBlenderTime(t, self.duration) 105 self.setRange(d)
106 self.durationDirty = False
107 # # todo: playing in blender should start ascoltami playback
108 # cur = BlenderPlayState(bpy.context.screen.is_animation_playing, bpy.context.screen.is_scrubbing)
109
91 return UPDATE_PERIOD 110 return UPDATE_PERIOD
111
112 def _followAscoTime(self):
113 if (t := self.ascoltamiPlayStateRO.getCurrentSongTime()) is not None:
114 self.setCurrentTime(t)
92 115
93 @persistent 116 @persistent
94 def _on_load_post(self, scene, deps): 117 def _on_load_post(self, scene, deps):
95 self.emitEvent(SceneLoaded()) 118 self._emitEvent(SceneLoaded())
119 self.loaded = True
120 self.curFrameDirty = True
96 121
97 @persistent 122 @persistent
98 def _on_frame_change_post(self, scene, deps): 123 def _on_frame_change_post(self, scene, deps):
99 # if scene.frame_current == self.lastSetFrame: 124 # log.info(f' _on_frame_change_post {self.lastPlayState}')
100 # return 125 if scene.frame_current == self.lastSetFrame:
126 return
101 # blender requested this frame change, either playing or paused (scrubbing timeline) 127 # blender requested this frame change, either playing or paused (scrubbing timeline)
102 self.lastSetFrame = scene.frame_current 128 self.lastSetFrame = scene.frame_current
103 t = round(scene.frame_current / scene.render.fps, 3) 129 t = round(scene.frame_current / scene.render.fps, 3)
104 if bpy.context.screen.is_animation_playing and not bpy.context.screen.is_scrubbing: 130 if self.lastPlayState.isAnimationPlaying and not self.lastPlayState.isScrubbing:
105 self.emitEvent(PlayingGotoTime(t=t)) 131 self._emitEvent(PlayingGotoTime(t=t))
106 else: 132 else:
107 self.emitEvent(PausedGotoTime(t=t)) 133 self._emitEvent(PausedGotoTime(t=t))