tictacsync 0.82a0__tar.gz → 0.91a0__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.82a0
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
@@ -32,7 +32,7 @@ setup(
32
32
  'multi2polywav = tictacsync.multi2polywav:main',
33
33
  ]
34
34
  },
35
- version = '0.82a',
35
+ version = '0.91a',
36
36
  description = "command for syncing audio video recordings",
37
37
  long_description_content_type='text/markdown',
38
38
  long_description = long_descr,
@@ -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"] == "build_audio_and_write_video")
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
@@ -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 == []:
@@ -29,7 +29,7 @@ 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
34
  # see extract_seems_TicTacCode() for duration criterion values
35
35
 
@@ -38,12 +38,6 @@ DEL_TEMP = False
38
38
  DB_RMS_SILENCE_SOX = -58
39
39
  MAXDRIFT = 10e-3 # in sec, normally 10e-3 (10 ms)
40
40
 
41
- SAFE_SILENCE_WINDOW_WIDTH = 400 # ms, not the full 500 ms, to accommodate decay
42
- # used in _get_silent_zone_indices()
43
- WORDWIDTHFACTOR = 2
44
- # see _get_word_width_parameters()
45
-
46
- OVER_NOISE_SYNC_DETECT_LEVEL = 2
47
41
 
48
42
  ################## pasted from FSKfreqCalculator.py output:
49
43
  F1 = 630.00 # Hertz
@@ -54,14 +48,14 @@ N_SYMBOLS = 35 # including sync pulse
54
48
 
55
49
  MINIMUM_LENGTH = 4 # sec
56
50
  TRIAL_TIMES = [ # in seconds
57
- (0.5, -2),
58
- (0.5, -3.5),
59
- (0.5, -5),
51
+ (3.5, -2),
52
+ (3.5, -3.5),
53
+ (3.5, -5),
60
54
  (2, -2),
61
55
  (2, -3.5),
62
56
  (2, -5),
63
- (3.5, -2),
64
- (3.5, -3.5),
57
+ (0.5, -2),
58
+ (0.5, -3.5),
65
59
  ]
66
60
  SOUND_EXTRACT_LENGTH = (10*SYMBOL_LENGTH*1e-3 + 1) # second
67
61
  SYMBOL_LENGTH_TOLERANCE = 0.07 # relative
@@ -144,17 +138,16 @@ def to_precision(x,p):
144
138
 
145
139
  class Decoder:
146
140
  """
147
- Object encapsulating DSP processes to demodulate TicTacCode track from audio file;
148
- Decoders are instantiated by their respective Recording object. Produces
149
- 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.
150
144
 
151
145
  Attributes:
152
146
 
153
- sound_extract : numpy.ndarray of int16, shaped (N)
154
- duration of about SOUND_EXTRACT_LENGTH sec. sound data extract,
155
- could be anywhere in the audio file (start, end, etc...) Set by
156
- Recording object. This audio signal might or might not be the TicTacCode
157
- 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.
158
151
 
159
152
  sound_extract_position : int
160
153
  where the sound_extract is located in the file, samples
@@ -181,10 +174,6 @@ class Decoder:
181
174
  detected_pulse_position : int
182
175
  pulse position (samples) relative to the start of self.sound_extract
183
176
 
184
- cached_convolution_fit : dict
185
- if _fit_triangular_signal_to_convoluted_env() has already been called,
186
- will use cached values if sound_extract_position is the same.
187
-
188
177
  """
189
178
 
190
179
  def __init__(self, aRec, do_plots):
@@ -277,13 +266,18 @@ class Decoder:
277
266
 
278
267
  def _plot_extract(self):
279
268
  fig, ax = plt.subplots()
280
- 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',
281
276
  linewidth=1.5,alpha=0.3, color='blue' )
282
- 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)),
283
278
  marker='o', markersize='1',
284
279
  linewidth=1.5,alpha=0.3,color='red')
285
280
  xt = ax.get_xaxis_transform()
286
- yt = ax.get_yaxis_transform()
287
281
  ax.hlines(self.pulse_detection_level, 0, 1,
288
282
  transform=yt, alpha=0.3,
289
283
  linewidth=2, colors='green')
@@ -296,7 +290,8 @@ class Decoder:
296
290
  custom_lines,
297
291
  'detection level, signal, detected region'.split(','),
298
292
  loc='lower right')
299
- ax.set_title('Finding word and sync pulse')
293
+ ax.set_title('Finding word + sync pulse')
294
+ plt.xlabel("Position in file (samples)")
300
295
  plt.show()
