tictacsync 0.3a4__py3-none-any.whl → 0.4a0__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
@@ -26,13 +26,13 @@ OUT_DIR_DEFAULT = 'SyncedMedia'
26
26
  # utility for accessing pathnames
27
27
  def _pathname(tempfile_or_path) -> str:
28
28
  if isinstance(tempfile_or_path, str):
29
- return tempfile_or_path
29
+ return tempfile_or_path ################################################
30
30
  if isinstance(tempfile_or_path, yaltc.Recording):
31
- return str(tempfile_or_path.AVpath)
31
+ return str(tempfile_or_path.AVpath) ####################################
32
32
  if isinstance(tempfile_or_path, Path):
33
- return str(tempfile_or_path)
33
+ return str(tempfile_or_path) ###########################################
34
34
  if isinstance(tempfile_or_path, tempfile._TemporaryFileWrapper):
35
- return tempfile_or_path.name
35
+ return tempfile_or_path.name ###########################################
36
36
  else:
37
37
  raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
38
38
 
@@ -62,17 +62,12 @@ def _extr_channel(source, dest, channel):
62
62
  status = sox_transform.build(str(source), str(dest))
63
63
  logger.debug('sox status %s'%status)
64
64
 
65
- def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
65
+ def _sox_keep(audio_file, kept_channels: list) -> tempfile.NamedTemporaryFile:
66
66
  """
67
67
  Returns a NamedTemporaryFile containing the selected kept_channels
68
68
 
69
- if len(kept_channels) == 1 then it's a mono mix on the specified track
70
- if len(kept_channels) == 2 then it's a stereo mix on the specified tracks
69
+ Channels numbers in kept_channels are not ZBIDXed as per SOX format
71
70
  """
72
-
73
-
74
-
75
-
76
71
  audio_file = _pathname(audio_file)
77
72
  nchan = sox.file_info.channels(audio_file)
78
73
  logger.debug('in file of %i chan, have to keep %s'%
@@ -83,8 +78,8 @@ def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
83
78
  # eg: {1: [3], 2: [4]} to keep channels 3 & 4
84
79
  kept_channels = [[n] for n in kept_channels]
85
80
  sox_remix_dict = dict(zip(all_channels, kept_channels))
86
- output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
87
- out_file = _pathname(output_fh)
81
+ output_tempfile = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
82
+ out_file = _pathname(output_tempfile)
88
83
  logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
89
84
  # sox_transform.set_output_format(channels=1)
90
85
  sox_transform = sox.Transformer()
@@ -103,8 +98,7 @@ def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
103
98
  stdout, stderr = p.communicate()
104
99
  logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
105
100
  stderr).decode('utf-8'))
106
- return output_fh
107
-
101
+ return output_tempfile
108
102
 
109
103
  def _split_channels(multi_chan_audio:Path) -> list:
110
104
  nchan = sox.file_info.channels(_pathname(multi_chan_audio))
