tictacsync 1.0.0a0__tar.gz → 1.0.2a0__tar.gz

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.

Files changed (21) hide show
  1. {tictacsync-1.0.0a0/tictacsync.egg-info → tictacsync-1.0.2a0}/PKG-INFO +2 -2
  2. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/README.md +1 -1
  3. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/setup.py +1 -1
  4. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/device_scanner.py +60 -29
  5. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/entry.py +12 -122
  6. tictacsync-1.0.2a0/tictacsync/mamsync.py +460 -0
  7. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/multi2polywav.py +1 -1
  8. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/newmix.py +4 -1
  9. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0/tictacsync.egg-info}/PKG-INFO +2 -2
  10. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/SOURCES.txt +1 -0
  11. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/LICENSE +0 -0
  12. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/setup.cfg +0 -0
  13. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/__init__.py +0 -0
  14. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/remergemix.py +0 -0
  15. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/timeline.py +0 -0
  16. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync/yaltc.py +0 -0
  17. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/dependency_links.txt +0 -0
  18. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/entry_points.txt +0 -0
  19. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/not-zip-safe +0 -0
  20. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/requires.txt +0 -0
  21. {tictacsync-1.0.0a0 → tictacsync-1.0.2a0}/tictacsync.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 1.0.0a0
3
+ Version: 1.0.2a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -58,7 +58,7 @@ Then pip install the syncing program:
58
58
  This should install python dependencies _and_ the `tictacsync` command.
59
59
  ## Usage
60
60
 
61
- Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (625 MB, sorry) unzip and run:
61
+ Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
62
62
 
63
63
  > tictacsync dailies/loose
64
64
  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:
@@ -35,7 +35,7 @@ Then pip install the syncing program:
35
35
  This should install python dependencies _and_ the `tictacsync` command.
36
36
  ## Usage
37
37
 
38
- Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (625 MB, sorry) unzip and run:
38
+ Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
39
39
 
40
40
  > tictacsync dailies/loose
41
41
  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:
@@ -33,7 +33,7 @@ setup(
33
33
  'multi2polywav = tictacsync.multi2polywav:main',
34
34
  ]
35
35
  },
36
- version = '1.0.0-alpha',
36
+ version = '1.0.2-alpha',
37
37
  description = "command for syncing audio video recordings",
38
38
  long_description_content_type='text/markdown',
39
39
  long_description = long_descr,
@@ -19,7 +19,7 @@ AMR APE AU AWB DSS DVF FLAC GSM IKLAX IVS M4A M4B M4P MMF MP3 MPC MSV NMF
19
19
  OGG OGA MOGG OPUS RA RM RAW RF64 SLN TTA VOC VOX WAV WMA WV WEBM 8SVX CDA MOV AVI BWF""".split()
20
20
 
21
21
  from dataclasses import dataclass
22
- import ffmpeg, os, sys
22
+ import ffmpeg, os, sys, shutil
23
23
  from os import listdir
24
24
  from os.path import isfile, join, isdir
25
25
  from collections import namedtuple
@@ -28,7 +28,7 @@ from pprint import pformat
28
28
  # from collections import defaultdict
29
29
  from loguru import logger
30
30
  # import pathlib, os.path
31
- import sox, tempfile
31
+ import sox, tempfile, platformdirs, filecmp
32
32
  # from functools import reduce
33
33
  from rich import print
34
34
  from itertools import groupby
@@ -36,8 +36,10 @@ from itertools import groupby
36
36
  # import distance
37
37
  try:
38
38
  from . import multi2polywav
39
+ from . import mamsync
39
40
  except:
40
41
  import multi2polywav
42
+ import mamsync
41
43
 
42
44
  MCCDIR = 'SyncedMulticamClips'
43
45
  SYNCEDFOLDER = 'SyncedMedia'
@@ -244,16 +246,7 @@ class Scanner:
244
246
  CAMs = [d for d in devices if d.dev_type == 'CAM']
245
247
  return len(set(CAMs))
246
248
 
247
- def get_device_in_path(self, a_media):
248
- """
249
- Return a device name taken from folders in path of a_media if and only
250
- if all media files below it are from the same device. Goes up max two
251
- levels from media; e.g., in /FOLDER2/FOLDER1/DSC00234.MOV will test up
252
- to FOLDER2 as candidate.
253
- """
254
-
255
-
256
- def scan_media_and_build_devices_UID(self, recursive=True):
249
+ def scan_media_and_build_devices_UID(self, synced_root = None):
257
250
  """
258
251
  Scans Scanner.top_directory recursively for files with known audio-video
