tictacsync 0.3a3__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,30 +62,24 @@ 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
- Building dict according to pysox.remix format.
70
- https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
71
- eg: 4 channels with TicTacCode_channel at #2
72
- returns {1: [1], 2: [3], 3: [4]}
73
- ie the number of channels drops by one and chan 2 is missing
74
- excluded_channels is a list of Zero Based indexing chan numbers
75
-
69
+ Channels numbers in kept_channels are not ZBIDXed as per SOX format
76
70
  """
77
71
  audio_file = _pathname(audio_file)
78
72
  nchan = sox.file_info.channels(audio_file)
79
73
  logger.debug('in file of %i chan, have to keep %s'%
80
74
  (nchan, kept_channels))
81
75
  all_channels = range(1, nchan + 1) # from 1 to nchan included
82
- # list of list for pysox API
83
- # eg [[1], [3], [4]]
76
+ # Building dict according to pysox.remix format.
77
+ # https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
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
- # {1: [1], 2: [3], 3: [4]} -> from 4 to 3 chan and chan 2 is dropped
87
- output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
88
- out_file = _pathname(output_fh)
81
+ output_tempfile = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
82
+ out_file = _pathname(output_tempfile)
89
83
  logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
90
84
  # sox_transform.set_output_format(channels=1)
91
85
  sox_transform = sox.Transformer()
@@ -104,8 +98,7 @@ def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
104
98
  stdout, stderr = p.communicate()
105
99
  logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
106
100
  stderr).decode('utf-8'))
107
- return output_fh
108
-
101
+ return output_tempfile
109
102
 
110
103
  def _split_channels(multi_chan_audio:Path) -> list:
111
104
  nchan = sox.file_info.channels(_pathname(multi_chan_audio))
@@ -133,7 +126,7 @@ def _sox_combine(paths) -> Path:
133
126
  """
134
127
  if len(paths) == 1: # one device only, nothing to stack
135
128
  logger.debug('one device only, nothing to stack')
136
- return paths[0]
129
+ return paths[0] ########################################################
137
130
  out_file_handle = tempfile.NamedTemporaryFile(suffix='.wav',
138
131
  delete=DEL_TEMP)
139
132
  filenames = [_pathname(p) for p in paths]
@@ -159,9 +152,96 @@ def _sox_combine(paths) -> Path:
159
152
  (merged_duration, nchan))
160
153
  return out_file_handle
161
154
 
162
- def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
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
+ """
173
+ n_chan_input = sox.file_info.channels(_pathname(multichan_tmpfl))
174
+ logger.debug('n chan input: %s'%n_chan_input)
175
+ if n_chan_input == 1: # nothing to mix down
176
+ return multichan_tmpfl #################################################
177
+ stereo_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
178
+ delete=DEL_TEMP)
179
+ tfm = sox.Transformer()
180
+ tfm.channels(1)
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:
163
191
  """
164
- mix files referred by the list of Path into a new temporary files passed on return
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))
231
+ logger.debug('n chan ouput: %s'%
232
+ sox.file_info.channels(_pathname(stereo_tempfile)))
233
+ logger.debug('sox.build status for _sox_mono2stereo(): %s'%status)
234
+ if status != True:
235
+ print('Error, sox did not normalize file in _sox_mono2stereo()')
236
+ sys.exit(1)
237
+ return stereo_tempfile
238
+
239
+
240
+ def _sox_mix_files(temp_files_to_mix:list) -> tempfile.NamedTemporaryFile:
241
+ """
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.
165
245
  """
166
246
  def _sox_norm(tempf):
167
247
  normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
@@ -171,26 +251,42 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
171
251
  status = tfm.build(_pathname(tempf),_pathname(normed_tempfile))
172
252
  logger.debug('sox.build status for norm(): %s'%status)
173
253
  if status != True:
174
- print('Error, sox did not normalize file in _sox_mix()')
254
+ print('Error, sox did not normalize file in _sox_mix_files()')
175
255
  sys.exit(1)
176
256
  return normed_tempfile
