tictacsync 0.1a14__py3-none-any.whl → 1.4.4b0__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/device_scanner.py +362 -169
- tictacsync/entry.py +240 -135
- tictacsync/mamconf.py +157 -0
- tictacsync/mamdav.py +642 -0
- tictacsync/mamreap.py +481 -0
- tictacsync/mamsync.py +343 -0
- tictacsync/multi2polywav.py +21 -14
- tictacsync/timeline.py +1126 -442
- tictacsync/yaltc.py +895 -1067
- tictacsync-1.4.4b0.dist-info/METADATA +118 -0
- tictacsync-1.4.4b0.dist-info/RECORD +16 -0
- tictacsync-1.4.4b0.dist-info/entry_points.txt +7 -0
- tictacsync/LTCcheck.py +0 -394
- tictacsync-0.1a14.dist-info/METADATA +0 -96
- tictacsync-0.1a14.dist-info/RECORD +0 -13
- tictacsync-0.1a14.dist-info/entry_points.txt +0 -4
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/LICENSE +0 -0
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/WHEEL +0 -0
- {tictacsync-0.1a14.dist-info → tictacsync-1.4.4b0.dist-info}/top_level.txt +0 -0
tictacsync/yaltc.py
CHANGED
|
@@ -5,7 +5,7 @@ import lmfit, tempfile
|
|
|
5
5
|
# from skimage.morphology import closing, square
|
|
6
6
|
from numpy import arcsin, sin, pi
|
|
7
7
|
from matplotlib.lines import Line2D
|
|
8
|
-
import math, re, os
|
|
8
|
+
import math, re, os, sys, itertools
|
|
9
9
|
import sox
|
|
10
10
|
from subprocess import Popen, PIPE
|
|
11
11
|
from pathlib import Path
|
|
@@ -15,109 +15,82 @@ logging.config.dictConfig({
|
|
|
15
15
|
'disable_existing_loggers': True,
|
|
16
16
|
}) # for sox "output file already exists and will be overwritten on build"
|
|
17
17
|
from datetime import datetime, timezone, timedelta
|
|
18
|
+
from pprint import pformat
|
|
18
19
|
from collections import deque
|
|
19
20
|
from loguru import logger
|
|
20
|
-
from
|
|
21
|
+
from skimage.morphology import closing, erosion, remove_small_objects
|
|
22
|
+
from skimage.measure import regionprops, label
|
|
21
23
|
import ffmpeg, shutil
|
|
22
|
-
# import opentimelineio as otio
|
|
23
24
|
from rich import print
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
# from rich.text import Text
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
try:
|
|
29
|
+
from . import device_scanner
|
|
30
|
+
except:
|
|
31
|
+
import device_scanner
|
|
24
32
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
|
|
34
|
+
|
|
35
|
+
# see extract_seems_TicTacCode() for duration criterion values
|
|
36
|
+
|
|
37
|
+
TRACKSFILE = 'tracks.txt'
|
|
38
|
+
SILENT_TRACK_TOKENS = '-0n'
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
CACHING = True
|
|
33
|
-
DEL_TEMP =
|
|
34
|
-
|
|
35
|
-
MAXDRIFT = 10e-3 # in sec, normally 10e-3 (10 ms)
|
|
36
|
-
# MAXDRIFT = 0 # in sec, normally 10e-3 (10 ms)
|
|
42
|
+
DEL_TEMP = False
|
|
43
|
+
MAXDRIFT = 15e-3 # in sec, for end of clip
|
|
37
44
|
|
|
38
|
-
SAFE_SILENCE_WINDOW_WIDTH = 400 # ms, not the full 500 ms, to accommodate decay
|
|
39
|
-
# used in _get_silent_zone_indices()
|
|
40
|
-
WORDWIDTHFACTOR = 2
|
|
41
|
-
# see _get_word_width_parameters()
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
################## pasted from FSKfreqCalculator.py output:
|
|
47
|
+
F1 = 630.00 # Hertz
|
|
48
|
+
F2 = 1190.00 # Hz , both from FSKfreqCalculator output
|
|
49
|
+
SYMBOL_LENGTH = 14.286 # ms, from FSKfreqCalculator.py
|
|
50
|
+
N_SYMBOLS = 35 # including sync pulse
|
|
51
|
+
##################
|
|
44
52
|
|
|
45
|
-
MINIMUM_LENGTH =
|
|
53
|
+
MINIMUM_LENGTH = 8 # sec
|
|
46
54
|
TRIAL_TIMES = [ # in seconds
|
|
47
|
-
(
|
|
48
|
-
(
|
|
49
|
-
(
|
|
55
|
+
(3.5, -2),
|
|
56
|
+
(3.5, -3.5),
|
|
57
|
+
(3.5, -5),
|
|
50
58
|
(2, -2),
|
|
51
59
|
(2, -3.5),
|
|
52
60
|
(2, -5),
|
|
53
|
-
(
|
|
54
|
-
(
|
|
61
|
+
(0.5, -2),
|
|
62
|
+
(0.5, -3.5),
|
|
55
63
|
]
|
|
56
|
-
SOUND_EXTRACT_LENGTH = 1
|
|
64
|
+
SOUND_EXTRACT_LENGTH = (10*SYMBOL_LENGTH*1e-3 + 1) # second
|
|
57
65
|
SYMBOL_LENGTH_TOLERANCE = 0.07 # relative
|
|
58
66
|
FSK_TOLERANCE = 60 # Hz
|
|
59
67
|
SAMD21_LATENCY = 63 # microseconds, for DAC conversion
|
|
60
68
|
YEAR_ZERO = 2021
|
|
61
69
|
|
|
62
|
-
################## pasted from FSKfreqCalculator.py output:
|
|
63
|
-
F1 = 630.00 # Hertz
|
|
64
|
-
F2 = 1190.00 # Hz , both from FSKfreqCalculator output
|
|
65
|
-
SYMBOL_LENGTH = 14.286 # ms, from FSKfreqCalculator.py
|
|
66
|
-
N_SYMBOLS_SAMD21 = 35 # including sync pulse
|
|
67
|
-
##################
|
|
68
70
|
|
|
69
71
|
BPF_LOW_FRQ, BPF_HIGH_FRQ = (0.5*F1, 2*F2)
|
|
70
72
|
|
|
71
|
-
# try:
|
|
72
|
-
# layouts, _ = (
|
|
73
|
-
# ffmpeg.input('').output('')
|
|
74
|
-
# .global_args("-loglevel", "quiet","-nostats","-hide_banner","-layouts")
|
|
75
|
-
# .run(capture_stdout=True)
|
|
76
|
-
# )
|
|
77
|
-
# except ffmpeg.Error as e:
|
|
78
|
-
# print('error',e.stderr)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# names_str, layouts = [l.split('\n') for l in
|
|
83
|
-
# layouts.decode("utf-8").split('DECOMPOSITION')]
|
|
84
|
-
# [names_str.pop(0) for i in range(2)]
|
|
85
|
-
# [names_str.pop(-1) for i in range(3)]
|
|
86
|
-
# layouts.pop(0)
|
|
87
|
-
# layouts.pop(-1)
|
|
88
|
-
# channel_name_regex = re.compile(r'(\w+)\s+(.+)')
|
|
89
|
-
# layouts_regex = re.compile(r'(\S+)\s+(.+)')
|
|
90
|
-
# FFMPEG_CHAN_NAMES = [
|
|
91
|
-
# channel_name_regex.match(name).group(1)
|
|
92
|
-
# for name in names_str
|
|
93
|
-
# ]
|
|
94
|
-
# FFMPEG_LAYO_NAMES = dict([
|
|
95
|
-
# layouts_regex.match(lay_out).groups()
|
|
96
|
-
# for lay_out in layouts
|
|
97
|
-
# ])
|
|
98
|
-
|
|
99
|
-
# """
|
|
100
|
-
# FFMPEG_CHAN_NAMES are the names used by ffmpeg for channels, eg: FL, FR, FC for
|
|
101
|
-
# front left, front right and front center.
|
|
102
|
-
# FFMPEG_LAYO_NAMES are the order of channels for each layout, eg: stereo is FL+FR
|
|
103
|
-
# and 5.1 is FL+FR+FC+LFE+BL+BR. Layout names are the dict keys.
|
|
104
|
-
# Run ffmpeg -layouts for details.
|
|
105
|
-
|
|
106
|
-
# """
|
|
107
73
|
|
|
108
74
|
# utility for accessing pathnames
|
|
109
75
|
def _pathname(tempfile_or_path):
|
|
76
|
+
# always returns a str
|
|
110
77
|
if isinstance(tempfile_or_path, type('')):
|
|
111
|
-
return tempfile_or_path
|
|
78
|
+
return tempfile_or_path ################################################
|
|
112
79
|
if isinstance(tempfile_or_path, Path):
|
|
113
|
-
return str(tempfile_or_path)
|
|
80
|
+
return str(tempfile_or_path) ###########################################
|
|
114
81
|
if isinstance(tempfile_or_path, tempfile._TemporaryFileWrapper):
|
|
115
|
-
return tempfile_or_path.name
|
|
82
|
+
return tempfile_or_path.name ###########################################
|
|
116
83
|
else:
|
|
117
84
|
raise Exception('%s should be Path or tempfile... is %s'%(
|
|
118
85
|
tempfile_or_path,
|
|
119
86
|
type(tempfile_or_path)))
|
|
120
87
|
|
|
88
|
+
# for skimage.measure.regionprops
|
|
89
|
+
def _width(region):
|
|
90
|
+
_,x1,_,x2 = region.bbox
|
|
91
|
+
return x2-x1
|
|
92
|
+
|
|
93
|
+
|
|
121
94
|
def to_precision(x,p):
|
|
122
95
|
"""
|
|
123
96
|
returns a string representation of x formatted with a precision of p
|
|
@@ -127,7 +100,7 @@ def to_precision(x,p):
|
|
|
127
100
|
"""
|
|
128
101
|
x = float(x)
|
|
129
102
|
if x == 0.:
|
|
130
|
-
return "0." + "0"*(p-1)
|
|
103
|
+
return "0." + "0"*(p-1) ################################################
|
|
131
104
|
out = []
|
|
132
105
|
if x < 0:
|
|
133
106
|
out.append("-")
|
|
@@ -168,19 +141,46 @@ def to_precision(x,p):
|
|
|
168
141
|
|
|
169
142
|
return "".join(out)
|
|
170
143
|
|
|
144
|
+
def read_audio_data_from_file(file, n_channels):
|
|
145
|
+
"""
|
|
146
|
+
reads file and returns a numpy.array of shape (N1 channels, N2 samples)
|
|
147
|
+
where N1 >= 2 (minimaly solo track + TC)
|
|
148
|
+
"""
|
|
149
|
+
dryrun = (ffmpeg
|
|
150
|
+
.input(_pathname(file))
|
|
151
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
152
|
+
.get_args())
|
|
153
|
+
dryrun = ' '.join(dryrun)
|
|
154
|
+
logger.debug('using ffmpeg-python built args to pipe audio stream into numpy array:\nffmpeg %s'%dryrun)
|
|
155
|
+
try:
|
|
156
|
+
out, _ = (ffmpeg
|
|
157
|
+
# .input(str(path), ss=time_where, t=chunk_length)
|
|
158
|
+
# .input(str(self.AVpath))
|
|
159
|
+
.input(_pathname(file))
|
|
160
|
+
.output('pipe:', format='s16le', acodec='pcm_s16le')
|
|
161
|
+
.global_args("-loglevel", "quiet")
|
|
162
|
+
.global_args("-nostats")
|
|
163
|
+
.global_args("-hide_banner")
|
|
164
|
+
.run(capture_stdout=True))
|
|
165
|
+
data = np.frombuffer(out, np.int16)
|
|
166
|
+
except ffmpeg.Error as e:
|
|
167
|
+
print('error',e.stderr)
|
|
168
|
+
# transform 1D interleaved channels to [chan1, chan2, chanN]
|
|
169
|
+
return data.reshape(int(len(data)/n_channels),n_channels).T
|
|
170
|
+
|
|
171
|
+
|
|
171
172
|
class Decoder:
|
|
172
173
|
"""
|
|
173
|
-
Object encapsulating DSP processes to demodulate
|
|
174
|
-
Decoders are instantiated by their respective Recording object.
|
|
175
|
-
plots on demand for diagnostic purposes.
|
|
174
|
+
Object encapsulating DSP processes to demodulate TicTacCode track from audio
|
|
175
|
+
file; Decoders are instantiated by their respective Recording object.
|
|
176
|
+
Produces plots on demand for diagnostic purposes.
|
|
176
177
|
|
|
177
178
|
Attributes:
|
|
178
179
|
|
|
179
|
-
sound_extract : numpy.ndarray of int16
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
track.
|
|
180
|
+
sound_extract : 1d numpy.ndarray of int16
|
|
181
|
+
length determined by SOUND_EXTRACT_LENGTH (sec). Could be anywhere
|
|
182
|
+
in the audio file (start, end, etc...) Set by Recording object.
|
|
183
|
+
This audio signal might or might not be the TicTacCode track.
|
|
184
184
|
|
|
185
185
|
sound_extract_position : int
|
|
186
186
|
where the sound_extract is located in the file, samples
|
|
@@ -191,8 +191,8 @@ class Decoder:
|
|
|
191
191
|
rec : Recording
|
|
192
192
|
recording on which the decoder is working
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
effective_word_duration : float
|
|
195
|
+
duration of a word, influenced by ucontroller clock
|
|
196
196
|
|
|
197
197
|
pulse_detection_level : float
|
|
198
198
|
level used to detect sync pulse
|
|
@@ -207,13 +207,9 @@ class Decoder:
|
|
|
207
207
|
detected_pulse_position : int
|
|
208
208
|
pulse position (samples) relative to the start of self.sound_extract
|
|
209
209
|
|
|
210
|
-
cached_convolution_fit : dict
|
|
211
|
-
if _fit_triangular_signal_to_convoluted_env() has already been called,
|
|
212
|
-
will use cached values if sound_extract_position is the same.
|
|
213
|
-
|
|
214
210
|
"""
|
|
215
211
|
|
|
216
|
-
def __init__(self, aRec):
|
|
212
|
+
def __init__(self, aRec, do_plots):
|
|
217
213
|
"""
|
|
218
214
|
Initialises Decoder
|
|
219
215
|
|
|
@@ -223,584 +219,333 @@ class Decoder:
|
|
|
223
219
|
|
|
224
220
|
"""
|
|
225
221
|
self.rec = aRec
|
|
222
|
+
self.do_plots = do_plots
|
|
226
223
|
self.clear_decoder()
|
|
227
224
|
|
|
228
225
|
def clear_decoder(self):
|
|
229
226
|
self.sound_data_extract = None
|
|
230
|
-
self.cached_convolution_fit = {'sound_extract_position': None}
|
|
231
227
|
self.pulse_detection_level = None
|
|
232
|
-
self.silent_zone_indices = None
|
|
233
228
|
self.detected_pulse_position = None
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def set_sound_extract_and_sr(self, extract, s_r, where):
|
|
237
|
-
self.sound_extract = extract
|
|
238
|
-
self.samplerate = s_r
|
|
239
|
-
self.sound_extract_position = where
|
|
240
|
-
self.cached_convolution_fit = {'sound_extract_position': None}
|
|
241
|
-
logger.debug('sound_extract set, samplerate %i location %i'%(s_r, where))
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# there's always at least one complete 0.5 silence interval in a 1.5 second signal
|
|
245
|
-
|
|
246
|
-
def _get_envelope(self):
|
|
229
|
+
|
|
230
|
+
def set_sound_extract_and_sr(self, sound_extract, samplerate, sound_extract_position):
|
|
247
231
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
maybe:
|
|
254
|
-
Values are roughly normalized: between 0 and approximately 1.0
|
|
255
|
-
|
|
256
|
-
Returns
|
|
257
|
-
-------
|
|
258
|
-
numpy.ndarray the same length of self.recording.sound_extract
|
|
232
|
+
Sets:
|
|
233
|
+
self.sound_extract -- mono data of short duration
|
|
234
|
+
self.samplerate -- in Hz
|
|
235
|
+
self.sound_extract_position -- position in the whole file
|
|
259
236
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
WINDOW_LENGTH, POLYORDER)
|
|
265
|
-
logger.debug('self.sound_extract envelope length %i samples'%len(envelope))
|
|
266
|
-
return envelope
|
|
267
|
-
|
|
268
|
-
def _get_signal_level(self):
|
|
269
|
-
abs_signal = abs(self.sound_extract)
|
|
270
|
-
return 2 * abs_signal.mean() # since 50% duty cycle
|
|
271
|
-
|
|
272
|
-
def _get_approx_pulse_position(self):
|
|
273
|
-
"""
|
|
274
|
-
Returns the estimated pulse position using the detected silent
|
|
275
|
-
zone . The position is in samples number
|
|
276
|
-
relative to extract beginning
|
|
277
|
-
"""
|
|
278
|
-
# if self.detected_pulse_position:
|
|
279
|
-
# logger.debug('returning detected value')
|
|
280
|
-
# return self.detected_pulse_position
|
|
281
|
-
if self.estimated_pulse_position:
|
|
282
|
-
logger.debug('returning cached estimated value')
|
|
283
|
-
return self.estimated_pulse_position
|
|
284
|
-
_, silence_center_x = self._fit_triangular_signal_to_convoluted_env()
|
|
285
|
-
# symbol_width_samples = 1e-3*SYMBOL_LENGTH
|
|
286
|
-
self.estimated_pulse_position = silence_center_x + int(0.5*(
|
|
287
|
-
0.5 - 1e-3*SYMBOL_LENGTH)*self.samplerate)
|
|
288
|
-
logger.debug('returning estimated value from silence mid position')
|
|
289
|
-
return self.estimated_pulse_position
|
|
290
|
-
|
|
291
|
-
def _get_pulse_position(self):
|
|
292
|
-
# relative to extract beginning
|
|
293
|
-
if self.detected_pulse_position:
|
|
294
|
-
logger.debug('returning detected value')
|
|
295
|
-
return self.detected_pulse_position
|
|
296
|
-
return None
|
|
297
|
-
|
|
298
|
-
def _get_pulse_detection_level(self):
|
|
299
|
-
# return level at which the sync pulse will be detected
|
|
300
|
-
if self.pulse_detection_level is None:
|
|
301
|
-
silence_floor = self._get_silence_floor()
|
|
302
|
-
# lower_BFSK_level = silence_floor
|
|
303
|
-
# pulse_position = self._get_pulse_position()
|
|
304
|
-
lower_BFSK_level = self._get_minimal_bfsk()
|
|
305
|
-
value = math.sqrt(silence_floor * lower_BFSK_level)
|
|
306
|
-
# value = OVER_NOISE_SYNC_DETECT_LEVEL * silence_floor
|
|
307
|
-
logger.debug('setting pulse_detection_level to %f'%value)
|
|
308
|
-
self.pulse_detection_level = value
|
|
309
|
-
return value
|
|
310
|
-
else:
|
|
311
|
-
return self.pulse_detection_level
|
|
237
|
+
Computes and sets:
|
|
238
|
+
self.pulse_detection_level
|
|
239
|
+
self.sound_extract_one_bit
|
|
240
|
+
self.words_props (contains the sync pulse too)
|
|
312
241
|
|
|
313
|
-
|
|
242
|
+
Returns nothing
|
|
314
243
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
244
|
+
logger.debug('sound_extract: %s, samplerate: %s Hz, sound_extract_position %s'%(
|
|
245
|
+
sound_extract, samplerate, sound_extract_position))
|
|
246
|
+
if len(sound_extract) == 0:
|
|
247
|
+
logger.error('sound extract is empty, is sound track duration OK?')
|
|
248
|
+
raise Exception('sound extract is empty, is sound track duration OK?')
|
|
249
|
+
self.sound_extract_position = sound_extract_position
|
|
250
|
+
self.samplerate = samplerate
|
|
251
|
+
self.sound_extract = sound_extract
|
|
252
|
+
self.pulse_detection_level = np.std(sound_extract)/4
|
|
253
|
+
logger.debug('pulse_detection_level %f'%self.pulse_detection_level)
|
|
254
|
+
bits = np.abs(sound_extract)>self.pulse_detection_level
|
|
255
|
+
N_ones = round(1.5*SYMBOL_LENGTH*1e-3*samplerate) # so it includes sync pulse
|
|
256
|
+
self.sound_extract_one_bit = closing(bits, np.ones(N_ones))
|
|
257
|
+
if self.do_plots:
|
|
258
|
+
self._plot_extract()
|
|
259
|
+
logger.debug('sound_extract_one_bit len %i'%len(self.sound_extract_one_bit))
|
|
260
|
+
self.words_props = regionprops(label(np.array(2*[self.sound_extract_one_bit]))) # new
|
|
261
|
+
|
|
262
|
+
def extract_seems_TicTacCode(self):
|
|
263
|
+
"""
|
|
264
|
+
Determines if signal in sound_extract seems to be TTC.
|
|
318
265
|
|
|
266
|
+
Uses the conditions below:
|
|
319
267
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
#2 a list of int, samples indexes where the convolution is computed
|
|
268
|
+
Extract duration is 1.143 s. (ie one sec + 1 symbol duration)
|
|
269
|
+
In self.word_props (list of morphology.regionprops):
|
|
270
|
+
if one region, duration should be in [0.499 0.512] sec
|
|
271
|
+
if two regions, total duration should be in [0.50 0.655]
|
|
326
272
|
|
|
273
|
+
Returns True if self.sound_data_extract seems TicTacCode
|
|
327
274
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if mean: # in case of zero padding (? dont remember why)
|
|
333
|
-
factor = 0.5/mean # since 50% duty cycle
|
|
275
|
+
failing_comment = '' # used as a flag
|
|
276
|
+
props = self.words_props
|
|
277
|
+
if len(props) not in [1,2]:
|
|
278
|
+
failing_comment = 'len(props) not in [1,2]: %i'%len(props)
|
|
334
279
|
else:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
word_end -= int(2*symbol_width_samples) # slide to the left a little
|
|
362
|
-
logger.debug('word start, end: %i %i (in extract)'%(
|
|
363
|
-
word_start, word_end))
|
|
364
|
-
logger.debug('word start, end: %i %i (in file)'%(
|
|
365
|
-
word_start + self.sound_extract_position,
|
|
366
|
-
word_end + self.sound_extract_position))
|
|
367
|
-
w_envelope = envelope[word_start : word_end]
|
|
368
|
-
word_envelope_truncated = word_end-word_start != len(w_envelope)
|
|
369
|
-
logger.debug('w_envelope is sliced out of bounds: %s'%(
|
|
370
|
-
str(word_envelope_truncated)))
|
|
371
|
-
logger.debug('word envelope length %i samples %f secs'%(
|
|
372
|
-
len(w_envelope), len(w_envelope)/SR))
|
|
373
|
-
max_period = int(self.samplerate*max(1/F1,1/F2))
|
|
374
|
-
logger.debug('max BFSK period %i in samples'%max_period)
|
|
375
|
-
period_window = np.ones(max_period,dtype=int)/max_period
|
|
376
|
-
# smooth over longest BFSK period
|
|
377
|
-
return np.convolve(w_envelope, period_window, mode='same')
|
|
378
|
-
|
|
379
|
-
def _get_minimal_bfsk(self):
|
|
380
|
-
"""
|
|
381
|
-
because of non-flat frequency response, bfsk bits dont have the same
|
|
382
|
-
amplitude. This returns the least of both by detecting a bimodal
|
|
383
|
-
gaussian distribution
|
|
384
|
-
|
|
385
|
-
"""
|
|
386
|
-
# w_envelope = self._get_word_envelope()
|
|
387
|
-
# word_start = int(min_position + shift + 0.3*self.samplerate)
|
|
388
|
-
# word = w_envelope[word_start : int(word_start + 0.4*self.samplerate)]
|
|
389
|
-
word = self._get_word_envelope()
|
|
390
|
-
# plt.plot(word)
|
|
391
|
-
# plt.show()
|
|
392
|
-
n = len(word)
|
|
393
|
-
word = word.reshape(n, 1)
|
|
394
|
-
gm = GaussianMixture(n_components=2, random_state=0).fit(word)
|
|
395
|
-
bfsk_minimal_amplitude = min(gm.means_)
|
|
396
|
-
logger.debug('bfsk_minimal_amplitude %f'%bfsk_minimal_amplitude)
|
|
397
|
-
return bfsk_minimal_amplitude
|
|
398
|
-
|
|
399
|
-
def _fit_triangular_signal_to_convoluted_env(self):
|
|
400
|
-
"""
|
|
401
|
-
Try to fit a triangular signal to the envelope convoluted with a square
|
|
402
|
-
signal to evaluate if audio is composed of 0.5 sec signal and 0.5 s
|
|
403
|
-
silence. If so, the convolution is a triangular fct and a
|
|
404
|
-
Levenberg–Marquardt fit is used to detect this occurence (lmfit).
|
|
405
|
-
Results are cached in self.cached_convolution_fit alongside
|
|
406
|
-
self.sound_extract_position for hit/miss checks.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
Returns
|
|
410
|
-
-------
|
|
411
|
-
float
|
|
412
|
-
chi_sqr from lmfit.minimize(), indicative of fit quality
|
|
413
|
-
int
|
|
414
|
-
position of triangular minimum (base of the v shape), this
|
|
415
|
-
corresponds to the center of silent zone.
|
|
416
|
-
|
|
417
|
-
"""
|
|
418
|
-
cached_value = self.cached_convolution_fit['sound_extract_position']
|
|
419
|
-
logger.debug('cache hit? asked:%s cached: %s'%(
|
|
420
|
-
self.sound_extract_position,
|
|
421
|
-
cached_value))
|
|
422
|
-
logger.debug('(sound_extract_position values)')
|
|
423
|
-
if (CACHING and
|
|
424
|
-
# cache_is_clean and
|
|
425
|
-
cached_value and
|
|
426
|
-
math.isclose(
|
|
427
|
-
self.sound_extract_position,
|
|
428
|
-
cached_value,
|
|
429
|
-
abs_tol=0.1)):
|
|
430
|
-
logger.debug('yes, fit values cached:')
|
|
431
|
-
v1 = self.cached_convolution_fit['chi_square']
|
|
432
|
-
v2 = self.cached_convolution_fit['minimum position']
|
|
433
|
-
v2_file = v2 + self.sound_extract_position
|
|
434
|
-
logger.debug('cached chi_sq: %s minimum position in file: %s'%(v1, v2_file))
|
|
435
|
-
return (v1, v2)
|
|
436
|
-
# cached!
|
|
437
|
-
x_shifted, convolution = self._get_square_convolution()
|
|
438
|
-
# see numpy.convolve(..., mode='valid')
|
|
439
|
-
x = np.arange(len(convolution))
|
|
440
|
-
trig_params = lmfit.Parameters()
|
|
441
|
-
trig_params.add(
|
|
442
|
-
'A', value=1, min=0, max=2
|
|
443
|
-
)
|
|
444
|
-
period0 = 2*self.samplerate
|
|
445
|
-
trig_params.add(
|
|
446
|
-
'period', value=period0, min=0.9*period0,
|
|
447
|
-
max=1.1*period0
|
|
448
|
-
)
|
|
449
|
-
trig_params.add(
|
|
450
|
-
'min_position', value=len(convolution)/2
|
|
451
|
-
) # at center
|
|
452
|
-
def trig_wave(pars, x, signal_data=None):
|
|
453
|
-
# looking for phase sx with a sin of 1 sec period and 0<y<1.0
|
|
454
|
-
A = pars['A']
|
|
455
|
-
p = pars['period']
|
|
456
|
-
mp = pars['min_position']
|
|
457
|
-
model = 2*A*arcsin(abs(sin((x - mp)*2*pi/p)))/pi
|
|
458
|
-
if signal_data is None:
|
|
459
|
-
return model
|
|
460
|
-
return model - signal_data
|
|
461
|
-
fit_trig = lmfit.minimize(
|
|
462
|
-
trig_wave, trig_params,
|
|
463
|
-
args=(x,), kws={'signal_data': convolution}
|
|
464
|
-
)
|
|
465
|
-
chi_square = fit_trig.chisqr
|
|
466
|
-
shift = x_shifted[0] # convolution is shorter than sound envelope
|
|
467
|
-
min_position = int(fit_trig.params['min_position'].value) + shift
|
|
468
|
-
logger.debug('chi_square %.1f minimum convolution position %i in file'%
|
|
469
|
-
(chi_square, min_position + self.sound_extract_position))
|
|
470
|
-
self.cached_convolution_fit['sound_extract_position'] = self.sound_extract_position
|
|
471
|
-
self.cached_convolution_fit['chi_square'] = chi_square
|
|
472
|
-
self.cached_convolution_fit['minimum position'] = min_position
|
|
473
|
-
|
|
474
|
-
return chi_square, min_position + shift
|
|
475
|
-
|
|
476
|
-
def extract_seems_YaLTC(self):
|
|
477
|
-
"""margin
|
|
478
|
-
evaluate if sound data is half signal, half silence
|
|
479
|
-
no test is done on frequency components nor BFSK modulation.
|
|
480
|
-
|
|
481
|
-
Returns
|
|
482
|
-
-------
|
|
483
|
-
True if sound seems YaLTC
|
|
484
|
-
|
|
485
|
-
"""
|
|
486
|
-
chi_square, _ = self._fit_triangular_signal_to_convoluted_env()
|
|
487
|
-
seems_YaLTC = chi_square < 200 # good fit so, yes
|
|
488
|
-
logger.debug('seems YaLTC: %s'%seems_YaLTC)
|
|
489
|
-
return seems_YaLTC
|
|
490
|
-
|
|
491
|
-
def _get_silent_zone_indices(self):
|
|
492
|
-
"""
|
|
493
|
-
Returns silent zone boundary positions relative to the start
|
|
494
|
-
of self.sound_extract.
|
|
495
|
-
|
|
496
|
-
Returns
|
|
497
|
-
-------
|
|
498
|
-
left_window_boundary : int
|
|
499
|
-
left indice.
|
|
500
|
-
right_window_boundary : int
|
|
501
|
-
right indice.
|
|
280
|
+
logger.debug('len(props), %i, is in [1,2]'%len(props))
|
|
281
|
+
if len(props) == 1:
|
|
282
|
+
logger.debug('one region')
|
|
283
|
+
w = _width(props[0])/self.samplerate
|
|
284
|
+
# self.effective_word_duration = w
|
|
285
|
+
# logger.debug('effective_word_duration %f (one region)'%w)
|
|
286
|
+
if not 0.499 < w < 0.512: # TODO: move as TOP OF FILE PARAMS
|
|
287
|
+
failing_comment = '_width %f not in [0.499 0.512]'%w
|
|
288
|
+
else:
|
|
289
|
+
logger.debug('0.499 < width < 0.512, %f'%w)
|
|
290
|
+
else: # 2 regions
|
|
291
|
+
logger.debug('two regions')
|
|
292
|
+
widths = [_width(p)/self.samplerate for p in props] # in sec
|
|
293
|
+
total_w = sum(widths)
|
|
294
|
+
# extra_window_duration = SOUND_EXTRACT_LENGTH - 1
|
|
295
|
+
# eff_w = total_w - extra_window_duration
|
|
296
|
+
# logger.debug('effective_word_duration %f (two regions)'%eff_w)
|
|
297
|
+
if not 0.5 < total_w < 0.656:
|
|
298
|
+
failing_comment = 'two regions duration %f not in [0.50 0.655]\n%s'%(total_w, widths)
|
|
299
|
+
# fig, ax = plt.subplots()
|
|
300
|
+
# p(ax, sound_extract_one_bit)
|
|
301
|
+
else:
|
|
302
|
+
logger.debug('0.5 < total_w < 0.656, %f'%total_w)
|
|
303
|
+
logger.debug('failing_comment: %s'%(
|
|
304
|
+
'none' if failing_comment=='' else failing_comment))
|
|
305
|
+
return failing_comment == '' # no comment = extract seems TicTacCode
|
|
502
306
|
|
|
503
|
-
|
|
504
|
-
if self.silent_zone_indices:
|
|
505
|
-
return self.silent_zone_indices
|
|
506
|
-
_, silence_center_position = self._fit_triangular_signal_to_convoluted_env()
|
|
507
|
-
srate = self.samplerate
|
|
508
|
-
half_window = int(SAFE_SILENCE_WINDOW_WIDTH * 1e-3 * srate/2)
|
|
509
|
-
left_window_boundary = silence_center_position - half_window
|
|
510
|
-
right_window_boundary = silence_center_position + half_window
|
|
511
|
-
# margin = 0.75 * srate
|
|
512
|
-
values = np.array([left_window_boundary, right_window_boundary,
|
|
513
|
-
silence_center_position])
|
|
514
|
-
values += self.sound_extract_position # samples pos in file
|
|
515
|
-
logger.debug('silent zone, left: %i, right %i, center %i'%tuple(values))
|
|
516
|
-
self.silent_zone_indices = (left_window_boundary, right_window_boundary)
|
|
517
|
-
return self.silent_zone_indices
|
|
518
|
-
|
|
519
|
-
def _get_silence_floor(self):
|
|
520
|
-
# analyses the 0.5 silence zone
|
|
521
|
-
start_silent_zone, end_silent_zone = self._get_silent_zone_indices()
|
|
522
|
-
signal = self.sound_extract
|
|
523
|
-
silent_signal = signal[start_silent_zone:end_silent_zone]
|
|
524
|
-
max_value = 1.001*np.abs(silent_signal).max() # a little headroom
|
|
525
|
-
max_value = 0 # should toggle this with a CLI option
|
|
526
|
-
five_sigmas = 5 * silent_signal.std()
|
|
527
|
-
return max(max_value, five_sigmas) # if guassian, five sigmas will do it
|
|
528
|
-
|
|
529
|
-
def make_silence_analysis_plot(self, title=None, filename=None):
|
|
530
|
-
# save figure in filename if set, otherwise
|
|
531
|
-
# start an interactive plot, title is for matplotlib
|
|
532
|
-
pulse_pos_in_file = self._get_approx_pulse_position()
|
|
533
|
-
pulse_pos_in_file += self.sound_extract_position
|
|
534
|
-
pulse_position_sec = pulse_pos_in_file/self.samplerate
|
|
535
|
-
duration_in_sec = self.rec.get_duration()
|
|
536
|
-
if pulse_position_sec > duration_in_sec/2:
|
|
537
|
-
pulse_position_sec -= duration_in_sec
|
|
538
|
-
title = 'Silence analysis around %.2f s'%(pulse_position_sec)
|
|
539
|
-
logger.debug('make_silence_analysis_plot(title=%s, filename=%s)'%(
|
|
540
|
-
title, filename))
|
|
541
|
-
start_silent_zone, end_silent_zone = self._get_silent_zone_indices()
|
|
542
|
-
signal = self.sound_extract
|
|
543
|
-
x_signal = range(len(signal))
|
|
544
|
-
x_convolution, convolution = self._get_square_convolution()
|
|
545
|
-
scaled_convo = self._get_signal_level()*convolution
|
|
546
|
-
# since 0 < convolution < 1
|
|
547
|
-
trig_level = self._get_pulse_detection_level()
|
|
548
|
-
sound_extract_position = self.sound_extract_position
|
|
549
|
-
def x2f(nx):
|
|
550
|
-
return nx + sound_extract_position
|
|
551
|
-
def f2x(nf):
|
|
552
|
-
return nf - sound_extract_position
|
|
307
|
+
def _plot_extract(self):
|
|
553
308
|
fig, ax = plt.subplots()
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
# secax.set_xlabel('position in file')
|
|
557
|
-
# ax.set_xlabel('position in extract')
|
|
309
|
+
start = self.sound_extract_position
|
|
310
|
+
i_samples = np.arange(start, start + len(self.sound_extract))
|
|
558
311
|
yt = ax.get_yaxis_transform()
|
|
312
|
+
ax.hlines(0, 0, 1,
|
|
313
|
+
transform=yt, alpha=0.3,
|
|
314
|
+
linewidth=2, colors='black')
|
|
315
|
+
ax.plot(i_samples, self.sound_extract, marker='o', markersize='1',
|
|
316
|
+
linewidth=1.5,alpha=0.3, color='blue' )
|
|
317
|
+
ax.plot(i_samples, self.sound_extract_one_bit*np.max(np.abs(self.sound_extract)),
|
|
318
|
+
marker='o', markersize='1',
|
|
319
|
+
linewidth=1.5,alpha=0.3,color='red')
|
|
559
320
|
xt = ax.get_xaxis_transform()
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
approx_pulse_x = self._get_approx_pulse_position()
|
|
564
|
-
ax.vlines(
|
|
565
|
-
silence_center_x, 0.4, 0.6,
|
|
566
|
-
transform=xt, linewidth=1, colors='black'
|
|
567
|
-
)
|
|
568
|
-
ax.vlines(
|
|
569
|
-
approx_pulse_x, 0.1, 0.9,
|
|
570
|
-
transform=xt, linewidth=1, colors='yellow'
|
|
571
|
-
)
|
|
572
|
-
bfsk_min = self._get_minimal_bfsk()
|
|
573
|
-
ax.hlines(
|
|
574
|
-
bfsk_min, 0, 1,
|
|
575
|
-
transform=yt, linewidth=1, colors='red'
|
|
576
|
-
)
|
|
577
|
-
ax.hlines(
|
|
578
|
-
trig_level, 0, 1,
|
|
579
|
-
transform=yt, linewidth=1, colors='blue'
|
|
580
|
-
)
|
|
581
|
-
ax.hlines(
|
|
582
|
-
-trig_level, 0, 1,
|
|
583
|
-
transform=yt, linewidth=1, colors='blue'
|
|
584
|
-
)
|
|
585
|
-
ax.hlines(
|
|
586
|
-
0, 0, 1,
|
|
587
|
-
transform=yt, linewidth=0.5, colors='black'
|
|
588
|
-
)
|
|
589
|
-
# plt.title(title)
|
|
321
|
+
ax.hlines(self.pulse_detection_level, 0, 1,
|
|
322
|
+
transform=yt, alpha=0.3,
|
|
323
|
+
linewidth=2, colors='green')
|
|
590
324
|
custom_lines = [
|
|
591
|
-
Line2D([0], [0], color='black', lw=2),
|
|
592
325
|
Line2D([0], [0], color='green', lw=2),
|
|
593
326
|
Line2D([0], [0], color='blue', lw=2),
|
|
594
327
|
Line2D([0], [0], color='red', lw=2),
|
|
595
|
-
Line2D([0], [0], color='yellow', lw=2),
|
|
596
328
|
]
|
|
597
329
|
ax.legend(
|
|
598
330
|
custom_lines,
|
|
599
|
-
'
|
|
331
|
+
'detection level, signal, detected region'.split(','),
|
|
600
332
|
loc='lower right')
|
|
601
|
-
ax.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
linewidth=0.3, color='purple', alpha=0.3)
|
|
605
|
-
ax.axvspan(
|
|
606
|
-
start_silent_zone, end_silent_zone,
|
|
607
|
-
alpha=0.1, color='green')
|
|
608
|
-
ax.plot(
|
|
609
|
-
x_convolution, scaled_convo,
|
|
610
|
-
marker='.', markersize='0.2',
|
|
611
|
-
linestyle='None', color='black', alpha=1)
|
|
612
|
-
# linewidth=0.3, linestyle='None', color='black', alpha=0.3)
|
|
613
|
-
# ax.set_xlabel('Decoder.sound_extract samples')
|
|
614
|
-
if filename == None:
|
|
615
|
-
plt.show()
|
|
616
|
-
else:
|
|
617
|
-
logger.debug('saving silence_analysis_plot to %s'%filename)
|
|
618
|
-
plt.savefig(
|
|
619
|
-
filename,
|
|
620
|
-
format="png")
|
|
621
|
-
plt.close()
|
|
333
|
+
ax.set_title('Finding word + sync pulse')
|
|
334
|
+
plt.xlabel("Position in file %s (samples)"%self.rec)
|
|
335
|
+
plt.show()
|
|
622
336
|
|
|
623
|
-
def
|
|
624
|
-
"""
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Returns the pulse position relative to the extract beginning.
|
|
632
|
-
|
|
633
|
-
Sets self.detected_pulse_position to the returned value.
|
|
337
|
+
def get_time_in_sound_extract(self):
|
|
338
|
+
"""
|
|
339
|
+
Tries to decode time present in self.sound_extract, if successfull
|
|
340
|
+
return a time dict, eg:{'version': 0, 'seconds':
|
|
341
|
+
44, 'minutes': 57, 'hours': 19,
|
|
342
|
+
'day': 1, 'month': 3, 'year offset': 1,
|
|
343
|
+
'pulse at': 670451.2217 } otherwise return None
|
|
634
344
|
"""
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
logger.debug('
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
345
|
+
pulse_detected = self._detect_sync_pulse_position()
|
|
346
|
+
if not pulse_detected:
|
|
347
|
+
return None
|
|
348
|
+
symbols_data = self._get_symbols_data()
|
|
349
|
+
frequencies = [self._get_main_frequency(data_slice)
|
|
350
|
+
for data_slice in symbols_data ]
|
|
351
|
+
logger.debug('found frequencies %s'%frequencies)
|
|
352
|
+
def _get_bit_from_freq(freq):
|
|
353
|
+
mid_FSK = 0.5*(F1 + F2)
|
|
354
|
+
return '1' if freq > mid_FSK else '0'
|
|
355
|
+
bits = [_get_bit_from_freq(f) for f in frequencies]
|
|
356
|
+
bits_string = ''.join(bits)
|
|
357
|
+
logger.debug('giving bits: LSB %s MSB'%bits_string)
|
|
358
|
+
|
|
359
|
+
def _values_from_bits(bits):
|
|
360
|
+
word_payload_bits_positions = {
|
|
361
|
+
# start, finish (excluded)
|
|
362
|
+
'version':(0,3), # 3 bits
|
|
363
|
+
'seconds':(3,9), # 6 bits
|
|
364
|
+
'minutes':(9,15),
|
|
365
|
+
'hours':(15,20),
|
|
366
|
+
'day':(20,25),
|
|
367
|
+
'month':(25,29),
|
|
368
|
+
'year offset':(29,34),
|
|
369
|
+
}
|
|
370
|
+
binary_words = { key : bits[slice(*value)]
|
|
371
|
+
for key, value
|
|
372
|
+
in word_payload_bits_positions.items()
|
|
373
|
+
}
|
|
374
|
+
int_values = { key : int(''.join(reversed(val)),2)
|
|
375
|
+
for key, val in binary_words.items()
|
|
376
|
+
}
|
|
377
|
+
return int_values
|
|
378
|
+
time_values = _values_from_bits(bits_string)
|
|
379
|
+
logger.debug(' decoded time %s'%time_values)
|
|
380
|
+
sync_pos_in_file = self.detected_pulse_position + \
|
|
381
|
+
self.sound_extract_position
|
|
382
|
+
time_values['pulse at'] = sync_pos_in_file
|
|
383
|
+
if not 0 <= time_values['seconds'] <= 59:
|
|
384
|
+
return None
|
|
385
|
+
if not 0 <= time_values['minutes'] <= 59:
|
|
386
|
+
return None
|
|
387
|
+
if not 0 <= time_values['hours'] <= 23:
|
|
388
|
+
return None
|
|
389
|
+
if not 1 <= time_values['month'] <= 12:
|
|
390
|
+
return None
|
|
391
|
+
return time_values
|
|
392
|
+
|
|
393
|
+
def _detect_sync_pulse_position(self):
|
|
394
|
+
# sets self.detected_pulse_position, relative to sound_extract
|
|
395
|
+
#
|
|
396
|
+
regions = self.words_props # contains the sync pulse too
|
|
397
|
+
# len(self.words_props) should be 1 or 2 for vallid TTC
|
|
398
|
+
logger.debug('len() of words_props: %i'%len(self.words_props))
|
|
399
|
+
whole_region = [p for p in regions if 0.499 < _width(p)/self.samplerate < 0.512]
|
|
400
|
+
logger.debug('region widths %s'%[_width(p)/self.samplerate for p in regions])
|
|
401
|
+
logger.debug('number of whole_region %i'%len(whole_region))
|
|
402
|
+
if len(regions) == 1 and len(whole_region) != 1:
|
|
403
|
+
# oops
|
|
404
|
+
logger.debug('len(regions) == 1 and len(whole_region) != 1, failed')
|
|
405
|
+
return False #######################################################
|
|
406
|
+
if len(whole_region) > 1:
|
|
407
|
+
print('error in _detect_sync_pulse_position: len(whole_region) > 1 ')
|
|
408
|
+
return False #######################################################
|
|
409
|
+
if len(whole_region) == 1:
|
|
410
|
+
# sync pulse at the begining of this one
|
|
411
|
+
_, spike, _, _ = whole_region[0].bbox
|
|
654
412
|
else:
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
413
|
+
# whole_region is [] (all fractionnal) and
|
|
414
|
+
# sync pulse at the begining of the 2nd region
|
|
415
|
+
_, spike, _, _ = regions[1].bbox
|
|
416
|
+
# but check there is still enough place for ten bits:
|
|
417
|
+
# 6 for secs + 3 for revision + blanck after sync
|
|
418
|
+
minimum_samples = int(self.samplerate*10*SYMBOL_LENGTH*1e-3)
|
|
419
|
+
whats_left = len(self.sound_extract) - spike
|
|
420
|
+
if whats_left < minimum_samples:
|
|
421
|
+
spike -= self.samplerate
|
|
422
|
+
# else: stay there, will decode seconds in whats_left
|
|
423
|
+
half_symbol_width = int(0.5*1e-3*SYMBOL_LENGTH*self.samplerate) # samples
|
|
424
|
+
left, right = (spike - half_symbol_width, spike+half_symbol_width)
|
|
425
|
+
spike_data = self.sound_extract[left:right]
|
|
426
|
+
biggest_positive = np.max(spike_data)
|
|
427
|
+
biggest_negative = np.min(spike_data)
|
|
428
|
+
if abs(biggest_negative) > biggest_positive:
|
|
429
|
+
# flip
|
|
430
|
+
spike_data = -1 * spike_data
|
|
431
|
+
def fit_line_until_negative():
|
|
432
|
+
import numpy as np
|
|
433
|
+
start = np.argmax(spike_data)
|
|
434
|
+
xs = [start]
|
|
435
|
+
ys = [spike_data[start]]
|
|
436
|
+
i = 1
|
|
437
|
+
while spike_data[start - i] > 0 and start - i >= 0:
|
|
438
|
+
xs.append(start - i)
|
|
439
|
+
ys.append(spike_data[start - i])
|
|
440
|
+
i += 1
|
|
441
|
+
# ax.scatter(xs, ys)
|
|
442
|
+
import numpy as np
|
|
443
|
+
coeff = np.polyfit(xs, ys, 1)
|
|
444
|
+
m, b = coeff
|
|
445
|
+
zero = int(-b/m)
|
|
446
|
+
# check if data is from USB audio and tweak
|
|
447
|
+
y_fit = np.poly1d(coeff)(xs)
|
|
448
|
+
err = abs(np.sum(np.abs(y_fit-ys))/np.mean(ys))
|
|
449
|
+
logger.debug('fit error for line in ramp: %f'%err)
|
|
450
|
+
if err < 0.01: #good fit so not analog
|
|
451
|
+
zero += 1
|
|
452
|
+
return zero
|
|
453
|
+
sync_sample = fit_line_until_negative() + left
|
|
454
|
+
logger.debug('sync pulse found at %i in extract, %i in file'%(
|
|
455
|
+
sync_sample, sync_sample + self.sound_extract_position))
|
|
456
|
+
self.detected_pulse_position = sync_sample
|
|
457
|
+
return True
|
|
458
|
+
|
|
459
|
+
def _get_symbols_data(self):
|
|
460
|
+
# part of extract AFTER sync pulse
|
|
461
|
+
whats_left = len(self.sound_extract) - self.detected_pulse_position # in samples
|
|
462
|
+
whats_left /= self.samplerate # in sec
|
|
463
|
+
whole_word_is_in_extr = whats_left > 0.512
|
|
464
|
+
if whole_word_is_in_extr:
|
|
465
|
+
# one region
|
|
466
|
+
logger.debug('word is in one sole region')
|
|
467
|
+
length_needed = round(0.5*self.samplerate)
|
|
468
|
+
length_needed += round(self.samplerate*SYMBOL_LENGTH*1e-3)
|
|
469
|
+
whole_word = self.sound_extract[self.detected_pulse_position:
|
|
470
|
+
self.detected_pulse_position + length_needed]
|
|
677
471
|
else:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
# word
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
#
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
logger.debug('
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
#
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
np.arange(float(0), float(N_SYMBOLS_SAMD21 + 1)) + \
|
|
756
|
-
pulse_position
|
|
757
|
-
int_symb_positions = symbol_positions.round().astype(int)
|
|
758
|
-
logger.debug('%i symbol positions %s samples in file'%
|
|
759
|
-
(
|
|
760
|
-
len(int_symb_positions),
|
|
761
|
-
int_symb_positions + self.sound_extract_position)
|
|
762
|
-
)
|
|
763
|
-
return int_symb_positions[:-1], left_boundary, right_boundary
|
|
764
|
-
|
|
765
|
-
def _values_from_bits(self, bits):
|
|
766
|
-
word_payload_bits_positions = {
|
|
767
|
-
'version':(0,2),
|
|
768
|
-
'clock source':(2,3),
|
|
769
|
-
'seconds':(3,9),
|
|
770
|
-
'minutes':(9,15),
|
|
771
|
-
'hours':(15,20),
|
|
772
|
-
'day':(20,25),
|
|
773
|
-
'month':(25,29),
|
|
774
|
-
'year offset':(29,34),
|
|
775
|
-
}
|
|
776
|
-
binary_words = { key : bits[slice(*value)]
|
|
777
|
-
for key, value
|
|
778
|
-
in word_payload_bits_positions.items()
|
|
779
|
-
}
|
|
780
|
-
int_values = { key : self._get_int_from_binary_str(val)
|
|
781
|
-
for key, val in binary_words.items()
|
|
782
|
-
}
|
|
783
|
-
logger.debug(' Demodulated values %s'%int_values)
|
|
784
|
-
return int_values
|
|
785
|
-
|
|
786
|
-
def _slice_sound_extract(self, symbols_indices):
|
|
787
|
-
indices_left_shifted = deque(list(symbols_indices))
|
|
788
|
-
indices_left_shifted.rotate(-1)
|
|
789
|
-
all_intervals = list(zip(
|
|
790
|
-
symbols_indices,
|
|
791
|
-
indices_left_shifted
|
|
792
|
-
))
|
|
793
|
-
word_intervals = all_intervals[1:-1]
|
|
794
|
-
# [0, 11, 23, 31, 45] => [(11, 23), (23, 31), (31, 45)]
|
|
795
|
-
logger.debug('slicing intervals, word_intervals = %s'%
|
|
796
|
-
word_intervals)
|
|
797
|
-
# skip sample after pulse, start at BFSK word
|
|
798
|
-
filtered_sound_extract = self._band_pass_filter(self.sound_extract)
|
|
799
|
-
slices = [filtered_sound_extract[slice(*pair)]
|
|
800
|
-
for pair in word_intervals]
|
|
801
|
-
np.set_printoptions(threshold=5)
|
|
802
|
-
# logger.debug('data slices: \n%s'%pprint.pformat(slices))
|
|
803
|
-
# raise Exception
|
|
472
|
+
# Two regions.
|
|
473
|
+
logger.debug('word is in two regions, will wrap past seconds')
|
|
474
|
+
# Consistency check: if not whole_word_is_in_extr
|
|
475
|
+
# check has been done so seconds are encoded in what s left
|
|
476
|
+
minimum_samples = round(self.samplerate*10*SYMBOL_LENGTH*1e-3)
|
|
477
|
+
if whats_left*self.samplerate < minimum_samples:
|
|
478
|
+
print('bug in _get_data_symbol():')
|
|
479
|
+
print(' whats_left*self.samplerate < minimum_samples')
|
|
480
|
+
# Should now build a whole 0.5 sec word by joining remaining data
|
|
481
|
+
# from previous second beep
|
|
482
|
+
left_piece = self.sound_extract[self.detected_pulse_position:]
|
|
483
|
+
one_second_before_idx = round(len(self.sound_extract) - self.samplerate)
|
|
484
|
+
length_needed = round(0.5*self.samplerate - len(left_piece))
|
|
485
|
+
length_needed += round(self.samplerate*SYMBOL_LENGTH*1e-3)
|
|
486
|
+
right_piece = self.sound_extract[one_second_before_idx:
|
|
487
|
+
one_second_before_idx + length_needed]
|
|
488
|
+
whole_word = np.concatenate((left_piece, right_piece))
|
|
489
|
+
logger.debug('two chunks lengths: %i %i samples'%(len(left_piece),
|
|
490
|
+
len(right_piece)))
|
|
491
|
+
# search for word start (some jitter because of Teensy Audio Lib)
|
|
492
|
+
symbol_length = round(self.samplerate*SYMBOL_LENGTH*1e-3)
|
|
493
|
+
start = round(0.5*symbol_length) # half symbol
|
|
494
|
+
end = start + symbol_length
|
|
495
|
+
word_begining = whole_word[start:]
|
|
496
|
+
gt_detection_level = np.argwhere(np.abs(word_begining)>self.pulse_detection_level)
|
|
497
|
+
word_start = gt_detection_level[0][0]
|
|
498
|
+
word_end = gt_detection_level[-1][0]
|
|
499
|
+
self.effective_word_duration = (word_end - word_start)/self.samplerate
|
|
500
|
+
logger.debug('effective_word_duration %f s'%self.effective_word_duration)
|
|
501
|
+
uCTRLR_error = self.effective_word_duration/((N_SYMBOLS -1)*SYMBOL_LENGTH*1e-3)
|
|
502
|
+
logger.debug('uCTRLR_error %f (time ratio)'%uCTRLR_error)
|
|
503
|
+
word_start += start # relative to Decoder extract
|
|
504
|
+
# check if gap is indeed less than TEENSY_MAX_LAG
|
|
505
|
+
silence_length = word_start
|
|
506
|
+
gap = silence_length - symbol_length
|
|
507
|
+
relative_gap = gap/(TEENSY_MAX_LAG*self.samplerate)
|
|
508
|
+
logger.debug('Audio update() gap between sync pulse and word start: ')
|
|
509
|
+
logger.debug('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
|
|
510
|
+
1e3*TEENSY_MAX_LAG))
|
|
511
|
+
logger.debug('relative audio_block gap %.2f'%(relative_gap))
|
|
512
|
+
# if relative_gap > 1: # dont tell: simply fail and try elsewhere
|
|
513
|
+
# print('Warning: gap between spike and word is too big for %s'%self.rec)
|
|
514
|
+
# print('Audio update() gap between sync pulse and word start: ')
|
|
515
|
+
# print('%.2f ms (max value %.2f)'%(1e3*gap/self.samplerate,
|
|
516
|
+
# 1e3*TEENSY_MAX_LAG))
|
|
517
|
+
symbol_width_samples_theor = self.samplerate*SYMBOL_LENGTH*1e-3
|
|
518
|
+
symbol_width_samples_eff = self.effective_word_duration * \
|
|
519
|
+
self.samplerate/(N_SYMBOLS - 1)
|
|
520
|
+
logger.debug('symbol width %i theo; %i effective (samples)'%(
|
|
521
|
+
symbol_width_samples_theor,
|
|
522
|
+
symbol_width_samples_eff))
|
|
523
|
+
symbol_positions = symbol_width_samples_eff * \
|
|
524
|
+
np.arange(float(0), float(N_SYMBOLS - 1)) + word_start
|
|
525
|
+
# symbols_indices contains 34 start of symbols (samples)
|
|
526
|
+
symbols_indices = symbol_positions.round().astype(int)
|
|
527
|
+
if self.do_plots:
|
|
528
|
+
fig, ax = plt.subplots()
|
|
529
|
+
ax.hlines(0, 0, 1,
|
|
530
|
+
transform=ax.get_yaxis_transform(), alpha=0.3,
|
|
531
|
+
linewidth=2, colors='black')
|
|
532
|
+
start = self.sound_extract_position
|
|
533
|
+
i_samples = np.arange(start, start + len(whole_word))
|
|
534
|
+
ax.plot(i_samples, whole_word, marker='o', markersize='1',
|
|
535
|
+
linewidth=1.5,alpha=0.3, color='blue' )
|
|
536
|
+
xt = ax.get_xaxis_transform()
|
|
537
|
+
for x in symbols_indices:
|
|
538
|
+
ax.vlines(x + start, 0, 1,
|
|
539
|
+
transform=xt,
|
|
540
|
+
linewidth=0.6, colors='green')
|
|
541
|
+
ax.set_title('Slicing the 34 bits word:')
|
|
542
|
+
plt.xlabel("Position in file %s (samples)"%self.rec)
|
|
543
|
+
ax.vlines(start, 0, 1,
|
|
544
|
+
transform=xt,
|
|
545
|
+
linewidth=0.6, colors='red')
|
|
546
|
+
plt.show()
|
|
547
|
+
slice_width = round(SYMBOL_LENGTH*1e-3*self.samplerate)
|
|
548
|
+
slices = [whole_word[i:i+slice_width] for i in symbols_indices]
|
|
804
549
|
return slices
|
|
805
550
|
|
|
806
551
|
def _get_main_frequency(self, symbol_data):
|
|
@@ -811,194 +556,7 @@ class Decoder:
|
|
|
811
556
|
freq_in_hertz = abs(freq * self.samplerate)
|
|
812
557
|
return int(round(freq_in_hertz))
|
|
813
558
|
|
|
814
|
-
# def _get_bit_from_freq(self, freq):
|
|
815
|
-
# if math.isclose(freq, F1, abs_tol=FSK_TOLERANCE):
|
|
816
|
-
# return '0'
|
|
817
|
-
# if math.isclose(freq, F2, abs_tol=FSK_TOLERANCE):
|
|
818
|
-
# return '1'
|
|
819
|
-
# else:
|
|
820
|
-
# return None
|
|
821
|
-
|
|
822
|
-
def _get_bit_from_freq(self, freq):
|
|
823
|
-
mid_FSK = 0.5*(F1 + F2)
|
|
824
|
-
return '1' if freq > mid_FSK else '0'
|
|
825
|
-
|
|
826
|
-
def _get_int_from_binary_str(self, string_of_01s):
|
|
827
|
-
return int(''.join(reversed(string_of_01s)),2)
|
|
828
|
-
# LSB is leftmost in YaLTC
|
|
829
|
-
|
|
830
|
-
def _demod_values_are_OK(self, values_dict):
|
|
831
|
-
ranges = {
|
|
832
|
-
'seconds': range(60),
|
|
833
|
-
'minutes': range(60),
|
|
834
|
-
'hours': range(24),
|
|
835
|
-
'day': range(1,32),
|
|
836
|
-
'month': range(1,13),
|
|
837
|
-
}
|
|
838
|
-
for key in ranges:
|
|
839
|
-
val = values_dict[key]
|
|
840
|
-
ok = val in ranges[key]
|
|
841
|
-
logger.debug(
|
|
842
|
-
'checking range for %s: %i, Ok? %s'%(key, val, ok))
|
|
843
|
-
if not ok:
|
|
844
|
-
logger.error('demodulated value is out of range')
|
|
845
|
-
return False
|
|
846
|
-
return True
|
|
847
559
|
|
|
848
|
-
def _plot_slices(self, sync_pulse, symbols_indices, word_lft, word_rght,
|
|
849
|
-
title=None, filename=None):
|
|
850
|
-
# save figure in filename if set, otherwise
|
|
851
|
-
# start an interactive plot, title is for matplotlib
|
|
852
|
-
signal = self.sound_extract
|
|
853
|
-
# signal = self._band_pass_filter(signal)
|
|
854
|
-
start = self.sound_extract_position
|
|
855
|
-
x_signal_in_file = range(
|
|
856
|
-
start,
|
|
857
|
-
start + len(signal)
|
|
858
|
-
)
|
|
859
|
-
wwp = self._get_word_width_parameters()
|
|
860
|
-
start_silent_zone, end_silent_zone = self._get_silent_zone_indices()
|
|
861
|
-
search_end_position = wwp['search_end_position'] + start
|
|
862
|
-
logger.debug('doing slice plot')
|
|
863
|
-
fig, ax = plt.subplots()
|
|
864
|
-
plt.title(title)
|
|
865
|
-
ax.plot(
|
|
866
|
-
x_signal_in_file, signal,
|
|
867
|
-
marker='.', markersize='1',
|
|
868
|
-
linewidth=0.3, color='purple', alpha=0.3)
|
|
869
|
-
yt = ax.get_yaxis_transform()
|
|
870
|
-
ax.hlines(
|
|
871
|
-
wwp['word_width_threshold'], 0, 1,
|
|
872
|
-
transform=yt, linewidth=0.6, colors='green')
|
|
873
|
-
ax.hlines(
|
|
874
|
-
0, 0, 1,
|
|
875
|
-
transform=yt, linewidth=0.6, colors='black')
|
|
876
|
-
ax.hlines(
|
|
877
|
-
-wwp['word_width_threshold'], 0, 1,
|
|
878
|
-
transform=yt, linewidth=0.6, colors='green')
|
|
879
|
-
pulse_level = self._get_pulse_detection_level()
|
|
880
|
-
ax.hlines(
|
|
881
|
-
pulse_level, 0, 1,
|
|
882
|
-
transform=yt, linewidth=0.6, colors='blue')
|
|
883
|
-
ax.hlines(
|
|
884
|
-
-pulse_level, 0, 1,
|
|
885
|
-
transform=yt, linewidth=0.6, colors='blue')
|
|
886
|
-
xt = ax.get_xaxis_transform()
|
|
887
|
-
# ax.vlines(
|
|
888
|
-
# search_start_position,
|
|
889
|
-
# 0, 1, transform=xt, linewidth=0.6, colors='blue')
|
|
890
|
-
ax.vlines(
|
|
891
|
-
search_end_position,
|
|
892
|
-
0, 1, transform=xt, linewidth=0.6, colors='blue')
|
|
893
|
-
ax.plot(
|
|
894
|
-
[sync_pulse + start], [0],
|
|
895
|
-
marker='D', markersize='7',
|
|
896
|
-
linewidth=0.3, color='blue', alpha=0.3)
|
|
897
|
-
ax.plot(
|
|
898
|
-
[start_silent_zone + start], [0],
|
|
899
|
-
marker='>', markersize='10',
|
|
900
|
-
linewidth=0.3, color='green', alpha=0.3)
|
|
901
|
-
ax.plot(
|
|
902
|
-
[end_silent_zone + start], [0],
|
|
903
|
-
marker='<', markersize='10',
|
|
904
|
-
linewidth=0.3, color='green', alpha=0.3)
|
|
905
|
-
boundaries_OK, word_lft, word_rght = \
|
|
906
|
-
self._get_BFSK_word_boundaries()
|
|
907
|
-
ax.vlines(
|
|
908
|
-
word_lft + start, 0, 1,
|
|
909
|
-
transform=ax.get_xaxis_transform(),
|
|
910
|
-
linewidth=0.6, colors='red')
|
|
911
|
-
ax.vlines(
|
|
912
|
-
word_rght + start, 0, 1,
|
|
913
|
-
transform=ax.get_xaxis_transform(),
|
|
914
|
-
linewidth=0.6, colors='red')
|
|
915
|
-
for x in symbols_indices + self.sound_extract_position:
|
|
916
|
-
ax.vlines(
|
|
917
|
-
x, 0, 1,
|
|
918
|
-
transform=ax.get_xaxis_transform(),
|
|
919
|
-
linewidth=0.3, colors='green')
|
|
920
|
-
ax.set_xlabel(
|
|
921
|
-
'samples in file')
|
|
922
|
-
plt.xlim(
|
|
923
|
-
[sync_pulse - 300 + start, wwp['search_end_position'] + 400 + start])
|
|
924
|
-
if filename == None:
|
|
925
|
-
plt.show()
|
|
926
|
-
else:
|
|
927
|
-
plt.ylim(
|
|
928
|
-
[-1.5*wwp['word_width_threshold'],
|
|
929
|
-
1.1*signal.max()])
|
|
930
|
-
height = 1000
|
|
931
|
-
plt.savefig(
|
|
932
|
-
filename,
|
|
933
|
-
format='png',
|
|
934
|
-
dpi=height/fig.get_size_inches()[1])
|
|
935
|
-
plt.close()
|
|
936
|
-
logger.debug('done slice plot')
|
|
937
|
-
|
|
938
|
-
def _band_pass_filter(self, data):
|
|
939
|
-
# return filtered data
|
|
940
|
-
def _bandpass(data: np.ndarray, edges: list[float], sample_rate: float, poles: int = 5):
|
|
941
|
-
sos = scipy.signal.butter(poles, edges, 'bandpass', fs=sample_rate, output='sos')
|
|
942
|
-
filtered_data = scipy.signal.sosfiltfilt(sos, data)
|
|
943
|
-
return filtered_data
|
|
944
|
-
sample_rate = self.samplerate
|
|
945
|
-
times = np.arange(len(data))/sample_rate
|
|
946
|
-
return _bandpass(data, [BPF_LOW_FRQ, BPF_HIGH_FRQ], sample_rate)
|
|
947
|
-
|
|
948
|
-
def get_time_in_sound_extract(self, plots):
|
|
949
|
-
if self.sound_extract is None:
|
|
950
|
-
return None
|
|
951
|
-
if plots:
|
|
952
|
-
self.make_silence_analysis_plot()
|
|
953
|
-
pulse_position = self._detect_sync_pulse_position()
|
|
954
|
-
pulse_pos_in_file = pulse_position + self.sound_extract_position
|
|
955
|
-
pulse_position_sec = pulse_pos_in_file/self.samplerate
|
|
956
|
-
logger.debug('found sync pulse at sample %i in file'%pulse_pos_in_file)
|
|
957
|
-
symbols_indices, word_lft, word_rght = \
|
|
958
|
-
self._get_BFSK_symbols_boundaries()
|
|
959
|
-
if plots:
|
|
960
|
-
title = 'Bit slicing at %s, %.2f s'%(pulse_pos_in_file,
|
|
961
|
-
pulse_position_sec)
|
|
962
|
-
# self.make_silence_analysis_plot()
|
|
963
|
-
logger.debug('calling _plot_slices()')
|
|
964
|
-
self._plot_slices(pulse_position, symbols_indices, word_lft,
|
|
965
|
-
word_rght, title)
|
|
966
|
-
if symbols_indices is None:
|
|
967
|
-
return None
|
|
968
|
-
sliced_data = self._slice_sound_extract(symbols_indices)
|
|
969
|
-
frequencies = [self._get_main_frequency(data_slice)
|
|
970
|
-
for data_slice
|
|
971
|
-
in sliced_data
|
|
972
|
-
]
|
|
973
|
-
logger.debug('frequencies = %s'%frequencies)
|
|
974
|
-
sr = self.samplerate
|
|
975
|
-
n_bits = N_SYMBOLS_SAMD21 - 1
|
|
976
|
-
eff_symbol_length = 1e3*(word_rght-word_lft)/(n_bits*sr)
|
|
977
|
-
length_ratio = eff_symbol_length / SYMBOL_LENGTH
|
|
978
|
-
logger.debug('symbol length_ratio (eff/supposed) %f'%length_ratio)
|
|
979
|
-
corrected_freq = np.array(frequencies)*length_ratio
|
|
980
|
-
logger.debug('corrected freq (using symbol length) = %s'%corrected_freq)
|
|
981
|
-
bits = [self._get_bit_from_freq(f) for f in corrected_freq]
|
|
982
|
-
for i, bit in enumerate(bits):
|
|
983
|
-
if bit == None:
|
|
984
|
-
logger.warning('cant decode frequency %i for bit at %i-%i'%(
|
|
985
|
-
corrected_freq[i],
|
|
986
|
-
symbols_indices[i],
|
|
987
|
-
symbols_indices[i+1]))
|
|
988
|
-
if None in bits:
|
|
989
|
-
return None
|
|
990
|
-
bits_string = ''.join(bits)
|
|
991
|
-
logger.debug('bits = %s'%bits_string)
|
|
992
|
-
time_values = self._values_from_bits(bits_string)
|
|
993
|
-
time_values['pulse at'] = (pulse_position +
|
|
994
|
-
self.sound_extract_position -
|
|
995
|
-
SAMD21_LATENCY*1e-6*self.samplerate)
|
|
996
|
-
time_values['clock source'] = 'GPS' \
|
|
997
|
-
if time_values['clock source'] == 1 else 'RTC'
|
|
998
|
-
if self._demod_values_are_OK(time_values):
|
|
999
|
-
return time_values
|
|
1000
|
-
else:
|
|
1001
|
-
return None
|
|
1002
560
|
|
|
1003
561
|
class Recording:
|
|
1004
562
|
"""
|
|
@@ -1006,28 +564,22 @@ class Recording:
|
|
|
1006
564
|
|
|
1007
565
|
Attributes:
|
|
1008
566
|
AVpath : pathlib.path
|
|
1009
|
-
path of video+sound+
|
|
567
|
+
path of video+sound+TicTacCode file, relative to working directory
|
|
1010
568
|
|
|
1011
|
-
|
|
1012
|
-
path of sound file stripped of silent and YaLTC channels
|
|
569
|
+
audio_data : in16 numpy.array of shape [nchan] x [N samples]
|
|
1013
570
|
|
|
1014
|
-
|
|
1015
|
-
|
|
571
|
+
# valid_sound : pathlib.path
|
|
572
|
+
# path of sound file stripped of silent and TicTacCode channels
|
|
1016
573
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
set by Timeline._rename_all_recs()
|
|
574
|
+
device : Device
|
|
575
|
+
identifies the device used for the recording, set in __init__()
|
|
1020
576
|
|
|
1021
577
|
probe : dict
|
|
1022
578
|
returned value of ffmpeg.probe(self.AVpath)
|
|
1023
579
|
|
|
1024
|
-
|
|
580
|
+
TicTacCode_channel : int
|
|
1025
581
|
which channel is sync track. 0 is first channel,
|
|
1026
|
-
set in
|
|
1027
|
-
|
|
1028
|
-
silent_channels : list of ints
|
|
1029
|
-
list of channel which are silent. 1 is first channel,
|
|
1030
|
-
set in _strip_yaltc()
|
|
582
|
+
set in _find_TicTacCode().
|
|
1031
583
|
|
|
1032
584
|
decoder : yaltc.decoder
|
|
1033
585
|
associated decoder object, if file is audiovideo
|
|
@@ -1043,7 +595,7 @@ class Recording:
|
|
|
1043
595
|
sync_position : int
|
|
1044
596
|
position of first detected syn pulse
|
|
1045
597
|
|
|
1046
|
-
|
|
598
|
+
is_audio_reference : bool (True for ref rec only)
|
|
1047
599
|
in multi recorders set-ups, user decides if a sound-only recording
|
|
1048
600
|
is the time reference for all other audio recordings. By
|
|
1049
601
|
default any video recording is the time reference for other audio,
|
|
@@ -1052,24 +604,24 @@ class Recording:
|
|
|
1052
604
|
|
|
1053
605
|
device_relative_speed : float
|
|
1054
606
|
the ratio of the recording device clock speed relative to the
|
|
1055
|
-
|
|
1056
|
-
pysox tempo transform. If value < 1.0 then the recording is
|
|
1057
|
-
than
|
|
1058
|
-
instance so the value can change
|
|
1059
|
-
|
|
607
|
+
video recorder clock device, in order to correct clock drift with
|
|
608
|
+
pysox tempo transform. If value < 1.0 then the recording is
|
|
609
|
+
slower than video recorder. Updated by each
|
|
610
|
+
MediaMerger instance so the value can change
|
|
611
|
+
depending on the video recording . A mean is calculated for all
|
|
1060
612
|
recordings of the same device in
|
|
1061
|
-
|
|
613
|
+
MediaMerger._get_concatenated_audiofile_for()
|
|
1062
614
|
|
|
1063
615
|
time_position : float
|
|
1064
616
|
The time (in seconds) at which the recording starts relative to the
|
|
1065
|
-
|
|
1066
|
-
instance so the value can change depending on the
|
|
617
|
+
video recording. Updated by each MediaMerger
|
|
618
|
+
instance so the value can change depending on the video
|
|
1067
619
|
recording (a video or main sound).
|
|
1068
620
|
|
|
1069
621
|
final_synced_file : a pathlib.Path
|
|
1070
622
|
contains the path of the merged video file after the call to
|
|
1071
|
-
AudioStitcher.
|
|
1072
|
-
|
|
623
|
+
AudioStitcher.build_audio_and_write_merged_media if the Recording is a
|
|
624
|
+
video recording, relative to the working directory
|
|
1073
625
|
|
|
1074
626
|
synced_audio : pathlib.Path
|
|
1075
627
|
contains the path of audio only of self.final_synced_file. Absolute
|
|
@@ -1083,51 +635,59 @@ class Recording:
|
|
|
1083
635
|
|
|
1084
636
|
"""
|
|
1085
637
|
|
|
1086
|
-
def __init__(self, media):
|
|
638
|
+
def __init__(self, media, do_plots=False):
|
|
1087
639
|
"""
|
|
640
|
+
Set AVfilename string and check if file exists, does not read any
|
|
641
|
+
media data right away but uses ffprobe to parses the file and sets
|
|
642
|
+
probe attribute.
|
|
643
|
+
|
|
644
|
+
Logs a warning and sets Recording.decoder to None if ffprobe cant
|
|
645
|
+
interpret the file or if file has no audio. If file contains audio,
|
|
646
|
+
initialise Recording.decoder(but doesnt try to decode anything yet).
|
|
647
|
+
|
|
1088
648
|
If multifile recording, AVfilename is sox merged audio file;
|
|
1089
|
-
Set AVfilename string and check if file exists, does not read
|
|
1090
|
-
any media data right away but uses ffprobe to parses the file and
|
|
1091
|
-
sets probe attribute.
|
|
1092
|
-
Logs a warning if ffprobe cant interpret the file or if file
|
|
1093
|
-
has no audio; if file contains audio, instantiates a Decoder object
|
|
1094
|
-
(but doesnt try to decode anything yet)
|
|
1095
649
|
|
|
1096
650
|
Parameters
|
|
1097
651
|
----------
|
|
1098
|
-
media :
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
652
|
+
media : Media dataclass with attributes:
|
|
653
|
+
path: Path
|
|
654
|
+
device: Device
|
|
655
|
+
|
|
656
|
+
with Device having attibutes (from device_scanner module):
|
|
657
|
+
UID: int
|
|
658
|
+
folder: Path
|
|
659
|
+
name: str
|
|
660
|
+
dev_type: str
|
|
661
|
+
tracks: Tracks
|
|
662
|
+
|
|
663
|
+
with Tracks having attributes (from device_scanner module):
|
|
664
|
+
ttc: int # track number of TicTacCode signal
|
|
665
|
+
unused: list # of unused tracks
|
|
666
|
+
stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
|
|
667
|
+
mix: list # of mixed tracks, if a pair, order is L than R
|
|
668
|
+
others: list #of all other tags: (tag, track#) tuples
|
|
669
|
+
rawtrx: list # list of strings read from file
|
|
670
|
+
error_msg: str # 'None' if none
|
|
1106
671
|
Raises
|
|
1107
672
|
------
|
|
1108
673
|
an Exception if AVfilename doesnt exist
|
|
1109
674
|
|
|
1110
675
|
"""
|
|
1111
|
-
|
|
1112
|
-
self.
|
|
1113
|
-
# self.device = media['dev UID']
|
|
1114
|
-
self.device = media['dev']
|
|
676
|
+
self.AVpath = media.path
|
|
677
|
+
self.device = media.device
|
|
1115
678
|
self.true_samplerate = None
|
|
1116
679
|
self.start_time = None
|
|
1117
680
|
self.in_cam_audio_sync_arror = 0
|
|
1118
681
|
self.decoder = None
|
|
1119
682
|
self.probe = None
|
|
1120
|
-
self.
|
|
1121
|
-
self.
|
|
683
|
+
self.TicTacCode_channel = None
|
|
684
|
+
self.is_audio_reference = False
|
|
1122
685
|
self.device_relative_speed = 1.0
|
|
1123
|
-
self.
|
|
686
|
+
# self.valid_sound = None
|
|
1124
687
|
self.final_synced_file = None
|
|
1125
688
|
self.synced_audio = None
|
|
1126
|
-
self.new_rec_name = media
|
|
1127
|
-
|
|
1128
|
-
# self.relative_position = 0
|
|
1129
|
-
# self.editing_front = {'what':None,'how': 0}
|
|
1130
|
-
# self.editing_rear = {'what':None,'how': 0}
|
|
689
|
+
# self.new_rec_name = media.path.name
|
|
690
|
+
self.do_plots = do_plots
|
|
1131
691
|
logger.debug('__init__ Recording object %s'%self.__repr__())
|
|
1132
692
|
logger.debug(' in directory %s'%self.AVpath.parent)
|
|
1133
693
|
recording_init_fail = ''
|
|
@@ -1150,7 +710,7 @@ class Recording:
|
|
|
1150
710
|
elif self.get_duration() < MINIMUM_LENGTH:
|
|
1151
711
|
recording_init_fail = 'file too short, %f s\n'%self.get_duration()
|
|
1152
712
|
if recording_init_fail == '': # success
|
|
1153
|
-
self.decoder = Decoder(self)
|
|
713
|
+
self.decoder = Decoder(self, do_plots)
|
|
1154
714
|
# self._set_multi_files_siblings()
|
|
1155
715
|
self._check_for_camera_error_correction()
|
|
1156
716
|
else:
|
|
@@ -1159,11 +719,25 @@ class Recording:
|
|
|
1159
719
|
print('Recording init failed: %s'%recording_init_fail)
|
|
1160
720
|
self.probe = None
|
|
1161
721
|
self.decoder = None
|
|
722
|
+
return
|
|
1162
723
|
logger.debug('ffprobe found: %s'%self.probe)
|
|
1163
724
|
logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
|
|
725
|
+
# self._read_audio_data()
|
|
726
|
+
N = self.get_audio_channels_nbr()
|
|
727
|
+
data = read_audio_data_from_file(self.AVpath, n_channels=N)
|
|
728
|
+
if len(data) == 1 and not self.is_video():
|
|
729
|
+
print(f'file sound is mono ({self.AVpath}), bye.')
|
|
730
|
+
sys.exit(0)
|
|
731
|
+
if np.isclose(np.std(data), 0, rtol=1e-2):
|
|
732
|
+
logger.error("ffmpeg can't extract audio from %s"%file)
|
|
733
|
+
sys.exit(0)
|
|
734
|
+
self.audio_data = data
|
|
735
|
+
logger.debug('Recording.audio_data: %s of shape %s'%(self.audio_data,
|
|
736
|
+
self.audio_data.shape))
|
|
1164
737
|
|
|
1165
738
|
def __repr__(self):
|
|
1166
|
-
return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
739
|
+
# return 'Recording of %s'%_pathname(self.new_rec_name)
|
|
740
|
+
return _pathname(self.AVpath)
|
|
1167
741
|
|
|
1168
742
|
def _check_for_camera_error_correction(self):
|
|
1169
743
|
# look for a file number
|
|
@@ -1180,7 +754,7 @@ class Recording:
|
|
|
1180
754
|
f = str(calib_file[0])
|
|
1181
755
|
print('problem parsing name of [gold1]%s[/gold1],'%f)
|
|
1182
756
|
print('move elsewhere and rerun, quitting.\n')
|
|
1183
|
-
|
|
757
|
+
sys.exit(1)
|
|
1184
758
|
self.in_cam_audio_sync_arror = value
|
|
1185
759
|
logger.debug('found error correction %i ms.'%value)
|
|
1186
760
|
|
|
@@ -1200,19 +774,19 @@ class Recording:
|
|
|
1200
774
|
recording duration in seconds.
|
|
1201
775
|
|
|
1202
776
|
"""
|
|
1203
|
-
if self.
|
|
1204
|
-
val = sox.file_info.duration(_pathname(self.
|
|
1205
|
-
logger.debug('duration of
|
|
1206
|
-
return val
|
|
777
|
+
if self.is_audio():
|
|
778
|
+
val = sox.file_info.duration(_pathname(self.AVpath))
|
|
779
|
+
logger.debug('sox duration of valid_sound %f for %s'%(val,_pathname(self.AVpath)))
|
|
780
|
+
return val #########################################################
|
|
1207
781
|
else:
|
|
1208
782
|
if self.probe is None:
|
|
1209
|
-
return 0
|
|
783
|
+
return 0 #######################################################
|
|
1210
784
|
try:
|
|
1211
785
|
probed_duration = float(self.probe['format']['duration'])
|
|
1212
786
|
except:
|
|
1213
787
|
logger.error('oups, cant find duration from ffprobe')
|
|
1214
788
|
raise Exception('stopping here')
|
|
1215
|
-
logger.debug('ffprobed duration is: %f sec'%probed_duration)
|
|
789
|
+
logger.debug('ffprobed duration is: %f sec for %s'%(probed_duration, self))
|
|
1216
790
|
return probed_duration # duration in s
|
|
1217
791
|
|
|
1218
792
|
def get_original_duration(self):
|
|
@@ -1228,27 +802,29 @@ class Recording:
|
|
|
1228
802
|
recording duration in seconds.
|
|
1229
803
|
|
|
1230
804
|
"""
|
|
1231
|
-
val = sox.file_info.duration(_pathname(self.
|
|
1232
|
-
logger.debug('duration of
|
|
805
|
+
val = sox.file_info.duration(_pathname(self.AVpath))
|
|
806
|
+
logger.debug('duration of AVpath %f'%val)
|
|
1233
807
|
return val
|
|
1234
808
|
|
|
1235
809
|
def get_corrected_duration(self):
|
|
1236
810
|
"""
|
|
1237
811
|
uses device_relative_speed to compute corrected duration. Updated by
|
|
1238
|
-
each
|
|
1239
|
-
|
|
812
|
+
each MediaMerger object in
|
|
813
|
+
MediaMerger._get_concatenated_audiofile_for()
|
|
1240
814
|
"""
|
|
1241
815
|
return self.get_duration()/self.device_relative_speed
|
|
1242
816
|
|
|
1243
817
|
def needs_dedrifting(self):
|
|
1244
818
|
rel_sp = self.device_relative_speed
|
|
1245
|
-
if rel_sp > 1:
|
|
1246
|
-
|
|
1247
|
-
else:
|
|
1248
|
-
|
|
819
|
+
# if rel_sp > 1:
|
|
820
|
+
# delta = (rel_sp - 1)*self.get_original_duration()
|
|
821
|
+
# else:
|
|
822
|
+
# delta = (1 - rel_sp)*self.get_original_duration()
|
|
823
|
+
delta = abs((1 - rel_sp)*self.get_original_duration())
|
|
1249
824
|
logger.debug('%s delta drift %.2f ms'%(str(self), delta*1e3))
|
|
1250
825
|
if delta > MAXDRIFT:
|
|
1251
|
-
print('%s
|
|
826
|
+
print('\n[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
|
|
827
|
+
(self.AVpath, delta*1e3))
|
|
1252
828
|
return delta > MAXDRIFT, delta
|
|
1253
829
|
|
|
1254
830
|
def get_end_time(self):
|
|
@@ -1268,32 +844,38 @@ class Recording:
|
|
|
1268
844
|
end = self.get_end_time()
|
|
1269
845
|
return start < datetime and datetime < end
|
|
1270
846
|
|
|
1271
|
-
def _find_time_around(self, time
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
847
|
+
def _find_time_around(self, time):
|
|
848
|
+
"""
|
|
849
|
+
Tries to decode FSK around time (in sec)
|
|
850
|
+
through decoder object; if successful return a time dict, eg:
|
|
851
|
+
{'version': 0, 'seconds': 44, 'minutes': 57,
|
|
852
|
+
'hours': 19, 'day': 1, 'month': 3, 'year offset': 1,
|
|
853
|
+
'pulse at': 670451.2217 }
|
|
854
|
+
otherwise return None
|
|
855
|
+
"""
|
|
856
|
+
if time < 0: # negative = referenced from the end
|
|
1279
857
|
there = self.get_duration() + time
|
|
1280
858
|
else:
|
|
1281
859
|
there = time
|
|
1282
|
-
self.
|
|
1283
|
-
if self.
|
|
860
|
+
self._find_TicTacCode(there, SOUND_EXTRACT_LENGTH)
|
|
861
|
+
if self.TicTacCode_channel is None:
|
|
1284
862
|
return None
|
|
1285
863
|
else:
|
|
1286
|
-
return self.decoder.get_time_in_sound_extract(
|
|
864
|
+
return self.decoder.get_time_in_sound_extract()
|
|
1287
865
|
|
|
1288
866
|
def _get_timedate_from_dict(self, time_dict):
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
867
|
+
try:
|
|
868
|
+
python_datetime = datetime(
|
|
869
|
+
time_dict['year offset'] + YEAR_ZERO,
|
|
870
|
+
time_dict['month'],
|
|
871
|
+
time_dict['day'],
|
|
872
|
+
time_dict['hours'],
|
|
873
|
+
time_dict['minutes'],
|
|
874
|
+
time_dict['seconds'],
|
|
875
|
+
tzinfo=timezone.utc)
|
|
876
|
+
except ValueError as e:
|
|
877
|
+
print('Error converting date in _get_timedate_from_dict',e)
|
|
878
|
+
sys.exit(1)
|
|
1297
879
|
python_datetime += timedelta(seconds=1) # PPS precedes NMEA sequ
|
|
1298
880
|
return python_datetime
|
|
1299
881
|
|
|
@@ -1301,7 +883,7 @@ class Recording:
|
|
|
1301
883
|
"""
|
|
1302
884
|
For error checking. This verifies if two sync pulse apart
|
|
1303
885
|
are correctly space with sample interval deduced from
|
|
1304
|
-
time difference of demodulated
|
|
886
|
+
time difference of demodulated TicTacCode times. The same
|
|
1305
887
|
process is used for determining the true sample rate
|
|
1306
888
|
in _compute_true_samplerate(). On entry check if either time
|
|
1307
889
|
is None, return False if so.
|
|
@@ -1315,12 +897,11 @@ class Recording:
|
|
|
1315
897
|
|
|
1316
898
|
Returns
|
|
1317
899
|
-------
|
|
1318
|
-
TYPE
|
|
1319
|
-
DESCRIPTION.
|
|
1320
900
|
|
|
1321
901
|
"""
|
|
1322
902
|
if t1 == None or t2 == None:
|
|
1323
|
-
return False
|
|
903
|
+
return False #######################################################
|
|
904
|
+
logger.debug('t1 : %s t2: %s'%(t1, t2))
|
|
1324
905
|
datetime_1 = self._get_timedate_from_dict(t1)
|
|
1325
906
|
datetime_2 = self._get_timedate_from_dict(t2)
|
|
1326
907
|
# if datetime_2 < datetime_1:
|
|
@@ -1361,19 +942,19 @@ class Recording:
|
|
|
1361
942
|
to_precision(true_samplerate, 8))
|
|
1362
943
|
return true_samplerate
|
|
1363
944
|
|
|
1364
|
-
def set_time_position_to(self,
|
|
945
|
+
def set_time_position_to(self, video_clip):
|
|
1365
946
|
"""
|
|
1366
947
|
Sets self.time_position, the time (in seconds) at which the recording
|
|
1367
|
-
starts relative to the
|
|
1368
|
-
instance so the value can change depending on the
|
|
948
|
+
starts relative to the video recording. Updated by each MediaMerger
|
|
949
|
+
instance so the value can change depending on the video
|
|
1369
950
|
recording (a video or main sound).
|
|
1370
951
|
|
|
1371
952
|
called by timeline.AudioStitcher._get_concatenated_audiofile_for()
|
|
1372
953
|
|
|
1373
954
|
"""
|
|
1374
|
-
|
|
955
|
+
video_start_time = video_clip.get_start_time()
|
|
1375
956
|
self.time_position = (self.get_start_time()
|
|
1376
|
-
-
|
|
957
|
+
- video_start_time).total_seconds()
|
|
1377
958
|
|
|
1378
959
|
def get_Dt_with(self, later_recording):
|
|
1379
960
|
"""
|
|
@@ -1385,25 +966,29 @@ class Recording:
|
|
|
1385
966
|
t2 = later_recording.get_start_time()
|
|
1386
967
|
return t2 - t1
|
|
1387
968
|
|
|
1388
|
-
def get_start_time(self
|
|
969
|
+
def get_start_time(self):
|
|
1389
970
|
"""
|
|
1390
|
-
Try to decode a
|
|
1391
|
-
if successful, returns a
|
|
971
|
+
Try to decode a TicTacCode_channel at start AND finish;
|
|
972
|
+
if successful, returns a datetime.datetime instance;
|
|
1392
973
|
if not returns None.
|
|
1393
|
-
If successful AND self is audio, sets self.sound_without_YaLTC
|
|
1394
974
|
"""
|
|
975
|
+
logger.debug('for %s, recording.start_time %s'%(self,
|
|
976
|
+
self.start_time))
|
|
977
|
+
if self.decoder is None:
|
|
978
|
+
return None # ffprobe failes or file too short, see __init__
|
|
1395
979
|
if self.start_time is not None:
|
|
1396
|
-
|
|
980
|
+
logger.debug('Recording.start_time already found %s'%self.start_time)
|
|
981
|
+
return self.start_time #############################################
|
|
1397
982
|
cached_times = {}
|
|
1398
|
-
def find_time(t_sec
|
|
983
|
+
def find_time(t_sec):
|
|
1399
984
|
time_k = int(t_sec)
|
|
1400
985
|
# if cached_times.has_key(time_k):
|
|
1401
986
|
if CACHING and time_k in cached_times:
|
|
1402
987
|
logger.debug('cache hit _find_time_around() for t=%s s'%time_k)
|
|
1403
|
-
return cached_times[time_k]
|
|
988
|
+
return cached_times[time_k] ####################################
|
|
1404
989
|
else:
|
|
1405
990
|
logger.debug('_find_time_around() for t=%s s not cached'%time_k)
|
|
1406
|
-
new_t = self._find_time_around(t_sec
|
|
991
|
+
new_t = self._find_time_around(t_sec)
|
|
1407
992
|
cached_times[time_k] = new_t
|
|
1408
993
|
return new_t
|
|
1409
994
|
for i, pair in enumerate(TRIAL_TIMES):
|
|
@@ -1416,12 +1001,12 @@ class Recording:
|
|
|
1416
1001
|
logger.warning('More than one trial: #%i/%i'%(i+1,
|
|
1417
1002
|
len(TRIAL_TIMES)))
|
|
1418
1003
|
# time_around_beginning = self._find_time_around(near_beg)
|
|
1419
|
-
time_around_beginning = find_time(near_beg
|
|
1420
|
-
if self.
|
|
1421
|
-
|
|
1004
|
+
time_around_beginning = find_time(near_beg)
|
|
1005
|
+
# if self.TicTacCode_channel is None:
|
|
1006
|
+
# return None ####################################################
|
|
1422
1007
|
logger.debug('Trial #%i, end at %f'%(i+1, near_end))
|
|
1423
1008
|
# time_around_end = self._find_time_around(near_end)
|
|
1424
|
-
time_around_end = find_time(near_end
|
|
1009
|
+
time_around_end = find_time(near_end)
|
|
1425
1010
|
logger.debug('trial result, time_around_beginning:\n %s'%
|
|
1426
1011
|
(time_around_beginning))
|
|
1427
1012
|
logger.debug('trial result, time_around_end:\n %s'%
|
|
@@ -1431,14 +1016,15 @@ class Recording:
|
|
|
1431
1016
|
time_around_end)
|
|
1432
1017
|
logger.debug('_two_times_are_coherent: %s'%coherence)
|
|
1433
1018
|
if coherence:
|
|
1019
|
+
logger.debug('Trial #%i successful'%(i+1))
|
|
1434
1020
|
break
|
|
1435
1021
|
if not coherence:
|
|
1436
1022
|
logger.warning('found times are incoherent')
|
|
1437
|
-
return None
|
|
1023
|
+
return None ########################################################
|
|
1438
1024
|
if None in [time_around_beginning, time_around_end]:
|
|
1439
1025
|
logger.warning('didnt find any time in file')
|
|
1440
1026
|
self.start_time = None
|
|
1441
|
-
return None
|
|
1027
|
+
return None ########################################################
|
|
1442
1028
|
true_sr = self._compute_true_samplerate(
|
|
1443
1029
|
time_around_beginning,
|
|
1444
1030
|
time_around_end)
|
|
@@ -1454,60 +1040,322 @@ class Recording:
|
|
|
1454
1040
|
logger.debug('recording started at %s'%start_UTC)
|
|
1455
1041
|
self.start_time = start_UTC
|
|
1456
1042
|
self.sync_position = time_around_beginning['pulse at']
|
|
1457
|
-
if self.is_audio():
|
|
1458
|
-
|
|
1043
|
+
# if self.is_audio():
|
|
1044
|
+
# self.valid_sound = self.AVpath
|
|
1459
1045
|
return start_UTC
|
|
1460
1046
|
|
|
1461
|
-
def
|
|
1462
|
-
"""
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1047
|
+
def _find_timed_tracks_(self, tracks_file) -> device_scanner.Tracks:
|
|
1048
|
+
"""
|
|
1049
|
+
Look for any ISO 8601 timestamp e.g.: 2007-04-05T14:30Z
|
|
1050
|
+
and choose the right chunk according to Recording.start_time
|
|
1051
|
+
"""
|
|
1052
|
+
file=open(tracks_file,"r")
|
|
1053
|
+
whole_txt = file.read()
|
|
1054
|
+
tracks_lines = []
|
|
1055
|
+
for l in whole_txt.splitlines():
|
|
1056
|
+
after_sharp = l.split('#')[0]
|
|
1057
|
+
if len(after_sharp) > 0:
|
|
1058
|
+
tracks_lines.append(after_sharp)
|
|
1059
|
+
logger.debug('file %s filtered lines:\n%s'%(tracks_file,
|
|
1060
|
+
pformat(tracks_lines)))
|
|
1061
|
+
def _seems_timestamp(line):
|
|
1062
|
+
# will validate format later with datetime.fromisoformat()
|
|
1063
|
+
m = re.match(r'ts=(.*)', line)
|
|
1064
|
+
# m = re.match(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z', line)
|
|
1065
|
+
if m != None:
|
|
1066
|
+
return m.groups()[0]
|
|
1067
|
+
else:
|
|
1068
|
+
return None
|
|
1069
|
+
chunks = []
|
|
1070
|
+
new_chunk = []
|
|
1071
|
+
timestamps_str = []
|
|
1072
|
+
for line in tracks_lines:
|
|
1073
|
+
timestamp_candidate = _seems_timestamp(line)
|
|
1074
|
+
if timestamp_candidate != None:
|
|
1075
|
+
logger.debug(f'timestamp: {line}')
|
|
1076
|
+
timestamps_str.append(timestamp_candidate)
|
|
1077
|
+
chunks.append(new_chunk)
|
|
1078
|
+
new_chunk = []
|
|
1079
|
+
else:
|
|
1080
|
+
new_chunk.append(line)
|
|
1081
|
+
chunks.append(new_chunk)
|
|
1082
|
+
logger.debug(f'chunks {chunks}, timestamps_str {timestamps_str}')
|
|
1083
|
+
str_frmt = '%Y-%m-%dT%H:%MZ'
|
|
1084
|
+
# from strings to datetime instances
|
|
1085
|
+
timestamps = []
|
|
1086
|
+
for dtstr in timestamps_str:
|
|
1087
|
+
try:
|
|
1088
|
+
ts = datetime.fromisoformat(dtstr)
|
|
1089
|
+
except:
|
|
1090
|
+
print(f'Error: in file {tracks_file},\ntimestamp {dtstr} is ill formatted, bye.')
|
|
1091
|
+
sys.exit(0)
|
|
1092
|
+
timestamps.append(ts)
|
|
1093
|
+
# timestamps = [datetime.strptime(dtstr, str_frmt, tzinfo=timezone.utc)
|
|
1094
|
+
# for dtstr in timestamps]
|
|
1095
|
+
logger.debug(f'datetime timestamps {timestamps}')
|
|
1096
|
+
# input validations, check order:
|
|
1097
|
+
if sorted(timestamps) != timestamps:
|
|
1098
|
+
print(f'Error in {tracks_file}\nSome timestamps are not in ascending order:\n')
|
|
1099
|
+
multi_lines = "\n".join(tracks_lines)
|
|
1100
|
+
print(f'{multi_lines}, Bye.')
|
|
1101
|
+
sys.exit(0)
|
|
1102
|
+
time_ranges = [t2-t1 for t1,t2 in zip(timestamps, timestamps[1:])]
|
|
1103
|
+
logger.debug(f'time_ranges {time_ranges} ')
|
|
1104
|
+
# check times between timestamps are realistic
|
|
1105
|
+
if timedelta(0) in time_ranges:
|
|
1106
|
+
print(f'Error in {tracks_file}\nSome timestamps are repeating:\n')
|
|
1107
|
+
multi_lines = "\n".join(tracks_lines)
|
|
1108
|
+
print(f'{multi_lines}, Bye.')
|
|
1109
|
+
sys.exit(0)
|
|
1110
|
+
if any([ dt < timedelta(minutes=2) for dt in time_ranges]):
|
|
1111
|
+
print(f'Warning in {tracks_file}\nSome timestamps are spaced by less than 2 minutes:\n')
|
|
1112
|
+
print("\n".join(tracks_lines))
|
|
1113
|
+
print(f'If this is an error, correct and rerun. For now will continue...')
|
|
1114
|
+
if any([ dt > timedelta(days=1) for dt in time_ranges]):
|
|
1115
|
+
print(f'Warning in {tracks_file}\nSome timestamps are spaced by more than 24 hrs:\n')
|
|
1116
|
+
print("\n".join(tracks_lines))
|
|
1117
|
+
print(f'If this is an error, correct and rerun. For now will continue...')
|
|
1118
|
+
# add 'infinite in future' to time stamps for time matching
|
|
1119
|
+
future = datetime.max
|
|
1120
|
+
future = future.replace(tzinfo=timezone.utc)
|
|
1121
|
+
timestamps.append(future)
|
|
1122
|
+
# zip it with chunks
|
|
1123
|
+
timed_chunks = list(zip(chunks,timestamps))
|
|
1124
|
+
logger.debug(f'timed_chunks\n{pformat(timed_chunks)} ')
|
|
1125
|
+
logger.debug(f'will find match with {self.start_time}')
|
|
1126
|
+
# for tch in timed_chunks:
|
|
1127
|
+
# print(tch[1], self.start_time)
|
|
1128
|
+
# print(tch[1] > self.start_time)
|
|
1129
|
+
idx = 0
|
|
1130
|
+
# while timed_chunks[idx][1] < self.start_time:
|
|
1131
|
+
# logger.debug(f'does {timed_chunks[idx][1]} < {self.start_time} ?')
|
|
1132
|
+
# idx += 1
|
|
1133
|
+
max_idx = len(timed_chunks) - 1
|
|
1134
|
+
while True:
|
|
1135
|
+
if timed_chunks[idx][1] > self.start_time or idx == max_idx:
|
|
1136
|
+
break
|
|
1137
|
+
idx += 1
|
|
1138
|
+
chunk_idx = idx
|
|
1139
|
+
logger.debug(f'chunk_idx {chunk_idx}')
|
|
1140
|
+
right_chunk = chunks[chunk_idx]
|
|
1141
|
+
logger.debug(f'found right chunk {right_chunk}')
|
|
1142
|
+
tracks_instance = self._parse_trx_lines(right_chunk, tracks_file)
|
|
1143
|
+
return tracks_instance
|
|
1144
|
+
|
|
1145
|
+
def _parse_trx_lines(self, tracks_lines_with_spaces, tracks_file):
|
|
1146
|
+
"""
|
|
1147
|
+
read track names for naming separated ISOs
|
|
1148
|
+
from tracks_file.
|
|
1149
|
+
|
|
1150
|
+
tokens looked for: mix; mix L; mix R; 0 and TC
|
|
1151
|
+
|
|
1152
|
+
repeating "mic*" pattern signals a stereo track
|
|
1153
|
+
and entries will correspondingly panned into
|
|
1154
|
+
a stero mix named mixL.wav and mixL.wav
|
|
1155
|
+
|
|
1156
|
+
mic L # spaces are ignored |
|
|
1157
|
+
mic R | stereo pair
|
|
1158
|
+
micB L
|
|
1159
|
+
micB R
|
|
1160
|
+
|
|
1161
|
+
Returns: a Tracks instance:
|
|
1162
|
+
# track numbers start at 1 for first track (as needed by sox)
|
|
1163
|
+
ttc: int # track number of TicTacCode signal
|
|
1164
|
+
unused: list # of unused tracks
|
|
1165
|
+
stereomics: list # of stereo mics track tuples (Lchan#, Rchan#)
|
|
1166
|
+
mix: list # of mixed tracks, if a pair, order is L than R
|
|
1167
|
+
others: list #of all other tags: (tag, track#) tuples
|
|
1168
|
+
rawtrx: list # list of strings read from file
|
|
1169
|
+
error_msg: str # 'None' if none
|
|
1170
|
+
e.g.: Tracks( ttc=2,
|
|
1171
|
+
unused=[],
|
|
1172
|
+
stereomics=[('mic', (4, 3)), ('mic2', (6, 5))],
|
|
1173
|
+
mix=[], others=[('clics', 1)],
|
|
1174
|
+
rawtrx=['clics', 'TC', 'micL', 'micR', 'mic2L;1000', 'mic2R;1000', 'mixL', 'mixR'],
|
|
1175
|
+
error_msg=None, lag_values=[None, None, None, None, '1000', '1000', None, None])
|
|
1176
|
+
"""
|
|
1177
|
+
def _WOspace(chaine):
|
|
1178
|
+
ch = [c for c in chaine if c != ' ']
|
|
1179
|
+
return ''.join(ch)
|
|
1180
|
+
tracks_lines = [_WOspace(l) for l in tracks_lines_with_spaces if len(l) > 0 ]
|
|
1181
|
+
rawtrx = [l for l in tracks_lines_with_spaces if len(l) > 0 ]
|
|
1182
|
+
# add index with tuples, starting at 1
|
|
1183
|
+
logger.debug('tracks_lines whole: %s'%tracks_lines)
|
|
1184
|
+
def _detach_lag_value(line):
|
|
1185
|
+
# look for ";number" ending any line, returns a two-list
|
|
1186
|
+
splt = line.split(';')
|
|
1187
|
+
if len(splt) == 1:
|
|
1188
|
+
splt += [None]
|
|
1189
|
+
if len(splt) != 2:
|
|
1190
|
+
# error
|
|
1191
|
+
print('\nText error in %s, line %s has too many ";"'%(
|
|
1192
|
+
tracks_file, line))
|
|
1193
|
+
return splt
|
|
1194
|
+
tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
|
|
1195
|
+
in tracks_lines])
|
|
1196
|
+
lag_values = [e for e in lag_values] # from tuple to list
|
|
1197
|
+
# logger.debug('tracks_lines WO lag: %s'%tracks_lines)
|
|
1198
|
+
tracks_lines = [l.lower() for l in tracks_lines]
|
|
1199
|
+
logger.debug('tracks_lines lower case: %s'%tracks_lines)
|
|
1200
|
+
# print(lag_values)
|
|
1201
|
+
logger.debug('lag_values: %s'%lag_values)
|
|
1202
|
+
tagsWOl_r = [e[:-1] for e in tracks_lines] # skip last letter
|
|
1203
|
+
logger.debug('tags WO LR letter %s'%tagsWOl_r)
|
|
1204
|
+
# find idx of start of pairs
|
|
1205
|
+
# ['clics', 'TC', 'micL', 'micR', 'mic2L', 'mic2R', 'mixL', 'mixR']
|
|
1206
|
+
def _micOrmix(a,b):
|
|
1207
|
+
# test if same and mic mic or mix mix
|
|
1208
|
+
if len(a) == 0:
|
|
1209
|
+
return False
|
|
1210
|
+
return (a == b) and (a in 'micmix')
|
|
1211
|
+
pair_idx_start =[i for i, same in enumerate([_micOrmix(a,b) for a,b
|
|
1212
|
+
in zip(tagsWOl_r,tagsWOl_r[1:])]) if same]
|
|
1213
|
+
logger.debug('pair_idx_start %s'%pair_idx_start)
|
|
1214
|
+
def LR_OK(idx):
|
|
1215
|
+
# in tracks_lines, check if idx ends a LR pair
|
|
1216
|
+
# delays, if any, have been removed
|
|
1217
|
+
a = tracks_lines[idx][-1]
|
|
1218
|
+
b = tracks_lines[idx+1][-1]
|
|
1219
|
+
return a+b in ['lr', 'rl']
|
|
1220
|
+
LR_OKs = [LR_OK(p) for p in pair_idx_start]
|
|
1221
|
+
logger.debug('LR_OKs %s'%LR_OKs)
|
|
1222
|
+
if not all(LR_OKs):
|
|
1223
|
+
print('\nError in %s'%tracks_file)
|
|
1224
|
+
print('Some tracks are paired but not L and R: %s'%rawtrx)
|
|
1225
|
+
print('quitting...')
|
|
1226
|
+
quit()
|
|
1227
|
+
complete_pairs_idx = pair_idx_start + [i + 1 for i in pair_idx_start]
|
|
1228
|
+
singles = set(range(len(tracks_lines))).difference(complete_pairs_idx)
|
|
1229
|
+
logger.debug('complete_pairs_idx %s'%complete_pairs_idx)
|
|
1230
|
+
logger.debug('singles %s'%singles)
|
|
1231
|
+
singles_tag = [tracks_lines[i] for i in singles]
|
|
1232
|
+
logger.debug('singles_tag %s'%singles_tag)
|
|
1233
|
+
n_tc_token = sum([t == 'tc' for t in singles_tag])
|
|
1234
|
+
logger.debug('n tc tags %s'%n_tc_token)
|
|
1235
|
+
if n_tc_token == 0:
|
|
1236
|
+
print('\nError in %s'%tracks_file)
|
|
1237
|
+
print('with %s'%rawtrx)
|
|
1238
|
+
print('no TC track found, quitting...')
|
|
1239
|
+
quit()
|
|
1240
|
+
if n_tc_token > 1:
|
|
1241
|
+
print('\nError in %s'%tracks_file)
|
|
1242
|
+
print('with %s'%rawtrx)
|
|
1243
|
+
print('more than one TC track, quitting...')
|
|
1244
|
+
quit()
|
|
1245
|
+
output_tracks = device_scanner.Tracks(None,[],[],[],[],rawtrx,None,[])
|
|
1246
|
+
output_tracks.ttc = tracks_lines.index('tc') + 1 # 1st = 1
|
|
1247
|
+
logger.debug('ttc_chan %s'%output_tracks.ttc)
|
|
1248
|
+
zeroed = [i+1 for i, t in enumerate(tracks_lines) if t == '0']
|
|
1249
|
+
logger.debug('zeroed %s'%zeroed)
|
|
1250
|
+
output_tracks.unused = zeroed
|
|
1251
|
+
output_tracks.others = [(st, tracks_lines.index(st)+1) for st
|
|
1252
|
+
in singles_tag if st not
|
|
1253
|
+
in ['tc', 'monomix', '0']]
|
|
1254
|
+
logger.debug('output_tracks.others %s'%output_tracks.others)
|
|
1255
|
+
# check for monomix
|
|
1256
|
+
if 'monomix' in tracks_lines:
|
|
1257
|
+
output_tracks.mix = [tracks_lines.index('monomix')+1]
|
|
1258
|
+
else:
|
|
1259
|
+
output_tracks.mix = []
|
|
1260
|
+
# check for stereo mix
|
|
1261
|
+
def _findLR(i_first):
|
|
1262
|
+
# returns L R indexes (+1 for sox non zero based indexing)
|
|
1263
|
+
i_2nd = i_first + 1
|
|
1264
|
+
a = tracks_lines[i_first][-1] # l|r at end
|
|
1265
|
+
b = tracks_lines[i_2nd][-1] # l|r at end
|
|
1266
|
+
if a == 'l':
|
|
1267
|
+
if b == 'r':
|
|
1268
|
+
# sequence is mixL mixR
|
|
1269
|
+
return i_first+1, i_2nd+1
|
|
1270
|
+
else:
|
|
1271
|
+
print('\nError in %s'%tracks_file)
|
|
1272
|
+
print('with %s'%rawtrx)
|
|
1273
|
+
print('can not find stereo mix')
|
|
1274
|
+
quit()
|
|
1275
|
+
elif a == 'r':
|
|
1276
|
+
if b == 'l':
|
|
1277
|
+
# sequence is mixR mixL
|
|
1278
|
+
return i_2nd+1, i_first+1
|
|
1279
|
+
else:
|
|
1280
|
+
print('\nError in %s'%tracks_file)
|
|
1281
|
+
print('with %s'%rawtrx)
|
|
1282
|
+
print('can not find stereo mix')
|
|
1283
|
+
quit()
|
|
1284
|
+
logger.debug('for now, output_tracks.mix %s'%output_tracks.mix)
|
|
1285
|
+
mix_pair = [p for p in pair_idx_start if tracks_lines[p][1:] == 'mix']
|
|
1286
|
+
if len(mix_pair) == 1:
|
|
1287
|
+
# one stereo mix, remove it from other pairs
|
|
1288
|
+
i = mix_pair[0]
|
|
1289
|
+
LR_pair = _findLR(i)
|
|
1290
|
+
logger.debug('LR_pair %s'%str(LR_pair))
|
|
1291
|
+
pair_idx_start.remove(i)
|
|
1292
|
+
# consistency check
|
|
1293
|
+
if output_tracks.mix != []:
|
|
1294
|
+
# already found a mono mix above!
|
|
1295
|
+
print('\nError in %s'%tracks_file)
|
|
1296
|
+
print('with %s'%rawtrx)
|
|
1297
|
+
print('found a mono mix AND a stereo mix')
|
|
1298
|
+
quit()
|
|
1299
|
+
output_tracks.mix = LR_pair
|
|
1300
|
+
logger.debug('finally, output_tracks.mix %s'%str(output_tracks.mix))
|
|
1301
|
+
logger.debug('remaining pairs %s'%pair_idx_start)
|
|
1302
|
+
# those are stereo pairs
|
|
1303
|
+
stereo_pairs = []
|
|
1304
|
+
for first_in_pair in pair_idx_start:
|
|
1305
|
+
suffix = tracks_lines[first_in_pair][:-1]
|
|
1306
|
+
stereo_pairs.append((suffix, _findLR(first_in_pair)))
|
|
1307
|
+
logger.debug('stereo_pairs %s'%stereo_pairs)
|
|
1308
|
+
output_tracks.stereomics = stereo_pairs
|
|
1309
|
+
logger.debug('finished: %s'%output_tracks)
|
|
1310
|
+
return output_tracks
|
|
1311
|
+
|
|
1312
|
+
def load_track_info(self):
|
|
1313
|
+
"""
|
|
1314
|
+
If audio rec, look for eventual track names in TRACKSFILE file, stored inside the
|
|
1315
|
+
recorder folder alongside the audio files. If there, store a Tracks
|
|
1316
|
+
object into Recording.device.tracks .
|
|
1468
1317
|
"""
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
return output_fh
|
|
1318
|
+
if self.is_video():
|
|
1319
|
+
return
|
|
1320
|
+
source_audio_folder = self.device.folder
|
|
1321
|
+
tracks_file = source_audio_folder/TRACKSFILE
|
|
1322
|
+
track_names = False
|
|
1323
|
+
# a_recording = [m for m in self.found_media_files
|
|
1324
|
+
# if m.device == device][0]
|
|
1325
|
+
# logger.debug('a_recording for device %s : %s'%(device, a_recording))
|
|
1326
|
+
nchan = self.get_audio_channels_nbr()
|
|
1327
|
+
# nchan = sox.file_info.channels(str(a_recording.path))
|
|
1328
|
+
if os.path.isfile(tracks_file):
|
|
1329
|
+
logger.debug('found file: %s'%(TRACKSFILE))
|
|
1330
|
+
tracks = self._find_timed_tracks_(tracks_file)
|
|
1331
|
+
if tracks.error_msg:
|
|
1332
|
+
print('\nError parsing [gold1]%s[/gold1] file: %s, quitting.\n'%
|
|
1333
|
+
(tracks_file, tracks.error_msg))
|
|
1334
|
+
sys.exit(1)
|
|
1335
|
+
logger.debug('parsed tracks %s'%tracks)
|
|
1336
|
+
ntracks = 2*len(tracks.stereomics)
|
|
1337
|
+
ntracks += len(tracks.mix)
|
|
1338
|
+
ntracks += len(tracks.unused)
|
|
1339
|
+
ntracks += len(tracks.others)
|
|
1340
|
+
ntracks += 1 # for ttc track
|
|
1341
|
+
logger.debug(' n chan: %i n tracks file: %i'%(nchan, ntracks))
|
|
1342
|
+
if ntracks != nchan:
|
|
1343
|
+
print('\nError parsing %s content'%tracks_file)
|
|
1344
|
+
print('incoherent number of tracks, %i vs %i quitting\n'%
|
|
1345
|
+
(nchan, ntracks))
|
|
1346
|
+
sys.exit(1)
|
|
1347
|
+
err_msg = tracks.error_msg
|
|
1348
|
+
if err_msg != None:
|
|
1349
|
+
print('\nError, quitting: in file %s, %s'%(tracks_file, err_msg))
|
|
1350
|
+
raise Exception
|
|
1351
|
+
else:
|
|
1352
|
+
self.device.tracks = tracks
|
|
1353
|
+
logger.debug('for rec %s'%self)
|
|
1354
|
+
logger.debug('tracks object: %s'%self.device.tracks)
|
|
1355
|
+
return
|
|
1356
|
+
else:
|
|
1357
|
+
logger.debug('no tracks.txt file found')
|
|
1358
|
+
return None
|
|
1511
1359
|
|
|
1512
1360
|
def _ffprobe_audio_stream(self):
|
|
1513
1361
|
streams = self.probe['streams']
|
|
@@ -1545,28 +1393,30 @@ class Recording:
|
|
|
1545
1393
|
ppm = - (nominal/true - 1) * 1e6
|
|
1546
1394
|
return int(ppm)
|
|
1547
1395
|
|
|
1548
|
-
def get_speed_ratio(self,
|
|
1396
|
+
def get_speed_ratio(self, videoclip):
|
|
1397
|
+
# ratio between real samplerates of audio and videoclip
|
|
1549
1398
|
nominal = self.get_samplerate()
|
|
1550
1399
|
true = self.true_samplerate
|
|
1551
1400
|
ratio = true/nominal
|
|
1552
|
-
|
|
1553
|
-
true_ref =
|
|
1554
|
-
ratio_ref = true_ref/
|
|
1401
|
+
nominal_vid = videoclip.get_samplerate()
|
|
1402
|
+
true_ref = videoclip.true_samplerate
|
|
1403
|
+
ratio_ref = true_ref/nominal_vid
|
|
1555
1404
|
return ratio/ratio_ref
|
|
1556
1405
|
|
|
1557
1406
|
def get_samplerate(self):
|
|
1558
|
-
#
|
|
1407
|
+
# returns int samplerate (nominal)
|
|
1559
1408
|
string = self._ffprobe_audio_stream()['sample_rate']
|
|
1560
|
-
|
|
1409
|
+
logger.debug('ffprobe samplerate: %s'%string)
|
|
1410
|
+
return eval(string)
|
|
1561
1411
|
|
|
1562
1412
|
def get_framerate(self):
|
|
1563
|
-
# return int samplerate (nominal)
|
|
1564
1413
|
string = self._ffprobe_video_stream()['avg_frame_rate']
|
|
1565
1414
|
return eval(string) # eg eval(24000/1001)
|
|
1566
1415
|
|
|
1567
|
-
def
|
|
1568
|
-
# returns a
|
|
1416
|
+
def get_start_timecode_string(self, with_offset=0):
|
|
1417
|
+
# returns a HH:MM:SS:FR string
|
|
1569
1418
|
start_datetime = self.get_start_time()
|
|
1419
|
+
# logger.debug('CLI_offset %s'%CLI_offset)
|
|
1570
1420
|
logger.debug('start_datetime %s'%start_datetime)
|
|
1571
1421
|
start_datetime += timedelta(seconds=with_offset)
|
|
1572
1422
|
logger.debug('shifted start_datetime %s (offset %f)'%(start_datetime,
|
|
@@ -1602,20 +1452,20 @@ class Recording:
|
|
|
1602
1452
|
|
|
1603
1453
|
def has_audio(self):
|
|
1604
1454
|
if not self.probe:
|
|
1605
|
-
return False
|
|
1455
|
+
return False #######################################################
|
|
1606
1456
|
streams = self.probe['streams']
|
|
1607
1457
|
codecs = [stream['codec_type'] for stream in streams]
|
|
1608
1458
|
return 'audio' in codecs
|
|
1609
1459
|
|
|
1610
1460
|
def get_audio_channels_nbr(self):
|
|
1611
1461
|
if not self.has_audio():
|
|
1612
|
-
return 0
|
|
1462
|
+
return 0 ###########################################################
|
|
1613
1463
|
audio_str = self._ffprobe_audio_stream()
|
|
1614
1464
|
return audio_str['channels']
|
|
1615
1465
|
|
|
1616
1466
|
def is_video(self):
|
|
1617
1467
|
if not self.probe:
|
|
1618
|
-
return False
|
|
1468
|
+
return False #######################################################
|
|
1619
1469
|
streams = self.probe['streams']
|
|
1620
1470
|
codecs = [stream['codec_type'] for stream in streams]
|
|
1621
1471
|
return 'video' in codecs
|
|
@@ -1623,15 +1473,17 @@ class Recording:
|
|
|
1623
1473
|
def is_audio(self):
|
|
1624
1474
|
return not self.is_video()
|
|
1625
1475
|
|
|
1626
|
-
def
|
|
1476
|
+
def _find_TicTacCode(self, time_where, chunk_length):
|
|
1627
1477
|
"""
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1478
|
+
Extracts a chunk from Recording.audio_data and sends it to
|
|
1479
|
+
Recording.decoder object with set_sound_extract_and_sr() to find which
|
|
1480
|
+
channel contains a TicTacCode track and sets TicTacCode_channel
|
|
1481
|
+
accordingly (index of channel). On exit, self.decoder.sound_extract
|
|
1482
|
+
contains TicTacCode data ready to be demodulated. If not,
|
|
1483
|
+
self.TicTacCode_channel is set to None.
|
|
1484
|
+
|
|
1485
|
+
If this has been called before (checking self.TicTacCode_channel) then
|
|
1486
|
+
is simply read the audio in and calls set_sound_extract_and_sr().
|
|
1635
1487
|
|
|
1636
1488
|
Args:
|
|
1637
1489
|
time_where : float
|
|
@@ -1643,7 +1495,8 @@ class Recording:
|
|
|
1643
1495
|
self.decoder.set_sound_extract_and_sr()
|
|
1644
1496
|
|
|
1645
1497
|
Sets:
|
|
1646
|
-
self.
|
|
1498
|
+
self.TicTacCode_channel = index of TTC chan
|
|
1499
|
+
self.device.ttc = index of TTC chan
|
|
1647
1500
|
|
|
1648
1501
|
Returns:
|
|
1649
1502
|
this Recording instance
|
|
@@ -1653,56 +1506,52 @@ class Recording:
|
|
|
1653
1506
|
decoder = self.decoder
|
|
1654
1507
|
if decoder:
|
|
1655
1508
|
decoder.clear_decoder()
|
|
1656
|
-
# decoder.cached_convolution_fit['is clean'] = False
|
|
1657
1509
|
if not self.has_audio():
|
|
1658
|
-
self.
|
|
1659
|
-
return
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
.
|
|
1670
|
-
|
|
1671
|
-
.
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
.
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1510
|
+
self.TicTacCode_channel = None
|
|
1511
|
+
return #############################################################
|
|
1512
|
+
sound_data_var = np.std(self.audio_data)
|
|
1513
|
+
sound_extract_position = int(self.get_samplerate()*time_where)
|
|
1514
|
+
logger.debug('extracting sound at %i with variance %f'%(
|
|
1515
|
+
sound_extract_position,
|
|
1516
|
+
sound_data_var))
|
|
1517
|
+
if self.TicTacCode_channel == None:
|
|
1518
|
+
logger.debug('first call, will loop through all %i channels'%len(
|
|
1519
|
+
self.audio_data))
|
|
1520
|
+
for i_chan, chan_dat in enumerate(self.audio_data):
|
|
1521
|
+
logger.debug('testing chan %i'%i_chan)
|
|
1522
|
+
start_idx = round(time_where*self.get_samplerate())
|
|
1523
|
+
extract_length = round(chunk_length*self.get_samplerate())
|
|
1524
|
+
end_idx = start_idx + extract_length
|
|
1525
|
+
extract_audio_data = chan_dat[start_idx:end_idx]
|
|
1526
|
+
decoder.set_sound_extract_and_sr(
|
|
1527
|
+
extract_audio_data,
|
|
1528
|
+
self.get_samplerate(),
|
|
1529
|
+
sound_extract_position
|
|
1530
|
+
)
|
|
1531
|
+
if decoder.extract_seems_TicTacCode():
|
|
1532
|
+
self.TicTacCode_channel = i_chan
|
|
1533
|
+
self.device.ttc = i_chan
|
|
1534
|
+
logger.debug('found TicTacCode channel: chan #%i'%
|
|
1535
|
+
self.TicTacCode_channel)
|
|
1536
|
+
return self ################################################
|
|
1537
|
+
# end of loop: none found
|
|
1538
|
+
# self.TicTacCode_channel = None # was None already
|
|
1539
|
+
logger.warning('found no TicTacCode channel')
|
|
1540
|
+
else:
|
|
1541
|
+
logger.debug('been here before, TTC chan is %i'%
|
|
1542
|
+
self.TicTacCode_channel)
|
|
1543
|
+
start_idx = round(time_where*self.get_samplerate())
|
|
1544
|
+
extract_length = round(chunk_length*self.get_samplerate())
|
|
1545
|
+
end_idx = start_idx + extract_length
|
|
1546
|
+
chan_dat = self.audio_data[self.TicTacCode_channel]
|
|
1547
|
+
extract_audio_data = chan_dat[start_idx:end_idx]
|
|
1685
1548
|
decoder.set_sound_extract_and_sr(
|
|
1686
|
-
|
|
1549
|
+
extract_audio_data,
|
|
1687
1550
|
self.get_samplerate(),
|
|
1688
1551
|
sound_extract_position
|
|
1689
1552
|
)
|
|
1690
|
-
if decoder.extract_seems_YaLTC():
|
|
1691
|
-
self.YaLTC_channel = i_chan
|
|
1692
|
-
logger.debug('find YaLTC channel, chan #%i'%
|
|
1693
|
-
self.YaLTC_channel)
|
|
1694
|
-
return self
|
|
1695
|
-
# end of loop: none found
|
|
1696
|
-
self.YaLTC_channel = None
|
|
1697
|
-
logger.warning('found no YaLTC channel')
|
|
1698
1553
|
return self
|
|
1699
1554
|
|
|
1700
|
-
def seems_to_have_YaLTC_at_beginning(self):
|
|
1701
|
-
if self.probe is None:
|
|
1702
|
-
return False
|
|
1703
|
-
self._read_sound_find_YaLTC(TRIAL_TIMES[0][0], SOUND_EXTRACT_LENGTH)
|
|
1704
|
-
return self.YaLTC_channel is not None
|
|
1705
|
-
|
|
1706
1555
|
def does_overlap_with_time(self, time):
|
|
1707
1556
|
A1, A2 = self.get_start_time(), self.get_end_time()
|
|
1708
1557
|
# R1, R2 = rec.get_start_time(), rec.get_end_time()
|
|
@@ -1747,24 +1596,3 @@ class Recording:
|
|
|
1747
1596
|
)
|
|
1748
1597
|
return clip
|
|
1749
1598
|
|
|
1750
|
-
import argparse
|
|
1751
|
-
|
|
1752
|
-
if __name__ == '__main__':
|
|
1753
|
-
parser = argparse.ArgumentParser()
|
|
1754
|
-
parser.add_argument("mediafile", nargs="?", help="path of media file with YaLTC")
|
|
1755
|
-
parser.add_argument('-v', action='store_true')
|
|
1756
|
-
args = parser.parse_args()
|
|
1757
|
-
if not args.v:
|
|
1758
|
-
logger.remove()
|
|
1759
|
-
fn = args.mediafile
|
|
1760
|
-
a_recording = Recording(fn)
|
|
1761
|
-
start_time = a_recording.get_start_time()
|
|
1762
|
-
if start_time is not None:
|
|
1763
|
-
print('%s started at %s'%(fn, start_time))
|
|
1764
|
-
else:
|
|
1765
|
-
print("can't find start time for %s"%fn)
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|