tictacsync 0.91a0__py3-none-any.whl → 0.96a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tictacsync might be problematic. Click here for more details.
- tictacsync/device_scanner.py +183 -133
- tictacsync/entry.py +152 -61
- tictacsync/multi2polywav.py +1 -1
- tictacsync/remergemix.py +4 -2
- tictacsync/remrgmx.py +28 -0
- tictacsync/timeline.py +285 -95
- tictacsync/yaltc.py +47 -32
- {tictacsync-0.91a0.dist-info → tictacsync-0.96a0.dist-info}/METADATA +1 -1
- tictacsync-0.96a0.dist-info/RECORD +16 -0
- tictacsync-0.91a0.dist-info/RECORD +0 -15
- {tictacsync-0.91a0.dist-info → tictacsync-0.96a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.96a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.96a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.91a0.dist-info → tictacsync-0.96a0.dist-info}/top_level.txt +0 -0
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,58 @@ 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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
|
|
339
|
+
soxed_audio : dict as {Recording : path}
|
|
340
|
+
keys are elements of matched_audio_recordings and the value are
|
|
341
|
+
the Pathlib path of the eventual edited audio(trimmed or padded).
|
|
331
342
|
|
|
332
343
|
synced_clip_dir : Path
|
|
333
344
|
where synced clips are written
|
|
334
345
|
|
|
335
346
|
"""
|
|
347
|
+
tempoed_recs = {}
|
|
336
348
|
|
|
337
349
|
def __init__(self, video_clip):
|
|
338
350
|
self.videoclip = video_clip
|
|
339
351
|
# self.matched_audio_recordings = []
|
|
340
|
-
self.
|
|
352
|
+
self.soxed_audio = {}
|
|
341
353
|
logger.debug('instantiating AudioStitcherVideoMerger for %s'%
|
|
342
354
|
video_clip)
|
|
343
355
|
|
|
344
356
|
def add_matched_audio(self, audio_rec):
|
|
345
357
|
"""
|
|
346
|
-
Populates AudioStitcherVideoMerger.
|
|
358
|
+
Populates AudioStitcherVideoMerger.soxed_audio,
|
|
347
359
|
a dict as {Recording : path}
|
|
348
360
|
|
|
349
361
|
This fct is called
|
|
350
362
|
within Matcher.scan_audio_for_each_videoclip()
|
|
351
363
|
|
|
352
|
-
Returns nothing, fills self.
|
|
364
|
+
Returns nothing, fills self.soxed_audio dict with
|
|
353
365
|
matched audio.
|
|
354
366
|
|
|
355
367
|
"""
|
|
356
|
-
self.
|
|
368
|
+
self.soxed_audio[audio_rec] = audio_rec.AVpath
|
|
357
369
|
"""
|
|
358
|
-
Here at this point, self.
|
|
359
|
-
after a call to _edit_audio_file(),
|
|
370
|
+
Here at this point, self.soxed_audio[audio_rec] is unedited but
|
|
371
|
+
after a call to _edit_audio_file(), soxed_audio[audio_rec] points to
|
|
360
372
|
a new file and audio_rec.AVpath is unchanged.
|
|
361
373
|
"""
|
|
362
374
|
return
|
|
@@ -364,9 +376,9 @@ class AudioStitcherVideoMerger:
|
|
|
364
376
|
def get_matched_audio_recs(self):
|
|
365
377
|
"""
|
|
366
378
|
Returns audio recordings that overlap self.videoclip.
|
|
367
|
-
Simply keys of self.
|
|
379
|
+
Simply keys of self.soxed_audio dict
|
|
368
380
|
"""
|
|
369
|
-
return list(self.
|
|
381
|
+
return list(self.soxed_audio.keys())
|
|
370
382
|
|
|
371
383
|
def _get_audio_devices(self):
|
|
372
384
|
devices = set([r.device for r in self.get_matched_audio_recs()])
|
|
@@ -375,6 +387,13 @@ class AudioStitcherVideoMerger:
|
|
|
375
387
|
logger.debug('devices %s'%devices)
|
|
376
388
|
return devices
|
|
377
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
|
+
|
|
378
397
|
def _get_all_recordings_for(self, device):
|
|
379
398
|
# return recordings for a particular device, sorted by time
|
|
380
399
|
recs = [a for a in self.get_matched_audio_recs() if a.device == device]
|
|
@@ -382,58 +401,77 @@ class AudioStitcherVideoMerger:
|
|
|
382
401
|
return recs
|
|
383
402
|
|
|
384
403
|
def _dedrift_rec(self, rec):
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
sox_transform = sox.Transformer()
|
|
388
|
-
# tempo_scale_factor = rec.device_relative_speed
|
|
404
|
+
# instanciates a sox.Transformer() with tempo() effect
|
|
405
|
+
# add applies it via a call to _edit_audio_file(rec, sox_transform)
|
|
389
406
|
tempo_scale_factor = rec.device_relative_speed
|
|
390
407
|
audio_dev = rec.device.name
|
|
391
408
|
video_dev = self.videoclip.device.name
|
|
409
|
+
print('when merging with [gold1]%s[/gold1].'%self.videoclip)
|
|
392
410
|
if tempo_scale_factor > 1:
|
|
393
|
-
print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]
|
|
411
|
+
print('Because [gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]: file is too long by a %.12f factor;'%
|
|
394
412
|
(audio_dev, video_dev, tempo_scale_factor))
|
|
395
413
|
else:
|
|
396
|
-
print('
|
|
414
|
+
print('Because [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1]: file is short by a %.12f factor'%
|
|
397
415
|
(audio_dev, video_dev, tempo_scale_factor))
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
416
|
+
logger.debug('tempoed_recs dict:%s'%AudioStitcherVideoMerger.tempoed_recs)
|
|
417
|
+
if rec in AudioStitcherVideoMerger.tempoed_recs:
|
|
418
|
+
logger.debug('%s already tempoed'%rec)
|
|
419
|
+
cached_factor, cached_file = AudioStitcherVideoMerger.tempoed_recs[rec]
|
|
420
|
+
error_factor = tempo_scale_factor/cached_factor
|
|
421
|
+
logger.debug('tempo factors, needed: %f cached %f'%(tempo_scale_factor,cached_factor))
|
|
422
|
+
delta_cache = abs((1 - error_factor)*rec.get_original_duration())
|
|
423
|
+
logger.debug('error if cache is used: %f ms'%(delta_cache*1e3))
|
|
424
|
+
delta_cache_is_ok = delta_cache < yaltc.MAXDRIFT
|
|
425
|
+
else:
|
|
426
|
+
delta_cache_is_ok = False
|
|
427
|
+
if delta_cache_is_ok:
|
|
428
|
+
logger.debug('ok, will use %s'%cached_file)
|
|
429
|
+
self.soxed_audio[rec] = cached_file
|
|
430
|
+
else:
|
|
431
|
+
logger.debug('%s not tempoed yet'%rec)
|
|
432
|
+
sox_transform = sox.Transformer()
|
|
433
|
+
sox_transform.tempo(tempo_scale_factor)
|
|
434
|
+
# scaled_file = self._get_soxed_file(rec, sox_transform)
|
|
435
|
+
logger.debug('sox_transform %s'%sox_transform.effects)
|
|
436
|
+
soxed_fh = self._edit_audio_file(rec, sox_transform)
|
|
437
|
+
scaled_file_name = _pathname(soxed_fh)
|
|
438
|
+
AudioStitcherVideoMerger.tempoed_recs[rec] = (tempo_scale_factor, soxed_fh)
|
|
439
|
+
new_duration = sox.file_info.duration(scaled_file_name)
|
|
440
|
+
initial_duration = sox.file_info.duration(
|
|
441
|
+
_pathname(rec.AVpath))
|
|
442
|
+
logger.debug('Verif: initial_duration %.12f new_duration %.12f ratio:%.12f'%(
|
|
443
|
+
initial_duration, new_duration, initial_duration/new_duration))
|
|
444
|
+
logger.debug('delta duration %f ms'%((new_duration-initial_duration)*1e3))
|
|
407
445
|
|
|
408
446
|
def _get_concatenated_audiofile_for(self, device):
|
|
409
447
|
"""
|
|
410
448
|
return a handle for the final audio file formed by all detected
|
|
411
|
-
overlapping recordings, produced by the same
|
|
449
|
+
overlapping recordings, produced by the same audio recorder.
|
|
412
450
|
|
|
413
451
|
"""
|
|
414
452
|
logger.debug('concatenating device %s'%str(device))
|
|
415
|
-
|
|
453
|
+
audio_recs = self._get_all_recordings_for(device)
|
|
416
454
|
# [TODO here] Check if all unidentified device files are not
|
|
417
455
|
# overlapping because they are considered produced by the same
|
|
418
456
|
# device. If some overlap then necessarily they're from different
|
|
419
457
|
# ones. List the files and warn the user there is a risk of error if
|
|
420
458
|
# they're not from the same device.
|
|
421
459
|
|
|
422
|
-
logger.debug('%i audio files for videoclip %s:'%(len(
|
|
460
|
+
logger.debug('%i audio files for videoclip %s:'%(len(audio_recs),
|
|
423
461
|
self.videoclip))
|
|
424
|
-
for r in
|
|
462
|
+
for r in audio_recs:
|
|
425
463
|
logger.debug(' %s'%r)
|
|
464
|
+
# ratio between real samplerates of audio and videoclip
|
|
426
465
|
speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
|
|
427
|
-
for rec in
|
|
466
|
+
for rec in audio_recs])
|
|
428
467
|
mean_speed = numpy.mean(speeds)
|
|
429
|
-
for
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
logger.debug('
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
r.time_position, self.videoclip))
|
|
468
|
+
for audio in audio_recs:
|
|
469
|
+
audio.device_relative_speed = mean_speed
|
|
470
|
+
logger.debug('set device_relative_speed for %s'%audio)
|
|
471
|
+
logger.debug(' value: %f'%audio.device_relative_speed)
|
|
472
|
+
audio.set_time_position_to(self.videoclip)
|
|
473
|
+
logger.debug('time_position for %s: %fs relative to %s'%(audio,
|
|
474
|
+
audio.time_position, self.videoclip))
|
|
437
475
|
# st_dev_speeds just to check for anomalous situation
|
|
438
476
|
st_dev_speeds = numpy.std(speeds)
|
|
439
477
|
logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
|
|
@@ -458,7 +496,7 @@ class AudioStitcherVideoMerger:
|
|
|
458
496
|
|
|
459
497
|
# process first element 'by hand' outside the loop
|
|
460
498
|
# first_audio is a Recording, not a path nor filehandle
|
|
461
|
-
first_audio =
|
|
499
|
+
first_audio = audio_recs[0]
|
|
462
500
|
needs_dedrift, delta = first_audio.needs_dedrifting()
|
|
463
501
|
logger.debug('first audio is %s'%first_audio)
|
|
464
502
|
logger.debug('checking drift, first audio: delta of %0.2f ms'%(
|
|
@@ -470,8 +508,8 @@ class AudioStitcherVideoMerger:
|
|
|
470
508
|
self._pad_or_trim_first_audio(first_audio)
|
|
471
509
|
# loop for the other files
|
|
472
510
|
# growing_file = first_audio.edited_version
|
|
473
|
-
growing_file = self.
|
|
474
|
-
for i, rec in enumerate(
|
|
511
|
+
growing_file = self.soxed_audio[first_audio]
|
|
512
|
+
for i, rec in enumerate(audio_recs[1:]):
|
|
475
513
|
logger.debug('Padding and joining for %s'%rec)
|
|
476
514
|
needs_dedrift, delta = rec.needs_dedrifting()
|
|
477
515
|
logger.debug('next audio is %s'%rec)
|
|
@@ -494,7 +532,7 @@ class AudioStitcherVideoMerger:
|
|
|
494
532
|
(rec,rec.time_position,end_time))
|
|
495
533
|
self._pad_file(rec, pad_duration)
|
|
496
534
|
# new_file = rec.edited_version
|
|
497
|
-
new_file = self.
|
|
535
|
+
new_file = self.soxed_audio[rec]
|
|
498
536
|
growing_file = self._concatenate_audio_files(growing_file, new_file)
|
|
499
537
|
end_time = sox.file_info.duration(growing_file.name)
|
|
500
538
|
logger.debug('total edited audio duration %.2f s'%end_time)
|
|
@@ -507,7 +545,7 @@ class AudioStitcherVideoMerger:
|
|
|
507
545
|
TODO: check if first_rec is a Recording or tempfile (maybe a tempfile if dedrifted)
|
|
508
546
|
NO: will change tempo after trimming/padding
|
|
509
547
|
|
|
510
|
-
Store (into Recording.
|
|
548
|
+
Store (into Recording.soxed_audio dict) the handle of the sox processed
|
|
511
549
|
first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
|
|
512
550
|
starting time. Length of the written file can differ from length of the
|
|
513
551
|
submitted Recording object if drift is corrected with sox tempo
|
|
@@ -571,22 +609,25 @@ class AudioStitcherVideoMerger:
|
|
|
571
609
|
def _edit_audio_file(self, audio_rec, sox_transform):
|
|
572
610
|
"""
|
|
573
611
|
Apply the specified sox_transform onto the audio_rec and update
|
|
574
|
-
self.
|
|
612
|
+
self.soxed_audio dict with the result (with audio_rec as the key)
|
|
613
|
+
Returns the filehandle of the result.
|
|
575
614
|
"""
|
|
576
615
|
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
577
616
|
logger.debug('transform: %s'%sox_transform.effects)
|
|
578
|
-
recording_fh = self.
|
|
617
|
+
recording_fh = self.soxed_audio[audio_rec]
|
|
579
618
|
logger.debug('for recording %s, matching %s'%(audio_rec,
|
|
580
619
|
self.videoclip))
|
|
581
620
|
input_file = _pathname(recording_fh)
|
|
582
|
-
logger.debug('AudioStitcherVideoMerger.
|
|
621
|
+
logger.debug('AudioStitcherVideoMerger.soxed_audio[audio_rec]: %s'%
|
|
583
622
|
input_file)
|
|
584
623
|
out_file = _pathname(output_fh)
|
|
585
624
|
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
625
|
+
logger.debug('calling sox_transform.build()')
|
|
586
626
|
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
587
627
|
logger.debug('sox.build exit code %s'%str(status))
|
|
588
628
|
# audio_rec.edited_version = output_fh
|
|
589
|
-
self.
|
|
629
|
+
self.soxed_audio[audio_rec] = output_fh
|
|
630
|
+
return output_fh
|
|
590
631
|
|
|
591
632
|
def _write_ISOs(self, edited_audio_all_devices):
|
|
592
633
|
"""
|
|
@@ -605,11 +646,11 @@ class AudioStitcherVideoMerger:
|
|
|
605
646
|
leftCAM/
|
|
606
647
|
|
|
607
648
|
canon24fps01.MOV ━━━━┓ name of clip is name of folder
|
|
608
|
-
|
|
649
|
+
canon24fps01_ISO/ <━━┛
|
|
609
650
|
chan_1.wav
|
|
610
651
|
chan_2.wav
|
|
611
652
|
canon24fps02.MOV
|
|
612
|
-
|
|
653
|
+
canon24fps01_ISO/
|
|
613
654
|
chan_1.wav
|
|
614
655
|
chan_2.wav
|
|
615
656
|
|
|
@@ -650,7 +691,7 @@ class AudioStitcherVideoMerger:
|
|
|
650
691
|
video_stem_WO_suffix = synced_clip_file.stem
|
|
651
692
|
# video_stem_WO_suffix = synced_clip_file.stem.split('.')[0]
|
|
652
693
|
# OUT_DIR_DEFAULT, D2 = ISOsDIR.split('/')
|
|
653
|
-
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '
|
|
694
|
+
ISOdir = synced_clip_dir/(video_stem_WO_suffix + '_ISO')
|
|
654
695
|
os.makedirs(ISOdir, exist_ok=True)
|
|
655
696
|
logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
|
|
656
697
|
logger.debug('ISOdir %s'%ISOdir)
|
|
@@ -683,16 +724,16 @@ class AudioStitcherVideoMerger:
|
|
|
683
724
|
In details:
|
|
684
725
|
|
|
685
726
|
If no device tracks.txt file declared a mix track (or if tracks.txt is
|
|
686
|
-
absent)
|
|
727
|
+
absent) a mix is done programmatically. Two possibilities:
|
|
687
728
|
|
|
688
729
|
#1- no stereo pairs were declared: a global mono mix is returned.
|
|
689
730
|
#2- one or more stereo pair mics were used and declared (micL, micR):
|
|
690
731
|
a global stereo mix is returned with mono tracks panned 50-50
|
|
691
732
|
|
|
692
|
-
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
|
|
693
734
|
stereo) mix track, this fct returns a tempfile containing the
|
|
694
|
-
corresponding tracks,
|
|
695
|
-
(
|
|
735
|
+
corresponding tracks, simply extracting them from multichan_tmpfl
|
|
736
|
+
(those covers cases #3 and #4)
|
|
696
737
|
|
|
697
738
|
Args:
|
|
698
739
|
device : device_scanner.Device dataclass
|
|
@@ -763,12 +804,13 @@ class AudioStitcherVideoMerger:
|
|
|
763
804
|
logger.debug('%s has mix %s'%(device.name, device.tracks.mix))
|
|
764
805
|
logger.debug('device %s'%device)
|
|
765
806
|
# just checking coherency
|
|
766
|
-
if '
|
|
767
|
-
trx_TTC_chan = device.tracks.rawtrx.index('ttc')
|
|
768
|
-
elif 'tc' in device.tracks.rawtrx:
|
|
807
|
+
if 'tc' in device.tracks.rawtrx:
|
|
769
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')
|
|
770
811
|
else:
|
|
771
812
|
print('Error: no tc or ttc tag in track.txt')
|
|
813
|
+
print(device.tracks.rawtrx)
|
|
772
814
|
sys.exit(1)
|
|
773
815
|
logger.debug('TTC chan %i, dev ttc %i'%(trx_TTC_chan, device.ttc))
|
|
774
816
|
if trx_TTC_chan != device.ttc:
|
|
@@ -797,17 +839,132 @@ class AudioStitcherVideoMerger:
|
|
|
797
839
|
logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
|
|
798
840
|
mono_tracks = [i for i in range(1, device.n_chan + 1)
|
|
799
841
|
if i not in stereo_mic_idx_flat]
|
|
800
|
-
logger.debug('mono_tracks: %s'%mono_tracks)
|
|
842
|
+
logger.debug('mono_tracks (with ttc+zeroed included): %s'%mono_tracks)
|
|
801
843
|
# remove TTC track number
|
|
802
|
-
|
|
803
|
-
|
|
844
|
+
to_remove = device.tracks.unused + [device.ttc+1]# unused is sox idx
|
|
845
|
+
[mono_tracks.remove(t) for t in to_remove]
|
|
846
|
+
# mono_tracks.remove(device.ttc + 1)
|
|
847
|
+
logger.debug('mono_tracks (ttc+zeroed removed)%s'%mono_tracks)
|
|
804
848
|
mono_files = [_sox_keep(multichan_tmpfl, [chan]) for chan
|
|
805
849
|
in mono_tracks]
|
|
806
850
|
new_stereo_files = [_sox_mono2stereo(f) for f in mono_files]
|
|
807
851
|
stereo_files = mic_stereo_files + new_stereo_files
|
|
808
852
|
return _sox_mix_files(stereo_files)
|
|
809
853
|
|
|
810
|
-
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,
|
|
811
968
|
write_multicam_structure,
|
|
812
969
|
asked_ISOs):
|
|
813
970
|
"""
|
|
@@ -830,12 +987,12 @@ class AudioStitcherVideoMerger:
|
|
|
830
987
|
|
|
831
988
|
Sets AudioStitcherVideoMerger.final_synced_file on completion
|
|
832
989
|
"""
|
|
833
|
-
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'%
|
|
834
991
|
(top_dir, output_dir, write_multicam_structure, asked_ISOs))
|
|
835
992
|
logger.debug('device for rec %s: %s'%(self.videoclip,
|
|
836
993
|
self.videoclip.device))
|
|
837
|
-
# suppose the user called tictacsync with 'mondayPM' as top folder
|
|
838
|
-
# 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):
|
|
839
996
|
if output_dir == None:
|
|
840
997
|
synced_clip_dir = Path(top_dir)/OUT_DIR_DEFAULT # = mondayPM/SyncedMedia
|
|
841
998
|
else:
|
|
@@ -850,13 +1007,18 @@ class AudioStitcherVideoMerger:
|
|
|
850
1007
|
Path(self.videoclip.new_rec_name).name
|
|
851
1008
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
852
1009
|
self.videoclip.final_synced_file = synced_clip_file # relative path
|
|
853
|
-
# Collecting edited audio by device, in (Device,
|
|
1010
|
+
# Collecting edited audio by device, in (Device, tempfiles) pairs:
|
|
854
1011
|
# for a given self.videoclip, each audio device will have a sequence
|
|
855
1012
|
# of matched, synced and joined audio files present in a single
|
|
856
1013
|
# edited audio file, returned by _get_concatenated_audiofile_for
|
|
857
1014
|
merged_audio_files_by_device = [
|
|
858
1015
|
(d, self._get_concatenated_audiofile_for(d))
|
|
859
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)
|
|
860
1022
|
if len(merged_audio_files_by_device) == 0:
|
|
861
1023
|
# no audio file overlaps for this clip
|
|
862
1024
|
return #############################################################
|
|
@@ -926,21 +1088,30 @@ class AudioStitcherVideoMerger:
|
|
|
926
1088
|
# generates track name for later if asked_ISOs
|
|
927
1089
|
# idx is from 0 to nchan-1 for this device
|
|
928
1090
|
if dev.tracks == None:
|
|
929
|
-
|
|
1091
|
+
chan_name = 'chan%s'%str(idx+1).zfill(2)
|
|
930
1092
|
else:
|
|
931
|
-
#
|
|
932
|
-
|
|
933
|
-
|
|
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)
|
|
934
1104
|
if multiple_recorders:
|
|
935
|
-
|
|
936
|
-
logger.debug('
|
|
937
|
-
return
|
|
1105
|
+
chan_name += '_' + dev.name # TODO: make this an option?
|
|
1106
|
+
logger.debug('track_name %s'%chan_name)
|
|
1107
|
+
return chan_name #####################################################
|
|
938
1108
|
# replace device, idx pair with track name (+ device name if many)
|
|
939
1109
|
# loop over devices than loop over tracks
|
|
940
1110
|
names_audio_tempfiles = []
|
|
941
1111
|
for dev, mono_tmpfiles_list in devices_and_monofiles:
|
|
942
1112
|
for idx, monotf in enumerate(mono_tmpfiles_list):
|
|
943
1113
|
track_name = _trnm(dev, idx)
|
|
1114
|
+
logger.debug('track_name %s'%track_name)
|
|
944
1115
|
if track_name[0] == '0': # muted, skip
|
|
945
1116
|
continue
|
|
946
1117
|
names_audio_tempfiles.append((track_name, monotf))
|
|
@@ -977,7 +1148,8 @@ class AudioStitcherVideoMerger:
|
|
|
977
1148
|
def _merge_audio_and_video(self):
|
|
978
1149
|
"""
|
|
979
1150
|
Calls ffmpeg to join video in self.videoclip.AVpath to
|
|
980
|
-
audio in self.videoclip.synced_audio
|
|
1151
|
+
audio in self.videoclip.synced_audio. Audio in original video
|
|
1152
|
+
is dropped.
|
|
981
1153
|
|
|
982
1154
|
On entry, videoclip.final_synced_file is a Path to an non existing
|
|
983
1155
|
file (contrarily to videoclip.synced_audio).
|
|
@@ -988,7 +1160,7 @@ class AudioStitcherVideoMerger:
|
|
|
988
1160
|
"""
|
|
989
1161
|
synced_clip_file = self.videoclip.final_synced_file
|
|
990
1162
|
video_path = self.videoclip.AVpath
|
|
991
|
-
timecode = self.videoclip.
|
|
1163
|
+
timecode = self.videoclip.get_start_timecode_string()
|
|
992
1164
|
# self.videoclip.synced_audio = audio_path
|
|
993
1165
|
audio_path = self.videoclip.synced_audio
|
|
994
1166
|
vid_only_handle = self._keep_VIDEO_only(video_path)
|
|
@@ -1004,7 +1176,8 @@ class AudioStitcherVideoMerger:
|
|
|
1004
1176
|
ffmpeg_args = (
|
|
1005
1177
|
ffmpeg
|
|
1006
1178
|
.input(v_n)
|
|
1007
|
-
.output(out_n, shortest=None, vcodec='copy',
|
|
1179
|
+
# .output(out_n, shortest=None, vcodec='copy',
|
|
1180
|
+
.output(out_n, vcodec='copy',
|
|
1008
1181
|
timecode=timecode)
|
|
1009
1182
|
.global_args('-i', a_n, "-hide_banner")
|
|
1010
1183
|
.overwrite_output()
|
|
@@ -1015,7 +1188,8 @@ class AudioStitcherVideoMerger:
|
|
|
1015
1188
|
_, out = (
|
|
1016
1189
|
ffmpeg
|
|
1017
1190
|
.input(v_n)
|
|
1018
|
-
.output(out_n,
|
|
1191
|
+
.output(out_n, vcodec='copy',
|
|
1192
|
+
# .output(out_n, shortest=None, vcodec='copy',
|
|
1019
1193
|
# metadata='reel_name=foo', not all container support gen MD
|
|
1020
1194
|
timecode=timecode,
|
|
1021
1195
|
)
|
|
@@ -1053,7 +1227,7 @@ class Matcher:
|
|
|
1053
1227
|
recordings : list of Recording instances
|
|
1054
1228
|
all the scanned recordings with valid TicTacCode, set in __init__()
|
|
1055
1229
|
|
|
1056
|
-
|
|
1230
|
+
mergers : list
|
|
1057
1231
|
of AudioStitcherVideoMerger Class instances, built by
|
|
1058
1232
|
scan_audio_for_each_videoclip(); each video has a corresponding
|
|
1059
1233
|
AudioStitcherVideoMerger object. An audio_stitch doesn't extend
|
|
@@ -1070,7 +1244,7 @@ class Matcher:
|
|
|
1070
1244
|
|
|
1071
1245
|
"""
|
|
1072
1246
|
self.recordings = recordings_list
|
|
1073
|
-
self.
|
|
1247
|
+
self.mergers = []
|
|
1074
1248
|
|
|
1075
1249
|
def _rename_all_recs(self):
|
|
1076
1250
|
"""
|
|
@@ -1118,12 +1292,12 @@ class Matcher:
|
|
|
1118
1292
|
logger.debug('recording %s overlaps,'%(audio))
|
|
1119
1293
|
# print(' recording [gold1]%s[/gold1] overlaps,'%(audio))
|
|
1120
1294
|
if len(audio_stitch.get_matched_audio_recs()) > 0:
|
|
1121
|
-
self.
|
|
1295
|
+
self.mergers.append(audio_stitch)
|
|
1122
1296
|
else:
|
|
1123
1297
|
logger.debug('\n nothing\n')
|
|
1124
1298
|
print('No overlap found for %s'%videoclip.AVpath.name)
|
|
1125
1299
|
del audio_stitch
|
|
1126
|
-
logger.debug('%i
|
|
1300
|
+
logger.debug('%i mergers created'%len(self.mergers))
|
|
1127
1301
|
|
|
1128
1302
|
def _does_overlap(self, videoclip, audio_rec):
|
|
1129
1303
|
A1, A2 = audio_rec.get_start_time(), audio_rec.get_end_time()
|
|
@@ -1136,25 +1310,33 @@ class Matcher:
|
|
|
1136
1310
|
case4 = R1 < A2 < R2
|
|
1137
1311
|
return case1 or case2 or case3 or case4
|
|
1138
1312
|
|
|
1139
|
-
def shrink_gaps_between_takes(self, with_gap=CLUSTER_GAP):
|
|
1313
|
+
def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
|
|
1140
1314
|
"""
|
|
1141
1315
|
for single cam shootings this simply sets the gap between takes,
|
|
1142
1316
|
tweaking each vid timecode metadata to distribute them next to each
|
|
1143
|
-
other along NLE timeline.
|
|
1317
|
+
other along NLE timeline.
|
|
1318
|
+
|
|
1319
|
+
Moves clusters at the timelineoffset
|
|
1320
|
+
|
|
1321
|
+
For multicam takes, shifts are computed so
|
|
1144
1322
|
video clusters are near but dont overlap, ex:
|
|
1145
1323
|
|
|
1146
|
-
Cluster 1
|
|
1147
|
-
1111111111111
|
|
1148
|
-
11111111111[
|
|
1324
|
+
Cluster 1 Cluster 2
|
|
1325
|
+
1111111111111 2222222222 (cam A)
|
|
1326
|
+
11111111111[inserted gap]222222222 (cam B)
|
|
1149
1327
|
|
|
1150
1328
|
or
|
|
1151
|
-
1111111111111
|
|
1152
|
-
1111111
|
|
1329
|
+
1111111111111[inserted 222222 (cam A)
|
|
1330
|
+
1111111 gap]222222222 (cam B)
|
|
1331
|
+
|
|
1332
|
+
argument:
|
|
1333
|
+
CLI_offset (str), option from command-line
|
|
1334
|
+
with_gap (float), the gap duration in seconds
|
|
1153
1335
|
|
|
1154
1336
|
Returns nothing, changes are done in the video files metadata
|
|
1155
1337
|
(each referenced by Recording.final_synced_file)
|
|
1156
1338
|
"""
|
|
1157
|
-
vids = [m.videoclip for m in self.
|
|
1339
|
+
vids = [m.videoclip for m in self.mergers]
|
|
1158
1340
|
logger.debug('vids %s'%vids)
|
|
1159
1341
|
if len(vids) == 1:
|
|
1160
1342
|
logger.debug('just one take, no gap to shrink')
|
|
@@ -1210,17 +1392,25 @@ class Matcher:
|
|
|
1210
1392
|
cummulative_offsets = [td.total_seconds() for td in cummulative_offsets]
|
|
1211
1393
|
logger.debug('cummulative_offsets: %s'%cummulative_offsets)
|
|
1212
1394
|
time_of_first = clusters[0]['start']
|
|
1395
|
+
# compute CLI_offset_in_seconds from HH:MM:SS:FF in CLI_offset
|
|
1396
|
+
h, m, s, f = [float(s) for s in CLI_offset[0].split(':')]
|
|
1397
|
+
logger.debug('CLI_offset float values %s'%[h,m,s,f])
|
|
1398
|
+
CLI_offset_in_seconds = 3600*h + 60*m + s + f/vids[0].get_framerate()
|
|
1399
|
+
logger.debug('CLI_offset in seconds %f'%CLI_offset_in_seconds)
|
|
1213
1400
|
offset_for_all_clips = - from_midnight(time_of_first).total_seconds()
|
|
1401
|
+
offset_for_all_clips += CLI_offset_in_seconds
|
|
1214
1402
|
logger.debug('time_of_first: %s'%time_of_first)
|
|
1215
1403
|
logger.debug('offset_for_all_clips: %s'%offset_for_all_clips)
|
|
1216
1404
|
for cluster, offset in zip(clusters, cummulative_offsets):
|
|
1405
|
+
# first one starts at 00:00:00:00
|
|
1217
1406
|
total_offset = offset + offset_for_all_clips
|
|
1218
1407
|
logger.debug('for %s offset in sec: %f'%(cluster['vids'],
|
|
1219
1408
|
total_offset))
|
|
1220
1409
|
for vid in cluster['vids']:
|
|
1221
|
-
tc = vid.
|
|
1410
|
+
# tc = vid.get_start_timecode_string(CLI_offset, with_offset=total_offset)
|
|
1411
|
+
tc = vid.get_start_timecode_string(with_offset=total_offset)
|
|
1222
1412
|
logger.debug('for %s old tc: %s new tc %s'%(vid,
|
|
1223
|
-
vid.
|
|
1413
|
+
vid.get_start_timecode_string(), tc))
|
|
1224
1414
|
vid.write_file_timecode(tc)
|
|
1225
1415
|
return
|
|
1226
1416
|
|