tictacsync 0.95a0__tar.gz → 0.97a0__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.95a0/tictacsync.egg-info → tictacsync-0.97a0}/PKG-INFO +7 -4
- {tictacsync-0.95a0 → tictacsync-0.97a0}/README.md +6 -3
- {tictacsync-0.95a0 → tictacsync-0.97a0}/setup.py +1 -1
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/device_scanner.py +152 -134
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/entry.py +67 -18
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/multi2polywav.py +2 -2
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/remergemix.py +4 -2
- tictacsync-0.97a0/tictacsync/remrgmx.py +38 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/timeline.py +172 -30
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/yaltc.py +9 -12
- {tictacsync-0.95a0 → tictacsync-0.97a0/tictacsync.egg-info}/PKG-INFO +7 -4
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync.egg-info/SOURCES.txt +1 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/LICENSE +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/setup.cfg +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync/__init__.py +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync.egg-info/entry_points.txt +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/tictacsync.egg-info/requires.txt +0 -0
- {tictacsync-0.95a0 → tictacsync-0.97a0}/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.97a0
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -73,19 +73,22 @@ The program `tictacsync` will recursively scan the directory given as argument,
|
|
|
73
73
|
If shooting multicam, put clips in their respective directories (using the camera name as folder name) _and_ the audio under their own directory. `tictacsync` will detect that structured input and will generate multicam folders ready to be imported into your NLE (for now only DaVinci Resolve has been validated).
|
|
74
74
|
|
|
75
75
|
## Options
|
|
76
|
+
#### `-v`
|
|
76
77
|
|
|
77
78
|
For a very verbose output add the `-v` flag:
|
|
78
79
|
|
|
79
80
|
> tictacsync -v dailies/loose/MVI_0024.MP4
|
|
80
|
-
|
|
81
|
+
#### `--terse`
|
|
81
82
|
For a one line output (or to suppress the progress bars) use the `--terse` flag:
|
|
82
83
|
|
|
83
84
|
> tictacsync --terse dailies/loose/MVI_0024.MP4
|
|
84
85
|
dailies/loose/MVI_0024.MP4 UTC:2024-03-12 23:07:01.4281 pulse: 27450 in chan 0
|
|
86
|
+
#### `--isos`
|
|
85
87
|
|
|
86
|
-
|
|
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/).
|
|
87
89
|
|
|
88
|
-
> tictacsync --isos dailies/
|
|
90
|
+
> tictacsync --isos dailies/day01
|
|
91
|
+
#### `-p`
|
|
89
92
|
|
|
90
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:
|
|
91
94
|
|
|
@@ -50,19 +50,22 @@ The program `tictacsync` will recursively scan the directory given as argument,
|
|
|
50
50
|
If shooting multicam, put clips in their respective directories (using the camera name as folder name) _and_ the audio under their own directory. `tictacsync` will detect that structured input and will generate multicam folders ready to be imported into your NLE (for now only DaVinci Resolve has been validated).
|
|
51
51
|
|
|
52
52
|
## Options
|
|
53
|
+
#### `-v`
|
|
53
54
|
|
|
54
55
|
For a very verbose output add the `-v` flag:
|
|
55
56
|
|
|
56
57
|
> tictacsync -v dailies/loose/MVI_0024.MP4
|
|
57
|
-
|
|
58
|
+
#### `--terse`
|
|
58
59
|
For a one line output (or to suppress the progress bars) use the `--terse` flag:
|
|
59
60
|
|
|
60
61
|
> tictacsync --terse dailies/loose/MVI_0024.MP4
|
|
61
62
|
dailies/loose/MVI_0024.MP4 UTC:2024-03-12 23:07:01.4281 pulse: 27450 in chan 0
|
|
63
|
+
#### `--isos`
|
|
62
64
|
|
|
63
|
-
|
|
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/).
|
|
64
66
|
|
|
65
|
-
> tictacsync --isos dailies/
|
|
67
|
+
> tictacsync --isos dailies/day01
|
|
68
|
+
#### `-p`
|
|
66
69
|
|
|
67
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:
|
|
68
71
|
|
|
@@ -32,7 +32,7 @@ setup(
|
|
|
32
32
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
|
-
version = '0.
|
|
35
|
+
version = '0.97a',
|
|
36
36
|
description = "command for syncing audio video recordings",
|
|
37
37
|
long_description_content_type='text/markdown',
|
|
38
38
|
long_description = long_descr,
|
|
@@ -58,7 +58,6 @@ def print_grby(grby):
|
|
|
58
58
|
for e in keylist:
|
|
59
59
|
print(' ', e)
|
|
60
60
|
|
|
61
|
-
|
|
62
61
|
@dataclass
|
|
63
62
|
class Tracks:
|
|
64
63
|
# track numbers start at 1 for first track (as needed by sox)
|
|
@@ -78,8 +77,9 @@ class Device:
|
|
|
78
77
|
name: str
|
|
79
78
|
dev_type: str # CAM or REC
|
|
80
79
|
n_chan: int
|
|
81
|
-
ttc: int
|
|
80
|
+
ttc: int # zero based index?
|
|
82
81
|
tracks: Tracks
|
|
82
|
+
sampling_freq: float # fps if cam
|
|
83
83
|
def __hash__(self):
|
|
84
84
|
return self.UID
|
|
85
85
|
def __eq__(self, other):
|
|
@@ -94,9 +94,9 @@ class Media:
|
|
|
94
94
|
|
|
95
95
|
def media_at_path(input_structure, p):
|
|
96
96
|
# return Media object for mediafile using ffprobe
|
|
97
|
-
dev_UID, dt = get_device_ffprobe_UID(p)
|
|
97
|
+
dev_UID, dt, sf = get_device_ffprobe_UID(p)
|
|
98
98
|
dev_name = None
|
|
99
|
-
logger.debug('ffprobe dev_UID:%s dt:%s'%(dev_UID, dt))
|
|
99
|
+
logger.debug('ffprobe dev_UID:%s dt:%s sf:%s'%(dev_UID, dt,sf))
|
|
100
100
|
if input_structure == 'folder_is_device':
|
|
101
101
|
dev_name = p.parent.name
|
|
102
102
|
if dev_UID is None:
|
|
@@ -110,7 +110,7 @@ def media_at_path(input_structure, p):
|
|
|
110
110
|
if stream['codec_type']=='audio'
|
|
111
111
|
]
|
|
112
112
|
if len(audio_streams) > 1:
|
|
113
|
-
print('
|
|
113
|
+
print('\nfor [gold1]%s[/gold1], ffprobe gave multiple audio streams, quitting.'%p)
|
|
114
114
|
quit()
|
|
115
115
|
# raise Exception('ffprobe gave multiple audio streams?')
|
|
116
116
|
if len(audio_streams) == 0:
|
|
@@ -124,7 +124,7 @@ def media_at_path(input_structure, p):
|
|
|
124
124
|
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
125
125
|
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
126
126
|
device = Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
|
|
127
|
-
n_chan=n, ttc=None, tracks=None)
|
|
127
|
+
n_chan=n, ttc=None, sampling_freq=sf, tracks=None)
|
|
128
128
|
logger.debug('for path: %s, device:%s'%(p,device))
|
|
129
129
|
return Media(p, device)
|
|
130
130
|
|
|
@@ -138,7 +138,7 @@ def get_device_ffprobe_UID(file):
|
|
|
138
138
|
Device UIDs are used later in Montage._get_concatenated_audiofile_for()
|
|
139
139
|
for grouping each audio or video clip along its own timeline track.
|
|
140
140
|
|
|
141
|
-
Returns a tuple: (UID, CAM|REC)
|
|
141
|
+
Returns a tuple: (UID, CAM|REC, sampling_freq)
|
|
142
142
|
|
|
143
143
|
If an ffmpeg.Error occurs, returns (None, None)
|
|
144
144
|
if no UID is found, but device type is identified, returns (None, CAM|REC)
|
|
@@ -155,8 +155,21 @@ def get_device_ffprobe_UID(file):
|
|
|
155
155
|
# fall back to folder name
|
|
156
156
|
logger.debug('ffprobe %s'%probe)
|
|
157
157
|
streams = probe['streams']
|
|
158
|
+
video_streams = [st for st in streams if st['codec_type'] == 'video']
|
|
159
|
+
audio_streams = [st for st in streams if st['codec_type'] == 'audio']
|
|
160
|
+
if len(video_streams) > 1:
|
|
161
|
+
print('\nmore than one video stream for %s... quitting'%file)
|
|
162
|
+
quit()
|
|
163
|
+
if len(audio_streams) != 1:
|
|
164
|
+
print('\nnbr of audio stream for %s not 1 ... quitting'%file)
|
|
165
|
+
quit()
|
|
158
166
|
codecs = [stream['codec_type'] for stream in streams]
|
|
159
|
-
|
|
167
|
+
# cameras have two streams: video AND audio
|
|
168
|
+
device_type = 'CAM' if len(video_streams) == 1 else 'REC'
|
|
169
|
+
if device_type == 'CAM':
|
|
170
|
+
sampling_freq = eval(video_streams[0]['r_frame_rate'])
|
|
171
|
+
else:
|
|
172
|
+
sampling_freq = float(audio_streams[0]['sample_rate'])
|
|
160
173
|
format_dict = probe['format'] # all files should have this
|
|
161
174
|
if 'tags' in format_dict:
|
|
162
175
|
probe_string = pformat(format_dict['tags'])
|
|
@@ -174,7 +187,7 @@ def get_device_ffprobe_UID(file):
|
|
|
174
187
|
if UID == 0: # empty probe_lines from Audacity ?!?
|
|
175
188
|
UID = None
|
|
176
189
|
logger.debug('ffprobe_UID is: %s'%UID)
|
|
177
|
-
return UID, device_type
|
|
190
|
+
return UID, device_type, sampling_freq
|
|
178
191
|
|
|
179
192
|
class Scanner:
|
|
180
193
|
"""
|
|
@@ -362,7 +375,7 @@ class Scanner:
|
|
|
362
375
|
sys.exit(1)
|
|
363
376
|
err_msg = tracks.error_msg
|
|
364
377
|
if err_msg != None:
|
|
365
|
-
print('
|
|
378
|
+
print('\nError, quitting: in file %s, %s'%(tracks_file, err_msg))
|
|
366
379
|
raise Exception
|
|
367
380
|
else:
|
|
368
381
|
logger.debug('tracks object%s'%tracks)
|
|
@@ -486,18 +499,16 @@ class Scanner:
|
|
|
486
499
|
read track names for naming separated ISOs
|
|
487
500
|
from tracks_file.
|
|
488
501
|
|
|
489
|
-
tokens looked for: mix
|
|
502
|
+
tokens looked for: mix; L mix; R mix; 0 and TC
|
|
490
503
|
|
|
491
|
-
|
|
504
|
+
repeating "mic*" pattern signals a stereo track
|
|
492
505
|
and entries will correspondingly panned into
|
|
493
|
-
a stero mix named
|
|
494
|
-
|
|
495
|
-
xyz L # spaces are ignored |
|
|
496
|
-
zyz R | stereo pair
|
|
497
|
-
abc L
|
|
498
|
-
abc R
|
|
506
|
+
a stero mix named Lmix.wav and Rmix.wav
|
|
499
507
|
|
|
500
|
-
|
|
508
|
+
L mic # spaces are ignored |
|
|
509
|
+
R mic | stereo pair
|
|
510
|
+
L micB
|
|
511
|
+
R micB
|
|
501
512
|
|
|
502
513
|
Returns: a Tracks instance:
|
|
503
514
|
# track numbers start at 1 for first track (as needed by sox)
|
|
@@ -508,6 +519,12 @@ class Scanner:
|
|
|
508
519
|
others: list #of all other tags: (tag, track#) tuples
|
|
509
520
|
rawtrx: list # list of strings read from file
|
|
510
521
|
error_msg: str # 'None' if none
|
|
522
|
+
e.g.: Tracks( ttc=2,
|
|
523
|
+
unused=[],
|
|
524
|
+
stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
|
|
525
|
+
mix=[], others=[('clics', 1)],
|
|
526
|
+
rawtrx=['clics', 'TC', 'L mic', 'R mic', 'L mic2;1000', 'R mic2;1000', 'Lmix', 'Rmix'],
|
|
527
|
+
error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
|
|
511
528
|
"""
|
|
512
529
|
def _WOspace(chaine):
|
|
513
530
|
ch = [c for c in chaine if c != ' ']
|
|
@@ -517,15 +534,15 @@ class Scanner:
|
|
|
517
534
|
return ''.join(ch)
|
|
518
535
|
def _seemsStereoMic(tag):
|
|
519
536
|
# is tag likely a stereo pair tag?
|
|
520
|
-
# should
|
|
521
|
-
return tag[:
|
|
537
|
+
# should ends with 'mic' and starts with 'l' or 'r'
|
|
538
|
+
return tag[1:4]=='mic' and tag[0] in 'lr'
|
|
522
539
|
file=open(tracks_file,"r")
|
|
523
540
|
whole_txt = file.read()
|
|
524
541
|
logger.debug('all_lines:\n%s'%whole_txt)
|
|
525
|
-
|
|
542
|
+
tracks_lines_wspaces = [l.split('#')[0] for l in whole_txt.splitlines()
|
|
526
543
|
if len(l) > 0 ]
|
|
527
|
-
tracks_lines = [_WOspace(l) for l in
|
|
528
|
-
rawtrx =
|
|
544
|
+
tracks_lines = [_WOspace(l) for l in tracks_lines_wspaces if len(l) > 0 ]
|
|
545
|
+
rawtrx = [l for l in tracks_lines_wspaces if len(l) > 0 ]
|
|
529
546
|
# add index with tuples, starting at 1
|
|
530
547
|
logger.debug('tracks_lines whole: %s'%tracks_lines)
|
|
531
548
|
def _detach_lag_value(line):
|
|
@@ -540,119 +557,120 @@ class Scanner:
|
|
|
540
557
|
return splt
|
|
541
558
|
tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
|
|
542
559
|
in tracks_lines])
|
|
543
|
-
|
|
544
|
-
logger.debug('
|
|
545
|
-
tracks_lines = [(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
560
|
+
lag_values = [e for e in lag_values] # from tuple to list
|
|
561
|
+
# logger.debug('tracks_lines WO lag: %s'%tracks_lines)
|
|
562
|
+
tracks_lines = [l.lower() for l in tracks_lines]
|
|
563
|
+
logger.debug('tracks_lines lower case: %s'%tracks_lines)
|
|
564
|
+
# print(lag_values)
|
|
565
|
+
logger.debug('lag_values: %s'%lag_values)
|
|
566
|
+
tagsWOl_r = [e[1:] for e in tracks_lines] # skip first letter
|
|
567
|
+
logger.debug('tags WO 1st letter %s'%tagsWOl_r)
|
|
568
|
+
# find idx of start of pairs
|
|
569
|
+
# ['clics', 'TC', 'Lmic', 'Rmic', 'Lmic2', 'Lmic2', 'Lmix', 'Rmix']
|
|
570
|
+
# ^ ^ ^
|
|
571
|
+
def _micOrmix(a,b):
|
|
572
|
+
# test if same and mic mic or mix mix
|
|
573
|
+
if len(a) == 0:
|
|
574
|
+
return False
|
|
575
|
+
return (a == b) and (a in 'micmix')
|
|
576
|
+
pair_idx_start =[i for i, same in enumerate([_micOrmix(a,b) for a,b
|
|
577
|
+
in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
|
|
578
|
+
logger.debug('pair_idx_start %s'%pair_idx_start)
|
|
579
|
+
def LR_OK(idx):
|
|
580
|
+
# in tracks_lines, check if idx start a LR pair
|
|
581
|
+
a = tracks_lines[idx][0]
|
|
582
|
+
b = tracks_lines[idx+1][0]
|
|
583
|
+
return a+b in ['lr', 'rl']
|
|
584
|
+
LR_OKs = [LR_OK(p) for p in pair_idx_start]
|
|
585
|
+
logger.debug('LR_OKs %s'%LR_OKs)
|
|
586
|
+
if not all(LR_OKs):
|
|
587
|
+
print('\nError in %s'%tracks_file)
|
|
588
|
+
print('Some tracks are paired but not L and R: %s'%rawtrx)
|
|
589
|
+
print('quitting...')
|
|
590
|
+
quit()
|
|
591
|
+
complete_pairs_idx = pair_idx_start + [i + 1 for i in pair_idx_start]
|
|
592
|
+
singles = set(range(len(tracks_lines))).difference(complete_pairs_idx)
|
|
593
|
+
logger.debug('complete_pairs_idx %s'%complete_pairs_idx)
|
|
594
|
+
logger.debug('singles %s'%singles)
|
|
595
|
+
singles_tag = [tracks_lines[i] for i in singles]
|
|
596
|
+
logger.debug('singles_tag %s'%singles_tag)
|
|
597
|
+
n_tc_token = sum([t == 'tc' for t in singles_tag])
|
|
598
|
+
logger.debug('n tc tags %s'%n_tc_token)
|
|
599
|
+
if n_tc_token == 0:
|
|
600
|
+
print('\nError in %s'%tracks_file)
|
|
601
|
+
print('with %s'%rawtrx)
|
|
602
|
+
print('no TC track found, quitting...')
|
|
603
|
+
quit()
|
|
604
|
+
if n_tc_token > 1:
|
|
605
|
+
print('\nError in %s'%tracks_file)
|
|
606
|
+
print('with %s'%rawtrx)
|
|
607
|
+
print('more than one TC track, quitting...')
|
|
608
|
+
quit()
|
|
555
609
|
output_tracks = Tracks(None,[],[],[],[],rawtrx,None,[])
|
|
556
|
-
output_tracks.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
# # check if L then R
|
|
560
|
-
# p1, p2 = p
|
|
561
|
-
# return p1[0][-1] == 'l' and p2[0][-1] == 'r'
|
|
562
|
-
if spairs:
|
|
563
|
-
even_idxes = range(0,len(spairs),2)
|
|
564
|
-
paired = [(spairs[i], spairs[i+1]) for i in even_idxes]
|
|
565
|
-
# eg [(('mic1L', 1), ('mic1R', 2)), (('mic2L', 3), ('mic2R', 4))]
|
|
566
|
-
def _mic_same(p):
|
|
567
|
-
# p = (('mic1l', 1), ('mic1r', 2))
|
|
568
|
-
# check if mic1 == mic1
|
|
569
|
-
p1, p2 = p
|
|
570
|
-
return _WO_LR(p1[0]) == _WO_LR(p2[0])
|
|
571
|
-
mic_prefix_OK = all([_mic_same(p) for p in paired])
|
|
572
|
-
logger.debug('mic_prefix_OK: %s'%mic_prefix_OK)
|
|
573
|
-
if not mic_prefix_OK:
|
|
574
|
-
return error_output_stereo
|
|
575
|
-
# mic_LR_OK = all([_LR(p) for p in paired])
|
|
576
|
-
# logger.debug('mic_LR_OK %s'%mic_LR_OK)
|
|
577
|
-
# if not mic_LR_OK:
|
|
578
|
-
# return error_output_stereo
|
|
579
|
-
def _stereo_mic_pref_chan(p):
|
|
580
|
-
# p = (('mic1R', 1), ('mic1L', 2))
|
|
581
|
-
# returns ('mic1', (1,2))
|
|
582
|
-
first, second = p
|
|
583
|
-
mic_prefix = _WO_LR(first[0])
|
|
584
|
-
# check if first token last char
|
|
585
|
-
if p[0][0][-1] == 'L':
|
|
586
|
-
logger.debug('sequence %s is L+R'%[p])
|
|
587
|
-
return (mic_prefix, (first[1], second[1]) )
|
|
588
|
-
else:
|
|
589
|
-
logger.debug('sequence %s is R+L'%[p])
|
|
590
|
-
return (mic_prefix, (second[1], first[1]) )
|
|
591
|
-
grouped_stereo_mic_channels = [_stereo_mic_pref_chan(p) for p
|
|
592
|
-
in paired]
|
|
593
|
-
logger.debug('grouped_stereo_mic_channels: %s'%
|
|
594
|
-
grouped_stereo_mic_channels)
|
|
595
|
-
output_tracks.stereomics = grouped_stereo_mic_channels
|
|
596
|
-
[tracks_lines.remove(e) for e in spairs]
|
|
597
|
-
logger.debug('stereo mic pairs done, continue with %s'%tracks_lines)
|
|
598
|
-
# second, check for stereo mix down (one mixL mixR pair)
|
|
599
|
-
def _seemsStereoMix(tag):
|
|
600
|
-
# is tag likely a stereo pair tag?
|
|
601
|
-
# should start with 'mic' and end with 'l' or 'r'
|
|
602
|
-
return tag[:3]=='mix' and tag[-1] in 'lr'
|
|
603
|
-
stereo_mix_tags = [e for e in tracks_lines if _seemsStereoMix(e[0])]
|
|
604
|
-
logger.debug('stereo_mix_tags: %s'%stereo_mix_tags)
|
|
605
|
-
str_msg = 'Confusing mix pair tags: %s L should appear before R'%' '.join([e[0]
|
|
606
|
-
for e in stereo_mix_tags])
|
|
607
|
-
# error_output = Tracks(None,[],[],[],[],msg)
|
|
608
|
-
def _error_Track(msg):
|
|
609
|
-
return Tracks(None,[],[],[],[],[],msg)
|
|
610
|
-
if stereo_mix_tags:
|
|
611
|
-
if len(stereo_mix_tags) != 2:
|
|
612
|
-
return _error_Track(str_msg)
|
|
613
|
-
mix_LR_OK = _LR(stereo_mix_tags)
|
|
614
|
-
logger.debug('mix_LR_OK %s'%mix_LR_OK)
|
|
615
|
-
if not mix_LR_OK:
|
|
616
|
-
return _error_Track(str_msg)
|
|
617
|
-
stereo_mix_channels = [t[1] for t in stereo_mix_tags]
|
|
618
|
-
output_tracks.mix = stereo_mix_channels
|
|
619
|
-
logger.debug('output_tracks.mix %s'%stereo_mix_channels)
|
|
620
|
-
[tracks_lines.remove(e) for e in stereo_mix_tags]
|
|
621
|
-
logger.debug('stereo mix done, will continue with %s'%tracks_lines)
|
|
622
|
-
# third, check for a mono mix
|
|
623
|
-
mono_mix_tags = [e for e in tracks_lines if e[0] == 'mix']
|
|
624
|
-
if not output_tracks.mix and mono_mix_tags:
|
|
625
|
-
logger.debug('mono_mix_tags: %s'%mono_mix_tags)
|
|
626
|
-
if len(mono_mix_tags) != 1:
|
|
627
|
-
return _error_Track('more than one "mix" token')
|
|
628
|
-
output_tracks.mix = [mono_mix_tags[0][1]]
|
|
629
|
-
[tracks_lines.remove(e) for e in mono_mix_tags]
|
|
630
|
-
logger.debug('mono mix done, will continue with %s'%tracks_lines)
|
|
631
|
-
# fourth, look for 'ttc'
|
|
632
|
-
ttc_chan = [idx for tag, idx in tracks_lines if tag == 'ttc']
|
|
633
|
-
if ttc_chan:
|
|
634
|
-
if len(ttc_chan) > 1:
|
|
635
|
-
return _error_Track('more than one "ttc" token')
|
|
636
|
-
output_tracks.ttc = ttc_chan[0]
|
|
637
|
-
tracks_lines.remove(('ttc', ttc_chan[0]))
|
|
638
|
-
else:
|
|
639
|
-
return _error_Track('no "ttc" token')
|
|
640
|
-
# fifth, check for '0'
|
|
641
|
-
logger.debug('ttc done, will continue with %s'%tracks_lines)
|
|
642
|
-
zeroed = [idx for tag, idx in tracks_lines if tag == '0']
|
|
610
|
+
output_tracks.ttc = tracks_lines.index('tc') + 1 # 1st = 1
|
|
611
|
+
logger.debug('ttc_chan %s'%output_tracks.ttc)
|
|
612
|
+
zeroed = [i+1 for i, t in enumerate(tracks_lines) if t == '0']
|
|
643
613
|
logger.debug('zeroed %s'%zeroed)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
614
|
+
output_tracks.unused = zeroed
|
|
615
|
+
output_tracks.others = [(st, tracks_lines.index(st)+1) for st
|
|
616
|
+
in singles_tag if st not
|
|
617
|
+
in ['tc', 'monomix', '0']]
|
|
618
|
+
logger.debug('output_tracks.others %s'%output_tracks.others)
|
|
619
|
+
# check for monomix
|
|
620
|
+
if 'monomix' in tracks_lines:
|
|
621
|
+
output_tracks.mix = [tracks_lines.index('monomix')+1]
|
|
647
622
|
else:
|
|
648
|
-
output_tracks.
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
623
|
+
output_tracks.mix = []
|
|
624
|
+
# check for stereo mix
|
|
625
|
+
def _findLR(i_first):
|
|
626
|
+
# returns L R indexes (+1 for sox non zero based indexing)
|
|
627
|
+
i_2nd = i_first + 1
|
|
628
|
+
a = tracks_lines[i_first][0]
|
|
629
|
+
b = tracks_lines[i_2nd][0]
|
|
630
|
+
if a == 'l':
|
|
631
|
+
if b == 'r':
|
|
632
|
+
# sequence is Lmix Rmix
|
|
633
|
+
return i_first+1, i_2nd+1
|
|
634
|
+
else:
|
|
635
|
+
print('\nError in %s'%tracks_file)
|
|
636
|
+
print('with %s'%rawtrx)
|
|
637
|
+
print('can not find stereo mix')
|
|
638
|
+
quit()
|
|
639
|
+
elif a == 'r':
|
|
640
|
+
if b == 'l':
|
|
641
|
+
# sequence is Rmix Lmix
|
|
642
|
+
return i_2nd+1, i_first+1
|
|
643
|
+
else:
|
|
644
|
+
print('\nError in %s'%tracks_file)
|
|
645
|
+
print('with %s'%rawtrx)
|
|
646
|
+
print('can not find stereo mix')
|
|
647
|
+
quit()
|
|
648
|
+
logger.debug('for now, output_tracks.mix %s'%output_tracks.mix)
|
|
649
|
+
mix_pair = [p for p in pair_idx_start if tracks_lines[p][1:] == 'mix']
|
|
650
|
+
if len(mix_pair) == 1:
|
|
651
|
+
# one stereo mix, remove it from other pairs
|
|
652
|
+
i = mix_pair[0]
|
|
653
|
+
LR_pair = _findLR(i)
|
|
654
|
+
logger.debug('LR_pair %s'%str(LR_pair))
|
|
655
|
+
pair_idx_start.remove(i)
|
|
656
|
+
# consistency check
|
|
657
|
+
if output_tracks.mix != []:
|
|
658
|
+
# already found a mono mix above!
|
|
659
|
+
print('\nError in %s'%tracks_file)
|
|
660
|
+
print('with %s'%rawtrx)
|
|
661
|
+
print('found a mono mix AND a stereo mix')
|
|
662
|
+
quit()
|
|
663
|
+
output_tracks.mix = LR_pair
|
|
664
|
+
logger.debug('finally, output_tracks.mix %s'%str(output_tracks.mix))
|
|
665
|
+
logger.debug('remaining pairs %s'%pair_idx_start)
|
|
666
|
+
# those are stereo pairs
|
|
667
|
+
stereo_pairs = []
|
|
668
|
+
for first_in_pair in pair_idx_start:
|
|
669
|
+
suffix = tracks_lines[first_in_pair][1:]
|
|
670
|
+
stereo_pairs.append((suffix, _findLR(first_in_pair)))
|
|
671
|
+
logger.debug('stereo_pairs %s'%stereo_pairs)
|
|
672
|
+
output_tracks.stereomics = stereo_pairs
|
|
673
|
+
logger.debug('finished: %s'%output_tracks)
|
|
656
674
|
return output_tracks
|
|
657
675
|
|
|
658
676
|
|
|
@@ -27,7 +27,8 @@ from rich.console import Console
|
|
|
27
27
|
# from rich.text import Text
|
|
28
28
|
from rich.table import Table
|
|
29
29
|
from rich import print
|
|
30
|
-
from pprint import pprint
|
|
30
|
+
from pprint import pprint
|
|
31
|
+
import numpy as np
|
|
31
32
|
|
|
32
33
|
DEL_TEMP = False
|
|
33
34
|
|
|
@@ -58,6 +59,7 @@ def process_files_with_progress_bars(medias):
|
|
|
58
59
|
return recordings, rec_with_TTC, times
|
|
59
60
|
|
|
60
61
|
def process_files(medias):
|
|
62
|
+
# maps Media objects -> Recording objects
|
|
61
63
|
recordings = []
|
|
62
64
|
rec_with_TTC = []
|
|
63
65
|
times = []
|
|
@@ -115,6 +117,7 @@ def process_lag_adjustement(media_object):
|
|
|
115
117
|
media_object.path.replace(backup_name)
|
|
116
118
|
logger.debug('channels %s'%channels)
|
|
117
119
|
def _trim(lag, chan_file):
|
|
120
|
+
# for lag
|
|
118
121
|
if lag == None:
|
|
119
122
|
return chan_file
|
|
120
123
|
else:
|
|
@@ -163,7 +166,7 @@ def main():
|
|
|
163
166
|
parser.add_argument('--isos',
|
|
164
167
|
action='store_true',
|
|
165
168
|
dest='write_ISOs',
|
|
166
|
-
help='
|
|
169
|
+
help='Cut ISO sound files')
|
|
167
170
|
parser.add_argument('--nosync',
|
|
168
171
|
action='store_true',
|
|
169
172
|
dest='nosync',
|
|
@@ -181,10 +184,10 @@ def main():
|
|
|
181
184
|
logger.add(sys.stderr, level="DEBUG")
|
|
182
185
|
# logger.add(sys.stdout, filter="__main__")
|
|
183
186
|
# logger.add(sys.stdout, filter="yaltc")
|
|
184
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
185
|
-
# logger.
|
|
186
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
187
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
187
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_audio_for_each_videoclip")
|
|
188
|
+
# logger.debug(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
189
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_build_and_write_audio")
|
|
190
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_build_audio_and_write_video")
|
|
188
191
|
top_dir = args.path[0]
|
|
189
192
|
if os.path.isfile(top_dir):
|
|
190
193
|
file = top_dir
|
|
@@ -209,7 +212,35 @@ def main():
|
|
|
209
212
|
if not all([lv == None for lv in m.device.tracks.lag_values]):
|
|
210
213
|
logger.debug('%s has lag_values %s'%(
|
|
211
214
|
m.path, m.device.tracks.lag_values))
|
|
212
|
-
process_lag_adjustement(m)
|
|
215
|
+
process_lag_adjustement(m)
|
|
216
|
+
audio_REC_only = all([m.device.dev_type == 'REC' for m
|
|
217
|
+
in scanner.found_media_files])
|
|
218
|
+
|
|
219
|
+
if audio_REC_only:
|
|
220
|
+
if scanner.input_structure != 'folder_is_device':
|
|
221
|
+
print('For merging audio only, use a directory per device, quitting')
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
print('\n\n\nOnly audio recordings are present')
|
|
224
|
+
print('Which device should be the reference?\n')
|
|
225
|
+
devices = scanner.get_devices()
|
|
226
|
+
maxch = len(devices)
|
|
227
|
+
for i, d in enumerate(devices):
|
|
228
|
+
print('\t%i - %s'%(i+1, d.name))
|
|
229
|
+
while True:
|
|
230
|
+
print('\nEnter your choice:', end='')
|
|
231
|
+
choice = input()
|
|
232
|
+
try:
|
|
233
|
+
choice = int(choice)
|
|
234
|
+
except:
|
|
235
|
+
print('Please use numeric digits.')
|
|
236
|
+
continue
|
|
237
|
+
if choice not in list(range(1, maxch + 1)):
|
|
238
|
+
print('Please enter a number in [1..%i]'%maxch)
|
|
239
|
+
continue
|
|
240
|
+
break
|
|
241
|
+
ref_device = list(devices)[choice - 1]
|
|
242
|
+
# ref_device = list(devices)[3 - 1]
|
|
243
|
+
print('When only audio recordings are present, ISOs files will be cut and written.')
|
|
213
244
|
if not args.terse:
|
|
214
245
|
if scanner.input_structure == 'folder_is_device':
|
|
215
246
|
print('\nDetected structured folders', end='')
|
|
@@ -226,20 +257,37 @@ def main():
|
|
|
226
257
|
len(scanner.found_media_files)), end='')
|
|
227
258
|
print('from [gold1]%i[/gold1] devices:\n'%(
|
|
228
259
|
scanner.get_devices_number()))
|
|
229
|
-
|
|
230
|
-
|
|
260
|
+
all_devices = scanner.get_devices()
|
|
261
|
+
for dev in all_devices:
|
|
262
|
+
dt = 'Camera' if dev.dev_type == 'CAM' else 'Recorder'
|
|
263
|
+
print('%s [gold1]%s[/gold1] with files:'%(dt, dev.name), end = ' ')
|
|
231
264
|
medias = scanner.get_media_for_device(dev)
|
|
232
|
-
for m in medias[:-1]:
|
|
265
|
+
for m in medias[:-1]: # last printed out of loop
|
|
233
266
|
print('[gold1]%s[/gold1]'%m.path.name, end=', ')
|
|
234
267
|
print('[gold1]%s[/gold1]'%medias[-1].path.name)
|
|
268
|
+
a_media = medias[0]
|
|
269
|
+
# check if all audio recorders have same sampling freq
|
|
270
|
+
freqs = [dev.sampling_freq for dev in all_devices if dev.dev_type == 'REC']
|
|
271
|
+
same = np.isclose(np.std(freqs),0)
|
|
272
|
+
logger.debug('sampling freqs from audio recoders %s, same:%s'%(freqs, same))
|
|
273
|
+
if not same:
|
|
274
|
+
print('some audio recorders have different sampling frequencies:')
|
|
275
|
+
print(freqs)
|
|
276
|
+
print('resulting in undefined results: quitting...')
|
|
277
|
+
quit()
|
|
235
278
|
print()
|
|
236
|
-
|
|
237
|
-
|
|
279
|
+
recordings, rec_with_TTC, times = \
|
|
280
|
+
process_files(scanner.found_media_files)
|
|
238
281
|
recordings_with_time = [
|
|
239
282
|
rec
|
|
240
283
|
for rec in rec_with_TTC
|
|
241
284
|
if rec.get_start_time()
|
|
242
285
|
]
|
|
286
|
+
if audio_REC_only:
|
|
287
|
+
for rec in recordings:
|
|
288
|
+
# print(rec, rec.device == ref_device)
|
|
289
|
+
if rec.device == ref_device:
|
|
290
|
+
rec.is_reference = True
|
|
243
291
|
if not args.terse:
|
|
244
292
|
table = Table(title="tictacsync results")
|
|
245
293
|
table.add_column("Recording\n", justify="center", style='gold1')
|
|
@@ -284,7 +332,7 @@ def main():
|
|
|
284
332
|
sys.exit(1)
|
|
285
333
|
matcher = timeline.Matcher(recordings_with_time)
|
|
286
334
|
matcher.scan_audio_for_each_videoclip()
|
|
287
|
-
if not matcher.
|
|
335
|
+
if not matcher.mergers:
|
|
288
336
|
if not args.terse:
|
|
289
337
|
print('\nNothing to sync, bye.\n')
|
|
290
338
|
sys.exit(1)
|
|
@@ -294,17 +342,18 @@ def main():
|
|
|
294
342
|
asked_ISOs = False
|
|
295
343
|
output_dir = args.o
|
|
296
344
|
# if args.verbose_output or args.terse: # verbose, so no progress bars
|
|
297
|
-
for merger in matcher.
|
|
298
|
-
merger.
|
|
345
|
+
for merger in matcher.mergers:
|
|
346
|
+
merger.build_audio_and_write_merged_media(top_dir, arg_out_dir,
|
|
299
347
|
OUT_struct_for_mcam,
|
|
300
|
-
asked_ISOs,
|
|
348
|
+
asked_ISOs,
|
|
349
|
+
audio_REC_only)
|
|
301
350
|
if not args.terse:
|
|
302
351
|
print("\n")
|
|
303
352
|
# find out where files were wrtitten
|
|
304
|
-
a_merger = matcher.
|
|
353
|
+
a_merger = matcher.mergers[0]
|
|
305
354
|
print('\nWrote output in folder [gold1]%s[/gold1]'%(
|
|
306
355
|
a_merger.synced_clip_dir))
|
|
307
|
-
for merger in matcher.
|
|
356
|
+
for merger in matcher.mergers:
|
|
308
357
|
print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
|
|
309
358
|
for audio in merger.get_matched_audio_recs():
|
|
310
359
|
print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
|
|
@@ -66,14 +66,14 @@ def build_poly_name(multifiles):
|
|
|
66
66
|
s1 = str(multifiles[0].stem)
|
|
67
67
|
s2 = str(multifiles[1].stem)
|
|
68
68
|
if len(s1) != len(s2):
|
|
69
|
-
print('
|
|
69
|
+
print('\nCan not build compound name with %s.wav and %s.wav'%(s1,s2))
|
|
70
70
|
print('names lengths differ.')
|
|
71
71
|
print('In folder "%s", quitting.'%multifiles[0].parent)
|
|
72
72
|
sys.exit(1)
|
|
73
73
|
pairs = list(zip(s1, s2))
|
|
74
74
|
not_same = [a for a, b in pairs if a != b ]
|
|
75
75
|
if len(not_same) > 2:
|
|
76
|
-
print('
|
|
76
|
+
print('\nCan not build compound name with %s.wav and %s.wav'%(s1,s2))
|
|
77
77
|
print('names differ by more than two characters.')
|
|
78
78
|
print('In folder "%s", quitting.'%multifiles[0].parent)
|
|
79
79
|
sys.exit(1)
|
|
@@ -67,7 +67,8 @@ def _join_audio2video(audio_path: Path, video: Path):
|
|
|
67
67
|
ffmpeg_args = (
|
|
68
68
|
ffmpeg
|
|
69
69
|
.input(v_n)
|
|
70
|
-
.output(out_n,
|
|
70
|
+
.output(out_n, vcodec='copy')
|
|
71
|
+
# .output(out_n, shortest=None, vcodec='copy')
|
|
71
72
|
.global_args('-i', a_n, "-hide_banner")
|
|
72
73
|
.overwrite_output()
|
|
73
74
|
.get_args()
|
|
@@ -77,7 +78,8 @@ def _join_audio2video(audio_path: Path, video: Path):
|
|
|
77
78
|
_, out = (
|
|
78
79
|
ffmpeg
|
|
79
80
|
.input(v_n)
|
|
80
|
-
.output(out_n, shortest=None, vcodec='copy')
|
|
81
|
+
# .output(out_n, shortest=None, vcodec='copy')
|
|
82
|
+
.output(out_n, vcodec='copy')
|
|
81
83
|
.global_args('-i', a_n, "-hide_banner")
|
|
82
84
|
.overwrite_output()
|
|
83
85
|
.run(capture_stderr=True)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os, itertools
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
video_extensions = \
|
|
5
|
+
"""webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
|
|
6
|
+
qt wmv yuv rm rmvb viv asf mp4 m4p m4v mpg mp2 mpeg mpe
|
|
7
|
+
mpv mpg mpeg m2v m4v svi 3gp 3g2 mxf roq nsv""".split() # from wikipedia
|
|
8
|
+
|
|
9
|
+
def is_video(f):
|
|
10
|
+
# True if name as video extension
|
|
11
|
+
name_ext = f.split('.')
|
|
12
|
+
if len(name_ext) != 2:
|
|
13
|
+
return False
|
|
14
|
+
name, ext = name_ext
|
|
15
|
+
return ext.lower() in video_extensions
|
|
16
|
+
|
|
17
|
+
def find_ISO_vids_pairs(top):
|
|
18
|
+
# top is
|
|
19
|
+
vids = []
|
|
20
|
+
ISOs = []
|
|
21
|
+
for (root,dirs,files) in os.walk(top, topdown=True):
|
|
22
|
+
for d in dirs:
|
|
23
|
+
if d[-4:] == '_ISO':
|
|
24
|
+
ISOs.append(Path(root)/d)
|
|
25
|
+
for f in files:
|
|
26
|
+
if is_video(f):
|
|
27
|
+
vids.append(Path(root)/f)
|
|
28
|
+
matches = []
|
|
29
|
+
for pair in list(itertools.product(vids, ISOs)):
|
|
30
|
+
# print(pair)
|
|
31
|
+
vid, ISO = pair
|
|
32
|
+
vidname, ext = vid.name.split('.')
|
|
33
|
+
if vidname == ISO.name[:-4]:
|
|
34
|
+
matches.append(pair)
|
|
35
|
+
# print(vidname, ISO.name[:-4])
|
|
36
|
+
return matches
|
|
37
|
+
|
|
38
|
+
[print( vid, ISO) for vid, ISO in find_ISO_vids_pairs('.')]
|
|
@@ -332,6 +332,10 @@ class AudioStitcherVideoMerger:
|
|
|
332
332
|
videoclip : a Recording instance
|
|
333
333
|
The video to which audio files are synced
|
|
334
334
|
|
|
335
|
+
ref_audio : a Recording instance
|
|
336
|
+
If no video is present, this is the reference audio to which others
|
|
337
|
+
audio files are synced
|
|
338
|
+
|
|
335
339
|
soxed_audio : dict as {Recording : path}
|
|
336
340
|
keys are elements of matched_audio_recordings and the value are
|
|
337
341
|
the Pathlib path of the eventual edited audio(trimmed or padded).
|
|
@@ -383,6 +387,13 @@ class AudioStitcherVideoMerger:
|
|
|
383
387
|
logger.debug('devices %s'%devices)
|
|
384
388
|
return devices
|
|
385
389
|
|
|
390
|
+
def _get_secondary_audio_devices(self):
|
|
391
|
+
devices = set([r.device for r in self.get_matched_audio_recs()])
|
|
392
|
+
logger.debug('get_matched_audio_recs: %s'%
|
|
393
|
+
pprint.pformat(self.get_matched_audio_recs()))
|
|
394
|
+
logger.debug('devices %s'%devices)
|
|
395
|
+
return devices
|
|
396
|
+
|
|
386
397
|
def _get_all_recordings_for(self, device):
|
|
387
398
|
# return recordings for a particular device, sorted by time
|
|
388
399
|
recs = [a for a in self.get_matched_audio_recs() if a.device == device]
|
|
@@ -635,11 +646,11 @@ class AudioStitcherVideoMerger:
|
|
|
635
646
|
leftCAM/
|
|
636
647
|
|
|
637
648
|
canon24fps01.MOV ━━━━┓ name of clip is name of folder
|
|
638
|
-
|
|
649
|
+
canon24fps01_ISO/ <━━┛
|
|
639
650
|
chan_1.wav
|
|
640
651
|
chan_2.wav
|
|
641
652
|
canon24fps02.MOV
|
|
642
|
-
|
|
653
|
+
canon24fps01_ISO/
|
|
643
654
|
chan_1.wav
|
|
644
655
|
chan_2.wav
|
|
645
656
|
|
|
@@ -680,7 +691,7 @@ class AudioStitcherVideoMerger:
|
|
|
680
691
|
video_stem_WO_suffix = synced_clip_file.stem
|
|
681
692
|
# video_stem_WO_suffix = synced_clip_file.stem.split('.')[0]
|
|
682
693
|
# OUT_DIR_DEFAULT, D2 = ISOsDIR.split('/')
|
|
683
|
-
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '
|
|
694
|
+
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_ISO')
|
|
684
695
|
os.makedirs(ISOdir, exist_ok=True)
|
|
685
696
|
logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
|
|
686
697
|
logger.debug('ISOdir %s'%ISOdir)
|
|
@@ -713,16 +724,16 @@ class AudioStitcherVideoMerger:
|
|
|
713
724
|
In details:
|
|
714
725
|
|
|
715
726
|
If no device tracks.txt file declared a mix track (or if tracks.txt is
|
|
716
|
-
absent)
|
|
727
|
+
absent) a mix is done programmatically. Two possibilities:
|
|
717
728
|
|
|
718
729
|
#1- no stereo pairs were declared: a global mono mix is returned.
|
|
719
730
|
#2- one or more stereo pair mics were used and declared (micL, micR):
|
|
720
731
|
a global stereo mix is returned with mono tracks panned 50-50
|
|
721
732
|
|
|
722
|
-
If device has an associated Tracks description AND it declares a(mono or
|
|
733
|
+
If device has an associated Tracks description AND it declares a (mono or
|
|
723
734
|
stereo) mix track, this fct returns a tempfile containing the
|
|
724
|
-
corresponding tracks,
|
|
725
|
-
(
|
|
735
|
+
corresponding tracks, simply extracting them from multichan_tmpfl
|
|
736
|
+
(those covers cases #3 and #4)
|
|
726
737
|
|
|
727
738
|
Args:
|
|
728
739
|
device : device_scanner.Device dataclass
|
|
@@ -793,12 +804,13 @@ class AudioStitcherVideoMerger:
|
|
|
793
804
|
logger.debug('%s has mix %s'%(device.name, device.tracks.mix))
|
|
794
805
|
logger.debug('device %s'%device)
|
|
795
806
|
# just checking coherency
|
|
796
|
-
if '
|
|
797
|
-
trx_TTC_chan = device.tracks.rawtrx.index('ttc')
|
|
798
|
-
elif 'tc' in device.tracks.rawtrx:
|
|
807
|
+
if 'tc' in device.tracks.rawtrx:
|
|
799
808
|
trx_TTC_chan = device.tracks.rawtrx.index('tc')
|
|
809
|
+
elif 'TC' in device.tracks.rawtrx:
|
|
810
|
+
trx_TTC_chan = device.tracks.rawtrx.index('TC')
|
|
800
811
|
else:
|
|
801
812
|
print('Error: no tc or ttc tag in track.txt')
|
|
813
|
+
print(device.tracks.rawtrx)
|
|
802
814
|
sys.exit(1)
|
|
803
815
|
logger.debug('TTC chan %i, dev ttc %i'%(trx_TTC_chan, device.ttc))
|
|
804
816
|
if trx_TTC_chan != device.ttc:
|
|
@@ -839,7 +851,120 @@ class AudioStitcherVideoMerger:
|
|
|
839
851
|
stereo_files = mic_stereo_files + new_stereo_files
|
|
840
852
|
return _sox_mix_files(stereo_files)
|
|
841
853
|
|
|
842
|
-
def
|
|
854
|
+
def build_audio_and_write_merged_media(self, top_dir, output_dir,
|
|
855
|
+
write_multicam_structure,
|
|
856
|
+
asked_ISOs, audio_REC_only):
|
|
857
|
+
# simply bifurcates depending if ref media is video (prob 99%)
|
|
858
|
+
# (then audio_REC_only == False)
|
|
859
|
+
# or if ref media is audio (no camera detected)
|
|
860
|
+
# (with audio_REC_only == True)
|
|
861
|
+
if not audio_REC_only:
|
|
862
|
+
self._build_audio_and_write_video(top_dir, output_dir,
|
|
863
|
+
write_multicam_structure, asked_ISOs)
|
|
864
|
+
else:
|
|
865
|
+
self._build_and_write_audio(top_dir, output_dir)
|
|
866
|
+
|
|
867
|
+
def _build_and_write_audio(self, top_dir, output_dir):
|
|
868
|
+
"""
|
|
869
|
+
This is called when only audio recorders were found (no cam).
|
|
870
|
+
|
|
871
|
+
top_dir: Path, directory where media were looked for
|
|
872
|
+
|
|
873
|
+
output_dir: str for optional folder specified as CLI argument, if
|
|
874
|
+
value is None, fall back to OUT_DIR_DEFAULT
|
|
875
|
+
|
|
876
|
+
For each audio devices found overlapping self.ref_audio: pad, trim
|
|
877
|
+
or stretch audio files by calling _get_concatenated_audiofile_for(), and
|
|
878
|
+
put them in merged_audio_files_by_device. More than one audio recorder
|
|
879
|
+
can be used for a shot: that's why merged_audio_files_by_device is a
|
|
880
|
+
list.
|
|
881
|
+
|
|
882
|
+
Returns nothing
|
|
883
|
+
|
|
884
|
+
Sets AudioStitcherVideoMerger.final_synced_file on completion to list
|
|
885
|
+
containing all the synced and patched audio files.
|
|
886
|
+
"""
|
|
887
|
+
self.ref_audio = self.videoclip # ref audio was stored in videoclip
|
|
888
|
+
logger.debug('Will merge audio against %s from %s'%(self.ref_audio,
|
|
889
|
+
self.ref_audio.device.name))
|
|
890
|
+
# eg, suppose the user called tictacsync with 'mondayPM' as top folder
|
|
891
|
+
# to scan for dailies (and 'somefolder' for output):
|
|
892
|
+
if output_dir == None:
|
|
893
|
+
synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
|
|
894
|
+
else:
|
|
895
|
+
synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
|
|
896
|
+
self.synced_clip_dir = synced_clip_dir
|
|
897
|
+
os.makedirs(synced_clip_dir, exist_ok=True)
|
|
898
|
+
logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
|
|
899
|
+
synced_clip_file = synced_clip_dir/\
|
|
900
|
+
Path(self.videoclip.new_rec_name).name
|
|
901
|
+
logger.debug('editing files for %s'%synced_clip_file)
|
|
902
|
+
self.ref_audio.final_synced_file = synced_clip_file # relative path
|
|
903
|
+
# Collecting edited audio by device, in (Device, tempfile) pairs:
|
|
904
|
+
# for a given self.ref_audio, each other audio device will have a sequence
|
|
905
|
+
# of matched, synced and joined audio files present in a single
|
|
906
|
+
# edited audio file, returned by _get_concatenated_audiofile_for
|
|
907
|
+
merged_audio_files_by_device = [
|
|
908
|
+
(d, self._get_concatenated_audiofile_for(d))
|
|
909
|
+
for d in self._get_secondary_audio_devices()]
|
|
910
|
+
# at this point, audio editing has been done in tempfiles
|
|
911
|
+
logger.debug('%i elements in merged_audio_files_by_device'%len(
|
|
912
|
+
merged_audio_files_by_device))
|
|
913
|
+
for d, f, in merged_audio_files_by_device:
|
|
914
|
+
logger.debug('device: %s'%d.name)
|
|
915
|
+
logger.debug('file %s of %i channels'%(f.name,
|
|
916
|
+
sox.file_info.channels(f.name)))
|
|
917
|
+
logger.debug('')
|
|
918
|
+
if not merged_audio_files_by_device:
|
|
919
|
+
# no audio file overlaps for this clip
|
|
920
|
+
return #############################################################
|
|
921
|
+
logger.debug('will output ISO files since no cam')
|
|
922
|
+
devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
|
|
923
|
+
for device, multi_chan_audio
|
|
924
|
+
in merged_audio_files_by_device]
|
|
925
|
+
# add device and file from self.ref_audio
|
|
926
|
+
new_tuple = (self.ref_audio.device,
|
|
927
|
+
_split_channels(self.ref_audio.AVpath))
|
|
928
|
+
devices_and_monofiles.append(new_tuple)
|
|
929
|
+
logger.debug('devices_and_monofiles: %s'%
|
|
930
|
+
pprint.pformat(devices_and_monofiles))
|
|
931
|
+
def _trnm(dev, idx): # used in the loop just below
|
|
932
|
+
# generates track name for later if asked_ISOs
|
|
933
|
+
# idx is from 0 to nchan-1 for this device
|
|
934
|
+
if dev.tracks == None:
|
|
935
|
+
chan_name = 'chan%s'%str(idx+1).zfill(2)
|
|
936
|
+
else:
|
|
937
|
+
# sanitize
|
|
938
|
+
symbols = set(r"""`~!@#$%^&*()_-+={[}}|\:;"'<,>.?/""")
|
|
939
|
+
chan_name = dev.tracks.rawtrx[idx]
|
|
940
|
+
logger.debug('raw chan_name %s'%chan_name)
|
|
941
|
+
chan_name = chan_name.split(';')[0] # if ex: "lav bob;25"
|
|
942
|
+
logger.debug('chan_name WO ; lag: %s'%chan_name)
|
|
943
|
+
chan_name =''.join([e if e not in symbols else ''
|
|
944
|
+
for e in chan_name])
|
|
945
|
+
logger.debug('chan_name WO special chars: %s'%chan_name)
|
|
946
|
+
chan_name = chan_name.replace(' ', '_')
|
|
947
|
+
logger.debug('chan_name WO spaces: %s'%chan_name)
|
|
948
|
+
chan_name += '_' + dev.name # TODO: make this an option?
|
|
949
|
+
logger.debug('track_name %s'%chan_name)
|
|
950
|
+
return chan_name #####################################################
|
|
951
|
+
# replace device, idx pair with track name (+ device name if many)
|
|
952
|
+
# loop over devices than loop over tracks
|
|
953
|
+
names_audio_tempfiles = []
|
|
954
|
+
for dev, mono_tmpfiles_list in devices_and_monofiles:
|
|
955
|
+
for idx, monotf in enumerate(mono_tmpfiles_list):
|
|
956
|
+
track_name = _trnm(dev, idx)
|
|
957
|
+
logger.debug('track_name %s'%track_name)
|
|
958
|
+
if track_name[0] == '0': # muted, skip
|
|
959
|
+
continue
|
|
960
|
+
names_audio_tempfiles.append((track_name, monotf))
|
|
961
|
+
logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
|
|
962
|
+
self._write_ISOs(names_audio_tempfiles)
|
|
963
|
+
logger.debug('merged_audio_files_by_device %s'%
|
|
964
|
+
merged_audio_files_by_device)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _build_audio_and_write_video(self, top_dir, output_dir,
|
|
843
968
|
write_multicam_structure,
|
|
844
969
|
asked_ISOs):
|
|
845
970
|
"""
|
|
@@ -862,12 +987,12 @@ class AudioStitcherVideoMerger:
|
|
|
862
987
|
|
|
863
988
|
Sets AudioStitcherVideoMerger.final_synced_file on completion
|
|
864
989
|
"""
|
|
865
|
-
logger.debug(' fct args: top_dir: %s; output_dir: %s; write_multicam_structure: %s; asked_ISOs: %s
|
|
990
|
+
logger.debug(' fct args: top_dir: %s; output_dir: %s; write_multicam_structure: %s; asked_ISOs: %s'%
|
|
866
991
|
(top_dir, output_dir, write_multicam_structure, asked_ISOs))
|
|
867
992
|
logger.debug('device for rec %s: %s'%(self.videoclip,
|
|
868
993
|
self.videoclip.device))
|
|
869
|
-
# suppose the user called tictacsync with 'mondayPM' as top folder
|
|
870
|
-
# scan for dailies (and 'somefolder' for output):
|
|
994
|
+
# eg, suppose the user called tictacsync with 'mondayPM' as top folder
|
|
995
|
+
# to scan for dailies (and 'somefolder' for output):
|
|
871
996
|
if output_dir == None:
|
|
872
997
|
synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
|
|
873
998
|
else:
|
|
@@ -882,13 +1007,18 @@ class AudioStitcherVideoMerger:
|
|
|
882
1007
|
Path(self.videoclip.new_rec_name).name
|
|
883
1008
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
884
1009
|
self.videoclip.final_synced_file = synced_clip_file # relative path
|
|
885
|
-
# Collecting edited audio by device, in (Device,
|
|
1010
|
+
# Collecting edited audio by device, in (Device, tempfiles) pairs:
|
|
886
1011
|
# for a given self.videoclip, each audio device will have a sequence
|
|
887
1012
|
# of matched, synced and joined audio files present in a single
|
|
888
1013
|
# edited audio file, returned by _get_concatenated_audiofile_for
|
|
889
1014
|
merged_audio_files_by_device = [
|
|
890
1015
|
(d, self._get_concatenated_audiofile_for(d))
|
|
891
1016
|
for d in self._get_audio_devices()]
|
|
1017
|
+
# at this point, audio editing has been done in multichan wav tempfiles
|
|
1018
|
+
logger.debug('merged_audio_files_by_device %s'%merged_audio_files_by_device)
|
|
1019
|
+
for d, f, in merged_audio_files_by_device:
|
|
1020
|
+
logger.debug('%s'%d)
|
|
1021
|
+
logger.debug('file %s'%f.name)
|
|
892
1022
|
if len(merged_audio_files_by_device) == 0:
|
|
893
1023
|
# no audio file overlaps for this clip
|
|
894
1024
|
return #############################################################
|
|
@@ -958,21 +1088,30 @@ class AudioStitcherVideoMerger:
|
|
|
958
1088
|
# generates track name for later if asked_ISOs
|
|
959
1089
|
# idx is from 0 to nchan-1 for this device
|
|
960
1090
|
if dev.tracks == None:
|
|
961
|
-
|
|
1091
|
+
chan_name = 'chan%s'%str(idx+1).zfill(2)
|
|
962
1092
|
else:
|
|
963
|
-
#
|
|
964
|
-
|
|
965
|
-
|
|
1093
|
+
# sanitize
|
|
1094
|
+
symbols = set(r"""`~!@#$%^&*()_-+={[}}|\:;"'<,>.?/""")
|
|
1095
|
+
chan_name = dev.tracks.rawtrx[idx]
|
|
1096
|
+
logger.debug('raw chan_name %s'%chan_name)
|
|
1097
|
+
chan_name = chan_name.split(';')[0] # if ex: "lav bob;25"
|
|
1098
|
+
logger.debug('chan_name WO ; lag: %s'%chan_name)
|
|
1099
|
+
chan_name =''.join([e if e not in symbols else ''
|
|
1100
|
+
for e in chan_name])
|
|
1101
|
+
logger.debug('chan_name WO special chars: %s'%chan_name)
|
|
1102
|
+
chan_name = chan_name.replace(' ', '_')
|
|
1103
|
+
logger.debug('chan_name WO spaces: %s'%chan_name)
|
|
966
1104
|
if multiple_recorders:
|
|
967
|
-
|
|
968
|
-
logger.debug('
|
|
969
|
-
return
|
|
1105
|
+
chan_name += '_' + dev.name # TODO: make this an option?
|
|
1106
|
+
logger.debug('track_name %s'%chan_name)
|
|
1107
|
+
return chan_name #####################################################
|
|
970
1108
|
# replace device, idx pair with track name (+ device name if many)
|
|
971
1109
|
# loop over devices than loop over tracks
|
|
972
1110
|
names_audio_tempfiles = []
|
|
973
1111
|
for dev, mono_tmpfiles_list in devices_and_monofiles:
|
|
974
1112
|
for idx, monotf in enumerate(mono_tmpfiles_list):
|
|
975
1113
|
track_name = _trnm(dev, idx)
|
|
1114
|
+
logger.debug('track_name %s'%track_name)
|
|
976
1115
|
if track_name[0] == '0': # muted, skip
|
|
977
1116
|
continue
|
|
978
1117
|
names_audio_tempfiles.append((track_name, monotf))
|
|
@@ -1009,7 +1148,8 @@ class AudioStitcherVideoMerger:
|
|
|
1009
1148
|
def _merge_audio_and_video(self):
|
|
1010
1149
|
"""
|
|
1011
1150
|
Calls ffmpeg to join video in self.videoclip.AVpath to
|
|
1012
|
-
audio in self.videoclip.synced_audio
|
|
1151
|
+
audio in self.videoclip.synced_audio. Audio in original video
|
|
1152
|
+
is dropped.
|
|
1013
1153
|
|
|
1014
1154
|
On entry, videoclip.final_synced_file is a Path to an non existing
|
|
1015
1155
|
file (contrarily to videoclip.synced_audio).
|
|
@@ -1036,7 +1176,8 @@ class AudioStitcherVideoMerger:
|
|
|
1036
1176
|
ffmpeg_args = (
|
|
1037
1177
|
ffmpeg
|
|
1038
1178
|
.input(v_n)
|
|
1039
|
-
.output(out_n, shortest=None, vcodec='copy',
|
|
1179
|
+
# .output(out_n, shortest=None, vcodec='copy',
|
|
1180
|
+
.output(out_n, vcodec='copy',
|
|
1040
1181
|
timecode=timecode)
|
|
1041
1182
|
.global_args('-i', a_n, "-hide_banner")
|
|
1042
1183
|
.overwrite_output()
|
|
@@ -1047,7 +1188,8 @@ class AudioStitcherVideoMerger:
|
|
|
1047
1188
|
_, out = (
|
|
1048
1189
|
ffmpeg
|
|
1049
1190
|
.input(v_n)
|
|
1050
|
-
.output(out_n,
|
|
1191
|
+
.output(out_n, vcodec='copy',
|
|
1192
|
+
# .output(out_n, shortest=None, vcodec='copy',
|
|
1051
1193
|
# metadata='reel_name=foo', not all container support gen MD
|
|
1052
1194
|
timecode=timecode,
|
|
1053
1195
|
)
|
|
@@ -1085,7 +1227,7 @@ class Matcher:
|
|
|
1085
1227
|
recordings : list of Recording instances
|
|
1086
1228
|
all the scanned recordings with valid TicTacCode, set in __init__()
|
|
1087
1229
|
|
|
1088
|
-
|
|
1230
|
+
mergers : list
|
|
1089
1231
|
of AudioStitcherVideoMerger Class instances, built by
|
|
1090
1232
|
scan_audio_for_each_videoclip(); each video has a corresponding
|
|
1091
1233
|
AudioStitcherVideoMerger object. An audio_stitch doesn't extend
|
|
@@ -1102,7 +1244,7 @@ class Matcher:
|
|
|
1102
1244
|
|
|
1103
1245
|
"""
|
|
1104
1246
|
self.recordings = recordings_list
|
|
1105
|
-
self.
|
|
1247
|
+
self.mergers = []
|
|
1106
1248
|
|
|
1107
1249
|
def _rename_all_recs(self):
|
|
1108
1250
|
"""
|
|
@@ -1150,12 +1292,12 @@ class Matcher:
|
|
|
1150
1292
|
logger.debug('recording %s overlaps,'%(audio))
|
|
1151
1293
|
# print(' recording [gold1]%s[/gold1] overlaps,'%(audio))
|
|
1152
1294
|
if len(audio_stitch.get_matched_audio_recs()) > 0:
|
|
1153
|
-
self.
|
|
1295
|
+
self.mergers.append(audio_stitch)
|
|
1154
1296
|
else:
|
|
1155
1297
|
logger.debug('\n nothing\n')
|
|
1156
1298
|
print('No overlap found for %s'%videoclip.AVpath.name)
|
|
1157
1299
|
del audio_stitch
|
|
1158
|
-
logger.debug('%i
|
|
1300
|
+
logger.debug('%i mergers created'%len(self.mergers))
|
|
1159
1301
|
|
|
1160
1302
|
def _does_overlap(self, videoclip, audio_rec):
|
|
1161
1303
|
A1, A2 = audio_rec.get_start_time(), audio_rec.get_end_time()
|
|
@@ -1194,7 +1336,7 @@ Moves clusters at the timelineoffset
|
|
|
1194
1336
|
Returns nothing, changes are done in the video files metadata
|
|
1195
1337
|
(each referenced by Recording.final_synced_file)
|
|
1196
1338
|
"""
|
|
1197
|
-
vids = [m.videoclip for m in self.
|
|
1339
|
+
vids = [m.videoclip for m in self.mergers]
|
|
1198
1340
|
logger.debug('vids %s'%vids)
|
|
1199
1341
|
if len(vids) == 1:
|
|
1200
1342
|
logger.debug('just one take, no gap to shrink')
|
|
@@ -35,7 +35,6 @@ TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
|
|
|
35
35
|
|
|
36
36
|
CACHING = True
|
|
37
37
|
DEL_TEMP = False
|
|
38
|
-
DB_RMS_SILENCE_SOX = -58
|
|
39
38
|
MAXDRIFT = 15e-3 # in sec, for end of clip
|
|
40
39
|
|
|
41
40
|
|
|
@@ -566,25 +565,24 @@ class Recording:
|
|
|
566
565
|
implicitly True for each video recordings (but not set)
|
|
567
566
|
|
|
568
567
|
device_relative_speed : float
|
|
569
|
-
Set by
|
|
570
568
|
the ratio of the recording device clock speed relative to the
|
|
571
569
|
video recorder clock device, in order to correct clock drift with
|
|
572
570
|
pysox tempo transform. If value < 1.0 then the recording is
|
|
573
571
|
slower than video recorder. Updated by each
|
|
574
|
-
|
|
572
|
+
MediaMerger instance so the value can change
|
|
575
573
|
depending on the video recording . A mean is calculated for all
|
|
576
574
|
recordings of the same device in
|
|
577
|
-
|
|
575
|
+
MediaMerger._get_concatenated_audiofile_for()
|
|
578
576
|
|
|
579
577
|
time_position : float
|
|
580
578
|
The time (in seconds) at which the recording starts relative to the
|
|
581
|
-
video recording. Updated by each
|
|
579
|
+
video recording. Updated by each MediaMerger
|
|
582
580
|
instance so the value can change depending on the video
|
|
583
581
|
recording (a video or main sound).
|
|
584
582
|
|
|
585
583
|
final_synced_file : a pathlib.Path
|
|
586
584
|
contains the path of the merged video file after the call to
|
|
587
|
-
AudioStitcher.
|
|
585
|
+
AudioStitcher.build_audio_and_write_merged_media if the Recording is a
|
|
588
586
|
video recording, relative to the working directory
|
|
589
587
|
|
|
590
588
|
synced_audio : pathlib.Path
|
|
@@ -792,8 +790,8 @@ Set by
|
|
|
792
790
|
def get_corrected_duration(self):
|
|
793
791
|
"""
|
|
794
792
|
uses device_relative_speed to compute corrected duration. Updated by
|
|
795
|
-
each
|
|
796
|
-
|
|
793
|
+
each MediaMerger object in
|
|
794
|
+
MediaMerger._get_concatenated_audiofile_for()
|
|
797
795
|
"""
|
|
798
796
|
return self.get_duration()/self.device_relative_speed
|
|
799
797
|
|
|
@@ -928,7 +926,7 @@ Set by
|
|
|
928
926
|
def set_time_position_to(self, video_clip):
|
|
929
927
|
"""
|
|
930
928
|
Sets self.time_position, the time (in seconds) at which the recording
|
|
931
|
-
starts relative to the video recording. Updated by each
|
|
929
|
+
starts relative to the video recording. Updated by each MediaMerger
|
|
932
930
|
instance so the value can change depending on the video
|
|
933
931
|
recording (a video or main sound).
|
|
934
932
|
|
|
@@ -1074,13 +1072,12 @@ Set by
|
|
|
1074
1072
|
return ratio/ratio_ref
|
|
1075
1073
|
|
|
1076
1074
|
def get_samplerate(self):
|
|
1077
|
-
#
|
|
1075
|
+
# returns int samplerate (nominal)
|
|
1078
1076
|
string = self._ffprobe_audio_stream()['sample_rate']
|
|
1079
1077
|
logger.debug('ffprobe samplerate: %s'%string)
|
|
1080
|
-
return eval(string)
|
|
1078
|
+
return eval(string)
|
|
1081
1079
|
|
|
1082
1080
|
def get_framerate(self):
|
|
1083
|
-
# return int samplerate (nominal)
|
|
1084
1081
|
string = self._ffprobe_video_stream()['avg_frame_rate']
|
|
1085
1082
|
return eval(string) # eg eval(24000/1001)
|
|
1086
1083
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.97a0
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -73,19 +73,22 @@ The program `tictacsync` will recursively scan the directory given as argument,
|
|
|
73
73
|
If shooting multicam, put clips in their respective directories (using the camera name as folder name) _and_ the audio under their own directory. `tictacsync` will detect that structured input and will generate multicam folders ready to be imported into your NLE (for now only DaVinci Resolve has been validated).
|
|
74
74
|
|
|
75
75
|
## Options
|
|
76
|
+
#### `-v`
|
|
76
77
|
|
|
77
78
|
For a very verbose output add the `-v` flag:
|
|
78
79
|
|
|
79
80
|
> tictacsync -v dailies/loose/MVI_0024.MP4
|
|
80
|
-
|
|
81
|
+
#### `--terse`
|
|
81
82
|
For a one line output (or to suppress the progress bars) use the `--terse` flag:
|
|
82
83
|
|
|
83
84
|
> tictacsync --terse dailies/loose/MVI_0024.MP4
|
|
84
85
|
dailies/loose/MVI_0024.MP4 UTC:2024-03-12 23:07:01.4281 pulse: 27450 in chan 0
|
|
86
|
+
#### `--isos`
|
|
85
87
|
|
|
86
|
-
|
|
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/).
|
|
87
89
|
|
|
88
|
-
> tictacsync --isos dailies/
|
|
90
|
+
> tictacsync --isos dailies/day01
|
|
91
|
+
#### `-p`
|
|
89
92
|
|
|
90
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:
|
|
91
94
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|