301
296
 
302
297
  def get_time_in_sound_extract(self):
@@ -450,14 +445,7 @@ class Decoder:
450
445
  start = round(0.5*symbol_length) # half symbol
451
446
  end = start + symbol_length
452
447
  word_begining = whole_word[start:]
453
- # word_one_bit = np.abs(word_begining)>self.pulse_detection_level
454
- # N_ones = round(1.5*SYMBOL_LENGTH*1e-3*self.samplerate) # so it includes sync pulse
455
- # word_one_bit = closing(word_one_bit, np.ones(N_ones))
456
448
  gt_detection_level = np.argwhere(np.abs(word_begining)>self.pulse_detection_level)
457
- # print(gt_detection_level)
458
- # plt.plot(word_one_bit)
459
- # plt.plot(word_begining/abs(np.max(word_begining)))
460
- # plt.show()
461
449
  word_start = gt_detection_level[0][0]
462
450
  word_end = gt_detection_level[-1][0]
463
451
  self.effective_word_duration = (word_end - word_start)/self.samplerate
@@ -474,7 +462,10 @@ class Decoder:
474
462
  1e3*TEENSY_MAX_LAG))
475
463
  logger.debug('relative audio_block gap %.2f'%(relative_gap))
476
464
  if relative_gap > 1:
477
- 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))
478
469
  symbol_width_samples_theor = self.samplerate*SYMBOL_LENGTH*1e-3
479
470
  symbol_width_samples_eff = self.effective_word_duration * \
480
471
  self.samplerate/(N_SYMBOLS - 1)
@@ -487,14 +478,23 @@ class Decoder:
487
478
  symbols_indices = symbol_positions.round().astype(int)
488
479
  if self.do_plots:
489
480
  fig, ax = plt.subplots()
490
- 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',
491
487
  linewidth=1.5,alpha=0.3, color='blue' )
492
488
  xt = ax.get_xaxis_transform()
493
489
  for x in symbols_indices:
494
- ax.vlines(x, 0, 1,
490
+ ax.vlines(x + start, 0, 1,
495
491
  transform=xt,
496
492
  linewidth=0.6, colors='green')
497
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')
498
498
  plt.show()
499
499
  slice_width = round(SYMBOL_LENGTH*1e-3*self.samplerate)
500
500
  slices = [whole_word[i:i+slice_width] for i in symbols_indices]
@@ -517,8 +517,10 @@ class Recording:
517
517
  AVpath : pathlib.path
518
518
  path of video+sound+TicTacCode file, relative to working directory
519
519
 
520
- valid_sound : pathlib.path
521
- 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
522
524
 
523
525
  device : Device
524
526
  identifies the device used for the recording, set in __init__()
@@ -532,7 +534,7 @@ class Recording:
532
534
 
533
535
  TicTacCode_channel : int
534
536
  which channel is sync track. 0 is first channel,
535
- set in _read_sound_find_TicTacCode().
537
+ set in _find_TicTacCode().
536
538
 
537
539
  decoder : yaltc.decoder
538
540
  associated decoder object, if file is audiovideo
@@ -635,7 +637,7 @@ class Recording:
635
637
  self.TicTacCode_channel = None
636
638
  self.is_reference = False
637
639
  self.device_relative_speed = 1.0
638
- self.valid_sound = None
640
+ # self.valid_sound = None
639
641
  self.final_synced_file = None
640
642
  self.synced_audio = None
641
643
  self.new_rec_name = media.path.name
@@ -673,6 +675,37 @@ class Recording:
673
675
  self.decoder = None
674
676
  logger.debug('ffprobe found: %s'%self.probe)
675
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))
676
709
 
677
710
  def __repr__(self):
678
711
  return 'Recording of %s'%_pathname(self.new_rec_name)
@@ -712,9 +745,9 @@ class Recording:
712
745
  recording duration in seconds.
713
746
 
714
747
  """
715
- if self.valid_sound:
716
- val = sox.file_info.duration(_pathname(self.valid_sound))
717
- 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)))
718
751
  return val #########################################################
719
752
  else:
720
753
  if self.probe is None:
@@ -740,8 +773,8 @@ class Recording:
740
773
  recording duration in seconds.
741
774
 
742
775
  """
743
- val = sox.file_info.duration(_pathname(self.valid_sound))
744
- 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)
745
778
  return val
746
779
 
747
780
  def get_corrected_duration(self):
