tictacsync 0.81a0__py3-none-any.whl → 0.91a0__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/entry.py CHANGED
@@ -72,6 +72,9 @@ def process_files(medias):
72
72
  def process_single(file, args):
73
73
  # argument is a single file
74
74
  m = device_scanner.media_at_path(None, Path(file))
75
+ if args.plotting:
76
+ print('\nPlots can be zoomed and panned...')
77
+ print('Close window for next one.')
75
78
  a_rec = yaltc.Recording(m, do_plots=args.plotting)
76
79
  time = a_rec.get_start_time()
77
80
  # time = a_rec.get_start_time(plots=args.plotting)
@@ -97,10 +100,10 @@ def process_single(file, args):
97
100
  def main():
98
101
  parser = argparse.ArgumentParser()
99
102
  parser.add_argument(
100
- "directory",
103
+ "path",
101
104
  type=str,
102
105
  nargs=1,
103
- help="path of media directory"
106
+ help="directory_name or media_file"
104
107
  )
105
108
  # parser.add_argument("directory", nargs="?", help="path of media directory")
106
109
  # parser.add_argument('-v', action='store_true')
@@ -108,29 +111,29 @@ def main():
108
111
  dest='verbose_output',
109
112
  help='Set verbose ouput')
110
113
  parser.add_argument('-o', nargs=1,
111
- help='where to write synced clips')
114
+ help='Where to write the SyncedMedia folder [default to "path" ]')
112
115
  parser.add_argument('-p', action='store_true', default=False,
113
116
  dest='plotting',
114
- help='Make plots')
117
+ help='Produce plots')
115
118
  parser.add_argument('--isos', action='store_true', default=False,
116
119
  dest='write_ISOs',
117
120
  help='Write ISO sound files')
118
121
  parser.add_argument('--nosync', action='store_true',
119
122
  dest='nosync',
120
- help='just scan and decode')
123
+ help='Just scan and decode')
121
124
  parser.add_argument('--terse', action='store_true',
122
125
  dest='terse',
123
- help='terse output')
126
+ help='Terse output')
124
127
  args = parser.parse_args()
125
128
  if args.verbose_output:
126
129
  logger.add(sys.stderr, level="DEBUG")
127
130
  # logger.add(sys.stdout, filter="__main__")
128
131
  # logger.add(sys.stdout, filter="yaltc")
129
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_read_sound_find_TicTacCode")
132
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "get_start_time")
130
133
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "_detect_sync_pulse_position")
131
134
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_device_mix")
132
135
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix_files")
133
- top_dir = args.directory[0]
136
+ top_dir = args.path[0]
134
137
  if os.path.isfile(top_dir):
135
138
  file = top_dir
136
139
  process_single(file, args)
@@ -173,14 +176,12 @@ def main():
173
176
  for m in medias[:-1]:
174
177
  print('[gold1]%s[/gold1]'%m.path.name, end=', ')
175
178
  print('[gold1]%s[/gold1]'%medias[-1].path.name)
176
- # print('\nThese recordings will be analysed for timestamps:\n')
177
- # for m in (scanner.found_media_files):
178
- # print(' ', '[gold1]%s[/gold1]'%m.path.name)
179
179
  print()
180
- if args.verbose_output or args.terse: # verbose or terse, so no progress bars
181
- rez = process_files(scanner.found_media_files)
182
- else:
183
- rez = process_files_with_progress_bars(scanner.found_media_files)
180
+ # if args.verbose_output or args.terse: # verbose or terse, so no progress bars
181
+ # rez = process_files(scanner.found_media_files)
182
+ # else:
183
+ # rez = process_files_with_progress_bars(scanner.found_media_files)
184
+ rez = process_files(scanner.found_media_files)
184
185
  recordings, rec_with_yaltc, times = rez
185
186
  recordings_with_time = [
186
187
  rec
@@ -188,10 +189,9 @@ def main():
188
189
  if rec.get_start_time()
189
190
  ]
190
191
  if not args.terse:
191
- print('\n\n')
192
192
  table = Table(title="tictacsync results")
193
193
  table.add_column("Recording\n", justify="center", style='gold1')
194
- table.add_column("TicTacCode\nChannel", justify="center", style='gold1')
194
+ table.add_column("TTC chan\n (1st=#0)", justify="center", style='gold1')
195
195
  # table.add_column("Device\n", justify="center", style='gold1')
196
196
  table.add_column("UTC times\nstart:end", justify="center", style='gold1')
197
197
  table.add_column("Clock drift\n(ppm)", justify="right", style='gold1')
@@ -244,18 +244,18 @@ def main():
244
244
  print('Warning, can\'t write ISOs without structured folders: [gold1]--isos[/gold1] option ignored.\n')
245
245
  asked_ISOs = False
