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 +29 -29
- tictacsync/timeline.py +19 -18
- tictacsync/yaltc.py +103 -133
- {tictacsync-0.81a0.dist-info → tictacsync-0.91a0.dist-info}/METADATA +1 -1
- tictacsync-0.91a0.dist-info/RECORD +15 -0
- tictacsync-0.81a0.dist-info/RECORD +0 -15
- {tictacsync-0.81a0.dist-info → tictacsync-0.91a0.dist-info}/LICENSE +0 -0
- {tictacsync-0.81a0.dist-info → tictacsync-0.91a0.dist-info}/WHEEL +0 -0
- {tictacsync-0.81a0.dist-info → tictacsync-0.91a0.dist-info}/entry_points.txt +0 -0
- {tictacsync-0.81a0.dist-info → tictacsync-0.91a0.dist-info}/top_level.txt +0 -0
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
|
-
"
|
|
103
|
+
"path",
|
|
101
104
|
type=str,
|
|
102
105
|
nargs=1,
|
|
103
|
-
help="
|
|
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='
|
|
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='
|
|
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='
|
|
123
|
+
help='Just scan and decode')
|
|
121
124
|
parser.add_argument('--terse', action='store_true',
|
|
122
125
|
dest='terse',
|
|
123
|
-
help='
|
|
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"] == "
|
|
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.
|
|
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
|
-
|
|
182
|
-
else:
|
|
183
|
-
|
|
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("
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
else:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
307
|
-
files in
|
|
308
|
-
object (
|
|
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
|
|
346
|
+
Populates AudioStitcherVideoMerger.edited_audio,
|
|
347
|
+
a dict as {Recording : path}
|
|
347
348
|
|
|
348
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
744
|
-
print('and the
|
|
745
|
-
(device_scanner.TRACKSFN,
|
|
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
|
-
(
|
|
57
|
-
(
|
|
58
|
-
(
|
|
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
|
-
(
|
|
63
|
-
(
|
|
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
|
|
147
|
-
Decoders are instantiated by their respective Recording object.
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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('
|
|
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.
|
|
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
|
-
|
|
520
|
-
|
|
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
|
|
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.
|
|
715
|
-
val = sox.file_info.duration(_pathname(self.
|
|
716
|
-
logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.
|
|
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.
|
|
743
|
-
logger.debug('duration of
|
|
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
|
-
|
|
786
|
-
through decoder object
|
|
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.
|
|
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
|
-
|
|
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
|
|
1129
|
+
def _find_TicTacCode(self, time_where, chunk_length):
|
|
1133
1130
|
"""
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1203
|
-
for i_chan, chan_dat in enumerate(
|
|
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 =
|
|
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.
|
|
1211
|
+
self._find_TicTacCode(TRIAL_TIMES[0][0],
|
|
1242
1212
|
SOUND_EXTRACT_LENGTH)
|
|
1243
1213
|
return self.TicTacCode_channel is not None
|
|
1244
1214
|
|
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|