@@ -132,7 +126,7 @@ def _sox_combine(paths) -> Path:
132
126
  """
133
127
  if len(paths) == 1: # one device only, nothing to stack
134
128
  logger.debug('one device only, nothing to stack')
135
- return paths[0]
129
+ return paths[0] ########################################################
136
130
  out_file_handle = tempfile.NamedTemporaryFile(suffix='.wav',
137
131
  delete=DEL_TEMP)
138
132
  filenames = [_pathname(p) for p in paths]
@@ -158,29 +152,96 @@ def _sox_combine(paths) -> Path:
158
152
  (merged_duration, nchan))
159
153
  return out_file_handle
160
154
 
161
- def _sox_multi2mono(multichan_tmpfl) -> tempfile.NamedTemporaryFile:
162
- # return a mono mix down
155
+ def _sox_multi2stereo(multichan_tmpfl, stereo_trxs) -> tempfile.NamedTemporaryFile:
156
+
157
+ """
158
+ This mixes down all the tracks in multichan_tmpfl to a stereo wav file. Any
159
+ mono tracks are panned 50-50 (mono tracks are those not present in argument
160
+ stereo_trxs)
161
+
162
+ Args:
163
+ multichan_tmpfl : tempfile.NamedTemporaryFile
164
+ contains the edited and synced audio, almost ready to be merged
165
+ with the concurrent video file
166
+ stereo_trxs : list of pairs of integers
167
+ each pairs identifies a left-right tracks, 1st track in
168
+ multichan_tmpfl is index 1 (sox is not ZBIDX)
169
+ Returns:
170
+ the tempfile.NamedTemporaryFile of a stereo wav file
171
+ containing the audio to be merged with the video
172
+ """
163
173
  n_chan_input = sox.file_info.channels(_pathname(multichan_tmpfl))
164
174
  logger.debug('n chan input: %s'%n_chan_input)
165
175
  if n_chan_input == 1: # nothing to mix down
166
- return multichan_tmpfl
167
- mono_tpfl = tempfile.NamedTemporaryFile(suffix='.wav',
176
+ return multichan_tmpfl #################################################
177
+ stereo_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
168
178
  delete=DEL_TEMP)
169
179
  tfm = sox.Transformer()
170
180
  tfm.channels(1)
171
- status = tfm.build(_pathname(multichan_tmpfl),_pathname(mono_tpfl))
181
+ status = tfm.build(_pathname(multichan_tmpfl),_pathname(stereo_tempfile))
182
+ logger.debug('n chan ouput: %s'%
183
+ sox.file_info.channels(_pathname(stereo_tempfile)))
184
+ logger.debug('sox.build status for _sox_multi2stereo(): %s'%status)
185
+ if status != True:
186
+ print('Error, sox did not normalize file in _sox_multi2stereo()')
187
+ sys.exit(1)
188
+ return stereo_tempfile
189
+
190
+ def _sox_mix_channels(multichan_tmpfl, stereo_pairs=[]) -> tempfile.NamedTemporaryFile:
191
+ """
192
+ Returns a mix down of the multichannel wav file. If stereo_pairs list is
193
+ empty, a mono mix is done with all the channel present in multichan_tmpfl.
194
+ If stereo_pairs contains one or more elements, a stereo mix is returned with
195
+ the specified Left-Right pairs and all other mono tracks (panned 50-50)
196
+
197
+ Note: stereo_pairs numbers are not ZBIDXed
198
+ """
199
+ n_chan_input = sox.file_info.channels(_pathname(multichan_tmpfl))
200
+ logger.debug('n chan input: %s'%n_chan_input)
201
+ if n_chan_input == 1: # nothing to mix down
202
+ return multichan_tmpfl #################################################
203
+ if stereo_pairs == []:
204
+ # all mono
205
+ mono_tpfl = tempfile.NamedTemporaryFile(suffix='.wav',
206
+ delete=DEL_TEMP)
207
+ tfm = sox.Transformer()
208
+ tfm.channels(1)
209
+ status = tfm.build(_pathname(multichan_tmpfl),_pathname(mono_tpfl))
210
+ logger.debug('number of chan in ouput: %s'%
211
+ sox.file_info.channels(_pathname(mono_tpfl)))
212
+ logger.debug('sox.build status for _sox_mix_channels(): %s'%status)
213
+ if status != True:
214
+ print('Error, sox did not normalize file in _sox_mix_channels()')
215
+ sys.exit(1)
216
+ return mono_tpfl
217
+ else:
218
+ # stereo tracks present, so stereo output
219
+ logger.debug('stereo tracks present %s, so stereo output'%stereo_pairs)
220
+ stereo_files = [_sox_keep(pair) for pair in stereo_pairs]
221
+ #### ???
222
+ return
223
+
224
+ def _sox_mono2stereo(temp_file) -> tempfile.NamedTemporaryFile:
225
+ # upgrade a mono file to stereo panning 50-50
226
+ stereo_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
227
+ delete=DEL_TEMP)
228
+ tfm = sox.Transformer()
229
+ tfm.channels(2)
230
+ status = tfm.build(_pathname(temp_file),_pathname(stereo_tempfile))
172
231
  logger.debug('n chan ouput: %s'%
173
- sox.file_info.channels(_pathname(mono_tpfl)))
174
- logger.debug('sox.build status for _sox_multi2mono(): %s'%status)
232
+ sox.file_info.channels(_pathname(stereo_tempfile)))
233
+ logger.debug('sox.build status for _sox_mono2stereo(): %s'%status)
175
234
  if status != True:
176
- print('Error, sox did not normalize file in _sox_multi2mono()')
235
+ print('Error, sox did not normalize file in _sox_mono2stereo()')
177
236
  sys.exit(1)
178
- return mono_tpfl
237
+ return stereo_tempfile
179
238
 
180
239
 
181
- def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
240
+ def _sox_mix_files(temp_files_to_mix:list) -> tempfile.NamedTemporaryFile:
182
241
  """
183
- mix files referred by the list of Path into a new temporary files passed on return
242
+ Mix files referred by the list of Path into a new temporary files passed on
243
+ return. If one of the files is stereo, upgrade each mono file to a panned
244
+ 50-50 stereo file before mixing.
184
245
  """
185
246
  def _sox_norm(tempf):
186
247
  normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
@@ -190,28 +251,42 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
190
251
  status = tfm.build(_pathname(tempf),_pathname(normed_tempfile))
191
252
  logger.debug('sox.build status for norm(): %s'%status)
192
253
  if status != True:
193
- print('Error, sox did not normalize file in _sox_mix()')
254
+ print('Error, sox did not normalize file in _sox_mix_files()')
194
255
  sys.exit(1)
195
256
  return normed_tempfile
196
- paths = [_sox_norm(p) for p in paths]
197
- cbn = sox.Combiner()
198
- N = len(paths)
257
+ N = len(temp_files_to_mix)
199
258
  if N == 1: # nothing to mix
200
259
  logger.debug('one file: nothing to mix')
201
- return paths[0]
260
+ return temp_files_to_mix[0] ########################################################
261
+ cbn = sox.Combiner()
202
262
  cbn.set_input_format(file_type=['wav']*N)
203
- filenames = [_pathname(p) for p in paths]
204
- logger.debug('%i files to mix %s'%(N, filenames))
205
- logger.debug('nchan for each file %s'%[sox.file_info.channels(f) for
206
- f in filenames])
263
+ # check if stereo files are present
264
+ max_n_chan = max([sox.file_info.channels(f) for f
265
+ in [_pathname(p) for p in temp_files_to_mix]])
266
+ logger.debug('max_n_chan %s'%max_n_chan)
267
+ if max_n_chan == 2:
268
+ # upgrade all mono to stereo
269
+ stereo_tempfiles = [p for p in temp_files_to_mix
270
+ if sox.file_info.channels(_pathname(p)) == 2 ]
271
+ mono_tempfiles = [p for p in temp_files_to_mix
272
+ if sox.file_info.channels(_pathname(p)) == 1 ]
273
+ logger.debug('there are %i mono files and %i stereo files'%
274
+ (len(stereo_tempfiles), len(mono_tempfiles)))
275
+ new_stereo = [_sox_mono2stereo(tmpfl) for tmpfl
276
+ in mono_tempfiles]
277
+ stereo_tempfiles += new_stereo
278
+ files_to_mix = [_pathname(tempfl) for tempfl in stereo_tempfiles]
279
+ else:
280
+ # all mono
281
+ files_to_mix = [_pathname(tempfl) for tempfl in temp_files_to_mix]
207
282
  mixed_tempf = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
208
- status = cbn.build(filenames,
283
+ status = cbn.build(files_to_mix,
209
284
  _pathname(mixed_tempf),
210
285
  combine_type='mix',
211
286
  input_volumes=[1/N]*N)
212
287
  logger.debug('sox.build status for mix: %s'%status)
213
288
  if status != True:
214
- print('Error, sox did not mix files in _sox_mix()')
289
+ print('Error, sox did not mix files in _sox_mix_files()')
215
290
  sys.exit(1)
216
291
  normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
217
292
  tfm = sox.Transformer()
@@ -219,16 +294,15 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
219
294
  status = tfm.build(_pathname(mixed_tempf),_pathname(normed_tempfile))
220
295
  logger.debug('sox.build status for norm(): %s'%status)
221
296
  if status != True:
222
- print('Error, sox did not normalize file in _sox_mix()')
297
+ print('Error, sox did not normalize file in _sox_mix_files()')
223
298
  sys.exit(1)
224
299
  return normed_tempfile
225
300
 
226
-
227
301
  class AudioStitcherVideoMerger:
228
302
  """
