tictacsync 0.97a0__py3-none-any.whl → 0.99a0__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,25 @@ 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
+ current_permissions = stat.S_IMODE(os.lstat(path).st_mode)
42
+ os.chmod(path, current_permissions & NO_WRITING)
27
43
 
28
44
  # utility for accessing pathnames
29
45
  def _pathname(tempfile_or_path) -> str:
@@ -64,6 +80,12 @@ def _extr_channel(source, dest, channel):
64
80
  status = sox_transform.build(str(source), str(dest))
65
81
  logger.debug('sox status %s'%status)
66
82
 
83
+ def _same(aList):
84
+ return aList.count(aList[0]) == len(aList)
85
+
86
+ def _flatten(xss):
87
+ return [x for xs in xss for x in xs]
88
+
67
89
  def _sox_keep(audio_file, kept_channels: list) -> tempfile.NamedTemporaryFile:
68
90
  """
69
91
  Returns a NamedTemporaryFile containing the selected kept_channels
@@ -102,7 +124,7 @@ def _sox_keep(audio_file, kept_channels: list) -> tempfile.NamedTemporaryFile:
102
124
  stderr).decode('utf-8'))
103
125
  return output_tempfile
104
126
 
105
- def _split_channels(multi_chan_audio:Path) -> list:
127
+ def _sox_split_channels(multi_chan_audio:Path) -> list:
106
128
  nchan = sox.file_info.channels(_pathname(multi_chan_audio))
107
129
  source = _pathname(multi_chan_audio)
108
130
  paths = []
@@ -383,14 +405,14 @@ class AudioStitcherVideoMerger:
383
405
  def _get_audio_devices(self):
384
406
  devices = set([r.device for r in self.get_matched_audio_recs()])
385
407
  logger.debug('get_matched_audio_recs: %s'%
386
- pprint.pformat(self.get_matched_audio_recs()))
408
+ pformat(self.get_matched_audio_recs()))
387
409
  logger.debug('devices %s'%devices)
388
410
  return devices
389
411
 
390
412
  def _get_secondary_audio_devices(self):
391
413
  devices = set([r.device for r in self.get_matched_audio_recs()])
392
414
  logger.debug('get_matched_audio_recs: %s'%
393
- pprint.pformat(self.get_matched_audio_recs()))
415
+ pformat(self.get_matched_audio_recs()))
394
416
  logger.debug('devices %s'%devices)
395
417
  return devices
396
418
 
@@ -641,7 +663,7 @@ class AudioStitcherVideoMerger:
641
663
  Returns nothing, output is written to filesystem as below.
642
664
  ISOs subfolders structure when user invokes the --isos flag:
643
665
 
644
- SyncedMedia/ (or output_dir)
666
+ SyncedMedia/ (or anchor_dir)
645
667
 
646
668
  leftCAM/
647
669
 
@@ -689,24 +711,16 @@ class AudioStitcherVideoMerger:
689
711
  synced_clip_dir = synced_clip_file.parent
690
712
  # build ISOs subfolders structure, see comment string below
691
713
  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
714
  ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_ISO')
695
715
  os.makedirs(ISOdir, exist_ok=True)
696
716
  logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
697
717
  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
718
  for name, mono_tmpfl in edited_audio_all_devices:
702
- # pad(start_duration: float = 0.0, end_duration: float = 0.0)[source]
703
719
  destination = ISOdir/('%s.wav'%name)
704
720
  mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
705
721
  shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
722
+ # remove_write_permissions(destination)
706
723
  logger.debug('destination:%s'%destination)
707
- # # mixNnormed = _sox_mix_files(tempfiles)
708
- # # print('516', _pathname(mixNnormed))
709
- # os.remove(ISO_multi_chan)
710
724
 
711
725
  def _get_device_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
712
726
  """
@@ -789,7 +803,7 @@ class AudioStitcherVideoMerger:
789
803
  # all, but here remove '0' and TTC tracks from mix
790
804
  all_channels = list(range(1, device.n_chan + 1)) # sox not ZBIDX
791
805
  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 #)'%
