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/device_scanner.py +27 -35
- tictacsync/entry.py +8 -8
- tictacsync/timeline.py +350 -161
- tictacsync/yaltc.py +179 -171
- {tictacsync-0.3a3.dist-info → tictacsync-0.4a0.dist-info}/METADATA +1 -1
- tictacsync-0.4a0.dist-info/RECORD +15 -0
- tictacsync-0.3a3.dist-info/RECORD +0 -15
- {tictacsync-0.3a3.dist-info → tictacsync-0.4a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.3a3.dist-info → tictacsync-0.4a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.3a3.dist-info → tictacsync-0.4a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.3a3.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,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
|
-
|
|
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
|
-
#
|
|
83
|
-
#
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
254
|
+
print('Error, sox did not normalize file in _sox_mix_files()')
|
|
175
255
|
sys.exit(1)
|
|
176
256
|
return normed_tempfile
|
|
177
|
-
|
|
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
|
|
260
|
+
return temp_files_to_mix[0] ########################################################
|
|
261
|
+
cbn = sox.Combiner()
|
|
183
262
|
cbn.set_input_format(file_type=['wav']*N)
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
227
|
-
The video
|
|
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,
|
|
242
|
-
self.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
|
330
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
426
|
-
if
|
|
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-
|
|
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 = (
|
|
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.
|
|
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.
|
|
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.
|
|
647
|
+
self.videoclip.get_duration())
|
|
553
648
|
return out_tf
|
|
554
|
-
synced_clip_file = self.
|
|
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 =
|
|
668
|
+
# # mixNnormed = _sox_mix_files(tempfiles)
|
|
574
669
|
# # print('516', _pathname(mixNnormed))
|
|
575
670
|
# os.remove(ISO_multi_chan)
|
|
576
671
|
|
|
577
|
-
def
|
|
578
|
-
"""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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.
|
|
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.
|
|
642
|
-
self.
|
|
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.
|
|
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.
|
|
832
|
+
Path(self.videoclip.new_rec_name).name
|
|
657
833
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
658
|
-
self.
|
|
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
|
-
#
|
|
848
|
+
# consistency check
|
|
673
849
|
nchan_sox = sox.file_info.channels(
|
|
674
850
|
_pathname(concatenate_audio_file))
|
|
675
|
-
logger.debug('nchan_sox: %i
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
684
|
-
#
|
|
685
|
-
#
|
|
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
|
-
#
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
#
|
|
697
|
-
#
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
722
|
-
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]
|
|
723
922
|
if multiple_recorders:
|
|
724
923
|
tag += '_' + dev.name
|
|
725
|
-
|
|
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.
|
|
766
|
-
audio in self.
|
|
964
|
+
Calls ffmpeg to join video in self.videoclip.AVpath to
|
|
965
|
+
audio in self.videoclip.synced_audio
|
|
767
966
|
|
|
768
|
-
On entry,
|
|
769
|
-
file (contrarily to
|
|
770
|
-
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
|
|
771
970
|
video file.
|
|
772
971
|
|
|
773
972
|
Returns nothing.
|
|
774
973
|
"""
|
|
775
|
-
synced_clip_file = self.
|
|
776
|
-
video_path = self.
|
|
777
|
-
timecode = self.
|
|
778
|
-
# self.
|
|
779
|
-
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
905
|
-
reference_tag = 'video' if
|
|
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
|
-
|
|
909
|
-
audio_stitch = AudioStitcherVideoMerger(
|
|
1097
|
+
videoclip))
|
|
1098
|
+
audio_stitch = AudioStitcherVideoMerger(videoclip)
|
|
910
1099
|
for audio in audio_recs:
|
|
911
|
-
if self._does_overlap(
|
|
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'%
|
|
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,
|
|
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 =
|
|
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.
|
|
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]
|