tictacsync 0.91a0__py3-none-any.whl → 0.95a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tictacsync/device_scanner.py +64 -32
- tictacsync/entry.py +96 -54
- tictacsync/multi2polywav.py +1 -1
- tictacsync/timeline.py +113 -65
- tictacsync/yaltc.py +39 -21
- {tictacsync-0.91a0.dist-info → tictacsync-0.95a0.dist-info}/METADATA +1 -1
- tictacsync-0.95a0.dist-info/RECORD +15 -0
- tictacsync-0.91a0.dist-info/RECORD +0 -15
- {tictacsync-0.91a0.dist-info → tictacsync-0.95a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.95a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.95a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.95a0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -69,6 +69,7 @@ class Tracks:
|
|
|
69
69
|
others: list #of all other tags: (tag, track#) tuples
|
|
70
70
|
rawtrx: list # list of strings read from file
|
|
71
71
|
error_msg: str # 'None' if none
|
|
72
|
+
lag_values: list # list of lag in ms, entry is None if not specified.
|
|
72
73
|
|
|
73
74
|
@dataclass
|
|
74
75
|
class Device:
|
|
@@ -109,7 +110,13 @@ def media_at_path(input_structure, p):
|
|
|
109
110
|
if stream['codec_type']=='audio'
|
|
110
111
|
]
|
|
111
112
|
if len(audio_streams) > 1:
|
|
112
|
-
|
|
113
|
+
print('for [gold1]%s[/gold1], ffprobe gave multiple audio streams, quitting.'%p)
|
|
114
|
+
quit()
|
|
115
|
+
# raise Exception('ffprobe gave multiple audio streams?')
|
|
116
|
+
if len(audio_streams) == 0:
|
|
117
|
+
print('ffprobe gave no audio stream for [gold1]%s[/gold1], quitting.'%p)
|
|
118
|
+
quit()
|
|
119
|
+
# raise Exception('ffprobe gave no audio stream for %s, quitting'%p)
|
|
113
120
|
audio_str = audio_streams[0]
|
|
114
121
|
n = audio_str['channels']
|
|
115
122
|
# pprint(ffmpeg.probe(p))
|
|
@@ -146,6 +153,7 @@ def get_device_ffprobe_UID(file):
|
|
|
146
153
|
print(e.stderr, file)
|
|
147
154
|
return None, None #-----------------------------------------------------
|
|
148
155
|
# fall back to folder name
|
|
156
|
+
logger.debug('ffprobe %s'%probe)
|
|
149
157
|
streams = probe['streams']
|
|
150
158
|
codecs = [stream['codec_type'] for stream in streams]
|
|
151
159
|
device_type = 'CAM' if 'video' in codecs else 'REC'
|
|
@@ -155,6 +163,7 @@ def get_device_ffprobe_UID(file):
|
|
|
155
163
|
probe_lines = [l for l in probe_string.split('\n')
|
|
156
164
|
if '_time' not in l
|
|
157
165
|
and 'time_' not in l
|
|
166
|
+
and 'location' not in l
|
|
158
167
|
and 'date' not in l ]
|
|
159
168
|
# this removes any metadata related to the file
|
|
160
169
|
# but keeps metadata related to the device
|
|
@@ -302,12 +311,12 @@ class Scanner:
|
|
|
302
311
|
audio_devices = [d for d in devices if d.dev_type == 'REC']
|
|
303
312
|
for recorder in audio_devices:
|
|
304
313
|
recorder.tracks = self._get_tracks_from_file(recorder)
|
|
314
|
+
if recorder.tracks:
|
|
315
|
+
if not all([lv == None for lv in recorder.tracks.lag_values]):
|
|
316
|
+
logger.debug('%s has lag_values %s'%(
|
|
317
|
+
recorder.name, recorder.tracks.lag_values))
|
|
305
318
|
no_name_devices = [m.device for m in self.found_media_files
|
|
306
319
|
if not m.device.name]
|
|
307
|
-
# if no_name_devices:
|
|
308
|
-
# pprint_no_name = pformat([(d, self.get_media_for_device(d)) for d in no_name_devices])
|
|
309
|
-
# print('those are anon devices%s\n'%pprint_no_name)
|
|
310
|
-
# logger.debug('those media have anon device%s'%no_name_devices)
|
|
311
320
|
for anon_dev in no_name_devices:
|
|
312
321
|
medias = self.get_media_for_device(anon_dev)
|
|
313
322
|
guess_name = _try_name(medias)
|
|
@@ -321,7 +330,7 @@ class Scanner:
|
|
|
321
330
|
|
|
322
331
|
def _get_tracks_from_file(self, device) -> Tracks:
|
|
323
332
|
"""
|
|
324
|
-
Look for track names in TRACKSFN file,
|
|
333
|
+
Look for eventual track names in TRACKSFN file, stored inside the
|
|
325
334
|
recorder folder alongside the audio files. If there, returns a Tracks
|
|
326
335
|
object, if not returns None.
|
|
327
336
|
"""
|
|
@@ -477,14 +486,16 @@ class Scanner:
|
|
|
477
486
|
read track names for naming separated ISOs
|
|
478
487
|
from tracks_file.
|
|
479
488
|
|
|
489
|
+
tokens looked for: mix mixL mixR 0 ttc
|
|
490
|
+
|
|
480
491
|
repeting prefixes signals a stereo track
|
|
481
492
|
and entries will correspondingly panned into
|
|
482
493
|
a stero mix named mixL.wav and mixR.wav
|
|
483
494
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
495
|
+
xyz L # spaces are ignored |
|
|
496
|
+
zyz R | stereo pair
|
|
497
|
+
abc L
|
|
498
|
+
abc R
|
|
488
499
|
|
|
489
500
|
mixL
|
|
490
501
|
|
|
@@ -502,41 +513,56 @@ class Scanner:
|
|
|
502
513
|
ch = [c for c in chaine if c != ' ']
|
|
503
514
|
return ''.join(ch)
|
|
504
515
|
def _WO_LR(chaine):
|
|
505
|
-
ch = [c for c in chaine if c not in '
|
|
516
|
+
ch = [c for c in chaine if c not in 'LR']
|
|
506
517
|
return ''.join(ch)
|
|
507
518
|
def _seemsStereoMic(tag):
|
|
508
519
|
# is tag likely a stereo pair tag?
|
|
509
|
-
# should start with 'mic' and end with '
|
|
510
|
-
return tag[:3]=='mic' and tag[-1] in '
|
|
520
|
+
# should start with 'mic' and end with 'L' or 'R'
|
|
521
|
+
return tag[:3]=='mic' and tag[-1] in 'LR'
|
|
511
522
|
file=open(tracks_file,"r")
|
|
512
523
|
whole_txt = file.read()
|
|
513
524
|
logger.debug('all_lines:\n%s'%whole_txt)
|
|
514
|
-
tracks_lines = [l.split('#')[0] for l in whole_txt.splitlines()
|
|
515
|
-
|
|
525
|
+
tracks_lines = [l.split('#')[0] for l in whole_txt.splitlines()
|
|
526
|
+
if len(l) > 0 ]
|
|
527
|
+
tracks_lines = [_WOspace(l) for l in tracks_lines if len(l) > 0 ]
|
|
516
528
|
rawtrx = tracks_lines
|
|
529
|
+
# add index with tuples, starting at 1
|
|
530
|
+
logger.debug('tracks_lines whole: %s'%tracks_lines)
|
|
531
|
+
def _detach_lag_value(line):
|
|
532
|
+
# look for ";number" ending any line, returns a two-list
|
|
533
|
+
splt = line.split(';')
|
|
534
|
+
if len(splt) == 1:
|
|
535
|
+
splt += [None]
|
|
536
|
+
if len(splt) != 2:
|
|
537
|
+
# error
|
|
538
|
+
print('Text error in %s, line %s has too many ";"'%(
|
|
539
|
+
tracks_file, line))
|
|
540
|
+
return splt
|
|
541
|
+
tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
|
|
542
|
+
in tracks_lines])
|
|
543
|
+
logger.debug('tracks_lines WO lag: %s'%[tracks_lines])
|
|
544
|
+
logger.debug('lag_values: %s'%[lag_values])
|
|
517
545
|
tracks_lines = [(t,ix+1) for ix,t in enumerate(tracks_lines)]
|
|
518
|
-
# tracks_lines = [l for l in tracks_lines if l not in ['ttc', '0']]
|
|
519
|
-
# track_names = [l.split()[0] for l in tracks_lines if l != 'ttc']
|
|
520
|
-
logger.debug('tracks_lines: %s'%tracks_lines)
|
|
521
546
|
# first check for stereo mic pairs (could be more than one pair):
|
|
522
547
|
spairs = [e for e in tracks_lines if _seemsStereoMic(e[0])]
|
|
523
548
|
# spairs is stereo pairs candidates
|
|
524
549
|
msg = 'Confusing stereo pair tags: %s'%' '.join([e[0]
|
|
525
550
|
for e in spairs])
|
|
526
|
-
error_output_stereo = Tracks(None,[],[],[],[],[],msg)
|
|
551
|
+
error_output_stereo = Tracks(None,[],[],[],[],[],msg,[])
|
|
527
552
|
if len(spairs)%2 == 1: # not pairs?, quit.
|
|
528
553
|
return error_output_stereo
|
|
529
554
|
logger.debug('_seemsStereoM: %s'%spairs)
|
|
530
|
-
output_tracks = Tracks(None,[],[],[],[],rawtrx,None)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
output_tracks = Tracks(None,[],[],[],[],rawtrx,None,[])
|
|
556
|
+
output_tracks.lag_values = lag_values
|
|
557
|
+
# def _LR(p):
|
|
558
|
+
# # p = (('mic1l', 1), ('mic1r', 2))
|
|
559
|
+
# # check if L then R
|
|
560
|
+
# p1, p2 = p
|
|
561
|
+
# return p1[0][-1] == 'l' and p2[0][-1] == 'r'
|
|
536
562
|
if spairs:
|
|
537
563
|
even_idxes = range(0,len(spairs),2)
|
|
538
564
|
paired = [(spairs[i], spairs[i+1]) for i in even_idxes]
|
|
539
|
-
# eg [(('
|
|
565
|
+
# eg [(('mic1L', 1), ('mic1R', 2)), (('mic2L', 3), ('mic2R', 4))]
|
|
540
566
|
def _mic_same(p):
|
|
541
567
|
# p = (('mic1l', 1), ('mic1r', 2))
|
|
542
568
|
# check if mic1 == mic1
|
|
@@ -546,16 +572,22 @@ class Scanner:
|
|
|
546
572
|
logger.debug('mic_prefix_OK: %s'%mic_prefix_OK)
|
|
547
573
|
if not mic_prefix_OK:
|
|
548
574
|
return error_output_stereo
|
|
549
|
-
mic_LR_OK = all([_LR(p) for p in paired])
|
|
550
|
-
logger.debug('mic_LR_OK %s'%mic_LR_OK)
|
|
551
|
-
if not mic_LR_OK:
|
|
552
|
-
|
|
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
|
|
553
579
|
def _stereo_mic_pref_chan(p):
|
|
554
|
-
# p = (('
|
|
580
|
+
# p = (('mic1R', 1), ('mic1L', 2))
|
|
555
581
|
# returns ('mic1', (1,2))
|
|
556
582
|
first, second = p
|
|
557
583
|
mic_prefix = _WO_LR(first[0])
|
|
558
|
-
|
|
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]) )
|
|
559
591
|
grouped_stereo_mic_channels = [_stereo_mic_pref_chan(p) for p
|
|
560
592
|
in paired]
|
|
561
593
|
logger.debug('grouped_stereo_mic_channels: %s'%
|
tictacsync/entry.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
print('Loading modules...', end='')
|
|
2
2
|
|
|
3
3
|
# I know, the following is ugly, but I need those try's to
|
|
4
4
|
# run the command in my dev setting AND from
|
|
@@ -16,11 +16,11 @@ except:
|
|
|
16
16
|
import timeline
|
|
17
17
|
import multi2polywav
|
|
18
18
|
|
|
19
|
-
import argparse
|
|
19
|
+
import argparse, tempfile
|
|
20
20
|
from loguru import logger
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
# import os, sys
|
|
23
|
-
import os, sys
|
|
23
|
+
import os, sys, sox
|
|
24
24
|
from rich.progress import track
|
|
25
25
|
# from pprint import pprint
|
|
26
26
|
from rich.console import Console
|
|
@@ -29,6 +29,8 @@ from rich.table import Table
|
|
|
29
29
|
from rich import print
|
|
30
30
|
from pprint import pprint
|
|
31
31
|
|
|
32
|
+
DEL_TEMP = False
|
|
33
|
+
|
|
32
34
|
av_file_extensions = \
|
|
33
35
|
"""MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
|
|
34
36
|
wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
|
|
@@ -40,7 +42,7 @@ logger.level("DEBUG", color="<yellow>")
|
|
|
40
42
|
|
|
41
43
|
def process_files_with_progress_bars(medias):
|
|
42
44
|
recordings = []
|
|
43
|
-
|
|
45
|
+
rec_with_TTC = []
|
|
44
46
|
times = []
|
|
45
47
|
for m in track(medias,
|
|
46
48
|
description="1/4 Initializing Recordings:"):
|
|
@@ -49,25 +51,25 @@ def process_files_with_progress_bars(medias):
|
|
|
49
51
|
for r in track(recordings,
|
|
50
52
|
description="2/4 Looking for TicTacCode:"):
|
|
51
53
|
if r.seems_to_have_TicTacCode_at_beginning():
|
|
52
|
-
|
|
53
|
-
for r in track(
|
|
54
|
+
rec_with_TTC.append(r)
|
|
55
|
+
for r in track(rec_with_TTC,
|
|
54
56
|
description="3/4 Finding start times:"):
|
|
55
57
|
times.append(r.get_start_time())
|
|
56
|
-
return recordings,
|
|
58
|
+
return recordings, rec_with_TTC, times
|
|
57
59
|
|
|
58
60
|
def process_files(medias):
|
|
59
61
|
recordings = []
|
|
60
|
-
|
|
62
|
+
rec_with_TTC = []
|
|
61
63
|
times = []
|
|
62
64
|
for m in medias:
|
|
63
65
|
recordings.append(yaltc.Recording(m))
|
|
64
66
|
for r in recordings:
|
|
65
67
|
# print('%s duration %.2fs'%(r.AVpath.name, r.get_duration()))
|
|
66
68
|
if r.seems_to_have_TicTacCode_at_beginning():
|
|
67
|
-
|
|
68
|
-
for r in
|
|
69
|
+
rec_with_TTC.append(r)
|
|
70
|
+
for r in rec_with_TTC:
|
|
69
71
|
times.append(r.get_start_time())
|
|
70
|
-
return recordings,
|
|
72
|
+
return recordings, rec_with_TTC, times
|
|
71
73
|
|
|
72
74
|
def process_single(file, args):
|
|
73
75
|
# argument is a single file
|
|
@@ -97,6 +99,42 @@ def process_single(file, args):
|
|
|
97
99
|
print('Start time couldnt be determined')
|
|
98
100
|
sys.exit(1)
|
|
99
101
|
|
|
102
|
+
def process_lag_adjustement(media_object):
|
|
103
|
+
# trim channels that are lagging (as stated in tracks.txt)
|
|
104
|
+
# replace the old file, and rename the old one with .wavbk
|
|
105
|
+
# if .wavbk exist, process was done already, so dont process
|
|
106
|
+
# returns nothing
|
|
107
|
+
lags = media_object.device.tracks.lag_values
|
|
108
|
+
logger.debug('will process %s lags'%[lags])
|
|
109
|
+
channels = timeline._split_channels(media_object.path)
|
|
110
|
+
# add bk to file on filesystem, but media_object.path is unchanged (?)
|
|
111
|
+
backup_name = str(media_object.path) + 'bk'
|
|
112
|
+
if Path(backup_name).exists():
|
|
113
|
+
logger.debug('%s exists, so return now.'%backup_name)
|
|
114
|
+
return
|
|
115
|
+
media_object.path.replace(backup_name)
|
|
116
|
+
logger.debug('channels %s'%channels)
|
|
117
|
+
def _trim(lag, chan_file):
|
|
118
|
+
if lag == None:
|
|
119
|
+
return chan_file
|
|
120
|
+
else:
|
|
121
|
+
logger.debug('process %s for lag of %s'%(chan_file, lag))
|
|
122
|
+
sox_transform = sox.Transformer()
|
|
123
|
+
sox_transform.trim(float(lag)*1e-3)
|
|
124
|
+
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
125
|
+
out_file = timeline._pathname(output_fh)
|
|
126
|
+
input_file = timeline._pathname(chan_file)
|
|
127
|
+
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
128
|
+
logger.debug('calling sox_transform.build()')
|
|
129
|
+
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
130
|
+
logger.debug('sox.build exit code %s'%str(status))
|
|
131
|
+
return output_fh
|
|
132
|
+
new_channels = [_trim(*e) for e in zip(lags, channels)]
|
|
133
|
+
logger.debug('new_channels %s'%new_channels)
|
|
134
|
+
trimmed_multichanfile = timeline._sox_combine(new_channels)
|
|
135
|
+
logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
|
|
136
|
+
Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
|
|
137
|
+
|
|
100
138
|
def main():
|
|
101
139
|
parser = argparse.ArgumentParser()
|
|
102
140
|
parser.add_argument(
|
|
@@ -107,32 +145,46 @@ def main():
|
|
|
107
145
|
)
|
|
108
146
|
# parser.add_argument("directory", nargs="?", help="path of media directory")
|
|
109
147
|
# parser.add_argument('-v', action='store_true')
|
|
110
|
-
parser.add_argument('-v',
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
parser.add_argument('-v',
|
|
149
|
+
action='store_true', #ie default False
|
|
150
|
+
dest='verbose_output',
|
|
151
|
+
help='Set verbose ouput')
|
|
113
152
|
parser.add_argument('-o', nargs=1,
|
|
114
153
|
help='Where to write the SyncedMedia folder [default to "path" ]')
|
|
115
|
-
parser.add_argument('-
|
|
154
|
+
parser.add_argument('-t','--timelineoffset',
|
|
155
|
+
nargs=1,
|
|
156
|
+
default=['00:00:00:00'],
|
|
157
|
+
dest='timelineoffset',
|
|
158
|
+
help='When processing multicam, where to place clips on NLE timeline (HH:MM:SS:FF)')
|
|
159
|
+
parser.add_argument('-p',
|
|
160
|
+
action='store_true',
|
|
116
161
|
dest='plotting',
|
|
117
162
|
help='Produce plots')
|
|
118
|
-
parser.add_argument('--isos',
|
|
163
|
+
parser.add_argument('--isos',
|
|
164
|
+
action='store_true',
|
|
119
165
|
dest='write_ISOs',
|
|
120
166
|
help='Write ISO sound files')
|
|
121
|
-
parser.add_argument('--nosync',
|
|
167
|
+
parser.add_argument('--nosync',
|
|
168
|
+
action='store_true',
|
|
122
169
|
dest='nosync',
|
|
123
170
|
help='Just scan and decode')
|
|
124
|
-
parser.add_argument('--terse',
|
|
171
|
+
parser.add_argument('--terse',
|
|
172
|
+
action='store_true',
|
|
125
173
|
dest='terse',
|
|
126
174
|
help='Terse output')
|
|
127
175
|
args = parser.parse_args()
|
|
176
|
+
# print(args)
|
|
177
|
+
if len(args.timelineoffset) != 1:
|
|
178
|
+
print('--timelineoffset needs one value, got %s'%args.timelineoffset)
|
|
179
|
+
quit()
|
|
128
180
|
if args.verbose_output:
|
|
129
181
|
logger.add(sys.stderr, level="DEBUG")
|
|
130
182
|
# logger.add(sys.stdout, filter="__main__")
|
|
131
183
|
# logger.add(sys.stdout, filter="yaltc")
|
|
132
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
133
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
134
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
135
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
184
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "process_lag_adjustement")
|
|
185
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_dedrift_rec")
|
|
186
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
187
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_parse_track_values")
|
|
136
188
|
top_dir = args.path[0]
|
|
137
189
|
if os.path.isfile(top_dir):
|
|
138
190
|
file = top_dir
|
|
@@ -151,7 +203,13 @@ def main():
|
|
|
151
203
|
sys.exit(1)
|
|
152
204
|
multi2polywav.poly_all(top_dir)
|
|
153
205
|
scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
|
|
154
|
-
scanner.scan_media_and_build_devices_UID()
|
|
206
|
+
scanner.scan_media_and_build_devices_UID()
|
|
207
|
+
for m in scanner.found_media_files:
|
|
208
|
+
if m.device.tracks:
|
|
209
|
+
if not all([lv == None for lv in m.device.tracks.lag_values]):
|
|
210
|
+
logger.debug('%s has lag_values %s'%(
|
|
211
|
+
m.path, m.device.tracks.lag_values))
|
|
212
|
+
process_lag_adjustement(m)
|
|
155
213
|
if not args.terse:
|
|
156
214
|
if scanner.input_structure == 'folder_is_device':
|
|
157
215
|
print('\nDetected structured folders', end='')
|
|
@@ -162,10 +220,8 @@ def main():
|
|
|
162
220
|
else:
|
|
163
221
|
print('\nDetected loose structure')
|
|
164
222
|
if scanner.CAM_numbers() > 1:
|
|
165
|
-
print('\nNote: different CAMs are present, will sync audio')
|
|
166
|
-
print('
|
|
167
|
-
print('respective timecode for NLE timeline alignement')
|
|
168
|
-
print('you should regroup clips by CAM under their own DIR.')
|
|
223
|
+
print('\nNote: different CAMs are present, will sync audio for each of them but if you want to set their')
|
|
224
|
+
print('respective timecode for NLE timeline alignement you should regroup clips by CAM under their own DIR.')
|
|
169
225
|
print('\nFound [gold1]%i[/gold1] media files '%(
|
|
170
226
|
len(scanner.found_media_files)), end='')
|
|
171
227
|
print('from [gold1]%i[/gold1] devices:\n'%(
|
|
@@ -177,15 +233,11 @@ def main():
|
|
|
177
233
|
print('[gold1]%s[/gold1]'%m.path.name, end=', ')
|
|
178
234
|
print('[gold1]%s[/gold1]'%medias[-1].path.name)
|
|
179
235
|
print()
|
|
180
|
-
# if args.verbose_output or args.terse: # verbose or terse, so no progress bars
|
|
181
|
-
# rez = process_files(scanner.found_media_files)
|
|
182
|
-
# else:
|
|
183
|
-
# rez = process_files_with_progress_bars(scanner.found_media_files)
|
|
184
236
|
rez = process_files(scanner.found_media_files)
|
|
185
|
-
recordings,
|
|
237
|
+
recordings, rec_with_TTC, times = rez
|
|
186
238
|
recordings_with_time = [
|
|
187
239
|
rec
|
|
188
|
-
for rec in
|
|
240
|
+
for rec in rec_with_TTC
|
|
189
241
|
if rec.get_start_time()
|
|
190
242
|
]
|
|
191
243
|
if not args.terse:
|
|
@@ -199,7 +251,7 @@ def main():
|
|
|
199
251
|
table.add_column("Date\n", justify="center", style='gold1')
|
|
200
252
|
rec_WO_time = [
|
|
201
253
|
rec.AVpath.name
|
|
202
|
-
for rec in
|
|
254
|
+
for rec in rec_with_TTC
|
|
203
255
|
if not rec.get_start_time()
|
|
204
256
|
]
|
|
205
257
|
if rec_WO_time:
|
|
@@ -226,9 +278,6 @@ def main():
|
|
|
226
278
|
print()
|
|
227
279
|
n_devices = scanner.get_devices_number()
|
|
228
280
|
OUT_struct_for_mcam = scanner.top_dir_has_multicam
|
|
229
|
-
# if n_devices > 2:
|
|
230
|
-
# print('\nMerging for more than 2 devices is not implemented yet, quitting...')
|
|
231
|
-
# sys.exit(1)
|
|
232
281
|
if len(recordings_with_time) < 2:
|
|
233
282
|
if not args.terse:
|
|
234
283
|
print('\nNothing to sync, exiting.\n')
|
|
@@ -245,31 +294,24 @@ def main():
|
|
|
245
294
|
asked_ISOs = False
|
|
246
295
|
output_dir = args.o
|
|
247
296
|
# if args.verbose_output or args.terse: # verbose, so no progress bars
|
|
248
|
-
for
|
|
249
|
-
|
|
297
|
+
for merger in matcher.video_mergers:
|
|
298
|
+
merger.build_audio_and_write_video(top_dir, arg_out_dir,
|
|
250
299
|
OUT_struct_for_mcam,
|
|
251
300
|
asked_ISOs,)
|
|
252
|
-
# else:
|
|
253
|
-
# print()
|
|
254
|
-
# for stitcher in track(matcher.video_mergers,
|
|
255
|
-
# description="4/4 Merging sound to videos:"):
|
|
256
|
-
# stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
|
|
257
|
-
# OUT_struct_for_mcam,
|
|
258
|
-
# asked_ISOs,)
|
|
259
301
|
if not args.terse:
|
|
260
302
|
print("\n")
|
|
261
303
|
# find out where files were wrtitten
|
|
262
|
-
|
|
304
|
+
a_merger = matcher.video_mergers[0]
|
|
263
305
|
print('\nWrote output in folder [gold1]%s[/gold1]'%(
|
|
264
|
-
|
|
265
|
-
for
|
|
266
|
-
print('[gold1]%s[/gold1]'%
|
|
267
|
-
for audio in
|
|
306
|
+
a_merger.synced_clip_dir))
|
|
307
|
+
for merger in matcher.video_mergers:
|
|
308
|
+
print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
|
|
309
|
+
for audio in merger.get_matched_audio_recs():
|
|
268
310
|
print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
|
|
269
|
-
new_file =
|
|
270
|
-
print(' became [gold1]%s[/gold1]'%
|
|
311
|
+
new_file = merger.videoclip.final_synced_file.parts
|
|
312
|
+
print(' became [gold1]%s[/gold1]'%merger.videoclip.final_synced_file.name)
|
|
271
313
|
# matcher._build_otio_tracks_for_cam()
|
|
272
|
-
matcher.shrink_gaps_between_takes()
|
|
314
|
+
matcher.shrink_gaps_between_takes(args.timelineoffset)
|
|
273
315
|
sys.exit(0)
|
|
274
316
|
|
|
275
317
|
if __name__ == '__main__':
|
tictacsync/multi2polywav.py
CHANGED
tictacsync/timeline.py
CHANGED
|
@@ -303,7 +303,7 @@ class AudioStitcherVideoMerger:
|
|
|
303
303
|
Typically each found video is associated with an AudioStitcherVideoMerger
|
|
304
304
|
instance. AudioStitcherVideoMerger does the actual audio-video file
|
|
305
305
|
processing of merging AudioStitcherVideoMerger.videoclip (gen. a video)
|
|
306
|
-
with all audio files in AudioStitcherVideoMerger.
|
|
306
|
+
with all audio files in AudioStitcherVideoMerger.soxed_audio as
|
|
307
307
|
determined by the Matcher object (Matcher instanciates and manages
|
|
308
308
|
AudioStitcherVideoMerger objects).
|
|
309
309
|
|
|
@@ -317,46 +317,54 @@ class AudioStitcherVideoMerger:
|
|
|
317
317
|
project.
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
Class attribute
|
|
321
|
+
|
|
322
|
+
tempoed_recs : dict as {Recording : path}
|
|
323
|
+
|
|
324
|
+
a cache for already time-stretched audio files. Keys are elements
|
|
325
|
+
of matched_audio_recordings and the value are tuples:
|
|
326
|
+
(factor, file_handle), the file_handle points to the precedently
|
|
327
|
+
produced NamedTemporaryFile; factor is the value that was used in
|
|
328
|
+
the sox tempo transform.
|
|
329
|
+
|
|
320
330
|
Attributes:
|
|
321
331
|
|
|
322
332
|
videoclip : a Recording instance
|
|
323
333
|
The video to which audio files are synced
|
|
324
334
|
|
|
325
|
-
|
|
326
|
-
keys are elements of matched_audio_recordings
|
|
327
|
-
|
|
328
|
-
audio (trimmed , padded or time stretched). Before building the
|
|
329
|
-
audio_montage, path points to the initial
|
|
330
|
-
Recording.valid_sound
|
|
335
|
+
soxed_audio : dict as {Recording : path}
|
|
336
|
+
keys are elements of matched_audio_recordings and the value are
|
|
337
|
+
the Pathlib path of the eventual edited audio(trimmed or padded).
|
|
331
338
|
|
|
332
339
|
synced_clip_dir : Path
|
|
333
340
|
where synced clips are written
|
|
334
341
|
|
|
335
342
|
"""
|
|
343
|
+
tempoed_recs = {}
|
|
336
344
|
|
|
337
345
|
def __init__(self, video_clip):
|
|
338
346
|
self.videoclip = video_clip
|
|
339
347
|
# self.matched_audio_recordings = []
|
|
340
|
-
self.
|
|
348
|
+
self.soxed_audio = {}
|
|
341
349
|
logger.debug('instantiating AudioStitcherVideoMerger for %s'%
|
|
342
350
|
video_clip)
|
|
343
351
|
|
|
344
352
|
def add_matched_audio(self, audio_rec):
|
|
345
353
|
"""
|
|
346
|
-
Populates AudioStitcherVideoMerger.
|
|
354
|
+
Populates AudioStitcherVideoMerger.soxed_audio,
|
|
347
355
|
a dict as {Recording : path}
|
|
348
356
|
|
|
349
357
|
This fct is called
|
|
350
358
|
within Matcher.scan_audio_for_each_videoclip()
|
|
351
359
|
|
|
352
|
-
Returns nothing, fills self.
|
|
360
|
+
Returns nothing, fills self.soxed_audio dict with
|
|
353
361
|
matched audio.
|
|
354
362
|
|
|
355
363
|
"""
|
|
356
|
-
self.
|
|
364
|
+
self.soxed_audio[audio_rec] = audio_rec.AVpath
|
|
357
365
|
"""
|
|
358
|
-
Here at this point, self.
|
|
359
|
-
after a call to _edit_audio_file(),
|
|
366
|
+
Here at this point, self.soxed_audio[audio_rec] is unedited but
|
|
367
|
+
after a call to _edit_audio_file(), soxed_audio[audio_rec] points to
|
|
360
368
|
a new file and audio_rec.AVpath is unchanged.
|
|
361
369
|
"""
|
|
362
370
|
return
|
|
@@ -364,9 +372,9 @@ class AudioStitcherVideoMerger:
|
|
|
364
372
|
def get_matched_audio_recs(self):
|
|
365
373
|
"""
|
|
366
374
|
Returns audio recordings that overlap self.videoclip.
|
|
367
|
-
Simply keys of self.
|
|
375
|
+
Simply keys of self.soxed_audio dict
|
|
368
376
|
"""
|
|
369
|
-
return list(self.
|
|
377
|
+
return list(self.soxed_audio.keys())
|
|
370
378
|
|
|
371
379
|
def _get_audio_devices(self):
|
|
372
380
|
devices = set([r.device for r in self.get_matched_audio_recs()])
|
|
@@ -382,58 +390,77 @@ class AudioStitcherVideoMerger:
|
|
|
382
390
|
return recs
|
|
383
391
|
|
|
384
392
|
def _dedrift_rec(self, rec):
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
sox_transform = sox.Transformer()
|
|
388
|
-
# tempo_scale_factor = rec.device_relative_speed
|
|
393
|
+
# instanciates a sox.Transformer() with tempo() effect
|
|
394
|
+
# add applies it via a call to _edit_audio_file(rec, sox_transform)
|
|
389
395
|
tempo_scale_factor = rec.device_relative_speed
|
|
390
396
|
audio_dev = rec.device.name
|
|
391
397
|
video_dev = self.videoclip.device.name
|
|
398
|
+
print('when merging with [gold1]%s[/gold1].'%self.videoclip)
|
|
392
399
|
if tempo_scale_factor > 1:
|
|
393
|
-
print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]
|
|
400
|
+
print('Because [gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]: file is too long by a %.12f factor;'%
|
|
394
401
|
(audio_dev, video_dev, tempo_scale_factor))
|
|
395
402
|
else:
|
|
396
|
-
print('
|
|
403
|
+
print('Because [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1]: file is short by a %.12f factor'%
|
|
397
404
|
(audio_dev, video_dev, tempo_scale_factor))
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
405
|
+
logger.debug('tempoed_recs dict:%s'%AudioStitcherVideoMerger.tempoed_recs)
|
|
406
|
+
if rec in AudioStitcherVideoMerger.tempoed_recs:
|
|
407
|
+
logger.debug('%s already tempoed'%rec)
|
|
408
|
+
cached_factor, cached_file = AudioStitcherVideoMerger.tempoed_recs[rec]
|
|
409
|
+
error_factor = tempo_scale_factor/cached_factor
|
|
410
|
+
logger.debug('tempo factors, needed: %f cached %f'%(tempo_scale_factor,cached_factor))
|
|
411
|
+
delta_cache = abs((1 - error_factor)*rec.get_original_duration())
|
|
412
|
+
logger.debug('error if cache is used: %f ms'%(delta_cache*1e3))
|
|
413
|
+
delta_cache_is_ok = delta_cache < yaltc.MAXDRIFT
|
|
414
|
+
else:
|
|
415
|
+
delta_cache_is_ok = False
|
|
416
|
+
if delta_cache_is_ok:
|
|
417
|
+
logger.debug('ok, will use %s'%cached_file)
|
|
418
|
+
self.soxed_audio[rec] = cached_file
|
|
419
|
+
else:
|
|
420
|
+
logger.debug('%s not tempoed yet'%rec)
|
|
421
|
+
sox_transform = sox.Transformer()
|
|
422
|
+
sox_transform.tempo(tempo_scale_factor)
|
|
423
|
+
# scaled_file = self._get_soxed_file(rec, sox_transform)
|
|
424
|
+
logger.debug('sox_transform %s'%sox_transform.effects)
|
|
425
|
+
soxed_fh = self._edit_audio_file(rec, sox_transform)
|
|
426
|
+
scaled_file_name = _pathname(soxed_fh)
|
|
427
|
+
AudioStitcherVideoMerger.tempoed_recs[rec] = (tempo_scale_factor, soxed_fh)
|
|
428
|
+
new_duration = sox.file_info.duration(scaled_file_name)
|
|
429
|
+
initial_duration = sox.file_info.duration(
|
|
430
|
+
_pathname(rec.AVpath))
|
|
431
|
+
logger.debug('Verif: initial_duration %.12f new_duration %.12f ratio:%.12f'%(
|
|
432
|
+
initial_duration, new_duration, initial_duration/new_duration))
|
|
433
|
+
logger.debug('delta duration %f ms'%((new_duration-initial_duration)*1e3))
|
|
407
434
|
|
|
408
435
|
def _get_concatenated_audiofile_for(self, device):
|
|
409
436
|
"""
|
|
410
437
|
return a handle for the final audio file formed by all detected
|
|
411
|
-
overlapping recordings, produced by the same
|
|
438
|
+
overlapping recordings, produced by the same audio recorder.
|
|
412
439
|
|
|
413
440
|
"""
|
|
414
441
|
logger.debug('concatenating device %s'%str(device))
|
|
415
|
-
|
|
442
|
+
audio_recs = self._get_all_recordings_for(device)
|
|
416
443
|
# [TODO here] Check if all unidentified device files are not
|
|
417
444
|
# overlapping because they are considered produced by the same
|
|
418
445
|
# device. If some overlap then necessarily they're from different
|
|
419
446
|
# ones. List the files and warn the user there is a risk of error if
|
|
420
447
|
# they're not from the same device.
|
|
421
448
|
|
|
422
|
-
logger.debug('%i audio files for videoclip %s:'%(len(
|
|
449
|
+
logger.debug('%i audio files for videoclip %s:'%(len(audio_recs),
|
|
423
450
|
self.videoclip))
|
|
424
|
-
for r in
|
|
451
|
+
for r in audio_recs:
|
|
425
452
|
logger.debug(' %s'%r)
|
|
453
|
+
# ratio between real samplerates of audio and videoclip
|
|
426
454
|
speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
|
|
427
|
-
for rec in
|
|
455
|
+
for rec in audio_recs])
|
|
428
456
|
mean_speed = numpy.mean(speeds)
|
|
429
|
-
for
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
logger.debug('
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
r.time_position, self.videoclip))
|
|
457
|
+
for audio in audio_recs:
|
|
458
|
+
audio.device_relative_speed = mean_speed
|
|
459
|
+
logger.debug('set device_relative_speed for %s'%audio)
|
|
460
|
+
logger.debug(' value: %f'%audio.device_relative_speed)
|
|
461
|
+
audio.set_time_position_to(self.videoclip)
|
|
462
|
+
logger.debug('time_position for %s: %fs relative to %s'%(audio,
|
|
463
|
+
audio.time_position, self.videoclip))
|
|
437
464
|
# st_dev_speeds just to check for anomalous situation
|
|
438
465
|
st_dev_speeds = numpy.std(speeds)
|
|
439
466
|
logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
|
|
@@ -458,7 +485,7 @@ class AudioStitcherVideoMerger:
|
|
|
458
485
|
|
|
459
486
|
# process first element 'by hand' outside the loop
|
|
460
487
|
# first_audio is a Recording, not a path nor filehandle
|
|
461
|
-
first_audio =
|
|
488
|
+
first_audio = audio_recs[0]
|
|
462
489
|
needs_dedrift, delta = first_audio.needs_dedrifting()
|
|
463
490
|
logger.debug('first audio is %s'%first_audio)
|
|
464
491
|
logger.debug('checking drift, first audio: delta of %0.2f ms'%(
|
|
@@ -470,8 +497,8 @@ class AudioStitcherVideoMerger:
|
|
|
470
497
|
self._pad_or_trim_first_audio(first_audio)
|
|
471
498
|
# loop for the other files
|
|
472
499
|
# growing_file = first_audio.edited_version
|
|
473
|
-
growing_file = self.
|
|
474
|
-
for i, rec in enumerate(
|
|
500
|
+
growing_file = self.soxed_audio[first_audio]
|
|
501
|
+
for i, rec in enumerate(audio_recs[1:]):
|
|
475
502
|
logger.debug('Padding and joining for %s'%rec)
|
|
476
503
|
needs_dedrift, delta = rec.needs_dedrifting()
|
|
477
504
|
logger.debug('next audio is %s'%rec)
|
|
@@ -494,7 +521,7 @@ class AudioStitcherVideoMerger:
|
|
|
494
521
|
(rec,rec.time_position,end_time))
|
|
495
522
|
self._pad_file(rec, pad_duration)
|
|
496
523
|
# new_file = rec.edited_version
|
|
497
|
-
new_file = self.
|
|
524
|
+
new_file = self.soxed_audio[rec]
|
|
498
525
|
growing_file = self._concatenate_audio_files(growing_file, new_file)
|
|
499
526
|
end_time = sox.file_info.duration(growing_file.name)
|
|
500
527
|
logger.debug('total edited audio duration %.2f s'%end_time)
|
|
@@ -507,7 +534,7 @@ class AudioStitcherVideoMerger:
|
|
|
507
534
|
TODO: check if first_rec is a Recording or tempfile (maybe a tempfile if dedrifted)
|
|
508
535
|
NO: will change tempo after trimming/padding
|
|
509
536
|
|
|
510
|
-
Store (into Recording.
|
|
537
|
+
Store (into Recording.soxed_audio dict) the handle of the sox processed
|
|
511
538
|
first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
|
|
512
539
|
starting time. Length of the written file can differ from length of the
|
|
513
540
|
submitted Recording object if drift is corrected with sox tempo
|
|
@@ -571,22 +598,25 @@ class AudioStitcherVideoMerger:
|
|
|
571
598
|
def _edit_audio_file(self, audio_rec, sox_transform):
|
|
572
599
|
"""
|
|
573
600
|
Apply the specified sox_transform onto the audio_rec and update
|
|
574
|
-
self.
|
|
601
|
+
self.soxed_audio dict with the result (with audio_rec as the key)
|
|
602
|
+
Returns the filehandle of the result.
|
|
575
603
|
"""
|
|
576
604
|
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
577
605
|
logger.debug('transform: %s'%sox_transform.effects)
|
|
578
|
-
recording_fh = self.
|
|
606
|
+
recording_fh = self.soxed_audio[audio_rec]
|
|
579
607
|
logger.debug('for recording %s, matching %s'%(audio_rec,
|
|
580
608
|
self.videoclip))
|
|
581
609
|
input_file = _pathname(recording_fh)
|
|
582
|
-
logger.debug('AudioStitcherVideoMerger.
|
|
610
|
+
logger.debug('AudioStitcherVideoMerger.soxed_audio[audio_rec]: %s'%
|
|
583
611
|
input_file)
|
|
584
612
|
out_file = _pathname(output_fh)
|
|
585
613
|
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
614
|
+
logger.debug('calling sox_transform.build()')
|
|
586
615
|
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
587
616
|
logger.debug('sox.build exit code %s'%str(status))
|
|
588
617
|
# audio_rec.edited_version = output_fh
|
|
589
|
-
self.
|
|
618
|
+
self.soxed_audio[audio_rec] = output_fh
|
|
619
|
+
return output_fh
|
|
590
620
|
|
|
591
621
|
def _write_ISOs(self, edited_audio_all_devices):
|
|
592
622
|
"""
|
|
@@ -797,10 +827,12 @@ class AudioStitcherVideoMerger:
|
|
|
797
827
|
logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
|
|
798
828
|
mono_tracks = [i for i in range(1, device.n_chan + 1)
|
|
799
829
|
if i not in stereo_mic_idx_flat]
|
|
800
|
-
logger.debug('mono_tracks: %s'%mono_tracks)
|
|
830
|
+
logger.debug('mono_tracks (with ttc+zeroed included): %s'%mono_tracks)
|
|
801
831
|
# remove TTC track number
|
|
802
|
-
|
|
803
|
-
|
|
832
|
+
to_remove = device.tracks.unused + [device.ttc+1]# unused is sox idx
|
|
833
|
+
[mono_tracks.remove(t) for t in to_remove]
|
|
834
|
+
# mono_tracks.remove(device.ttc + 1)
|
|
835
|
+
logger.debug('mono_tracks (ttc+zeroed removed)%s'%mono_tracks)
|
|
804
836
|
mono_files = [_sox_keep(multichan_tmpfl, [chan]) for chan
|
|
805
837
|
in mono_tracks]
|
|
806
838
|
new_stereo_files = [_sox_mono2stereo(f) for f in mono_files]
|
|
@@ -988,7 +1020,7 @@ class AudioStitcherVideoMerger:
|
|
|
988
1020
|
"""
|
|
989
1021
|
synced_clip_file = self.videoclip.final_synced_file
|
|
990
1022
|
video_path = self.videoclip.AVpath
|
|
991
|
-
timecode = self.videoclip.
|
|
1023
|
+
timecode = self.videoclip.get_start_timecode_string()
|
|
992
1024
|
# self.videoclip.synced_audio = audio_path
|
|
993
1025
|
audio_path = self.videoclip.synced_audio
|
|
994
1026
|
vid_only_handle = self._keep_VIDEO_only(video_path)
|
|
@@ -1136,20 +1168,28 @@ class Matcher:
|
|
|
1136
1168
|
case4 = R1 < A2 < R2
|
|
1137
1169
|
return case1 or case2 or case3 or case4
|
|
1138
1170
|
|
|
1139
|
-
def shrink_gaps_between_takes(self, with_gap=CLUSTER_GAP):
|
|
1171
|
+
def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
|
|
1140
1172
|
"""
|
|
1141
1173
|
for single cam shootings this simply sets the gap between takes,
|
|
1142
1174
|
tweaking each vid timecode metadata to distribute them next to each
|
|
1143
|
-
other along NLE timeline.
|
|
1175
|
+
other along NLE timeline.
|
|
1176
|
+
|
|
1177
|
+
Moves clusters at the timelineoffset
|
|
1178
|
+
|
|
1179
|
+
For multicam takes, shifts are computed so
|
|
1144
1180
|
video clusters are near but dont overlap, ex:
|
|
1145
1181
|
|
|
1146
|
-
Cluster 1
|
|
1147
|
-
1111111111111
|
|
1148
|
-
11111111111[
|
|
1182
|
+
Cluster 1 Cluster 2
|
|
1183
|
+
1111111111111 2222222222 (cam A)
|
|
1184
|
+
11111111111[inserted gap]222222222 (cam B)
|
|
1149
1185
|
|
|
1150
1186
|
or
|
|
1151
|
-
1111111111111
|
|
1152
|
-
1111111
|
|
1187
|
+
1111111111111[inserted 222222 (cam A)
|
|
1188
|
+
1111111 gap]222222222 (cam B)
|
|
1189
|
+
|
|
1190
|
+
argument:
|
|
1191
|
+
CLI_offset (str), option from command-line
|
|
1192
|
+
with_gap (float), the gap duration in seconds
|
|
1153
1193
|
|
|
1154
1194
|
Returns nothing, changes are done in the video files metadata
|
|
1155
1195
|
(each referenced by Recording.final_synced_file)
|
|
@@ -1210,17 +1250,25 @@ class Matcher:
|
|
|
1210
1250
|
cummulative_offsets = [td.total_seconds() for td in cummulative_offsets]
|
|
1211
1251
|
logger.debug('cummulative_offsets: %s'%cummulative_offsets)
|
|
1212
1252
|
time_of_first = clusters[0]['start']
|
|
1253
|
+
# compute CLI_offset_in_seconds from HH:MM:SS:FF in CLI_offset
|
|
1254
|
+
h, m, s, f = [float(s) for s in CLI_offset[0].split(':')]
|
|
1255
|
+
logger.debug('CLI_offset float values %s'%[h,m,s,f])
|
|
1256
|
+
CLI_offset_in_seconds = 3600*h + 60*m + s + f/vids[0].get_framerate()
|
|
1257
|
+
logger.debug('CLI_offset in seconds %f'%CLI_offset_in_seconds)
|
|
1213
1258
|
offset_for_all_clips = - from_midnight(time_of_first).total_seconds()
|
|
1259
|
+
offset_for_all_clips += CLI_offset_in_seconds
|
|
1214
1260
|
logger.debug('time_of_first: %s'%time_of_first)
|
|
1215
1261
|
logger.debug('offset_for_all_clips: %s'%offset_for_all_clips)
|
|
1216
1262
|
for cluster, offset in zip(clusters, cummulative_offsets):
|
|
1263
|
+
# first one starts at 00:00:00:00
|
|
1217
1264
|
total_offset = offset + offset_for_all_clips
|
|
1218
1265
|
logger.debug('for %s offset in sec: %f'%(cluster['vids'],
|
|
1219
1266
|
total_offset))
|
|
1220
1267
|
for vid in cluster['vids']:
|
|
1221
|
-
tc = vid.
|
|
1268
|
+
# tc = vid.get_start_timecode_string(CLI_offset, with_offset=total_offset)
|
|
1269
|
+
tc = vid.get_start_timecode_string(with_offset=total_offset)
|
|
1222
1270
|
logger.debug('for %s old tc: %s new tc %s'%(vid,
|
|
1223
|
-
vid.
|
|
1271
|
+
vid.get_start_timecode_string(), tc))
|
|
1224
1272
|
vid.write_file_timecode(tc)
|
|
1225
1273
|
return
|
|
1226
1274
|
|
tictacsync/yaltc.py
CHANGED
|
@@ -36,7 +36,7 @@ TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
|
|
|
36
36
|
CACHING = True
|
|
37
37
|
DEL_TEMP = False
|
|
38
38
|
DB_RMS_SILENCE_SOX = -58
|
|
39
|
-
MAXDRIFT =
|
|
39
|
+
MAXDRIFT = 15e-3 # in sec, for end of clip
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
################## pasted from FSKfreqCalculator.py output:
|
|
@@ -46,7 +46,7 @@ SYMBOL_LENGTH = 14.286 # ms, from FSKfreqCalculator.py
|
|
|
46
46
|
N_SYMBOLS = 35 # including sync pulse
|
|
47
47
|
##################
|
|
48
48
|
|
|
49
|
-
MINIMUM_LENGTH =
|
|
49
|
+
MINIMUM_LENGTH = 8 # sec
|
|
50
50
|
TRIAL_TIMES = [ # in seconds
|
|
51
51
|
(3.5, -2),
|
|
52
52
|
(3.5, -3.5),
|
|
@@ -233,7 +233,7 @@ class Decoder:
|
|
|
233
233
|
|
|
234
234
|
Uses the conditions below:
|
|
235
235
|
|
|
236
|
-
Extract duration is 1.143 s.
|
|
236
|
+
Extract duration is 1.143 s. (ie one sec + 1 symbol duration)
|
|
237
237
|
In self.word_props (list of morphology.regionprops):
|
|
238
238
|
if one region, duration should be in [0.499 0.512] sec
|
|
239
239
|
if two regions, total duration should be in [0.50 0.655]
|
|
@@ -244,13 +244,19 @@ class Decoder:
|
|
|
244
244
|
props = self.words_props
|
|
245
245
|
if len(props) not in [1,2]:
|
|
246
246
|
failing_comment = 'len(props) not in [1,2]: %i'%len(props)
|
|
247
|
+
else:
|
|
248
|
+
logger.debug('len(props), %i, is in [1,2]'%len(props))
|
|
247
249
|
if len(props) == 1:
|
|
250
|
+
logger.debug('one region')
|
|
248
251
|
w = _width(props[0])/self.samplerate
|
|
249
252
|
# self.effective_word_duration = w
|
|
250
253
|
# logger.debug('effective_word_duration %f (one region)'%w)
|
|
251
254
|
if not 0.499 < w < 0.512: # TODO: move as TOP OF FILE PARAMS
|
|
252
255
|
failing_comment = '_width %f not in [0.499 0.512]'%w
|
|
256
|
+
else:
|
|
257
|
+
logger.debug('0.499 < width < 0.512, %f'%w)
|
|
253
258
|
else: # 2 regions
|
|
259
|
+
logger.debug('two regions')
|
|
254
260
|
widths = [_width(p)/self.samplerate for p in props] # in sec
|
|
255
261
|
total_w = sum(widths)
|
|
256
262
|
# extra_window_duration = SOUND_EXTRACT_LENGTH - 1
|
|
@@ -260,6 +266,8 @@ class Decoder:
|
|
|
260
266
|
failing_comment = 'two regions duration %f not in [0.50 0.655]\n%s'%(total_w, widths)
|
|
261
267
|
# fig, ax = plt.subplots()
|
|
262
268
|
# p(ax, sound_extract_one_bit)
|
|
269
|
+
else:
|
|
270
|
+
logger.debug('0.5 < total_w < 0.656, %f'%total_w)
|
|
263
271
|
logger.debug('failing_comment: %s'%(
|
|
264
272
|
'none' if failing_comment=='' else failing_comment))
|
|
265
273
|
return failing_comment == '' # no comment = extract seems TicTacCode
|
|
@@ -558,7 +566,7 @@ class Recording:
|
|
|
558
566
|
implicitly True for each video recordings (but not set)
|
|
559
567
|
|
|
560
568
|
device_relative_speed : float
|
|
561
|
-
|
|
569
|
+
Set by
|
|
562
570
|
the ratio of the recording device clock speed relative to the
|
|
563
571
|
video recorder clock device, in order to correct clock drift with
|
|
564
572
|
pysox tempo transform. If value < 1.0 then the recording is
|
|
@@ -593,13 +601,15 @@ class Recording:
|
|
|
593
601
|
|
|
594
602
|
def __init__(self, media, do_plots=False):
|
|
595
603
|
"""
|
|
604
|
+
Set AVfilename string and check if file exists, does not read any
|
|
605
|
+
media data right away but uses ffprobe to parses the file and sets
|
|
606
|
+
probe attribute.
|
|
607
|
+
|
|
608
|
+
Logs a warning and sets Recording.decoder to None if ffprobe cant
|
|
609
|
+
interpret the file or if file has no audio. If file contains audio,
|
|
610
|
+
initialise Recording.decoder(but doesnt try to decode anything yet).
|
|
611
|
+
|
|
596
612
|
If multifile recording, AVfilename is sox merged audio file;
|
|
597
|
-
Set AVfilename string and check if file exists, does not read
|
|
598
|
-
any media data right away but uses ffprobe to parses the file and
|
|
599
|
-
sets probe attribute.
|
|
600
|
-
Logs a warning if ffprobe cant interpret the file or if file
|
|
601
|
-
has no audio; if file contains audio, instantiates a Decoder object
|
|
602
|
-
(but doesnt try to decode anything yet)
|
|
603
613
|
|
|
604
614
|
Parameters
|
|
605
615
|
----------
|
|
@@ -673,6 +683,7 @@ class Recording:
|
|
|
673
683
|
print('Recording init failed: %s'%recording_init_fail)
|
|
674
684
|
self.probe = None
|
|
675
685
|
self.decoder = None
|
|
686
|
+
return
|
|
676
687
|
logger.debug('ffprobe found: %s'%self.probe)
|
|
677
688
|
logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
|
|
678
689
|
self._read_audio_data()
|
|
@@ -708,7 +719,8 @@ class Recording:
|
|
|
708
719
|
self.audio_data.shape))
|
|
709
720
|
|
|
710
721
|
def __repr__(self):
|
|
711
|
-
return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
722
|
+
# return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
723
|
+
return _pathname(self.new_rec_name)
|
|
712
724
|
|
|
713
725
|
def _check_for_camera_error_correction(self):
|
|
714
726
|
# look for a file number
|
|
@@ -787,13 +799,14 @@ class Recording:
|
|
|
787
799
|
|
|
788
800
|
def needs_dedrifting(self):
|
|
789
801
|
rel_sp = self.device_relative_speed
|
|
790
|
-
if rel_sp > 1:
|
|
791
|
-
|
|
792
|
-
else:
|
|
793
|
-
|
|
802
|
+
# if rel_sp > 1:
|
|
803
|
+
# delta = (rel_sp - 1)*self.get_original_duration()
|
|
804
|
+
# else:
|
|
805
|
+
# delta = (1 - rel_sp)*self.get_original_duration()
|
|
806
|
+
delta = abs((1 - rel_sp)*self.get_original_duration())
|
|
794
807
|
logger.debug('%s delta drift %.2f ms'%(str(self), delta*1e3))
|
|
795
808
|
if delta > MAXDRIFT:
|
|
796
|
-
print('[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
|
|
809
|
+
print('\n[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
|
|
797
810
|
(self.AVpath, delta*1e3))
|
|
798
811
|
return delta > MAXDRIFT, delta
|
|
799
812
|
|
|
@@ -942,9 +955,12 @@ class Recording:
|
|
|
942
955
|
if successful, returns a datetime.datetime instance;
|
|
943
956
|
if not returns None.
|
|
944
957
|
"""
|
|
945
|
-
logger.debug('for
|
|
958
|
+
logger.debug('for %s, recording.start_time %s'%(self,
|
|
946
959
|
self.start_time))
|
|
960
|
+
if self.decoder is None:
|
|
961
|
+
return None # ffprobe failes or file too short, see __init__
|
|
947
962
|
if self.start_time is not None:
|
|
963
|
+
logger.debug('Recording.start_time already found %s'%self.start_time)
|
|
948
964
|
return self.start_time #############################################
|
|
949
965
|
cached_times = {}
|
|
950
966
|
def find_time(t_sec):
|
|
@@ -969,8 +985,8 @@ class Recording:
|
|
|
969
985
|
len(TRIAL_TIMES)))
|
|
970
986
|
# time_around_beginning = self._find_time_around(near_beg)
|
|
971
987
|
time_around_beginning = find_time(near_beg)
|
|
972
|
-
if self.TicTacCode_channel is None:
|
|
973
|
-
|
|
988
|
+
# if self.TicTacCode_channel is None:
|
|
989
|
+
# return None ####################################################
|
|
974
990
|
logger.debug('Trial #%i, end at %f'%(i+1, near_end))
|
|
975
991
|
# time_around_end = self._find_time_around(near_end)
|
|
976
992
|
time_around_end = find_time(near_end)
|
|
@@ -1048,6 +1064,7 @@ class Recording:
|
|
|
1048
1064
|
return int(ppm)
|
|
1049
1065
|
|
|
1050
1066
|
def get_speed_ratio(self, videoclip):
|
|
1067
|
+
# ratio between real samplerates of audio and videoclip
|
|
1051
1068
|
nominal = self.get_samplerate()
|
|
1052
1069
|
true = self.true_samplerate
|
|
1053
1070
|
ratio = true/nominal
|
|
@@ -1067,9 +1084,10 @@ class Recording:
|
|
|
1067
1084
|
string = self._ffprobe_video_stream()['avg_frame_rate']
|
|
1068
1085
|
return eval(string) # eg eval(24000/1001)
|
|
1069
1086
|
|
|
1070
|
-
def
|
|
1071
|
-
# returns a
|
|
1087
|
+
def get_start_timecode_string(self, with_offset=0):
|
|
1088
|
+
# returns a HH:MM:SS:FR string
|
|
1072
1089
|
start_datetime = self.get_start_time()
|
|
1090
|
+
# logger.debug('CLI_offset %s'%CLI_offset)
|
|
1073
1091
|
logger.debug('start_datetime %s'%start_datetime)
|
|
1074
1092
|
start_datetime += timedelta(seconds=with_offset)
|
|
1075
1093
|
logger.debug('shifted start_datetime %s (offset %f)'%(start_datetime,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
+
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tictacsync/device_scanner.py,sha256=kkwiO6qFNyBwqyZGqsf2q8WUqu4BBMFqFaUFFTeFfiY,29034
|
|
4
|
+
tictacsync/entry.py,sha256=fklCyTgqxJPWzKsn1ow3IxLPq8obv-N8Z72ieRzulCI,13352
|
|
5
|
+
tictacsync/multi2polywav.py,sha256=BsZxUjZo2Px6opKpFlgcvdZuUKDANEVTdapuWrX1jKw,7287
|
|
6
|
+
tictacsync/remergemix.py,sha256=FJTMipIS0O7mMl_tr8BhuYqWvanSydvjGkFCEd-jaDk,9829
|
|
7
|
+
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
8
|
+
tictacsync/timeline.py,sha256=YhnNqYnbTTf2YVN5nLlQY62UA4z9fTij6x18tR_u3Nc,60424
|
|
9
|
+
tictacsync/yaltc.py,sha256=EzAF5VnMMeBp_o2AOs7wj0p31mElcb6D57YJVKUbOxM,53400
|
|
10
|
+
tictacsync-0.95a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
11
|
+
tictacsync-0.95a0.dist-info/METADATA,sha256=oHUqw0Q9bboCClpSRB8wNs5rQv_Ex5RnYefGLV2bpik,5502
|
|
12
|
+
tictacsync-0.95a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
13
|
+
tictacsync-0.95a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
14
|
+
tictacsync-0.95a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
15
|
+
tictacsync-0.95a0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
-
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
tictacsync/device_scanner.py,sha256=lvWps5XPvzubnTqisFWjdRUpxsM9YMRYMp-xQu7H6Os,27515
|
|
4
|
-
tictacsync/entry.py,sha256=TgiFdwTKWvtj7xU-556nkwDo4OqAjJ55g97Jt-lSeBg,11377
|
|
5
|
-
tictacsync/multi2polywav.py,sha256=k7VU-yjO1_0DbygWNytYvaExbiAs3_0-n0UmgGTa8wM,7282
|
|
6
|
-
tictacsync/remergemix.py,sha256=FJTMipIS0O7mMl_tr8BhuYqWvanSydvjGkFCEd-jaDk,9829
|
|
7
|
-
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
8
|
-
tictacsync/timeline.py,sha256=0K1haVu7qUbyQmb0AsVoOB3CO8_OevHxpcJENx0kf48,57763
|
|
9
|
-
tictacsync/yaltc.py,sha256=2pMyDv69x56p3zq11L34PcqS_xC3-HwMPEDXGc7QhDg,52560
|
|
10
|
-
tictacsync-0.91a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
11
|
-
tictacsync-0.91a0.dist-info/METADATA,sha256=fnBgd-2zImEjHYIctys5-wF1TY4G-vxhCmdMz1wePk4,5502
|
|
12
|
-
tictacsync-0.91a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
13
|
-
tictacsync-0.91a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
14
|
-
tictacsync-0.91a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
15
|
-
tictacsync-0.91a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|