229
303
  Typically each found video is associated with an AudioStitcherVideoMerger
230
304
  instance. AudioStitcherVideoMerger does the actual audio-video file
231
- processing of merging self.ref_recording (gen. a video) with all audio
305
+ processing of merging self.videoclip (gen. a video) with all audio
232
306
  files in self.edited_audio as determined by the Matcher
233
307
  object (it instanciates and manages AudioStitcherVideoMerger objects).
234
308
 
@@ -237,15 +311,15 @@ class AudioStitcherVideoMerger:
237
311
  devices to match the precise clock value of the ref recording (to a few
238
312
  ppm), using sox tempo transform.
239
313
 
240
- N.B.: A audio_stitch doesn't extend beyond the corresponding ref_recording
314
+ N.B.: A audio_stitch doesn't extend beyond the corresponding videoclip
241
315
  video start and end times: it is not a audio montage for the whole movie
242
316
  project.
243
317
 
244
318
 
245
319
  Attributes:
246
320
 
247
- ref_recording : a Recording instance
248
- The video (or designated main sound) audio files are synced to
321
+ videoclip : a Recording instance
322
+ The video to which audio files are synced
249
323
 
250
324
  edited_audio : dict as {Recording : path}
251
325
  keys are elements of matched_audio_recordings of class Recording
@@ -259,19 +333,19 @@ class AudioStitcherVideoMerger:
259
333
 
260
334
  """
261
335
 
262
- def __init__(self, reference_recording):
263
- self.ref_recording = reference_recording
336
+ def __init__(self, video_clip):
337
+ self.videoclip = video_clip
264
338
  # self.matched_audio_recordings = []
265
339
  self.edited_audio = {}
266
340
  logger.debug('instantiating AudioStitcherVideoMerger for %s'%
267
- reference_recording)
341
+ video_clip)
268
342
 
269
343
  def add_matched_audio(self, audio_rec):
270
344
  """
271
345
  Populates self.edited_audio, a dict as {Recording : path}
272
346
 
273
347
  AudioStitcherVideoMerger.add_matched_audio() is called
274
- within Matcher.scan_audio_for_each_ref_rec()
348
+ within Matcher.scan_audio_for_each_videoclip()
275
349
 
276
350
  Returns nothing, fills self.edited_audio dict with
277
351
  matched audio.
@@ -290,7 +364,7 @@ class AudioStitcherVideoMerger:
290
364
 
291
365
  def get_matched_audio_recs(self):
292
366
  """
293
- Returns audio recordings that overlap self.ref_recording.
367
+ Returns audio recordings that overlap self.videoclip.
294
368
  Simply keys of self.edited_audio dict
295
369
  """
296
370
  return list(self.edited_audio.keys())
@@ -315,14 +389,14 @@ class AudioStitcherVideoMerger:
315
389
  sox_transform = sox.Transformer()
316
390
  # tempo_scale_factor = rec.device_relative_speed
317
391
  tempo_scale_factor = rec.device_relative_speed
318
- reC_dev = rec.device.name
319
- reF_dev = self.ref_recording.device.name
392
+ audio_dev = rec.device.name
393
+ video_dev = self.videoclip.device.name
320
394
  if tempo_scale_factor > 1:
321
395
  print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor\n'%
322
- (reC_dev, reF_dev, tempo_scale_factor))
396
+ (audio_dev, video_dev, tempo_scale_factor))
323
397
  else:
324
398
  print('[gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is too short by a %f factor\n'%
325
- (reC_dev, reF_dev, tempo_scale_factor))
399
+ (audio_dev, video_dev, tempo_scale_factor))
326
400
  sox_transform.tempo(tempo_scale_factor)
327
401
  # scaled_file = self._get_soxed_file(rec, sox_transform)
328
402
  logger.debug('sox_transform %s'%sox_transform.effects)
@@ -347,11 +421,11 @@ class AudioStitcherVideoMerger:
347
421
  # ones. List the files and warn the user there is a risk of error if
348
422
  # they're not from the same device.
349
423
 
350
- logger.debug('%i audio files for reference rec %s:'%(len(recordings),
351
- self.ref_recording))
424
+ logger.debug('%i audio files for videoclip %s:'%(len(recordings),
425
+ self.videoclip))
352
426
  for r in recordings:
353
427
  logger.debug(' %s'%r)
354
- speeds = numpy.array([rec.get_speed_ratio(self.ref_recording)
428
+ speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
355
429
  for rec in recordings])
356
430
  mean_speed = numpy.mean(speeds)
357
431
  for r in recordings:
@@ -359,9 +433,9 @@ class AudioStitcherVideoMerger:
359
433
  # r.device_relative_speed = 0.9
360
434
  logger.debug('set device_relative_speed for %s'%r)
361
435
  logger.debug(' value: %f'%r.device_relative_speed)
362
- r.set_time_position_to(self.ref_recording)
436
+ r.set_time_position_to(self.videoclip)
363
437
  logger.debug('time_position for %s: %fs relative to %s'%(r,
364
- r.time_position, self.ref_recording))
438
+ r.time_position, self.videoclip))
365
439
  # st_dev_speeds just to check for anomalous situation