246
246
  output_dir = args.o
247
- if args.verbose_output or args.terse: # verbose, so no progress bars
248
- for stitcher in matcher.video_mergers:
249
- stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
250
- OUT_struct_for_mcam,
251
- asked_ISOs,)
252
- else:
253
- print()
254
- for stitcher in track(matcher.video_mergers,
255
- description="4/4 Merging sound to videos:"):
256
- stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
257
- OUT_struct_for_mcam,
258
- asked_ISOs,)
247
+ # if args.verbose_output or args.terse: # verbose, so no progress bars
248
+ for stitcher in matcher.video_mergers:
249
+ stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
250
+ OUT_struct_for_mcam,
251
+ asked_ISOs,)
252
+ # else:
253
+ # print()
254
+ # for stitcher in track(matcher.video_mergers,
255
+ # description="4/4 Merging sound to videos:"):
256
+ # stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
257
+ # OUT_struct_for_mcam,
258
+ # asked_ISOs,)
259
259
  if not args.terse:
260
260
  print("\n")
261
261
  # find out where files were wrtitten
tictacsync/timeline.py CHANGED
@@ -237,7 +237,6 @@ def _sox_mono2stereo(temp_file) -> tempfile.NamedTemporaryFile:
237
237
  sys.exit(1)
238
238
  return stereo_tempfile
239
239
 
240
-
241
240
  def _sox_mix_files(temp_files_to_mix:list) -> tempfile.NamedTemporaryFile:
242
241
  """
243
242
  Mix files referred by the list of Path into a new temporary files passed on
@@ -303,9 +302,10 @@ class AudioStitcherVideoMerger:
303
302
  """
304
303
  Typically each found video is associated with an AudioStitcherVideoMerger
305
304
  instance. AudioStitcherVideoMerger does the actual audio-video file
306
- processing of merging self.videoclip (gen. a video) with all audio
307
- files in self.edited_audio as determined by the Matcher
308
- object (it instanciates and manages AudioStitcherVideoMerger objects).
305
+ processing of merging AudioStitcherVideoMerger.videoclip (gen. a video)
306
+ with all audio files in AudioStitcherVideoMerger.edited_audio as
307
+ determined by the Matcher object (Matcher instanciates and manages
308
+ AudioStitcherVideoMerger objects).
309
309
 
310
310
  All audio file edits are done using pysox and video+audio merging with
311
311
  ffmpeg. When necessary, clock drift is corrected for all overlapping audio
@@ -343,23 +343,21 @@ class AudioStitcherVideoMerger:
343
343
 
344
344
  def add_matched_audio(self, audio_rec):
345
345
  """
346
- Populates self.edited_audio, a dict as {Recording : path}
346
+ Populates AudioStitcherVideoMerger.edited_audio,
347
+ a dict as {Recording : path}
347
348
 
348
- AudioStitcherVideoMerger.add_matched_audio() is called
349
+ This fct is called
349
350
  within Matcher.scan_audio_for_each_videoclip()
350
351
 
351
352
  Returns nothing, fills self.edited_audio dict with
352
353
  matched audio.
353
354
 
354
355
  """
355
- self.edited_audio[audio_rec] = audio_rec.valid_sound
356
+ self.edited_audio[audio_rec] = audio_rec.AVpath
356
357
  """
357
358
  Here at this point, self.edited_audio[audio_rec] is unedited but
358
359
  after a call to _edit_audio_file(), edited_audio[audio_rec] points to
359
- a new file and the precedent is unchanged (that's why from
360
- AudioStitcherVideoMerger instance to another
361
- audio_rec.valid_sound doesn't need to be reinitialized since
362
- it stays unchanged)
360
+ a new file and audio_rec.AVpath is unchanged.
363
361
  """
364
362
  return
365
363
 
@@ -384,19 +382,18 @@ class AudioStitcherVideoMerger:
384
382
  return recs
385
383
 
386
384
  def _dedrift_rec(self, rec):
387
- # first_audio_p = rec.AVpath
388
385
  initial_duration = sox.file_info.duration(
389
- _pathname(rec.valid_sound))
386
+ _pathname(rec.AVpath))
390
387
  sox_transform = sox.Transformer()
391
388
  # tempo_scale_factor = rec.device_relative_speed
392
389
  tempo_scale_factor = rec.device_relative_speed
393
390
  audio_dev = rec.device.name
394
391
  video_dev = self.videoclip.device.name
395
392
  if tempo_scale_factor > 1:
396
- print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor\n'%
393
+ print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor;\n'%
397
394
  (audio_dev, video_dev, tempo_scale_factor))
398
395
  else:
399
- print('[gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is too short by a %f factor\n'%
396
+ print('hence [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is short by a %f factor\n'%
400
397
  (audio_dev, video_dev, tempo_scale_factor))
401
398
  sox_transform.tempo(tempo_scale_factor)
402
399
  # scaled_file = self._get_soxed_file(rec, sox_transform)
@@ -740,9 +737,10 @@ class AudioStitcherVideoMerger:
740
737
  (device.ttc, device.tracks.ttc))
741
738
  if device.ttc + 1 != device.tracks.ttc: # warn and quit
742
739
  print('Error: TicTacCode channel detected is [gold1]%i[/gold1]'%
743
- device.ttc + 1, end=' ')
744
- print('and the file [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
745
- (device_scanner.TRACKSFN, device.tracks.ttc))
740
+ (device.ttc), end=' ')
741
+ print('and [gold1]%s[/gold1] for the device [gold1]%s[/gold1] specifies channel [gold1]%i[/gold1],'%
742
+ (device_scanner.TRACKSFN,
743
+ device.name, device.tracks.ttc-1))
746
744
  print('Please correct the discrepancy and rerun. Quitting.')
747
745
  sys.exit(1)
748
746
  if device.tracks.mix == [] and device.tracks.stereomics == []:
@@ -888,6 +886,8 @@ class AudioStitcherVideoMerger:
888
886
  self.videoclip.synced_audio = \
889
887
  _sox_keep(concatenate_audio_file, [sox_kept_channel])
890
888
  self._merge_audio_and_video()
889
+ if asked_ISOs:
890
+ print('WARNING: you asked for ISO files but found one audio channel only...')
891
891
  return #########################################################
892
892
  #
893
893
  # if not returned yet from fct, either multitracks and/or multi
@@ -916,6 +916,7 @@ class AudioStitcherVideoMerger:
916
916
  # [(dev1, [mono1_ch1, mono1_ch2]), (dev2, [mono2_ch1, mono2_ch2)]] in
917
917
  # devices_and_monofiles:
918
918
  if asked_ISOs:
919
+ logger.debug('will output ISO files...')
919
920
  devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
920
921
  for device, multi_chan_audio
921
922
  in merged_audio_files_by_device]
tictacsync/yaltc.py CHANGED
@@ -29,20 +29,15 @@ try:
29
29
  except:
30
30
  import device_scanner
31
31
 
32
- TEENSY_MAX_LAG = 128/44100 # sec, duration of a default length audio block
32
+ TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
33
33
 
34
+ # see extract_seems_TicTacCode() for duration criterion values
34
35
 
35
36
  CACHING = True
36
37
  DEL_TEMP = False
37
38
  DB_RMS_SILENCE_SOX = -58
38
39
  MAXDRIFT = 10e-3 # in sec, normally 10e-3 (10 ms)
39
40
 
40
- SAFE_SILENCE_WINDOW_WIDTH = 400 # ms, not the full 500 ms, to accommodate decay
41
- # used in _get_silent_zone_indices()
42
- WORDWIDTHFACTOR = 2
43
- # see _get_word_width_parameters()
44
-
45
- OVER_NOISE_SYNC_DETECT_LEVEL = 2
46
41
 
47
42
  ################## pasted from FSKfreqCalculator.py output:
48
43
  F1 = 630.00 # Hertz
@@ -53,14 +48,14 @@ N_SYMBOLS = 35 # including sync pulse
53
48
 
54
49
  MINIMUM_LENGTH = 4 # sec
55
50
  TRIAL_TIMES = [ # in seconds
56
- (0.5, -2),
57
- (0.5, -3.5),
58
- (0.5, -5),
51
+ (3.5, -2),
52
+ (3.5, -3.5),
53
+ (3.5, -5),
59
54
  (2, -2),
60
55
  (2, -3.5),
61
56
  (2, -5),
62
- (3.5, -2),
63
- (3.5, -3.5),
57
+ (0.5, -2),
58
+ (0.5, -3.5),
64
59
  ]
65
60
  SOUND_EXTRACT_LENGTH = (10*SYMBOL_LENGTH*1e-3 + 1) # second
66
61
  SYMBOL_LENGTH_TOLERANCE = 0.07 # relative
@@ -143,17 +138,16 @@ def to_precision(x,p):
143
138
 
144
139
  class Decoder:
