tictacsync 0.96a0__py3-none-any.whl → 0.98a0__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/timeline.py CHANGED
@@ -9,8 +9,9 @@ from rich import print
9
9
  from itertools import groupby
10
10
  # import opentimelineio as otio
11
11
  from datetime import timedelta
12
- import pprint, shutil, os, sys
12
+ import shutil, os, sys, stat
13
13
  from subprocess import Popen, PIPE
14
+ from pprint import pformat
14
15
 
15
16
  from inspect import currentframe, getframeinfo
16
17
  try:
@@ -20,10 +21,26 @@ except:
20
21
  import yaltc
21
22
  import device_scanner
22
23
 
23
- CLUSTER_GAP = 0.5 # secs between multicam clusters
24
+ CLUSTER_GAP = 2 # secs between multicam clusters
24
25
  DEL_TEMP = False
25
26
  DB_OSX_NORM = -6 #dB
26
27
  OUT_DIR_DEFAULT = 'SyncedMedia'
28
+ MCCDIR = 'SyncedMulticamClips'
29
+
30
+ # utility to lock ISO audio files
31
+ def remove_write_permissions(path):
32
+ """Remove write permissions from this path, while keeping all other permissions intact.
33
+
34
+ Params:
35
+ path: The path whose permissions to alter.
36
+ """
37
+ NO_USER_WRITING = ~stat.S_IWUSR
38
+ NO_GROUP_WRITING = ~stat.S_IWGRP
39
+ NO_OTHER_WRITING = ~stat.S_IWOTH
40
+ NO_WRITING = NO_USER_WRITING & NO_GROUP_WRITING & NO_OTHER_WRITING
41
+
42
+ current_permissions = stat.S_IMODE(os.lstat(path).st_mode)
43
+ os.chmod(path, current_permissions & NO_WRITING)
27
44
 
28
45
  # utility for accessing pathnames
29
46
  def _pathname(tempfile_or_path) -> str:
@@ -64,6 +81,12 @@ def _extr_channel(source, dest, channel):
64
81
  status = sox_transform.build(str(source), str(dest))
65
82
  logger.debug('sox status %s'%status)
66
83
 
84
+ def _same(aList):
85
+ return aList.count(aList[0]) == len(aList)
86
+
87
+ def _flatten(xss):
88
+ return [x for xs in xss for x in xs]
89
+
67
90
  def _sox_keep(audio_file, kept_channels: list) -> tempfile.NamedTemporaryFile:
68
91
  """
69
92
  Returns a NamedTemporaryFile containing the selected kept_channels
@@ -102,7 +125,7 @@ def _sox_keep(audio_file, kept_channels: list) -> tempfile.NamedTemporaryFile:
102
125
  stderr).decode('utf-8'))
103
126
  return output_tempfile
104
127
 
105
- def _split_channels(multi_chan_audio:Path) -> list:
128
+ def _sox_split_channels(multi_chan_audio:Path) -> list:
106
129
  nchan = sox.file_info.channels(_pathname(multi_chan_audio))
107
130
  source = _pathname(multi_chan_audio)
108
131
  paths = []
@@ -383,14 +406,14 @@ class AudioStitcherVideoMerger:
383
406
  def _get_audio_devices(self):
384
407
  devices = set([r.device for r in self.get_matched_audio_recs()])
385
408
  logger.debug('get_matched_audio_recs: %s'%
386
- pprint.pformat(self.get_matched_audio_recs()))
409
+ pformat(self.get_matched_audio_recs()))
387
410
  logger.debug('devices %s'%devices)
388
411
  return devices
389
412
 
390
413
  def _get_secondary_audio_devices(self):
391
414
  devices = set([r.device for r in self.get_matched_audio_recs()])
392
415
  logger.debug('get_matched_audio_recs: %s'%
393
- pprint.pformat(self.get_matched_audio_recs()))
416
+ pformat(self.get_matched_audio_recs()))
394
417
  logger.debug('devices %s'%devices)
395
418
  return devices
396
419
 
@@ -641,7 +664,7 @@ class AudioStitcherVideoMerger:
641
664
  Returns nothing, output is written to filesystem as below.
642
665
  ISOs subfolders structure when user invokes the --isos flag:
643
666
 
644
- SyncedMedia/ (or output_dir)
667
+ SyncedMedia/ (or anchor_dir)
645
668
 
646
669
  leftCAM/
647
670
 
@@ -689,24 +712,16 @@ class AudioStitcherVideoMerger:
689
712
  synced_clip_dir = synced_clip_file.parent
690
713
  # build ISOs subfolders structure, see comment string below
691
714
  video_stem_WO_suffix = synced_clip_file.stem
692
- # video_stem_WO_suffix = synced_clip_file.stem.split('.')[0]
693
- # OUT_DIR_DEFAULT, D2 = ISOsDIR.split('/')
694
715
  ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_ISO')
695
716
  os.makedirs(ISOdir, exist_ok=True)
696
717
  logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
697
718
  logger.debug('ISOdir %s'%ISOdir)
698
- # ISO_multi_chan = ISOdir / 'ISO_multi_chan.wav'
699
- # logger.debug('temp file: %s'%(ISO_multi_chan))
700
- # logger.debug('will split audio to %s'%(ISOdir))
701
719
  for name, mono_tmpfl in edited_audio_all_devices:
702
- # pad(start_duration: float = 0.0, end_duration: float = 0.0)[source]
703
720
  destination = ISOdir/('%s.wav'%name)
704
721
  mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
705
722
  shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
723
+ remove_write_permissions(destination)
706
724
  logger.debug('destination:%s'%destination)
707
- # # mixNnormed = _sox_mix_files(tempfiles)
708
- # # print('516', _pathname(mixNnormed))
709
- # os.remove(ISO_multi_chan)
710
725
 
711
726
  def _get_device_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
712
727
  """