366
440
  st_dev_speeds = numpy.std(speeds)
367
441
  logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
@@ -427,7 +501,7 @@ class AudioStitcherVideoMerger:
427
501
  end_time = sox.file_info.duration(growing_file.name)
428
502
  logger.debug('total edited audio duration %.2f s'%end_time)
429
503
  logger.debug('video duration %.2f s'%
430
- self.ref_recording.get_duration())
504
+ self.videoclip.get_duration())
431
505
  return growing_file
432
506
 
433
507
  def _pad_or_trim_first_audio(self, first_rec):
@@ -436,17 +510,17 @@ class AudioStitcherVideoMerger:
436
510
  NO: will change tempo after trimming/padding
437
511
 
438
512
  Store (into Recording.edited_audio dict) the handle of the sox processed
439
- first recording, padded or chopped according to AudioStitcherVideoMerger.ref_recording
513
+ first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
440
514
  starting time. Length of the written file can differ from length of the
441
515
  submitted Recording object if drift is corrected with sox tempo
442
516
  transform, so check it with sox.file_info.duration()
443
517
  """
444
518
  logger.debug(' editing %s'%first_rec)
445
519
  audio_start = first_rec.get_start_time()
446
- ref_start = self.ref_recording.get_start_time()
447
- if ref_start < audio_start: # padding
520
+ video_start = self.videoclip.get_start_time()
521
+ if video_start < audio_start: # padding
448
522
  logger.debug('padding')
449
- pad_duration = (audio_start-ref_start).total_seconds()
523
+ pad_duration = (audio_start-video_start).total_seconds()
450
524
  """padding first_file:
451
525
  ┏━━━━━━━━━━━━━━━┓
452
526
  ┗━━━━━━━━━━━━━━━┛ref
@@ -456,7 +530,7 @@ class AudioStitcherVideoMerger:
456
530
  self._pad_file(first_rec, pad_duration)
457
531
  else:
458
532
  logger.debug('trimming')
459
- length = (ref_start-audio_start).total_seconds()
533
+ length = (video_start-audio_start).total_seconds()
460
534
  """chopping first_file:
461
535
  ┏━━━━━━━━━━━━━━━┓
462
536
  ┗━━━━━━━━━━━━━━━┛ref
@@ -505,7 +579,7 @@ class AudioStitcherVideoMerger:
505
579
  logger.debug('transform: %s'%sox_transform.effects)
506
580
  recording_fh = self.edited_audio[audio_rec]
507
581
  logger.debug('for recording %s, matching %s'%(audio_rec,
508
- self.ref_recording))
582
+ self.videoclip))
509
583
  input_file = _pathname(recording_fh)
510
584
  logger.debug('AudioStitcherVideoMerger.edited_audio[audio_rec]: %s'%
511
585
  input_file)
@@ -551,7 +625,7 @@ class AudioStitcherVideoMerger:
551
625
  """
552
626
  sox_transform = sox.Transformer()
553
627
  audio_length = sox.file_info.duration(_pathname(audio_tempfile))
554
- video_length = self.ref_recording.get_duration()
628
+ video_length = self.videoclip.get_duration()
555
629
  if audio_length > video_length:
556
630
  # trim audio
557
631
  sox_transform.trim(0, video_length)
@@ -570,9 +644,9 @@ class AudioStitcherVideoMerger:
570
644
  logger.debug('audio duration %.2f s'%
571
645
  sox.file_info.duration(_pathname(out_tf)))
572
646
  logger.debug('video duration %.2f s'%
573
- self.ref_recording.get_duration())
647
+ self.videoclip.get_duration())
574
648
  return out_tf
575
- synced_clip_file = self.ref_recording.final_synced_file
649
+ synced_clip_file = self.videoclip.final_synced_file
576
650
  synced_clip_dir = synced_clip_file.parent
577
651
  # build ISOs subfolders structure, see comment string below
578
652
  video_stem_WO_suffix = synced_clip_file.stem
@@ -591,75 +665,129 @@ class AudioStitcherVideoMerger:
591
665
  mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
592
666
  shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
593
667
  logger.debug('destination:%s'%destination)
594
- # # mixNnormed = _sox_mix(tempfiles)
668
+ # # mixNnormed = _sox_mix_files(tempfiles)
595
669
  # # print('516', _pathname(mixNnormed))
596
670
  # os.remove(ISO_multi_chan)
597
671
 
