LASP 1.0
Library for Acoustic Signal Processing
Loading...
Searching...
No Matches
lasp_measurement.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3from __future__ import annotations
4"""!
5Author: J.A. de Jong - ASCEE
6
7Description: Measurement class
8
9The ASCEE hdf5 measurement file format contains the following fields:
10
11- Attributes:
12
13'version': If not given, version 1 is assumed. For version 1, measurement data
14is assumed to be acoustic data.
15'samplerate': The audio data sample rate in Hz.
16'nchannels': The number of audio channels in the file
17'sensitivity': (Optionally) the stored sensitivity of the record channels.
18 This can be a single value, or a list of sensitivities for
19 each channel. Both representations are allowed.
20
21For measurement files of LASP < v1.0
22'qtys' : (Optionally): list of quantities that is recorded for each channel',
23if this array is not found. Quantities are defaulted to 'Number / Full scale'
24
25For measurement files of LASP >= 1.0
26
27- Datasets:
28
29'audio': 3-dimensional array of blocks of audio data. The first axis is the
30block index, the second axis the sample number and the third axis is the
31channel number. The data type is either int16, int32 or float64 / float32. If
32raw data is stored as integer values (int16, int32), the actual values should
33be pre-scaled by its maximum positive number (2**(nb-1) - 1), such that the
34corresponding 'number' lies between -1.0 and 1.0.
35
36'video': 4-dimensional array of video frames. The first index is the frame
37 number, the second the x-value of the pixel and the third is the
38 y-value of the pixel. Then, the last axis is the color. This axis has
39 length 3 and the colors are stored as (r,g,b). Where typically a
40 color depth of 256 is used (np.uint8 data format)
41
42The video dataset can possibly be not present in the data.
43
44"""
45
46__all__ = ['Measurement', 'scaleBlockSens']
47from contextlib import contextmanager
48import h5py as h5
49import numpy as np
50from .lasp_config import LASP_NUMPY_FLOAT_TYPE
51from scipy.io import wavfile
52import os, time, wave, logging
53from .lasp_common import SIQtys, Qty, getFreq
54from .lasp_cpp import Window, DaqChannel, LASP_VERSION_MAJOR, AvPowerSpectra
55from typing import List
56from functools import lru_cache
57
58
59def getSampWidth(dtype):
60 """Returns the width of a single sample in **bytes**.
61
62 Args:
63 dtype: numpy dtype
64
65 Returns:
66 Size of a sample in bytes (int)
67 """
68 if dtype in (np.int32, np.float32):
69 return 4
70 elif dtype == np.int16:
71 return 2
72 elif dtype == np.float64:
73 return 8
74 else:
75 raise ValueError('Invalid data type: %s' % dtype)
76
77
78def scaleBlockSens(block, sens):
79 """Scale a block of raw data to return raw acoustic pressure data.
80
81 Args:
82 block: block of raw data with integer data type
83 sens: array of sensitivity coeficients for
84 each channel.
85 """
86 sens = np.asarray(sens)
87 assert sens.size == block.shape[1]
88 if np.issubdtype(block.dtype.type, np.integer):
89 sw = getSampWidth(block.dtype)
90 fac = 2**(8 * sw - 1) - 1
91 else:
92 fac = 1.
93 return block.astype(LASP_NUMPY_FLOAT_TYPE) / fac / sens[np.newaxis, :]
94
95
97 """Iterate over stored blocks if the raw measurement data of a h5 file."""
98
99 def __init__(self, f, channels, **kwargs):
100 """Initialize a BlockIter object.
101
102 Args:
103 f: Audio dataset in the h5 file, accessed as f['audio']
104 channels: list of channel indices to use
105 istart: index of first sample
106 istop: index of last sample (not including istop)
107
108 """
109 assert isinstance(channels, list)
110 fa = f['audio']
111 self.fa = fa
112 self.i = 0
113
114 nblocks = fa.shape[0]
115 blocksize = fa.shape[1]
116 self.blocksize = blocksize
117 # nchannels = fa.shape[2]
118 self.channels = channels
119
120 self.istart = kwargs.pop('istart', 0)
121 self.istop = kwargs.pop('istop', blocksize*nblocks)
122
123 self.firstblock = self.istart // blocksize
124 self.lastblock = self.istop // blocksize
125 if self.istop % blocksize == 0:
126 self.lastblock -= 1
127
128 self.firstblock_start_offset = self.istart % blocksize
129
130 if self.istop < 0:
131 self.istop += blocksize*nblocks
132 if self.istop <= self.istart:
133 raise ValueError('Stop index is smaller than start index')
134
135 if self.istop != blocksize*nblocks:
136 self.lastblock_stop_offset = self.istop % blocksize
137 else:
138 self.lastblock_stop_offset = blocksize
139
140 def __iter__(self):
141 return self
142
143 def __next__(self):
144 """Return the next block."""
145 fa = self.fa
146
147 # nblocks_to_return = self.lastblock-self.firstblock+1
148
149 block = self.firstblock + self.i
150
151 if block > self.lastblock:
152 raise StopIteration
153
154 if block == self.firstblock:
155 start_offset = self.firstblock_start_offset
156 else:
157 start_offset = 0
158
159 if block == self.lastblock:
160 stop_offset = self.lastblock_stop_offset
161 else:
162 stop_offset = self.blocksize
163 # print(f'block: {block}, starto: {start_offset}, stopo {stop_offset}')
164
165 self.i += 1
166 return fa[block, start_offset:stop_offset, :][:, self.channels]
167
168
170 """
171 Iterate over blocks of data, scaled with sensitivity and integer scaling
172 between 0 and 1
173 """
174 def __init__(self, fa, channels, sensitivity, **kwargs):
175 super().__init__(fa, channels, **kwargs)
176 self.sens = np.asarray(sensitivity)[self.channels]
177 assert self.sens.ndim == 1
178
179 def __next__(self):
180 nextraw = super().__next__()
181 return scaleBlockSens(nextraw, self.sens)
182
183
185 """Provides access to measurement data stored in the h5 measurement file
186 format."""
187
188 def __init__(self, fn):
189 """Initialize a Measurement object based on the filename."""
190 if '.h5' not in fn:
191 fn += '.h5'
192
193 # Full filepath
194 self.fn = fn
195
196 # Base filename
197 self.fn_base = os.path.split(fn)[1]
198
199 # Open the h5 file in read-plus mode, to allow for changing the
200 # measurement comment.
201 with h5.File(fn, 'r') as f:
202 # Check for video data
203 try:
204 f['video']
205 self.has_video = True
206 except KeyError:
207 self.has_video = False
208
209 self.nblocks, self.blocksize, self.nchannels = f['audio'].shape
210 dtype = f['audio'].dtype
211 self.dtype = dtype
213
214 self.samplerate = f.attrs['samplerate']
215 self.N = (self.nblocks * self.blocksize)
216 self.T = self.N / self.samplerate
217
218 try:
219 self.version_major = f.attrs['LASP_VERSION_MAJOR']
220 self.version_minor = f.attrs['LASP_VERSION_MINOR']
221 except KeyError:
222 self.version_major = 0
223 self.version_minor = 1
224
225 # Due to a previous bug, the channel names were not stored
226 # consistently, i.e. as 'channel_names' and later camelcase.
227 try:
228 self._channelNames = f.attrs['channelNames']
229 except KeyError:
230 try:
231 self._channelNames = f.attrs['channel_names']
232 logging.info("Measurement file obtained which stores channel names with *old* attribute 'channel_names'")
233 except KeyError:
234 # No channel names found in measurement file
235 logging.info('No channel name data found in measurement')
236 self._channelNames = [f'Unnamed {i}' for i in range(self.nchannels)]
237
238 # comment = read-write thing
239 if 'comment' in f.attrs:
240 self._comment = f.attrs['comment']
241 else:
242 self._comment = ''
243
244 # Sensitivity
245 try:
246 sens = f.attrs['sensitivity']
247 self._sens = sens * \
248 np.ones(self.nchannels) if isinstance(
249 sens, float) else sens
250 except KeyError:
251 self._sens = np.ones(self.nchannels)
252
253 # The time is cached AND ALWAYS ASSUMED TO BE AN IMMUTABLE OBJECT.
254 # It is also cached. Changing the measurement timestamp should not
255 # be done.
256 self._time = f.attrs['time']
257
258 # Quantity stored as channel.
259 self._qtys = None
260
261 try:
262 qtys_enum_idx = f.attrs['qtys_enum_idx']
263 self._qtys = [SIQtys.fromInt(idx) for idx in qtys_enum_idx]
264 except KeyError:
265 try:
266 qtys_json = f.attrs['qtys']
267 # Load quantity data
268 self._qtys = [Qty.from_json(qty_json) for qty_json in qtys_json]
269 except KeyError:
270 # If quantity data is not available, this is an 'old'
271 # measurement file.
272 pass
273
274 if self._qtys is None:
275 self._qtys = [SIQtys.default() for i in range(self.nchannels)]
276 logging.debug(f'Physical quantity data not available in measurement file. Assuming {SIQtys.default}')
277
278 def setAttribute(self, atrname, value):
279 """
280 Set an attribute in the measurement file, and keep a local copy in
281 memory for efficient accessing.
282 """
283 with self.file('r+') as f:
284 # Update comment attribute in the file
285 f.attrs[atrname] = value
286 setattr(self, '_' + atrname, value)
287
288 @property
289 def name(self):
290 """Returns filename base without extension."""
291 return os.path.splitext(self.fn_base)[0]
292
293 @property
294 def channelNames(self):
295 return self._channelNames
296
297 @channelNames.setter
298 def channelNames(self, newchnames):
299 if len(newchnames) != self.nchannels:
300 raise RuntimeError('Invalid length of new channel names')
301 self.setAttribute('channelNames', newchnames)
302
303 @property
304 def channelConfig(self):
305 chcfg = []
308 ch = DaqChannel()
309 ch.enabled = True
310 ch.name = chname
311 ch.sensitivity = sens
312 ch.qty = qty.cpp_enum
313 chcfg.append(ch)
314 return chcfg
315
316 @channelConfig.setter
317 def channelConfig(self, chcfg: List[DaqChannel]):
318 chname = []
319 sens = []
320 qtys = []
321 for ch in chcfg:
322 chname.append(ch.name)
323 sens.append(ch.sensitivity)
324 qtys.append(SIQtys.fromCppEnum(ch.qty))
325
328 self.qtysqtysqtys = qtys
329
330 @property
331 def qtys(self):
332 return self._qtys
333
334 @qtys.setter
335 def qtys(self, newqtys):
336 if not len(newqtys) == len(self._qtys):
337 raise ValueError('Invalid number of quantities')
338 qtys_int = [qty.toInt() for qty in newqtys]
339 # Use setAttribute here, but thos store the jsonified version as well,
340 # which we have to overwrite again with the deserialized ones. This is
341 # actually not a very nice way of coding.
342 with self.file('r+') as f:
343 # Update comment attribute in the file
344 f.attrs['qtys_enum_idx'] = qtys_int
345
346 self._qtys = newqtys
347
348 @contextmanager
349 def file(self, mode='r'):
350 """Contextmanager which opens the storage file and yields the file.
351
352 Args:
353 mode: Opening mode for the file. Should either be 'r', or 'r+'
354 """
355 if mode not in ('r', 'r+'):
356 raise ValueError('Invalid file opening mode.')
357 with h5.File(self.fn, mode) as f:
358 yield f
359
360 @property
361 def comment(self):
362 """Return the measurement comment.
363
364 Returns:
365 The measurement comment (text string)
366 """
367 return self._comment
368
369 @comment.setter
370 def comment(self, cmt):
371 """Set the measurement comment.
372
373 Args:
374 cmt: Comment text string to set
375 """
376 with self.file('r+') as f:
377 # Update comment attribute in the file
378 f.attrs['comment'] = cmt
379 self._comment = cmt
380
381 @property
382 @lru_cache()
383 def recTime(self):
384 """Returns the total recording time of the measurement, in float
385 seconds."""
386 return self.blocksize * self.nblocks / self.samplerate
387
388 @property
389 def time(self):
390 """Returns the measurement time in seconds since the epoch."""
391 return self._time
392
393 @property
394 @lru_cache()
395 def timestr(self):
396 """
397 Return a properly formatted string of the measurement time, in order of
398
399 year-month-day hour etc.
400
401 """
402 time_struct = time.localtime(self.time)
403 time_string = time.strftime('%Y-%m-%d %H:%M:%S', time_struct)
404 return time_string
405
406 def rms(self, channels=None, substract_average=False):
407 """Returns the root mean square values for each channel
408
409 Args:
410 channels: list of channels
411 substract_average: If set to true, computes the rms of only the
412 oscillating component of the signal, which is in fact the signal
413 variance.
414
415 Returns:
416 1D array with rms values for each channel
417 """
418 meansquare = 0. # Mean square of the signal, including its average
419 sum_ = 0. # Sumf of the values of the signal, used to compute average
420 N = 0
421 with self.file() as f:
422 for block in self.iterData(channels):
423 Nblock = block.shape[0]
424 sum_ += np.sum(block, axis=0)
425 N += Nblock
426 meansquare += np.sum(block**2, axis=0) / self.N
427
428 avg = sum_/N
429 # In fact, this is not the complete RMS, as in includes the DC
430 # If p = p_dc + p_osc, then rms(p_osc) = sqrt(ms(p)-ms(p_dc))
431 if substract_average:
432 meansquare -= avg**2
433 rms = np.sqrt(meansquare)
434 return rms
435
436 def variance(self, channels=None):
437 return self.rms(substract_average=True)
438
439 def rawData(self, channels=None, **kwargs):
440 """Returns the raw data as stored in the measurement file,
441 without any transformations applied
442
443 args:
444 channels: list, or tuple of channel numbers to export. If not defined, all
445 channels in the measurement are returned
446
447 returns:
448 Numpy array with data. The first axis is always the time instance,
449 the second axis the channel number.
450
451 """
452 if channels is None:
453 channels = list(range(self.nchannels))
454
455 rawdata = []
456
457 with self.file() as f:
458 for block in IterRawData(f, channels, **kwargs):
459 rawdata.append(block)
460
461 return np.concatenate(rawdata, axis=0)
462
463 def iterData(self, channels, **kwargs):
464 sensitivity = kwargs.pop('sensitivity', self.sensitivitysensitivitysensitivity)
465 if channels is None:
466 channels = list(range(self.nchannels))
467 with self.file() as f:
468 for block in IterData(f, channels, sensitivity, **kwargs):
469 yield block
470
471 def data(self, channels=None, **kwargs):
472 """
473 Returns the measurement data, scaled and sensitivity applied.
474 """
475 data = []
476 for d in self.iterData(channels, **kwargs):
477 data.append(d)
478
479 return np.concatenate(data, axis=0)
480
481 def CPS(self, channels=None, **kwargs):
482 """
483 Compute single-sided Cross-Power-Spectrum of the measurement channels
484
485 Args:
486 channels: Channels to compute for (numbers)
487
488 Optional arguments:
489 nfft: FFT length
490 window: Window type
491 overlap: Overlap percentage (value between 0.0 and up to and
492 including 100.0)
493 weighting:
494
495 Returns:
496 Cross-power-spectra. C[freq, ch_i, ch_j] = C_ij
497
498 """
499 nfft = kwargs.pop('nfft', 2048)
500 window = kwargs.pop('windowType', Window.WindowType.Hann)
501 overlap = kwargs.pop('overlap', 50.0)
502
503 if channels is None:
504 channels = list(range(self.nchannels))
505
506 nchannels = len(channels)
507 aps = AvPowerSpectra(nfft, window, overlap)
508 freq = getFreq(self.samplerate, nfft)
509
510 for data in self.iterData(channels, **kwargs):
511 CS = aps.compute(data)
512
513 return freq, aps.get_est()
514
515 def periodicAverage(self, N, channels=None, noiseCorrection=True, **kwargs):
516 """
517 Return the (coherent) periodic average the measurement. This method is
518 useful for a situation of periodic excitation.
519
520 Args:
521 N: The number of samples in one period. This value should
522 correspond with the period of the excitation!
523 noiseCorrection: whether to apply coherent averaging, according to
524 the Sliding Window correlation method (SWiC): Telle et al.: A Novel
525 Approach for Impulse Response Measurements with Time-Varying Noise.
526 If set to False, just the arithmetic average is used.
527 """
528 # Create blocks of equal length N
529 Ntot = self.N
530 Nblocks = Ntot//N
531
532 # TODO: This method graps the whole measurement file into memory. Can
533 # only be done with relatively small measurement files.
534 signal = self.data(channels)
535
536 # Estimate noise power in block
537 blocks = [signal[i*N:(i+1)*N] for i in range(Nblocks)]
538
539 if noiseCorrection:
540 # The difference between the measured signal in the previous block and
541 # the current block
542 en = [None] + [blocks[i] - blocks[i-1] for i in range(1,Nblocks)]
543
544 noise_est = [None] + [-np.average(en[i]*en[i+1]) for i in range(1,len(en)-1)]
545
546 # Create weighting coefficients
547 sum_inverse_noise = sum([1/n for n in noise_est[1:]])
548 c_n = [1/(ni*sum_inverse_noise) for ni in noise_est[1:]]
549 else:
550 c_n = [1/(Nblocks-2)]*(Nblocks-2)
551
552 assert np.isclose(sum(c_n), 1.0)
553 assert Nblocks-2 == len(c_n)
554
555 # Average signal over blocks
556 avg = np.zeros((blocks[0].shape), dtype=float)
557 for n in range(0, Nblocks-2):
558 avg += c_n[n]*blocks[n+1]
559
560 return avg
561
562 def periodicCPS(self, N, channels=None, **kwargs):
563 """
564 Compute Cross-Spectral Density based on periodic excitation. Uses noise
565 reduction by time-averaging the data.
566 """
567
568 if channels is None:
569 channels = list(range(self.nchannels))
570
571 nchannels = len(channels)
572 window = Window.rectangular
573 ps = PowerSpectra(N, window)
574
575 avg = np.asfortranarray(self.periodicAverage(N, channels, **kwargs))
576 CS = ps.compute(avg)
577 freq = getFreq(self.samplerate, N)
578
579 return freq, CS
580
581
582 @property
583 def sensitivity(self):
584 """Sensitivity of the data in U^-1, from floating point data scaled
585 between -1.0 and 1.0 to Units [U].
586
587 If the sensitivity is not stored in the measurement file, this
588 function returns 1.0 for each channel
589 """
590 return self._sens
591
592 @sensitivity.setter
593 def sensitivity(self, sens):
594 """Set the sensitivity of the measurement in the file.
595
596 Args:
597 sens: sensitivity data, should be a float, or an array of floats
598 equal to the number of channels.
599 """
600 if isinstance(sens, float):
601 # Put all sensitivities equal
602 sens = sens * np.ones(self.nchannels)
603 elif isinstance(sens, list):
604 sens = np.asarray(sens)
605
606 valid = sens.ndim == 1
607 valid &= sens.shape[0] == self.nchannels
608 valid &= sens.dtype == float
609 if not valid:
610 raise ValueError('Invalid sensitivity value(s) given')
611 with self.file('r+') as f:
612 f.attrs['sensitivity'] = sens
613 self._sens = sens
614
615 def checkOverflow(self, channels):
616 """Coarse check for overflow in measurement.
617
618 Return:
619 True if overflow is possible, else False
620 """
621
622 for block in self.iterData(channels):
623 dtype = block.dtype
624 if dtype.kind == 'i':
625 # minvalue = np.iinfo(dtype).min
626 maxvalue = np.iinfo(dtype).max
627 if np.max(np.abs(block)) >= 0.9*maxvalue:
628 return True
629 else:
630 # Cannot check for floating point values.
631 return False
632 return False
633
634
635 def exportAsWave(self, fn=None, force=False, dtype=None,
636 normalize=False, **kwargs):
637 """Export measurement file as wave. In case the measurement data is
638 stored as floats, the values are scaled to the proper integer (PCM)
639 data format.
640
641 Args:
642 fn: If given, this will be the filename to write to. If the
643 filename does not end with '.wav', this extension is added.
644
645 force: If True, overwrites any existing files with the given name
646 , otherwise a RuntimeError is raised.
647
648 dtype: if not None, convert data to this data type.
649 Options are 'int16', 'int32', 'float32'.
650
651 normalize: If set: normalize the level to something sensible.
652 """
653 if fn is None:
654 fn = self.fn
655 fn = os.path.splitext(fn)[0]
656
657 if os.path.splitext(fn)[1] != '.wav':
658 fn += '.wav'
659
660 if os.path.exists(fn) and not force:
661 raise RuntimeError(f'File already exists: {fn}')
662
663 if not np.isclose(self.samplerate%1,0):
664 raise RuntimeError(f'Sample rates should be approximately integer for exporting to Wave to work')
665
666 # TODO: With VERY large measurment files, this is not possible! Is this
667 # a theoretical case?
668 # TODO: add sensitivity? Then use self.data() instead of self.rawData()
669 data = self.rawData(**kwargs)
670
671 if normalize:
672 # Scale back to maximum of absolute value
673 maxabs = np.max(np.abs(data))
674 data = data / maxabs # "data /= maxabs" fails if dtpyes differ
675
676 if dtype==None:
677 dtype = data.dtype # keep existing
678 logging.debug(f"dtype not passed as arg; using dtype = {dtype}")
679
680 # dtype conversion
681 if dtype=='int16':
682 newtype = np.int16
683 newsampwidth = 2
684 elif dtype=='int32':
685 newtype = np.int32
686 newsampwidth = 4
687 elif dtype=='float32':
688 newtype = np.float32
689 elif dtype=='float64':
690 newtype = np.float64
691 else:
692 logging.debug(f"cannot handle this dtype {dtype}")
693 pass
694
695 # Convert range to [-1, 1]
696 # TODO: this is wrong for float data where full scale > 1
697 sensone = np.ones_like(self.sensitivitysensitivitysensitivity)
698 data = scaleBlockSens(data, sensone)
699
700 if dtype=='int16' or dtype=='int32':
701 # Scale data to integer range and convert to integers
702 scalefac = 2**(8*newsampwidth-1)-1
703 data = (data*scalefac).astype(newtype)
704
705 wavfile.write(fn, int(self.samplerate), data.astype(newtype))
706
707 @staticmethod
708 def fromtxt(fn,
709 skiprows,
710 samplerate,
711 sensitivity,
712 mfn=None,
713 timestamp=None,
714 delimiter='\t',
715 firstcoltime=True):
716 """Converts a txt file to a LASP Measurement file, opens the associated
717 Measurement object and returns it. The measurement file will have the
718 same file name as the txt file, except with h5 extension.
719
720 Args:
721 fn: Filename of text file
722 skiprows: Number of header rows in text file to skip
723 samplerate: Sampling frequency in [Hz]
724 sensitivity: 1D array of channel sensitivities
725 mfn: Filepath where measurement file is stored. If not given,
726 a h5 file will be created along fn, which shares its basename
727 timestamp: If given, a custom timestamp for the measurement
728 (integer containing seconds since epoch). If not given, the
729 timestamp is obtained from the last modification time.
730 delimiter: Column delimiter
731 firstcoltime: If true, the first column is the treated as the
732 sample time.
733 """
734 if not os.path.exists(fn):
735 raise ValueError(f'File {fn} does not exist.')
736 if timestamp is None:
737 timestamp = os.path.getmtime(fn)
738 if mfn is None:
739 mfn = os.path.splitext(fn)[0] + '.h5'
740 else:
741 mfn = os.path.splitext(mfn)[0] + '.h5'
742
743 dat = np.loadtxt(fn, skiprows=skiprows, delimiter=delimiter)
744 if firstcoltime:
745 time = dat[:, 0]
746 if not np.isclose(time[1] - time[0], 1 / samplerate):
747 raise ValueError('Samplerate given does not agree with '
748 'samplerate in file')
749
750 # Chop off first column
751 dat = dat[:, 1:]
752 nchannels = dat.shape[1]
753 if nchannels != sensitivity.shape[0]:
754 raise ValueError(
755 f'Invalid sensitivity length given. Should be: {nchannels}')
756
757 with h5.File(mfn, 'w') as hf:
758 hf.attrs['samplerate'] = samplerate
759 hf.attrs['sensitivity'] = sensitivity
760 hf.attrs['time'] = timestamp
761 hf.attrs['blocksize'] = 1
762 hf.attrs['nchannels'] = nchannels
763 ad = hf.create_dataset('audio', (1, dat.shape[0], dat.shape[1]),
764 dtype=dat.dtype,
765 maxshape=(1, dat.shape[0], dat.shape[1]),
766 compression='gzip')
767 ad[0] = dat
768 return Measurement(mfn)
769
770 @staticmethod
771 def fromnpy(data,
772 samplerate,
773 sensitivity,
774 mfn,
775 timestamp=None,
776 qtys: List[SIQtys] = None,
777 channelNames: List[str] = None,
778 force=False) -> Measurement:
779 """
780 Creates a LASP measurement file from input numpy array data.
781 Opens the associated Measurement object and returns it.
782
783 Args:
784 data: Numpy array, first column is sample, second is channel. Can
785 also be specified with a single column for single-channel data.
786
787 samplerate: Sampling frequency in [Hz]
788
789 sensitivity: 1D array of channel sensitivities in [U^-1], where U is
790 the recorded unit.
791
792 mfn: Filepath of the file where the data is stored.
793
794 timestamp: If given, a custom timestamp for the measurement
795 (integer containing seconds since epoch).
796
797 qtys: If a list of physical quantity data is given here
798
799 channelNames: Name of the channels
800
801 force: If True, overwrites existing files with specified `mfn`
802 name.
803 """
804 if os.path.splitext(mfn)[1] != '.h5':
805 mfn += '.h5'
806 if os.path.exists(mfn) and not force:
807 raise ValueError(f'File {mfn} already exist.')
808 if timestamp is None:
809 timestamp = int(time.time())
810
811 if data.ndim != 2:
812 data = data[:, np.newaxis]
813
814
815 try:
816 len(sensitivity)
817 except:
818 raise ValueError('Sensitivity should be given as array-like data type')
819 sensitivity = np.asarray(sensitivity)
820
821 nchannels = data.shape[1]
822 if nchannels != sensitivity.shape[0]:
823 raise ValueError(
824 f'Invalid sensitivity length given. Should be: {nchannels}')
825
826 if channelNames is not None:
827 if len(channelNames) != nchannels:
828 raise RuntimeError("Illegal length of channelNames list given")
829
830 if qtys is None:
831 qtys = [SIQtys.AP]*nchannels
832 else:
833 if len(qtys) != nchannels:
834 raise RuntimeError("Illegal length of qtys list given")
835
836 qtyvals = [qty.value for qty in qtys]
837
838 with h5.File(mfn, 'w') as hf:
839 hf.attrs['samplerate'] = samplerate
840 hf.attrs['sensitivity'] = sensitivity
841 hf.attrs['time'] = timestamp
842 hf.attrs['blocksize'] = 1
843 hf.attrs['nchannels'] = nchannels
844
845 # Add physical quantity indices
846 hf.attrs['qtys_enum_idx'] = [qtyval.toInt() for qtyval in qtyvals]
847
848 # Add channel names in case given
849 if channelNames is not None:
850 hf.attrs['channelNames'] = channelNames
851
852 ad = hf.create_dataset('audio', (1, data.shape[0], data.shape[1]),
853 dtype=data.dtype,
854 maxshape=(1, data.shape[0], data.shape[1]),
855 compression='gzip')
856 ad[0] = data
857 return Measurement(mfn)
858
859 @staticmethod
860 def fromWaveFile(fn, newfn=None, force=False, timestamp=None):
861 """Convert a measurement file to a wave file, and return the
862 measurement handle."""
863 if timestamp is None:
864 timestamp = int(time.time())
865
866 base_fn = os.path.splitext(fn)[0]
867 if newfn is None:
868 newfn = base_fn + '.h5'
869 if os.path.exists(newfn) and not force:
870 raise RuntimeError(f'Measurement file name {newfn} already exists in path, set "force" to true to overwrite')
871
872 samplerate, data = wavfile.read(fn)
873 if data.ndim == 2:
874 nframes, nchannels = data.shape
875 else:
876 nchannels = 1
877 nframes = len(data)
878 data = data[:, np.newaxis]
879 sensitivity = np.ones(nchannels)
880
881 with h5.File(newfn, 'w') as hf:
882 hf.attrs['samplerate'] = samplerate
883 hf.attrs['nchannels'] = nchannels
884 hf.attrs['time'] = timestamp
885 hf.attrs['blocksize'] = 1
886 hf.attrs['sensitivity'] = sensitivity
887 ad = hf.create_dataset('audio', (1, nframes, nchannels),
888 dtype=data.dtype,
889 maxshape=(1, nframes, nchannels),
890 compression='gzip')
891 ad[0] = data
892
893 return Measurement(newfn)
Computes single-sided cross-power spectra for a group of channels. Only a single block of length fft,...
Iterate over blocks of data, scaled with sensitivity and integer scaling between 0 and 1.
__next__(self)
Return the next block.
__init__(self, fa, channels, sensitivity, **kwargs)
Initialize a BlockIter object.
Iterate over stored blocks if the raw measurement data of a h5 file.
__init__(self, f, channels, **kwargs)
Initialize a BlockIter object.
__next__(self)
Return the next block.
Provides access to measurement data stored in the h5 measurement file format.
sensitivity(self, sens)
Set the sensitivity of the measurement in the file.
periodicAverage(self, N, channels=None, noiseCorrection=True, **kwargs)
Return the (coherent) periodic average the measurement.
exportAsWave(self, fn=None, force=False, dtype=None, normalize=False, **kwargs)
Export measurement file as wave.
file(self, mode='r')
Contextmanager which opens the storage file and yields the file.
name(self)
Returns filename base without extension.
fromtxt(fn, skiprows, samplerate, sensitivity, mfn=None, timestamp=None, delimiter='\t', firstcoltime=True)
Converts a txt file to a LASP Measurement file, opens the associated Measurement object and returns i...
checkOverflow(self, channels)
Coarse check for overflow in measurement.
recTime(self)
Returns the total recording time of the measurement, in float seconds.
rms(self, channels=None, substract_average=False)
Returns the root mean square values for each channel.
periodicCPS(self, N, channels=None, **kwargs)
Compute Cross-Spectral Density based on periodic excitation.
rawData(self, channels=None, **kwargs)
Returns the raw data as stored in the measurement file, without any transformations applied.
sensitivity(self)
Sensitivity of the data in U^-1, from floating point data scaled between -1.0 and 1....
__init__(self, fn)
Initialize a Measurement object based on the filename.
timestr(self)
Return a properly formatted string of the measurement time, in order of.
Measurement fromnpy(data, samplerate, sensitivity, mfn, timestamp=None, List[SIQtys] qtys=None, List[str] channelNames=None, force=False)
Creates a LASP measurement file from input numpy array data.
CPS(self, channels=None, **kwargs)
Compute single-sided Cross-Power-Spectrum of the measurement channels.
comment(self)
Return the measurement comment.
time(self)
Returns the measurement time in seconds since the epoch.
data(self, channels=None, **kwargs)
Returns the measurement data, scaled and sensitivity applied.
setAttribute(self, atrname, value)
Set an attribute in the measurement file, and keep a local copy in memory for efficient accessing.
fromWaveFile(fn, newfn=None, force=False, timestamp=None)
Convert a measurement file to a wave file, and return the measurement handle.
iterData(self, channels, **kwargs)
getSampWidth(dtype)
Returns the width of a single sample in bytes.
scaleBlockSens(block, sens)
Scale a block of raw data to return raw acoustic pressure data.