145
140
  """
146
- Object encapsulating DSP processes to demodulate TicTacCode track from audio file;
147
- Decoders are instantiated by their respective Recording object. Produces
148
- plots on demand for diagnostic purposes.
141
+ Object encapsulating DSP processes to demodulate TicTacCode track from audio
142
+ file; Decoders are instantiated by their respective Recording object.
143
+ Produces plots on demand for diagnostic purposes.
149
144
 
150
145
  Attributes:
151
146
 
152
- sound_extract : numpy.ndarray of int16, shaped (N)
153
- duration of about SOUND_EXTRACT_LENGTH sec. sound data extract,
154
- could be anywhere in the audio file (start, end, etc...) Set by
155
- Recording object. This audio signal might or might not be the TicTacCode
156
- track.
147
+ sound_extract : 1d numpy.ndarray of int16
148
+ length determined by SOUND_EXTRACT_LENGTH (sec). Could be anywhere
149
+ in the audio file (start, end, etc...) Set by Recording object.
150
+ This audio signal might or might not be the TicTacCode track.
157
151
 
158
152
  sound_extract_position : int
159
153
  where the sound_extract is located in the file, samples
@@ -180,10 +174,6 @@ class Decoder:
180
174
  detected_pulse_position : int
181
175
  pulse position (samples) relative to the start of self.sound_extract
182
176
 
183
- cached_convolution_fit : dict
184
- if _fit_triangular_signal_to_convoluted_env() has already been called,
185
- will use cached values if sound_extract_position is the same.
186
-
187
177
  """
188
178
 
189
179
  def __init__(self, aRec, do_plots):
@@ -266,7 +256,7 @@ class Decoder:
266
256
  # extra_window_duration = SOUND_EXTRACT_LENGTH - 1
267
257
  # eff_w = total_w - extra_window_duration
268
258
  # logger.debug('effective_word_duration %f (two regions)'%eff_w)
269
- if not 0.5 < total_w < 0.655:
259
+ if not 0.5 < total_w < 0.656:
270
260
  failing_comment = 'two regions duration %f not in [0.50 0.655]\n%s'%(total_w, widths)
271
261
  # fig, ax = plt.subplots()
272
262
  # p(ax, sound_extract_one_bit)
@@ -276,13 +266,18 @@ class Decoder:
276
266
 
277
267
  def _plot_extract(self):
278
268
  fig, ax = plt.subplots()
279
- ax.plot(self.sound_extract, marker='o', markersize='1',
269
+ start = self.sound_extract_position
270
+ i_samples = np.arange(start, start + len(self.sound_extract))
271
+ yt = ax.get_yaxis_transform()
272
+ ax.hlines(0, 0, 1,
273
+ transform=yt, alpha=0.3,
274
+ linewidth=2, colors='black')
275
+ ax.plot(i_samples, self.sound_extract, marker='o', markersize='1',
280
276
  linewidth=1.5,alpha=0.3, color='blue' )
281
- ax.plot(self.sound_extract_one_bit*np.max(np.abs(self.sound_extract)),
277
+ ax.plot(i_samples, self.sound_extract_one_bit*np.max(np.abs(self.sound_extract)),
282
278
  marker='o', markersize='1',
283
279
  linewidth=1.5,alpha=0.3,color='red')
284
280
  xt = ax.get_xaxis_transform()
285
- yt = ax.get_yaxis_transform()
286
281
  ax.hlines(self.pulse_detection_level, 0, 1,
287
282
  transform=yt, alpha=0.3,
288
283
  linewidth=2, colors='green')
@@ -295,7 +290,8 @@ class Decoder:
295
290
  custom_lines,
296
291
  'detection level, signal, detected region'.split(','),
297
292
  loc='lower right')
298
- ax.set_title('Finding word and sync pulse')
293
+ ax.set_title('Finding word + sync pulse')
294
+ plt.xlabel("Position in file (samples)")
299
295
  plt.show()
300
296
 
301
297
  def get_time_in_sound_extract(self):
@@ -449,14 +445,7 @@ class Decoder:
449
445
  start = round(0.5*symbol_length) # half symbol
450
446
  end = start + symbol_length
451
447
  word_begining = whole_word[start:]
452
- # word_one_bit = np.abs(word_begining)>self.pulse_detection_level
453
- # N_ones = round(1.5*SYMBOL_LENGTH*1e-3*self.samplerate) # so it includes sync pulse
454
- # word_one_bit = closing(word_one_bit, np.ones(N_ones))
455
448
  gt_detection_level = np.argwhere(np.abs(word_begining)>self.pulse_detection_level)
456
- # print(gt_detection_level)
457
- # plt.plot(word_one_bit)
458
- # plt.plot(word_begining/abs(np.max(word_begining)))
459
- # plt.show()
460
449
  word_start = gt_detection_level[0][0]
461
450
  word_end = gt_detection_level[-1][0]
462
451
  self.effective_word_duration = (word_end - word_start)/self.samplerate
@@ -473,7 +462,10 @@ class Decoder:
473
462
  1e3*TEENSY_MAX_LAG))
474
463
  logger.debug('relative audio_block gap %.2f'%(relative_gap))
475
464
  if relative_gap > 1:
476
- print('bug with relative_gap')
465
+ print('Warning: gap between spike and word is too big for %s'%self.rec)
466
+ print('Audio update() gap between sync pulse and word start: ')
467
+ print('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
468
+ 1e3*TEENSY_MAX_LAG))
477
469
  symbol_width_samples_theor = self.samplerate*SYMBOL_LENGTH*1e-3
478
470
  symbol_width_samples_eff = self.effective_word_duration * \
479
471
  self.samplerate/(N_SYMBOLS - 1)
@@ -486,14 +478,23 @@ class Decoder:
486
478
  symbols_indices = symbol_positions.round().astype(int)
487
479
  if self.do_plots:
488
480
  fig, ax = plt.subplots()
489
- ax.plot(whole_word, marker='o', markersize='1',
481
+ ax.hlines(0, 0, 1,
482
+ transform=ax.get_yaxis_transform(), alpha=0.3,
483
+ linewidth=2, colors='black')
484
+ start = self.sound_extract_position
485
+ i_samples = np.arange(start, start + len(whole_word))
486
+ ax.plot(i_samples, whole_word, marker='o', markersize='1',
490
487
  linewidth=1.5,alpha=0.3, color='blue' )
491
488
  xt = ax.get_xaxis_transform()
492
489
  for x in symbols_indices:
493
- ax.vlines(x, 0, 1,
490
+ ax.vlines(x + start, 0, 1,
494
491
  transform=xt,
495
492
  linewidth=0.6, colors='green')
496
493
  ax.set_title('Slicing the 34 bits word:')
494
+ plt.xlabel("Position in file (samples)")
495
+ ax.vlines(start, 0, 1,
496
+ transform=xt,
497
+ linewidth=0.6, colors='red')
497
498
  plt.show()
498
499
  slice_width = round(SYMBOL_LENGTH*1e-3*self.samplerate)
499
500
  slices = [whole_word[i:i+slice_width] for i in symbols_indices]
@@ -516,8 +517,10 @@ class Recording:
516
517
  AVpath : pathlib.path
517
518
  path of video+sound+TicTacCode file, relative to working directory
518
519
 
519
- valid_sound : pathlib.path
520
- path of sound file stripped of silent and TicTacCode channels
520
+ audio_data : in16 numpy.array of shape [nchan] x [N samples]
521
+
522
+ # valid_sound : pathlib.path
523
+ # path of sound file stripped of silent and TicTacCode channels
521
524
 
522
525
  device : Device
523
526
  identifies the device used for the recording, set in __init__()
@@ -531,7 +534,7 @@ class Recording:
531
534
 
532
535
  TicTacCode_channel : int
533
536
  which channel is sync track. 0 is first channel,
534
- set in _read_sound_find_TicTacCode().
537
+ set in _find_TicTacCode().
535
538
 
536
539
  decoder : yaltc.decoder
537
540
  associated decoder object, if file is audiovideo
@@ -634,7 +637,7 @@ class Recording:
634
637
  self.TicTacCode_channel = None
635
638
  self.is_reference = False
636
639
  self.device_relative_speed = 1.0
637
- self.valid_sound = None
640
+ # self.valid_sound = None
638
641
  self.final_synced_file = None
639
642
  self.synced_audio = None
640
643
  self.new_rec_name = media.path.name
@@ -672,6 +675,37 @@ class Recording:
672
675
  self.decoder = None
673
676
  logger.debug('ffprobe found: %s'%self.probe)
674
677
  logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
678
+ self._read_audio_data()
679
+
680
+ def _read_audio_data(self):
681
+ # sets Recording.audio_data
682
+ dryrun = (ffmpeg
683
+ .input(str(self.AVpath))
684
+ .output('pipe:', format='s16le', acodec='pcm_s16le')
685
+ .get_args())
686
+ dryrun = ' '.join(dryrun)
687
+ logger.debug('using ffmpeg-python built args to pipe audio stream into numpy array:\nffmpeg %s'%dryrun)
688
+ try:
689
+ out, _ = (ffmpeg
690
+ # .input(str(path), ss=time_where, t=chunk_length)
691
+ .input(str(self.AVpath))
692
+ .output('pipe:', format='s16le', acodec='pcm_s16le')
693
+ .global_args("-loglevel", "quiet")
694
+ .global_args("-nostats")
695
+ .global_args("-hide_banner")
696
+ .run(capture_stdout=True))
697
+ data = np.frombuffer(out, np.int16)
698
+ except ffmpeg.Error as e:
699
+ print('error',e.stderr)
700
+ n_chan = self.get_audio_channels_nbr()
701
+ if n_chan == 1 and not self.is_video():
702
+ logger.error('file is sound mono')
703
+ if np.isclose(np.std(data), 0, rtol=1e-2):
704
+ logger.error("ffmpeg can't extract audio from %s"%self.AVpath)
705
+ # from 1D interleaved channels to [chan1, chan2, chanN]
706
+ self.audio_data = data.reshape(int(len(data)/n_chan),n_chan).T
707
+ logger.debug('Recording.audio_data: %s of shape %s'%(self.audio_data,
708
+ self.audio_data.shape))
675
709
 
676
710
  def __repr__(self):
677
711
  return 'Recording of %s'%_pathname(self.new_rec_name)
@@ -711,9 +745,9 @@ class Recording:
711
745
  recording duration in seconds.
712
746
 
713
747
  """
714
- if self.valid_sound:
715
- val = sox.file_info.duration(_pathname(self.valid_sound))
716
- logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.valid_sound)))
748
+ if self.is_audio():
749
+ val = sox.file_info.duration(_pathname(self.AVpath))
750
+ logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.AVpath)))
717
751
  return val #########################################################
718
752
  else:
719
753
  if self.probe is None:
@@ -739,8 +773,8 @@ class Recording:
739
773
  recording duration in seconds.
740
774
 
741
775
  """
742
- val = sox.file_info.duration(_pathname(self.valid_sound))
743
- logger.debug('duration of valid_sound %f'%val)
776
+ val = sox.file_info.duration(_pathname(self.AVpath))
777
+ logger.debug('duration of AVpath %f'%val)
744
778
  return val
745
779
 
746
780
  def get_corrected_duration(self):
@@ -782,8 +816,8 @@ class Recording:
782
816
 
783
817
  def _find_time_around(self, time):
784
818
  """
785
- Actually reads sound data and tries to decode it
786
- through decoder object, if successful return a time dict, eg:
819
+ Tries to decode FSK around time (in sec)
820
+ through decoder object; if successful return a time dict, eg:
787
821
  {'version': 0, 'seconds': 44, 'minutes': 57,
788
822
  'hours': 19, 'day': 1, 'month': 3, 'year offset': 1,
789
823
  'pulse at': 670451.2217 }
@@ -793,7 +827,7 @@ class Recording:
793
827
  there = self.get_duration() + time
794
828
  else:
795
829
  there = time
796
- self._read_sound_find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
830
+ self._find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
797
831
  if self.TicTacCode_channel is None:
798
832
  return None
799
833
  else:
@@ -907,8 +941,9 @@ class Recording:
907
941
  Try to decode a TicTacCode_channel at start AND finish;
908
942
  if successful, returns a datetime.datetime instance;
909
943
  if not returns None.
910
- If successful AND self is audio, sets self.valid_sound
911
944
  """
945
+ logger.debug('for recording %s, recording.start_time %s'%(self,
946
+ self.start_time))
912
947
  if self.start_time is not None:
913
948
  return self.start_time #############################################
914
949
  cached_times = {}
@@ -948,6 +983,7 @@ class Recording:
948
983
  time_around_end)
949
984
  logger.debug('_two_times_are_coherent: %s'%coherence)
950
985
  if coherence:
986
+ logger.debug('Trial #%i successful'%(i+1))
951
987
  break
952
988
  if not coherence:
953
989
  logger.warning('found times are incoherent')
@@ -971,49 +1007,10 @@ class Recording:
971
1007
  logger.debug('recording started at %s'%start_UTC)
972
1008
  self.start_time = start_UTC
973
1009
  self.sync_position = time_around_beginning['pulse at']
974
- if self.is_audio():
975
- # self.valid_sound = self._strip_TTC_and_Null() # why now? :-)
976
- self.valid_sound = self.AVpath
1010
+ # if self.is_audio():
1011
+ # self.valid_sound = self.AVpath
977
1012
  return start_UTC
978
1013
 
979
- def _sox_strip(self, audio_file, excluded_channels) -> tempfile.NamedTemporaryFile:
980
- # building dict according to pysox.remix format.
981
- # https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
982
- # eg: 4 channels with TicTacCode_channel at #2
983
- # returns {1: [1], 2: [3], 3: [4]}
984
- # ie the number of channels drops by one and chan 2 is missing
985
- # excluded_channels is a list of Zero Based indexing chan numbers
986
- n_channels = self.device.n_chan
987
- all_channels = range(1, n_channels + 1) # from 1 to n_channels included
988
- sox_excluded_channels = [n+1 for n in excluded_channels]
989
- logger.debug('for file %s'%self.AVpath.name)
990
- logger.debug('excluded chans %s (not ZBIDX)'%sox_excluded_channels)
991
- kept_chans = [[n] for n in all_channels if n not in sox_excluded_channels]
992
- # eg [[1], [3], [4]]
993
- sox_remix_dict = dict(zip(all_channels, kept_chans))
994
- # {1: [1], 2: [3], 3: [4]} -> from 4 to 3 chan and chan 2 is dropped
995
- output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
996
- out_file = _pathname(output_fh)
997
- logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
998
- # sox_transform.set_output_format(channels=1)
999
- sox_transform = sox.Transformer()
1000
- sox_transform.remix(sox_remix_dict)
1001
- logger.debug('sox remix transform: %s'%sox_transform)
1002
- logger.debug('sox remix dict: %s'%sox_remix_dict)
1003
- status = sox_transform.build(audio_file, out_file, return_output=True )
1004
- logger.debug('sox.build exit code %s'%str(status))
1005
- p = Popen('ffprobe %s -hide_banner'%audio_file,
1006
- shell=True, stdout=PIPE, stderr=PIPE)
1007
- stdout, stderr = p.communicate()
1008
- logger.debug('remixed input_file ffprobe:\n%s'%(stdout +
1009
- stderr).decode('utf-8'))
1010
- p = Popen('ffprobe %s -hide_banner'%out_file,
1011
- shell=True, stdout=PIPE, stderr=PIPE)
1012
- stdout, stderr = p.communicate()
1013
- logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
1014
- stderr).decode('utf-8'))
1015
- return output_fh
1016
-
1017
1014
  def _ffprobe_audio_stream(self):
