tictacsync 0.91a0__py3-none-any.whl → 0.96a0__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.

@@ -58,7 +58,6 @@ def print_grby(grby):
58
58
  for e in keylist:
59
59
  print(' ', e)
60
60
 
61
-
62
61
  @dataclass
63
62
  class Tracks:
64
63
  # track numbers start at 1 for first track (as needed by sox)
@@ -69,6 +68,7 @@ class Tracks:
69
68
  others: list #of all other tags: (tag, track#) tuples
70
69
  rawtrx: list # list of strings read from file
71
70
  error_msg: str # 'None' if none
71
+ lag_values: list # list of lag in ms, entry is None if not specified.
72
72
 
73
73
  @dataclass
74
74
  class Device:
@@ -77,8 +77,9 @@ class Device:
77
77
  name: str
78
78
  dev_type: str # CAM or REC
79
79
  n_chan: int
80
- ttc: int
80
+ ttc: int # zero based index?
81
81
  tracks: Tracks
82
+ sampling_freq: float # fps if cam
82
83
  def __hash__(self):
83
84
  return self.UID
84
85
  def __eq__(self, other):
@@ -93,9 +94,9 @@ class Media:
93
94
 
94
95
  def media_at_path(input_structure, p):
95
96
  # return Media object for mediafile using ffprobe
96
- dev_UID, dt = get_device_ffprobe_UID(p)
97
+ dev_UID, dt, sf = get_device_ffprobe_UID(p)
97
98
  dev_name = None
98
- logger.debug('ffprobe dev_UID:%s dt:%s'%(dev_UID, dt))
99
+ logger.debug('ffprobe dev_UID:%s dt:%s sf:%s'%(dev_UID, dt,sf))
99
100
  if input_structure == 'folder_is_device':
100
101
  dev_name = p.parent.name
101
102
  if dev_UID is None:
@@ -109,7 +110,13 @@ def media_at_path(input_structure, p):
109
110
  if stream['codec_type']=='audio'
110
111
  ]
111
112
  if len(audio_streams) > 1:
112
- raise Exception('ffprobe gave multiple audio streams?')
113
+ print('for [gold1]%s[/gold1], ffprobe gave multiple audio streams, quitting.'%p)
114
+ quit()
115
+ # raise Exception('ffprobe gave multiple audio streams?')
116
+ if len(audio_streams) == 0:
117
+ print('ffprobe gave no audio stream for [gold1]%s[/gold1], quitting.'%p)
118
+ quit()
119
+ # raise Exception('ffprobe gave no audio stream for %s, quitting'%p)
113
120
  audio_str = audio_streams[0]
114
121
  n = audio_str['channels']
115
122
  # pprint(ffmpeg.probe(p))
@@ -117,7 +124,7 @@ def media_at_path(input_structure, p):
117
124
  n = sox.file_info.channels(_pathname(p)) # eg 2
118
125
  logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
119
126
  device = Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
120
- n_chan=n, ttc=None, tracks=None)
127
+ n_chan=n, ttc=None, sampling_freq=sf, tracks=None)
121
128
  logger.debug('for path: %s, device:%s'%(p,device))
122
129
  return Media(p, device)
123
130
 
@@ -131,7 +138,7 @@ def get_device_ffprobe_UID(file):
131
138
  Device UIDs are used later in Montage._get_concatenated_audiofile_for()
132
139
  for grouping each audio or video clip along its own timeline track.
133
140
 
134
- Returns a tuple: (UID, CAM|REC)
141
+ Returns a tuple: (UID, CAM|REC, sampling_freq)
135
142
 
136
143
  If an ffmpeg.Error occurs, returns (None, None)
137
144
  if no UID is found, but device type is identified, returns (None, CAM|REC)
@@ -146,15 +153,30 @@ def get_device_ffprobe_UID(file):
146
153
  print(e.stderr, file)
