tictacsync 0.98a0__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/entry.py CHANGED
@@ -2,7 +2,7 @@
2
2
  # I know, the following is ugly, but I need those try's to
3
3
  # run the command in my dev setting AND from
4
4
  # a deployment set-up... surely I'm setting
5
- # things wrong [TODO] find why and clean up this mess
5
+ # things wrong [TODO]: find why and clean up this mess
6
6
 
7
7
  try:
8
8
  from . import yaltc
@@ -40,23 +40,6 @@ ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".spl
40
40
 
41
41
  logger.level("DEBUG", color="<yellow>")
42
42
 
43
- # def process_files_with_progress_bars(medias): # [todo, replace]
44
- # recordings = []
45
- # rec_with_TTC = []
46
- # times = []
47
- # for m in track(medias,
48
- # description="1/4 Initializing Recordings:"):
49
- # # file_alias = 'dummy'
50
- # recordings.append(yaltc.Recording(m))
51
- # for r in track(recordings,
52
- # description="2/4 Looking for TicTacCode:"):
53
- # if r.seems_to_have_TicTacCode_at_beginning():
54
- # rec_with_TTC.append(r)
55
- # for r in track(rec_with_TTC,
56
- # description="3/4 Finding start times:"):
57
- # times.append(r.get_start_time())
58
- # return recordings, rec_with_TTC, times
59
-
60
43
  def process_single(file, args):
61
44
  # argument is a single file
62
45
  m = device_scanner.media_at_path(None, Path(file))
@@ -101,7 +84,11 @@ def process_lag_adjustement(media_object):
101
84
  media_object.path.replace(backup_name)
102
85
  logger.debug('channels %s'%channels)
103
86
  def _trim(lag, chan_file):
104
- # for lag
87
+ # counter intuitive I know. if a file lags, there's too
88
+ # much samples at the start:
89
+ # ..................|.........
90
+ # .......................|....
91
+ # ^ play head ->
105
92
  if lag == None:
106
93
  return chan_file
107
94
  else:
@@ -122,67 +109,12 @@ def process_lag_adjustement(media_object):
122
109
  logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
123
110
  Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
124
111
 
125
- def start_proj(folders):
126
- # if existing values are found,
127
- # confirm new values with user.
128
- # returns (source_RAW, destination_synced)
129
- # either old (and confirmed) or new ones
130
- def _write_cfg():
131
- conf_dir = platformdirs.user_config_dir('tictacsync', 'plutz',
132
- ensure_exists=True)
133
- logger.debug('will start project with folders %s'%folders)
134
- conf_file = Path(conf_dir)/'mirrored.cfg'
135
- logger.debug('writing config in %s'%conf_file)
136
- conf_prs = configparser.ConfigParser()
137
- conf_prs['MIRRORED'] = {'source_RAW': folders[0],
138
- 'destination_synced': folders[1]}
139
- with open(conf_file, 'w') as configfile:
140
- conf_prs.write(configfile)
141
- known_values = get_proj()
142
- if known_values != ():
143
- source_RAW, destination_synced = known_values
144
- print('Warning: there is a current project')
145
- print('with source (RAW) folder: %s\nand destination (synced) folder: %s'%
146
- (source_RAW, destination_synced))
147
- answer = input("\nDo you want to change values? [YES|NO]")
148
- if answer.upper()[0] in ["Y", "YES"]:
149
- _write_cfg()
150
- return folders
151
- elif answer.upper()[0] in ["N", "NO"]:
152
- print('Ok, will keep old ones')
153
- return source_RAW, destination_synced
154
- else:
155
- _write_cfg()
156
- return folders
157
-
158
- sys.exit(0)
159
-
160
- def get_proj():
161
- # check if user started a project before.
162
- # stored in platformdirs.user_config_dir
163
- # returns (source_RAW, destination_synced) if any
164
- # () otherwise
165
- conf_dir = platformdirs.user_config_dir('tictacsync', 'plutz')
166
- conf_file = Path(conf_dir)/'mirrored.cfg'
167
- if conf_file.exists():
168
- logger.debug('reading config in %s'%conf_file)
169
- conf_prs = configparser.ConfigParser()
170
- conf_prs.read(conf_file)
171
- source_RAW = conf_prs.get('MIRRORED', 'source_RAW')
172
- destination_synced = conf_prs.get('MIRRORED', 'destination_synced')
173
- logger.debug('read source_RAW: %s and destination_synced: %s'%
174
- (source_RAW, destination_synced))
175
- return (source_RAW, destination_synced)
176
- else:
177
- logger.debug('no config file found')
178
- return ()
179
-
180
112
  def main():