177
- paths = [_sox_norm(p) for p in paths]
178
- cbn = sox.Combiner()
179
- N = len(paths)
257
+ N = len(temp_files_to_mix)
180
258
  if N == 1: # nothing to mix
181
259
  logger.debug('one file: nothing to mix')
182
- return paths[0]
260
+ return temp_files_to_mix[0] ########################################################
261
+ cbn = sox.Combiner()
183
262
  cbn.set_input_format(file_type=['wav']*N)
184
- filenames = [_pathname(p) for p in paths]
185
- logger.debug('%i files to mix %s'%(N, 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]
186
282
  mixed_tempf = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
187
- status = cbn.build(filenames,
283
+ status = cbn.build(files_to_mix,
188
284
  _pathname(mixed_tempf),
189
285
  combine_type='mix',
190
286
  input_volumes=[1/N]*N)
191
287
  logger.debug('sox.build status for mix: %s'%status)
192
288
  if status != True:
193
- print('Error, sox did not mix files in _sox_mix()')
289
+ print('Error, sox did not mix files in _sox_mix_files()')
194
290
  sys.exit(1)
195
291
  normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
196
292
  tfm = sox.Transformer()
@@ -198,16 +294,15 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
198
294
  status = tfm.build(_pathname(mixed_tempf),_pathname(normed_tempfile))
199
295
  logger.debug('sox.build status for norm(): %s'%status)
200
296
  if status != True:
201
- print('Error, sox did not normalize file in _sox_mix()')
297
+ print('Error, sox did not normalize file in _sox_mix_files()')
202
298
  sys.exit(1)
203
299
  return normed_tempfile
204
300
 
205
-
206
301
  class AudioStitcherVideoMerger:
207
302
  """
208
303
  Typically each found video is associated with an AudioStitcherVideoMerger
209
304
  instance. AudioStitcherVideoMerger does the actual audio-video file
210
- processing of merging self.ref_recording (gen. a video) with all audio
305
+ processing of merging self.videoclip (gen. a video) with all audio
211
306
  files in self.edited_audio as determined by the Matcher
212
307
  object (it instanciates and manages AudioStitcherVideoMerger objects).
213
308
 
@@ -216,15 +311,15 @@ class AudioStitcherVideoMerger:
216
311
  devices to match the precise clock value of the ref recording (to a few
217
312
  ppm), using sox tempo transform.
218
313
 
219
- 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
220
315
  video start and end times: it is not a audio montage for the whole movie
221
316
  project.
222
317
 
223
318
 
224
319
  Attributes:
225
320
 
226
- ref_recording : a Recording instance
227
- 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
228
323
 
229
324
  edited_audio : dict as {Recording : path}
230
325
  keys are elements of matched_audio_recordings of class Recording
@@ -238,19 +333,19 @@ class AudioStitcherVideoMerger:
238
333
 
239
334
  """
240
335
 
241
- def __init__(self, reference_recording):
242
- self.ref_recording = reference_recording
336
+ def __init__(self, video_clip):
337
+ self.videoclip = video_clip
243
338
  # self.matched_audio_recordings = []
244
339
  self.edited_audio = {}
245
340
  logger.debug('instantiating AudioStitcherVideoMerger for %s'%
246
- reference_recording)
341
+ video_clip)
247
342
 
248
343
  def add_matched_audio(self, audio_rec):
249
344
  """
250
345
  Populates self.edited_audio, a dict as {Recording : path}
251
346
 
252
347
  AudioStitcherVideoMerger.add_matched_audio() is called
253
- within Matcher.scan_audio_for_each_ref_rec()
348
+ within Matcher.scan_audio_for_each_videoclip()
254
349
 
255
350
  Returns nothing, fills self.edited_audio dict with
256
351
  matched audio.
@@ -269,7 +364,7 @@ class AudioStitcherVideoMerger:
269
364
 
270
365
  def get_matched_audio_recs(self):
271
366
  """
272
- Returns audio recordings that overlap self.ref_recording.
367
+ Returns audio recordings that overlap self.videoclip.
273
368
  Simply keys of self.edited_audio dict
274
369
  """
275
370
  return list(self.edited_audio.keys())
@@ -294,14 +389,14 @@ class AudioStitcherVideoMerger:
294
389
  sox_transform = sox.Transformer()
295
390
  # tempo_scale_factor = rec.device_relative_speed
296
391
  tempo_scale_factor = rec.device_relative_speed
297
- reC_dev = rec.device.name
298
- reF_dev = self.ref_recording.device.name
392
+ audio_dev = rec.device.name
393
+ video_dev = self.videoclip.device.name
299
394
  if tempo_scale_factor > 1:
300
395
  print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor\n'%
301
- (reC_dev, reF_dev, tempo_scale_factor))
396
+ (audio_dev, video_dev, tempo_scale_factor))
302
397
  else:
303
398
  print('[gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is too short by a %f factor\n'%
304
- (reC_dev, reF_dev, tempo_scale_factor))
399
+ (audio_dev, video_dev, tempo_scale_factor))
305
400
  sox_transform.tempo(tempo_scale_factor)
306
401
  # scaled_file = self._get_soxed_file(rec, sox_transform)
307
402
  logger.debug('sox_transform %s'%sox_transform.effects)
@@ -326,11 +421,11 @@ class AudioStitcherVideoMerger:
326
421
  # ones. List the files and warn the user there is a risk of error if
327
422
  # they're not from the same device.
328
423
 
329
- logger.debug('%i audio files for reference rec %s:'%(len(recordings),
330
- self.ref_recording))
424
+ logger.debug('%i audio files for videoclip %s:'%(len(recordings),
425
+ self.videoclip))
331
426
  for r in recordings:
332
427
  logger.debug(' %s'%r)
333
- speeds = numpy.array([rec.get_speed_ratio(self.ref_recording)
428
+ speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
334
429
  for rec in recordings])
335
430
  mean_speed = numpy.mean(speeds)
336
431
  for r in recordings:
@@ -338,9 +433,9 @@ class AudioStitcherVideoMerger:
338
433
  # r.device_relative_speed = 0.9
339
434
  logger.debug('set device_relative_speed for %s'%r)
340
435
  logger.debug(' value: %f'%r.device_relative_speed)
341
- r.set_time_position_to(self.ref_recording)
436
+ r.set_time_position_to(self.videoclip)
342
437
  logger.debug('time_position for %s: %fs relative to %s'%(r,
343
- r.time_position, self.ref_recording))
438
+ r.time_position, self.videoclip))
344
439
  # st_dev_speeds just to check for anomalous situation
345
440
  st_dev_speeds = numpy.std(speeds)
346
441
  logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
@@ -406,7 +501,7 @@ class AudioStitcherVideoMerger:
406
501
  end_time = sox.file_info.duration(growing_file.name)
407
502
  logger.debug('total edited audio duration %.2f s'%end_time)
408
503
  logger.debug('video duration %.2f s'%
409
- self.ref_recording.get_duration())
504
+ self.videoclip.get_duration())
410
505
  return growing_file
411
506
 
412
507
  def _pad_or_trim_first_audio(self, first_rec):
@@ -415,17 +510,17 @@ class AudioStitcherVideoMerger:
415
510
  NO: will change tempo after trimming/padding
416
511
 
417
512
  Store (into Recording.edited_audio dict) the handle of the sox processed
418
- first recording, padded or chopped according to AudioStitcherVideoMerger.ref_recording
513
+ first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
419
514
  starting time. Length of the written file can differ from length of the
420
515
  submitted Recording object if drift is corrected with sox tempo
421
516
  transform, so check it with sox.file_info.duration()
422
517
  """
423
518
  logger.debug(' editing %s'%first_rec)
424
519
  audio_start = first_rec.get_start_time()
425
- ref_start = self.ref_recording.get_start_time()
426
- if ref_start < audio_start: # padding
520
+ video_start = self.videoclip.get_start_time()
521
+ if video_start < audio_start: # padding
427
522
  logger.debug('padding')
428
- pad_duration = (audio_start-ref_start).total_seconds()
523
+ pad_duration = (audio_start-video_start).total_seconds()
429
524
  """padding first_file:
430
525
  ┏━━━━━━━━━━━━━━━┓
431
526
  ┗━━━━━━━━━━━━━━━┛ref
@@ -435,7 +530,7 @@ class AudioStitcherVideoMerger:
435
530
  self._pad_file(first_rec, pad_duration)
436
531
  else:
437
532
  logger.debug('trimming')
438
- length = (ref_start-audio_start).total_seconds()
533
+ length = (video_start-audio_start).total_seconds()
439
534
  """chopping first_file:
440
535
  ┏━━━━━━━━━━━━━━━┓
441
536
  ┗━━━━━━━━━━━━━━━┛ref
@@ -484,7 +579,7 @@ class AudioStitcherVideoMerger:
484
579
  logger.debug('transform: %s'%sox_transform.effects)
485
580
  recording_fh = self.edited_audio[audio_rec]
486
581
  logger.debug('for recording %s, matching %s'%(audio_rec,
487
- self.ref_recording))
582
+ self.videoclip))
488
583
  input_file = _pathname(recording_fh)
