tictacsync 0.82a0__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/timeline.py CHANGED
@@ -237,7 +237,6 @@ def _sox_mono2stereo(temp_file) -> tempfile.NamedTemporaryFile:
237
237
  sys.exit(1)
238
238
  return stereo_tempfile
239
239
 
240
-
241
240
  def _sox_mix_files(temp_files_to_mix:list) -> tempfile.NamedTemporaryFile:
242
241
  """
243
242
  Mix files referred by the list of Path into a new temporary files passed on
@@ -303,9 +302,10 @@ class AudioStitcherVideoMerger:
303
302
  """
304
303
  Typically each found video is associated with an AudioStitcherVideoMerger
305
304
  instance. AudioStitcherVideoMerger does the actual audio-video file
306
- processing of merging self.videoclip (gen. a video) with all audio
307
- files in self.edited_audio as determined by the Matcher
308
- object (it instanciates and manages AudioStitcherVideoMerger objects).
305
+ processing of merging AudioStitcherVideoMerger.videoclip (gen. a video)
306
+ with all audio files in AudioStitcherVideoMerger.soxed_audio as
307
+ determined by the Matcher object (Matcher instanciates and manages
308
+ AudioStitcherVideoMerger objects).
309
309
 
310
310
  All audio file edits are done using pysox and video+audio merging with
311
311
  ffmpeg. When necessary, clock drift is corrected for all overlapping audio
@@ -317,58 +317,64 @@ 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
+ 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.edited_audio = {}
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 self.edited_audio, a dict as {Recording : path}
354
+ Populates AudioStitcherVideoMerger.soxed_audio,
355
+ a dict as {Recording : path}
347
356
 
348
- AudioStitcherVideoMerger.add_matched_audio() is called
357
+ This fct is called
349
358
  within Matcher.scan_audio_for_each_videoclip()
350
359
 
351
- Returns nothing, fills self.edited_audio dict with
360
+ Returns nothing, fills self.soxed_audio dict with
352
361
  matched audio.
353
362
 
354
363
  """
355
- self.edited_audio[audio_rec] = audio_rec.valid_sound
364
+ self.soxed_audio[audio_rec] = audio_rec.AVpath
356
365
  """
357
- Here at this point, self.edited_audio[audio_rec] is unedited but
358
- after a call to _edit_audio_file(), edited_audio[audio_rec] points to
359
- a new file and the precedent is unchanged (that's why from
360
- AudioStitcherVideoMerger instance to another
361
- audio_rec.valid_sound doesn't need to be reinitialized since
362
- it stays unchanged)
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
368
+ a new file and audio_rec.AVpath is unchanged.
363
369
  """
364
370
  return
365
371
 
366
372
  def get_matched_audio_recs(self):
367
373
  """
368
374
  Returns audio recordings that overlap self.videoclip.
369
- Simply keys of self.edited_audio dict
375
+ Simply keys of self.soxed_audio dict
370
376
  """
371
- return list(self.edited_audio.keys())
377
+ return list(self.soxed_audio.keys())
372
378
 
373
379
  def _get_audio_devices(self):
374
380
  devices = set([r.device for r in self.get_matched_audio_recs()])
@@ -384,59 +390,77 @@ class AudioStitcherVideoMerger:
384
390
  return recs
385
391
 
386
392
  def _dedrift_rec(self, rec):
387
- # first_audio_p = rec.AVpath
388
- initial_duration = sox.file_info.duration(
389
- _pathname(rec.valid_sound))
390
- sox_transform = sox.Transformer()
391
- # 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)
392
395
  tempo_scale_factor = rec.device_relative_speed
393
396
  audio_dev = rec.device.name
394
397
  video_dev = self.videoclip.device.name
398
+ print('when merging with [gold1]%s[/gold1].'%self.videoclip)
395
399
  if tempo_scale_factor > 1:
396
- print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor\n'%
400
+ print('Because [gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]: file is too long by a %.12f factor;'%
397
401
  (audio_dev, video_dev, tempo_scale_factor))
398
402
  else:
399
- print('[gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is too short by a %f factor\n'%
403
+ print('Because [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1]: file is short by a %.12f factor'%
400
404
  (audio_dev, video_dev, tempo_scale_factor))
401
- sox_transform.tempo(tempo_scale_factor)
402
- # scaled_file = self._get_soxed_file(rec, sox_transform)
403
- logger.debug('sox_transform %s'%sox_transform.effects)
404
- self._edit_audio_file(rec, sox_transform)
405
- scaled_file_name = _pathname(self.edited_audio[rec])
406
- new_duration = sox.file_info.duration(scaled_file_name)
407
- # goal_duration = rec.get_corrected_duration()
408
- logger.debug('initial_duration %f new_duration %f ratio:%f'%(
409
- initial_duration, new_duration, initial_duration/new_duration))
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))
410
434
 
411
435
  def _get_concatenated_audiofile_for(self, device):
412
436
  """
