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.
- {tictacsync-0.82a0/tictacsync.egg-info → tictacsync-0.91a0}/PKG-INFO +1 -1
- {tictacsync-0.82a0 → tictacsync-0.91a0}/setup.py +1 -1
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/entry.py +29 -29
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/timeline.py +16 -18
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/yaltc.py +101 -132
- {tictacsync-0.82a0 → tictacsync-0.91a0/tictacsync.egg-info}/PKG-INFO +1 -1
- {tictacsync-0.82a0 → tictacsync-0.91a0}/LICENSE +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/README.md +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/setup.cfg +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/__init__.py +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/device_scanner.py +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/multi2polywav.py +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync/remergemix.py +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/SOURCES.txt +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/dependency_links.txt +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/entry_points.txt +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/not-zip-safe +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/requires.txt +0 -0
- {tictacsync-0.82a0 → tictacsync-0.91a0}/tictacsync.egg-info/top_level.txt +0 -0
|
@@ -32,7 +32,7 @@ setup(
|
|
|
32
32
|
'multi2polywav = tictacsync.multi2polywav:main',
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
|
-
version = '0.
|
|
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
|
-
"
|
|
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
|
|
@@ -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 == []:
|
|
@@ -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
|
-
(
|
|
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):
|
|
@@ -277,13 +266,18 @@ class Decoder:
|
|
|
277
266
|
|
|
278
267
|
def _plot_extract(self):
|
|
279
268
|
fig, ax = plt.subplots()
|
|
280
|
-
|
|
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
|
|
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('
|
|
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.
|
|
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
|
-
|
|
521
|
-
|
|
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
|
|
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.
|
|
716
|
-
val = sox.file_info.duration(_pathname(self.
|
|
717
|
-
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)))
|
|
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.
|
|
744
|
-
logger.debug('duration of
|
|
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
|
-
|
|
787
|
-
through decoder object
|
|
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.
|
|
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
|
-
|
|
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
|
|
1129
|
+
def _find_TicTacCode(self, time_where, chunk_length):
|
|
1134
1130
|
"""
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1204
|
-
for i_chan, chan_dat in enumerate(
|
|
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 =
|
|
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.
|
|
1211
|
+
self._find_TicTacCode(TRIAL_TIMES[0][0],
|
|
1243
1212
|
SOUND_EXTRACT_LENGTH)
|
|
1244
1213
|
return self.TicTacCode_channel is not None
|
|
1245
1214
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|