489
584
  logger.debug('AudioStitcherVideoMerger.edited_audio[audio_rec]: %s'%
490
585
  input_file)
@@ -530,7 +625,7 @@ class AudioStitcherVideoMerger:
530
625
  """
531
626
  sox_transform = sox.Transformer()
532
627
  audio_length = sox.file_info.duration(_pathname(audio_tempfile))
533
- video_length = self.ref_recording.get_duration()
628
+ video_length = self.videoclip.get_duration()
534
629
  if audio_length > video_length:
535
630
  # trim audio
536
631
  sox_transform.trim(0, video_length)
@@ -549,9 +644,9 @@ class AudioStitcherVideoMerger:
549
644
  logger.debug('audio duration %.2f s'%
550
645
  sox.file_info.duration(_pathname(out_tf)))
551
646
  logger.debug('video duration %.2f s'%
552
- self.ref_recording.get_duration())
647
+ self.videoclip.get_duration())
553
648
  return out_tf
554
- synced_clip_file = self.ref_recording.final_synced_file
649
+ synced_clip_file = self.videoclip.final_synced_file
555
650
  synced_clip_dir = synced_clip_file.parent
556
651
  # build ISOs subfolders structure, see comment string below
557
652
  video_stem_WO_suffix = synced_clip_file.stem
@@ -570,48 +665,129 @@ class AudioStitcherVideoMerger:
570
665
  mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
571
666
  shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
572
667
  logger.debug('destination:%s'%destination)
573
- # # mixNnormed = _sox_mix(tempfiles)
668
+ # # mixNnormed = _sox_mix_files(tempfiles)
574
669
  # # print('516', _pathname(mixNnormed))
575
670
  # os.remove(ISO_multi_chan)
576
671
 
577
- def _get_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
578
- """
579
- If device has an associated Tracks description that declares a (mono or
580
- stereo) mix track, returns a tmpfl containing the corresponding
581
- 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
710
+
582
711
 
583
712
  """
584
- if device.tracks is None:
585
- logger.debug('no tracks.txt, mixing all')
586
- return _sox_mix(_split_channels(multichan_tmpfl))
587
- mix_tracks = device.tracks.mix
588
- if mix_tracks == []:
589
- logger.debug('tracks.txt present but no mix trx, mixing all')
590
- return _sox_mix(_split_channels(multichan_tmpfl))
591
- logger.debug('%s has mix %s'%(device.name, mix_tracks))
592
713
  logger.debug('device %s'%device)
593
- if 'ttc' in device.tracks.rawtrx:
594
- sox_TTC_chan = device.tracks.rawtrx.index('ttc')
595
- elif 'tc' in device.tracks.rawtrx:
596
- sox_TTC_chan = device.tracks.rawtrx.index('tc')
597
- else:
598
- 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')
599
771
  sys.exit(1)
600
- sox_TTC_chan += 1 # sox NZBIDX
601
- logger.debug('TTC chan %i'%sox_TTC_chan)
602
- # redo indexing since tracks.txt numbers refere to complete
603
- # files and here audio file had TTC and muted channels
604
- # removed.
605
- shift = 0
606
- if mix_tracks[0] > sox_TTC_chan:
607
- shift += 1
608
- for unused_tr in device.tracks.unused:
609
- if mix_tracks[0] > unused_tr:
610
- shift += 1
611
- mix_tracks = [t-shift for t in mix_tracks]
612
- logger.debug('new mix_tracks: %s'%mix_tracks)
613
- return _sox_keep(multichan_tmpfl, mix_tracks)
614
-
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)
615
791
 
616
792
  def build_audio_and_write_video(self, top_dir, output_dir,
617
793
  write_multicam_structure,
@@ -626,7 +802,7 @@ class AudioStitcherVideoMerger:
626
802
 
627
803
  asked_ISOs: bool flag specified as CLI argument
628
804
 
629
- For each audio devices found overlapping self.ref_recording: pad, trim
805
+ For each audio devices found overlapping self.videoclip: pad, trim
630
806
  or stretch audio files by calling _get_concatenated_audiofile_for(), and
631
807
  put them in merged_audio_files_by_device. More than one audio recorder
632
808
  can be used for a shot: that's why merged_audio_files_by_device is a
@@ -638,8 +814,8 @@ class AudioStitcherVideoMerger:
638
814
  """
639
815
  logger.debug(' fct args: top_dir: %s; output_dir: %s; write_multicam_structure: %s; asked_ISOs: %s;'%
640
816
  (top_dir, output_dir, write_multicam_structure, asked_ISOs))
641
- logger.debug('device for rec %s: %s'%(self.ref_recording,
642
- self.ref_recording.device))
817
+ logger.debug('device for rec %s: %s'%(self.videoclip,
818
+ self.videoclip.device))
643
819
  # suppose the user called tictacsync with 'mondayPM' as top folder to
644
820
  # scan for dailies (and 'somefolder' for output):
645
821
  if output_dir == None:
@@ -647,58 +823,81 @@ class AudioStitcherVideoMerger:
647
823
  else:
648
824
  synced_clip_dir = Path(output_dir)/Path(top_dir).name # = somefolder/mondayPM
649
825
  if write_multicam_structure:
650
- device_name = self.ref_recording.device.name
826
+ device_name = self.videoclip.device.name
651
827
  synced_clip_dir = synced_clip_dir/device_name # = synced_clip_dir/ZOOM
652
828
  self.synced_clip_dir = synced_clip_dir
653
829
  os.makedirs(synced_clip_dir, exist_ok=True)
654
830
  logger.debug('synced_clip_dir is: %s'%synced_clip_dir)
655
831
  synced_clip_file = synced_clip_dir/\
656
- Path(self.ref_recording.new_rec_name).name
832
+ Path(self.videoclip.new_rec_name).name
657
833
  logger.debug('editing files for %s'%synced_clip_file)
658
- self.ref_recording.final_synced_file = synced_clip_file # relative
834
+ self.videoclip.final_synced_file = synced_clip_file # relative
659
835
  # collecting edited audio by device, in (Device, tempfile) pairs:
660
836
  merged_audio_files_by_device = [
661
837
  (d, self._get_concatenated_audiofile_for(d))
662
838
  for d in self._get_audio_devices()]
663
839
  if len(merged_audio_files_by_device) == 0:
664
840
  # no audio file overlaps for this clip
665
- return
841
+ return #############################################################
666
842
  if len(merged_audio_files_by_device) == 1:
667
843
  # only one audio recorder was used, pick singleton in list
668
844
  dev, concatenate_audio_file = merged_audio_files_by_device[0]
669
845
  logger.debug('one audio device only: %s'%dev)
670
846
  # check if this sole recorder is stereo
671
847
  if dev.n_chan == 2:
672
- # stereo minus TTC chan = mono, check consistency:
848
+ # consistency check
673
849
  nchan_sox = sox.file_info.channels(
674
850
  _pathname(concatenate_audio_file))
675
- logger.debug('nchan_sox: %i mono?'%nchan_sox)
676
- 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:
677
854
  raise Exception('Error in channel processing')
678
855
  # all OK, merge and return
679
- logger.debug('simply mono to merge')
680
- 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])
681
867
  self._merge_audio_and_video()
682
- return
683
- # if still here, either multitracks and/or multi recorders so check if a
684
- # mix has been done on location and identified as is in atracks.txt
685
- # 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
686
874
  #
687
875
  multiple_recorders = len(merged_audio_files_by_device) > 1
688
876
  logger.debug('multiple_recorder: %s'%multiple_recorders)
689
- # dev_mixes_mix contains all audio recorders if many
690
- 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)
691
880
  for device, multi_chan_audio
692
881
  in merged_audio_files_by_device]
693
- logger.debug('there are %i dev mixes'%len(mixes))
694
- logger.debug('mixes %s'%mixes)
695
- dev_mixes_mix = _sox_mix(mixes)
696
- # dev_mixes_mix = _sox_combine([audio for _, audio
697
- # in merged_audio_files_by_device]) # all devices
698
- logger.debug('will merge with %s'%(_pathname(dev_mixes_mix)))
699
- self.ref_recording.synced_audio = dev_mixes_mix
700
- logger.debug('dev_mixes_mix n chan: %i'%
701
- 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)))
702
901
  self._merge_audio_and_video()
703
902
  # devices_and_monofiles is list of (device, [monofiles])
704
903
  # [(dev1, multichan1), (dev2, multichan2)] in
@@ -717,12 +916,13 @@ class AudioStitcherVideoMerger:
717
916
  if dev.tracks == None:
718
917
  tag = 'chan%s'%str(idx+1).zfill(2)
719
918
  else:
720
- audio_tags = [tag for tag in dev.tracks.rawtrx
721
- if tag not in ['ttc','0','tc']]
722
- 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]
723
922
  if multiple_recorders:
724
923
  tag += '_' + dev.name
725
- return tag
924
+ logger.debug('tag %s'%tag)
925
+ return tag #####################################################
726
926
  # replace device, idx pair with track name (+ device name if many)
727
927
  # loop over devices than loop over tracks
728
928
  names_audio_tempfiles = []
@@ -758,25 +958,24 @@ class AudioStitcherVideoMerger:
758
958
  out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
759
959
  ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
760
960
  return file_handle
761
- # os.path.split audio channels if more than one
762
961
 
763
962
  def _merge_audio_and_video(self):
764
963
  """
765
- Calls ffmpeg to join video in self.ref_recording.AVpath to
766
- 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
767
966
 
768
- On entry, ref_recording.final_synced_file is a Path to an non existing
769
- file (contrarily to ref_recording.synced_audio).
770
- 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
771
970
  video file.
772
971
 
773
972
  Returns nothing.
774
973
  """
775
- synced_clip_file = self.ref_recording.final_synced_file
776
- video_path = self.ref_recording.AVpath
777
- timecode = self.ref_recording.get_timecode()
778
- # self.ref_recording.synced_audio = audio_path
779
- 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
780
979
  vid_only_handle = self._keep_VIDEO_only(video_path)
781
980
  a_n = _pathname(audio_path)
782
981
  v_n = str(vid_only_handle.name)
@@ -830,20 +1029,10 @@ class Matcher:
830
1029
  AudioStitcherVideoMerger objects that do the actual file manipulations. Each video
831
1030
  (and main sound) will have its AudioStitcherVideoMerger instance.
832
1031
 
833
- All videos are de facto reference recording and matching audio files are
834
- looked up for each one of them.
835
-
836
1032
  The Matcher doesn't keep neither set any editing information in itself: the
837
1033
  in and out time values (UTC times) used are those kept inside each Recording
838
1034
  instances.
839
1035
 
840
- [NOT YET IMPLEMENTED]: When shooting is done with multiple audio recorders,
841
- ONE audio device can be designated as 'main sound' and used as reference
842
- recording; then all audio tracks are synced together against this main
843
- sound audio file, keeping the TicTacCode track alongside for syncing against
844
- their video counterpart(in a second pass and after a mixdown editing).
845
- [/NOT YET IMPLEMENTED]
846
-
847
1036
  Attributes:
848
1037
 
849
1038
  recordings : list of Recording instances
@@ -851,7 +1040,7 @@ class Matcher:
851
1040
 
852
1041
  video_mergers : list
853
1042
  of AudioStitcherVideoMerger Class instances, built by
854
- scan_audio_for_each_ref_rec(); each video has a corresponding
1043
+ scan_audio_for_each_videoclip(); each video has a corresponding
855
1044
  AudioStitcherVideoMerger object. An audio_stitch doesn't extend
856
1045
  beyond the corresponding video start and end times.
857
1046
 
@@ -883,7 +1072,7 @@ class Matcher:
883
1072
  _pathname(rec.AVpath),
884
1073
  _pathname(rec.new_rec_name)))
885
1074
 
886
- def scan_audio_for_each_ref_rec(self):
1075
+ def scan_audio_for_each_videoclip(self):
887
1076
  """
888
1077
  For each video (and for the Main Sound) in self.recordings, this finds
889
1078
  any audio that has overlapping times and instantiates a
@@ -894,21 +1083,21 @@ class Matcher:
894
1083
  V3 checked against ...
895
1084
  Main Sound checked against A1, A2, A3, A4
896
1085
  """
897
- 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()
898
1087
  or r.is_reference]
899
1088
  audio_recs = [r for r in self.recordings if r.is_audio()
900
1089
  and not r.is_reference]
901
1090
  if not audio_recs:
902
1091
  print('\nNo audio recording found, syncing of videos only not implemented yet, exiting...\n')
903
1092
  sys.exit(1)
904
- for ref_rec in refeference_recordings:
905
- 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'
906
1095
  logger.debug('Looking for overlaps with %s %s'%(
907
1096
  reference_tag,
908
- ref_rec))
909
- audio_stitch = AudioStitcherVideoMerger(ref_rec)
1097
+ videoclip))
1098
+ audio_stitch = AudioStitcherVideoMerger(videoclip)
910
1099
  for audio in audio_recs:
911
- if self._does_overlap(ref_rec, audio):
1100
+ if self._does_overlap(videoclip, audio):
912
1101
  audio_stitch.add_matched_audio(audio)
913
1102
  logger.debug('recording %s overlaps,'%(audio))
914
1103
  # print(' recording [gold1]%s[/gold1] overlaps,'%(audio))
@@ -916,13 +1105,13 @@ class Matcher:
916
1105
  self.video_mergers.append(audio_stitch)
917
1106
  else:
918
1107
  logger.debug('\n nothing\n')
919
- print('No overlap found for %s'%ref_rec.AVpath.name)
1108
+ print('No overlap found for %s'%videoclip.AVpath.name)
920
1109
  del audio_stitch
921
1110
  logger.debug('%i video_mergers created'%len(self.video_mergers))
922
1111
 
923
- def _does_overlap(self, ref_rec, audio_rec):
1112
+ def _does_overlap(self, videoclip, audio_rec):
924
1113
  A1, A2 = audio_rec.get_start_time(), audio_rec.get_end_time()
925
- R1, R2 = ref_rec.get_start_time(), ref_rec.get_end_time()
1114
+ R1, R2 = videoclip.get_start_time(), videoclip.get_end_time()
926
1115
  no_overlap = (A2 < R1) or (A1 > R2)
927
1116
  return not no_overlap
928
1117
 
@@ -944,11 +1133,11 @@ class Matcher:
944
1133
  Returns nothing, changes are done in the video files metadata
945
1134
  (each referenced by Recording.final_synced_file)
946
1135
  """
947
- vids = [m.ref_recording for m in self.video_mergers]
1136
+ vids = [m.videoclip for m in self.video_mergers]
948
1137
  logger.debug('vids %s'%vids)
949
1138
  if len(vids) == 1:
950
1139
  logger.debug('just one take, no gap to shrink')
951
- return
1140
+ return #############################################################
952
1141
  # INs_and_OUTs contains (time, direction, video) for each video,
953
1142
  # where direction is 'in|out' and video an instance of Recording
954
1143
  INs_and_OUTs = [(vid.get_start_time(), 'in', vid) for vid in vids]