@@ -783,8 +816,8 @@ class Recording:
783
816
 
784
817
  def _find_time_around(self, time):
785
818
  """
786
- Actually reads sound data and tries to decode it
787
- 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:
788
821
  {'version': 0, 'seconds': 44, 'minutes': 57,
789
822
  'hours': 19, 'day': 1, 'month': 3, 'year offset': 1,
790
823
  'pulse at': 670451.2217 }
@@ -794,7 +827,7 @@ class Recording:
794
827
  there = self.get_duration() + time
795
828
  else:
796
829
  there = time
797
- self._read_sound_find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
830
+ self._find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
798
831
  if self.TicTacCode_channel is None:
799
832
  return None
800
833
  else:
@@ -908,8 +941,9 @@ class Recording:
908
941
  Try to decode a TicTacCode_channel at start AND finish;
909
942
  if successful, returns a datetime.datetime instance;
910
943
  if not returns None.
911
- If successful AND self is audio, sets self.valid_sound
912
944
  """
945
+ logger.debug('for recording %s, recording.start_time %s'%(self,
946
+ self.start_time))
913
947
  if self.start_time is not None:
914
948
  return self.start_time #############################################
915
949
  cached_times = {}
@@ -949,6 +983,7 @@ class Recording:
949
983
  time_around_end)
950
984
  logger.debug('_two_times_are_coherent: %s'%coherence)
951
985
  if coherence:
986
+ logger.debug('Trial #%i successful'%(i+1))
952
987
  break
953
988
  if not coherence:
954
989
  logger.warning('found times are incoherent')
@@ -972,49 +1007,10 @@ class Recording:
972
1007
  logger.debug('recording started at %s'%start_UTC)
973
1008
  self.start_time = start_UTC
974
1009
  self.sync_position = time_around_beginning['pulse at']
975
- if self.is_audio():
976
- # self.valid_sound = self._strip_TTC_and_Null() # why now? :-)
977
- self.valid_sound = self.AVpath
1010
+ # if self.is_audio():
1011
+ # self.valid_sound = self.AVpath
978
1012
  return start_UTC
979
1013
 
980
- def _sox_strip(self, audio_file, excluded_channels) -> tempfile.NamedTemporaryFile:
981
- # building dict according to pysox.remix format.
982
- # https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
983
- # eg: 4 channels with TicTacCode_channel at #2
984
- # returns {1: [1], 2: [3], 3: [4]}
985
- # ie the number of channels drops by one and chan 2 is missing
986
- # excluded_channels is a list of Zero Based indexing chan numbers
987
- n_channels = self.device.n_chan
988
- all_channels = range(1, n_channels + 1) # from 1 to n_channels included
989
- sox_excluded_channels = [n+1 for n in excluded_channels]
990
- logger.debug('for file %s'%self.AVpath.name)
991
- logger.debug('excluded chans %s (not ZBIDX)'%sox_excluded_channels)
992
- kept_chans = [[n] for n in all_channels if n not in sox_excluded_channels]
993
- # eg [[1], [3], [4]]
994
- sox_remix_dict = dict(zip(all_channels, kept_chans))
995
- # {1: [1], 2: [3], 3: [4]} -> from 4 to 3 chan and chan 2 is dropped
996
- output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
997
- out_file = _pathname(output_fh)
998
- logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
999
- # sox_transform.set_output_format(channels=1)
1000
- sox_transform = sox.Transformer()
1001
- sox_transform.remix(sox_remix_dict)
1002
- logger.debug('sox remix transform: %s'%sox_transform)
1003
- logger.debug('sox remix dict: %s'%sox_remix_dict)
1004
- status = sox_transform.build(audio_file, out_file, return_output=True )
1005
- logger.debug('sox.build exit code %s'%str(status))
1006
- p = Popen('ffprobe %s -hide_banner'%audio_file,
1007
- shell=True, stdout=PIPE, stderr=PIPE)
1008
- stdout, stderr = p.communicate()
1009
- logger.debug('remixed input_file ffprobe:\n%s'%(stdout +
1010
- stderr).decode('utf-8'))
1011
- p = Popen('ffprobe %s -hide_banner'%out_file,
1012
- shell=True, stdout=PIPE, stderr=PIPE)
1013
- stdout, stderr = p.communicate()
1014
- logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
1015
- stderr).decode('utf-8'))
1016
- return output_fh
1017
-
1018
1014
  def _ffprobe_audio_stream(self):