806
+ logger.debug('multitrack but no mix, mixing mono %s except # %s (sox #)'%
793
807
  (all_channels, to_remove))
794
808
  sox_kept_channels = [i for i in all_channels
795
809
  if i not in to_remove]
@@ -851,26 +865,25 @@ class AudioStitcherVideoMerger:
851
865
  stereo_files = mic_stereo_files + new_stereo_files
852
866
  return _sox_mix_files(stereo_files)
853
867
 
854
- def build_audio_and_write_merged_media(self, top_dir, output_dir,
855
- write_multicam_structure,
856
- asked_ISOs, audio_REC_only):
868
+ def build_audio_and_write_merged_media(self, top_dir,
869
+ dont_write_cam_folder, asked_ISOs, audio_REC_only):
857
870
  # simply bifurcates depending if ref media is video (prob 99%)
858
871
  # (then audio_REC_only == False)
859
- # or if ref media is audio (no camera detected)
872
+ # or if ref media is audio (no camera detected, 1% of cases)
860
873
  # (with audio_REC_only == True)
861
874
  if not audio_REC_only:
862
- self._build_audio_and_write_video(top_dir, output_dir,
863
- write_multicam_structure, asked_ISOs)
875
+ self._build_audio_and_write_video(top_dir,
876
+ dont_write_cam_folder, asked_ISOs)
864
877
  else:
865
- self._build_and_write_audio(top_dir, output_dir)
878
+ self._build_and_write_audio(top_dir, anchor_dir)
866
879
 
867
- def _build_and_write_audio(self, top_dir, output_dir):
880
+ def _build_and_write_audio(self, top_dir, anchor_dir):
868
881
  """
869
882
  This is called when only audio recorders were found (no cam).
870
883
 
871
884
  top_dir: Path, directory where media were looked for
872
885
 
873
- output_dir: str for optional folder specified as CLI argument, if
886
+ anchor_dir: str for optional folder specified as CLI argument, if
874
887
  value is None, fall back to OUT_DIR_DEFAULT
875
888
 
876
889
  For each audio devices found overlapping self.ref_audio: pad, trim
@@ -889,16 +902,16 @@ class AudioStitcherVideoMerger:
889
902
  self.ref_audio.device.name))
890
903
  # eg, suppose the user called tictacsync with 'mondayPM' as top folder
891
904
  # to scan for dailies (and 'somefolder' for output):
892
- if output_dir == None:
905
+ if anchor_dir == None:
893
906
  synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
894
907
  else:
895
- synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
908
+ synced_clip_dir = Path(anchor_dir)/Path(top_dir).name # = somefolder/mondayPM
896
909
  self.synced_clip_dir = synced_clip_dir
897
910
  os.makedirs(synced_clip_dir, exist_ok=True)
898
911
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
899
912
  synced_clip_file = synced_clip_dir/\
900
913
  Path(self.videoclip.new_rec_name).name
901
- logger.debug('editing files for %s'%synced_clip_file)
914
+ logger.debug('editing files for synced_clip_file%s'%synced_clip_file)
902
915
  self.ref_audio.final_synced_file = synced_clip_file # relative path
903
916
  # Collecting edited audio by device, in (Device, tempfile) pairs:
904
917
  # for a given self.ref_audio, each other audio device will have a sequence
@@ -919,15 +932,15 @@ class AudioStitcherVideoMerger:
919
932
  # no audio file overlaps for this clip
920
933
  return #############################################################
921
934
  logger.debug('will output ISO files since no cam')
922
- devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
935
+ devices_and_monofiles = [(device, _sox_split_channels(multi_chan_audio))
923
936
  for device, multi_chan_audio
924
937
  in merged_audio_files_by_device]
925
938
  # add device and file from self.ref_audio
926
939
  new_tuple = (self.ref_audio.device,
927
- _split_channels(self.ref_audio.AVpath))
940
+ _sox_split_channels(self.ref_audio.AVpath))
928
941
  devices_and_monofiles.append(new_tuple)
929
942
  logger.debug('devices_and_monofiles: %s'%
930
- pprint.pformat(devices_and_monofiles))
943
+ pformat(devices_and_monofiles))
931
944
  def _trnm(dev, idx): # used in the loop just below
932
945
  # generates track name for later if asked_ISOs
933
946
  # idx is from 0 to nchan-1 for this device
@@ -963,17 +976,15 @@ class AudioStitcherVideoMerger:
963
976
  logger.debug('merged_audio_files_by_device %s'%
964
977
  merged_audio_files_by_device)
965
978
 
966
-
967
- def _build_audio_and_write_video(self, top_dir, output_dir,
968
- write_multicam_structure,
969
- asked_ISOs):
979
+ def _build_audio_and_write_video(self, top_dir, dont_write_cam_folder,
980
+ asked_ISOs):
970
981
  """