413
437
  return a handle for the final audio file formed by all detected
414
- overlapping recordings, produced by the same specified device.
438
+ overlapping recordings, produced by the same audio recorder.
415
439
 
416
440
  """
417
441
  logger.debug('concatenating device %s'%str(device))
418
- recordings = self._get_all_recordings_for(device)
442
+ audio_recs = self._get_all_recordings_for(device)
419
443
  # [TODO here] Check if all unidentified device files are not
420
444
  # overlapping because they are considered produced by the same
421
445
  # device. If some overlap then necessarily they're from different
422
446
  # ones. List the files and warn the user there is a risk of error if
423
447
  # they're not from the same device.
424
448
 
425
- logger.debug('%i audio files for videoclip %s:'%(len(recordings),
449
+ logger.debug('%i audio files for videoclip %s:'%(len(audio_recs),
426
450
  self.videoclip))
427
- for r in recordings:
451
+ for r in audio_recs:
428
452
  logger.debug(' %s'%r)
453
+ # ratio between real samplerates of audio and videoclip
429
454
  speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
430
- for rec in recordings])
455
+ for rec in audio_recs])
431
456
  mean_speed = numpy.mean(speeds)
432
- for r in recordings:
433
- r.device_relative_speed = mean_speed
434
- # r.device_relative_speed = 0.9
435
- logger.debug('set device_relative_speed for %s'%r)
436
- logger.debug(' value: %f'%r.device_relative_speed)
437
- r.set_time_position_to(self.videoclip)
438
- logger.debug('time_position for %s: %fs relative to %s'%(r,
439
- 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))
440
464
  # st_dev_speeds just to check for anomalous situation
441
465
  st_dev_speeds = numpy.std(speeds)
442
466
  logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
@@ -461,7 +485,7 @@ class AudioStitcherVideoMerger:
461
485
 
462
486
  # process first element 'by hand' outside the loop
463
487
  # first_audio is a Recording, not a path nor filehandle
464
- first_audio = recordings[0]
488
+ first_audio = audio_recs[0]
465
489
  needs_dedrift, delta = first_audio.needs_dedrifting()
466
490
  logger.debug('first audio is %s'%first_audio)
467
491
  logger.debug('checking drift, first audio: delta of %0.2f ms'%(
@@ -473,8 +497,8 @@ class AudioStitcherVideoMerger:
473
497
  self._pad_or_trim_first_audio(first_audio)
474
498
  # loop for the other files
475
499
  # growing_file = first_audio.edited_version
476
- growing_file = self.edited_audio[first_audio]
477
- for i, rec in enumerate(recordings[1:]):
500
+ growing_file = self.soxed_audio[first_audio]
501
+ for i, rec in enumerate(audio_recs[1:]):
478
502
  logger.debug('Padding and joining for %s'%rec)
479
503
  needs_dedrift, delta = rec.needs_dedrifting()
480
504
  logger.debug('next audio is %s'%rec)
@@ -497,7 +521,7 @@ class AudioStitcherVideoMerger:
497
521
  (rec,rec.time_position,end_time))
498
522
  self._pad_file(rec, pad_duration)
499
523
  # new_file = rec.edited_version
500
- new_file = self.edited_audio[rec]
524
+ new_file = self.soxed_audio[rec]
501
525
  growing_file = self._concatenate_audio_files(growing_file, new_file)
502
526
  end_time = sox.file_info.duration(growing_file.name)
503
527
  logger.debug('total edited audio duration %.2f s'%end_time)
@@ -510,7 +534,7 @@ class AudioStitcherVideoMerger:
510
534
  TODO: check if first_rec is a Recording or tempfile (maybe a tempfile if dedrifted)
511
535
  NO: will change tempo after trimming/padding
512
536
 
513
- Store (into Recording.edited_audio dict) the handle of the sox processed
537
+ Store (into Recording.soxed_audio dict) the handle of the sox processed
514
538
  first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
515
539
  starting time. Length of the written file can differ from length of the
516
540
  submitted Recording object if drift is corrected with sox tempo
@@ -574,22 +598,25 @@ class AudioStitcherVideoMerger:
574
598
  def _edit_audio_file(self, audio_rec, sox_transform):
575
599
  """
