tictacsync 1.2.0b0__py3-none-any.whl → 1.4.5b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tictacsync might be problematic. Click here for more details.
- tictacsync/device_scanner.py +23 -245
- tictacsync/entry.py +15 -34
- tictacsync/mamconf.py +18 -36
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +13 -75
- tictacsync/multi2polywav.py +3 -2
- tictacsync/timeline.py +28 -26
- tictacsync/yaltc.py +357 -29
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.5b0.dist-info}/METADATA +2 -2
- tictacsync-1.4.5b0.dist-info/RECORD +16 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.5b0.dist-info}/entry_points.txt +2 -1
- tictacsync/LTCcheck.py +0 -394
- tictacsync/newmix.py +0 -483
- tictacsync/remergemix.py +0 -259
- tictacsync/remrgmx.py +0 -116
- tictacsync/synciso.py +0 -143
- tictacsync-1.2.0b0.dist-info/RECORD +0 -19
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.5b0.dist-info}/LICENSE +0 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.5b0.dist-info}/WHEEL +0 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.5b0.dist-info}/top_level.txt +0 -0
tictacsync/mamreap.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import json, pathlib, itertools, os, re, ffmpeg, shutil, time
|
|
2
|
+
import argparse, platformdirs, configparser, sys
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from pprint import pformat
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from rich import print
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from rich.progress import Progress
|
|
9
|
+
|
|
10
|
+
# send-to-sound DSC085 -> one clip only, find the ISOs, load the clip
|
|
11
|
+
# send-to-sound cut27.otio -> whole project
|
|
12
|
+
# send-to-sound cut27.otio cut27.mov
|
|
13
|
+
# cut27.mov has TC + duration -> can find clips in otio...
|
|
14
|
+
# place cut27.mov according to its TC
|
|
15
|
+
# produce a cut27mix.wav saved in SNDROOT/postprod
|
|
16
|
+
# three modes: one clip; some clips; all clips
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from . import mamconf
|
|
22
|
+
from . import mamdav
|
|
23
|
+
except:
|
|
24
|
+
import mamconf
|
|
25
|
+
import mamdav
|
|
26
|
+
|
|
27
|
+
dev = 'Cockos Incorporated'
|
|
28
|
+
app ='REAPER'
|
|
29
|
+
REAPER_SCRIPT_LOCATION = pathlib.Path(platformdirs.user_data_dir(app, dev)) / 'Scripts' / 'Atomic'
|
|
30
|
+
REAPER_LUA_CODE = """reaper.Main_OnCommand(40577, 0) -- lock left/right move
|
|
31
|
+
reaper.Main_OnCommand(40569, 0) -- lock enabled
|
|
32
|
+
local function placeWavsBeginingAtTrack(clip, start_idx)
|
|
33
|
+
for i, file in ipairs(clip.files) do
|
|
34
|
+
local track_idx = start_idx + i - 1
|
|
35
|
+
local track = reaper.GetTrack(nil,track_idx-1)
|
|
36
|
+
reaper.SetOnlyTrackSelected(track)
|
|
37
|
+
local left_trim = clip.in_time - clip.start_time
|
|
38
|
+
local where = clip.timeline_pos - left_trim
|
|
39
|
+
reaper.SetEditCurPos(where, false, false)
|
|
40
|
+
reaper.InsertMedia(file, 0 )
|
|
41
|
+
local item_cnt = reaper.CountTrackMediaItems( track )
|
|
42
|
+
local item = reaper.GetTrackMediaItem( track, item_cnt-1 )
|
|
43
|
+
local take = reaper.GetTake(item, 0)
|
|
44
|
+
-- reaper.GetSetMediaItemTakeInfo_String(take, "P_NAME", clip.name, true)
|
|
45
|
+
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
46
|
+
reaper.BR_SetItemEdges(item, clip.timeline_pos, clip.timeline_pos + clip.cut_duration)
|
|
47
|
+
reaper.SetMediaItemInfo_Value(item, "C_LOCK", 2)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
--cut here--
|
|
52
|
+
|
|
53
|
+
sample of the clips nested table (this will be discarded)
|
|
54
|
+
each clip has an EDL info table plus a sequence of ISO files:
|
|
55
|
+
|
|
56
|
+
clips =
|
|
57
|
+
{
|
|
58
|
+
{
|
|
59
|
+
name="canon24fps01.MOV", start_time=7.25, in_time=21.125, cut_duration=6.875, timeline_pos=3600,
|
|
60
|
+
files=
|
|
61
|
+
{
|
|
62
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Alice_canon24fps01.wav",
|
|
63
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Bob_canon24fps01.wav"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{name="DSC_8063.MOV", start_time=0.0, in_time=5.0, cut_duration=20.25, timeline_pos=3606.875,
|
|
67
|
+
files={"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Alice_DSC_8063.wav",
|
|
68
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Bob_DSC_8063.wav"}},
|
|
69
|
+
{name="canon24fps02.MOV", start_time=35.166666666666664, in_time=35.166666666666664, cut_duration=20.541666666666668, timeline_pos=3627.125, files={"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps02_SND/ISOfiles/Alice_canon24fps02.wav",
|
|
70
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps02_SND/ISOfiles/Bob_canon24fps02.wav"}}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
--cut here--
|
|
74
|
+
-- make room fro the tracks to come
|
|
75
|
+
amplitude_top = 0
|
|
76
|
+
amplitude_bottom = 0
|
|
77
|
+
for i_clip, cl in pairs(clips) do
|
|
78
|
+
if i_clip%2 ~= 1 then
|
|
79
|
+
amplitude_top = math.max(amplitude_top, #cl.files)
|
|
80
|
+
else
|
|
81
|
+
amplitude_bottom = math.max(amplitude_bottom, #cl.files)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
for i = 1 , amplitude_top + amplitude_bottom + 1 do
|
|
85
|
+
reaper.InsertTrackAtIndex( -1, false ) -- at end
|
|
86
|
+
end
|
|
87
|
+
track_count = reaper.CountTracks(0)
|
|
88
|
+
-- ISOs will be up and down the base_track index
|
|
89
|
+
base_track = track_count - amplitude_bottom
|
|
90
|
+
for iclip, clip in ipairs(clips) do
|
|
91
|
+
start_track_number = base_track
|
|
92
|
+
-- alternating even/odd, odd=below base_track
|
|
93
|
+
if iclip%2 == 0 then -- above base_track, start higher
|
|
94
|
+
start_track_number = base_track - #clip.files
|
|
95
|
+
end
|
|
96
|
+
placeWavsBeginingAtTrack(clip, start_track_number)
|
|
97
|
+
if #clips > 1 then -- interclips editing
|
|
98
|
+
reaper.AddProjectMarker(0, false, clip.timeline_pos, 0, '', -1)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
reaper.SetEditCurPos(3600, false, false)
|
|
102
|
+
reaper.Main_OnCommand(40151, 0)
|
|
103
|
+
if #clips > 1 then -- interclips editing
|
|
104
|
+
-- last marker at the end
|
|
105
|
+
last_clip = clips[#clips]
|
|
106
|
+
reaper.AddProjectMarker(0, false, last_clip.timeline_pos + last_clip.cut_duration, 0, '', -1)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
v_file_extensions = \
|
|
111
|
+
"""MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
|
|
112
|
+
wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
|
|
113
|
+
m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp""".split()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
logger.level("DEBUG", color="<yellow>")
|
|
118
|
+
logger.add(sys.stdout, level="DEBUG")
|
|
119
|
+
logger.remove()
|
|
120
|
+
|
|
121
|
+
# class Modes(Enum):
|
|
122
|
+
# INTRACLIP = 1 # send-to-sound --clip DSC085 -> find the ISOs, load the clip in Reaper
|
|
123
|
+
# INTERCLIP_SOME = 2 # send-to-sound cut27.otio cut27.mov
|
|
124
|
+
# # cut27.mov has TC + duration -> can find clips in otio...
|
|
125
|
+
# # place cut27.mov according to its TC
|
|
126
|
+
# # Reaper will then produces a cut27mix.wav saved in SNDROOT/postprod
|
|
127
|
+
# INTERCLIP_ALL = 3 # send-to-sound cut27.otio -> whole project
|
|
128
|
+
|
|
129
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "parse_args_get_mode")
|
|
130
|
+
def parse_args_get_mode():
|
|
131
|
+
"""
|
|
132
|
+
parse args and determine which one of modes is used: INTRACLIP, INTERCLIP_SOME or
|
|
133
|
+
INTERCLIP_ALL.
|
|
134
|
+
Returns a 4-tuple: (mode, clip_argument, otio_file, render_file);
|
|
135
|
+
mode is of type mamdav.Modes(Enum); each of clip_argument, otio_file and render_file is None
|
|
136
|
+
if unset on the command line and of type str if set.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
descr = """Take the video clip (-c option) or parse the submitted OTIO timeline
|
|
140
|
+
(-a and -p options) to build a Reaper Script which loads the corresponding
|
|
141
|
+
ISO files from SNDROOT (see mamconf --show)."""
|
|
142
|
+
parser = argparse.ArgumentParser(description=descr)
|
|
143
|
+
parser.add_argument('-e',
|
|
144
|
+
action='store_true',
|
|
145
|
+
help="exit on completion (don't wait for the wav mix to be rendered by Reaper)")
|
|
146
|
+
parser.add_argument('-c',
|
|
147
|
+
dest='the_clip',
|
|
148
|
+
nargs=1,
|
|
149
|
+
help="send only this specified clip to Reaper (partial name is OK)")
|
|
150
|
+
parser.add_argument('-a',
|
|
151
|
+
dest='all',
|
|
152
|
+
nargs='*',
|
|
153
|
+
help="all the timeline will be sent and edited in Reaper")
|
|
154
|
+
parser.add_argument('-p',
|
|
155
|
+
dest='partial',
|
|
156
|
+
nargs='*',
|
|
157
|
+
help="only a timeline selected region will be edited in Reaper")
|
|
158
|
+
args = parser.parse_args()
|
|
159
|
+
logger.debug('args %s'%args)
|
|
160
|
+
if args.the_clip != None:
|
|
161
|
+
if len(args.the_clip) > 1:
|
|
162
|
+
print('Error: -c <A_CLIP> option should be used alone without any other argument. Bye.')
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
else:
|
|
165
|
+
# e.g. send-to-sound DSC087
|
|
166
|
+
# return: mode, clip_argument, otio_file, render
|
|
167
|
+
mode, clip_argument, otio_file, render_file = \
|
|
168
|
+
(mamdav.Modes.INTRACLIP, *args.the_clip, None, None)
|
|
169
|
+
exit = args.e
|
|
170
|
+
logger.debug('mode, clip_argument, otio_file, render_file, exit:')
|
|
171
|
+
logger.debug(f'{str(mode)}, { clip_argument}, {otio_file}, {render_file}, {exit}.')
|
|
172
|
+
return mode, clip_argument, otio_file, render_file, exit #################
|
|
173
|
+
def _is_otio(f):
|
|
174
|
+
components = f.split('.')
|
|
175
|
+
if len(components) == 1:
|
|
176
|
+
return False
|
|
177
|
+
return components[-1].lower() == 'otio'
|
|
178
|
+
if args.all != None:
|
|
179
|
+
otio_and_render = args.all
|
|
180
|
+
if args.partial != None:
|
|
181
|
+
print('Error: -a and -p are mutually exclusive, bye.')
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
if args.partial != None:
|
|
184
|
+
otio_and_render = args.partial
|
|
185
|
+
if args.all != None:
|
|
186
|
+
print('Error: -a and -p are mutually exclusive, bye.')
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
if len(otio_and_render) > 2:
|
|
189
|
+
print(f'Error: no more than two files are needed, bye.')
|
|
190
|
+
sys.exit(0)
|
|
191
|
+
otio_candidate = [f for f in otio_and_render if _is_otio(f)]
|
|
192
|
+
logger.debug(f'otio_candidate {otio_candidate}')
|
|
193
|
+
if len(otio_candidate) == 0:
|
|
194
|
+
print('Error: an OTIO file (or a -c argument) is needed. Bye.')
|
|
195
|
+
sys.exit(0)
|
|
196
|
+
if len(otio_candidate) > 1:
|
|
197
|
+
print(f'Error: one OTIO file is needed, not {len(otio_candidate)}. Bye.')
|
|
198
|
+
sys.exit(0)
|
|
199
|
+
otio = otio_candidate[0]
|
|
200
|
+
if len(otio_and_render) == 1:
|
|
201
|
+
# e.g.: send-to-sound cut27.otio
|
|
202
|
+
# return: mode, clip_argument, otio_file, render
|
|
203
|
+
mode, clip_argument, otio_file, render_file = \
|
|
204
|
+
(mamdav.Modes.INTERCLIP_ALL, None, otio, None)
|
|
205
|
+
exit = args.e
|
|
206
|
+
logger.debug('mode, clip_argument, otio_file, render_file, exit:')
|
|
207
|
+
logger.debug(f'{str(mode)}, { clip_argument}, {otio_file}, {render_file}, {exit}.')
|
|
208
|
+
return mode, clip_argument, otio_file, render_file, exit #####################
|
|
209
|
+
render = [f for f in otio_and_render if f != otio][0]
|
|
210
|
+
if render.split('.')[-1].lower() not in v_file_extensions:
|
|
211
|
+
print(f'Error: "{render}" does not have a video file extension, bye.')
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
# e.g.: send-to-sound cut27.otio cut27.mov
|
|
214
|
+
# return: mode, clip_argument, otio_file, render
|
|
215
|
+
mode, clip_argument, otio_file, render_file = \
|
|
216
|
+
(mamdav.Modes.INTERCLIP_SOME, None, otio, render)
|
|
217
|
+
exit = args.e
|
|
218
|
+
logger.debug('mode, clip_argument, otio_file, render_file, exit:')
|
|
219
|
+
logger.debug(f'{str(mode)}, { clip_argument}, {otio_file}, {render_file}, {exit}.')
|
|
220
|
+
return mode, clip_argument, otio_file, render_file, exit #########################
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class Clip:
|
|
224
|
+
# all time in seconds
|
|
225
|
+
start_time: float # the start time of the clip, != 0 if metadata TC
|
|
226
|
+
in_time: float # time of 'in' point, if in_time == start_time, no left trim
|
|
227
|
+
cut_duration: float # with this value, right trim is detemined, if needed
|
|
228
|
+
whole_duration: float # unedited clip duration
|
|
229
|
+
name: str #
|
|
230
|
+
path: str # path of clip
|
|
231
|
+
timeline_pos: float # when on the timeline the clip starts
|
|
232
|
+
ISOdir: None # folder of ISO files for clip
|
|
233
|
+
|
|
234
|
+
def clip_info_from_json(jsoncl):
|
|
235
|
+
"""
|
|
236
|
+
parse data from an OTIO json Clip
|
|
237
|
+
https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-serialized-schema.html#clip-2
|
|
238
|
+
returns a list composed of (all times are in seconds):
|
|
239
|
+
st, start time (from clip metadata TC)
|
|
240
|
+
In, the "in time", if in_time == start_time, no left trim
|
|
241
|
+
cd, the cut duration
|
|
242
|
+
wl, the whole length of the unedited clip
|
|
243
|
+
the clip file path (string)
|
|
244
|
+
name (string)
|
|
245
|
+
NB: Clip.timeline_pos (the position on the global timeline) is not set here but latter computed from summing cut times
|
|
246
|
+
"""
|
|
247
|
+
def _float_time(json_rationaltime):
|
|
248
|
+
# returns a time in seconds (float)
|
|
249
|
+
return json_rationaltime['value']/json_rationaltime['rate']
|
|
250
|
+
av_range = jsoncl['media_references']['DEFAULT_MEDIA']['available_range']
|
|
251
|
+
src_rg = jsoncl['source_range']
|
|
252
|
+
st = av_range['start_time']
|
|
253
|
+
In = src_rg['start_time']
|
|
254
|
+
cd = src_rg['duration']
|
|
255
|
+
wl = av_range['duration']
|
|
256
|
+
path = jsoncl['media_references']['DEFAULT_MEDIA']['target_url']
|
|
257
|
+
name = jsoncl['media_references']['DEFAULT_MEDIA']['name']
|
|
258
|
+
return Clip(*[_float_time(t) for t in [st, In, cd, wl,]] + \
|
|
259
|
+
[name, path, 0, None])
|
|
260
|
+
|
|
261
|
+
def get_SND_dirs(snd_root):
|
|
262
|
+
# returns all directories found under snd_root
|
|
263
|
+
def _searchDirectory(cwd,searchResults):
|
|
264
|
+
dirs = os.listdir(cwd)
|
|
265
|
+
for dir in dirs:
|
|
266
|
+
fullpath = os.path.join(cwd,dir)
|
|
267
|
+
if os.path.isdir(fullpath):
|
|
268
|
+
searchResults.append(fullpath)
|
|
269
|
+
_searchDirectory(fullpath,searchResults)
|
|
270
|
+
searchResults = []
|
|
271
|
+
_searchDirectory(snd_root,searchResults)
|
|
272
|
+
return searchResults
|
|
273
|
+
|
|
274
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "find_and_set_ISO_dir")
|
|
275
|
+
def find_and_set_ISO_dir(clip, SND_dirs):
|
|
276
|
+
"""
|
|
277
|
+
SND_dirs contains all the *_SND directories found in snd_root.
|
|
278
|
+
This fct finds out which one corresponds to the clip
|
|
279
|
+
and sets the found path to clip.ISOdir.
|
|
280
|
+
Returns nothing.
|
|
281
|
+
"""
|
|
282
|
+
clip_argument = pathlib.Path(clip.path).stem
|
|
283
|
+
logger.debug(f'clip_argument {clip_argument}')
|
|
284
|
+
m = re.match(r'(.*)_v(\w{32})', clip_argument) #
|
|
285
|
+
logger.debug(f'{clip_argument} match (.*)v([AB]*) { m.groups() if m != None else None}')
|
|
286
|
+
if m != None:
|
|
287
|
+
clip_argument = m.groups()[0]
|
|
288
|
+
# /MyBigMovie/day01/leftCAM/card01/canon24fps01_SND -> canon24fps01_SND
|
|
289
|
+
names_only = [p.name for p in SND_dirs]
|
|
290
|
+
logger.debug(f'names-only {pformat(names_only)}')
|
|
291
|
+
clip_stem_SND = f'{clip_argument}_SND'
|
|
292
|
+
if clip_stem_SND in names_only:
|
|
293
|
+
where = names_only.index(clip_stem_SND)
|
|
294
|
+
else:
|
|
295
|
+
print(f'Error: OTIO file contains clip not in SYNCEDROOT: {clip_argument} (check with mamconf --show)')
|
|
296
|
+
sys.exit(0)
|
|
297
|
+
complete_path = SND_dirs[where]
|
|
298
|
+
logger.debug(f'found {complete_path}')
|
|
299
|
+
clip.ISOdir = str(complete_path)
|
|
300
|
+
|
|
301
|
+
def gen_lua_table(clips):
|
|
302
|
+
# returns a string defining a lua nested table
|
|
303
|
+
# top level: a sequence of clips
|
|
304
|
+
# a clip has keys: name, start_time, in_time, cut_duration, timeline_pos, files
|
|
305
|
+
# clip.files is a sequence of ISO wav files
|
|
306
|
+
def _list_ISO(dir):
|
|
307
|
+
iso_dir = pathlib.Path(dir)/'ISOfiles'
|
|
308
|
+
ISOs = [f for f in iso_dir.iterdir() if f.suffix.lower() == '.wav']
|
|
309
|
+
# ISOs = [f for f in ISOs if f.name[:2] != 'tc'] # no timecode
|
|
310
|
+
logger.debug(f'ISOs {ISOs}')
|
|
311
|
+
sequence = '{'
|
|
312
|
+
for file in ISOs:
|
|
313
|
+
sequence += f'"{file}",\n'
|
|
314
|
+
sequence += '}'
|
|
315
|
+
return sequence
|
|
316
|
+
lua_clips = '{'
|
|
317
|
+
for cl in clips:
|
|
318
|
+
ISOs = _list_ISO(cl.ISOdir)
|
|
319
|
+
# logger.debug(f'sequence {ISOs}')
|
|
320
|
+
clip_table = f'{{name="{cl.name}", start_time={cl.start_time}, in_time={cl.in_time}, cut_duration={cl.cut_duration}, timeline_pos={cl.timeline_pos}, files={ISOs}}}'
|
|
321
|
+
lua_clips += f'{clip_table},\n'
|
|
322
|
+
logger.debug(f'clip_table {clip_table}')
|
|
323
|
+
lua_clips += '}'
|
|
324
|
+
return lua_clips
|
|
325
|
+
|
|
326
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "read_OTIO_file")
|
|
327
|
+
def read_OTIO_file(f):
|
|
328
|
+
"""
|
|
329
|
+
returns framerate and a list of Clip instances parsed from
|
|
330
|
+
the OTIO file passed as (string) argument f;
|
|
331
|
+
warns and exists if more than one video track.
|
|
332
|
+
"""
|
|
333
|
+
with open(f) as fh:
|
|
334
|
+
oti = json.load(fh)
|
|
335
|
+
video_tracks = [tr for tr in oti['tracks']['children'] if tr['kind'] == 'Video']
|
|
336
|
+
if len(video_tracks) > 1:
|
|
337
|
+
print(f"Can only process timeline with one video track, this one has {len(video_tracks)}. Bye.")
|
|
338
|
+
sys.exit(0)
|
|
339
|
+
video_track = video_tracks[0]
|
|
340
|
+
clips = [clip_info_from_json(jscl) for jscl in video_track['children']]
|
|
341
|
+
# compute each clip global timeline position
|
|
342
|
+
clip_starts = [0] + list(itertools.accumulate([cl.cut_duration for cl in clips]))[:-1]
|
|
343
|
+
# Reaper can't handle negative item position (for the trimmed part)
|
|
344
|
+
# so starts at 1:00:00
|
|
345
|
+
clip_starts = [t + 3600 for t in clip_starts]
|
|
346
|
+
logger.debug(f'clip_starts: {clip_starts}')
|
|
347
|
+
for time, clip in zip(clip_starts, clips):
|
|
348
|
+
clip.timeline_pos = time
|
|
349
|
+
logger.debug(f'clips: {pformat(clips)}')
|
|
350
|
+
return int(oti['global_start_time']['rate']), clips
|
|
351
|
+
|
|
352
|
+
def build_reaper_render_action(wav_destination):
|
|
353
|
+
directory = wav_destination.absolute().parent
|
|
354
|
+
return f"""reaper.GetSetProjectInfo_String(0, "RENDER_FILE","{directory}",true)
|
|
355
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN","{wav_destination.name}",true)
|
|
356
|
+
reaper.SNM_SetIntConfigVar("projintmix", 4)
|
|
357
|
+
reaper.Main_OnCommand(40015, 0)
|
|
358
|
+
"""
|
|
359
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "complete_clip_path")
|
|
360
|
+
def complete_clip_path(clip_argument, synced_proj):
|
|
361
|
+
match = []
|
|
362
|
+
for (root,dirs,files) in os.walk(synced_proj):
|
|
363
|
+
for f in files:
|
|
364
|
+
p = pathlib.Path(root)/f
|
|
365
|
+
if p.is_symlink() or p.suffix == '.reapeaks':
|
|
366
|
+
continue
|
|
367
|
+
# logger.debug(f'{f}')
|
|
368
|
+
if clip_argument in f.split('.')[0]: # match XYZvA.mov
|
|
369
|
+
match.append(p)
|
|
370
|
+
logger.debug(f'matches {match}')
|
|
371
|
+
if len(match) > 1:
|
|
372
|
+
print(f'Warning, some filenames collide:')
|
|
373
|
+
[print(m) for m in match]
|
|
374
|
+
print('Bye.')
|
|
375
|
+
sys.exit(0)
|
|
376
|
+
if len(match) == 0:
|
|
377
|
+
print(f"Error, didn't find any clip containing *{clip_argument}*. Bye.")
|
|
378
|
+
sys.exit(0)
|
|
379
|
+
return match[0]
|
|
380
|
+
|
|
381
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
382
|
+
def main():
|
|
383
|
+
mode, clip_argument, otio_file, render, exit = parse_args_get_mode()
|
|
384
|
+
# def _where(a,x):
|
|
385
|
+
# # find in which clip time x (in seconds) does fall.
|
|
386
|
+
# n = 0
|
|
387
|
+
# while n<len(a):
|
|
388
|
+
# if a[n].timeline_pos > x:
|
|
389
|
+
# break
|
|
390
|
+
# else:
|
|
391
|
+
# n += 1
|
|
392
|
+
# return n-1
|
|
393
|
+
raw_root, synced_root, snd_root, proxies = mamconf.get_proj(False)
|
|
394
|
+
proj_name = pathlib.Path(raw_root).stem
|
|
395
|
+
synced_proj = pathlib.Path(synced_root)/proj_name
|
|
396
|
+
logger.debug(f'proj_name {proj_name}')
|
|
397
|
+
logger.debug(f'will search {snd_root} for ISOs')
|
|
398
|
+
all_SNDROOT_dirs = [pathlib.Path(f) for f in get_SND_dirs(snd_root)]
|
|
399
|
+
# keep only XYZ_SND dirs
|
|
400
|
+
SND_dirs = [p for p in all_SNDROOT_dirs if p.name[-4:] == '_SND']
|
|
401
|
+
logger.debug(f'SND_dirs {pformat(SND_dirs)}')
|
|
402
|
+
match mode:
|
|
403
|
+
case mamdav.Modes.INTRACLIP:
|
|
404
|
+
# e.g.: send-to-sound DSC087
|
|
405
|
+
logger.debug('Modes.INTRACLIP, intraclip sound edit, clips will have one clip')
|
|
406
|
+
# traverse synced_root to find clip path
|
|
407
|
+
clip_path = complete_clip_path(clip_argument, synced_proj)
|
|
408
|
+
clip_stem = clip_path.stem
|
|
409
|
+
probe = ffmpeg.probe(clip_path)
|
|
410
|
+
duration = float(probe['format']['duration'])
|
|
411
|
+
clips = [Clip(
|
|
412
|
+
start_time=0,
|
|
413
|
+
in_time=0,
|
|
414
|
+
cut_duration=duration,
|
|
415
|
+
whole_duration=duration,
|
|
416
|
+
name=clip_argument,
|
|
417
|
+
path=clip_path,
|
|
418
|
+
timeline_pos=3600,
|
|
419
|
+
ISOdir='')]
|
|
420
|
+
[find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
|
|
421
|
+
print(f'For video clip: \n{clip_path}\nfound audio in:\n{clips[0].ISOdir}')
|
|
422
|
+
case mamdav.Modes.INTERCLIP_SOME:
|
|
423
|
+
# [TODO]
|
|
424
|
+
# e.g.: mamreap -p cut27.otio cut27.mov
|
|
425
|
+
pass
|
|
426
|
+
case mamdav.Modes.INTERCLIP_ALL:
|
|
427
|
+
# e.g.: send-to-sound cut27.otio
|
|
428
|
+
logger.debug('Modes.INTERCLIP_ALL, interclip sound edit, filling up ALL clips')
|
|
429
|
+
_, clips = read_OTIO_file(otio_file)
|
|
430
|
+
[find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
|
|
431
|
+
logger.debug(f'clips with found ISOdir: {pformat(clips)}')
|
|
432
|
+
lua_clips = gen_lua_table(clips)
|
|
433
|
+
logger.debug(f'lua_clips {lua_clips}')
|
|
434
|
+
# title = "Load MyBigMovie Audio.lua" either Modes
|
|
435
|
+
title = f'Load {pathlib.Path(raw_root).name} Audio'
|
|
436
|
+
script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'{title}.lua'
|
|
437
|
+
Lua_script_pre, _ , Lua_script_post = REAPER_LUA_CODE.split('--cut here--')
|
|
438
|
+
script = Lua_script_pre + 'clips=' + lua_clips + Lua_script_post
|
|
439
|
+
with open(script_path, 'w') as fh:
|
|
440
|
+
fh.write(script)
|
|
441
|
+
print(f'Wrote ReaScripts "{script_path.stem}"', end=' ')
|
|
442
|
+
if mode == mamdav.Modes.INTRACLIP:
|
|
443
|
+
render_destination = pathlib.Path(clips[0].ISOdir)/f'{clip_stem}_mix.wav'
|
|
444
|
+
else:
|
|
445
|
+
logger.debug('render for mode all clips')
|
|
446
|
+
op = pathlib.Path(otio_file)
|
|
447
|
+
render_destination = op.parent/f'{op.stem}_mix.wav'
|
|
448
|
+
logger.debug(f'render destination {render_destination}')
|
|
449
|
+
logger.debug(f'will build rendering clip with dest: {render_destination}')
|
|
450
|
+
lua_code = build_reaper_render_action(render_destination)
|
|
451
|
+
if render_destination.exists():
|
|
452
|
+
render_destination.unlink()
|
|
453
|
+
logger.debug(f'clip\n{lua_code}')
|
|
454
|
+
script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Render Movie Audio.lua'
|
|
455
|
+
with open(script_path, 'w') as fh:
|
|
456
|
+
fh.write(lua_code)
|
|
457
|
+
print(f'and "{script_path.stem}"')
|
|
458
|
+
print(f'Reaper will render audio to "{render_destination.absolute()}"')
|
|
459
|
+
if mode in [mamdav.Modes.INTERCLIP_ALL, mamdav.Modes.INTERCLIP_SOME]:
|
|
460
|
+
print(f'Warning: once saved, "{render_destination.name}" wont be of any use if not paired with "{op.name}", so keep them in the same directory.')
|
|
461
|
+
if not exit:
|
|
462
|
+
# wait for mix and lauch mamdav
|
|
463
|
+
print('Go editing in Reaper...')
|
|
464
|
+
with Progress(transient=True) as progress:
|
|
465
|
+
task = progress.add_task(f"[green]Waiting for {render_destination.name}...", total=None)
|
|
466
|
+
while not render_destination.exists():
|
|
467
|
+
time.sleep(5)
|
|
468
|
+
progress.stop()
|
|
469
|
+
time.sleep(3) # finishing writing?
|
|
470
|
+
print(f'saw {render_destination.name}: ')
|
|
471
|
+
# print('go mamdav!')
|
|
472
|
+
wav_path = render_destination
|
|
473
|
+
movie_path = None
|
|
474
|
+
otio_path = op
|
|
475
|
+
mamdav.go(mode, otio_path, movie_path, wav_path)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
if __name__ == '__main__':
|
|
480
|
+
main()
|
|
481
|
+
|
tictacsync/mamsync.py
CHANGED
|
@@ -10,14 +10,16 @@ try:
|
|
|
10
10
|
from . import timeline
|
|
11
11
|
from . import multi2polywav
|
|
12
12
|
from . import mamconf
|
|
13
|
+
from . import entry
|
|
13
14
|
except:
|
|
14
15
|
import yaltc
|
|
15
16
|
import device_scanner
|
|
16
17
|
import timeline
|
|
17
18
|
import multi2polywav
|
|
18
19
|
import mamconf
|
|
20
|
+
import entry
|
|
19
21
|
|
|
20
|
-
import argparse, tempfile, configparser
|
|
22
|
+
import argparse, tempfile, configparser, re
|
|
21
23
|
from loguru import logger
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
# import os, sys
|
|
@@ -43,75 +45,12 @@ amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
|
|
|
43
45
|
ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
|
|
44
46
|
|
|
45
47
|
logger.remove()
|
|
48
|
+
# logger.level("DEBUG", color="<yellow>")
|
|
46
49
|
# logger.add(sys.stdout, level="DEBUG")
|
|
47
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
50
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "__init__" and r["module"] == "yaltc")
|
|
51
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_fit_length")
|
|
48
52
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_write_ISOs")
|
|
49
53
|
|
|
50
|
-
def process_single(file, args):
|
|
51
|
-
# argument is a single file
|
|
52
|
-
m = device_scanner.media_at_path(None, Path(file))
|
|
53
|
-
if args.plotting:
|
|
54
|
-
print('\nPlots can be zoomed and panned...')
|
|
55
|
-
print('Close window for next one.')
|
|
56
|
-
a_rec = yaltc.Recording(m, do_plots=args.plotting)
|
|
57
|
-
time = a_rec.get_start_time()
|
|
58
|
-
# time = a_rec.get_start_time(plots=args.plotting)
|
|
59
|
-
if time != None:
|
|
60
|
-
frac_time = int(time.microsecond / 1e2)
|
|
61
|
-
d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
|
|
62
|
-
if args.terse:
|
|
63
|
-
print('%s UTC:%s pulse: %i in chan %i'%(file, d, a_rec.sync_position,
|
|
64
|
-
a_rec.TicTacCode_channel))
|
|
65
|
-
else:
|
|
66
|
-
print('\nRecording started at [gold1]%s[/gold1] UTC'%d)
|
|
67
|
-
print('true sample rate: [gold1]%.3f Hz[/gold1]'%a_rec.true_samplerate)
|
|
68
|
-
print('first sync at [gold1]%i[/gold1] samples in channel %i'%(a_rec.sync_position,
|
|
69
|
-
a_rec.TicTacCode_channel))
|
|
70
|
-
print('N.B.: all results are precise to the displayed digits!\n')
|
|
71
|
-
else:
|
|
72
|
-
if args.terse:
|
|
73
|
-
print('%s UTC: None'%(file))
|
|
74
|
-
else:
|
|
75
|
-
print('Start time couldnt be determined')
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
|
|
78
|
-
def process_lag_adjustement(media_object):
|
|
79
|
-
# trim channels that are lagging (as stated in tracks.txt)
|
|
80
|
-
# replace the old file, and rename the old one with .wavbk
|
|
81
|
-
# if .wavbk exist, process was done already, so dont process
|
|
82
|
-
# returns nothing
|
|
83
|
-
lags = media_object.device.tracks.lag_values
|
|
84
|
-
logger.debug('will process %s lags'%[lags])
|
|
85
|
-
channels = timeline._sox_split_channels(media_object.path)
|
|
86
|
-
# add bk to file on filesystem, but media_object.path is unchanged (?)
|
|
87
|
-
backup_name = str(media_object.path) + 'bk'
|
|
88
|
-
if Path(backup_name).exists():
|
|
89
|
-
logger.debug('%s exists, so return now.'%backup_name)
|
|
90
|
-
return
|
|
91
|
-
media_object.path.replace(backup_name)
|
|
92
|
-
logger.debug('channels %s'%channels)
|
|
93
|
-
def _trim(lag, chan_file):
|
|
94
|
-
# for lag
|
|
95
|
-
if lag == None:
|
|
96
|
-
return chan_file
|
|
97
|
-
else:
|
|
98
|
-
logger.debug('process %s for lag of %s'%(chan_file, lag))
|
|
99
|
-
sox_transform = sox.Transformer()
|
|
100
|
-
sox_transform.trim(float(lag)*1e-3)
|
|
101
|
-
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
102
|
-
out_file = timeline._pathname(output_fh)
|
|
103
|
-
input_file = timeline._pathname(chan_file)
|
|
104
|
-
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
105
|
-
logger.debug('calling sox_transform.build()')
|
|
106
|
-
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
107
|
-
logger.debug('sox.build exit code %s'%str(status))
|
|
108
|
-
return output_fh
|
|
109
|
-
new_channels = [_trim(*e) for e in zip(lags, channels)]
|
|
110
|
-
logger.debug('new_channels %s'%new_channels)
|
|
111
|
-
trimmed_multichanfile = timeline._sox_combine(new_channels)
|
|
112
|
-
logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
|
|
113
|
-
Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
|
|
114
|
-
|
|
115
54
|
def copy_to_syncedroot(raw_root, synced_root):
|
|
116
55
|
# args are str
|
|
117
56
|
# copy dirs and non AV files
|
|
@@ -123,7 +62,6 @@ def copy_to_syncedroot(raw_root, synced_root):
|
|
|
123
62
|
if ext not in av_file_extensions and not is_DS_Store:
|
|
124
63
|
logger.debug(f'raw_path: {raw_path}')
|
|
125
64
|
# dont copy WAVs either, they will be in ISOs
|
|
126
|
-
# synced_path = Path(synced_root)/str(raw_path)[1:] # cant join abs. paths
|
|
127
65
|
rel = raw_path.relative_to(raw_root)
|
|
128
66
|
logger.debug(f'relative path {rel}')
|
|
129
67
|
synced_path = Path(synced_root)/Path(raw_root).name/rel
|
|
@@ -155,8 +93,6 @@ def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
|
|
|
155
93
|
if raw_path.is_dir():
|
|
156
94
|
synced_path.mkdir(parents=True, exist_ok=True)
|
|
157
95
|
|
|
158
|
-
|
|
159
|
-
|
|
160
96
|
def new_parser():
|
|
161
97
|
parser = argparse.ArgumentParser()
|
|
162
98
|
parser.add_argument('--resync',
|
|
@@ -228,6 +164,8 @@ def main():
|
|
|
228
164
|
top_dir = raw_root
|
|
229
165
|
if args.resync:
|
|
230
166
|
clear_log()
|
|
167
|
+
# deleted = clean_synced(raw_root, synced_root)
|
|
168
|
+
# logger.debug(f'deleted older clip versions: {deleted}')
|
|
231
169
|
# go, mamsync!
|
|
232
170
|
copy_to_syncedroot(raw_root, synced_root)
|
|
233
171
|
# copy_raw_root_tree_to_sndroot(raw_root, snd_root) # why?
|
|
@@ -240,9 +178,9 @@ def main():
|
|
|
240
178
|
logger.debug('%s has lag_values %s'%(
|
|
241
179
|
m.path, m.device.tracks.lag_values))
|
|
242
180
|
# any lag for a channel is specified by user in tracks.txt
|
|
243
|
-
process_lag_adjustement(m)
|
|
181
|
+
entry.process_lag_adjustement(m)
|
|
244
182
|
audio_REC_only = all([m.device.dev_type == 'REC' for m
|
|
245
|
-
|
|
183
|
+
in scanner.found_media_files])
|
|
246
184
|
if not args.terse:
|
|
247
185
|
if scanner.input_structure == 'ordered':
|
|
248
186
|
print('\nDetected structured folders')
|
|
@@ -279,12 +217,13 @@ def main():
|
|
|
279
217
|
quit()
|
|
280
218
|
print()
|
|
281
219
|
recordings = [yaltc.Recording(m, do_plots=False) for m
|
|
282
|
-
|
|
220
|
+
in scanner.found_media_files]
|
|
283
221
|
recordings_with_time = [
|
|
284
222
|
rec
|
|
285
223
|
for rec in recordings
|
|
286
224
|
if rec.get_start_time()
|
|
287
225
|
]
|
|
226
|
+
[r.load_track_info() for r in recordings_with_time if r.is_audio()]
|
|
288
227
|
if not args.terse:
|
|
289
228
|
table = Table(title="tictacsync results")
|
|
290
229
|
table.add_column("Recording\n", justify="center", style='gold1')
|
|
@@ -333,10 +272,9 @@ def main():
|
|
|
333
272
|
sys.exit(1)
|
|
334
273
|
if scanner.input_structure != 'ordered':
|
|
335
274
|
print('Warning, can\'t run mamsync without structured folders: [gold1]--isos[/gold1] option ignored.\n')
|
|
336
|
-
print('Merging...')
|
|
337
275
|
asked_ISOs = True # par defaut
|
|
338
276
|
dont_write_cam_folder = False # write them
|
|
339
|
-
for merger in matcher.mergers:
|
|
277
|
+
for merger in track(matcher.mergers, description="Merging..."):
|
|
340
278
|
merger._build_audio_and_write_video(top_dir,
|
|
341
279
|
dont_write_cam_folder,
|
|
342
280
|
asked_ISOs,
|
tictacsync/multi2polywav.py
CHANGED
|
@@ -29,8 +29,10 @@ def print_grby(grby):
|
|
|
29
29
|
print(' ', e)
|
|
30
30
|
|
|
31
31
|
def wav_recursive_scan(top_directory):
|
|
32
|
-
files_lower_case = Path(top_directory).rglob('*.wav')
|
|
32
|
+
files_lower_case = Path(top_directory).rglob('*.wav')
|
|
33
|
+
files_lower_case = [p for p in files_lower_case if p.name[0] != '.']
|
|
33
34
|
files_upper_case = Path(top_directory).rglob('*.WAV')
|
|
35
|
+
files_upper_case = [p for p in files_upper_case if p.name[0] != '.']
|
|
34
36
|
files = set(list(files_lower_case) + list(files_upper_case))
|
|
35
37
|
paths = [
|
|
36
38
|
p
|
|
@@ -56,7 +58,6 @@ def nframes(path):
|
|
|
56
58
|
return duration_ts
|
|
57
59
|
return n_frames
|
|
58
60
|
|
|
59
|
-
|
|
60
61
|
def build_poly_name(multifiles):
|
|
61
62
|
"""
|
|
62
63
|
Returns string of polywav filename, constructed from similitudes between
|