971
982
  top_dir: Path, directory where media were looked for
972
983
 
973
- output_dir: str for optional folder specified as CLI argument, if
984
+ anchor_dir: str for optional folder specified as CLI argument, if
974
985
  value is None, fall back to OUT_DIR_DEFAULT
975
986
 
976
- write_multicam_structure: True if needs to write multicam folders
987
+ dont_write_cam_folder: True if needs to bypass writing multicam folders
977
988
 
978
989
  asked_ISOs: bool flag specified as CLI argument
979
990
 
@@ -987,25 +998,28 @@ class AudioStitcherVideoMerger:
987
998
 
988
999
  Sets AudioStitcherVideoMerger.final_synced_file on completion
989
1000
  """
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))
1001
+ logger.debug(' fct args: top_dir %s, dont_write_cam_folder %s, asked_ISOs %s'%
1002
+ (top_dir, dont_write_cam_folder, asked_ISOs))
992
1003
  logger.debug('device for rec %s: %s'%(self.videoclip,
993
1004
  self.videoclip.device))
994
1005
  # eg, suppose the user called tictacsync with 'mondayPM' as top folder
995
1006
  # 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
1007
+ # if anchor_dir == None:
1008
+ # then "alongside mode" i.e., in a folder neighboring the video clip
1009
+ # WAS: synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
1010
+ logger.debug('"alongside mode" for clip: %s'%self.videoclip.AVpath)
1011
+ synced_clip_dir = self.videoclip.AVpath.parent/OUT_DIR_DEFAULT
1012
+ # else: # [TODO: replace original clip in a mirrored file structure]
1013
+ # logger.debug('mirrored mode for clip: %s'%self.videoclip.AVpath)
1014
+ # synced_clip_dir = Path(anchor_dir)/Path(top_dir).name # = somefolder/mondayPM
1015
+ # if write_multicam_structure:
1016
+ # device_name = self.videoclip.device.name
1017
+ # synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
1003
1018
  self.synced_clip_dir = synced_clip_dir
1004
1019
  os.makedirs(synced_clip_dir, exist_ok=True)
1005
1020
  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)
1021
+ synced_clip_file = synced_clip_dir/self.videoclip.AVpath.name
1022
+ logger.debug('editing files for synced_clip_file %s'%synced_clip_file)
1009
1023
  self.videoclip.final_synced_file = synced_clip_file # relative path
1010
1024
  # Collecting edited audio by device, in (Device, tempfiles) pairs:
1011
1025
  # for a given self.videoclip, each audio device will have a sequence
@@ -1069,7 +1083,7 @@ class AudioStitcherVideoMerger:
1069
1083
  mix_of_device_mixes = _sox_mix_files(device_mixes)
1070
1084
  logger.debug('will merge with %s'%(_pathname(mix_of_device_mixes)))
1071
1085
  self.videoclip.synced_audio = mix_of_device_mixes
1072
- logger.debug('mix_of_device_mixes n chan: %i'%
1086
+ logger.debug('mix_of_device_mixes (final mix) has %i channels'%
1073
1087
  sox.file_info.channels(_pathname(mix_of_device_mixes)))
1074
1088
  self._merge_audio_and_video()
1075
1089
  # devices_and_monofiles is list of (device, [monofiles])
@@ -1079,18 +1093,20 @@ class AudioStitcherVideoMerger:
1079
1093
  # devices_and_monofiles:
1080
1094
  if asked_ISOs:
1081
1095
  logger.debug('will output ISO files...')
1082
- devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
1096
+ devices_and_monofiles = [(device, _sox_split_channels(multi_chan_audio))
1083
1097
  for device, multi_chan_audio
1084
1098
  in merged_audio_files_by_device]
1085
1099
  logger.debug('devices_and_monofiles: %s'%
1086
- pprint.pformat(devices_and_monofiles))
1087
- def _trnm(dev, idx): # used in the loop just below
1100
+ pformat(devices_and_monofiles))
1101
+ def _build_from_tracks_txt(dev, idx):
1102
+ # used in the loop just below
1088
1103
  # generates track name for later if asked_ISOs
1089
1104
  # idx is from 0 to nchan-1 for this device
1090
1105
  if dev.tracks == None:
1106
+ # no tracks.txt was found so use ascending numbers for name
1091
1107
  chan_name = 'chan%s'%str(idx+1).zfill(2)
1092
1108
  else:
1093
- # sanitize
1109
+ # sanitize names in tracks.txt
1094
1110
  symbols = set(r"""`~!@#$%^&*()_-+={[}}|\:;"'<,>.?/""")
1095
1111
  chan_name = dev.tracks.rawtrx[idx]
1096
1112
  logger.debug('raw chan_name %s'%chan_name)
@@ -1110,12 +1126,12 @@ class AudioStitcherVideoMerger:
1110
1126
  names_audio_tempfiles = []
1111
1127
  for dev, mono_tmpfiles_list in devices_and_monofiles:
1112
1128
  for idx, monotf in enumerate(mono_tmpfiles_list):
1113
- track_name = _trnm(dev, idx)
1129
+ track_name = _build_from_tracks_txt(dev, idx)
1114
1130
  logger.debug('track_name %s'%track_name)
1115
1131
  if track_name[0] == '0': # muted, skip
1116
1132
  continue
1117
1133
  names_audio_tempfiles.append((track_name, monotf))
1118
- logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
1134
+ logger.debug('names_audio_tempfiles %s'%pformat(names_audio_tempfiles))
1119
1135
  self._write_ISOs(names_audio_tempfiles)
1120
1136
  logger.debug('merged_audio_files_by_device %s'%
1121
1137
  merged_audio_files_by_device)
@@ -1233,6 +1249,10 @@ class Matcher:
1233
1249
  AudioStitcherVideoMerger object. An audio_stitch doesn't extend
1234
1250
  beyond the corresponding video start and end times.
1235
1251
 
1252
+ multicam_clips_clusters : list
1253
+ of {'end': t1, 'start': t2, 'vids': [r1,r3]} where r1 and r3
1254
+ are overlapping.
1255
+
1236
1256
  """
1237
1257
 
1238
1258
  def __init__(self, recordings_list):
@@ -1246,20 +1266,20 @@ class Matcher:
1246
1266
  self.recordings = recordings_list
1247
1267
  self.mergers = []
1248
1268
 
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)))
1269
+ # def _rename_all_recs(self):
1270
+ # """
1271
+ # Add _synced to filenames of synced video files. Change stored name only:
1272
+ # files have yet to be written to.
1273
+ # """
1274
+ # # match IO_structure:
1275
+ # # case 'foldercam':
1276
+ # for rec in self.recordings:
1277
+ # rec_extension = rec.AVpath.suffix
1278
+ # rel_path_new_name = '%s%s'%(rec.AVpath.stem, rec_extension)
1279
+ # rec.new_rec_name = Path(rel_path_new_name)
1280
+ # logger.debug('for %s new name: %s'%(
1281
+ # _pathname(rec.AVpath),
1282
+ # _pathname(rec.new_rec_name)))
1263
1283
 
1264
1284
  def scan_audio_for_each_videoclip(self):
1265
1285
  """
@@ -1295,7 +1315,7 @@ class Matcher:
1295
1315
  self.mergers.append(audio_stitch)
1296
1316
  else:
1297
1317
  logger.debug('\n nothing\n')
1298
- print('No overlap found for %s'%videoclip.AVpath.name)
1318
+ print('No overlap found for [gold1]%s[/gold1]'%videoclip.AVpath.name)
1299
1319
  del audio_stitch
1300
1320
  logger.debug('%i mergers created'%len(self.mergers))
1301
1321
 
@@ -1310,49 +1330,29 @@ class Matcher:
1310
1330
  case4 = R1 < A2 < R2
1311
1331
  return case1 or case2 or case3 or case4
1312
1332
 
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
- """
1333
+ def set_up_clusters(self):
1334
+ # builds the list self.multicam_clips_clusters. A list
1335
+ # of {'end': t1, 'start': t2, 'vids': [r1,r3]} where r1 and r3
1336
+ # are overlapping.
1337
+ # if no overlap occurs, length of vid = 1, ex 'vids': [r1]
1338
+ # so not really a cluster in those cases
1339
+ # returns nothing and sets Matcher.multicam_clips_clusters
1339
1340
  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
1341
  # INs_and_OUTs contains (time, direction, video) for each video,
1345
1342
  # where direction is 'in|out' and video an instance of Recording
1346
1343
  INs_and_OUTs = [(vid.get_start_time(), 'in', vid) for vid in vids]
1347
1344
  for vid in vids:
1348
1345
  INs_and_OUTs.append((vid.get_end_time(), 'out', vid))
1349
1346
  INs_and_OUTs = sorted(INs_and_OUTs, key=lambda vtuple: vtuple[0])
1350
- logger.debug('INs_and_OUTs: %s'%INs_and_OUTs)
1347
+ logger.debug('INs_and_OUTs: %s'%pformat(INs_and_OUTs))
1351
1348
  new_cluster = True
1352
1349
  current_cluster = {'vids':[]}
1353
1350
  N_in, N_out = (0, 0)
1354
1351
  # clusters is a list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
1355
1352
  clusters = []
1353
+ # a cluster begins (and grows) when a time of type 'in' is encountered
1354
+ # a cluster degrows when a time of type 'out' is encountered and
1355
+ # closes when its size (N_currently_open) reach back to zero
1356
1356
  for t, direction, video in INs_and_OUTs:
1357
1357
  if new_cluster and direction == 'out':
1358
1358
  logger.error('cant begin a cluster with a out time %s'%video)
@@ -1373,7 +1373,44 @@ Moves clusters at the timelineoffset
1373
1373
  new_cluster = True
1374
1374
  current_cluster = {'vids':[]}
1375
1375
  N_in, N_out = (0, 0)
1376
- logger.debug('clusters: %s'%pprint.pformat(clusters))
1376
+ logger.debug('clusters: %s'%pformat(clusters))
1377
+ self.multicam_clips_clusters = clusters
1378
+ return
1379
+
1380
+ def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
1381
+ """
1382
+ for single cam shootings this simply sets the gap between takes,
1383
+ tweaking each vid timecode metadata to distribute them next to each
1384
+ other along NLE timeline.
1385
+
1386
+ Moves clusters at the timelineoffset
1387
+
1388
+ For multicam takes, shifts are computed so
1389
+ video clusters are near but dont overlap, ex:
1390
+
1391
+ ***** are inserted gaps
1392
+
1393
+ Cluster 1 Cluster 2
1394
+ 1111111111111 2222222222 (cam A)
1395
+ 11111111111******222222222 (cam B)
1396
+
1397
+ or
1398
+ 11111111111111 222222 (cam A)
1399
+ 1111111 ******222222222 (cam B)
1400
+
1401
+ argument:
1402
+ CLI_offset (str), option from command-line
1403
+ with_gap (float), the gap duration in seconds
1404
+
1405
+ Returns nothing, changes are done in the video files metadata
1406
+ (each referenced by Recording.final_synced_file)
1407
+ """
1408
+ vids = [m.videoclip for m in self.mergers]
1409
+ logger.debug('vids %s'%vids)
1410
+ if len(vids) == 1:
1411
+ logger.debug('just one take, no gap to shrink')
1412
+ return #############################################################
1413
+ clusters = self.multicam_clips_clusters
1377
1414
  # if there are N clusters, there are N-1 gaps to evaluate and shorten
1378
1415
  # (lengthen?) to a value of with_gap seconds
1379
1416
  gaps = [c2['start'] - c1['end'] for c1, c2
@@ -1414,7 +1451,61 @@ Moves clusters at the timelineoffset
1414
1451
  vid.write_file_timecode(tc)
1415
1452
  return
1416
1453
 
1454
+ def move_multicam_to_dir(self):
1455
+ # creates a dedicated multicam directory and move clusters there
1456
+ # e.g., for "top/day01/camA/roll02"
1457
+ # ^ at that level
1458
+ # 0 1 2 3
1459
+ # Note: ROLLxx maybe present or not.
1460
+ #
1461
+ # check for consistency: are all clips at the same level from topdir?
1462
+ # Only for video, not audio (which doesnt fill up cards)
1463
+ video_medias = [m for m in self.recordings if m.device.dev_type == 'CAM']
1464
+ video_paths = [m.AVpath.parts for m in video_medias]
1465
+ AV_path_lengths = [len(p) for p in video_paths]
1466
+ # print('AV_path_lengths', AV_path_lengths)
1467
+ if not _same(AV_path_lengths):
1468
+ print('\nError with some clips, check if their locations are consistent (all at the same level in folders).')
1469
+ print('Video synced but could not regroup multicam clips.')
1470
+ sys.exit(0)
1471
+ # now find at which level CAMs reside (maybe there are ROLLxx)
1472
+ CAM_levels = [vm.AVpath.parts.index(vm.device.name)
1473
+ for vm in video_medias]
1474
+ # find for all, should be same
1475
+ if not _same(CAM_levels):
1476
+ print('\nError with some clips, check if their locations are consistent (all at the same level in folders).')
1477
+ print('Video synced but could not regroup multicam clips.')
1478
+ sys.exit(0)
1479
+ # pick first
1480
+ CAM_level, avp = CAM_levels[0], video_medias[0].AVpath
1481
+ logger.debug('CAM_levels: %s for ex \n%s'%(CAM_level, avp))
1482
+ # MCCDIR = 'SyncedMulticamClips'
1483
+ parts_up_a_level = avp.parts[:CAM_level]
1484
+ multicam_dir = Path('').joinpath(*parts_up_a_level)/MCCDIR
1485
+ logger.debug('multicam_dir: %s'%multicam_dir)
1486
+ Path.mkdir(multicam_dir, exist_ok=True)
1487
+ cam_clips = []
1488
+ [cam_clips.append(cl['vids']) for cl in self.multicam_clips_clusters]
1489
+ cam_clips = _flatten(cam_clips)
1490
+ logger.debug('cam_clips: %s'%cam_clips)
1491
+ cam_names = set([r.device.name for r in cam_clips])
1492
+ # create new dirs for each CAM
1493
+ [Path.mkdir(multicam_dir/cam_name, exist_ok=True)
1494
+ for cam_name in cam_names]
1495
+ # move clips there
1496
+ for r in cam_clips:
1497
+ cam = r.device.name
1498
+ clip_name = r.AVpath.name
1499
+ dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
1500
+ logger.debug('dest: %s'%dest)
1501
+ origin_folder = r.final_synced_file.parent
1502
+ folder_now_empty = len(list(origin_folder.glob('*'))) == 0
1503
+ if folder_now_empty:
1504
+ logger.debug('after moving %s, folder is now empty, removing it'%dest)
1505
+ origin_folder.rmdir()
1506
+ print('\nMoved %i multicam clips in %s'%(len(cam_clips), multicam_dir))
1417
1507
 
1508
+
1418
1509
 
1419
1510
 
1420
1511
 
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.97a0
3
+ Version: 0.99a0
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/P3gbZR4GgGy8xQp/download/dailies1_1.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:
@@ -96,7 +97,7 @@ For a one line output (or to suppress the progress bars) use the `--terse` flag:
96
97
 
97
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/).
98
99
 
99
- > tictacsync --isos dailies/day01
100
+ > tictacsync --isos dailies/structured
100
101
  #### `-p`
101
102
 
102
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: