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/device_scanner.py +8 -17
- tictacsync/entry.py +8 -8
- tictacsync/timeline.py +335 -192
- tictacsync/yaltc.py +179 -171
- {tictacsync-0.3a4.dist-info → tictacsync-0.4a0.dist-info}/METADATA +1 -1
- tictacsync-0.4a0.dist-info/RECORD +15 -0
- tictacsync-0.3a4.dist-info/RECORD +0 -15
- {tictacsync-0.3a4.dist-info → tictacsync-0.4a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.4a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.4a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.3a4.dist-info → tictacsync-0.4a0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
|
|
87
|
-
out_file = _pathname(
|
|
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
|
|
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
|
|
162
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
174
|
-
logger.debug('sox.build status for
|
|
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
|
|
235
|
+
print('Error, sox did not normalize file in _sox_mono2stereo()')
|
|
177
236
|
sys.exit(1)
|
|
178
|
-
return
|
|
237
|
+
return stereo_tempfile
|
|
179
238
|
|
|
180
239
|
|
|
181
|
-
def
|
|
240
|
+
def _sox_mix_files(temp_files_to_mix:list) -> tempfile.NamedTemporaryFile:
|
|
182
241
|
"""
|
|
183
|
-
|
|
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
|
|
254
|
+
print('Error, sox did not normalize file in _sox_mix_files()')
|
|
194
255
|
sys.exit(1)
|
|
195
256
|
return normed_tempfile
|
|
196
|
-
|
|
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
|
|
260
|
+
return temp_files_to_mix[0] ########################################################
|
|
261
|
+
cbn = sox.Combiner()
|
|
202
262
|
cbn.set_input_format(file_type=['wav']*N)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
248
|
-
The video
|
|
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,
|
|
263
|
-
self.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
|
351
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
447
|
-
if
|
|
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-
|
|
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 = (
|
|
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.
|
|
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.
|
|
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.
|
|
647
|
+
self.videoclip.get_duration())
|
|
574
648
|
return out_tf
|
|
575
|
-
synced_clip_file = self.
|
|
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 =
|
|
668
|
+
# # mixNnormed = _sox_mix_files(tempfiles)
|
|
595
669
|
# # print('516', _pathname(mixNnormed))
|
|
596
670
|
# os.remove(ISO_multi_chan)
|
|
597
671
|
|
|
598
|
-
def
|
|
599
|
-
"""
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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.
|
|
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.
|
|
690
|
-
self.
|
|
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.
|
|
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.
|
|
832
|
+
Path(self.videoclip.new_rec_name).name
|
|
705
833
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
706
|
-
self.
|
|
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
|
-
#
|
|
848
|
+
# consistency check
|
|
721
849
|
nchan_sox = sox.file_info.channels(
|
|
722
850
|
_pathname(concatenate_audio_file))
|
|
723
|
-
logger.debug('nchan_sox: %i
|
|
724
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
732
|
-
#
|
|
733
|
-
#
|
|
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
|
-
#
|
|
738
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
768
|
-
tag =
|
|
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
|
-
|
|
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.
|
|
812
|
-
audio in self.
|
|
964
|
+
Calls ffmpeg to join video in self.videoclip.AVpath to
|
|
965
|
+
audio in self.videoclip.synced_audio
|
|
813
966
|
|
|
814
|
-
On entry,
|
|
815
|
-
file (contrarily to
|
|
816
|
-
On exit, self.
|
|
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.
|
|
822
|
-
video_path = self.
|
|
823
|
-
timecode = self.
|
|
824
|
-
# self.
|
|
825
|
-
audio_path = self.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
951
|
-
reference_tag = 'video' if
|
|
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
|
-
|
|
955
|
-
audio_stitch = AudioStitcherVideoMerger(
|
|
1097
|
+
videoclip))
|
|
1098
|
+
audio_stitch = AudioStitcherVideoMerger(videoclip)
|
|
956
1099
|
for audio in audio_recs:
|
|
957
|
-
if self._does_overlap(
|
|
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'%
|
|
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,
|
|
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 =
|
|
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.
|
|
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]
|