147
154
  return None, None #-----------------------------------------------------
148
155
  # fall back to folder name
156
+ logger.debug('ffprobe %s'%probe)
149
157
  streams = probe['streams']
158
+ video_streams = [st for st in streams if st['codec_type'] == 'video']
159
+ audio_streams = [st for st in streams if st['codec_type'] == 'audio']
160
+ if len(video_streams) > 1:
161
+ print('more than one video stream for %s... quitting'%file)
162
+ quit()
163
+ if len(audio_streams) != 1:
164
+ print('nbr of audio stream for %s not 1 ... quitting'%file)
165
+ quit()
150
166
  codecs = [stream['codec_type'] for stream in streams]
151
- device_type = 'CAM' if 'video' in codecs else 'REC'
167
+ # cameras have two streams: video AND audio
168
+ device_type = 'CAM' if len(video_streams) == 1 else 'REC'
169
+ if device_type == 'CAM':
170
+ sampling_freq = eval(video_streams[0]['r_frame_rate'])
171
+ else:
172
+ sampling_freq = float(audio_streams[0]['sample_rate'])
152
173
  format_dict = probe['format'] # all files should have this
153
174
  if 'tags' in format_dict:
154
175
  probe_string = pformat(format_dict['tags'])
155
176
  probe_lines = [l for l in probe_string.split('\n')
156
177
  if '_time' not in l
157
178
  and 'time_' not in l
179
+ and 'location' not in l
158
180
  and 'date' not in l ]
159
181
  # this removes any metadata related to the file
160
182
  # but keeps metadata related to the device
@@ -165,7 +187,7 @@ def get_device_ffprobe_UID(file):
165
187
  if UID == 0: # empty probe_lines from Audacity ?!?
166
188
  UID = None
167
189
  logger.debug('ffprobe_UID is: %s'%UID)
168
- return UID, device_type
190
+ return UID, device_type, sampling_freq
169
191
 
170
192
  class Scanner:
171
193
  """
@@ -302,12 +324,12 @@ class Scanner:
302
324
  audio_devices = [d for d in devices if d.dev_type == 'REC']
303
325
  for recorder in audio_devices:
304
326
  recorder.tracks = self._get_tracks_from_file(recorder)
327
+ if recorder.tracks:
328
+ if not all([lv == None for lv in recorder.tracks.lag_values]):
329
+ logger.debug('%s has lag_values %s'%(
330
+ recorder.name, recorder.tracks.lag_values))
305
331
  no_name_devices = [m.device for m in self.found_media_files
306
332
  if not m.device.name]
307
- # if no_name_devices:
308
- # pprint_no_name = pformat([(d, self.get_media_for_device(d)) for d in no_name_devices])
309
- # print('those are anon devices%s\n'%pprint_no_name)
310
- # logger.debug('those media have anon device%s'%no_name_devices)
311
333
  for anon_dev in no_name_devices:
312
334
  medias = self.get_media_for_device(anon_dev)
313
335
  guess_name = _try_name(medias)
@@ -321,7 +343,7 @@ class Scanner:
321
343
 
322
344
  def _get_tracks_from_file(self, device) -> Tracks:
323
345
  """
324
- Look for track names in TRACKSFN file, possibly stored inside the
346
+ Look for eventual track names in TRACKSFN file, stored inside the
325
347
  recorder folder alongside the audio files. If there, returns a Tracks
326
348
  object, if not returns None.