576
600
  Apply the specified sox_transform onto the audio_rec and update
577
- self.edited_audio dict with the result (with audio_rec as the key)
601
+ self.soxed_audio dict with the result (with audio_rec as the key)
602
+ Returns the filehandle of the result.
578
603
  """
579
604
  output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
580
605
  logger.debug('transform: %s'%sox_transform.effects)
581
- recording_fh = self.edited_audio[audio_rec]
606
+ recording_fh = self.soxed_audio[audio_rec]
582
607
  logger.debug('for recording %s, matching %s'%(audio_rec,
583
608
  self.videoclip))
584
609
  input_file = _pathname(recording_fh)
585
- logger.debug('AudioStitcherVideoMerger.edited_audio[audio_rec]: %s'%
610
+ logger.debug('AudioStitcherVideoMerger.soxed_audio[audio_rec]: %s'%
586
611
  input_file)
587
612
  out_file = _pathname(output_fh)
588
613
  logger.debug('sox in and out files: %s %s'%(input_file, out_file))
614
+ logger.debug('calling sox_transform.build()')
589
615
  status = sox_transform.build(input_file, out_file, return_output=True )
590
616
  logger.debug('sox.build exit code %s'%str(status))
591
617
  # audio_rec.edited_version = output_fh
592
- self.edited_audio[audio_rec] = output_fh
618
+ self.soxed_audio[audio_rec] = output_fh
619
+ return output_fh
593
620
 
594
621
  def _write_ISOs(self, edited_audio_all_devices):
595
622
  """
@@ -740,9 +767,10 @@ class AudioStitcherVideoMerger:
740
767
  (device.ttc, device.tracks.ttc))
741
768
  if device.ttc + 1 != device.tracks.ttc: # warn and quit
742
769
  print('Error: TicTacCode channel detected is [gold1]%i[/gold1]'%
743
- device.ttc + 1, end=' ')
744
- print('and the file [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
745
- (device_scanner.TRACKSFN, device.tracks.ttc))
770
+ (device.ttc), end=' ')
771
+ print('and [gold1]%s[/gold1] for the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
772
+ (device_scanner.TRACKSFN,
773
+ device.name, device.tracks.ttc-1))
746
774
  print('Please correct the discrepancy and rerun. Quitting.')
747
775
  sys.exit(1)
748
776
  if device.tracks.mix == [] and device.tracks.stereomics == []:
@@ -799,10 +827,12 @@ class AudioStitcherVideoMerger:
799
827
  logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
800
828
  mono_tracks = [i for i in range(1, device.n_chan + 1)
801
829
  if i not in stereo_mic_idx_flat]
