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/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.edited_audio as
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
- edited_audio : dict as {Recording : path}
326
- keys are elements of matched_audio_recordings of class Recording
327
- and the value stores the Pathlib path of the eventual edited
328
- audio (trimmed , padded or time stretched). Before building the
329
- audio_montage, path points to the initial
330
- Recording.valid_sound
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.edited_audio = {}
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.edited_audio,
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.edited_audio dict with
364
+ Returns nothing, fills self.soxed_audio dict with
353
365
  matched audio.
354
366
 
355
367
  """
356
- self.edited_audio[audio_rec] = audio_rec.AVpath
368
+ self.soxed_audio[audio_rec] = audio_rec.AVpath
357
369
  """
358
- Here at this point, self.edited_audio[audio_rec] is unedited but
359
- after a call to _edit_audio_file(), edited_audio[audio_rec] points to
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.edited_audio dict
379
+ Simply keys of self.soxed_audio dict
368
380
  """
369
- return list(self.edited_audio.keys())
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
- initial_duration = sox.file_info.duration(
386
- _pathname(rec.AVpath))
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] so file is too long by a %f factor;\n'%
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('hence [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is short by a %f factor\n'%
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
- sox_transform.tempo(tempo_scale_factor)
399
- # scaled_file = self._get_soxed_file(rec, sox_transform)
400
- logger.debug('sox_transform %s'%sox_transform.effects)
401
- self._edit_audio_file(rec, sox_transform)
402
- scaled_file_name = _pathname(self.edited_audio[rec])
403
- new_duration = sox.file_info.duration(scaled_file_name)
404
- # goal_duration = rec.get_corrected_duration()
405
- logger.debug('initial_duration %f new_duration %f ratio:%f'%(
406
- initial_duration, new_duration, initial_duration/new_duration))
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 specified device.
449
+ overlapping recordings, produced by the same audio recorder.
412
450
 
413
451
  """
414
452
  logger.debug('concatenating device %s'%str(device))
415
- recordings = self._get_all_recordings_for(device)
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(recordings),
460
+ logger.debug('%i audio files for videoclip %s:'%(len(audio_recs),
423
461
  self.videoclip))
424
- for r in recordings:
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 recordings])
466
+ for rec in audio_recs])
428
467
  mean_speed = numpy.mean(speeds)
429
- for r in recordings:
430
- r.device_relative_speed = mean_speed
431
- # r.device_relative_speed = 0.9
432
- logger.debug('set device_relative_speed for %s'%r)
433
- logger.debug(' value: %f'%r.device_relative_speed)
434
- r.set_time_position_to(self.videoclip)
435
- logger.debug('time_position for %s: %fs relative to %s'%(r,
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 = recordings[0]
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.edited_audio[first_audio]
474
- for i, rec in enumerate(recordings[1:]):
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.edited_audio[rec]
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.edited_audio dict) the handle of the sox processed
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.edited_audio dict with the result (with audio_rec as the key)
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.edited_audio[audio_rec]
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.edited_audio[audio_rec]: %s'%
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.edited_audio[audio_rec] = output_fh
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
- canon24fps01.ISO/ <━━┛
649
+ canon24fps01_ISO/ <━━┛
609
650
  chan_1.wav
610
651
  chan_2.wav
611
652
  canon24fps02.MOV
612
- canon24fps01.ISO/
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 + '.ISO')
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), a mix is done programmatically. Two possibilities:
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, simpley extarcting them from multichan_tmpfl
695
- (thos covers cases #3 and #4)
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 'ttc' in device.tracks.rawtrx:
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
- mono_tracks.remove(device.ttc + 1)
803
- logger.debug('mono_tracks %s'%mono_tracks)
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 build_audio_and_write_video(self, top_dir, output_dir,
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 to
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, tempfile) pairs:
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
- tag = 'chan%s'%str(idx+1).zfill(2)
1091
+ chan_name = 'chan%s'%str(idx+1).zfill(2)
930
1092
  else:
931
- # audio_tags = [tag for tag in dev.tracks.rawtrx
932
- # if tag not in ['ttc','0','tc']]
933
- tag = dev.tracks.rawtrx[idx]
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
- tag += '_' + dev.name
936
- logger.debug('tag %s'%tag)
937
- return tag #####################################################
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.get_timecode()
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, shortest=None, vcodec='copy',
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
- video_mergers : list
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.video_mergers = []
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.video_mergers.append(audio_stitch)
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 video_mergers created'%len(self.video_mergers))
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. For multicam takes, shifts are computed so
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 Cluster 2
1147
- 1111111111111 2222222222 (cam A)
1148
- 11111111111[...]222222222 (cam B)
1324
+ Cluster 1 Cluster 2
1325
+ 1111111111111 2222222222 (cam A)
1326
+ 11111111111[inserted gap]222222222 (cam B)
1149
1327
 
1150
1328
  or
1151
- 1111111111111 222222 (cam A)
1152
- 1111111 22222 (cam B)
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.video_mergers]
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.get_timecode(with_offset=total_offset)
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.get_timecode(), tc))
1413
+ vid.get_start_timecode_string(), tc))
1224
1414
  vid.write_file_timecode(tc)
1225
1415
  return
1226
1416