1018
1015
  streams = self.probe['streams']
1019
1016
  audio_streams = [
@@ -1129,12 +1126,11 @@ class Recording:
1129
1126
  def is_audio(self):
1130
1127
  return not self.is_video()
1131
1128
 
1132
- def _read_sound_find_TicTacCode(self, time_where, chunk_length):
1129
+ def _find_TicTacCode(self, time_where, chunk_length):
1133
1130
  """
1134
- If this is called for the first time for the recording, it loads audio
1135
- data reading from self.AVpath; Split data into channels if stereo; Send
1136
- this data to Decoder object with set_sound_extract_and_sr() to find
1137
- which channel contains a TicTacCode track and sets TicTacCode_channel
1131
+ Extracts a chunk from Recording.audio_data and sends it to
1132
+ Recording.decoder object with set_sound_extract_and_sr() to find which
1133
+ channel contains a TicTacCode track and sets TicTacCode_channel
1138
1134
  accordingly (index of channel). On exit, self.decoder.sound_extract
1139
1135
  contains TicTacCode data ready to be demodulated. If not,
1140
1136
  self.TicTacCode_channel is set to None.
@@ -1163,44 +1159,18 @@ class Recording:
1163
1159
  decoder = self.decoder
1164
1160
  if decoder:
1165
1161
  decoder.clear_decoder()
1166
- # decoder.cached_convolution_fit['is clean'] = False
1167
1162
  if not self.has_audio():
1168
1163
  self.TicTacCode_channel = None
1169
1164
  return #############################################################
1170
- logger.debug('will read around %.2f sec'%time_where)
1171
- dryrun = (ffmpeg
1172
- .input(str(path))
1173
- .output('pipe:', format='s16le', acodec='pcm_s16le')
1174
- .get_args())
1175
- dryrun = ' '.join(dryrun)
1176
- logger.debug('using ffmpeg-python built args to pipe wav file into numpy array:\nffmpeg %s'%dryrun)
1177
- try:
1178
- out, _ = (ffmpeg
1179
- # .input(str(path), ss=time_where, t=chunk_length)
1180
- .input(str(path))
1181
- .output('pipe:', format='s16le', acodec='pcm_s16le')
1182
- .global_args("-loglevel", "quiet")
1183
- .global_args("-nostats")
1184
- .global_args("-hide_banner")
1185
- .run(capture_stdout=True))
1186
- data = np.frombuffer(out, np.int16)
1187
- except ffmpeg.Error as e:
1188
- print('error',e.stderr)
1189
- sound_data_var = np.std(data)
1190
- logger.debug('extracting sound, ffmpeg output:%s with variance %f'%(data,
1191
- sound_data_var))
1192
- sound_extract_position = int(self.get_samplerate()*time_where) # from sec to samples
1193
- n_chan = self.get_audio_channels_nbr()
1194
- if n_chan == 1 and not self.is_video():
1195
- logger.warning('file is sound mono')
1196
- if np.isclose(sound_data_var, 0, rtol=1e-2):
1197
- logger.warning("ffmpeg can't extract audio from %s"%self.AVpath)
1198
- # from 1D interleaved channels to [chan1, chan2, chanN]
1199
- all_channels_data = data.reshape(int(len(data)/n_chan),n_chan).T
1165
+ sound_data_var = np.std(self.audio_data)
1166
+ sound_extract_position = int(self.get_samplerate()*time_where)
1167
+ logger.debug('extracting sound at %i with variance %f'%(
1168
+ sound_extract_position,
1169
+ sound_data_var))
1200
1170
  if self.TicTacCode_channel == None:
1201
1171
  logger.debug('first call, will loop through all %i channels'%len(
1202
- all_channels_data))
1203
- for i_chan, chan_dat in enumerate(all_channels_data):
1172
+ self.audio_data))
1173
+ for i_chan, chan_dat in enumerate(self.audio_data):
1204
1174
  logger.debug('testing chan %i'%i_chan)
1205
1175
  start_idx = round(time_where*self.get_samplerate())
1206
1176
  extract_length = round(chunk_length*self.get_samplerate())
@@ -1226,7 +1196,7 @@ class Recording:
1226
1196
  start_idx = round(time_where*self.get_samplerate())
1227
1197
  extract_length = round(chunk_length*self.get_samplerate())
1228
1198
  end_idx = start_idx + extract_length
1229
- chan_dat = all_channels_data[self.TicTacCode_channel]
1199
+ chan_dat = self.audio_data[self.TicTacCode_channel]
1230
1200
  extract_audio_data = chan_dat[start_idx:end_idx]
1231
1201
  decoder.set_sound_extract_and_sr(
1232
1202
  extract_audio_data,
@@ -1238,7 +1208,7 @@ class Recording:
1238
1208
  def seems_to_have_TicTacCode_at_beginning(self):
1239
1209
  if self.probe is None:
1240
1210
  return False #######################################################
1241
- self._read_sound_find_TicTacCode(TRIAL_TIMES[0][0],
1211
+ self._find_TicTacCode(TRIAL_TIMES[0][0],
1242
1212
  SOUND_EXTRACT_LENGTH)
1243
1213
  return self.TicTacCode_channel is not None
1244
1214
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.81a0
3
+ Version: 0.91a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -0,0 +1,15 @@
1
+ tictacsync/LTCcheck.py,sha256=IEfpB_ZajWuRTWtqji0H-B2g7GQvWmGVjfT0Icumv7o,15704
2
+ tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ tictacsync/device_scanner.py,sha256=lvWps5XPvzubnTqisFWjdRUpxsM9YMRYMp-xQu7H6Os,27515
4
+ tictacsync/entry.py,sha256=TgiFdwTKWvtj7xU-556nkwDo4OqAjJ55g97Jt-lSeBg,11377
5
+ tictacsync/multi2polywav.py,sha256=k7VU-yjO1_0DbygWNytYvaExbiAs3_0-n0UmgGTa8wM,7282
6
+ tictacsync/remergemix.py,sha256=FJTMipIS0O7mMl_tr8BhuYqWvanSydvjGkFCEd-jaDk,9829
7
+ tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
8
+ tictacsync/timeline.py,sha256=0K1haVu7qUbyQmb0AsVoOB3CO8_OevHxpcJENx0kf48,57763
9
+ tictacsync/yaltc.py,sha256=2pMyDv69x56p3zq11L34PcqS_xC3-HwMPEDXGc7QhDg,52560
10
+ tictacsync-0.91a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
11
+ tictacsync-0.91a0.dist-info/METADATA,sha256=fnBgd-2zImEjHYIctys5-wF1TY4G-vxhCmdMz1wePk4,5502
12
+ tictacsync-0.91a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
13
+ tictacsync-0.91a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
14
+ tictacsync-0.91a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
15
+ tictacsync-0.91a0.dist-info/RECORD,,
@@ -1,15 +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=lvWps5XPvzubnTqisFWjdRUpxsM9YMRYMp-xQu7H6Os,27515
4
- tictacsync/entry.py,sha256=QGJmoidTPGkF6zjkvqmHSZH4-tU89w7uWJNvrSFaNic,11386
5
- tictacsync/multi2polywav.py,sha256=k7VU-yjO1_0DbygWNytYvaExbiAs3_0-n0UmgGTa8wM,7282
6
- tictacsync/remergemix.py,sha256=FJTMipIS0O7mMl_tr8BhuYqWvanSydvjGkFCEd-jaDk,9829
7
- tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
8
- tictacsync/timeline.py,sha256=WBlvn5Ej_ZlDddXyCBl1vUDWRHwn8C02UEyBeo2AGdo,57695
9
- tictacsync/yaltc.py,sha256=3WWldA-Fa86uQeJItcM3H5VCIuBe2tAih7S_BbopF00,54394
10
- tictacsync-0.81a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
11
- tictacsync-0.81a0.dist-info/METADATA,sha256=GRh8BiqMDc2AQYXQNRfJr4Yc2_DoZG7PucaAv7y8gsc,5502
12
- tictacsync-0.81a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
13
- tictacsync-0.81a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
14
- tictacsync-0.81a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
15
- tictacsync-0.81a0.dist-info/RECORD,,