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/device_scanner.py +62 -268
- tictacsync/entry.py +57 -166
- tictacsync/load_fieldr_reaper.py +352 -0
- tictacsync/mamconf.py +157 -0
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +343 -0
- tictacsync/multi2polywav.py +4 -3
- tictacsync/new-sound-resolve.py +469 -0
- tictacsync/newmix.py +483 -0
- tictacsync/remrgmx.py +6 -10
- tictacsync/splitmix.py +87 -0
- tictacsync/timeline.py +154 -98
- tictacsync/yaltc.py +359 -31
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/METADATA +5 -6
- tictacsync-1.4.0b0.dist-info/RECORD +24 -0
- tictacsync-1.4.0b0.dist-info/entry_points.txt +7 -0
- tictacsync-0.98a0.dist-info/RECORD +0 -16
- tictacsync-0.98a0.dist-info/entry_points.txt +0 -4
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/LICENSE +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/WHEEL +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
parser.add_argument('--start-project', '-s' ,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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"] == "
|
|
246
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
+
|