802
- logger.debug('mono_tracks: %s'%mono_tracks)
830
+ logger.debug('mono_tracks (with ttc+zeroed included): %s'%mono_tracks)
803
831
  # remove TTC track number
804
- mono_tracks.remove(device.ttc + 1)
805
- logger.debug('mono_tracks %s'%mono_tracks)
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)
806
836
  mono_files = [_sox_keep(multichan_tmpfl, [chan]) for chan
807
837
  in mono_tracks]
808
838
  new_stereo_files = [_sox_mono2stereo(f) for f in mono_files]
@@ -990,7 +1020,7 @@ class AudioStitcherVideoMerger:
990
1020
  """
991
1021
  synced_clip_file = self.videoclip.final_synced_file
992
1022
  video_path = self.videoclip.AVpath
993
- timecode = self.videoclip.get_timecode()
1023
+ timecode = self.videoclip.get_start_timecode_string()
994
1024
  # self.videoclip.synced_audio = audio_path
995
1025
  audio_path = self.videoclip.synced_audio
996
1026
  vid_only_handle = self._keep_VIDEO_only(video_path)
@@ -1138,20 +1168,28 @@ class Matcher:
1138
1168
  case4 = R1 < A2 < R2
1139
1169
  return case1 or case2 or case3 or case4
1140
1170
 
1141
- def shrink_gaps_between_takes(self, with_gap=CLUSTER_GAP):
1171
+ def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
1142
1172
  """
1143
1173
  for single cam shootings this simply sets the gap between takes,
1144
1174
  tweaking each vid timecode metadata to distribute them next to each
1145
- other along NLE timeline. For multicam takes, shifts are computed so
1175
+ other along NLE timeline.
1176
+
1177
+ Moves clusters at the timelineoffset
1178
+
1179
+ For multicam takes, shifts are computed so
1146
1180
  video clusters are near but dont overlap, ex:
1147
1181
 
1148
- Cluster 1 Cluster 2
1149
- 1111111111111 2222222222 (cam A)
1150
- 11111111111[...]222222222 (cam B)
1182
+ Cluster 1 Cluster 2
1183
+ 1111111111111 2222222222 (cam A)
1184
+ 11111111111[inserted gap]222222222 (cam B)
1151
1185
 
1152
1186
  or
1153
- 1111111111111 222222 (cam A)
1154
- 1111111 22222 (cam B)
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
1155
1193
 
1156
1194
  Returns nothing, changes are done in the video files metadata
1157
1195
  (each referenced by Recording.final_synced_file)
@@ -1212,17 +1250,25 @@ class Matcher:
1212
1250
  cummulative_offsets = [td.total_seconds() for td in cummulative_offsets]
1213
1251
  logger.debug('cummulative_offsets: %s'%cummulative_offsets)
1214
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)
1215
1258
  offset_for_all_clips = - from_midnight(time_of_first).total_seconds()
1259
+ offset_for_all_clips += CLI_offset_in_seconds
1216
1260
  logger.debug('time_of_first: %s'%time_of_first)
1217
1261
  logger.debug('offset_for_all_clips: %s'%offset_for_all_clips)
1218
1262
  for cluster, offset in zip(clusters, cummulative_offsets):
1263
+ # first one starts at 00:00:00:00
1219
1264
  total_offset = offset + offset_for_all_clips
1220
1265
  logger.debug('for %s offset in sec: %f'%(cluster['vids'],
1221
1266
  total_offset))
1222
1267
  for vid in cluster['vids']:
1223
- tc = vid.get_timecode(with_offset=total_offset)
1268
+ # tc = vid.get_start_timecode_string(CLI_offset, with_offset=total_offset)
1269
+ tc = vid.get_start_timecode_string(with_offset=total_offset)
1224
1270
  logger.debug('for %s old tc: %s new tc %s'%(vid,
1225
- vid.get_timecode(), tc))
1271
+ vid.get_start_timecode_string(), tc))
1226
1272
  vid.write_file_timecode(tc)
1227
1273
  return
1228
1274