tictacsync 0.98a0__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 +62 -268
- tictacsync/entry.py +57 -166
- tictacsync/load_fieldr_reaper.py +352 -0
- tictacsync/mamconf.py +157 -0
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +343 -0
- tictacsync/multi2polywav.py +4 -3
- tictacsync/new-sound-resolve.py +469 -0
- tictacsync/newmix.py +483 -0
- tictacsync/remrgmx.py +6 -10
- tictacsync/splitmix.py +87 -0
- tictacsync/timeline.py +154 -98
- tictacsync/yaltc.py +359 -31
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/METADATA +5 -6
- tictacsync-1.4.0b0.dist-info/RECORD +24 -0
- tictacsync-1.4.0b0.dist-info/entry_points.txt +7 -0
- tictacsync-0.98a0.dist-info/RECORD +0 -16
- tictacsync-0.98a0.dist-info/entry_points.txt +0 -4
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/LICENSE +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/WHEEL +0 -0
- {tictacsync-0.98a0.dist-info → tictacsync-1.4.0b0.dist-info}/top_level.txt +0 -0
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
|
|
@@ -561,7 +595,7 @@ class Recording:
|
|
|
561
595
|
sync_position : int
|
|
562
596
|
position of first detected syn pulse
|
|
563
597
|
|
|
564
|
-
|
|
598
|
+
is_audio_reference : bool (True for ref rec only)
|
|
565
599
|
in multi recorders set-ups, user decides if a sound-only recording
|
|
566
600
|
is the time reference for all other audio recordings. By
|
|
567
601
|
default any video recording is the time reference for other audio,
|
|
@@ -647,7 +681,7 @@ class Recording:
|
|
|
647
681
|
self.decoder = None
|
|
648
682
|
self.probe = None
|
|
649
683
|
self.TicTacCode_channel = None
|
|
650
|
-
self.
|
|
684
|
+
self.is_audio_reference = False
|
|
651
685
|
self.device_relative_speed = 1.0
|
|
652
686
|
# self.valid_sound = None
|
|
653
687
|
self.final_synced_file = None
|
|
@@ -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,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tictacsync
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.4.0b0
|
|
4
|
+
Summary: commands for syncing audio video recordings
|
|
5
5
|
Home-page: https://tictacsync.org/
|
|
6
6
|
Author: Raymond Lutz
|
|
7
7
|
Author-email: lutzrayblog@mac.com
|
|
8
|
-
Classifier: Development Status ::
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: End Users/Desktop
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -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
|
|
|
@@ -68,7 +67,7 @@ Then pip install the syncing program:
|
|
|
68
67
|
This should install python dependencies _and_ the `tictacsync` command.
|
|
69
68
|
## Usage
|
|
70
69
|
|
|
71
|
-
Download multiple sample files [here](https://nuage.lutz.quebec/s/
|
|
70
|
+
Download multiple sample files [here](https://nuage.lutz.quebec/s/4jw4xgqysLPS8EQ/download/dailies1_3.zip) (700+ MB, sorry) unzip and run:
|
|
72
71
|
|
|
73
72
|
> tictacsync dailies/loose
|
|
74
73
|
The program `tictacsync` will recursively scan the directory given as argument, find all audio that coincide with any video and merge them into a subfolder named `SyncedMedia`. When the argument is an unique media file (not a directory), no syncing will occur but the decoded starting time will be printed to stdout:
|
|
@@ -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,16 +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=6XTO4N0ipJ3HNa1I0aSKkTNIgPk_BtCoDDjwCVhOjpI,35446
|
|
4
|
-
tictacsync/entry.py,sha256=KOhB8ivgme3GPpWShad2adS1lvIU9v0yMFY0CELwAmM,20673
|
|
5
|
-
tictacsync/multi2polywav.py,sha256=-nX5reZo6QNxFYdhsliHTs8bTfMjPzcONDT8vJbkZUA,7291
|
|
6
|
-
tictacsync/remergemix.py,sha256=bRyi1hyNcyM1rTkHh8DmSsIQjYpwPprxSyyVipnxz30,9909
|
|
7
|
-
tictacsync/remrgmx.py,sha256=nGuNg55BtXpKTpklwZqunsgVNi-1h-_22OFSnGk7K8k,4340
|
|
8
|
-
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
9
|
-
tictacsync/timeline.py,sha256=2CkTzMDiazYlBq2F1fhM2w4r6CIgmpQk1L2ZvAYRcnA,72532
|
|
10
|
-
tictacsync/yaltc.py,sha256=xbMucI19UJKrEvIzyfpOsi3piSWzqM1gKgooeT9DV8g,53167
|
|
11
|
-
tictacsync-0.98a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
12
|
-
tictacsync-0.98a0.dist-info/METADATA,sha256=PuGAcwkwrbbkh8wwudBmw4s5DU0kbgKsXJq27dYSXzY,5693
|
|
13
|
-
tictacsync-0.98a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
14
|
-
tictacsync-0.98a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
15
|
-
tictacsync-0.98a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
16
|
-
tictacsync-0.98a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|