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