181
113
  parser = argparse.ArgumentParser()
182
114
  parser.add_argument(
183
115
  "path",
184
116
  type=str,
185
- nargs='*',
117
+ nargs=1,
186
118
  help="directory_name or media_file"
187
119
  )
188
120
  # parser.add_argument("directory", nargs="?", help="path of media directory")
@@ -191,15 +123,15 @@ def main():
191
123
  action='store_true', #ie default False
192
124
  dest='verbose_output',
193
125
  help='Set verbose ouput')
194
- parser.add_argument('--stop_mirroring',
195
- action='store_true', #ie default False
196
- dest='stop_mirroring',
197
- help='Stop mirroring mode, will write synced files alongside originals.')
198
- parser.add_argument('--start-project', '-s' ,
199
- nargs=2,
200
- dest='proj_folders',
201
- default = [],
202
- help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
126
+ # parser.add_argument('--stop_mirroring',
127
+ # action='store_true', #ie default False
128
+ # dest='stop_mirroring',
129
+ # help='Stop mirroring mode, will write synced files alongside originals.')
130
+ # parser.add_argument('--start-project', '-s' ,
131
+ # nargs=2,
132
+ # dest='proj_folders',
133
+ # default = [],
134
+ # help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
203
135
  parser.add_argument('-t','--timelineoffset',
204
136
  nargs=1,
205
137
  default=['00:00:00:00'],
@@ -236,39 +168,13 @@ def main():
236
168
  quit()
237
169
  if args.verbose_output:
238
170
  logger.add(sys.stderr, level="DEBUG")
239
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "move_multicam_to_dir")
240
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_merge_audio_and_video")
241
- #
242
- # logger.add(sys.stdout, filter="__main__")
243
- # logger.add(sys.stdout, filter="yaltc")
244
171
 
245
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "start_proj")
246
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_dev_type_for_name")
172
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "move_multicam_to_dir")
173
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_build_audio_and_write_video")
174
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
175
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_build_from_tracks_txt")
247
176
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
248
177
 
249
-
250
- if args.proj_folders != []:
251
- print('\bSorry, mirrored output mode not implemented yet: synced clips will')
252
- print('be written alongside originals in a SyncedMedia folder. Bye.')
253
- sys.exit(0)
254
-
255
- if not len(args.proj_folders) in [0, 2]:
256
- print('Error, -s option requires two folders')
257
- sys.exit(0)
258
- if len(args.proj_folders) == 2:
259
- source_RAW, destination_synced = start_proj(args.proj_folders)
260
- logger.debug('source_RAW: %s destination_synced: %s'%(source_RAW,
261
- destination_synced))
262
- proj_folders = get_proj()
263
- if proj_folders != ():
264
- # mirrored mode
265
- source_RAW, destination_synced = proj_folders
266
- logger.debug('Mirrored mode ON, with %s '%str(proj_folders))
267
- else:
268
- source_RAW, destination_synced = None, None
269
- logger.debug('Mirrored mode OFF, ')
270
- if source_RAW or destination_synced:
271
- print('Mirrored not implemented yet, ignoring.')
272
178
  top_dir = args.path[0]
273
179
  if os.path.isfile(top_dir):
274
180
  file = top_dir
@@ -276,15 +182,6 @@ def main():
276
182
  if not os.path.isdir(top_dir):