1019
1015
  streams = self.probe['streams']
1020
1016
  audio_streams = [
@@ -1130,12 +1126,11 @@ class Recording:
1130
1126
  def is_audio(self):
1131
1127
  return not self.is_video()
1132
1128
 
1133
- def _read_sound_find_TicTacCode(self, time_where, chunk_length):
1129
+ def _find_TicTacCode(self, time_where, chunk_length):
1134
1130
  """
1135
- If this is called for the first time for the recording, it loads audio
1136
- data reading from self.AVpath; Split data into channels if stereo; Send
1137
- this data to Decoder object with set_sound_extract_and_sr() to find
1138
- 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
1139
1134
  accordingly (index of channel). On exit, self.decoder.sound_extract
1140
1135
  contains TicTacCode data ready to be demodulated. If not,
1141
1136
  self.TicTacCode_channel is set to None.
@@ -1164,44 +1159,18 @@ class Recording:
1164
1159
  decoder = self.decoder
1165
1160
  if decoder:
1166
1161
  decoder.clear_decoder()
1167
- # decoder.cached_convolution_fit['is clean'] = False
1168
1162
  if not self.has_audio():
1169
1163
  self.TicTacCode_channel = None
1170
1164
  return #############################################################
1171
- logger.debug('will read around %.2f sec'%time_where)
1172
- dryrun = (ffmpeg
1173
- .input(str(path))
1174
- .output('pipe:', format='s16le', acodec='pcm_s16le')
1175
- .get_args())
1176
- dryrun = ' '.join(dryrun)
1177
- logger.debug('using ffmpeg-python built args to pipe wav file into numpy array:\nffmpeg %s'%dryrun)
1178
- try:
1179
- out, _ = (ffmpeg
1180
- # .input(str(path), ss=time_where, t=chunk_length)
1181
- .input(str(path))
1182
- .output('pipe:', format='s16le', acodec='pcm_s16le')
1183
- .global_args("-loglevel", "quiet")
1184
- .global_args("-nostats")
1185
- .global_args("-hide_banner")
1186
- .run(capture_stdout=True))
1187
- data = np.frombuffer(out, np.int16)
1188
- except ffmpeg.Error as e:
1189
- print('error',e.stderr)
1190
- sound_data_var = np.std(data)
1191
- logger.debug('extracting sound, ffmpeg output:%s with variance %f'%(data,
1192
- sound_data_var))
1193
- sound_extract_position = int(self.get_samplerate()*time_where) # from sec to samples
1194
- n_chan = self.get_audio_channels_nbr()
1195
- if n_chan == 1 and not self.is_video():
1196
- logger.warning('file is sound mono')
1197
- if np.isclose(sound_data_var, 0, rtol=1e-2):
1198
- logger.warning("ffmpeg can't extract audio from %s"%self.AVpath)
1199
- # from 1D interleaved channels to [chan1, chan2, chanN]
1200
- 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))
1201
1170
  if self.TicTacCode_channel == None:
1202
1171
  logger.debug('first call, will loop through all %i channels'%len(
1203
- all_channels_data))
1204
- 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):
1205
1174
  logger.debug('testing chan %i'%i_chan)
1206
1175
  start_idx = round(time_where*self.get_samplerate())
1207
1176
  extract_length = round(chunk_length*self.get_samplerate())
@@ -1227,7 +1196,7 @@ class Recording:
1227
1196
  start_idx = round(time_where*self.get_samplerate())
1228
1197
  extract_length = round(chunk_length*self.get_samplerate())
1229
1198
  end_idx = start_idx + extract_length
1230
- chan_dat = all_channels_data[self.TicTacCode_channel]
1199
+ chan_dat = self.audio_data[self.TicTacCode_channel]
1231
1200
  extract_audio_data = chan_dat[start_idx:end_idx]
1232
1201
  decoder.set_sound_extract_and_sr(
1233
1202
  extract_audio_data,
@@ -1239,7 +1208,7 @@ class Recording:
1239
1208
  def seems_to_have_TicTacCode_at_beginning(self):
1240
1209
  if self.probe is None:
1241
1210
  return False #######################################################
1242
- self._read_sound_find_TicTacCode(TRIAL_TIMES[0][0],
1211
+ self._find_TicTacCode(TRIAL_TIMES[0][0],
1243
1212
  SOUND_EXTRACT_LENGTH)
1244
1213
  return self.TicTacCode_channel is not None
1245
1214
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.82a0
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
File without changes
File without changes
File without changes