@@ -789,7 +804,7 @@ class AudioStitcherVideoMerger:
789
804
  # all, but here remove '0' and TTC tracks from mix
790
805
  all_channels = list(range(1, device.n_chan + 1)) # sox not ZBIDX
791
806
  to_remove = device.tracks.unused + [device.ttc+1]# unused is sox idx
792
- logger.debug('multitrack but no tracks.txt, mixing %s except # %s (sox #)'%
807
+ logger.debug('multitrack but no mix, mixing mono %s except # %s (sox #)'%
793
808
  (all_channels, to_remove))
794
809
  sox_kept_channels = [i for i in all_channels
795
810
  if i not in to_remove]
@@ -851,26 +866,25 @@ class AudioStitcherVideoMerger:
851
866
  stereo_files = mic_stereo_files + new_stereo_files
852
867
  return _sox_mix_files(stereo_files)
853
868
 
854
- def build_audio_and_write_merged_media(self, top_dir, output_dir,
855
- write_multicam_structure,
856
- asked_ISOs, audio_REC_only):
869
+ def build_audio_and_write_merged_media(self, top_dir,
870
+ dont_write_cam_folder, asked_ISOs, audio_REC_only):
857
871
  # simply bifurcates depending if ref media is video (prob 99%)
858
872
  # (then audio_REC_only == False)
859
- # or if ref media is audio (no camera detected)
873
+ # or if ref media is audio (no camera detected, 1% of cases)
860
874
  # (with audio_REC_only == True)
861
875
  if not audio_REC_only:
862
- self._build_audio_and_write_video(top_dir, output_dir,
863
- write_multicam_structure, asked_ISOs)
876
+ self._build_audio_and_write_video(top_dir,
877
+ dont_write_cam_folder, asked_ISOs)
864
878
  else:
865
- self._build_and_write_audio(top_dir, output_dir)
879
+ self._build_and_write_audio(top_dir, anchor_dir)
866
880
 
867
- def _build_and_write_audio(self, top_dir, output_dir):
881
+ def _build_and_write_audio(self, top_dir, anchor_dir):
868
882
  """
869
883
  This is called when only audio recorders were found (no cam).
870
884
 
871
885
  top_dir: Path, directory where media were looked for
872
886
 
873
- output_dir: str for optional folder specified as CLI argument, if
887
+ anchor_dir: str for optional folder specified as CLI argument, if
874
888
  value is None, fall back to OUT_DIR_DEFAULT
875
889
 
876
890
  For each audio devices found overlapping self.ref_audio: pad, trim
@@ -889,16 +903,16 @@ class AudioStitcherVideoMerger:
889
903
  self.ref_audio.device.name))
890
904
  # eg, suppose the user called tictacsync with 'mondayPM' as top folder
891
905
  # to scan for dailies (and 'somefolder' for output):
892
- if output_dir == None:
906
+ if anchor_dir == None:
893
907
  synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
894
908
  else:
895
- synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
909
+ synced_clip_dir = Path(anchor_dir)/Path(top_dir).name # = somefolder/mondayPM
896
910
  self.synced_clip_dir = synced_clip_dir
897
911
  os.makedirs(synced_clip_dir, exist_ok=True)
898
912
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
899
913
  synced_clip_file = synced_clip_dir/\
900
914
  Path(self.videoclip.new_rec_name).name
901
- logger.debug('editing files for %s'%synced_clip_file)
915
+ logger.debug('editing files for synced_clip_file%s'%synced_clip_file)
902
916
  self.ref_audio.final_synced_file = synced_clip_file # relative path
903
917
  # Collecting edited audio by device, in (Device, tempfile) pairs:
904
918
  # for a given self.ref_audio, each other audio device will have a sequence
@@ -919,15 +933,15 @@ class AudioStitcherVideoMerger:
919
933
  # no audio file overlaps for this clip
920
934
  return #############################################################
921
935
  logger.debug('will output ISO files since no cam')
922
- devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
936
+ devices_and_monofiles = [(device, _sox_split_channels(multi_chan_audio))
923
937
  for device, multi_chan_audio
924
938
  in merged_audio_files_by_device]
925
939
  # add device and file from self.ref_audio
926
940
  new_tuple = (self.ref_audio.device,
927
- _split_channels(self.ref_audio.AVpath))
941
+ _sox_split_channels(self.ref_audio.AVpath))
928
942
  devices_and_monofiles.append(new_tuple)
929
943
  logger.debug('devices_and_monofiles: %s'%
930
- pprint.pformat(devices_and_monofiles))
944
+ pformat(devices_and_monofiles))
931
945
  def _trnm(dev, idx): # used in the loop just below
932
946
  # generates track name for later if asked_ISOs
933
947
  # idx is from 0 to nchan-1 for this device
@@ -963,17 +977,15 @@ class AudioStitcherVideoMerger:
963
977
  logger.debug('merged_audio_files_by_device %s'%
964
978
  merged_audio_files_by_device)
965
979
 
966
-
967
- def _build_audio_and_write_video(self, top_dir, output_dir,
968
- write_multicam_structure,
969
- asked_ISOs):
980
+ def _build_audio_and_write_video(self, top_dir, dont_write_cam_folder,
981
+ asked_ISOs):
970
982
  """
971
983
  top_dir: Path, directory where media were looked for
972
984
 
973
- output_dir: str for optional folder specified as CLI argument, if
985
+ anchor_dir: str for optional folder specified as CLI argument, if
974
986
  value is None, fall back to OUT_DIR_DEFAULT
975
987
 
976
- write_multicam_structure: True if needs to write multicam folders
988
+ dont_write_cam_folder: True if needs to bypass writing multicam folders
977
989
 
978
990
  asked_ISOs: bool flag specified as CLI argument
979
991
 
@@ -987,25 +999,28 @@ class AudioStitcherVideoMerger:
987
999
 
988
1000
  Sets AudioStitcherVideoMerger.final_synced_file on completion
989
1001
  """
990
- logger.debug(' fct args: top_dir: %s; output_dir: %s; write_multicam_structure: %s; asked_ISOs: %s'%
991
- (top_dir, output_dir, write_multicam_structure, asked_ISOs))
1002
+ logger.debug(' fct args: top_dir %s, dont_write_cam_folder %s, asked_ISOs %s'%
1003
+ (top_dir, dont_write_cam_folder, asked_ISOs))
992
1004
  logger.debug('device for rec %s: %s'%(self.videoclip,
993
1005
  self.videoclip.device))
994
1006
  # eg, suppose the user called tictacsync with 'mondayPM' as top folder
995
1007
  # to scan for dailies (and 'somefolder' for output):
996
- if output_dir == None:
997
- synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
998
- else:
999
- synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
1000
- if write_multicam_structure:
1001
- device_name = self.videoclip.device.name
1002
- synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
1008
+ # if anchor_dir == None:
1009
+ # then "alongside mode" i.e., in a folder neighboring the video clip
1010
+ # WAS: synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
1011
+ logger.debug('"alongside mode" for clip: %s'%self.videoclip.AVpath)
1012
+ synced_clip_dir = self.videoclip.AVpath.parent/OUT_DIR_DEFAULT
1013
+ # else: # [TODO: replace original clip in a mirrored file structure]
1014
+ # logger.debug('mirrored mode for clip: %s'%self.videoclip.AVpath)
1015
+ # synced_clip_dir = Path(anchor_dir)/Path(top_dir).name # = somefolder/mondayPM
1016
+ # if write_multicam_structure:
1017
+ # device_name = self.videoclip.device.name
1018
+ # synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
1003
1019
  self.synced_clip_dir = synced_clip_dir
1004
1020
  os.makedirs(synced_clip_dir, exist_ok=True)
1005
1021
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
1006
- synced_clip_file = synced_clip_dir/\
1007
- Path(self.videoclip.new_rec_name).name
1008
- logger.debug('editing files for %s'%synced_clip_file)
1022
+ synced_clip_file = synced_clip_dir/self.videoclip.AVpath.name
1023
+ logger.debug('editing files for synced_clip_file %s'%synced_clip_file)
1009
1024
  self.videoclip.final_synced_file = synced_clip_file # relative path
1010
1025
  # Collecting edited audio by device, in (Device, tempfiles) pairs:
1011
1026
  # for a given self.videoclip, each audio device will have a sequence
@@ -1069,7 +1084,7 @@ class AudioStitcherVideoMerger:
1069
1084
  mix_of_device_mixes = _sox_mix_files(device_mixes)
1070
1085
  logger.debug('will merge with %s'%(_pathname(mix_of_device_mixes)))
1071
1086
  self.videoclip.synced_audio = mix_of_device_mixes
1072
- logger.debug('mix_of_device_mixes n chan: %i'%
1087
+ logger.debug('mix_of_device_mixes (final mix) has %i channels'%
1073
1088
  sox.file_info.channels(_pathname(mix_of_device_mixes)))
1074
1089
  self._merge_audio_and_video()
1075
1090
  # devices_and_monofiles is list of (device, [monofiles])
@@ -1079,18 +1094,20 @@ class AudioStitcherVideoMerger:
1079
1094
  # devices_and_monofiles:
1080
1095
  if asked_ISOs:
1081
1096
  logger.debug('will output ISO files...')
1082
- devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
1097
+ devices_and_monofiles = [(device, _sox_split_channels(multi_chan_audio))
1083
1098
  for device, multi_chan_audio
1084
1099
  in merged_audio_files_by_device]
1085
1100
  logger.debug('devices_and_monofiles: %s'%
1086
- pprint.pformat(devices_and_monofiles))
1087
- def _trnm(dev, idx): # used in the loop just below
1101
+ pformat(devices_and_monofiles))
1102
+ def _build_from_tracks_txt(dev, idx):
1103
+ # used in the loop just below
1088
1104
  # generates track name for later if asked_ISOs
1089
1105
  # idx is from 0 to nchan-1 for this device
1090
1106
  if dev.tracks == None:
1107
+ # no tracks.txt was found so use ascending numbers for name
1091
1108
  chan_name = 'chan%s'%str(idx+1).zfill(2)
1092
1109
  else:
1093
- # sanitize
1110
+ # sanitize names in tracks.txt
1094
1111
  symbols = set(r"""`~!@#$%^&*()_-+={[}}|\:;"'<,>.?/""")
1095
1112
  chan_name = dev.tracks.rawtrx[idx]
1096
1113
  logger.debug('raw chan_name %s'%chan_name)
@@ -1110,12 +1127,12 @@ class AudioStitcherVideoMerger:
1110
1127
  names_audio_tempfiles = []
1111
1128
  for dev, mono_tmpfiles_list in devices_and_monofiles:
1112
1129
  for idx, monotf in enumerate(mono_tmpfiles_list):
1113
- track_name = _trnm(dev, idx)
1130
+ track_name = _build_from_tracks_txt(dev, idx)
1114
1131
  logger.debug('track_name %s'%track_name)
1115
1132
  if track_name[0] == '0': # muted, skip
1116
1133
  continue
1117
1134
  names_audio_tempfiles.append((track_name, monotf))
1118
- logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
1135
+ logger.debug('names_audio_tempfiles %s'%pformat(names_audio_tempfiles))
1119
1136
  self._write_ISOs(names_audio_tempfiles)
1120
1137
  logger.debug('merged_audio_files_by_device %s'%
1121
1138
  merged_audio_files_by_device)
@@ -1233,6 +1250,10 @@ class Matcher:
1233
1250
  AudioStitcherVideoMerger object. An audio_stitch doesn't extend
1234
1251
  beyond the corresponding video start and end times.
1235
1252
 
1253
+ multicam_clips_clusters : list
1254
+ of {'end': t1, 'start': t2, 'vids': [r1,r3]} where r1 and r3
1255
+ are overlapping.
1256
+
1236
1257
  """
1237
1258
 
1238
1259
  def __init__(self, recordings_list):
@@ -1246,20 +1267,20 @@ class Matcher:
1246
1267
  self.recordings = recordings_list
1247
1268
  self.mergers = []
1248
1269
 
1249
- def _rename_all_recs(self):
1250
- """
1251
- Add _synced to filenames of synced video files. Change stored name only:
1252
- files have yet to be written to.
1253
- """
1254
- # match IO_structure:
1255
- # case 'foldercam':
1256
- for rec in self.recordings:
1257
- rec_extension = rec.AVpath.suffix
1258
- rel_path_new_name = '%s%s'%(rec.AVpath.stem, rec_extension)
1259
- rec.new_rec_name = Path(rel_path_new_name)
1260
- logger.debug('for %s new name: %s'%(
1261
- _pathname(rec.AVpath),
1262
- _pathname(rec.new_rec_name)))
1270
+ # def _rename_all_recs(self):
1271
+ # """
1272
+ # Add _synced to filenames of synced video files. Change stored name only:
1273
+ # files have yet to be written to.
1274
+ # """
1275
+ # # match IO_structure:
1276
+ # # case 'foldercam':
1277
+ # for rec in self.recordings:
1278
+ # rec_extension = rec.AVpath.suffix
1279
+ # rel_path_new_name = '%s%s'%(rec.AVpath.stem, rec_extension)
1280
+ # rec.new_rec_name = Path(rel_path_new_name)
1281
+ # logger.debug('for %s new name: %s'%(
1282
+ # _pathname(rec.AVpath),
1283
+ # _pathname(rec.new_rec_name)))
1263
1284
 
1264
1285
  def scan_audio_for_each_videoclip(self):
1265
1286
  """
@@ -1295,7 +1316,7 @@ class Matcher:
1295
1316
  self.mergers.append(audio_stitch)
1296
1317
  else:
1297
1318
  logger.debug('\n nothing\n')
1298
- print('No overlap found for %s'%videoclip.AVpath.name)
1319
+ print('No overlap found for [gold1]%s[/gold1]'%videoclip.AVpath.name)
1299
1320
  del audio_stitch
1300
1321
  logger.debug('%i mergers created'%len(self.mergers))
1301
1322
 
@@ -1310,49 +1331,29 @@ class Matcher:
1310
1331
  case4 = R1 < A2 < R2
1311
1332
  return case1 or case2 or case3 or case4
1312
1333
 
1313
- def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
1314
- """
1315
- for single cam shootings this simply sets the gap between takes,
1316
- tweaking each vid timecode metadata to distribute them next to each
1317
- other along NLE timeline.
1318
-
1319
- Moves clusters at the timelineoffset
1320
-
1321
- For multicam takes, shifts are computed so
1322
- video clusters are near but dont overlap, ex:
1323
-
1324
- Cluster 1 Cluster 2
1325
- 1111111111111 2222222222 (cam A)
1326
- 11111111111[inserted gap]222222222 (cam B)
1327
-
1328
- or
1329
- 1111111111111[inserted 222222 (cam A)
1330
- 1111111 gap]222222222 (cam B)
1331
-
1332
- argument:
1333
- CLI_offset (str), option from command-line
1334
- with_gap (float), the gap duration in seconds
1335
-
1336
- Returns nothing, changes are done in the video files metadata
1337
- (each referenced by Recording.final_synced_file)
1338
- """
1334
+ def set_up_clusters(self):
1335
+ # builds the list self.multicam_clips_clusters. A list
1336
+ # of {'end': t1, 'start': t2, 'vids': [r1,r3]} where r1 and r3
1337
+ # are overlapping.
1338
+ # if no overlap occurs, length of vid = 1, ex 'vids': [r1]
1339
+ # so not really a cluster in those cases
1340
+ # returns nothing and sets Matcher.multicam_clips_clusters
1339
1341
  vids = [m.videoclip for m in self.mergers]
1340
- logger.debug('vids %s'%vids)
1341
- if len(vids) == 1:
1342
- logger.debug('just one take, no gap to shrink')
1343
- return #############################################################
1344
1342
  # INs_and_OUTs contains (time, direction, video) for each video,
1345
1343
  # where direction is 'in|out' and video an instance of Recording
1346
1344
  INs_and_OUTs = [(vid.get_start_time(), 'in', vid) for vid in vids]
1347
1345
  for vid in vids:
1348
1346
  INs_and_OUTs.append((vid.get_end_time(), 'out', vid))
1349
1347
  INs_and_OUTs = sorted(INs_and_OUTs, key=lambda vtuple: vtuple[0])
1350
- logger.debug('INs_and_OUTs: %s'%INs_and_OUTs)
1348
+ logger.debug('INs_and_OUTs: %s'%pformat(INs_and_OUTs))
1351
1349
  new_cluster = True
1352
1350
  current_cluster = {'vids':[]}
1353
1351
  N_in, N_out = (0, 0)
1354
1352
  # clusters is a list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
1355
1353
  clusters = []
1354
+ # a cluster begins (and grows) when a time of type 'in' is encountered
1355
+ # a cluster degrows when a time of type 'out' is encountered and
1356
+ # closes when its size (N_currently_open) reach back to zero
1356
1357
  for t, direction, video in INs_and_OUTs:
1357
1358
  if new_cluster and direction == 'out':
1358
1359
  logger.error('cant begin a cluster with a out time %s'%video)
@@ -1373,7 +1374,44 @@ Moves clusters at the timelineoffset
1373
1374
  new_cluster = True
1374
1375
  current_cluster = {'vids':[]}
1375
1376
  N_in, N_out = (0, 0)
1376
- logger.debug('clusters: %s'%pprint.pformat(clusters))
1377
+ logger.debug('clusters: %s'%pformat(clusters))
1378
+ self.multicam_clips_clusters = clusters
1379
+ return
1380
+
1381
+ def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
1382
+ """
1383
+ for single cam shootings this simply sets the gap between takes,
1384
+ tweaking each vid timecode metadata to distribute them next to each
1385
+ other along NLE timeline.
1386
+
1387
+ Moves clusters at the timelineoffset
1388
+
1389
+ For multicam takes, shifts are computed so
1390
+ video clusters are near but dont overlap, ex:
1391
+
1392
+ ***** are inserted gaps
1393
+
1394
+ Cluster 1 Cluster 2
1395
+ 1111111111111 2222222222 (cam A)
1396
+ 11111111111******222222222 (cam B)
1397
+
1398
+ or
1399
+ 11111111111111 222222 (cam A)
1400
+ 1111111 ******222222222 (cam B)
1401
+
1402
+ argument:
1403
+ CLI_offset (str), option from command-line
1404
+ with_gap (float), the gap duration in seconds
1405
+
1406
+ Returns nothing, changes are done in the video files metadata
1407
+ (each referenced by Recording.final_synced_file)
1408
+ """
1409
+ vids = [m.videoclip for m in self.mergers]
1410
+ logger.debug('vids %s'%vids)
1411
+ if len(vids) == 1:
1412
+ logger.debug('just one take, no gap to shrink')
1413
+ return #############################################################
1414
+ clusters = self.multicam_clips_clusters
1377
1415
  # if there are N clusters, there are N-1 gaps to evaluate and shorten
1378
1416
  # (lengthen?) to a value of with_gap seconds
1379
1417
  gaps = [c2['start'] - c1['end'] for c1, c2
@@ -1414,7 +1452,61 @@ Moves clusters at the timelineoffset
1414
1452
  vid.write_file_timecode(tc)
1415
1453
  return
1416
1454
 
1455
+ def move_multicam_to_dir(self):
1456
+ # creates a dedicated multicam directory and move clusters there
1457
+ # e.g., for "top/day01/camA/roll02"
1458
+ # ^ at that level
1459
+ # 0 1 2 3
1460
+ # Note: ROLLxx maybe present or not.
1461
+ #
1462
+ # check for consistency: are all clips at the same level from topdir?
1463
+ # Only for video, not audio (which doesnt fill up cards)
1464
+ video_medias = [m for m in self.recordings if m.device.dev_type == 'CAM']
1465
+ video_paths = [m.AVpath.parts for m in video_medias]
1466
+ AV_path_lengths = [len(p) for p in video_paths]
1467
+ # print('AV_path_lengths', AV_path_lengths)
1468
+ if not _same(AV_path_lengths):
1469
+ print('\nError with some clips, check if their locations are consistent (all at the same level in folders).')
1470
+ print('Video synced but could not regroup multicam clips.')
1471
+ sys.exit(0)
1472
+ # now find at which level CAMs reside (maybe there are ROLLxx)
1473
+ CAM_levels = [vm.AVpath.parts.index(vm.device.name)
1474
+ for vm in video_medias]
1475
+ # find for all, should be same
1476
+ if not _same(CAM_levels):
1477
+ print('\nError with some clips, check if their locations are consistent (all at the same level in folders).')
1478
+ print('Video synced but could not regroup multicam clips.')
1479
+ sys.exit(0)
1480
+ # pick first
1481
+ CAM_level, avp = CAM_levels[0], video_medias[0].AVpath
1482
+ logger.debug('CAM_levels: %s for ex \n%s'%(CAM_level, avp))
1483
+ # MCCDIR = 'SyncedMulticamClips'
1484
+ parts_up_a_level = avp.parts[:CAM_level]
1485
+ multicam_dir = Path('').joinpath(*parts_up_a_level)/MCCDIR
1486
+ logger.debug('multicam_dir: %s'%multicam_dir)
1487
+ Path.mkdir(multicam_dir, exist_ok=True)
1488
+ cam_clips = []
1489
+ [cam_clips.append(cl['vids']) for cl in self.multicam_clips_clusters]
1490
+ cam_clips = _flatten(cam_clips)
1491
+ logger.debug('cam_clips: %s'%cam_clips)
1492
+ cam_names = set([r.device.name for r in cam_clips])
1493
+ # create new dirs for each CAM
1494
+ [Path.mkdir(multicam_dir/cam_name, exist_ok=True)
1495
+ for cam_name in cam_names]
1496
+ # move clips there
1497
+ for r in cam_clips:
1498
+ cam = r.device.name
1499
+ clip_name = r.AVpath.name
1500
+ dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
1501
+ logger.debug('dest: %s'%dest)
1502
+ origin_folder = r.final_synced_file.parent
1503
+ folder_now_empty = len(list(origin_folder.glob('*.*'))) == 0
1504
+ if folder_now_empty:
1505
+ logger.debug('after moving %s, folder is now empty, removing it'%dest)
1506
+ origin_folder.rmdir()
1507
+ print('\nMoved %i multicam clips in %s'%(len(cam_clips), multicam_dir))
1417
1508
 
1509
+
1418
1510
 
1419
1511
 
1420
1512
 
tictacsync/yaltc.py CHANGED
@@ -298,7 +298,7 @@ class Decoder:
298
298
  'detection level, signal, detected region'.split(','),
299
299
  loc='lower right')
300
300
  ax.set_title('Finding word + sync pulse')
301
- plt.xlabel("Position in file (samples)")
301
+ plt.xlabel("Position in file %s (samples)"%self.rec)
302
302
  plt.show()
303
303
 
304
304
  def get_time_in_sound_extract(self):
@@ -347,6 +347,14 @@ class Decoder:
347
347
  sync_pos_in_file = self.detected_pulse_position + \
348
348
  self.sound_extract_position
349
349
  time_values['pulse at'] = sync_pos_in_file
350
+ if not 0 <= time_values['seconds'] <= 59:
351
+ return None
352
+ if not 0 <= time_values['minutes'] <= 59:
353
+ return None
354
+ if not 0 <= time_values['hours'] <= 23:
355
+ return None
356
+ if not 1 <= time_values['month'] <= 12:
357
+ return None
350
358
  return time_values
351
359
 
352
360
  def _detect_sync_pulse_position(self):
@@ -468,11 +476,11 @@ class Decoder:
468
476
  logger.debug('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
469
477
  1e3*TEENSY_MAX_LAG))
470
478
  logger.debug('relative audio_block gap %.2f'%(relative_gap))
471
- if relative_gap > 1:
472
- print('Warning: gap between spike and word is too big for %s'%self.rec)
473
- print('Audio update() gap between sync pulse and word start: ')
474
- print('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
475
- 1e3*TEENSY_MAX_LAG))
479
+ # if relative_gap > 1: # dont tell: simply fail and try elsewhere
480
+ # print('Warning: gap between spike and word is too big for %s'%self.rec)
481
+ # print('Audio update() gap between sync pulse and word start: ')
482
+ # print('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
483
+ # 1e3*TEENSY_MAX_LAG))
476
484
  symbol_width_samples_theor = self.samplerate*SYMBOL_LENGTH*1e-3
477
485
  symbol_width_samples_eff = self.effective_word_duration * \
478
486
  self.samplerate/(N_SYMBOLS - 1)
@@ -498,7 +506,7 @@ class Decoder:
498
506
  transform=xt,
499
507
  linewidth=0.6, colors='green')
500
508
  ax.set_title('Slicing the 34 bits word:')
501
- plt.xlabel("Position in file (samples)")
509
+ plt.xlabel("Position in file %s (samples)"%self.rec)
502
510
  ax.vlines(start, 0, 1,
503
511
  transform=xt,
504
512
  linewidth=0.6, colors='red')
@@ -532,10 +540,6 @@ class Recording:
532
540
  device : Device
533
541
  identifies the device used for the recording, set in __init__()
534
542
 
535
- new_rec_name : str
536
- built using the device name, ex: "CAM_A001"
537
- set by Timeline._rename_all_recs()
538
-
539
543
  probe : dict
540
544
  returned value of ffmpeg.probe(self.AVpath)
541
545
 
@@ -648,7 +652,7 @@ class Recording:
648
652
  # self.valid_sound = None
649
653
  self.final_synced_file = None
650
654
  self.synced_audio = None
651
- self.new_rec_name = media.path.name
655
+ # self.new_rec_name = media.path.name
652
656
  self.do_plots = do_plots
653
657
  logger.debug('__init__ Recording object %s'%self.__repr__())
654
658
  logger.debug(' in directory %s'%self.AVpath.parent)
@@ -718,7 +722,7 @@ class Recording:
718
722
 
719
723
  def __repr__(self):
720
724
  # return 'Recording of %s'%_pathname(self.new_rec_name)
721
- return _pathname(self.new_rec_name)
725
+ return _pathname(self.AVpath)
722
726
 
723
727
  def _check_for_camera_error_correction(self):
724
728
  # look for a file number
@@ -1220,13 +1224,6 @@ class Recording:
1220
1224
  )
1221
1225
  return self
1222
1226
 
1223
- def seems_to_have_TicTacCode_at_beginning(self):
1224
- if self.probe is None:
1225
- return False #######################################################
1226
- self._find_TicTacCode(TRIAL_TIMES[0][0],
1227
- SOUND_EXTRACT_LENGTH)
1228
- return self.TicTacCode_channel is not None
1229
-
1230
1227
  def does_overlap_with_time(self, time):
1231
1228
  A1, A2 = self.get_start_time(), self.get_end_time()
1232
1229
  # R1, R2 = rec.get_start_time(), rec.get_end_time()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.96a0
3
+ Version: 0.98a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -29,6 +29,7 @@ Requires-Dist: rich >=10.12.0
29
29
  Requires-Dist: lmfit
30
30
  Requires-Dist: scikit-image
31
31
  Requires-Dist: scipy >=1.10.1
32
+ Requires-Dist: platformdirs
32
33
 
33
34
  # tictacsync
34
35
 
@@ -67,7 +68,7 @@ Then pip install the syncing program:
67
68
  This should install python dependencies _and_ the `tictacsync` command.
68
69
  ## Usage
69
70
 
70
- Download multiple sample files [here](https://nuage.lutz.quebec/s/nY54nQyxmZbTLti/download/dailies.zip) (625 MB, sorry) unzip and run:
71
+ Download multiple sample files [here](https://nuage.lutz.quebec/s/NpjzXH5R7DrQEWS/download/dailies.zip) (625 MB, sorry) unzip and run:
71
72
 
72
73
  > tictacsync dailies/loose
73
74
  The program `tictacsync` will recursively scan the directory given as argument, find all audio that coincide with any video and merge them into a subfolder named `SyncedMedia`. When the argument is an unique media file (not a directory), no syncing will occur but the decoded starting time will be printed to stdout:
@@ -82,19 +83,22 @@ The program `tictacsync` will recursively scan the directory given as argument,
82
83
  If shooting multicam, put clips in their respective directories (using the camera name as folder name) _and_ the audio under their own directory. `tictacsync` will detect that structured input and will generate multicam folders ready to be imported into your NLE (for now only DaVinci Resolve has been validated).
83
84
 
84
85
  ## Options
86
+ #### `-v`
85
87
 
86
88
  For a very verbose output add the `-v` flag:
87
89
 
88
90
  > tictacsync -v dailies/loose/MVI_0024.MP4
89
-
91
+ #### `--terse`
90
92
  For a one line output (or to suppress the progress bars) use the `--terse` flag:
91
93
 
92
94
  > tictacsync --terse dailies/loose/MVI_0024.MP4
93
95
  dailies/loose/MVI_0024.MP4 UTC:2024-03-12 23:07:01.4281 pulse: 27450 in chan 0
96
+ #### `--isos`
94
97
 
95
- To also produce _synced_ ISO audio files, specify `--isos` . A directory named `ISOs` will contain _for each synced video_ a set of ISO audio files of exact same length, padded or trimmed to coincide with the video track. After re-editing and re-mixing a `remergemix` command will resync the new audio with the video and _the new sound track will be updated on your NLE timeline_, at least in Kdenlive...
98
+ Specifying `--isos` produces _synced_ ISO audio files: for each synced \<video-clip\> a directory named `<video-clip>_ISO` will contain a set of ISO audio files each of exact same length, padded or trimmed to coincide with the video start. After re-editing and re-mixing in your DAW of choice a `remergemix` command will resync the new audio with the video and _the new sound track will be updated on your NLE timeline_, _automagically_ on some NLEs or on command for [Davinci Resolve](https://www.niwa.nu/dr-scripts/).
96
99
 
97
100
  > tictacsync --isos dailies/structured
101
+ #### `-p`
98
102
 
99
103
  When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
100
104