tictacsync 0.4a0__py3-none-any.whl → 0.6a0__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 +13 -10
- tictacsync/entry.py +8 -11
- tictacsync/multi2polywav.py +2 -1
- tictacsync/remergemix.py +205 -92
- tictacsync/timeline.py +43 -23
- tictacsync/yaltc.py +361 -915
- {tictacsync-0.4a0.dist-info → tictacsync-0.6a0.dist-info}/METADATA +15 -9
- tictacsync-0.6a0.dist-info/RECORD +15 -0
- tictacsync-0.4a0.dist-info/RECORD +0 -15
- {tictacsync-0.4a0.dist-info → tictacsync-0.6a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.4a0.dist-info → tictacsync-0.6a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.4a0.dist-info → tictacsync-0.6a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.4a0.dist-info → tictacsync-0.6a0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -90,10 +90,12 @@ class Media:
|
|
|
90
90
|
"""
|
|
91
91
|
path: Path
|
|
92
92
|
device: Device
|
|
93
|
+
|
|
93
94
|
def media_at_path(input_structure, p):
|
|
94
95
|
# return Media object for mediafile using ffprobe
|
|
95
96
|
dev_UID, dt = get_device_ffprobe_UID(p)
|
|
96
|
-
dev_name = None
|
|
97
|
+
dev_name = None
|
|
98
|
+
logger.debug('ffprobe dev_UID:%s dt:%s'%(dev_UID, dt))
|
|
97
99
|
if input_structure == 'folder_is_device':
|
|
98
100
|
dev_name = p.parent.name
|
|
99
101
|
if dev_UID is None:
|
|
@@ -114,9 +116,10 @@ def media_at_path(input_structure, p):
|
|
|
114
116
|
else:
|
|
115
117
|
n = sox.file_info.channels(_pathname(p)) # eg 2
|
|
116
118
|
logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
device = Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
|
|
120
|
+
n_chan=n, ttc=None, tracks=None)
|
|
121
|
+
logger.debug('for path: %s, device:%s'%(p,device))
|
|
122
|
+
return Media(p, device)
|
|
120
123
|
|
|
121
124
|
def get_device_ffprobe_UID(file):
|
|
122
125
|
"""
|
|
@@ -141,7 +144,7 @@ def get_device_ffprobe_UID(file):
|
|
|
141
144
|
except ffmpeg.Error as e:
|
|
142
145
|
print('ffmpeg.probe error')
|
|
143
146
|
print(e.stderr, file)
|
|
144
|
-
return None, None
|
|
147
|
+
return None, None #-----------------------------------------------------
|
|
145
148
|
# fall back to folder name
|
|
146
149
|
streams = probe['streams']
|
|
147
150
|
codecs = [stream['codec_type'] for stream in streams]
|
|
@@ -155,9 +158,12 @@ def get_device_ffprobe_UID(file):
|
|
|
155
158
|
and 'date' not in l ]
|
|
156
159
|
# this removes any metadata related to the file
|
|
157
160
|
# but keeps metadata related to the device
|
|
161
|
+
logger.debug('probe_lines %s'%probe_lines)
|
|
158
162
|
UID = hash(''.join(probe_lines))
|
|
159
163
|
else:
|
|
160
164
|
UID = None
|
|
165
|
+
if UID == 0: # empty probe_lines from Audacity ?!?
|
|
166
|
+
UID = None
|
|
161
167
|
logger.debug('ffprobe_UID is: %s'%UID)
|
|
162
168
|
return UID, device_type
|
|
163
169
|
|
|
@@ -257,9 +263,6 @@ class Scanner:
|
|
|
257
263
|
for p in paths:
|
|
258
264
|
new_media = media_at_path(self.input_structure, p) # dev UID set here
|
|
259
265
|
self.found_media_files.append(new_media)
|
|
260
|
-
# files from devices without UID or name
|
|
261
|
-
# def _list_all_the_same(l):
|
|
262
|
-
# return all(e == l[0] for e in l)
|
|
263
266
|
def _try_name(medias):
|
|
264
267
|
# return common first strings in filename
|
|
265
268
|
names = [m.path.name for m in medias]
|
|
@@ -274,7 +277,6 @@ class Scanner:
|
|
|
274
277
|
if not m.device.UID]
|
|
275
278
|
if no_device_UID_media:
|
|
276
279
|
logger.debug('no_device_UID_media %s'%no_device_UID_media)
|
|
277
|
-
# print(no_device_UID_media)
|
|
278
280
|
start_string = _try_name(no_device_UID_media)
|
|
279
281
|
if len(start_string) < 2:
|
|
280
282
|
print('\nError, cant identify the device for those files:')
|
|
@@ -286,7 +288,8 @@ class Scanner:
|
|
|
286
288
|
if not one_device.UID:
|
|
287
289
|
one_device.UID = hash(start_string)
|
|
288
290
|
print('\nWarning, guessing a device ID for those files:')
|
|
289
|
-
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
291
|
+
[print('[gold1]%s[/gold1], '%m.path.name, end='') for m
|
|
292
|
+
in no_device_UID_media]
|
|
290
293
|
print('UID: [gold1]%s[/gold1]'%start_string)
|
|
291
294
|
for m in no_device_UID_media:
|
|
292
295
|
m.device = one_device
|
tictacsync/entry.py
CHANGED
|
@@ -72,8 +72,9 @@ def process_files(medias):
|
|
|
72
72
|
def process_single(file, args):
|
|
73
73
|
# argument is a single file
|
|
74
74
|
m = device_scanner.media_at_path(None, Path(file))
|
|
75
|
-
a_rec = yaltc.Recording(m)
|
|
76
|
-
time = a_rec.get_start_time(
|
|
75
|
+
a_rec = yaltc.Recording(m, do_plots=args.plotting)
|
|
76
|
+
time = a_rec.get_start_time()
|
|
77
|
+
# time = a_rec.get_start_time(plots=args.plotting)
|
|
77
78
|
if time != None:
|
|
78
79
|
frac_time = int(time.microsecond / 1e2)
|
|
79
80
|
d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
|
|
@@ -124,11 +125,10 @@ def main():
|
|
|
124
125
|
if args.verbose_output:
|
|
125
126
|
logger.add(sys.stderr, level="DEBUG")
|
|
126
127
|
# logger.add(sys.stdout, filter="__main__")
|
|
127
|
-
# logger.add(sys.stdout, filter="
|
|
128
|
-
# logger.add(sys.stdout, filter="
|
|
129
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
128
|
+
# logger.add(sys.stdout, filter="yaltc")
|
|
129
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_read_sound_find_TicTacCode")
|
|
130
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_detect_sync_pulse_position")
|
|
130
131
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_device_mix")
|
|
131
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mono2stereo")
|
|
132
132
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix_files")
|
|
133
133
|
top_dir = args.directory[0]
|
|
134
134
|
if os.path.isfile(top_dir):
|
|
@@ -195,7 +195,7 @@ def main():
|
|
|
195
195
|
# table.add_column("Device\n", justify="center", style='gold1')
|
|
196
196
|
table.add_column("UTC times\nstart:end", justify="center", style='gold1')
|
|
197
197
|
table.add_column("Clock drift\n(ppm)", justify="right", style='gold1')
|
|
198
|
-
table.add_column("SN ratio\n(dB)", justify="center", style='gold1')
|
|
198
|
+
# table.add_column("SN ratio\n(dB)", justify="center", style='gold1')
|
|
199
199
|
table.add_column("Date\n", justify="center", style='gold1')
|
|
200
200
|
rec_WO_time = [
|
|
201
201
|
rec.AVpath.name
|
|
@@ -218,7 +218,7 @@ def main():
|
|
|
218
218
|
times_range,
|
|
219
219
|
# '%.6f'%(r.true_samplerate/1e3),
|
|
220
220
|
'%2i'%(r.get_samplerate_drift()),
|
|
221
|
-
'%.0f'%r.decoder.SN_ratio,
|
|
221
|
+
# '%.0f'%r.decoder.SN_ratio,
|
|
222
222
|
date
|
|
223
223
|
)
|
|
224
224
|
console = Console()
|
|
@@ -277,6 +277,3 @@ if __name__ == '__main__':
|
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
tictacsync/multi2polywav.py
CHANGED
|
@@ -61,7 +61,7 @@ def build_poly_name(multifiles):
|
|
|
61
61
|
"""
|
|
62
62
|
Returns string of polywav filename, constructed from similitudes between
|
|
63
63
|
multifile names. Ex:
|
|
64
|
-
|
|
64
|
+
4CH002I.wav and 4CH002M.wav returns 4CH002X.wav
|
|
65
65
|
"""
|
|
66
66
|
s1 = str(multifiles[0].stem)
|
|
67
67
|
s2 = str(multifiles[1].stem)
|
|
@@ -96,6 +96,7 @@ def build_poly(multifiles):
|
|
|
96
96
|
# multifiles is list of Path
|
|
97
97
|
# change extensions to mfw (multifile wav)
|
|
98
98
|
dir_multi = multifiles[0].parent
|
|
99
|
+
multifiles.reverse()
|
|
99
100
|
poly_name_b = build_poly_name(multifiles) # base only
|
|
100
101
|
poly_name = str(dir_multi/Path(poly_name_b))
|
|
101
102
|
filenames = [str(p) for p in multifiles]
|
tictacsync/remergemix.py
CHANGED
|
@@ -3,13 +3,35 @@ from loguru import logger
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import sox, tempfile, os, ffmpeg
|
|
5
5
|
from rich import print
|
|
6
|
-
import shutil
|
|
6
|
+
import shutil, sys, re
|
|
7
|
+
from pprint import pformat
|
|
8
|
+
from itertools import groupby
|
|
7
9
|
|
|
10
|
+
try:
|
|
11
|
+
from . import timeline
|
|
12
|
+
except:
|
|
13
|
+
import timeline
|
|
8
14
|
|
|
15
|
+
DEL_TEMP = False
|
|
16
|
+
|
|
17
|
+
logger.level("DEBUG", color="<yellow>")
|
|
9
18
|
logger.remove()
|
|
10
|
-
|
|
19
|
+
|
|
20
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_ISO_dirs")
|
|
21
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_join_audio2video")
|
|
22
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
23
|
+
# logger.add(sys.stdout, filter="__main__")
|
|
24
|
+
|
|
25
|
+
OUT_DIR = 'SyncedMedia'
|
|
26
|
+
SEC_DELAY_CHANGED_ISO = 10 #sec, ISO_DIR changed if diff time is bigger
|
|
27
|
+
|
|
28
|
+
video_extensions = \
|
|
29
|
+
"""webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
|
|
30
|
+
qt wmv yuv rm rmvb viv asf mp4 m4p m4v mpg mp2 mpeg mpe
|
|
31
|
+
mpv mpg mpeg m2v m4v svi 3gp 3g2 mxf roq nsv""".split() # from wikipedia
|
|
11
32
|
|
|
12
33
|
def _pathname(tempfile_or_path) -> str:
|
|
34
|
+
# utility for obtaining a str from different filesystem objects
|
|
13
35
|
if isinstance(tempfile_or_path, str):
|
|
14
36
|
return tempfile_or_path
|
|
15
37
|
if isinstance(tempfile_or_path, Path):
|
|
@@ -19,125 +41,216 @@ def _pathname(tempfile_or_path) -> str:
|
|
|
19
41
|
else:
|
|
20
42
|
raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
|
|
21
43
|
|
|
22
|
-
def
|
|
44
|
+
def _keep_VIDEO_only(video_path):
|
|
45
|
+
# return file handle to a temp video file formed from the video_path
|
|
46
|
+
# stripped of its sound
|
|
47
|
+
in1 = ffmpeg.input(_pathname(video_path))
|
|
48
|
+
video_extension = video_path.suffix
|
|
49
|
+
silenced_opts = ["-loglevel", "quiet", "-nostats", "-hide_banner"]
|
|
50
|
+
file_handle = tempfile.NamedTemporaryFile(suffix=video_extension,
|
|
51
|
+
delete=DEL_TEMP)
|
|
52
|
+
out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
|
|
53
|
+
ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
|
|
54
|
+
return file_handle
|
|
55
|
+
|
|
56
|
+
def _join_audio2video(audio_path: Path, video: Path):
|
|
57
|
+
"""
|
|
58
|
+
Replace audio in video (argument) by the audio contained in
|
|
59
|
+
audio_path (argument) returns nothing
|
|
60
|
+
"""
|
|
23
61
|
video_ext = video.name.split('.')[1]
|
|
24
|
-
|
|
62
|
+
vid_only_handle = _keep_VIDEO_only(video)
|
|
63
|
+
a_n = _pathname(audio_path)
|
|
64
|
+
v_n = _pathname(vid_only_handle)
|
|
65
|
+
out_n = _pathname(video)
|
|
66
|
+
# building args for debug purpose only:
|
|
67
|
+
ffmpeg_args = (
|
|
25
68
|
ffmpeg
|
|
26
|
-
.input(
|
|
27
|
-
|
|
69
|
+
.input(v_n)
|
|
70
|
+
.output(out_n, shortest=None, vcodec='copy')
|
|
71
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
72
|
+
.overwrite_output()
|
|
73
|
+
.get_args()
|
|
74
|
+
)
|
|
75
|
+
logger.debug('ffmpeg args: %s'%' '.join(ffmpeg_args))
|
|
76
|
+
try: # for real now
|
|
77
|
+
_, out = (
|
|
28
78
|
ffmpeg
|
|
29
|
-
.input(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
(ffmpeg
|
|
34
|
-
.filter((audio_left, audio_right), 'join', inputs=2,
|
|
35
|
-
channel_layout='stereo')
|
|
36
|
-
.output(input_video.video, _pathname(out_file),
|
|
37
|
-
shortest=None, vcodec='copy', loglevel="quiet")
|
|
79
|
+
.input(v_n)
|
|
80
|
+
.output(out_n, shortest=None, vcodec='copy')
|
|
81
|
+
.global_args('-i', a_n, "-hide_banner")
|
|
38
82
|
.overwrite_output()
|
|
39
|
-
.run()
|
|
83
|
+
.run(capture_stderr=True)
|
|
84
|
+
)
|
|
85
|
+
logger.debug('ffmpeg output')
|
|
86
|
+
for l in out.decode("utf-8").split('\n'):
|
|
87
|
+
logger.debug(l)
|
|
40
88
|
except ffmpeg.Error as e:
|
|
89
|
+
print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
|
|
90
|
+
audio_path,
|
|
91
|
+
video_path,
|
|
92
|
+
synced_clip_file
|
|
93
|
+
))
|
|
41
94
|
print(e)
|
|
42
95
|
print(e.stderr.decode('UTF-8'))
|
|
43
96
|
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def sync_cam(dir):
|
|
48
|
-
# dir is a CAM dir, contents are clips and ISO folders
|
|
49
|
-
ISOs = list(dir.glob('*.ISO'))
|
|
50
|
-
for iso in ISOs:
|
|
51
|
-
# iso is a folder
|
|
52
|
-
statResult = iso.stat()
|
|
53
|
-
mtime = statResult.st_mtime
|
|
54
|
-
# print('%s: %s'%(iso.name, biggest_mtime_in_dir(iso)))
|
|
55
|
-
iso_mod_time = biggest_mtime_in_dir(iso)
|
|
56
|
-
clip = clip_from_iso(iso)
|
|
57
|
-
clip_mod_time = clip.stat().st_mtime
|
|
58
|
-
iso_edited = iso_mod_time > clip_mod_time
|
|
59
|
-
# print('clip %s should be resync: %s'%(clip.name, iso_edited))
|
|
60
|
-
# print(clip)
|
|
61
|
-
if iso_edited:
|
|
62
|
-
print('Resyncing [gold1]%s[/gold1]'%clip.name)
|
|
63
|
-
LR_channels = list(valid_audio_files(iso))
|
|
64
|
-
_join(LR_channels, clip, 'test.MOV')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def valid_audio_files(isofolder: Path) -> list:
|
|
97
|
+
|
|
98
|
+
def _changed(dir) -> bool:
|
|
68
99
|
"""
|
|
69
|
-
Returns
|
|
70
|
-
|
|
71
|
-
case B - more than two files, two are mixL mixR
|
|
72
|
-
case C - one file only -> sys.exit(1)
|
|
73
|
-
case D - more than two files, no mixL mixR -> sys.exit(1)
|
|
100
|
+
Returns True if any content of dir (arg) is more recent than dir itself by a
|
|
101
|
+
delay of SEC_DELAY_CHANGED_ISO. Uses modification times.
|
|
74
102
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
logger.debug(f'checking {dir.name} for change')
|
|
104
|
+
ISO_modification_time = biggest_mtime_in_dir(dir)
|
|
105
|
+
clip = clip_from_iso(dir)
|
|
106
|
+
clip_mod_time = clip.stat().st_mtime
|
|
107
|
+
# difference of modification time in secs
|
|
108
|
+
ISO_more_recent_by = ISO_modification_time - clip_mod_time
|
|
109
|
+
logger.debug('ISO_more_recent_by: %s'%(ISO_more_recent_by))
|
|
110
|
+
iso_edited = ISO_more_recent_by > SEC_DELAY_CHANGED_ISO
|
|
111
|
+
logger.debug(f'_changed: {iso_edited}')
|
|
112
|
+
return iso_edited
|
|
113
|
+
|
|
114
|
+
def sox_mix(ISOdir):
|
|
115
|
+
"""
|
|
116
|
+
Mixes all wav files present in ISOdir (so excludes ttc file and nullified
|
|
117
|
+
ones).
|
|
118
|
+
Returns a mono or stereo tempfile
|
|
119
|
+
"""
|
|
120
|
+
logger.debug(f'mixing {ISOdir}')
|
|
121
|
+
def is_stereo_mic(p):
|
|
122
|
+
re_result = re.match(r'mic([lrLR])*', p.name)
|
|
123
|
+
# logger.debug(f're_result {re_result}')
|
|
124
|
+
return re_result is not None
|
|
125
|
+
stereo_mics = [p for p in ISOdir.iterdir() if is_stereo_mic(p)]
|
|
126
|
+
monofiles = [p for p in ISOdir.iterdir() if p not in stereo_mics]
|
|
127
|
+
# removing ttc files
|
|
128
|
+
def notTTC(p):
|
|
129
|
+
return p.name[:3] != 'ttc'
|
|
130
|
+
monofiles = [p for p in monofiles if notTTC(p)]
|
|
131
|
+
if stereo_mics == []: # mono
|
|
132
|
+
return timeline._sox_mix_files(monofiles) #-----------------------------
|
|
133
|
+
logger.debug(f'stereo_mics: {stereo_mics}')
|
|
134
|
+
def mic(p):
|
|
135
|
+
return re.search(r'(mic\d*)([lrLR])*', p.name).groups()
|
|
136
|
+
mics = [mic(p) for p in stereo_mics]
|
|
137
|
+
p_and_mic = list(zip(stereo_mics, mics))
|
|
138
|
+
logger.debug(f'p_and_mic: {p_and_mic}')
|
|
139
|
+
same_mic_key = lambda pair: pair[1][0]
|
|
140
|
+
p_and_mic = sorted(p_and_mic, key=same_mic_key)
|
|
141
|
+
grouped_by_mic = [ (k, list(iterator)) for k, iterator
|
|
142
|
+
in groupby(p_and_mic, same_mic_key)]
|
|
143
|
+
logger.debug(f'grouped_by_mic: {grouped_by_mic}')
|
|
144
|
+
def order_left_right(groupby_element):
|
|
145
|
+
# returns left and right path for a mic
|
|
146
|
+
name, paths = groupby_element
|
|
147
|
+
def chan(pair):
|
|
148
|
+
# (PosixPath('mic1r_ZOOM.wav'), ('mic1', 'r')) -> 'r'
|
|
149
|
+
return pair[1][1]
|
|
150
|
+
path_n_mic = sorted(paths, key=lambda pair: pair[1][1])
|
|
151
|
+
return [p[0] for p in path_n_mic] # just the path, not ('mic1', 'r')
|
|
152
|
+
left_right_paths = [order_left_right(e) for e in grouped_by_mic]
|
|
153
|
+
# logger.debug(f'left_right_paths: {left_right_paths}')
|
|
154
|
+
stereo_files = [timeline._sox_combine(pair) for pair in left_right_paths]
|
|
155
|
+
monoNstereo = monofiles + stereo_files
|
|
156
|
+
return timeline._sox_mix_files(monoNstereo)
|
|
157
|
+
|
|
158
|
+
def get_mix_file(iso_dir):
|
|
159
|
+
"""
|
|
160
|
+
If iso_dir (arg) contains a mono mix sound file or a stereo mix, returns its
|
|
161
|
+
path. If not, this creates the mix and returns it.
|
|
162
|
+
"""
|
|
163
|
+
wav_files = list(iso_dir.iterdir())
|
|
164
|
+
logger.debug(f'wav_files {wav_files}')
|
|
165
|
+
def is_mix(p):
|
|
166
|
+
re_result = re.match(r'mix([lrLR])*', p.name)
|
|
167
|
+
# logger.debug(f're_result {re_result}')
|
|
168
|
+
return re_result is not None
|
|
169
|
+
location_mix = [p for p in wav_files if is_mix(p)]
|
|
170
|
+
if location_mix == []:
|
|
171
|
+
logger.debug('no mix track, do the mix')
|
|
172
|
+
return sox_mix(iso_dir)
|
|
173
|
+
else:
|
|
174
|
+
return location_mix
|
|
101
175
|
|
|
102
176
|
def biggest_mtime_in_dir(folder: Path) -> float:
|
|
103
|
-
# return the most recent mod time in a folder
|
|
177
|
+
# return the most recent mod time of the files in a folder
|
|
104
178
|
dir_content = list(folder.iterdir())
|
|
105
179
|
stats = [p.stat() for p in dir_content]
|
|
106
180
|
mtimes = [stat.st_mtime for stat in stats]
|
|
107
181
|
return max(mtimes)
|
|
108
182
|
|
|
109
|
-
def clip_from_iso(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
183
|
+
def clip_from_iso(ISO_dir: Path) -> Path:
|
|
184
|
+
# find the sibling video file of ISO_dir. eg : MVI_01.ISO -> MVI_01.MP4
|
|
185
|
+
folder = ISO_dir.parent
|
|
186
|
+
siblings = list(folder.glob('%s.*'%ISO_dir.stem))
|
|
187
|
+
candidates =[p for p in siblings if p.name.split('.')[1] != 'ISO']
|
|
188
|
+
# should be unique
|
|
189
|
+
if len(candidates) != 1:
|
|
190
|
+
print(f'Error finding video corresponding to {ISO_dir}, quitting')
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
return candidates[0]
|
|
193
|
+
|
|
194
|
+
def _get_ISO_dirs(top_dir):
|
|
195
|
+
"""
|
|
196
|
+
Check if top_dir contains (or somewhere beneath) videos with their
|
|
197
|
+
accompanying ISO folder (like the pair MVI_023.MP4 + MVI_023.ISO). If not
|
|
198
|
+
warns and exits.
|
|
199
|
+
|
|
200
|
+
Returns list of paths pointing to ISO dirs.
|
|
201
|
+
"""
|
|
202
|
+
p = Path(top_dir)
|
|
203
|
+
ISO_dirs = list(Path(top_dir).rglob('*.ISO'))
|
|
204
|
+
logger.debug('all files: %s'%pformat(ISO_dirs))
|
|
205
|
+
# validation: .ISO should be dir
|
|
206
|
+
all_are_dir = all([p.is_dir() for p in ISO_dirs])
|
|
207
|
+
logger.debug('.ISO are all dir %s'%all_are_dir)
|
|
120
208
|
if not all_are_dir:
|
|
121
|
-
print('Error:
|
|
122
|
-
print('Rerun tictacsync with one directory for each device.')
|
|
209
|
+
print('Error: some .ISO are not folders??? Quitting. %s'%ISO_dirs)
|
|
123
210
|
sys.exit(1)
|
|
124
|
-
|
|
211
|
+
# for each folder check a video file exists with the same stem
|
|
212
|
+
# but with a video format extension (listed in video_extensions)
|
|
213
|
+
for ISO in ISO_dirs:
|
|
214
|
+
logger.debug(f'checking {ISO}')
|
|
215
|
+
voisins = list(ISO.parent.glob(f'{ISO.stem}.*'))
|
|
216
|
+
voisins_suffixes = [p.suffix for p in voisins]
|
|
217
|
+
# remove ISO
|
|
218
|
+
voisins_suffixes.remove('.ISO')
|
|
219
|
+
# validations: should remain one element and should be video
|
|
220
|
+
suffix = voisins_suffixes[0].lower()[1:] # remove dot
|
|
221
|
+
logger.debug(f'remaining ext: {suffix}')
|
|
222
|
+
if len(voisins_suffixes) != 1 or suffix not in video_extensions:
|
|
223
|
+
print(f'Error with {voisins}, no video unique sibling?')
|
|
224
|
+
sys.exit(1) #-------------------------------------------------------
|
|
225
|
+
logger.debug(f'All ok, returning {ISO_dirs}')
|
|
226
|
+
return ISO_dirs
|
|
125
227
|
|
|
126
228
|
def main():
|
|
127
229
|
parser = argparse.ArgumentParser()
|
|
128
230
|
parser.add_argument(
|
|
129
231
|
"directory",
|
|
130
232
|
type=str,
|
|
131
|
-
nargs=
|
|
132
|
-
help="path of media directory containing Synced videos and their
|
|
233
|
+
nargs=1,
|
|
234
|
+
help="path of media directory containing Synced videos and their .ISO folder",
|
|
133
235
|
default='.'
|
|
134
236
|
)
|
|
135
237
|
args = parser.parse_args()
|
|
136
|
-
# logger.info('arguments: %s'%args)
|
|
137
238
|
logger.debug('args %s'%args)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
239
|
+
ISO_dirs = _get_ISO_dirs(args.directory[0])
|
|
240
|
+
logger.debug(f'Will check any change in {pformat(ISO_dirs)}')
|
|
241
|
+
changed_ISOs = [isod for isod in ISO_dirs if _changed(isod)]
|
|
242
|
+
logger.debug(f'changed_ISOs: {changed_ISOs}')
|
|
243
|
+
if changed_ISOs != []:
|
|
244
|
+
print('Will remix audio for:')
|
|
245
|
+
for p in changed_ISOs:
|
|
246
|
+
print(p.name)
|
|
247
|
+
newaudio_and_videos = [(get_mix_file(iso), clip_from_iso(iso)) for iso
|
|
248
|
+
in changed_ISOs]
|
|
249
|
+
for audio, video_clip in newaudio_and_videos:
|
|
250
|
+
print(f'Will remerge {video_clip.name}')
|
|
251
|
+
_join_audio2video(audio, video_clip)
|
|
252
|
+
if newaudio_and_videos == []:
|
|
253
|
+
print('Nothing has changed, bye.')
|
|
141
254
|
sys.exit(0)
|
|
142
255
|
|
|
143
256
|
if __name__ == '__main__':
|
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
|
|
@@ -218,7 +220,6 @@ def _sox_mix_channels(multichan_tmpfl, stereo_pairs=[]) -> tempfile.NamedTempora
|
|
|
218
220
|
# stereo tracks present, so stereo output
|
|
219
221
|
logger.debug('stereo tracks present %s, so stereo output'%stereo_pairs)
|
|
220
222
|
stereo_files = [_sox_keep(pair) for pair in stereo_pairs]
|
|
221
|
-
#### ???
|
|
222
223
|
return
|
|
223
224
|
|
|
224
225
|
def _sox_mono2stereo(temp_file) -> tempfile.NamedTemporaryFile:
|
|
@@ -717,6 +718,7 @@ class AudioStitcherVideoMerger:
|
|
|
717
718
|
kept_channel = (device.ttc + 1)%2 # 1 -> 0 and 0 -> 1
|
|
718
719
|
logger.debug('no tracks.txt, keeping one chan %i'%kept_channel)
|
|
719
720
|
return _sox_keep(multichan_tmpfl, [kept_channel + 1]) #-------------
|
|
721
|
+
logger.debug('device.n_chan != 2, so multitrack')
|
|
720
722
|
# it's multitrack (more than 2 channels)
|
|
721
723
|
if device.tracks is None:
|
|
722
724
|
# multitrack but no mix done on location, so do mono mix with all
|
|
@@ -728,7 +730,21 @@ class AudioStitcherVideoMerger:
|
|
|
728
730
|
logger.debug('mixing channels: %s (sox #)'%sox_kept_channels)
|
|
729
731
|
kept_audio = _sox_keep(multichan_tmpfl, sox_kept_channels)
|
|
730
732
|
return _sox_mix_channels(kept_audio) #------------------------------
|
|
731
|
-
|
|
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)
|
|
732
748
|
if device.tracks.mix == [] and device.tracks.stereomics == []:
|
|
733
749
|
# it's multitrac and no mix done on location, so do a mono mix with
|
|
734
750
|
# all, but here remove '0' and TTC tracks from mix
|
|
@@ -741,6 +757,7 @@ class AudioStitcherVideoMerger:
|
|
|
741
757
|
logger.debug('mixing channels: %s (sox #)'%sox_kept_channels)
|
|
742
758
|
kept_audio = _sox_keep(multichan_tmpfl, sox_kept_channels)
|
|
743
759
|
return _sox_mix_channels(kept_audio) #------------------------------
|
|
760
|
+
logger.debug('device.tracks.mix != [] or device.tracks.stereomics != []')
|
|
744
761
|
if device.tracks.mix != []:
|
|
745
762
|
# Mix were done on location, no and we only have to extracted it
|
|
746
763
|
# from the recording. If mono mix, device.tracks.mix has one element;
|
|
@@ -764,8 +781,10 @@ class AudioStitcherVideoMerger:
|
|
|
764
781
|
logger.debug('%s mix declared on channel %s (sox #)'%
|
|
765
782
|
(mix_kind, device.tracks.mix))
|
|
766
783
|
return _sox_keep(multichan_tmpfl, device.tracks.mix) #--------------
|
|
767
|
-
|
|
768
|
-
#
|
|
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
|
|
769
788
|
if device.tracks.stereomics == []:
|
|
770
789
|
print('Error, no stereo mic?, check tracks.txt. Quitting')
|
|
771
790
|
sys.exit(1)
|
|
@@ -780,6 +799,7 @@ class AudioStitcherVideoMerger:
|
|
|
780
799
|
logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
|
|
781
800
|
mono_tracks = [i for i in range(1, device.n_chan + 1)
|
|
782
801
|
if i not in stereo_mic_idx_flat]
|
|
802
|
+
logger.debug('mono_tracks: %s'%mono_tracks)
|
|
783
803
|
# remove TTC track number
|
|
784
804
|
mono_tracks.remove(device.ttc + 1)
|
|
785
805
|
logger.debug('mono_tracks %s'%mono_tracks)
|
|
@@ -831,8 +851,11 @@ class AudioStitcherVideoMerger:
|
|
|
831
851
|
synced_clip_file = synced_clip_dir/\
|
|
832
852
|
Path(self.videoclip.new_rec_name).name
|
|
833
853
|
logger.debug('editing files for %s'%synced_clip_file)
|
|
834
|
-
self.videoclip.final_synced_file = synced_clip_file # relative
|
|
835
|
-
#
|
|
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
|
|
836
859
|
merged_audio_files_by_device = [
|
|
837
860
|
(d, self._get_concatenated_audiofile_for(d))
|
|
838
861
|
for d in self._get_audio_devices()]
|
|
@@ -879,18 +902,6 @@ class AudioStitcherVideoMerger:
|
|
|
879
902
|
device_mixes = [self._get_device_mix(device, multi_chan_audio)
|
|
880
903
|
for device, multi_chan_audio
|
|
881
904
|
in merged_audio_files_by_device]
|
|
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
905
|
logger.debug('there are %i dev device_mixes'%len(device_mixes))
|
|
895
906
|
logger.debug('device_mixes %s'%device_mixes)
|
|
896
907
|
mix_of_device_mixes = _sox_mix_files(device_mixes)
|
|
@@ -910,7 +921,7 @@ class AudioStitcherVideoMerger:
|
|
|
910
921
|
in merged_audio_files_by_device]
|
|
911
922
|
logger.debug('devices_and_monofiles: %s'%
|
|
912
923
|
pprint.pformat(devices_and_monofiles))
|
|
913
|
-
def _trnm(dev, idx): # used in the
|
|
924
|
+
def _trnm(dev, idx): # used in the loop just below
|
|
914
925
|
# generates track name for later if asked_ISOs
|
|
915
926
|
# idx is from 0 to nchan-1 for this device
|
|
916
927
|
if dev.tracks == None:
|
|
@@ -928,7 +939,10 @@ class AudioStitcherVideoMerger:
|
|
|
928
939
|
names_audio_tempfiles = []
|
|
929
940
|
for dev, mono_tmpfiles_list in devices_and_monofiles:
|
|
930
941
|
for idx, monotf in enumerate(mono_tmpfiles_list):
|
|
931
|
-
|
|
942
|
+
track_name = _trnm(dev, idx)
|
|
943
|
+
if track_name[0] == '0': # muted, skip
|
|
944
|
+
continue
|
|
945
|
+
names_audio_tempfiles.append((track_name, monotf))
|
|
932
946
|
logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
|
|
933
947
|
self._write_ISOs(names_audio_tempfiles)
|
|
934
948
|
logger.debug('merged_audio_files_by_device %s'%
|
|
@@ -1097,6 +1111,7 @@ class Matcher:
|
|
|
1097
1111
|
videoclip))
|
|
1098
1112
|
audio_stitch = AudioStitcherVideoMerger(videoclip)
|
|
1099
1113
|
for audio in audio_recs:
|
|
1114
|
+
logger.debug('checking %s'%audio)
|
|
1100
1115
|
if self._does_overlap(videoclip, audio):
|
|
1101
1116
|
audio_stitch.add_matched_audio(audio)
|
|
1102
1117
|
logger.debug('recording %s overlaps,'%(audio))
|
|
@@ -1111,9 +1126,14 @@ class Matcher:
|
|
|
1111
1126
|
|
|
1112
1127
|
def _does_overlap(self, videoclip, audio_rec):
|
|
1113
1128
|
A1, A2 = audio_rec.get_start_time(), audio_rec.get_end_time()
|
|
1129
|
+
logger.debug('audio str stp: %s %s'%(A1,A2))
|
|
1114
1130
|
R1, R2 = videoclip.get_start_time(), videoclip.get_end_time()
|
|
1115
|
-
|
|
1116
|
-
|
|
1131
|
+
logger.debug('video str stp: %s %s'%(R1,R2))
|
|
1132
|
+
case1 = A1 < R1 < A2
|
|
1133
|
+
case2 = A1 < R2 < A2
|
|
1134
|
+
case3 = R1 < A1 < R2
|
|
1135
|
+
case4 = R1 < A2 < R2
|
|
1136
|
+
return case1 or case2 or case3 or case4
|
|
1117
1137
|
|
|
1118
1138
|
def shrink_gaps_between_takes(self, with_gap=CLUSTER_GAP):
|
|
1119
1139
|
"""
|