tictacsync 1.2.0b0__py3-none-any.whl → 1.4.5b0__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/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] for the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
832
- (device_scanner.TRACKSFILE,
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
- rel = self.videoclip.AVpath.parent.relative_to(raw_root).parent
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
- # .output(out_n, shortest=None, vcodec='copy',
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
- # .output(out_n, shortest=None, vcodec='copy',
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
- def _read_audio_data(self):
694
- # sets Recording.audio_data
695
- dryrun = (ffmpeg
696
- .input(str(self.AVpath))
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"%self.AVpath)
718
- # from 1D interleaved channels to [chan1, chan2, chanN]
719
- self.audio_data = data.reshape(int(len(data)/n_chan),n_chan).T
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.2.0b0
3
+ Version: 1.4.5b0
4
4
  Summary: commands for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -33,7 +33,7 @@ Requires-Dist: platformdirs
33
33
 
34
34
  # tictacsync
35
35
 
36
- ## Warning: this is at pre-alpha stage
36
+ ## Warning: this is at beta stage
37
37
 
38
38
  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
39
 
@@ -0,0 +1,16 @@
1
+ tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tictacsync/device_scanner.py,sha256=lYCMWSCeI0KNQclrRMOzeIERh2ME2gvBxV-q9Y2uou0,26175
3
+ tictacsync/entry.py,sha256=JmOx7B6d4LnGLWGuuMFpKV9GxkWhmAG4fdKayI7qzUA,15024
4
+ tictacsync/mamconf.py,sha256=nfXTwabx-tJmBcpnDR4CRkFe9W4fudzfnbq_nHUg0qE,6424
5
+ tictacsync/mamdav.py,sha256=2we8tfIbJBtDMQdpZZVlCQ9hCQRMbKmV2aU3dDEUf2k,27457
6
+ tictacsync/mamreap.py,sha256=ej7Ap8nbVBCkfah2j5hrE7QBWuqL6Zm-OEsQpNK8mYg,21085
7
+ tictacsync/mamsync.py,sha256=orwP-TzKdRTiTCoiM7BsQgVK1KtAIs3SpKe9K8ZWM_Q,13872
8
+ tictacsync/multi2polywav.py,sha256=OX72eDtanaax-lGc6JJXwOz9MaveNcYlgBfBijzR8oA,7583
9
+ tictacsync/timeline.py,sha256=ykmB8EfnprQZoEHXRYzriASNWZ7bHfkmQ2-TR6gxZ6Y,75985
10
+ tictacsync/yaltc.py,sha256=xrgL7qokP1A7B_VF4W_BZcC7q9APSmYpmtWH8_t3VWc,68003
11
+ tictacsync-1.4.5b0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
12
+ tictacsync-1.4.5b0.dist-info/METADATA,sha256=isx8DzvfowkLJgEPTX6y7Q3xyqEvH2Rt2yRHPgHvMVM,5689
13
+ tictacsync-1.4.5b0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
14
+ tictacsync-1.4.5b0.dist-info/entry_points.txt,sha256=0R8K6T0iUJGj87LDZ0vNO8pToshbkxrXZqTRgcjBlMk,244
15
+ tictacsync-1.4.5b0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
16
+ tictacsync-1.4.5b0.dist-info/RECORD,,
@@ -1,6 +1,7 @@
1
1
  [console_scripts]
2
2
  mamconf = tictacsync.mamconf:main
3
+ mamdav = tictacsync.mamdav:called_from_cli
4
+ mamreap = tictacsync.mamreap:main
3
5
  mamsync = tictacsync.mamsync:main
4
6
  multi2polywav = tictacsync.multi2polywav:main
5
- newmix = tictacsync.newmix:main
6
7
  tictacsync = tictacsync.entry:main