tictacsync 0.97a0__tar.gz → 0.99a0__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.
- {tictacsync-0.97a0/tictacsync.egg-info → tictacsync-0.99a0}/PKG-INFO +3 -3
- {tictacsync-0.97a0 → tictacsync-0.99a0}/README.md +2 -2
- {tictacsync-0.97a0 → tictacsync-0.99a0}/setup.py +2 -1
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/device_scanner.py +203 -107
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/entry.py +177 -75
- tictacsync-0.99a0/tictacsync/remrgmx.py +116 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/timeline.py +194 -103
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/yaltc.py +17 -20
- {tictacsync-0.97a0 → tictacsync-0.99a0/tictacsync.egg-info}/PKG-INFO +3 -3
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/requires.txt +1 -0
- tictacsync-0.97a0/tictacsync/remrgmx.py +0 -38
- {tictacsync-0.97a0 → tictacsync-0.99a0}/LICENSE +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/setup.cfg +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/__init__.py +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/multi2polywav.py +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync/remergemix.py +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/SOURCES.txt +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/entry_points.txt +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-0.97a0 → tictacsync-0.99a0}/tictacsync.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -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/
|
|
61
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (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:
|
|
@@ -87,7 +87,7 @@ For a one line output (or to suppress the progress bars) use the `--terse` flag:
|
|
|
87
87
|
|
|
88
88
|
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/).
|
|
89
89
|
|
|
90
|
-
> tictacsync --isos dailies/
|
|
90
|
+
> tictacsync --isos dailies/structured
|
|
91
91
|
#### `-p`
|
|
92
92
|
|
|
93
93
|
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:
|
|
@@ -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/
|
|
38
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/P3gbZR4GgGy8xQp/download/dailies1_1.zip) (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:
|
|
@@ -64,7 +64,7 @@ For a one line output (or to suppress the progress bars) use the `--terse` flag:
|
|
|
64
64
|
|
|
65
65
|
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/).
|
|
66
66
|
|
|
67
|
-
> tictacsync --isos dailies/
|
|
67
|
+
> tictacsync --isos dailies/structured
|
|
68
68
|
#### `-p`
|
|
69
69
|
|
|
70
70
|
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:
|
|
@@ -23,6 +23,7 @@ setup(
|
|
|
23
23
|
'lmfit',
|
|
24
24
|
'scikit-image',
|
|
25
25
|
'scipy>=1.10.1',
|
|
26
|
+
'platformdirs',
|
|
26
27
|
],
|
|
27
28
|
python_requires='>=3.10',
|
|
28
29
|
entry_points = {
|
|
@@ -32,7 +33,7 @@ setup(
|
|
|
32
33
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
33
34
|
]
|
|
34
35
|
},
|
|
35
|
-
version = '0.
|
|
36
|
+
version = '0.99a',
|
|
36
37
|
description = "command for syncing audio video recordings",
|
|
37
38
|
long_description_content_type='text/markdown',
|
|
38
39
|
long_description = long_descr,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
TRACKSFN = 'tracks.txt'
|
|
7
7
|
SILENT_TRACK_TOKENS = '-0n'
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
av_file_extensions = \
|
|
11
10
|
"""webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
|
|
12
11
|
wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
|
|
@@ -40,6 +39,8 @@ try:
|
|
|
40
39
|
except:
|
|
41
40
|
import multi2polywav
|
|
42
41
|
|
|
42
|
+
MCCDIR = 'SyncedMulticamClips'
|
|
43
|
+
SYNCEDFOLDER = 'SyncedMedia'
|
|
43
44
|
|
|
44
45
|
# utility for accessing pathnames
|
|
45
46
|
def _pathname(tempfile_or_path):
|
|
@@ -73,7 +74,7 @@ class Tracks:
|
|
|
73
74
|
@dataclass
|
|
74
75
|
class Device:
|
|
75
76
|
UID: int
|
|
76
|
-
folder: Path
|
|
77
|
+
folder: Path # media's parent folder
|
|
77
78
|
name: str
|
|
78
79
|
dev_type: str # CAM or REC
|
|
79
80
|
n_chan: int
|
|
@@ -97,7 +98,7 @@ def media_at_path(input_structure, p):
|
|
|
97
98
|
dev_UID, dt, sf = get_device_ffprobe_UID(p)
|
|
98
99
|
dev_name = None
|
|
99
100
|
logger.debug('ffprobe dev_UID:%s dt:%s sf:%s'%(dev_UID, dt,sf))
|
|
100
|
-
if input_structure == '
|
|
101
|
+
if input_structure == 'ordered':
|
|
101
102
|
dev_name = p.parent.name
|
|
102
103
|
if dev_UID is None:
|
|
103
104
|
dev_UID = hash(dev_name)
|
|
@@ -132,7 +133,7 @@ def get_device_ffprobe_UID(file):
|
|
|
132
133
|
"""
|
|
133
134
|
Tries to find an unique hash integer identifying the device that produced
|
|
134
135
|
the file based on the string inside ffprobe metadata without any
|
|
135
|
-
reference to date
|
|
136
|
+
reference to date, location, length or time. Find out with ffprobe the type
|
|
136
137
|
of device: CAM or REC for videocamera or audio recorder.
|
|
137
138
|
|
|
138
139
|
Device UIDs are used later in Montage._get_concatenated_audiofile_for()
|
|
@@ -192,7 +193,7 @@ def get_device_ffprobe_UID(file):
|
|
|
192
193
|
class Scanner:
|
|
193
194
|
"""
|
|
194
195
|
Class that encapsulates scanning of the directory given as CLI argument.
|
|
195
|
-
Depending on the input_structure detected (loose|
|
|
196
|
+
Depending on the input_structure detected (loose|ordered), enforce
|
|
196
197
|
some directory structure (or not). Build a list of media files found and a
|
|
197
198
|
try to indentify uniquely the device used to record each media file.
|
|
198
199
|
|
|
@@ -202,15 +203,15 @@ class Scanner:
|
|
|
202
203
|
Any of:
|
|
203
204
|
'loose'
|
|
204
205
|
all files audio + video are in top folder
|
|
205
|
-
'
|
|
206
|
+
'ordered'
|
|
206
207
|
eg for multicam on Davinci Resolve
|
|
207
208
|
input_structure is set in scan_media_and_build_devices_UID()
|
|
208
209
|
|
|
209
210
|
top_directory : string
|
|
210
211
|
String of path where to start searching for media files.
|
|
211
212
|
|
|
212
|
-
top_dir_has_multicam : bool
|
|
213
|
-
|
|
213
|
+
# top_dir_has_multicam : bool
|
|
214
|
+
# If top dir is folder structures AND more than on cam
|
|
214
215
|
|
|
215
216
|
found_media_files: list of Media objects
|
|
216
217
|
"""
|
|
@@ -243,6 +244,15 @@ class Scanner:
|
|
|
243
244
|
CAMs = [d for d in devices if d.dev_type == 'CAM']
|
|
244
245
|
return len(set(CAMs))
|
|
245
246
|
|
|
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
|
+
|
|
246
256
|
def scan_media_and_build_devices_UID(self, recursive=True):
|
|
247
257
|
"""
|
|
248
258
|
Scans Scanner.top_directory recursively for files with known audio-video
|
|
@@ -256,89 +266,149 @@ class Scanner:
|
|
|
256
266
|
|
|
257
267
|
Populates Scanner.found_media_files, a list of Media objects
|
|
258
268
|
|
|
259
|
-
Sets Scanner.input_structure = 'loose'|'
|
|
269
|
+
Sets Scanner.input_structure = 'loose'|'ordered'
|
|
260
270
|
|
|
261
271
|
"""
|
|
262
272
|
files = Path(self.top_directory).rglob('*.*')
|
|
273
|
+
def _last4letters(part):
|
|
274
|
+
return
|
|
275
|
+
|
|
263
276
|
paths = [
|
|
264
277
|
p
|
|
265
278
|
for p in files
|
|
266
279
|
if p.suffix[1:] in av_file_extensions
|
|
267
|
-
and
|
|
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
|
|
268
283
|
]
|
|
269
284
|
logger.debug('found media files %s'%paths)
|
|
270
285
|
parents = [p.parent for p in paths]
|
|
271
|
-
logger.debug('found parents %s'%parents)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if all_parents_are_the_same:
|
|
286
|
+
logger.debug('found parents %s'%pformat(parents))
|
|
287
|
+
# True if all elements are identical
|
|
288
|
+
AV_files_have_same_parent = parents.count(parents[0]) == len(parents)
|
|
289
|
+
logger.debug('AV_files_have_same_parent %s'%AV_files_have_same_parent)
|
|
290
|
+
if AV_files_have_same_parent:
|
|
277
291
|
# all media (video + audio) are in a same folder, so this is loose
|
|
278
292
|
self.input_structure = 'loose'
|
|
279
293
|
# for now (TO DO?) 'loose' == no multi-cam
|
|
280
|
-
self.top_dir_has_multicam = False
|
|
294
|
+
# self.top_dir_has_multicam = False
|
|
281
295
|
else:
|
|
282
|
-
# check later if inside each folder, media have same device
|
|
283
|
-
# for now, we'll guess structure is '
|
|
284
|
-
self.input_structure = '
|
|
296
|
+
# check later if inside each folder, media have same device
|
|
297
|
+
# for now, we'll guess structure is 'ordered'
|
|
298
|
+
self.input_structure = 'ordered'
|
|
285
299
|
for p in paths:
|
|
286
300
|
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
287
301
|
self.found_media_files.append(new_media)
|
|
288
|
-
|
|
302
|
+
# for non UIDed try building UID from filenam
|
|
303
|
+
def _try_name_from_files(medias):
|
|
289
304
|
# return common first strings in filename
|
|
305
|
+
def _all_identical(a_list):
|
|
306
|
+
return a_list.count(a_list[0]) == len(a_list)
|
|
290
307
|
names = [m.path.name for m in medias]
|
|
291
308
|
transposed_names = list(map(list, zip(*names)))
|
|
292
|
-
same = list(map(
|
|
309
|
+
same = list(map(_all_identical, transposed_names))
|
|
293
310
|
try:
|
|
294
311
|
first_diff = same.index(False)
|
|
295
312
|
except:
|
|
296
313
|
return names[0].split('.')[0]
|
|
297
314
|
return names[0][:first_diff]
|
|
298
|
-
|
|
315
|
+
no_device_UID_medias = [m for m in self.found_media_files
|
|
299
316
|
if not m.device.UID]
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
logger.debug('those media have no device UID %s'%no_device_UID_medias)
|
|
318
|
+
if no_device_UID_medias:
|
|
319
|
+
# will guess a device name from media filenames
|
|
320
|
+
logger.debug('no_device_UID_medias %s'%no_device_UID_medias)
|
|
321
|
+
start_string = _try_name_from_files(no_device_UID_medias)
|
|
303
322
|
if len(start_string) < 2:
|
|
304
323
|
print('\nError, cant identify the device for those files:')
|
|
305
|
-
[print('%s, '%m.path.name, end='') for m in
|
|
324
|
+
[print('%s, '%m.path.name, end='') for m in no_device_UID_medias]
|
|
306
325
|
print('\n')
|
|
307
326
|
sys.exit(1)
|
|
308
|
-
one_device =
|
|
327
|
+
one_device = no_device_UID_medias[0].device
|
|
309
328
|
one_device.name = start_string
|
|
310
329
|
if not one_device.UID:
|
|
311
330
|
one_device.UID = hash(start_string)
|
|
312
331
|
print('\nWarning, guessing a device ID for those files:')
|
|
313
332
|
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
314
|
-
in
|
|
333
|
+
in no_device_UID_medias]
|
|
315
334
|
print('UID: [gold1]%s[/gold1]'%start_string)
|
|
316
|
-
for m in
|
|
335
|
+
for m in no_device_UID_medias:
|
|
317
336
|
m.device = one_device
|
|
318
337
|
logger.debug('new device added %s'%self.found_media_files)
|
|
319
|
-
logger.debug('Scanner.found_media_files = %s'%self.found_media_files)
|
|
320
|
-
if self.input_structure == '
|
|
321
|
-
self.
|
|
338
|
+
logger.debug('Scanner.found_media_files = %s'%pformat(self.found_media_files))
|
|
339
|
+
if self.input_structure == 'ordered':
|
|
340
|
+
self._confirm_folders_have_same_device()
|
|
322
341
|
# self._use_folder_as_device_name()
|
|
323
342
|
devices = set([m.device for m in self.found_media_files])
|
|
324
343
|
audio_devices = [d for d in devices if d.dev_type == 'REC']
|
|
325
344
|
for recorder in audio_devices:
|
|
345
|
+
# process tracks.txt for audio recorders
|
|
326
346
|
recorder.tracks = self._get_tracks_from_file(recorder)
|
|
347
|
+
# logging only:
|
|
327
348
|
if recorder.tracks:
|
|
328
349
|
if not all([lv == None for lv in recorder.tracks.lag_values]):
|
|
329
350
|
logger.debug('%s has lag_values %s'%(
|
|
330
351
|
recorder.name, recorder.tracks.lag_values))
|
|
352
|
+
# check if device is in fact two parents up (and parent = ROLLxx):
|
|
353
|
+
# Group media by folder 2up and verify all media for each
|
|
354
|
+
# group have same device.
|
|
355
|
+
folder2up = lambda m: m.path.parent.parent
|
|
356
|
+
# logger.debug('folder2up: %s'%pformat([folder2up(m) for m
|
|
357
|
+
# in self.found_media_files]))
|
|
358
|
+
medias = sorted(self.found_media_files, key=folder2up)
|
|
359
|
+
# build lists for multiple reference of iterators
|
|
360
|
+
media_grouped_by_folder2up = [ (k, list(iterator)) for k, iterator
|
|
361
|
+
in groupby(medias, folder2up)]
|
|
362
|
+
logger.debug('media_grouped_by_folder2up: %s'%pformat(
|
|
363
|
+
media_grouped_by_folder2up))
|
|
364
|
+
folder_and_UIDs = [(f, [m.device.UID for m in medias])
|
|
365
|
+
for f, medias in media_grouped_by_folder2up]
|
|
366
|
+
logger.debug('devices: %s'%pformat(folder_and_UIDs))
|
|
367
|
+
def _multiple_and_same(a_list):
|
|
368
|
+
same = a_list.count(a_list[0]) == len(a_list)
|
|
369
|
+
return len(a_list) > 1 and same
|
|
370
|
+
folders_with_same_dev = [(f.name, UIDs[0]) for f, UIDs
|
|
371
|
+
in folder_and_UIDs
|
|
372
|
+
if _multiple_and_same(UIDs)]
|
|
373
|
+
logger.debug('folders_with_same_dev: %s'%pformat(folders_with_same_dev))
|
|
374
|
+
for name, UID in folders_with_same_dev:
|
|
375
|
+
for m in self.found_media_files:
|
|
376
|
+
if m.device.UID == UID:
|
|
377
|
+
m.device.name = name
|
|
378
|
+
# logger.debug('renamed device media: %s'%pformat(self.found_media_files))
|
|
331
379
|
no_name_devices = [m.device for m in self.found_media_files
|
|
332
|
-
|
|
380
|
+
if not m.device.name]
|
|
381
|
+
# possible if self.input_structure == 'loose'
|
|
382
|
+
def _try_name_from_metadata(media):
|
|
383
|
+
# search model and make from fprobe
|
|
384
|
+
file = Path(media.path)
|
|
385
|
+
logger.debug('trying to find maker model for %s'%file)
|
|
386
|
+
try:
|
|
387
|
+
probe = ffmpeg.probe(file)
|
|
388
|
+
except ffmpeg.Error as e:
|
|
389
|
+
print('ffmpeg.probe error')
|
|
390
|
+
print(e.stderr, file)
|
|
391
|
+
return None, None #-----------------------------------------------------
|
|
392
|
+
# fall back to folder name
|
|
393
|
+
logger.debug('ffprobe %s'%pformat(probe))
|
|
394
|
+
# [TO BE COMPLETED]
|
|
395
|
+
# could reside in ['format','tags','com.apple.quicktime.model'],
|
|
396
|
+
# or ['format','tags','model'],
|
|
397
|
+
# or ['streams'][0]['tags']['vendor_id']) :-(
|
|
398
|
+
|
|
333
399
|
for anon_dev in no_name_devices:
|
|
334
400
|
medias = self.get_media_for_device(anon_dev)
|
|
335
|
-
guess_name =
|
|
401
|
+
guess_name = _try_name_from_files(medias)
|
|
336
402
|
# print('dev %s has no name, guessing %s'%(anon_dev, guess_name))
|
|
337
403
|
logger.debug('dev %s has no name, guessing %s'%(anon_dev, guess_name))
|
|
338
404
|
anon_dev.name = guess_name
|
|
339
405
|
pprint_found_media_files = pformat(self.found_media_files)
|
|
340
406
|
logger.debug('scanner.found_media_files = %s'%pprint_found_media_files)
|
|
341
407
|
logger.debug('all devices %s'%[m.device for m in self.found_media_files])
|
|
408
|
+
dev_is_REC = [m.device.dev_type == 'REC' for m in self.found_media_files]
|
|
409
|
+
if not any(dev_is_REC): # no audio recordings!
|
|
410
|
+
print('\rNo audio recording found, nothing to sync, bye.')
|
|
411
|
+
sys.exit(0)
|
|
342
412
|
# print('devices 312 %s'%set([m.device for m in self.found_media_files]))
|
|
343
413
|
|
|
344
414
|
def _get_tracks_from_file(self, device) -> Tracks:
|
|
@@ -384,60 +454,69 @@ class Scanner:
|
|
|
384
454
|
logger.debug('no tracks.txt file found')
|
|
385
455
|
return None
|
|
386
456
|
|
|
387
|
-
def
|
|
457
|
+
def _confirm_folders_have_same_device(self):
|
|
388
458
|
"""
|
|
389
|
-
Since input_structure == '
|
|
459
|
+
Since input_structure == 'ordered',
|
|
390
460
|
checks for files in self.found_media_files for structure as following.
|
|
391
461
|
|
|
392
462
|
Warns user and quit program for:
|
|
393
463
|
A- folders with mix of video and audio
|
|
394
464
|
B- folders with mix of uniquely identified devices and unUIDied ones
|
|
395
|
-
C- folders with mixed audio
|
|
465
|
+
C- folders with mixed audio an video files
|
|
396
466
|
|
|
397
467
|
Warns user but proceeds for:
|
|
398
468
|
D- folder with only unUIDied files (overlaps will be check later)
|
|
399
469
|
|
|
470
|
+
Changes self.input_structure to 'loose' if a folder contains files
|
|
471
|
+
from different devices.
|
|
472
|
+
|
|
400
473
|
Proceeds silently if
|
|
401
474
|
E- all files in the folder are from the same device
|
|
402
475
|
|
|
403
476
|
Returns nothing
|
|
404
477
|
"""
|
|
405
|
-
def
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
478
|
+
def _exit_on_folder_name_clash():
|
|
479
|
+
# Check media parent folders are unique
|
|
480
|
+
# returns media_grouped_by_folder
|
|
481
|
+
def _list_duplicates(seq):
|
|
482
|
+
seen = set()
|
|
483
|
+
seen_add = seen.add
|
|
484
|
+
# adds all elements it doesn't know yet to seen and all other to seen_twice
|
|
485
|
+
seen_twice = set( x for x in seq if x in seen or seen_add(x) )
|
|
486
|
+
# turn the set into a list (as requested)
|
|
487
|
+
return list( seen_twice )
|
|
488
|
+
folder_key = lambda m: m.path.parent
|
|
489
|
+
medias = sorted(self.found_media_files, key=folder_key)
|
|
490
|
+
# build lists for multiple reference of iterators
|
|
491
|
+
media_grouped_by_folder = [ (k, list(iterator)) for k, iterator
|
|
492
|
+
in groupby(medias, folder_key)]
|
|
493
|
+
logger.debug('media_grouped_by_folder %s'%pformat(
|
|
494
|
+
media_grouped_by_folder))
|
|
495
|
+
complete_path_folders = [e[0] for e in media_grouped_by_folder]
|
|
496
|
+
name_of_folders = [p.name for p in complete_path_folders]
|
|
497
|
+
logger.debug('complete_path_folders with media files %s'%
|
|
498
|
+
complete_path_folders)
|
|
499
|
+
logger.debug('name_of_folders with media files %s'%name_of_folders)
|
|
500
|
+
# unique_folder_names = set(name_of_folders) [TODO] is this useful ?
|
|
501
|
+
# repeated_folders = _list_duplicates(name_of_folders)
|
|
502
|
+
# logger.debug('repeated_folders %s'%repeated_folders)
|
|
503
|
+
# if repeated_folders:
|
|
504
|
+
# print('There are conflicts for some repeated folder names:')
|
|
505
|
+
# for f in [str(p) for p in repeated_folders]:
|
|
506
|
+
# print(' [gold1]%s[/gold1]'%f)
|
|
507
|
+
# print('Here are the complete paths:')
|
|
508
|
+
# for f in [str(p) for p in complete_path_folders]:
|
|
509
|
+
# print(' [gold1]%s[/gold1]'%f)
|
|
510
|
+
# print('please rename and rerun. Quitting..')
|
|
511
|
+
# sys.exit(1) ####################################################
|
|
512
|
+
return media_grouped_by_folder
|
|
513
|
+
media_grouped_by_folder = _exit_on_folder_name_clash()
|
|
435
514
|
n_CAM_folder = 0
|
|
436
515
|
for folder, list_of_medias_in_folder in media_grouped_by_folder:
|
|
437
516
|
# check all medias are either video or audio recordings in folder
|
|
438
517
|
# if not, warn user and quit.
|
|
439
518
|
dev_types = set([m.device.dev_type for m in list_of_medias_in_folder])
|
|
440
|
-
logger.debug('dev_types %s'%dev_types)
|
|
519
|
+
logger.debug('dev_types for folder%s: %s'%(folder,dev_types))
|
|
441
520
|
if dev_types == {'CAM'}:
|
|
442
521
|
n_CAM_folder += 1
|
|
443
522
|
if len(dev_types) != 1:
|
|
@@ -446,14 +525,14 @@ class Scanner:
|
|
|
446
525
|
[print('[gold1]%s[/gold1]'%m.path.name, end =', ')
|
|
447
526
|
for m in list_of_medias_in_folder]
|
|
448
527
|
print('\nplease move them in exclusive folders and rerun.\n')
|
|
449
|
-
sys.exit(1)
|
|
528
|
+
sys.exit(1) ######################################################
|
|
450
529
|
unidentified = [m for m in list_of_medias_in_folder
|
|
451
530
|
if m.device.UID == None]
|
|
452
531
|
UIDed = [m for m in list_of_medias_in_folder
|
|
453
532
|
if m.device.UID != None]
|
|
454
533
|
logger.debug('devices in folder %s:'%folder)
|
|
455
|
-
logger.debug(' media with unknown devices %s'%unidentified)
|
|
456
|
-
logger.debug(' media with UIDed devices %s'%UIDed)
|
|
534
|
+
logger.debug(' media with unknown devices %s'%pformat(unidentified))
|
|
535
|
+
logger.debug(' media with UIDed devices %s'%pformat(UIDed))
|
|
457
536
|
if len(unidentified) != 0 and len(UIDed) != 0:
|
|
458
537
|
print('\nProblem while grouping files in [gold1]%s[/gold1]:'%folder)
|
|
459
538
|
print('There is a mix of unidentifiable and identified devices.')
|
|
@@ -466,16 +545,38 @@ class Scanner:
|
|
|
466
545
|
elif answer.upper() in ["N", "NO"]:
|
|
467
546
|
# Do action you need
|
|
468
547
|
print('please move the following files in a folder named appropriately:\n')
|
|
469
|
-
sys.exit(1)
|
|
548
|
+
sys.exit(1) ################################################
|
|
470
549
|
# if, in a folder, there's a mix of different identified devices,
|
|
471
550
|
# Warn user and quit.
|
|
551
|
+
UIDs = [m.device.UID for m in UIDed]
|
|
552
|
+
all_same_device = UIDs.count(UIDs[0]) == len(UIDs)
|
|
553
|
+
logger.debug('UIDs in %s: %s. all_same_device %s'%(folder,
|
|
554
|
+
pformat(UIDs), all_same_device))
|
|
555
|
+
if not all_same_device:
|
|
556
|
+
self.input_structure = 'loose'
|
|
557
|
+
# self.top_dir_has_multicam = False
|
|
558
|
+
logger.debug('changed input_structure to loose')
|
|
559
|
+
# device name should be generated (it isn't the folder name...)
|
|
560
|
+
distinct_UIDS = set(UIDs)
|
|
561
|
+
n_UIDs = len(distinct_UIDS)
|
|
562
|
+
logger.debug('There are %i UIDs: %s'%(n_UIDs, distinct_UIDS))
|
|
563
|
+
# Buid CAM01, CAM02 or REC01, REC02.
|
|
564
|
+
# Get dev type from first media in list
|
|
565
|
+
devT = UIDed[0].device.dev_type # 'CAM' or 'REC'
|
|
566
|
+
generic_names = [devT + str(i).zfill(2) for i in range(n_UIDs)]
|
|
567
|
+
devUIDs_names = dict(zip(distinct_UIDS, generic_names))
|
|
568
|
+
logger.debug('devUIDs_names %s'%pformat(devUIDs_names))
|
|
569
|
+
# rename
|
|
570
|
+
for m in UIDed:
|
|
571
|
+
m.device.name = devUIDs_names[m.device.UID]
|
|
572
|
+
logger.debug('new name %s'%m.device.name)
|
|
472
573
|
if len(dev_types) != 1:
|
|
473
574
|
print('\nProblem while scanning for media files. In [gold1]%s[/gold1]:'%folder)
|
|
474
575
|
print('There is a mix of files from different devices:')
|
|
475
576
|
[print('[gold1]%s[/gold1]'%m.path.name, end =', ')
|
|
476
577
|
for m in list_of_medias_in_folder]
|
|
477
578
|
print('\nplease move them in exclusive folders and rerun.\n')
|
|
478
|
-
sys.exit(1)
|
|
579
|
+
sys.exit(1) ####################################################
|
|
479
580
|
if len(unidentified) == len(list_of_medias_in_folder):
|
|
480
581
|
# all unidentified
|
|
481
582
|
if len(unidentified) > 1:
|
|
@@ -487,11 +588,6 @@ class Scanner:
|
|
|
487
588
|
# all files in folder are from unidentified device or
|
|
488
589
|
# all files in folder are from the same identified device
|
|
489
590
|
logger.debug('n_CAM_folder %i'%n_CAM_folder)
|
|
490
|
-
if n_CAM_folder > 1 :
|
|
491
|
-
self.top_dir_has_multicam = True
|
|
492
|
-
else:
|
|
493
|
-
self.top_dir_has_multicam = False
|
|
494
|
-
logger.debug('top_dir_has_multicam: %s'%self.top_dir_has_multicam)
|
|
495
591
|
return
|
|
496
592
|
|
|
497
593
|
def _parse_track_values(self, tracks_file) -> Tracks:
|
|
@@ -499,16 +595,16 @@ class Scanner:
|
|
|
499
595
|
read track names for naming separated ISOs
|
|
500
596
|
from tracks_file.
|
|
501
597
|
|
|
502
|
-
tokens looked for: mix; L mix
|
|
598
|
+
tokens looked for: mix; mix L; mix R; 0 and TC
|
|
503
599
|
|
|
504
600
|
repeating "mic*" pattern signals a stereo track
|
|
505
601
|
and entries will correspondingly panned into
|
|
506
|
-
a stero mix named
|
|
602
|
+
a stero mix named mixL.wav and mixL.wav
|
|
507
603
|
|
|
508
|
-
L
|
|
509
|
-
R
|
|
510
|
-
L
|
|
511
|
-
R
|
|
604
|
+
mic L # spaces are ignored |
|
|
605
|
+
mic R | stereo pair
|
|
606
|
+
micB L
|
|
607
|
+
micB R
|
|
512
608
|
|
|
513
609
|
Returns: a Tracks instance:
|
|
514
610
|
# track numbers start at 1 for first track (as needed by sox)
|
|
@@ -523,22 +619,22 @@ class Scanner:
|
|
|
523
619
|
unused=[],
|
|
524
620
|
stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
|
|
525
621
|
mix=[], others=[('clics', 1)],
|
|
526
|
-
rawtrx=['clics', 'TC', '
|
|
622
|
+
rawtrx=['clics', 'TC', 'micL', 'micR', 'mic2L;1000', 'mic2R;1000', 'mixL', 'mixR'],
|
|
527
623
|
error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
|
|
528
624
|
"""
|
|
529
625
|
def _WOspace(chaine):
|
|
530
626
|
ch = [c for c in chaine if c != ' ']
|
|
531
627
|
return ''.join(ch)
|
|
532
|
-
def _WO_LR(chaine):
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def _seemsStereoMic(tag):
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
628
|
+
# def _WO_LR(chaine):
|
|
629
|
+
# ch = [c for c in chaine if c not in 'LR']
|
|
630
|
+
# return ''.join(ch)
|
|
631
|
+
# def _seemsStereoMic(tag):
|
|
632
|
+
# # is tag likely a stereo pair tag?
|
|
633
|
+
# # should starts with 'mic' and ends with 'l' or 'r'
|
|
634
|
+
# return tag[1:4]=='mic' and tag[0] in 'lr'
|
|
539
635
|
file=open(tracks_file,"r")
|
|
540
636
|
whole_txt = file.read()
|
|
541
|
-
logger.debug('all_lines:\n%s'%whole_txt)
|
|
637
|
+
logger.debug('file %s all_lines:\n%s'%(tracks_file, whole_txt))
|
|
542
638
|
tracks_lines_wspaces = [l.split('#')[0] for l in whole_txt.splitlines()
|
|
543
639
|
if len(l) > 0 ]
|
|
544
640
|
tracks_lines = [_WOspace(l) for l in tracks_lines_wspaces if len(l) > 0 ]
|
|
@@ -552,7 +648,7 @@ class Scanner:
|
|
|
552
648
|
splt += [None]
|
|
553
649
|
if len(splt) != 2:
|
|
554
650
|
# error
|
|
555
|
-
print('
|
|
651
|
+
print('\nText error in %s, line %s has too many ";"'%(
|
|
556
652
|
tracks_file, line))
|
|
557
653
|
return splt
|
|
558
654
|
tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
|
|
@@ -563,11 +659,10 @@ class Scanner:
|
|
|
563
659
|
logger.debug('tracks_lines lower case: %s'%tracks_lines)
|
|
564
660
|
# print(lag_values)
|
|
565
661
|
logger.debug('lag_values: %s'%lag_values)
|
|
566
|
-
tagsWOl_r = [e[1
|
|
567
|
-
logger.debug('tags WO
|
|
662
|
+
tagsWOl_r = [e[:-1] for e in tracks_lines] # skip last letter
|
|
663
|
+
logger.debug('tags WO LR letter %s'%tagsWOl_r)
|
|
568
664
|
# find idx of start of pairs
|
|
569
|
-
# ['clics', 'TC', '
|
|
570
|
-
# ^ ^ ^
|
|
665
|
+
# ['clics', 'TC', 'micL', 'micR', 'mic2L', 'mic2R', 'mixL', 'mixR']
|
|
571
666
|
def _micOrmix(a,b):
|
|
572
667
|
# test if same and mic mic or mix mix
|
|
573
668
|
if len(a) == 0:
|
|
@@ -577,9 +672,10 @@ class Scanner:
|
|
|
577
672
|
in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
|
|
578
673
|
logger.debug('pair_idx_start %s'%pair_idx_start)
|
|
579
674
|
def LR_OK(idx):
|
|
580
|
-
# in tracks_lines, check if idx
|
|
581
|
-
|
|
582
|
-
|
|
675
|
+
# in tracks_lines, check if idx ends a LR pair
|
|
676
|
+
# delays, if any, have been removed
|
|
677
|
+
a = tracks_lines[idx][-1]
|
|
678
|
+
b = tracks_lines[idx+1][-1]
|
|
583
679
|
return a+b in ['lr', 'rl']
|
|
584
680
|
LR_OKs = [LR_OK(p) for p in pair_idx_start]
|
|
585
681
|
logger.debug('LR_OKs %s'%LR_OKs)
|
|
@@ -625,11 +721,11 @@ class Scanner:
|
|
|
625
721
|
def _findLR(i_first):
|
|
626
722
|
# returns L R indexes (+1 for sox non zero based indexing)
|
|
627
723
|
i_2nd = i_first + 1
|
|
628
|
-
a = tracks_lines[i_first][
|
|
629
|
-
b = tracks_lines[i_2nd][
|
|
724
|
+
a = tracks_lines[i_first][-1] # l|r at end
|
|
725
|
+
b = tracks_lines[i_2nd][-1] # l|r at end
|
|
630
726
|
if a == 'l':
|
|
631
727
|
if b == 'r':
|
|
632
|
-
# sequence is
|
|
728
|
+
# sequence is mixL mixR
|
|
633
729
|
return i_first+1, i_2nd+1
|
|
634
730
|
else:
|
|
635
731
|
print('\nError in %s'%tracks_file)
|
|
@@ -638,7 +734,7 @@ class Scanner:
|
|
|
638
734
|
quit()
|
|
639
735
|
elif a == 'r':
|
|
640
736
|
if b == 'l':
|
|
641
|
-
# sequence is
|
|
737
|
+
# sequence is mixR mixL
|
|
642
738
|
return i_2nd+1, i_first+1
|
|
643
739
|
else:
|
|
644
740
|
print('\nError in %s'%tracks_file)
|
|
@@ -666,7 +762,7 @@ class Scanner:
|
|
|
666
762
|
# those are stereo pairs
|
|
667
763
|
stereo_pairs = []
|
|
668
764
|
for first_in_pair in pair_idx_start:
|
|
669
|
-
suffix = tracks_lines[first_in_pair][1
|
|
765
|
+
suffix = tracks_lines[first_in_pair][:-1]
|
|
670
766
|
stereo_pairs.append((suffix, _findLR(first_in_pair)))
|
|
671
767
|
logger.debug('stereo_pairs %s'%stereo_pairs)
|
|
672
768
|
output_tracks.stereomics = stereo_pairs
|