259
252
  extensions. For each file found, a device fingerprint is obtained from
@@ -269,20 +262,59 @@ class Scanner:
269
262
  Sets Scanner.input_structure = 'loose'|'ordered'
270
263
 
271
264
  """
272
- files = Path(self.top_directory).rglob('*.*')
273
- def _last4letters(part):
274
- return
275
-
276
- paths = [
277
- p
278
- for p in files
279
- if p.suffix[1:] in av_file_extensions
280
- and SYNCEDFOLDER not in p.parts # SyncedMedia
281
- and MCCDIR not in p.parts # SyncedMulticamClips
282
- and '_ISO' not in [part[-4:] for part in p.parts] # exclude ISO wav files
283
- ]
284
- logger.debug('found media files %s'%paths)
285
- parents = [p.parent for p in paths]
265
+ logger.debug(f'on entry synced_root: {synced_root}')
266
+ if synced_root != None: # mam mode
267
+ p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamsync.LOG_FILE
268
+ with open(p, 'r') as fh:
269
+ done = set(fh.read().split()) # sets of strings of abs path
270
+ logger.debug(f'done clips: {pformat(done)}')
271
+ files = Path(self.top_directory).rglob('*')
272
+ clip_paths = []
273
+ some_done = False
274
+ for raw_path in files:
275
+ if raw_path.suffix[1:] in av_file_extensions:
276
+ if SYNCEDFOLDER not in raw_path.parts: # SyncedMedia
277
+ if MCCDIR not in raw_path.parts: # SyncedMulticamClips
278
+ if '_ISO' not in [part[-4:] for part in raw_path.parts]: # exclude ISO wav files
279
+ if synced_root != None and str(raw_path) in done:
280
+ logger.debug(f'{raw_path} done')
281
+ some_done = True
282
+ continue
283
+ else:
284
+ clip_paths.append(raw_path)
285
+ ############################################################ MOVE
286
+ elif synced_root != None: # MAM mode
287
+ #non AV files and directories
288
+ synced_path = Path(synced_root)/str(raw_path)[1:] # cant join abs. paths
289
+ if raw_path.is_dir():
290
+ synced_path.mkdir(parents=True, exist_ok=True)
291
+ continue
292
+ # if here, it's a file
293
+ if not synced_path.exists():
294
+ print(f'will mirror non AV file at {synced_path}')
295
+ logger.debug(f'will mirror non AV file at {synced_path}')
296
+ shutil.copy2(raw_path, synced_path)
297
+ continue
298
+ # file exists, check if same
299
+ same = filecmp.cmp(raw_path, synced_path, shallow=False)
300
+ logger.debug(f'copy exists of:\n{raw_path}\n{synced_path}')
301
+ if not same:
302
+ print(f'file changed, copying again\n{raw_path}')
303
+ shutil.copy2(raw_path, synced_path)
304
+ else:
305
+ logger.debug('same content, next')
306
+ continue # next raw_path in loop
307
+ ###################################################################
308
+ if some_done:
309
+ print('Somme media files were already synced...')
310
+ logger.debug('found media files %s'%clip_paths)
311
+ if len(clip_paths) == 0:
312
+ print('No media found, bye.')
313
+ sys.exit(0)
314
+ # self.found_media_files = []
315
+ # self.input_structure = 'loose'
316
+ # return
317
+ parents = [p.parent for p in clip_paths]
286
318
  logger.debug('found parents %s'%pformat(parents))
287
319
  # True if all elements are identical
288
320
  AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
@@ -296,7 +328,7 @@ class Scanner:
296
328
  # check later if inside each folder, media have same device
297
329
  # for now, we'll guess structure is 'ordered'
298
330
  self.input_structure = 'ordered'
299
- for p in paths:
331
+ for p in clip_paths:
300
332
  new_media = media_at_path(self.input_structure, p) # dev UID set here
301
333
  self.found_media_files.append(new_media)
302
334
  # for non UIDed try building UID from filenam
@@ -379,7 +411,7 @@ class Scanner:
379
411
  no_name_devices = [m.device for m in self.found_media_files
380
412
  if not m.device.name]
381
413
  # possible if self.input_structure == 'loose'
382
- def _try_name_from_metadata(media):
414
+ def _try_name_from_metadata(media): # unused for now
383
415
  # search model and make from fprobe
384
416
  file = Path(media.path)
385
417
  logger.debug('trying to find maker model for %s'%file)
@@ -395,7 +427,6 @@ class Scanner:
395
427
  # could reside in ['format','tags','com.apple.quicktime.model'],
396
428
  # or ['format','tags','model'],
397
429
  # or ['streams'][0]['tags']['vendor_id']) :-(
398
-
399
430
  for anon_dev in no_name_devices:
400
431
  medias = self.get_media_for_device(anon_dev)
401
432
  guess_name = _try_name_from_files(medias)
@@ -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))
@@ -122,61 +105,6 @@ def process_lag_adjustement(media_object):
122
105
  logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
123
106
  Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
124
107
 
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
108
  def main():
181
109
  parser = argparse.ArgumentParser()
182
110
  parser.add_argument(
@@ -191,15 +119,15 @@ def main():
191
119
  action='store_true', #ie default False
192
120
  dest='verbose_output',
193
121
  help='Set verbose ouput')
194
- parser.add_argument('--stop_mirroring',
195
- action='store_true', #ie default False
196
- dest='stop_mirroring',
197
- help='Stop mirroring mode, will write synced files alongside originals.')
198
- parser.add_argument('--start-project', '-s' ,
199
- nargs=2,
200
- dest='proj_folders',
201
- default = [],
202
- help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
122
+ # parser.add_argument('--stop_mirroring',
123
+ # action='store_true', #ie default False
124
+ # dest='stop_mirroring',
125
+ # help='Stop mirroring mode, will write synced files alongside originals.')
126
+ # parser.add_argument('--start-project', '-s' ,
127
+ # nargs=2,
128
+ # dest='proj_folders',
129
+ # default = [],
130
+ # help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
203
131
  parser.add_argument('-t','--timelineoffset',
204
132
  nargs=1,
205
133
  default=['00:00:00:00'],
@@ -242,33 +170,10 @@ def main():
242
170
  # logger.add(sys.stdout, filter="__main__")
243
171
  # logger.add(sys.stdout, filter="yaltc")
244
172
 
245
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "start_proj")
173
+ logger.add(sys.stdout, filter=lambda r: r["function"] == "start_proj")
246
174
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "_dev_type_for_name")
247
175
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
248
176
 
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
177
  top_dir = args.path[0]
273
178
  if os.path.isfile(top_dir):
274
179
  file = top_dir
@@ -276,15 +181,6 @@ def main():
276
181
  if not os.path.isdir(top_dir):
277
182
  print('%s is not a directory or doesnt exist.'%top_dir)
278
183
  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
184
  multi2polywav.poly_all(top_dir)
289
185
  scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
290
186
  scanner.scan_media_and_build_devices_UID()
@@ -406,13 +302,6 @@ def main():
406
302
  console.print(table)
407
303
  print()
408
304
  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
305
  if len(recordings_with_time) < 2:
417
306
  if not args.terse:
418
307
  print('\nNothing to sync, exiting.\n')
@@ -429,6 +318,7 @@ def main():
429
318
  asked_ISOs = False
430
319
  # output_dir = args.o
431
320
  # if args.verbose_output or args.terse: # verbose, so no progress bars
321
+ print('Merging...')
432
322
  for merger in matcher.mergers:
433
323
  merger.build_audio_and_write_merged_media(top_dir,
434
324
  args.dont_write_cam_folder,
@@ -0,0 +1,460 @@
1
+
2
+ # I know, the following is ugly, but I need those try's to
3
+ # run the command in my dev setting AND from
4
+ # a deployment set-up... surely I'm setting
5
+ # things wrong [TODO]: find why and clean up this mess
6
+
7
+ try:
8
+ from . import yaltc
9
+ from . import device_scanner
10
+ from . import timeline
11
+ from . import multi2polywav
12
+ except:
13
+ import yaltc
14
+ import device_scanner
15
+ import timeline
16
+ import multi2polywav
17
+
18
+ import argparse, tempfile, configparser
19
+ from loguru import logger
20
+ from pathlib import Path
21
+ # import os, sys
22
+ import os, sys, sox, platformdirs
23
+ from rich.progress import track
24
+ # from pprint import pprint
25
+ from rich.console import Console
26
+ # from rich.text import Text
27
+ from rich.table import Table
28
+ from rich import print
29
+ from pprint import pprint, pformat
30
+ import numpy as np
31
+
32
+ DEL_TEMP = False
33
+ CONF_FILE = 'mamsync.cfg'
34
+ LOG_FILE = 'mamdone.txt'
35
+
36
+ av_file_extensions = \
37
+ """MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
38
+ wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
39
+ m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp aa aac aax act aiff alac
40
+ amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
41
+ ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
42
+
43
+ # logger.add(sys.stdout, level="DEBUG")
44
+
45
+
46
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
47
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
48
+
49
+
50
+ def process_single(file, args):
51
+ # argument is a single file
52
+ m = device_scanner.media_at_path(None, Path(file))
53
+ if args.plotting:
54
+ print('\nPlots can be zoomed and panned...')
55
+ print('Close window for next one.')
56
+ a_rec = yaltc.Recording(m, do_plots=args.plotting)
57
+ time = a_rec.get_start_time()
58
+ # time = a_rec.get_start_time(plots=args.plotting)
59
+ if time != None:
60
+ frac_time = int(time.microsecond / 1e2)
61
+ d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
62
+ if args.terse:
63
+ print('%s UTC:%s pulse: %i in chan %i'%(file, d, a_rec.sync_position,
64
+ a_rec.TicTacCode_channel))
65
+ else:
66
+ print('\nRecording started at [gold1]%s[/gold1] UTC'%d)
67
+ print('true sample rate: [gold1]%.3f Hz[/gold1]'%a_rec.true_samplerate)
68
+ print('first sync at [gold1]%i[/gold1] samples in channel %i'%(a_rec.sync_position,
69
+ a_rec.TicTacCode_channel))
70
+ print('N.B.: all results are precise to the displayed digits!\n')
71
+ else:
72
+ if args.terse:
73
+ print('%s UTC: None'%(file))
74
+ else:
75
+ print('Start time couldnt be determined')
76
+ sys.exit(1)
77
+
78
+ def process_lag_adjustement(media_object):
79
+ # trim channels that are lagging (as stated in tracks.txt)
80
+ # replace the old file, and rename the old one with .wavbk
81
+ # if .wavbk exist, process was done already, so dont process
82
+ # returns nothing
83
+ lags = media_object.device.tracks.lag_values
84
+ logger.debug('will process %s lags'%[lags])
85
+ channels = timeline._sox_split_channels(media_object.path)
86
+ # add bk to file on filesystem, but media_object.path is unchanged (?)
87
+ backup_name = str(media_object.path) + 'bk'
88
+ if Path(backup_name).exists():
89
+ logger.debug('%s exists, so return now.'%backup_name)
90
+ return
91
+ media_object.path.replace(backup_name)
92
+ logger.debug('channels %s'%channels)
93
+ def _trim(lag, chan_file):
94
+ # for lag
95
+ if lag == None:
96
+ return chan_file
97
+ else:
98
+ logger.debug('process %s for lag of %s'%(chan_file, lag))
99
+ sox_transform = sox.Transformer()
100
+ sox_transform.trim(float(lag)*1e-3)
101
+ output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
102
+ out_file = timeline._pathname(output_fh)
103
+ input_file = timeline._pathname(chan_file)
104
+ logger.debug('sox in and out files: %s %s'%(input_file, out_file))
105
+ logger.debug('calling sox_transform.build()')
106
+ status = sox_transform.build(input_file, out_file, return_output=True )
107
+ logger.debug('sox.build exit code %s'%str(status))
108
+ return output_fh
109
+ new_channels = [_trim(*e) for e in zip(lags, channels)]
110
+ logger.debug('new_channels %s'%new_channels)
111
+ trimmed_multichanfile = timeline._sox_combine(new_channels)
112
+ logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
113
+ Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
114
+
115
+ def print_out_conf(raw_root, synced_root, snd_root):
116
+ print(f'RAWROOT (source with TC): "{raw_root}"')
117
+ print(f'SYNCEDROOT (destination of synced clips): "{synced_root}"')
118
+ print(f'SNDROOT (destination of ISOs files): "{snd_root}"')
119
+
120
+ def clear_log():
121
+ # clear the file logging clips already synced
122
+ data_dir = platformdirs.user_data_dir('mamsync', 'plutz', ensure_exists=True)
123
+ log_file = Path(data_dir)/LOG_FILE
124
+ print('Clearing log file "%s"'%log_file)
125
+ with open(log_file, 'w') as fh:
126
+ fh.write('done:\n')
127
+
128
+ def write_conf(raw_root, synced_root, snd_root):
129
+ # args are pahtlib.Paths.
130
+ # RAWROOT: files with TC (and ROLL folders), as is from cameras
131
+ # SYNCEDROOT: synced and no more TC (ROLL flattened)
132
+ # Writes configuration on filesystem for later retrieval
133
+ # Clears log of already synced clips.
134
+ conf_dir = platformdirs.user_config_dir('mamsync', 'plutz', ensure_exists=True)
135
+ logger.debug(f'will start project with raw_root:{raw_root}, synced_root:{synced_root}')
136
+ conf_file = Path(conf_dir)/CONF_FILE
137
+ logger.debug('writing config in %s'%conf_file)
138
+ print(f'\nWriting folders paths in configuration file "{conf_file}"')
139
+ print_out_conf(raw_root, synced_root, snd_root)
140
+ conf_prs = configparser.ConfigParser()
141
+ conf_prs['SECTION1'] = {'RAWROOT': raw_root,
142
+ 'SYNCEDROOT': synced_root,
143
+ 'SNDROOT': snd_root}
144
+ with open(conf_file, 'w') as configfile_handle:
145
+ conf_prs.write(configfile_handle)
146
+ with open(conf_file, 'r') as configfile_handle:
147
+ logger.debug(f'config file content: \n{configfile_handle.read()}')
148
+ clear_log()
149
+
150
+
151
+
152
+ # if known_values != ():
153
+ # RAWROOT, SYNCEDROOTS = known_values
154
+ # print('Warning: there is a current project')
155
+ # print('with source (RAW) folder: %s\nand destination (synced) folder: %s'%
156
+ # (RAWROOT, SYNCEDROOTS))
157
+ # answer = input("\nDo you want to change values? [YES|NO]")
158
+ # if answer.upper()[0] in ["Y", "YES"]:
159
+ # _write_cfg()
160
+ # return folders
161
+ # elif answer.upper()[0] in ["N", "NO"]:
162
+ # print('Ok, will keep old ones')
163
+ # return RAWROOT, SYNCEDROOTS
164
+ # else:
165
+ # _write_cfg()
166
+ # return folders
167
+ # sys.exit(0)
168
+
169
+ def get_proj(print_conf_stdout=False):
170
+ # check if user started a project before.
171
+ # stored in platformdirs.user_config_dir
172
+ # returns (RAWROOT, SYNCEDROOTS) if any, () otherwise.
173
+ # print location of conf file if print_conf_stdout
174
+ conf_dir = platformdirs.user_config_dir('mamsync', 'plutz')
175
+ conf_file = Path(conf_dir)/CONF_FILE
176
+ logger.debug('try reading config in %s'%conf_file)
177
+ if print_conf_stdout:
178
+ print(f'\nReading configuration from file {conf_file}')
179
+ if conf_file.exists():
180
+ conf_prs = configparser.ConfigParser()
181
+ conf_prs.read(conf_file)
182
+ RAWROOT = conf_prs.get('SECTION1', 'RAWROOT')
183
+ SYNCEDROOT = conf_prs.get('SECTION1', 'SYNCEDROOT')
184
+ SNDROOT = conf_prs.get('SECTION1', 'SNDROOT')
185
+ logger.debug('read from conf: RAWROOT= %s SYNCEDROOT= %s SNDROOT=%s'%
186
+ (RAWROOT, SYNCEDROOT, SNDROOT))
187
+ return RAWROOT, SYNCEDROOT, SNDROOT
188
+ else:
189
+ logger.debug(f'no config file found at {conf_file}')
190
+ return ()
191
+
192
+
193
+ def main():
194
+ parser = argparse.ArgumentParser()
195
+ parser.add_argument('--rawfolder',
196
+ nargs = 1,
197
+ dest='rawfolder',
198
+ help='Sets new value for raw root folder (i.e.: clips with TC)')
199
+ parser.add_argument('--syncedfolder',
200
+ nargs = 1,
201
+ dest='syncedfolder',
202
+ help='Sets new value for synced root folder (i.e.: synced clips without TC)')
203
+ parser.add_argument('--sndfolder',
204
+ nargs = 1,
205
+ dest='sndfolder',
206
+ help='Sets new value for sound folder (where ISOs will be stored)')
207
+ parser.add_argument('--showconf',
208
+ action='store_true',
209
+ dest='showconf',
210
+ help='Show current configured values.')
211
+ parser.add_argument('--resync',
212
+ action='store_true',
213
+ dest='resync',
214
+ help='Resync previously done clips.')
215
+ parser.add_argument(
216
+ "sub_dir",
217
+ type=str,
218
+ nargs='?',
219
+ help="Sub directory to scan, should under RAWROOT."
220
+ )
221
+ parser.add_argument('--terse',
222
+ action='store_true',
223
+ dest='terse',
224
+ help='Terse output')
225
+ parser.add_argument('--isos',
226
+ action='store_true',
227
+ dest='write_ISOs',
228
+ help='Cut ISO sound files')
229
+ parser.add_argument('-t','--timelineoffset',
230
+ nargs=1,
231
+ default=['00:00:00:00'],
232
+ dest='timelineoffset',
233
+ help='When processing multicam, where to place clips on NLE timeline (HH:MM:SS:FF)')
234
+ args = parser.parse_args()
235
+ logger.debug(f'arguments from argparse {args}')
236
+ # check --rawfolder, --syncedfolder and --sndfolder are used together
237
+ at_least_one = args.rawfolder != None or args.syncedfolder != None or args.sndfolder != None
238
+ tous = args.rawfolder != None and args.syncedfolder != None and args.sndfolder != None
239
+ if at_least_one:
240
+ if not tous:
241
+ print('Error: all --rawfolder, --syncedfolder and --sndfolder must be specified.')
242
+ sys.exit(0)
243
+ # check the 3 paths are ok
244
+ roots = [Path(e) for e in [args.rawfolder[0],
245
+ args.syncedfolder[0], args.sndfolder[0]]]
246
+ for r in roots:
247
+ if not r.is_absolute():
248
+ print(f'\rError: folder {r} must be an absolute path. Bye')
249
+ sys.exit(0)
250
+ if not r.exists():
251
+ print(f'\rError: folder {r} does not exist. Bye')
252
+ sys.exit(0)
253
+ if not r.is_dir():
254
+ print(f'\rError: path {r} is not a folder. Bye')
255
+ sys.exit(0)
256
+ # if still here, everything in 3 folders is ok
257
+ write_conf(*roots)
258
+ sys.exit(0)
259
+ if args.showconf:
260
+ raw_root, synced_root = get_proj(True)
261
+ print_out_conf(raw_root, synced_root)
262
+ sys.exit(0)
263
+ # if still here, no rawfolder, --syncedfolder --sndfolder or --showconf
264
+ # so go for a scan and sync, maybe with a sub_dir
265
+ raw_root, synced_root, snd_root = get_proj(False)
266
+ if args.sub_dir != None:
267
+ top_dir = args.sub_dir
268
+ logger.debug(f'sub _dir: {args.sub_dir}')
269
+ if not Path(top_dir).exists():
270
+ print(f"\rError: folder {top_dir} doesn't exist, bye.")
271
+ sys.exit(0)
272
+ else:
273
+ top_dir = raw_root
274
+ if args.resync:
275
+ clear_log()
276
+ multi2polywav.poly_all(top_dir)
277
+ scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
278
+ scanner.scan_media_and_build_devices_UID(synced_root=synced_root)
279
+ for m in scanner.found_media_files:
280
+ if m.device.tracks:
281
+ if not all([lv == None for lv in m.device.tracks.lag_values]):
282
+ logger.debug('%s has lag_values %s'%(
283
+ m.path, m.device.tracks.lag_values))
284
+ # any lag for a channel is specified by user in tracks.txt
285
+ process_lag_adjustement(m)
286
+ audio_REC_only = all([m.device.dev_type == 'REC' for m
287
+ in scanner.found_media_files])
288
+ # if audio_REC_only:
289
+ # if scanner.input_structure != 'ordered':
290
+ # print('For merging audio only, use a directory per device, quitting')
291
+ # sys.exit(1)
292
+ # print('\n\n\nOnly audio recordings are present')
293
+ # print('Which device should be the reference?\n')
294
+ # devices = scanner.get_devices()
295
+ # maxch = len(devices)
296
+ # for i, d in enumerate(devices):
297
+ # print('\t%i - %s'%(i+1, d.name))
298
+ # while True:
299
+ # print('\nEnter your choice:', end='')
300
+ # choice = input()
301
+ # try:
302
+ # choice = int(choice)
303
+ # except:
304
+ # print('Please use numeric digits.')
305
+ # continue
306
+ # if choice not in list(range(1, maxch + 1)):
307
+ # print('Please enter a number in [1..%i]'%maxch)
308
+ # continue
309
+ # break
310
+ # ref_device = list(devices)[choice - 1]
311
+ # # ref_device = list(devices)[3 - 1]
312
+ # print('When only audio recordings are present, ISOs files will be cut and written.')
313
+ if not args.terse:
314
+ if scanner.input_structure == 'ordered':
315
+ print('\nDetected structured folders')
316
+ # if scanner.top_dir_has_multicam:
317
+ # print(', multicam')
318
+ # else:
319
+ # print()
320
+ else:
321
+ print('\nDetected loose structure')
322
+ if scanner.CAM_numbers() > 1:
323
+ print('\nNote: different CAMs are present, will sync audio for each of them but if you want to set their')
324
+ print('respective timecode for NLE timeline alignement you should regroup clips by CAM under their own DIR.')
325
+ print('\nFound [gold1]%i[/gold1] media files '%(
326
+ len(scanner.found_media_files)), end='')
327
+ print('from [gold1]%i[/gold1] devices:\n'%(
328
+ scanner.get_devices_number()))
329
+ all_devices = scanner.get_devices()
330
+ for dev in all_devices:
331
+ dt = 'Camera' if dev.dev_type == 'CAM' else 'Recorder'
332
+ print('%s [gold1]%s[/gold1] with files:'%(dt, dev.name), end = ' ')
333
+ medias = scanner.get_media_for_device(dev)
334
+ for m in medias[:-1]: # last printed out of loop
335
+ print('[gold1]%s[/gold1]'%m.path.name, end=', ')
336
+ print('[gold1]%s[/gold1]'%medias[-1].path.name)
337
+ a_media = medias[0]
338
+ # check if all audio recorders have same sampling freq
339
+ freqs = [dev.sampling_freq for dev in all_devices if dev.dev_type == 'REC']
340
+ same = np.isclose(np.std(freqs),0)
341
+ logger.debug('sampling freqs from audio recorders %s, same:%s'%(freqs, same))
342
+ if not same:
343
+ print('some audio recorders have different sampling frequencies:')
344
+ print(freqs)
345
+ print('resulting in undefined results: quitting...')
346
+ quit()
347
+ print()
348
+ # recordings, rec_with_TTC = process_files(scanner.found_media_files, args)
349
+ recordings = [yaltc.Recording(m, do_plots=False) for m
350
+ in scanner.found_media_files]
351
+ recordings_with_time = [
352
+ rec
353
+ for rec in recordings
354
+ if rec.get_start_time()
355
+ ]
356
+ # if audio_REC_only:
357
+ # for rec in recordings:
358
+ # # print(rec, rec.device == ref_device)
359
+ # if rec.device == ref_device:
360
+ # rec.is_reference = True
361
+ if not args.terse:
362
+ table = Table(title="tictacsync results")
363
+ table.add_column("Recording\n", justify="center", style='gold1')
364
+ table.add_column("TTC chan\n (1st=#0)", justify="center", style='gold1')
365
+ # table.add_column("Device\n", justify="center", style='gold1')
366
+ table.add_column("UTC times\nstart:end", justify="center", style='gold1')
367
+ table.add_column("Clock drift\n(ppm)", justify="right", style='gold1')
368
+ # table.add_column("SN ratio\n(dB)", justify="center", style='gold1')
369
+ table.add_column("Date\n", justify="center", style='gold1')
370
+ rec_WO_time = [
371
+ rec.AVpath.name
372
+ for rec in recordings
373
+ if rec not in recordings_with_time]
374
+ if rec_WO_time:
375
+ print('No time found for: ',end='')
376
+ [print(rec, end=' ') for rec in rec_WO_time]
377
+ print('\n')
378
+ for r in recordings_with_time:
379
+ date = r.get_start_time().strftime("%y-%m-%d")
380
+ start_HHMMSS = r.get_start_time().strftime("%Hh%Mm%Ss")
381
+ end_MMSS = r.get_end_time().strftime("%Mm%Ss")
382
+ times_range = start_HHMMSS + ':' + end_MMSS
383
+ table.add_row(
384
+ str(r.AVpath.name),
385
+ str(r.TicTacCode_channel),
386
+ # r.device,
387
+ times_range,
388
+ # '%.6f'%(r.true_samplerate/1e3),
389
+ '%2i'%(r.get_samplerate_drift()),
390
+ # '%.0f'%r.decoder.SN_ratio,
391
+ date
392
+ )
393
+ console = Console()
394
+ console.print(table)
395
+ print()
396
+ n_devices = scanner.get_devices_number()
397
+ if len(recordings_with_time) < 2:
398
+ if not args.terse:
399
+ print('\nNothing to sync, exiting.\n')
400
+ sys.exit(1)
401
+ matcher = timeline.Matcher(recordings_with_time)
402
+ matcher.scan_audio_for_each_videoclip()
403
+ if not matcher.mergers:
404
+ if not args.terse:
405
+ print('\nNothing to sync, bye.\n')
406
+ sys.exit(1)
407
+ asked_ISOs = args.write_ISOs
408
+ if asked_ISOs and scanner.input_structure != 'ordered':
409
+ print('Warning, can\'t write ISOs without structured folders: [gold1]--isos[/gold1] option ignored.\n')
410
+ asked_ISOs = False
411
+ # output_dir = args.o
412
+ # if args.verbose_output or args.terse: # verbose, so no progress bars
413
+ print('Merging...')
414
+ for merger in matcher.mergers:
415
+ merger.build_audio_and_write_merged_media(top_dir,
416
+ False, # args.dont_write_cam_folder,
417
+ asked_ISOs,
418
+ audio_REC_only)
419
+ if not args.terse:
420
+ print("\n")
421
+ # find out where files were written
422
+ # a_merger = matcher.mergers[0]
423
+ # log file
424
+ p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/LOG_FILE
425
+ log_filehandle = open(p, 'a')
426
+ for merger in matcher.mergers:
427
+ print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
428
+ for audio in merger.get_matched_audio_recs():
429
+ print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
430
+ new_file = merger.videoclip.final_synced_file.parts
431
+ final_p = merger.videoclip.final_synced_file
432
+ nameAnd2Parents = Path('').joinpath(*final_p.parts[-2:])
433
+ print(' became [gold1]%s[/gold1]'%nameAnd2Parents)
434
+ # add full path to log file
435
+ log_filehandle.write(f'{merger.videoclip.AVpath}\n')
436
+ # matcher._build_otio_tracks_for_cam()
437
+ log_filehandle.close()
438
+ matcher.set_up_clusters() # multicam
439
+ matcher.shrink_gaps_between_takes(args.timelineoffset)
440
+ logger.debug('matcher.multicam_clips_clusters %s'%
441
+ pformat(matcher.multicam_clips_clusters))
442
+ # clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
443
+ # really_clusters is True if one of them has len() > 1
444
+ really_clusters = any([len(cl['vids']) > 1 for cl
445
+ in matcher.multicam_clips_clusters])
446
+ if really_clusters:
447
+ if scanner.input_structure == 'loose':
448
+ print('\nThere are synced multicam clips but without structured folders')
449
+ print('they were not grouped together under the same folder.')
450
+ else:
451
+ matcher.move_multicam_to_dir()
452
+ else:
453
+ logger.debug('not really a multicam cluster, nothing to move')
454
+ sys.exit(0)
455
+
456
+ if __name__ == '__main__':
457
+ main()
458
+
459
+
460
+
@@ -31,7 +31,7 @@ def print_grby(grby):
31
31
  def wav_recursive_scan(top_directory):