598
- def _get_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
599
- """
600
- If device has an associated Tracks description that declares a (mono or
601
- stereo) mix track, returns a tmpfl containing the corresponding
602
- tracks. If not, mix all the tracks with sox.
672
+ def _get_device_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
673
+ """
674
+ Build or get a mix from edited and joined audio for a given device
675
+
676
+ Returns a mix for merging with video clip. The way the mix is obtained
677
+ (or created) depends if a tracks.txt for the device was submitted and
678
+ depends on its content. There are 4 cases (explained later):
679
+
680
+ #1 no mix (or no tracks.txt), all mono
681
+ #2 no mix, one or more stereo mics
682
+ #3 mono mix declared
683
+ #4 stereo mix declared
684
+
685
+ In details:
686
+
687
+ If no device tracks.txt file declared a mix track (or if tracks.txt is
688
+ absent), a mix is done programmatically. Two possibilities:
689
+
690
+ #1- no stereo pairs were declared: a global mono mix is returned.
691
+ #2- one or more stereo pair mics were used and declared (micL, micR):
692
+ a global stereo mix is returned with mono tracks panned 50-50
693
+
694
+ If device has an associated Tracks description AND it declares a(mono or
695
+ stereo) mix track, this fct returns a tempfile containing the
696
+ corresponding tracks, simpley extarcting them from multichan_tmpfl
697
+ (thos covers cases #3 and #4)
698
+
699
+ Args:
700
+ device : device_scanner.Device dataclass
701
+ the device that recorded the audio found in multichan_tmpfl
702
+ multichan_tmpfl : tempfile.NamedTemporaryFile
703
+ contains the edited and synced audio, almost ready to be merged
704
+ with the concurrent video file (after mix down)
705
+
706
+ Returns:
707
+ the tempfile.NamedTemporaryFile of a stereo or mono wav file
708
+ containing the audio to be merged with the video in
709
+ self.videoclip
603
710
 
604
- If no L-R tracks are declared in tracks.txt, a mono mix is returned;
605
- If some
606
- micL micR or mixL mixR
607
711
 
608
712
  """
609
- if device.tracks is None:
610
- logger.debug('no tracks.txt, mixing all')
611
- return _sox_multi2mono(multichan_tmpfl)
612
- mix_tracks = device.tracks.mix
613
- if mix_tracks == []:
614
- logger.debug('tracks.txt present but no mix trx, mixing all')
615
- return _sox_multi2mono(multichan_tmpfl)
616
- # if here, mix exists
617
- logger.debug('%s has mix %s'%(device.name, mix_tracks))
618
713
  logger.debug('device %s'%device)
619
- if 'ttc' in device.tracks.rawtrx:
620
- sox_TTC_chan = device.tracks.rawtrx.index('ttc')
621
- elif 'tc' in device.tracks.rawtrx:
622
- sox_TTC_chan = device.tracks.rawtrx.index('tc')
623
- else:
624
- print('Error: no tc or ttc tag in track.txt')
714
+ if device.n_chan == 2:
715
+ # tracks.txt or not,
716
+ # it's stereo, ie audio + TTC, so remove TTC and return
717
+ kept_channel = (device.ttc + 1)%2 # 1 -> 0 and 0 -> 1
718
+ logger.debug('no tracks.txt, keeping one chan %i'%kept_channel)
719
+ return _sox_keep(multichan_tmpfl, [kept_channel + 1]) #-------------
720
+ # it's multitrack (more than 2 channels)
721
+ if device.tracks is None:
722
+ # multitrack but no mix done on location, so do mono mix with all
723
+ all_channels = list(range(device.n_chan))
724
+ logger.debug('multitrack but no tracks.txt, mixing %s except TTC at %i'%
725
+ (all_channels, device.ttc))
726
+ all_channels.remove(device.ttc)
727
+ sox_kept_channels = [i + 1 for i in all_channels] # sox indexing
728
+ logger.debug('mixing channels: %s (sox #)'%sox_kept_channels)
729
+ kept_audio = _sox_keep(multichan_tmpfl, sox_kept_channels)
730
+ return _sox_mix_channels(kept_audio) #------------------------------
731
+ # user wrote a tracks.txt metadata file, check it
732
+ if device.tracks.mix == [] and device.tracks.stereomics == []:
733
+ # it's multitrac and no mix done on location, so do a mono mix with
734
+ # all, but here remove '0' and TTC tracks from mix
735
+ all_channels = list(range(1, device.n_chan + 1)) # sox not ZBIDX
736
+ to_remove = device.tracks.unused + [device.ttc+1]# unused is sox idx
737
+ logger.debug('multitrack but no tracks.txt, mixing %s except # %s (sox #)'%
738
+ (all_channels, to_remove))
739
+ sox_kept_channels = [i for i in all_channels
740
+ if i not in to_remove]
741
+ logger.debug('mixing channels: %s (sox #)'%sox_kept_channels)
742
+ kept_audio = _sox_keep(multichan_tmpfl, sox_kept_channels)
743
+ return _sox_mix_channels(kept_audio) #------------------------------
744
+ if device.tracks.mix != []:
745
+ # Mix were done on location, no and we only have to extracted it
746
+ # from the recording. If mono mix, device.tracks.mix has one element;
747
+ # if stereo mix, device.tracks.mix is a pair of number:
748
+ logger.debug('%s has mix %s'%(device.name, device.tracks.mix))
749
+ logger.debug('device %s'%device)
750
+ # just checking coherency
751
+ if 'ttc' in device.tracks.rawtrx:
752
+ trx_TTC_chan = device.tracks.rawtrx.index('ttc')
753
+ elif 'tc' in device.tracks.rawtrx:
754
+ trx_TTC_chan = device.tracks.rawtrx.index('tc')
755
+ else:
756
+ print('Error: no tc or ttc tag in track.txt')
757
+ sys.exit(1)
758
+ logger.debug('TTC chan %i, dev ttc %i'%(trx_TTC_chan, device.ttc))
759
+ if trx_TTC_chan != device.ttc:
760
+ print('Error: ttc channel # incoherency in track.txt')
761
+ sys.exit(1)
762
+ # coherency check done, extract mix track (or tracks if stereo)
763
+ mix_kind = 'mono' if len(device.tracks.mix) == 1 else 'stereo'
764
+ logger.debug('%s mix declared on channel %s (sox #)'%
765
+ (mix_kind, device.tracks.mix))
766
+ return _sox_keep(multichan_tmpfl, device.tracks.mix) #--------------
767
+ # if here, all cases have been covered except tracks.txt AND no mix AND
768
+ # stereo mic(s) so first a coherency check, and then proceed
769
+ if device.tracks.stereomics == []:
770
+ print('Error, no stereo mic?, check tracks.txt. Quitting')
625
771
  sys.exit(1)
626
- sox_TTC_chan += 1 # sox Not ZBIDX
627
- logger.debug('TTC chan %i'%sox_TTC_chan)
628
- # redo indexing since tracks.txt numbers refere to complete
629
- # files and here audio file had TTC and muted channels
630
- # removed:
631
- if len(mix_tracks) == 2: # two tracks to shift
632
- mixL_chan, mixR_chan = mix_tracks
633
- # shifting left chan if necessary
634
- shift = 0
635
- if mixL_chan > sox_TTC_chan:
636
- shift += 1
637
- for unused_tr in device.tracks.unused:
638
- if mixL_chan > unused_tr:
639
- shift += 1
640
- mixL_chan -= shift
641
- # shifting right chan if necessary
642
- shift = 0
643
- if mixR_chan > sox_TTC_chan:
644
- shift += 1
645
- for unused_tr in device.tracks.unused:
646
- if mixR_chan > unused_tr:
647
- shift += 1
648
- mixR_chan -= shift
649
- mix_tracks = [mixL_chan, mixR_chan]
650
- else: # mono, one track to shift
651
- monomix_chan = mix_tracks[0]
652
- shift = 0
653
- if monomix_chan > sox_TTC_chan:
654
- shift += 1
655
- for unused_tr in device.tracks.unused:
656
- if monomix_chan > unused_tr:
657
- shift += 1
658
- monomix_chan -= shift
659
- mix_tracks = [monomix_chan]
660
- logger.debug('new mix_tracks: %s'%mix_tracks)
661
- return _sox_keep(multichan_tmpfl, mix_tracks)
662
-
772
+ logger.debug('processing stereo pair(s) %s'%device.tracks.stereomics)
773
+ stereo_mic_idx_pairs = [pair for name, pair in device.tracks.stereomics]
774
+ logger.debug('stereo pairs idxs %s'%stereo_mic_idx_pairs)
775
+ mic_stereo_files = [_sox_keep(multichan_tmpfl, pair) for pair
776
+ in stereo_mic_idx_pairs]
777
+ # flatten list of tuples of channels being stereo mics
778
+ stereo_mic_idx_flat = [item for sublist in stereo_mic_idx_pairs
779
+ for item in sublist]
780
+ logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
781
+ mono_tracks = [i for i in range(1, device.n_chan + 1)
782
+ if i not in stereo_mic_idx_flat]
783
+ # remove TTC track number
784
+ mono_tracks.remove(device.ttc + 1)
785
+ logger.debug('mono_tracks %s'%mono_tracks)
786
+ mono_files = [_sox_keep(multichan_tmpfl, [chan]) for chan
787
+ in mono_tracks]
788
+ new_stereo_files = [_sox_mono2stereo(f) for f in mono_files]
789
+ stereo_files = mic_stereo_files + new_stereo_files
790
+ return _sox_mix_files(stereo_files)
663
791
 
664
792
  def build_audio_and_write_video(self, top_dir, output_dir,
665
793
  write_multicam_structure,
@@ -674,7 +802,7 @@ class AudioStitcherVideoMerger:
674
802
 
675
803
  asked_ISOs: bool flag specified as CLI argument
676
804
 
677
- For each audio devices found overlapping self.ref_recording: pad, trim
805
+ For each audio devices found overlapping self.videoclip: pad, trim
678
806
  or stretch audio files by calling _get_concatenated_audiofile_for(), and
679
807
  put them in merged_audio_files_by_device. More than one audio recorder
680
808
  can be used for a shot: that's why merged_audio_files_by_device is a
@@ -686,8 +814,8 @@ class AudioStitcherVideoMerger:
686
814
  """
687
815
  logger.debug(' fct args: top_dir: %s; output_dir: %s; write_multicam_structure: %s; asked_ISOs: %s;'%
688
816
  (top_dir, output_dir, write_multicam_structure, asked_ISOs))
689
- logger.debug('device for rec %s: %s'%(self.ref_recording,
690
- self.ref_recording.device))
817
+ logger.debug('device for rec %s: %s'%(self.videoclip,
818
+ self.videoclip.device))
691
819
  # suppose the user called tictacsync with 'mondayPM' as top folder to
692
820
  # scan for dailies (and 'somefolder' for output):
693
821
  if output_dir == None:
@@ -695,56 +823,81 @@ class AudioStitcherVideoMerger:
695
823
  else:
696
824
  synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
697
825
  if write_multicam_structure:
698
- device_name = self.ref_recording.device.name
826
+ device_name = self.videoclip.device.name
699
827
  synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
700
828
  self.synced_clip_dir = synced_clip_dir
701
829
  os.makedirs(synced_clip_dir, exist_ok=True)
702
830
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
703
831
  synced_clip_file = synced_clip_dir/\
704
- Path(self.ref_recording.new_rec_name).name
832
+ Path(self.videoclip.new_rec_name).name
705
833
  logger.debug('editing files for %s'%synced_clip_file)
706
- self.ref_recording.final_synced_file = synced_clip_file # relative
834
+ self.videoclip.final_synced_file = synced_clip_file # relative
707
835
  # collecting edited audio by device, in (Device, tempfile) pairs:
708
836
  merged_audio_files_by_device = [
709
837
  (d, self._get_concatenated_audiofile_for(d))
710
838
  for d in self._get_audio_devices()]
711
839
  if len(merged_audio_files_by_device) == 0:
712
840
  # no audio file overlaps for this clip
713
- return
841
+ return #############################################################
714
842
  if len(merged_audio_files_by_device) == 1:
715
843
  # only one audio recorder was used, pick singleton in list
716
844
  dev, concatenate_audio_file = merged_audio_files_by_device[0]
717
845
  logger.debug('one audio device only: %s'%dev)
718
846
  # check if this sole recorder is stereo
719
847
  if dev.n_chan == 2:
720
- # stereo minus TTC chan = mono, check consistency:
848
+ # consistency check
721
849
  nchan_sox = sox.file_info.channels(
722
850
  _pathname(concatenate_audio_file))
723
- logger.debug('nchan_sox: %i mono?'%nchan_sox)
724
- if not nchan_sox == 1:
851
+ logger.debug('Two chan only, nchan_sox: %i dev.n_chan %i'%
852
+ (nchan_sox, dev.n_chan))
853
+ if not nchan_sox == 2:
725
854
  raise Exception('Error in channel processing')
726
855
  # all OK, merge and return
727
- logger.debug('simply mono to merge')
728
- self.ref_recording.synced_audio = concatenate_audio_file
856
+ logger.debug('simply mono to merge, TTC on chan %i'%
857
+ dev.ttc)
858
+ # only 2 channels so keep the channel OTHER than TTC
859
+ if dev.ttc == 1:
860
+ # keep channel 0, but + 1 because of sox indexing
861
+ sox_kept_channel = 1
862
+ else:
863
+ # dev.ttc == 0 so keep ch 1, but + 1 (sox indexing)
864
+ sox_kept_channel = 2
865
+ self.videoclip.synced_audio = \
866
+ _sox_keep(concatenate_audio_file, [sox_kept_channel])
729
867
  self._merge_audio_and_video()
730
- return
731
- # if still here, either multitracks and/or multi recorders so check if a
732
- # mix has been done on location and identified as is in atracks.txt
733
- # file. Split audio channels in mono wav tempfiles at the same time
868
+ return #########################################################
869
+ #
870
+ # if not returned yet from fct, either multitracks and/or multi
871
+ # recorders so check if a mix has been done on location and identified
872
+ # as is in atracks.txt file. Split audio channels in mono wav tempfiles
873
+ # at the same time
734
874
  #
735
875
  multiple_recorders = len(merged_audio_files_by_device) > 1
736
876
  logger.debug('multiple_recorder: %s'%multiple_recorders)
737
- # dev_mixes_mix contains all audio recorders if many
738
- mixes = [self._get_mix(device, multi_chan_audio)
877
+ # the device_mixes list contains all audio recorders if many. If only
878
+ # one audiorecorder was used (most of the cases) len(device_mixes) is 1
879
+ device_mixes = [self._get_device_mix(device, multi_chan_audio)
739
880
  for device, multi_chan_audio
740
881
  in merged_audio_files_by_device]
741
- logger.debug('there are %i dev mixes'%len(mixes))
742
- logger.debug('mixes %s'%mixes)
743
- dev_mixes_mix = _sox_mix(mixes)
744
- logger.debug('will merge with %s'%(_pathname(dev_mixes_mix)))
745
- self.ref_recording.synced_audio = dev_mixes_mix
746
- logger.debug('dev_mixes_mix n chan: %i'%
747
- sox.file_info.channels(_pathname(dev_mixes_mix)))
882
+
883
+
884
+
885
+ # If multiple audio recorders were used and one of
886
+ # them has mixL and mixR tracks, two possibilities:
887
+
888
+ # A- others have mixL mixR too: mix of device_mixes are done (and none mix
889
+ # tracks are ignored but copied in the ISOs folder if asked)
890
+ # B- others don't have mixL-mixR: all tracks from them are panned
891
+ # 50-50 and stereo-mixed
892
+
893
+
894
+ logger.debug('there are %i dev device_mixes'%len(device_mixes))
895
+ logger.debug('device_mixes %s'%device_mixes)
896
+ mix_of_device_mixes = _sox_mix_files(device_mixes)
897
+ logger.debug('will merge with %s'%(_pathname(mix_of_device_mixes)))
898
+ self.videoclip.synced_audio = mix_of_device_mixes
899
+ logger.debug('mix_of_device_mixes n chan: %i'%
900
+ sox.file_info.channels(_pathname(mix_of_device_mixes)))
748
901
  self._merge_audio_and_video()
749
902
  # devices_and_monofiles is list of (device, [monofiles])
750
903
  # [(dev1, multichan1), (dev2, multichan2)] in
@@ -763,12 +916,13 @@ class AudioStitcherVideoMerger:
763
916
  if dev.tracks == None:
764
917
  tag = 'chan%s'%str(idx+1).zfill(2)
765
918
  else:
766
- audio_tags = [tag for tag in dev.tracks.rawtrx
767
- if tag not in ['ttc','0','tc']]
768
- tag = audio_tags[idx]
919
+ # audio_tags = [tag for tag in dev.tracks.rawtrx
920
+ # if tag not in ['ttc','0','tc']]
921
+ tag = dev.tracks.rawtrx[idx]
769
922
  if multiple_recorders:
770
923
  tag += '_' + dev.name
771
- return tag
924
+ logger.debug('tag %s'%tag)
925
+ return tag #####################################################
772
926
  # replace device, idx pair with track name (+ device name if many)
773
927
  # loop over devices than loop over tracks
774
928
  names_audio_tempfiles = []
@@ -804,25 +958,24 @@ class AudioStitcherVideoMerger:
804
958
  out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
805
959
  ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
806
960
  return file_handle
807
- # os.path.split audio channels if more than one
808
961
 
809
962
  def _merge_audio_and_video(self):
810
963
  """
811
- Calls ffmpeg to join video in self.ref_recording.AVpath to
812
- audio in self.ref_recording.synced_audio
964
+ Calls ffmpeg to join video in self.videoclip.AVpath to
965
+ audio in self.videoclip.synced_audio
813
966
 
814
- On entry, ref_recording.final_synced_file is a Path to an non existing
815
- file (contrarily to ref_recording.synced_audio).
816
- On exit, self.ref_recording.final_synced_file points to the final synced
967
+ On entry, videoclip.final_synced_file is a Path to an non existing
968
+ file (contrarily to videoclip.synced_audio).
969
+ On exit, self.videoclip.final_synced_file points to the final synced
817
970
  video file.
818
971
 
819
972
  Returns nothing.
820
973
  """
821
- synced_clip_file = self.ref_recording.final_synced_file
822
- video_path = self.ref_recording.AVpath
823
- timecode = self.ref_recording.get_timecode()
824
- # self.ref_recording.synced_audio = audio_path
825
- audio_path = self.ref_recording.synced_audio
974
+ synced_clip_file = self.videoclip.final_synced_file
975
+ video_path = self.videoclip.AVpath
976
+ timecode = self.videoclip.get_timecode()
977
+ # self.videoclip.synced_audio = audio_path
978
+ audio_path = self.videoclip.synced_audio
826
979
  vid_only_handle = self._keep_VIDEO_only(video_path)
827
980
  a_n = _pathname(audio_path)
828
981
  v_n = str(vid_only_handle.name)
@@ -876,20 +1029,10 @@ class Matcher:
876
1029
  AudioStitcherVideoMerger objects that do the actual file manipulations. Each video
877
1030
  (and main sound) will have its AudioStitcherVideoMerger instance.
878
1031
 
879
- All videos are de facto reference recording and matching audio files are
880
- looked up for each one of them.
881
-
882
1032
  The Matcher doesn't keep neither set any editing information in itself: the
883
1033
  in and out time values (UTC times) used are those kept inside each Recording
884
1034
  instances.
885
1035
 
886
- [NOT YET IMPLEMENTED]: When shooting is done with multiple audio recorders,
887
- ONE audio device can be designated as 'main sound' and used as reference
888
- recording; then all audio tracks are synced together against this main
889
- sound audio file, keeping the TicTacCode track alongside for syncing against
890
- their video counterpart(in a second pass and after a mixdown editing).
891
- [/NOT YET IMPLEMENTED]
892
-
893
1036
  Attributes:
894
1037
 
895
1038
  recordings : list of Recording instances
@@ -897,7 +1040,7 @@ class Matcher:
897
1040
 
898
1041
  video_mergers : list
899
1042
  of AudioStitcherVideoMerger Class instances, built by
900
- scan_audio_for_each_ref_rec(); each video has a corresponding
1043
+ scan_audio_for_each_videoclip(); each video has a corresponding
901
1044
  AudioStitcherVideoMerger object. An audio_stitch doesn't extend
902
1045
  beyond the corresponding video start and end times.
903
1046
 
@@ -929,7 +1072,7 @@ class Matcher:
929
1072
  _pathname(rec.AVpath),
930
1073
  _pathname(rec.new_rec_name)))