327
349
  """
@@ -477,16 +499,16 @@ class Scanner:
477
499
  read track names for naming separated ISOs
478
500
  from tracks_file.
479
501
 
480
- repeting prefixes signals a stereo track
481
- and entries will correspondingly panned into
482
- a stero mix named mixL.wav and mixR.wav
502
+ tokens looked for: mix; L mix; R mix; 0 and TC
483
503
 
484
- mic1 L # space is optionnal and will be removed
485
- mic1 R
486
- mic2 L
487
- mic2 R
504
+ repeating "mic*" pattern signals a stereo track
505
+ and entries will correspondingly panned into
506
+ a stero mix named Lmix.wav and Rmix.wav
488
507
 
489
- mixL
508
+ L mic # spaces are ignored |
509
+ R mic | stereo pair
510
+ L micB
511
+ R micB
490
512
 
491
513
  Returns: a Tracks instance:
492
514
  # track numbers start at 1 for first track (as needed by sox)
@@ -497,130 +519,158 @@ class Scanner:
497
519
  others: list #of all other tags: (tag, track#) tuples
498
520
  rawtrx: list # list of strings read from file
499
521
  error_msg: str # 'None' if none
522
+ e.g.: Tracks( ttc=2,
523
+ unused=[],
524
+ stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
525
+ mix=[], others=[('clics', 1)],
526
+ rawtrx=['clics', 'TC', 'L mic', 'R mic', 'L mic2;1000', 'R mic2;1000', 'Lmix', 'Rmix'],
527
+ error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
500
528
  """
501
529
  def _WOspace(chaine):
502
530
  ch = [c for c in chaine if c != ' ']
503
531
  return ''.join(ch)
504
532
  def _WO_LR(chaine):
505
- ch = [c for c in chaine if c not in 'lr']
533
+ ch = [c for c in chaine if c not in 'LR']
506
534
  return ''.join(ch)
507
535
  def _seemsStereoMic(tag):
508
536
  # is tag likely a stereo pair tag?
509
- # should start with 'mic' and end with 'l' or 'r'
510
- return tag[:3]=='mic' and tag[-1] in 'lr'
537
+ # should ends with 'mic' and starts with 'l' or 'r'
538
+ return tag[1:4]=='mic' and tag[0] in 'lr'
511
539
  file=open(tracks_file,"r")
512
540
  whole_txt = file.read()
513
541
  logger.debug('all_lines:\n%s'%whole_txt)
