tictacsync 0.2a8__tar.gz → 0.3a1__tar.gz
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-0.2a8/tictacsync.egg-info → tictacsync-0.3a1}/PKG-INFO +2 -2
- {tictacsync-0.2a8 → tictacsync-0.3a1}/README.md +1 -1
- {tictacsync-0.2a8 → tictacsync-0.3a1}/setup.py +1 -1
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/device_scanner.py +38 -35
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/entry.py +4 -3
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/remergemix.py +2 -2
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/timeline.py +183 -65
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/yaltc.py +15 -9
- {tictacsync-0.2a8 → tictacsync-0.3a1/tictacsync.egg-info}/PKG-INFO +2 -2
- {tictacsync-0.2a8 → tictacsync-0.3a1}/LICENSE +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/setup.cfg +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/__init__.py +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync/multi2polywav.py +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/SOURCES.txt +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/entry_points.txt +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/requires.txt +0 -0
- {tictacsync-0.2a8 → tictacsync-0.3a1}/tictacsync.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a1
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -83,7 +83,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
|
|
|
83
83
|
|
|
84
84
|
When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
|
|
85
85
|
|
|
86
|
-
> tictacsync -p
|
|
86
|
+
> tictacsync -p dailies/loose/MVI_0024.MP4
|
|
87
87
|
|
|
88
88
|
Typical first plot produced :
|
|
89
89
|
|
|
@@ -60,7 +60,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
|
|
|
60
60
|
|
|
61
61
|
When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
|
|
62
62
|
|
|
63
|
-
> tictacsync -p
|
|
63
|
+
> tictacsync -p dailies/loose/MVI_0024.MP4
|
|
64
64
|
|
|
65
65
|
Typical first plot produced :
|
|
66
66
|
|
|
@@ -31,7 +31,7 @@ setup(
|
|
|
31
31
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
|
-
version = '0.
|
|
34
|
+
version = '0.3a1',
|
|
35
35
|
description = "command for syncing audio video recordings",
|
|
36
36
|
long_description_content_type='text/markdown',
|
|
37
37
|
long_description = long_descr,
|
|
@@ -58,28 +58,6 @@ def print_grby(grby):
|
|
|
58
58
|
for e in keylist:
|
|
59
59
|
print(' ', e)
|
|
60
60
|
|
|
61
|
-
def media_at_path(p):
|
|
62
|
-
# return Media object for mediafile using ffprobe
|
|
63
|
-
dev_UID, dt = get_device_ffprobe_UID(p)
|
|
64
|
-
if dt == 'CAM':
|
|
65
|
-
streams = ffmpeg.probe(p)['streams']
|
|
66
|
-
audio_streams = [
|
|
67
|
-
stream
|
|
68
|
-
for stream
|
|
69
|
-
in streams
|
|
70
|
-
if stream['codec_type']=='audio'
|
|
71
|
-
]
|
|
72
|
-
if len(audio_streams) > 1:
|
|
73
|
-
raise Exception('ffprobe gave multiple audio streams?')
|
|
74
|
-
audio_str = audio_streams[0]
|
|
75
|
-
n = audio_str['channels']
|
|
76
|
-
# pprint(ffmpeg.probe(p))
|
|
77
|
-
else:
|
|
78
|
-
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
79
|
-
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
80
|
-
return Media(p,
|
|
81
|
-
Device(UID=dev_UID, folder=p.parent, name=None, dev_type=dt,
|
|
82
|
-
n_chan=n, tracks=None))
|
|
83
61
|
|
|
84
62
|
@dataclass
|
|
85
63
|
class Tracks:
|
|
@@ -111,8 +89,33 @@ class Media:
|
|
|
111
89
|
"""
|
|
112
90
|
path: Path
|
|
113
91
|
device: Device
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
def media_at_path(input_structure, p):
|
|
93
|
+
# return Media object for mediafile using ffprobe
|
|
94
|
+
dev_UID, dt = get_device_ffprobe_UID(p)
|
|
95
|
+
dev_name = None
|
|
96
|
+
if input_structure == 'folder_is_device':
|
|
97
|
+
dev_name = p.parent.name
|
|
98
|
+
if dev_UID is None:
|
|
99
|
+
dev_UID = hash(dev_name)
|
|
100
|
+
if dt == 'CAM':
|
|
101
|
+
streams = ffmpeg.probe(p)['streams']
|
|
102
|
+
audio_streams = [
|
|
103
|
+
stream
|
|
104
|
+
for stream
|
|
105
|
+
in streams
|
|
106
|
+
if stream['codec_type']=='audio'
|
|
107
|
+
]
|
|
108
|
+
if len(audio_streams) > 1:
|
|
109
|
+
raise Exception('ffprobe gave multiple audio streams?')
|
|
110
|
+
audio_str = audio_streams[0]
|
|
111
|
+
n = audio_str['channels']
|
|
112
|
+
# pprint(ffmpeg.probe(p))
|
|
113
|
+
else:
|
|
114
|
+
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
115
|
+
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
116
|
+
return Media(p,
|
|
117
|
+
Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
|
|
118
|
+
n_chan=n, tracks=None))
|
|
116
119
|
|
|
117
120
|
def get_device_ffprobe_UID(file):
|
|
118
121
|
"""
|
|
@@ -250,7 +253,7 @@ class Scanner:
|
|
|
250
253
|
and 'SyncedMedia' not in p.parts
|
|
251
254
|
]
|
|
252
255
|
for p in paths:
|
|
253
|
-
new_media = media_at_path(p) # dev UID set here
|
|
256
|
+
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
254
257
|
self.found_media_files.append(new_media)
|
|
255
258
|
# files from devices without UID or name
|
|
256
259
|
def _same(l):
|
|
@@ -289,7 +292,7 @@ class Scanner:
|
|
|
289
292
|
logger.debug('Scanner.found_media_files = %s'%self.found_media_files)
|
|
290
293
|
if self.input_structure == 'folder_is_device':
|
|
291
294
|
self._check_folders_have_same_device()
|
|
292
|
-
self._use_folder_as_device_name()
|
|
295
|
+
# self._use_folder_as_device_name()
|
|
293
296
|
devices = set([m.device for m in self.found_media_files])
|
|
294
297
|
audio_devices = [d for d in devices if d.dev_type == 'REC']
|
|
295
298
|
for recorder in audio_devices:
|
|
@@ -353,16 +356,16 @@ class Scanner:
|
|
|
353
356
|
logger.debug('no tracks.txt file found')
|
|
354
357
|
return None
|
|
355
358
|
|
|
356
|
-
def _use_folder_as_device_name(self):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
# def _use_folder_as_device_name(self):
|
|
360
|
+
# """
|
|
361
|
+
# For each media in self.found_media_files replace existing Device.name by
|
|
362
|
+
# folder name.
|
|
360
363
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
# Returns nothing
|
|
365
|
+
# """
|
|
366
|
+
# for m in self.found_media_files:
|
|
367
|
+
# m.device.name = m.path.parent.name
|
|
368
|
+
# logger.debug(self.found_media_files)
|
|
366
369
|
|
|
367
370
|
def _check_folders_have_same_device(self):
|
|
368
371
|
"""
|
|
@@ -71,7 +71,7 @@ def process_files(medias):
|
|
|
71
71
|
|
|
72
72
|
def process_single(file, args):
|
|
73
73
|
# argument is a single file
|
|
74
|
-
m = device_scanner.media_at_path(Path(file))
|
|
74
|
+
m = device_scanner.media_at_path(None, Path(file))
|
|
75
75
|
a_rec = yaltc.Recording(m)
|
|
76
76
|
time = a_rec.get_start_time(plots=args.plotting)
|
|
77
77
|
if time != None:
|
|
@@ -126,9 +126,10 @@ def main():
|
|
|
126
126
|
# logger.add(sys.stdout, filter="__main__")
|
|
127
127
|
# logger.add(sys.stdout, filter="device_scanner")
|
|
128
128
|
# logger.add(sys.stdout, filter="yaltc") _extract_sound_to_merge
|
|
129
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "media_at_path")
|
|
130
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
|
|
131
129
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "build_audio_and_write_video")
|
|
130
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_audio_devices")
|
|
131
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_mix")
|
|
132
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix")
|
|
132
133
|
top_dir = args.directory[0]
|
|
133
134
|
if os.path.isfile(top_dir):
|
|
134
135
|
file = top_dir
|
|
@@ -7,7 +7,7 @@ import shutil
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
logger.remove()
|
|
10
|
-
OUT_DIR = 'SyncedMedia'
|
|
10
|
+
# OUT_DIR = 'SyncedMedia'
|
|
11
11
|
|
|
12
12
|
def _pathname(tempfile_or_path) -> str:
|
|
13
13
|
if isinstance(tempfile_or_path, str):
|
|
@@ -129,7 +129,7 @@ def main():
|
|
|
129
129
|
"directory",
|
|
130
130
|
type=str,
|
|
131
131
|
nargs='+',
|
|
132
|
-
help="path of media directory containing
|
|
132
|
+
help="path of media directory containing Synced videos and their ISOs",
|
|
133
133
|
default='.'
|
|
134
134
|
)
|
|
135
135
|
args = parser.parse_args()
|
|
@@ -10,6 +10,7 @@ from itertools import groupby
|
|
|
10
10
|
# import opentimelineio as otio
|
|
11
11
|
from datetime import timedelta
|
|
12
12
|
import pprint, shutil, os
|
|
13
|
+
from subprocess import Popen, PIPE
|
|
13
14
|
|
|
14
15
|
from inspect import currentframe, getframeinfo
|
|
15
16
|
try:
|
|
@@ -19,6 +20,7 @@ except:
|
|
|
19
20
|
|
|
20
21
|
CLUSTER_GAP = 0.5 # secs between multicam clusters
|
|
21
22
|
DEL_TEMP = False
|
|
23
|
+
DB_OSX_NORM = -6 #dB
|
|
22
24
|
OUT_DIR_DEFAULT = 'SyncedMedia'
|
|
23
25
|
|
|
24
26
|
# utility for accessing pathnames
|
|
@@ -60,6 +62,51 @@ def _extr_channel(source, dest, channel):
|
|
|
60
62
|
status = sox_transform.build(str(source), str(dest))
|
|
61
63
|
logger.debug('sox status %s'%status)
|
|
62
64
|
|
|
65
|
+
def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
|
|
66
|
+
"""
|
|
67
|
+
Returns a NamedTemporaryFile containing the selected kept_channels
|
|
68
|
+
|
|
69
|
+
Building dict according to pysox.remix format.
|
|
70
|
+
https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
|
|
71
|
+
eg: 4 channels with TicTacCode_channel at #2
|
|
72
|
+
returns {1: [1], 2: [3], 3: [4]}
|
|
73
|
+
ie the number of channels drops by one and chan 2 is missing
|
|
74
|
+
excluded_channels is a list of Zero Based indexing chan numbers
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
audio_file = _pathname(audio_file)
|
|
78
|
+
nchan = sox.file_info.channels(audio_file)
|
|
79
|
+
logger.debug('in file of %i chan, have to keep %s'%
|
|
80
|
+
(nchan, kept_channels))
|
|
81
|
+
all_channels = range(1, nchan + 1) # from 1 to nchan included
|
|
82
|
+
# list of list for pysox API
|
|
83
|
+
# eg [[1], [3], [4]]
|
|
84
|
+
kept_channels = [[n] for n in kept_channels]
|
|
85
|
+
sox_remix_dict = dict(zip(all_channels, kept_channels))
|
|
86
|
+
# {1: [1], 2: [3], 3: [4]} -> from 4 to 3 chan and chan 2 is dropped
|
|
87
|
+
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
88
|
+
out_file = _pathname(output_fh)
|
|
89
|
+
logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
|
|
90
|
+
# sox_transform.set_output_format(channels=1)
|
|
91
|
+
sox_transform = sox.Transformer()
|
|
92
|
+
sox_transform.remix(sox_remix_dict)
|
|
93
|
+
logger.debug('sox remix transform: %s'%sox_transform)
|
|
94
|
+
logger.debug('sox remix dict: %s'%sox_remix_dict)
|
|
95
|
+
status = sox_transform.build(audio_file, out_file, return_output=True )
|
|
96
|
+
logger.debug('sox.build exit code %s'%str(status))
|
|
97
|
+
p = Popen('ffprobe %s -hide_banner'%audio_file,
|
|
98
|
+
shell=True, stdout=PIPE, stderr=PIPE)
|
|
99
|
+
stdout, stderr = p.communicate()
|
|
100
|
+
logger.debug('remixed input_file ffprobe:\n%s'%(stdout +
|
|
101
|
+
stderr).decode('utf-8'))
|
|
102
|
+
p = Popen('ffprobe %s -hide_banner'%out_file,
|
|
103
|
+
shell=True, stdout=PIPE, stderr=PIPE)
|
|
104
|
+
stdout, stderr = p.communicate()
|
|
105
|
+
logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
|
|
106
|
+
stderr).decode('utf-8'))
|
|
107
|
+
return output_fh
|
|
108
|
+
|
|
109
|
+
|
|
63
110
|
def _split_channels(multi_chan_audio:Path) -> list:
|
|
64
111
|
nchan = sox.file_info.channels(_pathname(multi_chan_audio))
|
|
65
112
|
source = _pathname(multi_chan_audio)
|
|
@@ -78,11 +125,10 @@ def _split_channels(multi_chan_audio:Path) -> list:
|
|
|
78
125
|
logger.debug('paths %s'%paths)
|
|
79
126
|
return paths
|
|
80
127
|
|
|
81
|
-
|
|
82
128
|
def _sox_combine(paths) -> Path:
|
|
83
129
|
"""
|
|
84
|
-
Combines files referred by the list of Path into a new temporary
|
|
85
|
-
passed on return each files are stacked in a different channel, so
|
|
130
|
+
Combines (stacks) files referred by the list of Path into a new temporary
|
|
131
|
+
files passed on return each files are stacked in a different channel, so
|
|
86
132
|
len(paths) == n_channels
|
|
87
133
|
"""
|
|
88
134
|
if len(paths) == 1: # one device only, nothing to stack
|
|
@@ -117,13 +163,26 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
|
|
|
117
163
|
"""
|
|
118
164
|
mix files referred by the list of Path into a new temporary files passed on return
|
|
119
165
|
"""
|
|
166
|
+
def _sox_norm(tempf):
|
|
167
|
+
normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
|
|
168
|
+
delete=DEL_TEMP)
|
|
169
|
+
tfm = sox.Transformer()
|
|
170
|
+
tfm.norm(DB_OSX_NORM)
|
|
171
|
+
status = tfm.build(_pathname(tempf),_pathname(normed_tempfile))
|
|
172
|
+
logger.debug('sox.build status for norm(): %s'%status)
|
|
173
|
+
if status != True:
|
|
174
|
+
print('Error, sox did not normalize file in _sox_mix()')
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
return normed_tempfile
|
|
177
|
+
paths = [_sox_norm(p) for p in paths]
|
|
120
178
|
cbn = sox.Combiner()
|
|
121
179
|
N = len(paths)
|
|
122
180
|
if N == 1: # nothing to mix
|
|
181
|
+
logger.debug('one file: nothing to mix')
|
|
123
182
|
return paths[0]
|
|
124
183
|
cbn.set_input_format(file_type=['wav']*N)
|
|
125
184
|
filenames = [_pathname(p) for p in paths]
|
|
126
|
-
logger.debug('files to mix %s'%filenames)
|
|
185
|
+
logger.debug('%i files to mix %s'%(N, filenames))
|
|
127
186
|
mixed_tempf = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
|
|
128
187
|
status = cbn.build(filenames,
|
|
129
188
|
_pathname(mixed_tempf),
|
|
@@ -135,7 +194,7 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
|
|
|
135
194
|
sys.exit(1)
|
|
136
195
|
normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
|
|
137
196
|
tfm = sox.Transformer()
|
|
138
|
-
tfm.norm(
|
|
197
|
+
tfm.norm(DB_OSX_NORM)
|
|
139
198
|
status = tfm.build(_pathname(mixed_tempf),_pathname(normed_tempfile))
|
|
140
199
|
logger.debug('sox.build status for norm(): %s'%status)
|
|
141
200
|
if status != True:
|
|
@@ -216,8 +275,11 @@ class AudioStitcherVideoMerger:
|
|
|
216
275
|
return list(self.edited_audio.keys())
|
|
217
276
|
|
|
218
277
|
def _get_audio_devices(self):
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
devices = set([r.device for r in self.get_matched_audio_recs()])
|
|
279
|
+
logger.debug('get_matched_audio_recs: %s'%
|
|
280
|
+
pprint.pformat(self.get_matched_audio_recs()))
|
|
281
|
+
logger.debug('devices %s'%devices)
|
|
282
|
+
return devices
|
|
221
283
|
|
|
222
284
|
def _get_all_recordings_for(self, device):
|
|
223
285
|
# return recordings for a particular device, sorted by time
|
|
@@ -433,20 +495,14 @@ class AudioStitcherVideoMerger:
|
|
|
433
495
|
# audio_rec.edited_version = output_fh
|
|
434
496
|
self.edited_audio[audio_rec] = output_fh
|
|
435
497
|
|
|
436
|
-
|
|
437
498
|
def _write_ISOs(self, edited_audio_all_devices):
|
|
438
499
|
"""
|
|
439
500
|
Writes isolated audio files that were synced to synced_clip_file,
|
|
440
501
|
each track will have its dedicated monofile, named sequentially or with
|
|
441
502
|
the name find in TRACKSFN if any, see Scanner._get_tracks_from_file()
|
|
442
503
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
device:
|
|
446
|
-
instance of device_scanner.Device
|
|
447
|
-
dev_joined_audio:
|
|
448
|
-
NamedTemporaryFile for the joined synced (multi channel) audio for
|
|
449
|
-
a specific device
|
|
504
|
+
edited_audio_all_devices:
|
|
505
|
+
a list of (name, mono_tempfile)
|
|
450
506
|
|
|
451
507
|
Returns nothing, output is written to filesystem as below.
|
|
452
508
|
ISOs subfolders structure when user invokes the --isos flag:
|
|
@@ -508,16 +564,52 @@ class AudioStitcherVideoMerger:
|
|
|
508
564
|
# ISO_multi_chan = ISOdir / 'ISO_multi_chan.wav'
|
|
509
565
|
# logger.debug('temp file: %s'%(ISO_multi_chan))
|
|
510
566
|
# logger.debug('will split audio to %s'%(ISOdir))
|
|
511
|
-
for name,
|
|
567
|
+
for name, mono_tmpfl in edited_audio_all_devices:
|
|
512
568
|
# pad(start_duration: float = 0.0, end_duration: float = 0.0)[source]
|
|
513
569
|
destination = ISOdir/('%s.wav'%name)
|
|
514
|
-
|
|
515
|
-
shutil.copy(_pathname(
|
|
570
|
+
mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
|
|
571
|
+
shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
|
|
516
572
|
logger.debug('destination:%s'%destination)
|
|
517
573
|
# # mixNnormed = _sox_mix(tempfiles)
|
|
518
574
|
# # print('516', _pathname(mixNnormed))
|
|
519
575
|
# os.remove(ISO_multi_chan)
|
|
520
576
|
|
|
577
|
+
def _get_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
|
|
578
|
+
"""
|
|
579
|
+
If device has an associated Tracks description that declares a (mono or
|
|
580
|
+
stereo) mix track, returns a tmpfl containing the corresponding
|
|
581
|
+
tracks. If not, mix all the tracks with sox.
|
|
582
|
+
|
|
583
|
+
"""
|
|
584
|
+
if device.tracks != None:
|
|
585
|
+
mix_tracks = device.tracks.mix
|
|
586
|
+
if len(mix_tracks) > 0:
|
|
587
|
+
logger.debug('%s has mix %s'%(device.name, mix_tracks))
|
|
588
|
+
logger.debug('device %s'%device)
|
|
589
|
+
if 'ttc' in device.tracks.rawtrx:
|
|
590
|
+
sox_TTC_chan = device.tracks.rawtrx.index('ttc')
|
|
591
|
+
elif 'tc' in device.tracks.rawtrx:
|
|
592
|
+
sox_TTC_chan = device.tracks.rawtrx.index('tc')
|
|
593
|
+
else:
|
|
594
|
+
print('Error: no tc or ttc tag in track.txt')
|
|
595
|
+
sys.exit(1)
|
|
596
|
+
sox_TTC_chan += 1 # sox NZBIDX
|
|
597
|
+
logger.debug('TTC chan %i'%sox_TTC_chan)
|
|
598
|
+
# redo indexing since tracks.txt numbers refere to complete
|
|
599
|
+
# files and here audio file had TTC and muted channels
|
|
600
|
+
# removed.
|
|
601
|
+
shift = 0
|
|
602
|
+
if mix_tracks[0] > sox_TTC_chan:
|
|
603
|
+
shift += 1
|
|
604
|
+
for unused_tr in device.tracks.unused:
|
|
605
|
+
if mix_tracks[0] > unused_tr:
|
|
606
|
+
shift += 1
|
|
607
|
+
mix_tracks = [t-shift for t in mix_tracks]
|
|
608
|
+
logger.debug('new mix_tracks: %s'%mix_tracks)
|
|
609
|
+
return _sox_keep(multichan_tmpfl, mix_tracks)
|
|
610
|
+
else: # no tracks declaration, mix programmatically
|
|
611
|
+
return _sox_mix(_split_channels(multichan_tmpfl))
|
|
612
|
+
|
|
521
613
|
def build_audio_and_write_video(self, top_dir, output_dir,
|
|
522
614
|
write_multicam_structure,
|
|
523
615
|
asked_ISOs):
|
|
@@ -532,7 +624,7 @@ class AudioStitcherVideoMerger:
|
|
|
532
624
|
asked_ISOs: bool flag specified as CLI argument
|
|
533
625
|
|
|
534
626
|
For each audio devices found overlapping self.ref_recording: pad, trim
|
|
535
|
-
or stretch audio files calling _get_concatenated_audiofile_for(), and
|
|
627
|
+
or stretch audio files by calling _get_concatenated_audiofile_for(), and
|
|
536
628
|
put them in merged_audio_files_by_device. More than one audio recorder
|
|
537
629
|
can be used for a shot: that's why merged_audio_files_by_device is a
|
|
538
630
|
list
|
|
@@ -560,6 +652,7 @@ class AudioStitcherVideoMerger:
|
|
|
560
652
|
synced_clip_file = synced_clip_dir/\
|
|
561
653
|
Path(self.ref_recording.new_rec_name).name
|
|
562
654
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
655
|
+
self.ref_recording.final_synced_file = synced_clip_file # relative
|
|
563
656
|
# collecting edited audio by device, in (Device, tempfile) pairs:
|
|
564
657
|
merged_audio_files_by_device = [
|
|
565
658
|
(d, self._get_concatenated_audiofile_for(d))
|
|
@@ -567,45 +660,77 @@ class AudioStitcherVideoMerger:
|
|
|
567
660
|
if len(merged_audio_files_by_device) == 0:
|
|
568
661
|
# no audio file overlaps for this clip
|
|
569
662
|
return
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
663
|
+
if len(merged_audio_files_by_device) == 1:
|
|
664
|
+
# only one audio recorder was used, pick singleton in list
|
|
665
|
+
dev, concatenate_audio_file = merged_audio_files_by_device[0]
|
|
666
|
+
logger.debug('one audio device only: %s'%dev)
|
|
667
|
+
# check if this sole recorder is stereo
|
|
668
|
+
if dev.n_chan == 2:
|
|
669
|
+
# stereo minus TTC chan = mono, check consistency:
|
|
670
|
+
nchan_sox = sox.file_info.channels(
|
|
671
|
+
_pathname(concatenate_audio_file))
|
|
672
|
+
logger.debug('nchan_sox: %i mono?'%nchan_sox)
|
|
673
|
+
if not nchan_sox == 1:
|
|
674
|
+
raise Exception('Error in channel processing')
|
|
675
|
+
# all OK, merge and return
|
|
676
|
+
logger.debug('simply mono to merge')
|
|
677
|
+
self.ref_recording.synced_audio = concatenate_audio_file
|
|
678
|
+
self._merge_audio_and_video()
|
|
679
|
+
return
|
|
680
|
+
# if still here, either multitracks and/or multi recorders so check if a
|
|
681
|
+
# mix has been done on location and identified as is in atracks.txt
|
|
682
|
+
# file. Split audio channels in mono wav tempfiles at the same time
|
|
683
|
+
#
|
|
578
684
|
multiple_recorders = len(merged_audio_files_by_device) > 1
|
|
579
685
|
logger.debug('multiple_recorder: %s'%multiple_recorders)
|
|
580
|
-
#
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
for dev, idx, audio_tempfile in edited_audio_all_devices]
|
|
593
|
-
logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
|
|
594
|
-
video_path = self.ref_recording.AVpath
|
|
595
|
-
# stacked audio contains all audio recorders if many
|
|
596
|
-
mixed_audio = _sox_combine([audio for _, audio
|
|
597
|
-
in merged_audio_files_by_device]) # all devices
|
|
598
|
-
logger.debug('will merge with %s'%(_pathname(mixed_audio)))
|
|
599
|
-
self.ref_recording.synced_audio = mixed_audio
|
|
600
|
-
nchan = sox.file_info.channels(_pathname(mixed_audio))
|
|
601
|
-
logger.debug('mixed_audio n chan: %i'%nchan)
|
|
602
|
-
self.ref_recording.final_synced_file = synced_clip_file # relative
|
|
686
|
+
# dev_mixes_mix contains all audio recorders if many
|
|
687
|
+
mixes = [self._get_mix(device, multi_chan_audio)
|
|
688
|
+
for device, multi_chan_audio
|
|
689
|
+
in merged_audio_files_by_device]
|
|
690
|
+
logger.debug('thera are %i dev mixes'%len(mixes))
|
|
691
|
+
dev_mixes_mix = _sox_mix(mixes)
|
|
692
|
+
# dev_mixes_mix = _sox_combine([audio for _, audio
|
|
693
|
+
# in merged_audio_files_by_device]) # all devices
|
|
694
|
+
logger.debug('will merge with %s'%(_pathname(dev_mixes_mix)))
|
|
695
|
+
self.ref_recording.synced_audio = dev_mixes_mix
|
|
696
|
+
logger.debug('dev_mixes_mix n chan: %i'%
|
|
697
|
+
sox.file_info.channels(_pathname(dev_mixes_mix)))
|
|
603
698
|
self._merge_audio_and_video()
|
|
699
|
+
# devices_and_monofiles is list of (device, [monofiles])
|
|
700
|
+
# [(dev1, multichan1), (dev2, multichan2)] in
|
|
701
|
+
# merged_audio_files_by_device ->
|
|
702
|
+
# [(dev1, [mono1_ch1, mono1_ch2]), (dev2, [mono2_ch1, mono2_ch2)]] in
|
|
703
|
+
# devices_and_monofiles:
|
|
604
704
|
if asked_ISOs:
|
|
605
|
-
|
|
705
|
+
devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
|
|
706
|
+
for device, multi_chan_audio
|
|
707
|
+
in merged_audio_files_by_device]
|
|
708
|
+
logger.debug('devices_and_monofiles: %s'%
|
|
709
|
+
pprint.pformat(devices_and_monofiles))
|
|
710
|
+
def _trnm(dev, idx): # used in the list comprehension just below
|
|
711
|
+
# generates track name for later if asked_ISOs
|
|
712
|
+
# idx is from 0 to nchan-1 for this device
|
|
713
|
+
if dev.tracks == None:
|
|
714
|
+
tag = 'chan%s'%str(idx+1).zfill(2)
|
|
715
|
+
else:
|
|
716
|
+
audio_tags = [tag for tag in dev.tracks.rawtrx
|
|
717
|
+
if tag not in ['ttc','0','tc']]
|
|
718
|
+
tag = audio_tags[idx]
|
|
719
|
+
if multiple_recorders:
|
|
720
|
+
tag += '_' + dev.name
|
|
721
|
+
return tag
|
|
722
|
+
# replace device, idx pair with track name (+ device name if many)
|
|
723
|
+
# loop over devices than loop over tracks
|
|
724
|
+
names_audio_tempfiles = []
|
|
725
|
+
for dev, mono_tmpfiles_list in devices_and_monofiles:
|
|
726
|
+
for idx, monotf in enumerate(mono_tmpfiles_list):
|
|
727
|
+
names_audio_tempfiles.append((_trnm(dev, idx), monotf))
|
|
728
|
+
logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
|
|
729
|
+
self._write_ISOs(names_audio_tempfiles)
|
|
606
730
|
logger.debug('merged_audio_files_by_device %s'%
|
|
607
731
|
merged_audio_files_by_device)
|
|
608
|
-
|
|
732
|
+
# This loop below for logging purpose only:
|
|
733
|
+
for idx, pair in enumerate(merged_audio_files_by_device):
|
|
609
734
|
# dev_joined_audio is mono, stereo or even polywav from multitrack
|
|
610
735
|
# recorders. For one video there could be more than one dev_joined_audio
|
|
611
736
|
# if multiple audio recorders where used during the take.
|
|
@@ -617,15 +742,6 @@ class AudioStitcherVideoMerger:
|
|
|
617
742
|
(_pathname(dev_joined_audio), nchan))
|
|
618
743
|
logger.debug('duration %f s'%
|
|
619
744
|
sox.file_info.duration(_pathname(dev_joined_audio)))
|
|
620
|
-
# generates ISOs too
|
|
621
|
-
# if asked_ISOs:
|
|
622
|
-
# print('Writing ISO files for:')
|
|
623
|
-
# for idx, pair in enumerate(merged_audio_files_by_device):
|
|
624
|
-
# device, dev_joined_audio = pair
|
|
625
|
-
# logger.debug('device %i, dev_joined_audio %s %s'%
|
|
626
|
-
# (idx, device, _pathname(dev_joined_audio)))
|
|
627
|
-
# self._write_ISOs_for(synced_clip_file,
|
|
628
|
-
# device, dev_joined_audio)
|
|
629
745
|
|
|
630
746
|
def _keep_VIDEO_only(self, video_path):
|
|
631
747
|
# return file handle to a temp video file formed from the video_path
|
|
@@ -642,12 +758,15 @@ class AudioStitcherVideoMerger:
|
|
|
642
758
|
|
|
643
759
|
def _merge_audio_and_video(self):
|
|
644
760
|
"""
|
|
645
|
-
Calls ffmpeg to join
|
|
761
|
+
Calls ffmpeg to join video in self.ref_recording.AVpath to
|
|
762
|
+
audio in self.ref_recording.synced_audio
|
|
646
763
|
|
|
647
764
|
On entry, ref_recording.final_synced_file is a Path to an non existing
|
|
648
765
|
file (contrarily to ref_recording.synced_audio).
|
|
649
|
-
On exit, ref_recording.final_synced_file points to the final synced
|
|
650
|
-
video file.
|
|
766
|
+
On exit, self.ref_recording.final_synced_file points to the final synced
|
|
767
|
+
video file.
|
|
768
|
+
|
|
769
|
+
Returns nothing.
|
|
651
770
|
"""
|
|
652
771
|
synced_clip_file = self.ref_recording.final_synced_file
|
|
653
772
|
video_path = self.ref_recording.AVpath
|
|
@@ -699,7 +818,6 @@ class AudioStitcherVideoMerger:
|
|
|
699
818
|
print(e.stderr.decode('UTF-8'))
|
|
700
819
|
sys.exit(1)
|
|
701
820
|
|
|
702
|
-
|
|
703
821
|
class Matcher:
|
|
704
822
|
"""
|
|
705
823
|
Matcher looks for any video in self.recordings and for each one finds out
|
|
@@ -791,11 +791,12 @@ class Decoder:
|
|
|
791
791
|
# LSB is leftmost in TicTacCode
|
|
792
792
|
|
|
793
793
|
def _demod_values_are_OK(self, values_dict):
|
|
794
|
+
# TODO: use _get_timedate_from_dict rather (catching any ValueError)
|
|
794
795
|
ranges = {
|
|
795
796
|
'seconds': range(60),
|
|
796
797
|
'minutes': range(60),
|
|
797
798
|
'hours': range(24),
|
|
798
|
-
'day': range(1,32),
|
|
799
|
+
'day': range(1,32), # 32 ?
|
|
799
800
|
'month': range(1,13),
|
|
800
801
|
}
|
|
801
802
|
for key in ranges:
|
|
@@ -1253,14 +1254,18 @@ class Recording:
|
|
|
1253
1254
|
return self.decoder.get_time_in_sound_extract(plots)
|
|
1254
1255
|
|
|
1255
1256
|
def _get_timedate_from_dict(self, time_dict):
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1257
|
+
try:
|
|
1258
|
+
python_datetime = datetime(
|
|
1259
|
+
time_dict['year offset'] + YEAR_ZERO,
|
|
1260
|
+
time_dict['month'],
|
|
1261
|
+
time_dict['day'],
|
|
1262
|
+
time_dict['hours'],
|
|
1263
|
+
time_dict['minutes'],
|
|
1264
|
+
time_dict['seconds'],
|
|
1265
|
+
tzinfo=timezone.utc)
|
|
1266
|
+
except ValueError as e:
|
|
1267
|
+
print('Error converting date in _get_timedate_from_dict',e)
|
|
1268
|
+
sys.exit(1)
|
|
1264
1269
|
python_datetime += timedelta(seconds=1) # PPS precedes NMEA sequ
|
|
1265
1270
|
return python_datetime
|
|
1266
1271
|
|
|
@@ -1288,6 +1293,7 @@ class Recording:
|
|
|
1288
1293
|
"""
|
|
1289
1294
|
if t1 == None or t2 == None:
|
|
1290
1295
|
return False
|
|
1296
|
+
logger.debug('t1 : %s t2: %s'%(t1, t2))
|
|
1291
1297
|
datetime_1 = self._get_timedate_from_dict(t1)
|
|
1292
1298
|
datetime_2 = self._get_timedate_from_dict(t2)
|
|
1293
1299
|
# if datetime_2 < datetime_1:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a1
|
|
4
4
|
Summary: command for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -83,7 +83,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
|
|
|
83
83
|
|
|
84
84
|
When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
|
|
85
85
|
|
|
86
|
-
> tictacsync -p
|
|
86
|
+
> tictacsync -p dailies/loose/MVI_0024.MP4
|
|
87
87
|
|
|
88
88
|
Typical first plot produced :
|
|
89
89
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|