tictacsync 1.2.0b0__py3-none-any.whl → 1.4.0b0__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 +21 -243
- tictacsync/entry.py +14 -10
- tictacsync/load_fieldr_reaper.py +352 -0
- tictacsync/mamconf.py +18 -36
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +31 -75
- tictacsync/multi2polywav.py +0 -1
- tictacsync/new-sound-resolve.py +469 -0
- tictacsync/splitmix.py +87 -0
- tictacsync/timeline.py +28 -26
- tictacsync/yaltc.py +357 -29
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.0b0.dist-info}/METADATA +2 -3
- tictacsync-1.4.0b0.dist-info/RECORD +24 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.0b0.dist-info}/entry_points.txt +2 -1
- tictacsync-1.2.0b0.dist-info/RECORD +0 -19
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.0b0.dist-info}/LICENSE +0 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.0b0.dist-info}/WHEEL +0 -0
- {tictacsync-1.2.0b0.dist-info → tictacsync-1.4.0b0.dist-info}/top_level.txt +0 -0
tictacsync/timeline.py
CHANGED
|
@@ -55,6 +55,10 @@ def _pathname(tempfile_or_path) -> str:
|
|
|
55
55
|
else:
|
|
56
56
|
raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
|
|
57
57
|
|
|
58
|
+
def ffprobe_duration(f):
|
|
59
|
+
pr = ffmpeg.probe(f)
|
|
60
|
+
return pr['format']['duration']
|
|
61
|
+
|
|
58
62
|
# utility for printing groupby results
|
|
59
63
|
def print_grby(grby):
|
|
60
64
|
for key, keylist in grby:
|
|
@@ -711,6 +715,7 @@ class AudioStitcherVideoMerger:
|
|
|
711
715
|
sox.file_info.duration(_pathname(out_tf)))
|
|
712
716
|
logger.debug('video duration %.2f s'%
|
|
713
717
|
self.videoclip.get_duration())
|
|
718
|
+
logger.debug(f'video {self.videoclip}')
|
|
714
719
|
return out_tf
|
|
715
720
|
def _meta_wav_dest(p1, p2, p3):
|
|
716
721
|
"""
|
|
@@ -745,7 +750,8 @@ class AudioStitcherVideoMerger:
|
|
|
745
750
|
logger.debug('ISOdir %s'%ISOdir)
|
|
746
751
|
for name, mono_tmpfl, device in edited_audio_all_devices:
|
|
747
752
|
logger.debug(f'name:{name} mono_tmpfl:{mono_tmpfl} device:{pformat(device)}')
|
|
748
|
-
destination = ISOdir/(f'{video_stem_WO_suffix}_{name}.wav')
|
|
753
|
+
# destination = ISOdir/(f'{video_stem_WO_suffix}_{name}.wav')
|
|
754
|
+
destination = ISOdir/(f'{name}_{video_stem_WO_suffix}.wav')
|
|
749
755
|
mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
|
|
750
756
|
# if audio_only, self.ref_audio does not have itself as matching audio
|
|
751
757
|
if audio_only and device == self.ref_audio.device:
|
|
@@ -828,8 +834,8 @@ class AudioStitcherVideoMerger:
|
|
|
828
834
|
if device.ttc + 1 != device.tracks.ttc: # warn and quit
|
|
829
835
|
print('Error: TicTacCode channel detected is [gold1]%i[/gold1]'%
|
|
830
836
|
(device.ttc), end=' ')
|
|
831
|
-
print('and [gold1]%s[/gold1]
|
|
832
|
-
(
|
|
837
|
+
print('and file [gold1]%s[/gold1]\nfor the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
|
|
838
|
+
(device.folder/Path(yaltc.TRACKSFILE),
|
|
833
839
|
device.name, device.tracks.ttc-1))
|
|
834
840
|
print('Please correct the discrepancy and rerun. Quitting.')
|
|
835
841
|
sys.exit(1)
|
|
@@ -900,20 +906,6 @@ class AudioStitcherVideoMerger:
|
|
|
900
906
|
stereo_files = mic_stereo_files + new_stereo_files
|
|
901
907
|
return _sox_mix_files(stereo_files)
|
|
902
908
|
|
|
903
|
-
# def build_audio_and_write_merged_media(self, top_dir,
|
|
904
|
-
# dont_write_cam_folder, asked_ISOs, audio_REC_only):
|
|
905
|
-
# # simply bifurcates depending if ref media is video (prob 99%)
|
|
906
|
-
# # (then audio_REC_only == False)
|
|
907
|
-
# # or if ref media is audio (no camera detected, 1% of cases)
|
|
908
|
-
# # (with audio_REC_only == True)
|
|
909
|
-
# if not audio_REC_only:
|
|
910
|
-
# # almost always syncing audio to video clips
|
|
911
|
-
# self._build_audio_and_write_video(top_dir,
|
|
912
|
-
# dont_write_cam_folder, asked_ISOs)
|
|
913
|
-
# else:
|
|
914
|
-
# # rare
|
|
915
|
-
# self._build_and_write_audio(top_dir, anchor_dir)
|
|
916
|
-
|
|
917
909
|
def _build_and_write_audio(self, top_dir, anchor_dir=None):
|
|
918
910
|
"""
|
|
919
911
|
This is called when only audio recorders were found (no cam).
|
|
@@ -1048,7 +1040,10 @@ class AudioStitcherVideoMerger:
|
|
|
1048
1040
|
# MAM mode
|
|
1049
1041
|
logger.debug('MAM mode')
|
|
1050
1042
|
synced_clip_dir = Path(synced_root)/str(self.videoclip.AVpath.parent)[1:] # strip leading /
|
|
1051
|
-
|
|
1043
|
+
logger.debug(f'self.videoclip.AVpath.parent: {self.videoclip.AVpath.parent}')
|
|
1044
|
+
logger.debug(f'raw_root {raw_root}')
|
|
1045
|
+
# rel = self.videoclip.AVpath.parent.relative_to(raw_root).parent # removes ROLL01?
|
|
1046
|
+
rel = self.videoclip.AVpath.parent.relative_to(raw_root)
|
|
1052
1047
|
logger.debug(f'relative path {rel}')
|
|
1053
1048
|
synced_clip_dir = Path(synced_root)/Path(raw_root).name/rel
|
|
1054
1049
|
logger.debug(f'will save in {synced_clip_dir}')
|
|
@@ -1140,6 +1135,7 @@ class AudioStitcherVideoMerger:
|
|
|
1140
1135
|
# generates track name for later if asked_ISOs
|
|
1141
1136
|
# idx is from 0 to nchan-1 for this device
|
|
1142
1137
|
if dev.tracks == None:
|
|
1138
|
+
logger.debug('dev.tracks == None')
|
|
1143
1139
|
# no tracks.txt was found so use ascending numbers for name
|
|
1144
1140
|
chan_name = 'chan%s'%str(idx+1).zfill(2)
|
|
1145
1141
|
else:
|
|
@@ -1214,9 +1210,12 @@ class AudioStitcherVideoMerger:
|
|
|
1214
1210
|
"""
|
|
1215
1211
|
synced_clip_file = self.videoclip.final_synced_file
|
|
1216
1212
|
video_path = self.videoclip.AVpath
|
|
1213
|
+
logger.debug(f'original clip {video_path}')
|
|
1214
|
+
logger.debug(f'clip duration {ffprobe_duration(video_path)} s')
|
|
1217
1215
|
timecode = self.videoclip.get_start_timecode_string()
|
|
1218
1216
|
# self.videoclip.synced_audio = audio_path
|
|
1219
1217
|
audio_path = self.videoclip.synced_audio
|
|
1218
|
+
logger.debug(f'audio duration {sox.file_info.duration(_pathname(audio_path))}')
|
|
1220
1219
|
vid_only_handle = self._keep_VIDEO_only(video_path)
|
|
1221
1220
|
a_n = _pathname(audio_path)
|
|
1222
1221
|
v_n = str(vid_only_handle.name)
|
|
@@ -1230,8 +1229,8 @@ class AudioStitcherVideoMerger:
|
|
|
1230
1229
|
ffmpeg_args = (
|
|
1231
1230
|
ffmpeg
|
|
1232
1231
|
.input(v_n)
|
|
1233
|
-
|
|
1234
|
-
.output(out_n, vcodec='copy',
|
|
1232
|
+
.output(out_n, shortest=None, vcodec='copy',
|
|
1233
|
+
# .output(out_n, vcodec='copy',
|
|
1235
1234
|
timecode=timecode)
|
|
1236
1235
|
.global_args('-i', a_n, "-hide_banner")
|
|
1237
1236
|
.overwrite_output()
|
|
@@ -1242,9 +1241,8 @@ class AudioStitcherVideoMerger:
|
|
|
1242
1241
|
_, out = (
|
|
1243
1242
|
ffmpeg
|
|
1244
1243
|
.input(v_n)
|
|
1245
|
-
.output(out_n, vcodec='copy',
|
|
1246
|
-
|
|
1247
|
-
# metadata='reel_name=foo', not all container support gen MD
|
|
1244
|
+
# .output(out_n, vcodec='copy',
|
|
1245
|
+
.output(out_n, shortest=None, vcodec='copy',
|
|
1248
1246
|
timecode=timecode,
|
|
1249
1247
|
)
|
|
1250
1248
|
.global_args('-i', a_n, "-hide_banner")
|
|
@@ -1252,8 +1250,8 @@ class AudioStitcherVideoMerger:
|
|
|
1252
1250
|
.run(capture_stderr=True)
|
|
1253
1251
|
)
|
|
1254
1252
|
logger.debug('ffmpeg output')
|
|
1255
|
-
for l in out.decode("utf-8").split('\n'):
|
|
1256
|
-
logger.debug(l)
|
|
1253
|
+
# for l in out.decode("utf-8").split('\n'):
|
|
1254
|
+
# logger.debug(l)
|
|
1257
1255
|
except ffmpeg.Error as e:
|
|
1258
1256
|
print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
|
|
1259
1257
|
audio_path,
|
|
@@ -1263,6 +1261,9 @@ class AudioStitcherVideoMerger:
|
|
|
1263
1261
|
print(e)
|
|
1264
1262
|
print(e.stderr.decode('UTF-8'))
|
|
1265
1263
|
sys.exit(1)
|
|
1264
|
+
logger.debug(f'merged clip {out_n}')
|
|
1265
|
+
logger.debug(f'clip duration {ffprobe_duration(out_n)} s')
|
|
1266
|
+
|
|
1266
1267
|
|
|
1267
1268
|
class Matcher:
|
|
1268
1269
|
"""
|
|
@@ -1510,7 +1511,7 @@ class Matcher:
|
|
|
1510
1511
|
if synced_root == None:
|
|
1511
1512
|
# alongside mode
|
|
1512
1513
|
logger.debug('alongside mode')
|
|
1513
|
-
multicam_dir = Path('').joinpath(*parts_up_a_level)/MCCDIR
|
|
1514
|
+
multicam_dir = Path('/').joinpath(*parts_up_a_level)/MCCDIR
|
|
1514
1515
|
else:
|
|
1515
1516
|
# MAM mode
|
|
1516
1517
|
logger.debug('MAM mode')
|
|
@@ -1549,6 +1550,7 @@ class Matcher:
|
|
|
1549
1550
|
for r in cam_clips:
|
|
1550
1551
|
cam = r.device.name
|
|
1551
1552
|
clip_name = r.AVpath.name
|
|
1553
|
+
logger.debug(f'r.final_synced_file: {r.final_synced_file}')
|
|
1552
1554
|
dest = r.final_synced_file.replace(multicam_dir/cam/clip_name)
|
|
1553
1555
|
# leave a symlink behind
|
|
1554
1556
|
os.symlink(multicam_dir/cam/clip_name, r.final_synced_file)
|
tictacsync/yaltc.py
CHANGED
|
@@ -15,6 +15,7 @@ logging.config.dictConfig({
|
|
|
15
15
|
'disable_existing_loggers': True,
|
|
16
16
|
}) # for sox "output file already exists and will be overwritten on build"
|
|
17
17
|
from datetime import datetime, timezone, timedelta
|
|
18
|
+
from pprint import pformat
|
|
18
19
|
from collections import deque
|
|
19
20
|
from loguru import logger
|
|
20
21
|
from skimage.morphology import closing, erosion, remove_small_objects
|
|
@@ -33,6 +34,10 @@ TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
|
|
|
33
34
|
|
|
34
35
|
# see extract_seems_TicTacCode() for duration criterion values
|
|
35
36
|
|
|
37
|
+
TRACKSFILE = 'tracks.txt'
|
|
38
|
+
SILENT_TRACK_TOKENS = '-0n'
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
CACHING = True
|
|
37
42
|
DEL_TEMP = False
|
|
38
43
|
MAXDRIFT = 15e-3 # in sec, for end of clip
|
|
@@ -68,6 +73,7 @@ BPF_LOW_FRQ, BPF_HIGH_FRQ = (0.5*F1, 2*F2)
|
|
|
68
73
|
|
|
69
74
|
# utility for accessing pathnames
|
|
70
75
|
def _pathname(tempfile_or_path):
|
|
76
|
+
# always returns a str
|
|
71
77
|
if isinstance(tempfile_or_path, type('')):
|
|
72
78
|
return tempfile_or_path ################################################
|
|
73
79
|
if isinstance(tempfile_or_path, Path):
|
|
@@ -135,6 +141,34 @@ def to_precision(x,p):
|
|
|
135
141
|
|
|
136
142
|
return "".join(out)
|
|
137
143
|
|
|
144
|
+
def read_audio_data_from_file(file, n_channels):
|
|
145
|
+
"""
|
|
146
|
+
reads file and returns a numpy.array of shape (N1 channels, N2 samples)
|
|
147
|
+
where N1 >= 2 (minimaly solo track + TC)
|
|
148
|
+
"""
|
|
149
|
+
dryrun = (ffmpeg
|
|
150
|
+
.input(_pathname(file))
|
|
151
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
152
|
+
.get_args())
|
|
153
|
+
dryrun = ' '.join(dryrun)
|
|
154
|
+
logger.debug('using ffmpeg-python built args to pipe audio stream into numpy array:\nffmpeg %s'%dryrun)
|
|
155
|
+
try:
|
|
156
|
+
out, _ = (ffmpeg
|
|
157
|
+
# .input(str(path), ss=time_where, t=chunk_length)
|
|
158
|
+
# .input(str(self.AVpath))
|
|
159
|
+
.input(_pathname(file))
|
|
160
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
161
|
+
.global_args("-loglevel", "quiet")
|
|
162
|
+
.global_args("-nostats")
|
|
163
|
+
.global_args("-hide_banner")
|
|
164
|
+
.run(capture_stdout=True))
|
|
165
|
+
data = np.frombuffer(out, np.int16)
|
|
166
|
+
except ffmpeg.Error as e:
|
|
167
|
+
print('error',e.stderr)
|
|
168
|
+
# transform 1D interleaved channels to [chan1, chan2, chanN]
|
|
169
|
+
return data.reshape(int(len(data)/n_channels),n_channels).T
|
|
170
|
+
|
|
171
|
+
|
|
138
172
|
class Decoder:
|
|
139
173
|
"""
|
|
140
174
|
Object encapsulating DSP processes to demodulate TicTacCode track from audio
|
|
@@ -188,7 +222,6 @@ class Decoder:
|
|
|
188
222
|
self.do_plots = do_plots
|
|
189
223
|
self.clear_decoder()
|
|
190
224
|
|
|
191
|
-
|
|
192
225
|
def clear_decoder(self):
|
|
193
226
|
self.sound_data_extract = None
|
|
194
227
|
self.pulse_detection_level = None
|
|
@@ -524,6 +557,7 @@ class Decoder:
|
|
|
524
557
|
return int(round(freq_in_hertz))
|
|
525
558
|
|
|
526
559
|
|
|
560
|
+
|
|
527
561
|
class Recording:
|
|
528
562
|
"""
|
|
529
563
|
Wrapper for file objects, ffmpeg read operations and fprobe functions
|
|
@@ -688,35 +722,16 @@ class Recording:
|
|
|
688
722
|
return
|
|
689
723
|
logger.debug('ffprobe found: %s'%self.probe)
|
|
690
724
|
logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
|
|
691
|
-
self._read_audio_data()
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
.
|
|
697
|
-
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
698
|
-
.get_args())
|
|
699
|
-
dryrun = ' '.join(dryrun)
|
|
700
|
-
logger.debug('using ffmpeg-python built args to pipe audio stream into numpy array:\nffmpeg %s'%dryrun)
|
|
701
|
-
try:
|
|
702
|
-
out, _ = (ffmpeg
|
|
703
|
-
# .input(str(path), ss=time_where, t=chunk_length)
|
|
704
|
-
.input(str(self.AVpath))
|
|
705
|
-
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
706
|
-
.global_args("-loglevel", "quiet")
|
|
707
|
-
.global_args("-nostats")
|
|
708
|
-
.global_args("-hide_banner")
|
|
709
|
-
.run(capture_stdout=True))
|
|
710
|
-
data = np.frombuffer(out, np.int16)
|
|
711
|
-
except ffmpeg.Error as e:
|
|
712
|
-
print('error',e.stderr)
|
|
713
|
-
n_chan = self.get_audio_channels_nbr()
|
|
714
|
-
if n_chan == 1 and not self.is_video():
|
|
715
|
-
logger.error('file is sound mono')
|
|
725
|
+
# self._read_audio_data()
|
|
726
|
+
N = self.get_audio_channels_nbr()
|
|
727
|
+
data = read_audio_data_from_file(self.AVpath, n_channels=N)
|
|
728
|
+
if len(data) == 1 and not self.is_video():
|
|
729
|
+
print(f'file sound is mono ({self.AVpath}), bye.')
|
|
730
|
+
sys.exit(0)
|
|
716
731
|
if np.isclose(np.std(data), 0, rtol=1e-2):
|
|
717
|
-
logger.error("ffmpeg can't extract audio from %s"%
|
|
718
|
-
|
|
719
|
-
self.audio_data = data
|
|
732
|
+
logger.error("ffmpeg can't extract audio from %s"%file)
|
|
733
|
+
sys.exit(0)
|
|
734
|
+
self.audio_data = data
|
|
720
735
|
logger.debug('Recording.audio_data: %s of shape %s'%(self.audio_data,
|
|
721
736
|
self.audio_data.shape))
|
|
722
737
|
|
|
@@ -1029,6 +1044,319 @@ class Recording:
|
|
|
1029
1044
|
# self.valid_sound = self.AVpath
|
|
1030
1045
|
return start_UTC
|
|
1031
1046
|
|
|
1047
|
+
def _find_timed_tracks_(self, tracks_file) -> device_scanner.Tracks:
|
|
1048
|
+
"""
|
|
1049
|
+
Look for any ISO 8601 timestamp e.g.: 2007-04-05T14:30Z
|
|
1050
|
+
and choose the right chunk according to Recording.start_time
|
|
1051
|
+
"""
|
|
1052
|
+
file=open(tracks_file,"r")
|
|
1053
|
+
whole_txt = file.read()
|
|
1054
|
+
tracks_lines = []
|
|
1055
|
+
for l in whole_txt.splitlines():
|
|
1056
|
+
after_sharp = l.split('#')[0]
|
|
1057
|
+
if len(after_sharp) > 0:
|
|
1058
|
+
tracks_lines.append(after_sharp)
|
|
1059
|
+
logger.debug('file %s filtered lines:\n%s'%(tracks_file,
|
|
1060
|
+
pformat(tracks_lines)))
|
|
1061
|
+
def _seems_timestamp(line):
|
|
1062
|
+
# will validate format later with datetime.fromisoformat()
|
|
1063
|
+
m = re.match(r'ts=(.*)', line)
|
|
1064
|
+
# m = re.match(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z', line)
|
|
1065
|
+
if m != None:
|
|
1066
|
+
return m.groups()[0]
|
|
1067
|
+
else:
|
|
1068
|
+
return None
|
|
1069
|
+
chunks = []
|
|
1070
|
+
new_chunk = []
|
|
1071
|
+
timestamps_str = []
|
|
1072
|
+
for line in tracks_lines:
|
|
1073
|
+
timestamp_candidate = _seems_timestamp(line)
|
|
1074
|
+
if timestamp_candidate != None:
|
|
1075
|
+
logger.debug(f'timestamp: {line}')
|
|
1076
|
+
timestamps_str.append(timestamp_candidate)
|
|
1077
|
+
chunks.append(new_chunk)
|
|
1078
|
+
new_chunk = []
|
|
1079
|
+
else:
|
|
1080
|
+
new_chunk.append(line)
|
|
1081
|
+
chunks.append(new_chunk)
|
|
1082
|
+
logger.debug(f'chunks {chunks}, timestamps_str {timestamps_str}')
|
|
1083
|
+
str_frmt = '%Y-%m-%dT%H:%MZ'
|
|
1084
|
+
# from strings to datetime instances
|
|
1085
|
+
timestamps = []
|
|
1086
|
+
for dtstr in timestamps_str:
|
|
1087
|
+
try:
|
|
1088
|
+
ts = datetime.fromisoformat(dtstr)
|
|
1089
|
+
except:
|
|
1090
|
+
print(f'Error: in file {tracks_file},\ntimestamp {dtstr} is ill formatted, bye.')
|
|
1091
|
+
sys.exit(0)
|
|
1092
|
+
timestamps.append(ts)
|
|
1093
|
+
# timestamps = [datetime.strptime(dtstr, str_frmt, tzinfo=timezone.utc)
|
|
1094
|
+
# for dtstr in timestamps]
|
|
1095
|
+
logger.debug(f'datetime timestamps {timestamps}')
|
|
1096
|
+
# input validations, check order:
|
|
1097
|
+
if sorted(timestamps) != timestamps:
|
|
1098
|
+
print(f'Error in {tracks_file}\nSome timestamps are not in ascending order:\n')
|
|
1099
|
+
multi_lines = "\n".join(tracks_lines)
|
|
1100
|
+
print(f'{multi_lines}, Bye.')
|
|
1101
|
+
sys.exit(0)
|
|
1102
|
+
time_ranges = [t2-t1 for t1,t2 in zip(timestamps, timestamps[1:])]
|
|
1103
|
+
logger.debug(f'time_ranges {time_ranges} ')
|
|
1104
|
+
# check times between timestamps are realistic
|
|
1105
|
+
if timedelta(0) in time_ranges:
|
|
1106
|
+
print(f'Error in {tracks_file}\nSome timestamps are repeating:\n')
|
|
1107
|
+
multi_lines = "\n".join(tracks_lines)
|
|
1108
|
+
print(f'{multi_lines}, Bye.')
|
|
1109
|
+
sys.exit(0)
|
|
1110
|
+
if any([ dt < timedelta(minutes=2) for dt in time_ranges]):
|
|
1111
|
+
print(f'Warning in {tracks_file}\nSome timestamps are spaced by less than 2 minutes:\n')
|
|
1112
|
+
print("\n".join(tracks_lines))
|
|
1113
|
+
print(f'If this is an error, correct and rerun. For now will continue...')
|
|
1114
|
+
if any([ dt > timedelta(days=1) for dt in time_ranges]):
|
|
1115
|
+
print(f'Warning in {tracks_file}\nSome timestamps are spaced by more than 24 hrs:\n')
|
|
1116
|
+
print("\n".join(tracks_lines))
|
|
1117
|
+
print(f'If this is an error, correct and rerun. For now will continue...')
|
|
1118
|
+
# add 'infinite in future' to time stamps for time matching
|
|
1119
|
+
future = datetime.max
|
|
1120
|
+
future = future.replace(tzinfo=timezone.utc)
|
|
1121
|
+
timestamps.append(future)
|
|
1122
|
+
# zip it with chunks
|
|
1123
|
+
timed_chunks = list(zip(chunks,timestamps))
|
|
1124
|
+
logger.debug(f'timed_chunks\n{pformat(timed_chunks)} ')
|
|
1125
|
+
logger.debug(f'will find match with {self.start_time}')
|
|
1126
|
+
# for tch in timed_chunks:
|
|
1127
|
+
# print(tch[1], self.start_time)
|
|
1128
|
+
# print(tch[1] > self.start_time)
|
|
1129
|
+
idx = 0
|
|
1130
|
+
# while timed_chunks[idx][1] < self.start_time:
|
|
1131
|
+
# logger.debug(f'does {timed_chunks[idx][1]} < {self.start_time} ?')
|
|
1132
|
+
# idx += 1
|
|
1133
|
+
max_idx = len(timed_chunks) - 1
|
|
1134
|
+
while True:
|
|
1135
|
+
if timed_chunks[idx][1] > self.start_time or idx == max_idx:
|
|
1136
|
+
break
|
|
1137
|
+
idx += 1
|
|
1138
|
+
chunk_idx = idx
|
|
1139
|
+
logger.debug(f'chunk_idx {chunk_idx}')
|
|
1140
|
+
right_chunk = chunks[chunk_idx]
|
|
1141
|
+
logger.debug(f'found right chunk {right_chunk}')
|
|
1142
|
+
tracks_instance = self._parse_trx_lines(right_chunk, tracks_file)
|
|
1143
|
+
return tracks_instance
|
|
1144
|
+
|
|
1145
|
+
def _parse_trx_lines(self, tracks_lines_with_spaces, tracks_file):
|
|
1146
|
+
"""
|
|
1147
|
+
read track names for naming separated ISOs
|
|
1148
|
+
from tracks_file.
|
|
1149
|
+
|
|
1150
|
+
tokens looked for: mix; mix L; mix R; 0 and TC
|
|
1151
|
+
|
|
1152
|
+
repeating "mic*" pattern signals a stereo track
|
|
1153
|
+
and entries will correspondingly panned into
|
|
1154
|
+
a stero mix named mixL.wav and mixL.wav
|
|
1155
|
+
|
|
1156
|
+
mic L # spaces are ignored |
|
|
1157
|
+
mic R | stereo pair
|
|
1158
|
+
micB L
|
|
1159
|
+
micB R
|
|
1160
|
+
|
|
1161
|
+
Returns: a Tracks instance:
|
|
1162
|
+
# track numbers start at 1 for first track (as needed by sox)
|
|
1163
|
+
ttc: int # track number of TicTacCode signal
|
|
1164
|
+
unused: list # of unused tracks
|
|
1165
|
+
stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
|
|
1166
|
+
mix: list # of mixed tracks, if a pair, order is L than R
|
|
1167
|
+
others: list #of all other tags: (tag, track#) tuples
|
|
1168
|
+
rawtrx: list # list of strings read from file
|
|
1169
|
+
error_msg: str # 'None' if none
|
|
1170
|
+
e.g.: Tracks( ttc=2,
|
|
1171
|
+
unused=[],
|
|
1172
|
+
stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
|
|
1173
|
+
mix=[], others=[('clics', 1)],
|
|
1174
|
+
rawtrx=['clics', 'TC', 'micL', 'micR', 'mic2L;1000', 'mic2R;1000', 'mixL', 'mixR'],
|
|
1175
|
+
error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
|
|
1176
|
+
"""
|
|
1177
|
+
def _WOspace(chaine):
|
|
1178
|
+
ch = [c for c in chaine if c != ' ']
|
|
1179
|
+
return ''.join(ch)
|
|
1180
|
+
tracks_lines = [_WOspace(l) for l in tracks_lines_with_spaces if len(l) > 0 ]
|
|
1181
|
+
rawtrx = [l for l in tracks_lines_with_spaces if len(l) > 0 ]
|
|
1182
|
+
# add index with tuples, starting at 1
|
|
1183
|
+
logger.debug('tracks_lines whole: %s'%tracks_lines)
|
|
1184
|
+
def _detach_lag_value(line):
|
|
1185
|
+
# look for ";number" ending any line, returns a two-list
|
|
1186
|
+
splt = line.split(';')
|
|
1187
|
+
if len(splt) == 1:
|
|
1188
|
+
splt += [None]
|
|
1189
|
+
if len(splt) != 2:
|
|
1190
|
+
# error
|
|
1191
|
+
print('\nText error in %s, line %s has too many ";"'%(
|
|
1192
|
+
tracks_file, line))
|
|
1193
|
+
return splt
|
|
1194
|
+
tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
|
|
1195
|
+
in tracks_lines])
|
|
1196
|
+
lag_values = [e for e in lag_values] # from tuple to list
|
|
1197
|
+
# logger.debug('tracks_lines WO lag: %s'%tracks_lines)
|
|
1198
|
+
tracks_lines = [l.lower() for l in tracks_lines]
|
|
1199
|
+
logger.debug('tracks_lines lower case: %s'%tracks_lines)
|
|
1200
|
+
# print(lag_values)
|
|
1201
|
+
logger.debug('lag_values: %s'%lag_values)
|
|
1202
|
+
tagsWOl_r = [e[:-1] for e in tracks_lines] # skip last letter
|
|
1203
|
+
logger.debug('tags WO LR letter %s'%tagsWOl_r)
|
|
1204
|
+
# find idx of start of pairs
|
|
1205
|
+
# ['clics', 'TC', 'micL', 'micR', 'mic2L', 'mic2R', 'mixL', 'mixR']
|
|
1206
|
+
def _micOrmix(a,b):
|
|
1207
|
+
# test if same and mic mic or mix mix
|
|
1208
|
+
if len(a) == 0:
|
|
1209
|
+
return False
|
|
1210
|
+
return (a == b) and (a in 'micmix')
|
|
1211
|
+
pair_idx_start =[i for i, same in enumerate([_micOrmix(a,b) for a,b
|
|
1212
|
+
in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
|
|
1213
|
+
logger.debug('pair_idx_start %s'%pair_idx_start)
|
|
1214
|
+
def LR_OK(idx):
|
|
1215
|
+
# in tracks_lines, check if idx ends a LR pair
|
|
1216
|
+
# delays, if any, have been removed
|
|
1217
|
+
a = tracks_lines[idx][-1]
|
|
1218
|
+
b = tracks_lines[idx+1][-1]
|
|
1219
|
+
return a+b in ['lr', 'rl']
|
|
1220
|
+
LR_OKs = [LR_OK(p) for p in pair_idx_start]
|
|
1221
|
+
logger.debug('LR_OKs %s'%LR_OKs)
|
|
1222
|
+
if not all(LR_OKs):
|
|
1223
|
+
print('\nError in %s'%tracks_file)
|
|
1224
|
+
print('Some tracks are paired but not L and R: %s'%rawtrx)
|
|
1225
|
+
print('quitting...')
|
|
1226
|
+
quit()
|
|
1227
|
+
complete_pairs_idx = pair_idx_start + [i + 1 for i in pair_idx_start]
|
|
1228
|
+
singles = set(range(len(tracks_lines))).difference(complete_pairs_idx)
|
|
1229
|
+
logger.debug('complete_pairs_idx %s'%complete_pairs_idx)
|
|
1230
|
+
logger.debug('singles %s'%singles)
|
|
1231
|
+
singles_tag = [tracks_lines[i] for i in singles]
|
|
1232
|
+
logger.debug('singles_tag %s'%singles_tag)
|
|
1233
|
+
n_tc_token = sum([t == 'tc' for t in singles_tag])
|
|
1234
|
+
logger.debug('n tc tags %s'%n_tc_token)
|
|
1235
|
+
if n_tc_token == 0:
|
|
1236
|
+
print('\nError in %s'%tracks_file)
|
|
1237
|
+
print('with %s'%rawtrx)
|
|
1238
|
+
print('no TC track found, quitting...')
|
|
1239
|
+
quit()
|
|
1240
|
+
if n_tc_token > 1:
|
|
1241
|
+
print('\nError in %s'%tracks_file)
|
|
1242
|
+
print('with %s'%rawtrx)
|
|
1243
|
+
print('more than one TC track, quitting...')
|
|
1244
|
+
quit()
|
|
1245
|
+
output_tracks = device_scanner.Tracks(None,[],[],[],[],rawtrx,None,[])
|
|
1246
|
+
output_tracks.ttc = tracks_lines.index('tc') + 1 # 1st = 1
|
|
1247
|
+
logger.debug('ttc_chan %s'%output_tracks.ttc)
|
|
1248
|
+
zeroed = [i+1 for i, t in enumerate(tracks_lines) if t == '0']
|
|
1249
|
+
logger.debug('zeroed %s'%zeroed)
|
|
1250
|
+
output_tracks.unused = zeroed
|
|
1251
|
+
output_tracks.others = [(st, tracks_lines.index(st)+1) for st
|
|
1252
|
+
in singles_tag if st not
|
|
1253
|
+
in ['tc', 'monomix', '0']]
|
|
1254
|
+
logger.debug('output_tracks.others %s'%output_tracks.others)
|
|
1255
|
+
# check for monomix
|
|
1256
|
+
if 'monomix' in tracks_lines:
|
|
1257
|
+
output_tracks.mix = [tracks_lines.index('monomix')+1]
|
|
1258
|
+
else:
|
|
1259
|
+
output_tracks.mix = []
|
|
1260
|
+
# check for stereo mix
|
|
1261
|
+
def _findLR(i_first):
|
|
1262
|
+
# returns L R indexes (+1 for sox non zero based indexing)
|
|
1263
|
+
i_2nd = i_first + 1
|
|
1264
|
+
a = tracks_lines[i_first][-1] # l|r at end
|
|
1265
|
+
b = tracks_lines[i_2nd][-1] # l|r at end
|
|
1266
|
+
if a == 'l':
|
|
1267
|
+
if b == 'r':
|
|
1268
|
+
# sequence is mixL mixR
|
|
1269
|
+
return i_first+1, i_2nd+1
|
|
1270
|
+
else:
|
|
1271
|
+
print('\nError in %s'%tracks_file)
|
|
1272
|
+
print('with %s'%rawtrx)
|
|
1273
|
+
print('can not find stereo mix')
|
|
1274
|
+
quit()
|
|
1275
|
+
elif a == 'r':
|
|
1276
|
+
if b == 'l':
|
|
1277
|
+
# sequence is mixR mixL
|
|
1278
|
+
return i_2nd+1, i_first+1
|
|
1279
|
+
else:
|
|
1280
|
+
print('\nError in %s'%tracks_file)
|
|
1281
|
+
print('with %s'%rawtrx)
|
|
1282
|
+
print('can not find stereo mix')
|
|
1283
|
+
quit()
|
|
1284
|
+
logger.debug('for now, output_tracks.mix %s'%output_tracks.mix)
|
|
1285
|
+
mix_pair = [p for p in pair_idx_start if tracks_lines[p][1:] == 'mix']
|
|
1286
|
+
if len(mix_pair) == 1:
|
|
1287
|
+
# one stereo mix, remove it from other pairs
|
|
1288
|
+
i = mix_pair[0]
|
|
1289
|
+
LR_pair = _findLR(i)
|
|
1290
|
+
logger.debug('LR_pair %s'%str(LR_pair))
|
|
1291
|
+
pair_idx_start.remove(i)
|
|
1292
|
+
# consistency check
|
|
1293
|
+
if output_tracks.mix != []:
|
|
1294
|
+
# already found a mono mix above!
|
|
1295
|
+
print('\nError in %s'%tracks_file)
|
|
1296
|
+
print('with %s'%rawtrx)
|
|
1297
|
+
print('found a mono mix AND a stereo mix')
|
|
1298
|
+
quit()
|
|
1299
|
+
output_tracks.mix = LR_pair
|
|
1300
|
+
logger.debug('finally, output_tracks.mix %s'%str(output_tracks.mix))
|
|
1301
|
+
logger.debug('remaining pairs %s'%pair_idx_start)
|
|
1302
|
+
# those are stereo pairs
|
|
1303
|
+
stereo_pairs = []
|
|
1304
|
+
for first_in_pair in pair_idx_start:
|
|
1305
|
+
suffix = tracks_lines[first_in_pair][:-1]
|
|
1306
|
+
stereo_pairs.append((suffix, _findLR(first_in_pair)))
|
|
1307
|
+
logger.debug('stereo_pairs %s'%stereo_pairs)
|
|
1308
|
+
output_tracks.stereomics = stereo_pairs
|
|
1309
|
+
logger.debug('finished: %s'%output_tracks)
|
|
1310
|
+
return output_tracks
|
|
1311
|
+
|
|
1312
|
+
def load_track_info(self):
|
|
1313
|
+
"""
|
|
1314
|
+
If audio rec, look for eventual track names in TRACKSFILE file, stored inside the
|
|
1315
|
+
recorder folder alongside the audio files. If there, store a Tracks
|
|
1316
|
+
object into Recording.device.tracks .
|
|
1317
|
+
"""
|
|
1318
|
+
if self.is_video():
|
|
1319
|
+
return
|
|
1320
|
+
source_audio_folder = self.device.folder
|
|
1321
|
+
tracks_file = source_audio_folder/TRACKSFILE
|
|
1322
|
+
track_names = False
|
|
1323
|
+
# a_recording = [m for m in self.found_media_files
|
|
1324
|
+
# if m.device == device][0]
|
|
1325
|
+
# logger.debug('a_recording for device %s : %s'%(device, a_recording))
|
|
1326
|
+
nchan = self.get_audio_channels_nbr()
|
|
1327
|
+
# nchan = sox.file_info.channels(str(a_recording.path))
|
|
1328
|
+
if os.path.isfile(tracks_file):
|
|
1329
|
+
logger.debug('found file: %s'%(TRACKSFILE))
|
|
1330
|
+
tracks = self._find_timed_tracks_(tracks_file)
|
|
1331
|
+
if tracks.error_msg:
|
|
1332
|
+
print('\nError parsing [gold1]%s[/gold1] file: %s, quitting.\n'%
|
|
1333
|
+
(tracks_file, tracks.error_msg))
|
|
1334
|
+
sys.exit(1)
|
|
1335
|
+
logger.debug('parsed tracks %s'%tracks)
|
|
1336
|
+
ntracks = 2*len(tracks.stereomics)
|
|
1337
|
+
ntracks += len(tracks.mix)
|
|
1338
|
+
ntracks += len(tracks.unused)
|
|
1339
|
+
ntracks += len(tracks.others)
|
|
1340
|
+
ntracks += 1 # for ttc track
|
|
1341
|
+
logger.debug(' n chan: %i n tracks file: %i'%(nchan, ntracks))
|
|
1342
|
+
if ntracks != nchan:
|
|
1343
|
+
print('\nError parsing %s content'%tracks_file)
|
|
1344
|
+
print('incoherent number of tracks, %i vs %i quitting\n'%
|
|
1345
|
+
(nchan, ntracks))
|
|
1346
|
+
sys.exit(1)
|
|
1347
|
+
err_msg = tracks.error_msg
|
|
1348
|
+
if err_msg != None:
|
|
1349
|
+
print('\nError, quitting: in file %s, %s'%(tracks_file, err_msg))
|
|
1350
|
+
raise Exception
|
|
1351
|
+
else:
|
|
1352
|
+
self.device.tracks = tracks
|
|
1353
|
+
logger.debug('for rec %s'%self)
|
|
1354
|
+
logger.debug('tracks object: %s'%self.device.tracks)
|
|
1355
|
+
return
|
|
1356
|
+
else:
|
|
1357
|
+
logger.debug('no tracks.txt file found')
|
|
1358
|
+
return None
|
|
1359
|
+
|
|
1032
1360
|
def _ffprobe_audio_stream(self):
|
|
1033
1361
|
streams = self.probe['streams']
|
|
1034
1362
|
audio_streams = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0b0
|
|
4
4
|
Summary: commands for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
@@ -26,14 +26,13 @@ Requires-Dist: loguru >=0.6.0
|
|
|
26
26
|
Requires-Dist: matplotlib >=3.7.1
|
|
27
27
|
Requires-Dist: numpy >=1.24.3
|
|
28
28
|
Requires-Dist: rich >=10.12.0
|
|
29
|
-
Requires-Dist: lmfit
|
|
30
29
|
Requires-Dist: scikit-image
|
|
31
30
|
Requires-Dist: scipy >=1.10.1
|
|
32
31
|
Requires-Dist: platformdirs
|
|
33
32
|
|
|
34
33
|
# tictacsync
|
|
35
34
|
|
|
36
|
-
## Warning: this is at
|
|
35
|
+
## Warning: this is at beta stage
|
|
37
36
|
|
|
38
37
|
Unfinished sloppy code ahead, but should run without errors. Some functionalities are still missing. Don't run the code without parental supervision. Suggestions and enquiries are welcome via the [lists hosted on sourcehut](https://sr.ht/~proflutz/TicTacSync/lists).
|
|
39
38
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
+
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tictacsync/device_scanner.py,sha256=YxA3_0O1ZPE1ZuD-OgD-dWhtTWE-LamDhqXXyLo3IMw,26132
|
|
4
|
+
tictacsync/entry.py,sha256=pcGuS4_o0o5dREpcccx1_X3w14PeHdQi5z1Ikzmhpwk,16198
|
|
5
|
+
tictacsync/load_fieldr_reaper.py,sha256=tat0tTZpOyshzVvlqyu1r0u3cpf6TNgK-2C6xJk3_Fw,14708
|
|
6
|
+
tictacsync/mamconf.py,sha256=nfXTwabx-tJmBcpnDR4CRkFe9W4fudzfnbq_nHUg0qE,6424
|
|
7
|
+
tictacsync/mamdav.py,sha256=2we8tfIbJBtDMQdpZZVlCQ9hCQRMbKmV2aU3dDEUf2k,27457
|
|
8
|
+
tictacsync/mamreap.py,sha256=ej7Ap8nbVBCkfah2j5hrE7QBWuqL6Zm-OEsQpNK8mYg,21085
|
|
9
|
+
tictacsync/mamsync.py,sha256=mpoHUAuJWiZ1JfVCECiiSLH_HNdXNV1Z_VlUlJBlPcM,14565
|
|
10
|
+
tictacsync/multi2polywav.py,sha256=qJJhjwIgP1BCTpi2e0wfR95XlgZ2-EIqmefVh-jUBPc,7438
|
|
11
|
+
tictacsync/new-sound-resolve.py,sha256=si7NC_VE_2rNV9jR_Nz_YxK1c92JwzWw5BIWdvLdvAQ,18994
|
|
12
|
+
tictacsync/newmix.py,sha256=-zDxr6_O-rjyo1QfgktvHgwqy_un07eFI4zKi8nygIQ,19188
|
|
13
|
+
tictacsync/remergemix.py,sha256=bRyi1hyNcyM1rTkHh8DmSsIQjYpwPprxSyyVipnxz30,9909
|
|
14
|
+
tictacsync/remrgmx.py,sha256=FxaAo5qqynpj6O56ekQGD31YP6X2g-kEdwVpHSCoh4Q,4265
|
|
15
|
+
tictacsync/splitmix.py,sha256=dpTQYXXCYoertGOXPnMVCrh6xYh390YqmvOHK9hDg90,3148
|
|
16
|
+
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
17
|
+
tictacsync/timeline.py,sha256=ykmB8EfnprQZoEHXRYzriASNWZ7bHfkmQ2-TR6gxZ6Y,75985
|
|
18
|
+
tictacsync/yaltc.py,sha256=xrgL7qokP1A7B_VF4W_BZcC7q9APSmYpmtWH8_t3VWc,68003
|
|
19
|
+
tictacsync-1.4.0b0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
20
|
+
tictacsync-1.4.0b0.dist-info/METADATA,sha256=HMz1ALEb9soZkeMFu0XWdfSTPMhGq_0zVLDiNjZfu1E,5668
|
|
21
|
+
tictacsync-1.4.0b0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
22
|
+
tictacsync-1.4.0b0.dist-info/entry_points.txt,sha256=1ymCpiosJdolsqz4yPx8aDbBBePqVt4Zz2m228JkBZ4,211
|
|
23
|
+
tictacsync-1.4.0b0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
24
|
+
tictacsync-1.4.0b0.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
|
|
2
|
-
tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
tictacsync/device_scanner.py,sha256=ZF4ufRQGUv-6zMFW4fzNZpcFYGr8uUPKRK6jM9_b-P4,36435
|
|
4
|
-
tictacsync/entry.py,sha256=xwqziTH1M1m1-6ZUOVO_B7xht9zRZ22G_6ITBtRsEXE,15911
|
|
5
|
-
tictacsync/mamconf.py,sha256=FCgwsAadFzrhEUHa7IFSA0nBJV1SyKK55iSTAFR5Nwc,6974
|
|
6
|
-
tictacsync/mamsync.py,sha256=2XPhlw2P6IBUoNFUjBhpV4eKP3JqD501zy0iZFXXEjo,16696
|
|
7
|
-
tictacsync/multi2polywav.py,sha256=78W5yzKBfWy4nmD837VKwmcHAUZm10zRNe8ARUBJWCI,7439
|
|
8
|
-
tictacsync/newmix.py,sha256=-zDxr6_O-rjyo1QfgktvHgwqy_un07eFI4zKi8nygIQ,19188
|
|
9
|
-
tictacsync/remergemix.py,sha256=bRyi1hyNcyM1rTkHh8DmSsIQjYpwPprxSyyVipnxz30,9909
|
|
10
|
-
tictacsync/remrgmx.py,sha256=FxaAo5qqynpj6O56ekQGD31YP6X2g-kEdwVpHSCoh4Q,4265
|
|
11
|
-
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
12
|
-
tictacsync/timeline.py,sha256=wcj3n5nAWavJlrZ5Ia-WFmdI2lflU8V1uL13yhrUI7s,75836
|
|
13
|
-
tictacsync/yaltc.py,sha256=1XlOmdz-8ZUKF97A30MEkIA0F5oThqDgWPV8Ik3CHn4,53179
|
|
14
|
-
tictacsync-1.2.0b0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
15
|
-
tictacsync-1.2.0b0.dist-info/METADATA,sha256=SzpJguMsyz9ZRg6uhP7Csw-oa2URSuCNKjj4kWVj66E,5694
|
|
16
|
-
tictacsync-1.2.0b0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
17
|
-
tictacsync-1.2.0b0.dist-info/entry_points.txt,sha256=EZrwgJ0nlXDdVUDcMEijMnha5lb1YCkkg4DY9iz97BE,199
|
|
18
|
-
tictacsync-1.2.0b0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
19
|
-
tictacsync-1.2.0b0.dist-info/RECORD,,
|
|
File without changes
|