514
- tracks_lines = [l.split('#')[0] for l in whole_txt.splitlines() if len(l) > 0 ]
515
- tracks_lines = [_WOspace(l).lower() for l in tracks_lines if len(l) > 0 ]
516
- rawtrx = tracks_lines
517
- tracks_lines = [(t,ix+1) for ix,t in enumerate(tracks_lines)]
518
- # tracks_lines = [l for l in tracks_lines if l not in ['ttc', '0']]
519
- # track_names = [l.split()[0] for l in tracks_lines if l != 'ttc']
520
- logger.debug('tracks_lines: %s'%tracks_lines)
521
- # first check for stereo mic pairs (could be more than one pair):
522
- spairs = [e for e in tracks_lines if _seemsStereoMic(e[0])]
523
- # spairs is stereo pairs candidates
524
- msg = 'Confusing stereo pair tags: %s'%' '.join([e[0]
525
- for e in spairs])
526
- error_output_stereo = Tracks(None,[],[],[],[],[],msg)
527
- if len(spairs)%2 == 1: # not pairs?, quit.
528
- return error_output_stereo
529
- logger.debug('_seemsStereoM: %s'%spairs)
530
- output_tracks = Tracks(None,[],[],[],[],rawtrx,None)
531
- def _LR(p):
532
- # p = (('mic1l', 1), ('mic1r', 2))
533
- # check if L then R
534
- p1, p2 = p
535
- return p1[0][-1] == 'l' and p2[0][-1] == 'r'
536
- if spairs:
537
- even_idxes = range(0,len(spairs),2)
538
- paired = [(spairs[i], spairs[i+1]) for i in even_idxes]
539
- # eg [(('mic1l', 1), ('mic1r', 2)), (('mic2l', 3), ('mic2r', 4))]
540
- def _mic_same(p):
541
- # p = (('mic1l', 1), ('mic1r', 2))
542
- # check if mic1 == mic1
543
- p1, p2 = p
544
- return _WO_LR(p1[0]) == _WO_LR(p2[0])
545
- mic_prefix_OK = all([_mic_same(p) for p in paired])
546
- logger.debug('mic_prefix_OK: %s'%mic_prefix_OK)
547
- if not mic_prefix_OK:
548
- return error_output_stereo
549
- mic_LR_OK = all([_LR(p) for p in paired])
550
- logger.debug('mic_LR_OK %s'%mic_LR_OK)
551
- if not mic_LR_OK:
552
- return error_output_stereo
553
- def _stereo_mic_pref_chan(p):
554
- # p = (('mic1l', 1), ('mic1r', 2))
555
- # returns ('mic1', (1,2))
556
- first, second = p
557
- mic_prefix = _WO_LR(first[0])
558
- return (mic_prefix, (first[1], second[1]) )
559
- grouped_stereo_mic_channels = [_stereo_mic_pref_chan(p) for p
560
- in paired]
561
- logger.debug('grouped_stereo_mic_channels: %s'%
562
- grouped_stereo_mic_channels)
563
- output_tracks.stereomics = grouped_stereo_mic_channels
564
- [tracks_lines.remove(e) for e in spairs]
565
- logger.debug('stereo mic pairs done, continue with %s'%tracks_lines)
566
- # second, check for stereo mix down (one mixL mixR pair)
567
- def _seemsStereoMix(tag):
568
- # is tag likely a stereo pair tag?
569
- # should start with 'mic' and end with 'l' or 'r'
570
- return tag[:3]=='mix' and tag[-1] in 'lr'
571
- stereo_mix_tags = [e for e in tracks_lines if _seemsStereoMix(e[0])]
572
- logger.debug('stereo_mix_tags: %s'%stereo_mix_tags)
573
- str_msg = 'Confusing mix pair tags: %s L should appear before R'%' '.join([e[0]
574
- for e in stereo_mix_tags])
575
- # error_output = Tracks(None,[],[],[],[],msg)
576
- def _error_Track(msg):
577
- return Tracks(None,[],[],[],[],[],msg)
578
- if stereo_mix_tags:
579
- if len(stereo_mix_tags) != 2:
580
- return _error_Track(str_msg)
581
- mix_LR_OK = _LR(stereo_mix_tags)
582
- logger.debug('mix_LR_OK %s'%mix_LR_OK)
583
- if not mix_LR_OK:
584
- return _error_Track(str_msg)
585
- stereo_mix_channels = [t[1] for t in stereo_mix_tags]
586
- output_tracks.mix = stereo_mix_channels
587
- logger.debug('output_tracks.mix %s'%stereo_mix_channels)
588
- [tracks_lines.remove(e) for e in stereo_mix_tags]
589
- logger.debug('stereo mix done, will continue with %s'%tracks_lines)
590
- # third, check for a mono mix
591
- mono_mix_tags = [e for e in tracks_lines if e[0] == 'mix']
592
- if not output_tracks.mix and mono_mix_tags:
593
- logger.debug('mono_mix_tags: %s'%mono_mix_tags)
594
- if len(mono_mix_tags) != 1:
595
- return _error_Track('more than one "mix" token')
596
- output_tracks.mix = [mono_mix_tags[0][1]]
597
- [tracks_lines.remove(e) for e in mono_mix_tags]
598
- logger.debug('mono mix done, will continue with %s'%tracks_lines)
599
- # fourth, look for 'ttc'
600
- ttc_chan = [idx for tag, idx in tracks_lines if tag == 'ttc']
601
- if ttc_chan:
602
- if len(ttc_chan) > 1:
603
- return _error_Track('more than one "ttc" token')
604
- output_tracks.ttc = ttc_chan[0]
605
- tracks_lines.remove(('ttc', ttc_chan[0]))
606
- else:
607
- return _error_Track('no "ttc" token')
608
- # fifth, check for '0'
609
- logger.debug('ttc done, will continue with %s'%tracks_lines)
610
- zeroed = [idx for tag, idx in tracks_lines if tag == '0']
542
+ tracks_lines_wspaces = [l.split('#')[0] for l in whole_txt.splitlines()
543
+ if len(l) > 0 ]
544
+ tracks_lines = [_WOspace(l) for l in tracks_lines_wspaces if len(l) > 0 ]
545
+ rawtrx = [l for l in tracks_lines_wspaces if len(l) > 0 ]
546
+ # add index with tuples, starting at 1
547
+ logger.debug('tracks_lines whole: %s'%tracks_lines)
548
+ def _detach_lag_value(line):
549
+ # look for ";number" ending any line, returns a two-list
550
+ splt = line.split(';')
551
+ if len(splt) == 1:
552
+ splt += [None]
553
+ if len(splt) != 2:
554
+ # error
555
+ print('Text error in %s, line %s has too many ";"'%(
556
+ tracks_file, line))
557
+ return splt
558
+ tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
559
+ in tracks_lines])
560
+ lag_values = [e for e in lag_values] # from tuple to list
561
+ # logger.debug('tracks_lines WO lag: %s'%tracks_lines)
562
+ tracks_lines = [l.lower() for l in tracks_lines]
563
+ logger.debug('tracks_lines lower case: %s'%tracks_lines)
564
+ # print(lag_values)
565
+ logger.debug('lag_values: %s'%lag_values)
566
+ tagsWOl_r = [e[1:] for e in tracks_lines] # skip first letter
567
+ logger.debug('tags WO 1st letter %s'%tagsWOl_r)
568
+ # find idx of start of pairs
569
+ # ['clics', 'TC', 'Lmic', 'Rmic', 'Lmic2', 'Lmic2', 'Lmix', 'Rmix']
570
+ # ^ ^ ^
571
+ def _micOrmix(a,b):
572
+ # test if same and mic mic or mix mix
573
+ if len(a) == 0:
574
+ return False
575
+ return (a == b) and (a in 'micmix')
576
+ pair_idx_start =[i for i, same in enumerate([_micOrmix(a,b) for a,b
577
+ in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
578
+ logger.debug('pair_idx_start %s'%pair_idx_start)
579
+ def LR_OK(idx):
580
+ # in tracks_lines, check if idx start a LR pair
581
+ a = tracks_lines[idx][0]
582
+ b = tracks_lines[idx+1][0]
583
+ return a+b in ['lr', 'rl']
584
+ LR_OKs = [LR_OK(p) for p in pair_idx_start]
585
+ logger.debug('LR_OKs %s'%LR_OKs)
586
+ if not all(LR_OKs):
587
+ print('Error in %s'%tracks_file)
588
+ print('Some tracks are paired but not L and R: %s'%rawtrx)
589
+ print('quitting...')
590
+ quit()
591
+ complete_pairs_idx = pair_idx_start + [i + 1 for i in pair_idx_start]
592
+ singles = set(range(len(tracks_lines))).difference(complete_pairs_idx)
593
+ logger.debug('complete_pairs_idx %s'%complete_pairs_idx)
594
+ logger.debug('singles %s'%singles)
595
+ singles_tag = [tracks_lines[i] for i in singles]
596
+ logger.debug('singles_tag %s'%singles_tag)
597
+ n_tc_token = sum([t == 'tc' for t in singles_tag])
598
+ logger.debug('n tc tags %s'%n_tc_token)
599
+ if n_tc_token == 0:
600
+ print('Error in %s'%tracks_file)
601
+ print('with %s'%rawtrx)
602
+ print('no TC track found, quitting...')
603
+ quit()
604
+ if n_tc_token > 1:
605
+ print('Error in %s'%tracks_file)
606
+ print('with %s'%rawtrx)
607
+ print('more than one TC track, quitting...')
608
+ quit()
609
+ output_tracks = Tracks(None,[],[],[],[],rawtrx,None,[])
610
+ output_tracks.ttc = tracks_lines.index('tc') + 1 # 1st = 1
611
+ logger.debug('ttc_chan %s'%output_tracks.ttc)
612
+ zeroed = [i+1 for i, t in enumerate(tracks_lines) if t == '0']
611
613
  logger.debug('zeroed %s'%zeroed)
612
- if zeroed:
613
- output_tracks.unused = zeroed
614
- [tracks_lines.remove(('0',i)) for i in zeroed]
615
- else:
616
- output_tracks.unused = []
617
- # sixth, check for 'others'
618
- logger.debug('0s done, will continue with %s'%tracks_lines)
619
- if tracks_lines:
620
- output_tracks.others = tracks_lines
614
+ output_tracks.unused = zeroed
615
+ output_tracks.others = [(st, tracks_lines.index(st)+1) for st
616
+ in singles_tag if st not
617
+ in ['tc', 'monomix', '0']]
618
+ logger.debug('output_tracks.others %s'%output_tracks.others)
619
+ # check for monomix
620
+ if 'monomix' in tracks_lines:
621
+ output_tracks.mix = [tracks_lines.index('monomix')+1]
621
622
  else:
622
- output_tracks.others = []
623
- logger.debug('Tracks %s'%output_tracks)
623
+ output_tracks.mix = []
624
+ # check for stereo mix
625
+ def _findLR(i_first):
626
+ # returns L R indexes (+1 for sox non zero based indexing)
627
+ i_2nd = i_first + 1
628
+ a = tracks_lines[i_first][0]
629
+ b = tracks_lines[i_2nd][0]
630
+ if a == 'l':
631
+ if b == 'r':
632
+ # sequence is Lmix Rmix
633
+ return i_first+1, i_2nd+1
634
+ else:
635
+ print('Error in %s'%tracks_file)
636
+ print('with %s'%rawtrx)
637
+ print('can not find stereo mix')
638
+ quit()
639
+ elif a == 'r':
640
+ if b == 'l':
641
+ # sequence is Rmix Lmix
642
+ return i_2nd+1, i_first+1
643
+ else:
644
+ print('Error in %s'%tracks_file)
645
+ print('with %s'%rawtrx)
646
+ print('can not find stereo mix')
647
+ quit()
648
+ logger.debug('for now, output_tracks.mix %s'%output_tracks.mix)
649
+ mix_pair = [p for p in pair_idx_start if tracks_lines[p][1:] == 'mix']
650
+ if len(mix_pair) == 1:
651
+ # one stereo mix, remove it from other pairs
652
+ i = mix_pair[0]
653
+ LR_pair = _findLR(i)
654
+ logger.debug('LR_pair %s'%str(LR_pair))
655
+ pair_idx_start.remove(i)
656
+ # consistency check
657
+ if output_tracks.mix != []:
658
+ # already found a mono mix above!
659
+ print('Error in %s'%tracks_file)
660
+ print('with %s'%rawtrx)
661
+ print('found a mono mix AND a stereo mix')
662
+ quit()
663
+ output_tracks.mix = LR_pair
664
+ logger.debug('finally, output_tracks.mix %s'%str(output_tracks.mix))
665
+ logger.debug('remaining pairs %s'%pair_idx_start)
666
+ # those are stereo pairs
667
+ stereo_pairs = []
668
+ for first_in_pair in pair_idx_start:
669
+ suffix = tracks_lines[first_in_pair][1:]
670
+ stereo_pairs.append((suffix, _findLR(first_in_pair)))
671
+ logger.debug('stereo_pairs %s'%stereo_pairs)
672
+ output_tracks.stereomics = stereo_pairs
673
+ logger.debug('finished: %s'%output_tracks)
624
674
  return output_tracks
625
675
 
626
676