931
1074
 
932
- def scan_audio_for_each_ref_rec(self):
1075
+ def scan_audio_for_each_videoclip(self):
933
1076
  """
934
1077
  For each video (and for the Main Sound) in self.recordings, this finds
935
1078
  any audio that has overlapping times and instantiates a
@@ -940,21 +1083,21 @@ class Matcher:
940
1083
  V3 checked against ...
941
1084
  Main Sound checked against A1, A2, A3, A4
942
1085
  """
943
- refeference_recordings = [r for r in self.recordings if r.is_video()
1086
+ video_recordings = [r for r in self.recordings if r.is_video()
944
1087
  or r.is_reference]
945
1088
  audio_recs = [r for r in self.recordings if r.is_audio()
946
1089
  and not r.is_reference]
947
1090
  if not audio_recs:
948
1091
  print('\nNo audio recording found, syncing of videos only not implemented yet, exiting...\n')
949
1092
  sys.exit(1)
950
- for ref_rec in refeference_recordings:
951
- reference_tag = 'video' if ref_rec.is_video() else 'audio'
1093
+ for videoclip in video_recordings:
1094
+ reference_tag = 'video' if videoclip.is_video() else 'audio'
952
1095
  logger.debug('Looking for overlaps with %s %s'%(
953
1096
  reference_tag,
954
- ref_rec))
955
- audio_stitch = AudioStitcherVideoMerger(ref_rec)
1097
+ videoclip))
1098
+ audio_stitch = AudioStitcherVideoMerger(videoclip)
956
1099
  for audio in audio_recs:
