tictacsync 1.2.0b0__py3-none-any.whl → 1.4.0b0__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/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"] == "main")
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
@@ -147,6 +85,24 @@ def copy_to_syncedroot(raw_root, synced_root):
147
85
  logger.debug('same content, next')
148
86
  continue # next raw_path in loop
149
87
 
88
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "clean_synced")
89
+ # def clean_synced(raw_root, synced_root):
90
+ # # removes <>v<capitalLetter>.mov if any
91
+ # # returns a list of deleted pathlib.Path
92
+ # project = Path(raw_root).name
93
+ # logger.debug(f'project {project}')
94
+ # synced_proj = Path(synced_root)/project
95
+ # if not synced_proj.exists():
96
+ # synced_proj.mkdir()
97
+ # deleted = []
98
+ # for raw_path in Path(synced_proj).rglob('*'):
99
+ # m = re.match(r'.*(v[A-Z])\..+', raw_path.name)
100
+ # if m != None:
101
+ # logger.debug(f'cleaning {raw_path}')
102
+ # raw_path.unlink()
103
+ # deleted.append(raw_path)
104
+ # return deleted
105
+
150
106
  def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
151
107
  # args are str
152
108
  # copy only tree structure, no files
@@ -155,8 +111,6 @@ def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
155
111
  if raw_path.is_dir():
156
112
  synced_path.mkdir(parents=True, exist_ok=True)
157
113
 
158
-
159
-
160
114
  def new_parser():
161
115
  parser = argparse.ArgumentParser()
162
116
  parser.add_argument('--resync',
@@ -228,6 +182,8 @@ def main():
228
182
  top_dir = raw_root
229
183
  if args.resync:
230
184
  clear_log()
185
+ # deleted = clean_synced(raw_root, synced_root)
186
+ # logger.debug(f'deleted older clip versions: {deleted}')
231
187
  # go, mamsync!
232
188
  copy_to_syncedroot(raw_root, synced_root)
233
189
  # copy_raw_root_tree_to_sndroot(raw_root, snd_root) # why?
@@ -240,9 +196,9 @@ def main():
240
196
  logger.debug('%s has lag_values %s'%(
241
197
  m.path, m.device.tracks.lag_values))
242
198
  # any lag for a channel is specified by user in tracks.txt
243
- process_lag_adjustement(m)
199
+ entry.process_lag_adjustement(m)
244
200
  audio_REC_only = all([m.device.dev_type == 'REC' for m
245
- in scanner.found_media_files])
201
+ in scanner.found_media_files])
246
202
  if not args.terse:
247
203
  if scanner.input_structure == 'ordered':
248
204
  print('\nDetected structured folders')
@@ -279,12 +235,13 @@ def main():
279
235
  quit()
280
236
  print()
281
237
  recordings = [yaltc.Recording(m, do_plots=False) for m
282
- in scanner.found_media_files]
238
+ in scanner.found_media_files]
283
239
  recordings_with_time = [
284
240
  rec
285
241
  for rec in recordings
286
242
  if rec.get_start_time()
287
243
  ]
244
+ [r.load_track_info() for r in recordings_with_time if r.is_audio()]
288
245
  if not args.terse:
289
246
  table = Table(title="tictacsync results")
290
247
  table.add_column("Recording\n", justify="center", style='gold1')
@@ -333,10 +290,9 @@ def main():
333
290
  sys.exit(1)
334
291
  if scanner.input_structure != 'ordered':
335
292
  print('Warning, can\'t run mamsync without structured folders: [gold1]--isos[/gold1] option ignored.\n')
336
- print('Merging...')
337
293
  asked_ISOs = True # par defaut
338
294
  dont_write_cam_folder = False # write them
339
- for merger in matcher.mergers:
295
+ for merger in track(matcher.mergers, description="Merging..."):
340
296
  merger._build_audio_and_write_video(top_dir,
341
297
  dont_write_cam_folder,
342
298
  asked_ISOs,
@@ -56,7 +56,6 @@ def nframes(path):
56
56
  return duration_ts
57
57
  return n_frames
58
58
 
59
-
60
59
  def build_poly_name(multifiles):
61
60
  """
62
61
  Returns string of polywav filename, constructed from similitudes between