tictacsync 0.82a0__py3-none-any.whl → 0.95a0__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.
- tictacsync/device_scanner.py +64 -32
- tictacsync/entry.py +111 -69
- tictacsync/multi2polywav.py +1 -1
- tictacsync/timeline.py +123 -77
- tictacsync/yaltc.py +139 -152
- {tictacsync-0.82a0.dist-info → tictacsync-0.95a0.dist-info}/METADATA +1 -1
- tictacsync-0.95a0.dist-info/RECORD +15 -0
- tictacsync-0.82a0.dist-info/RECORD +0 -15
- {tictacsync-0.82a0.dist-info → tictacsync-0.95a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.82a0.dist-info → tictacsync-0.95a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.82a0.dist-info → tictacsync-0.95a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.82a0.dist-info → tictacsync-0.95a0.dist-info}/top_level.txt +0 -0
tictacsync/yaltc.py
CHANGED
|
@@ -29,21 +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
34
|
# see extract_seems_TicTacCode() for duration criterion values
|
|
35
35
|
|
|
36
36
|
CACHING = True
|
|
37
37
|
DEL_TEMP = False
|
|
38
38
|
DB_RMS_SILENCE_SOX = -58
|
|
39
|
-
MAXDRIFT =
|
|
39
|
+
MAXDRIFT = 15e-3 # in sec, for end of clip
|
|
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
|
|
@@ -52,16 +46,16 @@ SYMBOL_LENGTH = 14.286 # ms, from FSKfreqCalculator.py
|
|
|
52
46
|
N_SYMBOLS = 35 # including sync pulse
|
|
53
47
|
##################
|
|
54
48
|
|
|
55
|
-
MINIMUM_LENGTH =
|
|
49
|
+
MINIMUM_LENGTH = 8 # sec
|
|
56
50
|
TRIAL_TIMES = [ # in seconds
|
|
57
|
-
(
|
|
58
|
-
(
|
|
59
|
-
(
|
|
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
|
-
(
|
|
64
|
-
(
|
|
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
|
|
148
|
-
Decoders are instantiated by their respective Recording object.
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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):
|
|
@@ -244,7 +233,7 @@ class Decoder:
|
|
|
244
233
|
|
|
245
234
|
Uses the conditions below:
|
|
246
235
|
|
|
247
|
-
Extract duration is 1.143 s.
|
|
236
|
+
Extract duration is 1.143 s. (ie one sec + 1 symbol duration)
|
|
248
237
|
In self.word_props (list of morphology.regionprops):
|
|
249
238
|
if one region, duration should be in [0.499 0.512] sec
|
|
250
239
|
if two regions, total duration should be in [0.50 0.655]
|
|
@@ -255,13 +244,19 @@ class Decoder:
|
|
|
255
244
|
props = self.words_props
|
|
256
245
|
if len(props) not in [1,2]:
|
|
257
246
|
failing_comment = 'len(props) not in [1,2]: %i'%len(props)
|
|
247
|
+
else:
|
|
248
|
+
logger.debug('len(props), %i, is in [1,2]'%len(props))
|
|
258
249
|
if len(props) == 1:
|
|
250
|
+
logger.debug('one region')
|
|
259
251
|
w = _width(props[0])/self.samplerate
|
|
260
252
|
# self.effective_word_duration = w
|
|
261
253
|
# logger.debug('effective_word_duration %f (one region)'%w)
|
|
262
254
|
if not 0.499 < w < 0.512: # TODO: move as TOP OF FILE PARAMS
|
|
263
255
|
failing_comment = '_width %f not in [0.499 0.512]'%w
|
|
256
|
+
else:
|
|
257
|
+
logger.debug('0.499 < width < 0.512, %f'%w)
|
|
264
258
|
else: # 2 regions
|
|
259
|
+
logger.debug('two regions')
|
|
265
260
|
widths = [_width(p)/self.samplerate for p in props] # in sec
|
|
266
261
|
total_w = sum(widths)
|
|
267
262
|
# extra_window_duration = SOUND_EXTRACT_LENGTH - 1
|
|
@@ -271,19 +266,26 @@ class Decoder:
|
|
|
271
266
|
failing_comment = 'two regions duration %f not in [0.50 0.655]\n%s'%(total_w, widths)
|
|
272
267
|
# fig, ax = plt.subplots()
|
|
273
268
|
# p(ax, sound_extract_one_bit)
|
|
269
|
+
else:
|
|
270
|
+
logger.debug('0.5 < total_w < 0.656, %f'%total_w)
|
|
274
271
|
logger.debug('failing_comment: %s'%(
|
|
275
272
|
'none' if failing_comment=='' else failing_comment))
|
|
276
273
|
return failing_comment == '' # no comment = extract seems TicTacCode
|
|
277
274
|
|
|
278
275
|
def _plot_extract(self):
|
|
279
276
|
fig, ax = plt.subplots()
|
|
280
|
-
|
|
277
|
+
start = self.sound_extract_position
|
|
278
|
+
i_samples = np.arange(start, start + len(self.sound_extract))
|
|
279
|
+
yt = ax.get_yaxis_transform()
|
|
280
|
+
ax.hlines(0, 0, 1,
|
|
281
|
+
transform=yt, alpha=0.3,
|
|
282
|
+
linewidth=2, colors='black')
|
|
283
|
+
ax.plot(i_samples, self.sound_extract, marker='o', markersize='1',
|
|
281
284
|
linewidth=1.5,alpha=0.3, color='blue' )
|
|
282
|
-
ax.plot(self.sound_extract_one_bit*np.max(np.abs(self.sound_extract)),
|
|
285
|
+
ax.plot(i_samples, self.sound_extract_one_bit*np.max(np.abs(self.sound_extract)),
|
|
283
286
|
marker='o', markersize='1',
|
|
284
287
|
linewidth=1.5,alpha=0.3,color='red')
|
|
285
288
|
xt = ax.get_xaxis_transform()
|
|
286
|
-
yt = ax.get_yaxis_transform()
|
|
287
289
|
ax.hlines(self.pulse_detection_level, 0, 1,
|
|
288
290
|
transform=yt, alpha=0.3,
|
|
289
291
|
linewidth=2, colors='green')
|
|
@@ -296,7 +298,8 @@ class Decoder:
|
|
|
296
298
|
custom_lines,
|
|
297
299
|
'detection level, signal, detected region'.split(','),
|
|
298
300
|
loc='lower right')
|
|
299
|
-
ax.set_title('Finding word
|
|
301
|
+
ax.set_title('Finding word + sync pulse')
|
|
302
|
+
plt.xlabel("Position in file (samples)")
|
|
300
303
|
plt.show()
|
|
301
304
|
|
|
302
305
|
def get_time_in_sound_extract(self):
|
|
@@ -450,14 +453,7 @@ class Decoder:
|
|
|
450
453
|
start = round(0.5*symbol_length) # half symbol
|
|
451
454
|
end = start + symbol_length
|
|
452
455
|
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
456
|
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
457
|
word_start = gt_detection_level[0][0]
|
|
462
458
|
word_end = gt_detection_level[-1][0]
|
|
463
459
|
self.effective_word_duration = (word_end - word_start)/self.samplerate
|
|
@@ -474,7 +470,10 @@ class Decoder:
|
|
|
474
470
|
1e3*TEENSY_MAX_LAG))
|
|
475
471
|
logger.debug('relative audio_block gap %.2f'%(relative_gap))
|
|
476
472
|
if relative_gap > 1:
|
|
477
|
-
print('
|
|
473
|
+
print('Warning: gap between spike and word is too big for %s'%self.rec)
|
|
474
|
+
print('Audio update() gap between sync pulse and word start: ')
|
|
475
|
+
print('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
|
|
476
|
+
1e3*TEENSY_MAX_LAG))
|
|
478
477
|
symbol_width_samples_theor = self.samplerate*SYMBOL_LENGTH*1e-3
|
|
479
478
|
symbol_width_samples_eff = self.effective_word_duration * \
|
|
480
479
|
self.samplerate/(N_SYMBOLS - 1)
|
|
@@ -487,14 +486,23 @@ class Decoder:
|
|
|
487
486
|
symbols_indices = symbol_positions.round().astype(int)
|
|
488
487
|
if self.do_plots:
|
|
489
488
|
fig, ax = plt.subplots()
|
|
490
|
-
ax.
|
|
489
|
+
ax.hlines(0, 0, 1,
|
|
490
|
+
transform=ax.get_yaxis_transform(), alpha=0.3,
|
|
491
|
+
linewidth=2, colors='black')
|
|
492
|
+
start = self.sound_extract_position
|
|
493
|
+
i_samples = np.arange(start, start + len(whole_word))
|
|
494
|
+
ax.plot(i_samples, whole_word, marker='o', markersize='1',
|
|
491
495
|
linewidth=1.5,alpha=0.3, color='blue' )
|
|
492
496
|
xt = ax.get_xaxis_transform()
|
|
493
497
|
for x in symbols_indices:
|
|
494
|
-
ax.vlines(x, 0, 1,
|
|
498
|
+
ax.vlines(x + start, 0, 1,
|
|
495
499
|
transform=xt,
|
|
496
500
|
linewidth=0.6, colors='green')
|
|
497
501
|
ax.set_title('Slicing the 34 bits word:')
|
|
502
|
+
plt.xlabel("Position in file (samples)")
|
|
503
|
+
ax.vlines(start, 0, 1,
|
|
504
|
+
transform=xt,
|
|
505
|
+
linewidth=0.6, colors='red')
|
|
498
506
|
plt.show()
|
|
499
507
|
slice_width = round(SYMBOL_LENGTH*1e-3*self.samplerate)
|
|
500
508
|
slices = [whole_word[i:i+slice_width] for i in symbols_indices]
|
|
@@ -517,8 +525,10 @@ class Recording:
|
|
|
517
525
|
AVpath : pathlib.path
|
|
518
526
|
path of video+sound+TicTacCode file, relative to working directory
|
|
519
527
|
|
|
520
|
-
|
|
521
|
-
|
|
528
|
+
audio_data : in16 numpy.array of shape [nchan] x [N samples]
|
|
529
|
+
|
|
530
|
+
# valid_sound : pathlib.path
|
|
531
|
+
# path of sound file stripped of silent and TicTacCode channels
|
|
522
532
|
|
|
523
533
|
device : Device
|
|
524
534
|
identifies the device used for the recording, set in __init__()
|
|
@@ -532,7 +542,7 @@ class Recording:
|
|
|
532
542
|
|
|
533
543
|
TicTacCode_channel : int
|
|
534
544
|
which channel is sync track. 0 is first channel,
|
|
535
|
-
set in
|
|
545
|
+
set in _find_TicTacCode().
|
|
536
546
|
|
|
537
547
|
decoder : yaltc.decoder
|
|
538
548
|
associated decoder object, if file is audiovideo
|
|
@@ -556,7 +566,7 @@ class Recording:
|
|
|
556
566
|
implicitly True for each video recordings (but not set)
|
|
557
567
|
|
|
558
568
|
device_relative_speed : float
|
|
559
|
-
|
|
569
|
+
Set by
|
|
560
570
|
the ratio of the recording device clock speed relative to the
|
|
561
571
|
video recorder clock device, in order to correct clock drift with
|
|
562
572
|
pysox tempo transform. If value < 1.0 then the recording is
|
|
@@ -591,13 +601,15 @@ class Recording:
|
|
|
591
601
|
|
|
592
602
|
def __init__(self, media, do_plots=False):
|
|
593
603
|
"""
|
|
604
|
+
Set AVfilename string and check if file exists, does not read any
|
|
605
|
+
media data right away but uses ffprobe to parses the file and sets
|
|
606
|
+
probe attribute.
|
|
607
|
+
|
|
608
|
+
Logs a warning and sets Recording.decoder to None if ffprobe cant
|
|
609
|
+
interpret the file or if file has no audio. If file contains audio,
|
|
610
|
+
initialise Recording.decoder(but doesnt try to decode anything yet).
|
|
611
|
+
|
|
594
612
|
If multifile recording, AVfilename is sox merged audio file;
|
|
595
|
-
Set AVfilename string and check if file exists, does not read
|
|
596
|
-
any media data right away but uses ffprobe to parses the file and
|
|
597
|
-
sets probe attribute.
|
|
598
|
-
Logs a warning if ffprobe cant interpret the file or if file
|
|
599
|
-
has no audio; if file contains audio, instantiates a Decoder object
|
|
600
|
-
(but doesnt try to decode anything yet)
|
|
601
613
|
|
|
602
614
|
Parameters
|
|
603
615
|
----------
|
|
@@ -635,7 +647,7 @@ class Recording:
|
|
|
635
647
|
self.TicTacCode_channel = None
|
|
636
648
|
self.is_reference = False
|
|
637
649
|
self.device_relative_speed = 1.0
|
|
638
|
-
self.valid_sound = None
|
|
650
|
+
# self.valid_sound = None
|
|
639
651
|
self.final_synced_file = None
|
|
640
652
|
self.synced_audio = None
|
|
641
653
|
self.new_rec_name = media.path.name
|
|
@@ -671,11 +683,44 @@ class Recording:
|
|
|
671
683
|
print('Recording init failed: %s'%recording_init_fail)
|
|
672
684
|
self.probe = None
|
|
673
685
|
self.decoder = None
|
|
686
|
+
return
|
|
674
687
|
logger.debug('ffprobe found: %s'%self.probe)
|
|
675
688
|
logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
|
|
689
|
+
self._read_audio_data()
|
|
690
|
+
|
|
691
|
+
def _read_audio_data(self):
|
|
692
|
+
# sets Recording.audio_data
|
|
693
|
+
dryrun = (ffmpeg
|
|
694
|
+
.input(str(self.AVpath))
|
|
695
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
696
|
+
.get_args())
|
|
697
|
+
dryrun = ' '.join(dryrun)
|
|
698
|
+
logger.debug('using ffmpeg-python built args to pipe audio stream into numpy array:\nffmpeg %s'%dryrun)
|
|
699
|
+
try:
|
|
700
|
+
out, _ = (ffmpeg
|
|
701
|
+
# .input(str(path), ss=time_where, t=chunk_length)
|
|
702
|
+
.input(str(self.AVpath))
|
|
703
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
704
|
+
.global_args("-loglevel", "quiet")
|
|
705
|
+
.global_args("-nostats")
|
|
706
|
+
.global_args("-hide_banner")
|
|
707
|
+
.run(capture_stdout=True))
|
|
708
|
+
data = np.frombuffer(out, np.int16)
|
|
709
|
+
except ffmpeg.Error as e:
|
|
710
|
+
print('error',e.stderr)
|
|
711
|
+
n_chan = self.get_audio_channels_nbr()
|
|
712
|
+
if n_chan == 1 and not self.is_video():
|
|
713
|
+
logger.error('file is sound mono')
|
|
714
|
+
if np.isclose(np.std(data), 0, rtol=1e-2):
|
|
715
|
+
logger.error("ffmpeg can't extract audio from %s"%self.AVpath)
|
|
716
|
+
# from 1D interleaved channels to [chan1, chan2, chanN]
|
|
717
|
+
self.audio_data = data.reshape(int(len(data)/n_chan),n_chan).T
|
|
718
|
+
logger.debug('Recording.audio_data: %s of shape %s'%(self.audio_data,
|
|
719
|
+
self.audio_data.shape))
|
|
676
720
|
|
|
677
721
|
def __repr__(self):
|
|
678
|
-
return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
722
|
+
# return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
723
|
+
return _pathname(self.new_rec_name)
|
|
679
724
|
|
|
680
725
|
def _check_for_camera_error_correction(self):
|
|
681
726
|
# look for a file number
|
|
@@ -712,9 +757,9 @@ class Recording:
|
|
|
712
757
|
recording duration in seconds.
|
|
713
758
|
|
|
714
759
|
"""
|
|
715
|
-
if self.
|
|
716
|
-
val = sox.file_info.duration(_pathname(self.
|
|
717
|
-
logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.
|
|
760
|
+
if self.is_audio():
|
|
761
|
+
val = sox.file_info.duration(_pathname(self.AVpath))
|
|
762
|
+
logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.AVpath)))
|
|
718
763
|
return val #########################################################
|
|
719
764
|
else:
|
|
720
765
|
if self.probe is None:
|
|
@@ -740,8 +785,8 @@ class Recording:
|
|
|
740
785
|
recording duration in seconds.
|
|
741
786
|
|
|
742
787
|
"""
|
|
743
|
-
val = sox.file_info.duration(_pathname(self.
|
|
744
|
-
logger.debug('duration of
|
|
788
|
+
val = sox.file_info.duration(_pathname(self.AVpath))
|
|
789
|
+
logger.debug('duration of AVpath %f'%val)
|
|
745
790
|
return val
|
|
746
791
|
|
|
747
792
|
def get_corrected_duration(self):
|
|
@@ -754,13 +799,14 @@ class Recording:
|
|
|
754
799
|
|
|
755
800
|
def needs_dedrifting(self):
|
|
756
801
|
rel_sp = self.device_relative_speed
|
|
757
|
-
if rel_sp > 1:
|
|
758
|
-
|
|
759
|
-
else:
|
|
760
|
-
|
|
802
|
+
# if rel_sp > 1:
|
|
803
|
+
# delta = (rel_sp - 1)*self.get_original_duration()
|
|
804
|
+
# else:
|
|
805
|
+
# delta = (1 - rel_sp)*self.get_original_duration()
|
|
806
|
+
delta = abs((1 - rel_sp)*self.get_original_duration())
|
|
761
807
|
logger.debug('%s delta drift %.2f ms'%(str(self), delta*1e3))
|
|
762
808
|
if delta > MAXDRIFT:
|
|
763
|
-
print('[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
|
|
809
|
+
print('\n[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
|
|
764
810
|
(self.AVpath, delta*1e3))
|
|
765
811
|
return delta > MAXDRIFT, delta
|
|
766
812
|
|
|
@@ -783,8 +829,8 @@ class Recording:
|
|
|
783
829
|
|
|
784
830
|
def _find_time_around(self, time):
|
|
785
831
|
"""
|
|
786
|
-
|
|
787
|
-
through decoder object
|
|
832
|
+
Tries to decode FSK around time (in sec)
|
|
833
|
+
through decoder object; if successful return a time dict, eg:
|
|
788
834
|
{'version': 0, 'seconds': 44, 'minutes': 57,
|
|
789
835
|
'hours': 19, 'day': 1, 'month': 3, 'year offset': 1,
|
|
790
836
|
'pulse at': 670451.2217 }
|
|
@@ -794,7 +840,7 @@ class Recording:
|
|
|
794
840
|
there = self.get_duration() + time
|
|
795
841
|
else:
|
|
796
842
|
there = time
|
|
797
|
-
self.
|
|
843
|
+
self._find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
|
|
798
844
|
if self.TicTacCode_channel is None:
|
|
799
845
|
return None
|
|
800
846
|
else:
|
|
@@ -908,9 +954,13 @@ class Recording:
|
|
|
908
954
|
Try to decode a TicTacCode_channel at start AND finish;
|
|
909
955
|
if successful, returns a datetime.datetime instance;
|
|
910
956
|
if not returns None.
|
|
911
|
-
If successful AND self is audio, sets self.valid_sound
|
|
912
957
|
"""
|
|
958
|
+
logger.debug('for %s, recording.start_time %s'%(self,
|
|
959
|
+
self.start_time))
|
|
960
|
+
if self.decoder is None:
|
|
961
|
+
return None # ffprobe failes or file too short, see __init__
|
|
913
962
|
if self.start_time is not None:
|
|
963
|
+
logger.debug('Recording.start_time already found %s'%self.start_time)
|
|
914
964
|
return self.start_time #############################################
|
|
915
965
|
cached_times = {}
|
|
916
966
|
def find_time(t_sec):
|
|
@@ -935,8 +985,8 @@ class Recording:
|
|
|
935
985
|
len(TRIAL_TIMES)))
|
|
936
986
|
# time_around_beginning = self._find_time_around(near_beg)
|
|
937
987
|
time_around_beginning = find_time(near_beg)
|
|
938
|
-
if self.TicTacCode_channel is None:
|
|
939
|
-
|
|
988
|
+
# if self.TicTacCode_channel is None:
|
|
989
|
+
# return None ####################################################
|
|
940
990
|
logger.debug('Trial #%i, end at %f'%(i+1, near_end))
|
|
941
991
|
# time_around_end = self._find_time_around(near_end)
|
|
942
992
|
time_around_end = find_time(near_end)
|
|
@@ -949,6 +999,7 @@ class Recording:
|
|
|
949
999
|
time_around_end)
|
|
950
1000
|
logger.debug('_two_times_are_coherent: %s'%coherence)
|
|
951
1001
|
if coherence:
|
|
1002
|
+
logger.debug('Trial #%i successful'%(i+1))
|
|
952
1003
|
break
|
|
953
1004
|
if not coherence:
|
|
954
1005
|
logger.warning('found times are incoherent')
|
|
@@ -972,49 +1023,10 @@ class Recording:
|
|
|
972
1023
|
logger.debug('recording started at %s'%start_UTC)
|
|
973
1024
|
self.start_time = start_UTC
|
|
974
1025
|
self.sync_position = time_around_beginning['pulse at']
|
|
975
|
-
if self.is_audio():
|
|
976
|
-
|
|
977
|
-
self.valid_sound = self.AVpath
|
|
1026
|
+
# if self.is_audio():
|
|
1027
|
+
# self.valid_sound = self.AVpath
|
|
978
1028
|
return start_UTC
|
|
979
1029
|
|
|
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
1030
|
def _ffprobe_audio_stream(self):
|
|
1019
1031
|
streams = self.probe['streams']
|
|
1020
1032
|
audio_streams = [
|
|
@@ -1052,6 +1064,7 @@ class Recording:
|
|
|
1052
1064
|
return int(ppm)
|
|
1053
1065
|
|
|
1054
1066
|
def get_speed_ratio(self, videoclip):
|
|
1067
|
+
# ratio between real samplerates of audio and videoclip
|
|
1055
1068
|
nominal = self.get_samplerate()
|
|
1056
1069
|
true = self.true_samplerate
|
|
1057
1070
|
ratio = true/nominal
|
|
@@ -1071,9 +1084,10 @@ class Recording:
|
|
|
1071
1084
|
string = self._ffprobe_video_stream()['avg_frame_rate']
|
|
1072
1085
|
return eval(string) # eg eval(24000/1001)
|
|
1073
1086
|
|
|
1074
|
-
def
|
|
1075
|
-
# returns a
|
|
1087
|
+
def get_start_timecode_string(self, with_offset=0):
|
|
1088
|
+
# returns a HH:MM:SS:FR string
|
|
1076
1089
|
start_datetime = self.get_start_time()
|
|
1090
|
+
# logger.debug('CLI_offset %s'%CLI_offset)
|
|
1077
1091
|
logger.debug('start_datetime %s'%start_datetime)
|
|
1078
1092
|
start_datetime += timedelta(seconds=with_offset)
|
|
1079
1093
|
logger.debug('shifted start_datetime %s (offset %f)'%(start_datetime,
|
|
@@ -1130,12 +1144,11 @@ class Recording:
|
|
|
1130
1144
|
def is_audio(self):
|
|
1131
1145
|
return not self.is_video()
|
|
1132
1146
|
|
|
1133
|
-
def
|
|
1147
|
+
def _find_TicTacCode(self, time_where, chunk_length):
|
|
1134
1148
|
"""
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
which channel contains a TicTacCode track and sets TicTacCode_channel
|
|
1149
|
+
Extracts a chunk from Recording.audio_data and sends it to
|
|
1150
|
+
Recording.decoder object with set_sound_extract_and_sr() to find which
|
|
1151
|
+
channel contains a TicTacCode track and sets TicTacCode_channel
|
|
1139
1152
|
accordingly (index of channel). On exit, self.decoder.sound_extract
|
|
1140
1153
|
contains TicTacCode data ready to be demodulated. If not,
|
|
1141
1154
|
self.TicTacCode_channel is set to None.
|
|
@@ -1164,44 +1177,18 @@ class Recording:
|
|
|
1164
1177
|
decoder = self.decoder
|
|
1165
1178
|
if decoder:
|
|
1166
1179
|
decoder.clear_decoder()
|
|
1167
|
-
# decoder.cached_convolution_fit['is clean'] = False
|
|
1168
1180
|
if not self.has_audio():
|
|
1169
1181
|
self.TicTacCode_channel = None
|
|
1170
1182
|
return #############################################################
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
|
1183
|
+
sound_data_var = np.std(self.audio_data)
|
|
1184
|
+
sound_extract_position = int(self.get_samplerate()*time_where)
|
|
1185
|
+
logger.debug('extracting sound at %i with variance %f'%(
|
|
1186
|
+
sound_extract_position,
|
|
1187
|
+
sound_data_var))
|
|
1201
1188
|
if self.TicTacCode_channel == None:
|
|
1202
1189
|
logger.debug('first call, will loop through all %i channels'%len(
|
|
1203
|
-
|
|
1204
|
-
for i_chan, chan_dat in enumerate(
|
|
1190
|
+
self.audio_data))
|
|
1191
|
+
for i_chan, chan_dat in enumerate(self.audio_data):
|
|
1205
1192
|
logger.debug('testing chan %i'%i_chan)
|
|
1206
1193
|
start_idx = round(time_where*self.get_samplerate())
|
|
1207
1194
|
extract_length = round(chunk_length*self.get_samplerate())
|
|
@@ -1227,7 +1214,7 @@ class Recording:
|
|
|
1227
1214
|
start_idx = round(time_where*self.get_samplerate())
|
|
1228
1215
|
extract_length = round(chunk_length*self.get_samplerate())
|
|
1229
1216
|
end_idx = start_idx + extract_length
|
|
1230
|
-
chan_dat =
|
|
1217
|
+
chan_dat = self.audio_data[self.TicTacCode_channel]
|
|
1231
1218
|
extract_audio_data = chan_dat[start_idx:end_idx]
|
|
1232
1219
|
decoder.set_sound_extract_and_sr(
|
|
1233
1220
|
extract_audio_data,
|
|
@@ -1239,7 +1226,7 @@ class Recording:
|
|
|
1239
1226
|
def seems_to_have_TicTacCode_at_beginning(self):
|
|
1240
1227
|
if self.probe is None:
|
|
1241
1228
|
return False #######################################################
|
|
1242
|
-
self.
|
|
1229
|
+
self._find_TicTacCode(TRIAL_TIMES[0][0],
|
|
1243
1230
|
SOUND_EXTRACT_LENGTH)
|
|
1244
1231
|
return self.TicTacCode_channel is not None
|
|
1245
1232
|
|
|
@@ -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=kkwiO6qFNyBwqyZGqsf2q8WUqu4BBMFqFaUFFTeFfiY,29034
|
|
4
|
+
tictacsync/entry.py,sha256=fklCyTgqxJPWzKsn1ow3IxLPq8obv-N8Z72ieRzulCI,13352
|
|
5
|
+
tictacsync/multi2polywav.py,sha256=BsZxUjZo2Px6opKpFlgcvdZuUKDANEVTdapuWrX1jKw,7287
|
|
6
|
+
tictacsync/remergemix.py,sha256=FJTMipIS0O7mMl_tr8BhuYqWvanSydvjGkFCEd-jaDk,9829
|
|
7
|
+
tictacsync/synciso.py,sha256=XmUcdUF9rl4VdCm7XW4PeYWYWM0vgAY9dC2hapoul9g,4821
|
|
8
|
+
tictacsync/timeline.py,sha256=YhnNqYnbTTf2YVN5nLlQY62UA4z9fTij6x18tR_u3Nc,60424
|
|
9
|
+
tictacsync/yaltc.py,sha256=EzAF5VnMMeBp_o2AOs7wj0p31mElcb6D57YJVKUbOxM,53400
|
|
10
|
+
tictacsync-0.95a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
11
|
+
tictacsync-0.95a0.dist-info/METADATA,sha256=oHUqw0Q9bboCClpSRB8wNs5rQv_Ex5RnYefGLV2bpik,5502
|
|
12
|
+
tictacsync-0.95a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
13
|
+
tictacsync-0.95a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
14
|
+
tictacsync-0.95a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
15
|
+
tictacsync-0.95a0.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=Uf-vSHVmGfWnyXq8Ee4bWLzzJMoo9G1iuRZKOeYk8aE,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=HvSy8_0auI2-jE1rZ9Bu9PcCIAHhNyUv9tpL75Po-uk,57877
|
|
9
|
-
tictacsync/yaltc.py,sha256=zXbAvM00t1-VXRooH28AO3AcYgLSoo0p2Fq2LN4SALU,54457
|
|
10
|
-
tictacsync-0.82a0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
|
|
11
|
-
tictacsync-0.82a0.dist-info/METADATA,sha256=z4G9ejQfvAURTp340mIdCbhoAkV-6pTJdjd8TN8UgDI,5502
|
|
12
|
-
tictacsync-0.82a0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
13
|
-
tictacsync-0.82a0.dist-info/entry_points.txt,sha256=g3tdFFrVRcrKpuyKOCLUVBMgYfV65q9kpLZUOD_XCKg,139
|
|
14
|
-
tictacsync-0.82a0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
|
|
15
|
-
tictacsync-0.82a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|