957
- if self._does_overlap(ref_rec, audio):
1100
+ if self._does_overlap(videoclip, audio):
958
1101
  audio_stitch.add_matched_audio(audio)
959
1102
  logger.debug('recording %s overlaps,'%(audio))
960
1103
  # print(' recording [gold1]%s[/gold1] overlaps,'%(audio))
@@ -962,13 +1105,13 @@ class Matcher:
962
1105
  self.video_mergers.append(audio_stitch)
963
1106
  else:
964
1107
  logger.debug('\n nothing\n')
965
- print('No overlap found for %s'%ref_rec.AVpath.name)
1108
+ print('No overlap found for %s'%videoclip.AVpath.name)
966
1109
  del audio_stitch
967
1110
  logger.debug('%i video_mergers created'%len(self.video_mergers))
968
1111
 
969
- def _does_overlap(self, ref_rec, audio_rec):
1112
+ def _does_overlap(self, videoclip, audio_rec):
970
1113
  A1, A2 = audio_rec.get_start_time(), audio_rec.get_end_time()
971
- R1, R2 = ref_rec.get_start_time(), ref_rec.get_end_time()
1114
+ R1, R2 = videoclip.get_start_time(), videoclip.get_end_time()
972
1115
  no_overlap = (A2 < R1) or (A1 > R2)
973
1116
  return not no_overlap
974
1117
 
@@ -990,11 +1133,11 @@ class Matcher:
990
1133
  Returns nothing, changes are done in the video files metadata
991
1134
  (each referenced by Recording.final_synced_file)
992
1135
  """
993
- vids = [m.ref_recording for m in self.video_mergers]
1136
+ vids = [m.videoclip for m in self.video_mergers]
994
1137
  logger.debug('vids %s'%vids)
995
1138
  if len(vids) == 1:
996
1139
  logger.debug('just one take, no gap to shrink')
997
- return
1140
+ return #############################################################
998
1141
  # INs_and_OUTs contains (time, direction, video) for each video,
999
1142
  # where direction is 'in|out' and video an instance of Recording
1000
1143
  INs_and_OUTs = [(vid.get_start_time(), 'in', vid) for vid in vids]