277
183
  print('%s is not a directory or doesnt exist.'%top_dir)
278
184
  sys.exit(1)
279
- # logger.debug('args.o %s'%args.o)
280
- # print('args.o %s'%args.o)
281
- # if args.anchor:
282
- # anchor_dir = args.anchor[0]
283
- # else:
284
- # anchor_dir = None
285
- # if args.anchor and not os.path.isdir(anchor_dir):
286
- # print('%s is not a directory or doesnt exist.'%anchor_dir)
287
- # sys.exit(1)
288
185
  multi2polywav.poly_all(top_dir)
289
186
  scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
290
187
  scanner.scan_media_and_build_devices_UID()
@@ -307,20 +204,7 @@ def main():
307
204
  maxch = len(devices)
308
205
  for i, d in enumerate(devices):
309
206
  print('\t%i - %s'%(i+1, d.name))
310
- while True:
311
- print('\nEnter your choice:', end='')
312
- choice = input()
313
- try:
314
- choice = int(choice)
315
- except:
316
- print('Please use numeric digits.')
317
- continue
318
- if choice not in list(range(1, maxch + 1)):
319
- print('Please enter a number in [1..%i]'%maxch)
320
- continue
321
- break
322
- ref_device = list(devices)[choice - 1]
323
- # ref_device = list(devices)[3 - 1]
207
+ ref_device = list(devices)[3 - 1]
324
208
  print('When only audio recordings are present, ISOs files will be cut and written.')
325
209
  if not args.terse:
326
210
  if scanner.input_structure == 'ordered':
@@ -357,7 +241,6 @@ def main():
357
241
  print('resulting in undefined results: quitting...')
358
242
  quit()
359
243
  print()
360
- # recordings, rec_with_TTC = process_files(scanner.found_media_files, args)
361
244
  recordings = [yaltc.Recording(m, do_plots=args.plotting) for m
362
245
  in scanner.found_media_files]
363
246
  recordings_with_time = [
@@ -365,11 +248,16 @@ def main():
365
248
  for rec in recordings
366
249
  if rec.get_start_time()
367
250
  ]
251
+ [r.load_track_info() for r in recordings_with_time if r.is_audio()]
252
+ for r in recordings:
253
+ # print(f'{r} device: #{id(r.device):x}')
254
+ logger.debug(f'{r} \nDevice instance id: #{id(r.device):x}')
255
+ logger.debug(f'device content: {r.device}')
368
256
  if audio_REC_only:
369
257
  for rec in recordings:
370
258
  # print(rec, rec.device == ref_device)
371
259
  if rec.device == ref_device:
372
- rec.is_reference = True
260
+ rec.is_audio_reference = True
373
261
  if not args.terse:
374
262
  table = Table(title="tictacsync results")
375
263
  table.add_column("Recording\n", justify="center", style='gold1')
@@ -406,13 +294,6 @@ def main():
406
294
  console.print(table)
407
295
  print()
408
296
  n_devices = scanner.get_devices_number()
409
- # OUT_struct_for_mcam = scanner.top_dir_has_multicam and \
410
- # scanner.input_structure != 'loose'
411
- # OUT_struct_for_mcam = args.multicam
412
- # if OUT_struct_for_mcam and scanner.input_structure == 'loose':
413
- # print("\nSorry, can't output multicam structure if input is not structured:")
414
- # print("each camera must have its own folder with its clips stored inside, quitting.")
415
- # sys.exit(0)
416
297
  if len(recordings_with_time) < 2:
417
298
  if not args.terse:
418
299
  print('\nNothing to sync, exiting.\n')
@@ -429,11 +310,20 @@ def main():
429
310
  asked_ISOs = False
430
311
  # output_dir = args.o
431
312
  # if args.verbose_output or args.terse: # verbose, so no progress bars
313
+ print('Merging...')
314
+ # for merger in matcher.mergers:
315
+ # merger.build_audio_and_write_merged_media(top_dir,
316
+ # args.dont_write_cam_folder,
317
+ # asked_ISOs,
318
+ # audio_REC_only)
432
319
  for merger in matcher.mergers:
433
- merger.build_audio_and_write_merged_media(top_dir,
434
- args.dont_write_cam_folder,
435
- asked_ISOs,
436
- audio_REC_only)
320
+ if audio_REC_only:
321
+ # rare
322
+ merger._build_and_write_audio(top_dir)
323
+ else:
324
+ # almost always syncing audio to video clips
325
+ merger._build_audio_and_write_video(top_dir,
326
+ args.dont_write_cam_folder, asked_ISOs)
437
327
  if not args.terse:
438
328
  print("\n")
439
329
  # find out where files were written
@@ -447,23 +337,24 @@ def main():
447
337
  nameAnd2Parents = Path('').joinpath(*final_p.parts[-2:])
448
338
  print(' became [gold1]%s[/gold1]'%nameAnd2Parents)
449
339
  # matcher._build_otio_tracks_for_cam()
450
- matcher.set_up_clusters() # multicam
451
- matcher.shrink_gaps_between_takes(args.timelineoffset)
452
- logger.debug('matcher.multicam_clips_clusters %s'%
453
- pformat(matcher.multicam_clips_clusters))
454
- # clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
455
- # really_clusters is True if one of them has len() > 1
456
- really_clusters = any([len(cl['vids']) > 1 for cl
457
- in matcher.multicam_clips_clusters])
458
- if really_clusters:
459
- if scanner.input_structure == 'loose':
460
- print('\nThere are synced multicam clips but without structured folders')
461
- print('they were not grouped together under the same folder.')
340
+ if not audio_REC_only:
341
+ matcher.set_up_clusters() # multicam
342
+ matcher.shrink_gaps_between_takes(args.timelineoffset)
343
+ logger.debug('matcher.multicam_clips_clusters %s'%
344
+ pformat(matcher.multicam_clips_clusters))
345
+ # clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
346
+ # really_clusters is True if one of them has len() > 1
347
+ really_clusters = any([len(cl['vids']) > 1 for cl
348
+ in matcher.multicam_clips_clusters])
349
+ if really_clusters:
350
+ if scanner.input_structure == 'loose':
351
+ print('\nThere are synced multicam clips but without structured folders')
352
+ print('they were not grouped together under the same folder.')
353
+ else:
354
+ matcher.move_multicam_to_dir()
462
355
  else:
463
- matcher.move_multicam_to_dir()
464
- else:
465
- logger.debug('not really a multicam cluster, nothing to move')
466
- sys.exit(0)
356
+ logger.debug('not really a multicam cluster, nothing to move')
357
+ sys.exit(0)
467
358
 
468
359
  if __name__ == '__main__':
469
360
  main()