32
32
  files_lower_case = Path(top_directory).rglob('*.wav')
33
33
  files_upper_case = Path(top_directory).rglob('*.WAV')
34
- files = list(files_lower_case) + list(files_upper_case)
34
+ files = set(list(files_lower_case) + list(files_upper_case))
35
35
  paths = [
36
36
  p
37
37
  for p in files
@@ -97,7 +97,7 @@ def parse_and_check_arguments():
97
97
  parser.add_argument('-b',
98
98
  nargs=1,
99
99
  dest='both_audio_vid',
100
- help='Directory scanned for both audio and video')
100
+ help='Directory scanned for both audio and video, when tictacsync was used in "alongside mode"')
101
101
  parser.add_argument('--dry',
102
102
  action='store_true',
103
103
  dest='scan_only',
@@ -133,6 +133,9 @@ def parse_and_check_arguments():
133
133
  # parser.print_help(sys.stderr)
134
134
  # sys.exit(0)
135
135
  # list of singletons, so flatten. Keep None and False as is
136
+ if args.scan_only:
137
+ print('Sorry, --dry option not implemented yet, bye.')
138
+ sys.exit(0)
136
139
  return args
137
140
 
138
141
  def get_recent_mix(SND_dir, vid):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 1.0.0a0
3
+ Version: 1.0.2a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -58,7 +58,7 @@ Then pip install the syncing program:
58
58
  This should install python dependencies _and_ the `tictacsync` command.
59
59
  ## Usage
60
60
 
61
- Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (625 MB, sorry) unzip and run:
61
+ Download multiple sample files [here](https://nuage.lutz.quebec/s/D4DgzHrLaAjqNmY) (625 MB, sorry) unzip and run:
62
62
 
63
63
  > tictacsync dailies/loose
64
64
  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:
@@ -4,6 +4,7 @@ setup.py
4
4
  tictacsync/__init__.py
5
5
  tictacsync/device_scanner.py
6
6
  tictacsync/entry.py
7
+ tictacsync/mamsync.py
7
8
  tictacsync/multi2polywav.py
8
9
  tictacsync/newmix.py
9
10
  tictacsync/remergemix.py
File without changes
File without changes