@@ -0,0 +1,352 @@
1
+ import json, pathlib, itertools, os, re, ffmpeg
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
+
8
+ try:
9
+ from . import mamconf
10
+ except:
11
+ import mamconf
12
+
13
+ dev = 'Cockos Incorporated'
14
+ app ='REAPER'
15
+
16
+ REAPER_SCRIPT_LOCATION = pathlib.Path(platformdirs.user_data_dir(app, dev)) / 'Scripts' / 'Atomic'
17
+
18
+ REAPER_LUA_CODE = """reaper.Main_OnCommand(40577, 0) -- lock left/right move
19
+ reaper.Main_OnCommand(40569, 0) -- lock enabled
20
+ local function placeWavsBeginingAtTrack(clip, start_idx)
21
+ for i, file in ipairs(clip.files) do
22
+ local track_idx = start_idx + i - 1
23
+ local track = reaper.GetTrack(nil,track_idx-1)
24
+ reaper.SetOnlyTrackSelected(track)
25
+ local left_trim = clip.in_time - clip.start_time
26
+ local where = clip.timeline_pos - left_trim
27
+ reaper.SetEditCurPos(where, false, false)
28
+ reaper.InsertMedia(file, 0 )
29
+ local item_cnt = reaper.CountTrackMediaItems( track )
30
+ local item = reaper.GetTrackMediaItem( track, item_cnt-1 )
31
+ local take = reaper.GetTake(item, 0)
32
+ -- reaper.GetSetMediaItemTakeInfo_String(take, "P_NAME", clip.name, true)
33
+ local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
34
+ reaper.BR_SetItemEdges(item, clip.timeline_pos, clip.timeline_pos + clip.cut_duration)
35
+ reaper.SetMediaItemInfo_Value(item, "C_LOCK", 2)
36
+ end
37
+ end
38
+
39
+ --cut here--
40
+
41
+ sample of the clips nested table (this will be discarded)
42
+ each clip has an EDL info table plus a sequence of ISO files:
43
+
44
+ clips =
45
+ {
46
+ {
47
+ name="canon24fps01.MOV", start_time=7.25, in_time=21.125, cut_duration=6.875, timeline_pos=3600,
48
+ files=
49
+ {
50
+ "/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Alice_canon24fps01.wav",
51
+ "/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Bob_canon24fps01.wav"
52
+ }
53
+ },
54
+ {name="DSC_8063.MOV", start_time=0.0, in_time=5.0, cut_duration=20.25, timeline_pos=3606.875,
55
+ files={"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Alice_DSC_8063.wav",
56
+ "/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Bob_DSC_8063.wav"}},
57
+ {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",
58
+ "/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps02_SND/ISOfiles/Bob_canon24fps02.wav"}}
59
+ }
60
+
61
+ --cut here--
62
+ -- make room fro the tracks to come
63
+ amplitude_top = 0
64
+ amplitude_bottom = 0
65
+ for i_clip, cl in pairs(clips) do
66
+ if i_clip%2 ~= 1 then
67
+ amplitude_top = math.max(amplitude_top, #cl.files)
68
+ else
69
+ amplitude_bottom = math.max(amplitude_bottom, #cl.files)
70
+ end
71
+ end
72
+ for i = 1 , amplitude_top + amplitude_bottom + 1 do
73
+ reaper.InsertTrackAtIndex( -1, false ) -- at end
74
+ end
75
+ track_count = reaper.CountTracks(0)
76
+ -- ISOs will be up and down the base_track index
77
+ base_track = track_count - amplitude_bottom
78
+ for iclip, clip in ipairs(clips) do
79
+ start_track_number = base_track
80
+ -- alternating even/odd, odd=below base_track
81
+ if iclip%2 == 0 then -- above base_track, start higher
82
+ start_track_number = base_track - #clip.files
83
+ end
84
+ placeWavsBeginingAtTrack(clip, start_track_number)
85
+ if #clips > 1 then -- interclips editing
86
+ reaper.AddProjectMarker(0, false, clip.timeline_pos, 0, '', -1)
87
+ end
88
+ end
89
+ reaper.SetEditCurPos(3600, false, false)
90
+ reaper.Main_OnCommand(40151, 0)
91
+ if #clips > 1 then -- interclips editing
92
+ -- last marker at the end
93
+ last_clip = clips[#clips]
94
+ reaper.AddProjectMarker(0, false, last_clip.timeline_pos + last_clip.cut_duration, 0, '', -1)
95
+ end
96
+
97
+ """
98
+
99
+ logger.level("DEBUG", color="<yellow>")
100
+ logger.add(sys.stdout, level="DEBUG")
101
+ logger.remove()
102
+
103
+ def parse_and_check_arguments():
104
+ # parses directories from command arguments
105
+ # check for consistencies and warn user and exits,
106
+ # returns parser.parse_args()
107
+ descr = "Parse the submitted OTIO timeline and build a Reaper Script to load the corresponding ISO files from SNDROOT (see mamconf --show)"
108
+ parser = argparse.ArgumentParser(description=descr)
109
+ parser.add_argument(
110
+ "a_file_argument",
111
+ type=str,
112
+ nargs=1,
113
+ help="path of timeline saved under OTIO format"
114
+ )
115
+ parser.add_argument('--interval',
116
+ dest='interval',
117
+ nargs=2,
118
+ help="One or two timecodes, space seperated, delimiting the zone to process (if not specified the whole timeline is processed)")
119
+ args = parser.parse_args()
120
+ logger.debug('args %s'%args)
121
+ return args
122
+
123
+ @dataclass
124
+ class Clip:
125
+ # all time in seconds
126
+ start_time: float # the start time of the clip in
127
+ in_time: float # time of 'in' point, relative to clip start_time
128
+ cut_duration: float
129
+ whole_duration: float # unedited clip duration
130
+ name: str #
131
+ path: str # path of clip
132
+ timeline_pos: float # when on the timeline the clip starts
133
+ ISOdir: None # folder of ISO files for clip
134
+
135
+ def clip_info_from_json(jsoncl):
136
+ """
137
+ parse data from an OTIO json Clip
138
+ https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-serialized-schema.html#clip-2
139
+ returns a list composed of (all times are in seconds):
140
+ st, start time (from clip metadata TC)
141
+ In, the "in time"
142
+ cd, the cut duration
143
+ wl, the whole length of the unedited clip
144
+ the clip file path (string)
145
+ name (string)
146
+ NB: the position on the global timeline is not stored but latter computed from summing cut times
147
+ """
148
+ def _float_time(json_rationaltime):
149
+ return json_rationaltime['value']/json_rationaltime['rate']
150
+ av_range = jsoncl['media_references']['DEFAULT_MEDIA']['available_range']
151
+ src_rg = jsoncl['source_range']
152
+ st = av_range['start_time']
153
+ In = src_rg['start_time']
154
+ cd = src_rg['duration']
155
+ wl = av_range['duration']
156
+ path = jsoncl['media_references']['DEFAULT_MEDIA']['target_url']
157
+ name = jsoncl['media_references']['DEFAULT_MEDIA']['name']
158
+ return Clip(*[_float_time(t) for t in [st, In, cd, wl,]] + \
159
+ [name, path, 0, None])
160
+
161
+ def get_SND_dirs(snd_root):
162
+ # returns all directories found under snd_root
163
+ def _searchDirectory(cwd,searchResults):
164
+ dirs = os.listdir(cwd)
165
+ for dir in dirs:
166
+ fullpath = os.path.join(cwd,dir)
167
+ if os.path.isdir(fullpath):
168
+ searchResults.append(fullpath)
169
+ _searchDirectory(fullpath,searchResults)
170
+ searchResults = []
171
+ _searchDirectory(snd_root,searchResults)
172
+ return searchResults
173
+
174
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "find_and_set_ISO_dir")
175
+ def find_and_set_ISO_dir(clip, SND_dirs):
176
+ """
177
+ SND_dirs contains all the *_SND directories found in snd_root.
178
+ This fct finds out which one corresponds to the clip
179
+ and sets the found path to clip.ISOdir.
180
+ Returns nothing.
181
+ """
182
+ clip_stem = pathlib.Path(clip.path).stem
183
+ logger.debug(f'clip_stem {clip_stem}')
184
+ m = re.match('(.*)v([AB]*)', clip_stem)
185
+ logger.debug(f'{clip_stem} match (.*)v([AB]*) { m.groups() if m != None else None}')
186
+ if m != None:
187
+ clip_stem = m.groups()[0]
188
+ # /MyBigMovie/day01/leftCAM/card01/canon24fps01_SND -> canon24fps01_SND
189
+ names_only = [p.name for p in SND_dirs]
190
+ logger.debug(f'names-only {pformat(names_only)}')
191
+ clip_stem_SND = f'{clip_stem}_SND'
192
+ if clip_stem_SND in names_only:
193
+ where = names_only.index(clip_stem_SND)
194
+ else:
195
+ print(f'Error: OTIO file contains clip not in SYNCEDROOT: {clip_stem} (check with mamconf --show)')
196
+ sys.exit(0)
197
+ complete_path = SND_dirs[where]
198
+ logger.debug(f'found {complete_path}')
199
+ clip.ISOdir = str(complete_path)
200
+
201
+ def gen_lua_table(clips):
202
+ # returns a string defining a lua nested table
203
+ # top level: a sequence of clips
204
+ # a clip has keys: name, start_time, in_time, cut_duration, timeline_pos, files
205
+ # clip.files is a sequence of ISO wav files
206
+ def _list_ISO(dir):
207
+ iso_dir = pathlib.Path(dir)/'ISOfiles'
208
+ ISOs = [f for f in iso_dir.iterdir() if f.suffix.lower() == '.wav']
209
+ # ISOs = [f for f in ISOs if f.name[:2] != 'tc'] # no timecode
210
+ logger.debug(f'ISOs {ISOs}')
211
+ sequence = '{'
212
+ for file in ISOs:
213
+ sequence += f'"{file}",\n'
214
+ sequence += '}'
215
+ return sequence
216
+ lua_clips = '{'
217
+ for cl in clips:
218
+ ISOs = _list_ISO(cl.ISOdir)
219
+ # logger.debug(f'sequence {ISOs}')
220
+ 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}}}'
221
+ lua_clips += f'{clip_table},\n'
222
+ logger.debug(f'clip_table {clip_table}')
223
+ lua_clips += '}'
224
+ return lua_clips
225
+
226
+ def read_OTIO_file(f):
227
+ """
228
+ returns framerate and a list of Clip instances parsed from
229
+ the OTIO file passed as (string) argument f;
230
+ warns and exists if more than one video track.
231
+ """
232
+ with open(f) as fh:
233
+ oti = json.load(fh)
234
+ video_tracks = [tr for tr in oti['tracks']['children'] if tr['kind'] == 'Video']
235
+ if len(video_tracks) > 1:
236
+ print(f"Can only process timeline with one video track, this one has {len(video_tracks)}. Bye.")
237
+ sys.exit(0)
238
+ video_track = video_tracks[0]
239
+ clips = [clip_info_from_json(jscl) for jscl in video_track['children']]
240
+ logger.debug(f'clips: {pformat(clips)}')
241
+ # compute each clip global timeline position
242
+ clip_starts = [0] + list(itertools.accumulate([cl.cut_duration for cl in clips]))[:-1]
243
+ # Reaper can't handle negative item position (for the trimmed part)
244
+ # so starts at 1:00:00
245
+ clip_starts = [t + 3600 for t in clip_starts]
246
+ logger.debug(f'clip_starts: {clip_starts}')
247
+ for time, clip in zip(clip_starts, clips):
248
+ clip.timeline_pos = time
249
+ return int(oti['global_start_time']['rate']), clips
250
+
251
+ def reaper_save_action(wav_destination):
252
+ return f"""reaper.GetSetProjectInfo_String(0, "RENDER_FILE","{wav_destination.parent}",true)
253
+ reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN","{wav_destination.name}",true)
254
+ reaper.SNM_SetIntConfigVar("projintmix", 4)
255
+ reaper.Main_OnCommand(40015, 0)
256
+ """
257
+
258
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "complete_clip_path")
259
+ def complete_clip_path(clip_stem, synced_proj):
260
+ match = []
261
+ for (root,dirs,files) in os.walk(synced_proj):
262
+ for f in files:
263
+ p = pathlib.Path(root)/f
264
+ if p.is_symlink() or p.suffix == '.reapeaks':
265
+ continue
266
+ # logger.debug(f'{f}')
267
+ if clip_stem in f.split('.')[0]: # match XYZvA.mov
268
+ match.append(p)
269
+ logger.debug(f'matches {match}')
270
+ if len(match) > 1:
271
+ print(f'Warning, some filenames collide {pformat(match)}, Bye.')
272
+ sys.exit(0)
273
+ if len(match) == 0:
274
+ print(f"Error, didn't find any clip containing *{clip_stem}*. Bye.")
275
+ sys.exit(0)
276
+ return match[0]
277
+
278
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
279
+ def main():
280
+ def _where(a,x):
281
+ # find in which clip time x (in seconds) does fall.
282
+ n = 0
283
+ while n<len(a):
284
+ if a[n].timeline_pos > x:
285
+ break
286
+ else:
287
+ n += 1
288
+ return n-1
289
+ raw_root, synced_root, snd_root, proxies = mamconf.get_proj(False)
290
+ proj_name = pathlib.Path(raw_root).stem
291
+ synced_proj = pathlib.Path(synced_root)/proj_name
292
+ logger.debug(f'proj_name {proj_name}')
293
+ logger.debug(f'will search {snd_root} for ISOs')
294
+ all_SNDROOT_dirs = [pathlib.Path(f) for f in get_SND_dirs(snd_root)]
295
+ # keep only XYZ_SND dirs
296
+ SND_dirs = [p for p in all_SNDROOT_dirs if p.name[-4:] == '_SND']
297
+ logger.debug(f'SND_dirs {pformat(SND_dirs)}')
298
+ args = parse_and_check_arguments()
299
+ file_arg = pathlib.Path(args.a_file_argument[0])
300
+ # check if its intraclip or interclip sound edit
301
+ # if otio file then interclip
302
+ if file_arg.suffix == '.otio':
303
+ logger.debug('interclip sound edit, filling up clips')
304
+ _, clips = read_OTIO_file(file_arg)
305
+ [find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
306
+ else:
307
+ logger.debug('intraclip sound edit, clips will have one clip')
308
+ # traverse synced_root to find clip path
309
+ clip_path = complete_clip_path(file_arg.stem, synced_proj)
310
+ probe = ffmpeg.probe(clip_path)
311
+ duration = float(probe['format']['duration'])
312
+ clips = [Clip(
313
+ start_time=0,
314
+ in_time=0,
315
+ cut_duration=duration,
316
+ whole_duration=duration,
317
+ name=file_arg.stem,
318
+ path=clip_path,
319
+ timeline_pos=3600,
320
+ ISOdir='')]
321
+ [find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
322
+ print(f'For video clip \n{clip_path}\nfound audio in\n{clips[0].ISOdir}')
323
+ logger.debug(f'clips with found ISOdir: {pformat(clips)}')
324
+ lua_clips = gen_lua_table(clips)
325
+ logger.debug(f'lua_clips {lua_clips}')
326
+ # title = "Load cut26_MyBigMovie" or "Load clip026_MyBigMovie"
327
+ arg_name = pathlib.Path(args.a_file_argument[0]).stem
328
+ title = f'Load {arg_name}_{pathlib.Path(raw_root).stem}'
329
+ # script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'{title}.lua'
330
+ script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Load Clip Audio.lua'
331
+ # script += f'os.remove("{script_path}")\n' # doesnt work
332
+ Lua_script_pre, _ , Lua_script_post = REAPER_LUA_CODE.split('--cut here--')
333
+ script = Lua_script_pre + 'clips=' + lua_clips + Lua_script_post
334
+ with open(script_path, 'w') as fh:
335
+ fh.write(script)
336
+ print(f'Wrote script {script_path}')
337
+ if file_arg.suffix != 'otio':
338
+ # build "Set rendering for" action
339
+ destination = pathlib.Path(clips[0].ISOdir)/'mix.wav'
340
+ logger.debug(f'will build set rendering for {arg_name} with dest: {destination}')
341
+ render_action = reaper_save_action(destination)
342
+ logger.debug(f'clip\n{render_action}')
343
+ # script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Set rendering for {arg_name}.lua'
344
+ script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Render Clip Audio.lua'
345
+ with open(script_path, 'w') as fh:
346
+ fh.write(render_action)
347
+ print(f'Wrote script {script_path}')
348
+
349
+
350
+ if __name__ == '__main__':
351
+ main()
352
+