acoular 24.10__py3-none-any.whl → 25.1__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.
acoular/process.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # ------------------------------------------------------------------------------
2
2
  # Copyright (c) Acoular Development Team.
3
3
  # ------------------------------------------------------------------------------
4
- """Implements general purpose blockwise processing methods independent of the domain (time or frequency).
4
+ """General purpose blockwise processing methods independent of the domain (time or frequency).
5
5
 
6
6
  .. autosummary::
7
7
  :toctree: generated/
@@ -11,6 +11,7 @@
11
11
  SampleSplitter
12
12
  TimeAverage
13
13
  TimeCache
14
+ SamplesBuffer
14
15
  """
15
16
 
16
17
  import threading
@@ -18,13 +19,17 @@ from collections import deque
18
19
  from inspect import currentframe
19
20
  from warnings import warn
20
21
 
21
- from traits.api import Bool, Dict, Instance, Int, Property, Trait, cached_property, on_trait_change
22
+ import numpy as np
23
+ from traits.api import Any, Array, Bool, Dict, Enum, Instance, Int, Property, Union, cached_property, on_trait_change
22
24
 
25
+ # acoular imports
23
26
  from .base import Generator, InOut
24
27
  from .configuration import config
28
+ from .deprecation import deprecated_alias
25
29
  from .h5cache import H5cache
26
30
  from .h5files import H5CacheFileBase
27
31
  from .internal import digest
32
+ from .tools.utils import find_basename
28
33
 
29
34
 
30
35
  class LockedGenerator:
@@ -42,40 +47,45 @@ class LockedGenerator:
42
47
  return self.it.__next__()
43
48
 
44
49
 
50
+ @deprecated_alias({'naverage': 'num_per_average', 'numsamples': 'num_samples'}, read_only=['numsamples'])
45
51
  class Average(InOut):
46
52
  """Calculates the average across consecutive time samples or frequency snapshots.
47
53
 
48
54
  The average operation is performed differently depending on the source type.
49
- If the source is a time domain source (e.g. derived from :class:`~acoular.base.SamplesGenerator`),
50
- the average is calculated over a certain number of time samples given by :attr:`naverage`.
51
- If the source is a frequency domain source (e.g. derived from :class:`~acoular.base.SpectraGenerator`),
52
- the average is calculated over a certain number of snapshots given by :attr:`naverage`.
55
+ If the source is a time domain source
56
+ (e.g. derived from :class:`~acoular.base.SamplesGenerator`), the average is
57
+ calculated over a certain number of time samples given by :attr:`num_per_average`.
58
+ If the source is a frequency domain source (e.g. derived from
59
+ :class:`~acoular.base.SpectraGenerator`), the average is calculated over a certain
60
+ number of snapshots given by :attr:`num_per_average`.
53
61
 
54
62
  Examples
55
63
  --------
56
- For estimate the RMS of a white noise (time-domain) signal, the average of the squared signal can be calculated:
64
+ For estimate the RMS of a white noise (time-domain) signal, the average of the squared
65
+ signal can be calculated:
57
66
 
58
67
  >>> import acoular as ac
59
68
  >>> import numpy as np
60
69
  >>>
61
- >>> signal = ac.WNoiseGenerator(sample_freq=51200, numsamples=51200, rms=2.0).signal()
70
+ >>> signal = ac.WNoiseGenerator(sample_freq=51200, num_samples=51200, rms=2.0).signal()
62
71
  >>> ts = ac.TimeSamples(data=signal[:, np.newaxis], sample_freq=51200)
63
72
  >>> tp = ac.TimePower(source=ts)
64
- >>> avg = ac.Average(source=tp, naverage=512)
73
+ >>> avg = ac.Average(source=tp, num_per_average=512)
65
74
  >>> mean_squared_value = next(avg.result(num=1))
66
75
  >>> rms = np.sqrt(mean_squared_value)[0, 0]
67
76
  >>> print(rms)
68
77
  1.9985200025816718
69
78
 
70
- Here, each evaluation of the generator created by the :meth:`result` method of the :class:`Average` object
71
- via the :meth:`next` function returns :code:`num=1` average across a snapshot of 512 samples.
79
+ Here, each evaluation of the generator created by the :meth:`result` method of the
80
+ :class:`Average` object via the :meth:`next` function returns :code:`num=1` average across a
81
+ snapshot of 512 samples.
72
82
 
73
- If the source is a frequency domain source, the average is calculated over a certain number of
74
- snapshots, defined by :attr:`naverage`.
83
+ If the source is a frequency domain source, the average is calculated over a certain number
84
+ of snapshots, defined by :attr:`num_per_average`.
75
85
 
76
86
  >>> fft = ac.RFFT(source=ts, block_size=64)
77
87
  >>> ps = ac.AutoPowerSpectra(source=fft)
78
- >>> avg = ac.Average(source=ps, naverage=16)
88
+ >>> avg = ac.Average(source=ps, num_per_average=16)
79
89
  >>> mean_power = next(avg.result(num=1))
80
90
  >>> print(np.sqrt(mean_power.sum()))
81
91
  2.0024960894399295
@@ -87,17 +97,17 @@ class Average(InOut):
87
97
 
88
98
  #: Number of samples (time domain source) or snapshots (frequency domain source)
89
99
  #: to average over, defaults to 64.
90
- naverage = Int(64, desc='number of samples to average over')
100
+ num_per_average = Int(64, desc='number of samples/snapshots to average over')
91
101
 
92
102
  #: Sampling frequency of the output signal, is set automatically.
93
- sample_freq = Property(depends_on='source.sample_freq, naverage')
103
+ sample_freq = Property(depends_on=['source.sample_freq', 'num_per_average'])
94
104
 
95
105
  #: Number of samples (time domain) or snapshots (frequency domain) of the output signal.
96
106
  #: Is set automatically.
97
- numsamples = Property(depends_on='source.numsamples, naverage')
107
+ num_samples = Property(depends_on=['source.num_samples', 'num_per_average'])
98
108
 
99
109
  # internal identifier
100
- digest = Property(depends_on=['source.digest', '__class__', 'naverage'])
110
+ digest = Property(depends_on=['source.digest', 'num_per_average'])
101
111
 
102
112
  @cached_property
103
113
  def _get_digest(self):
@@ -106,13 +116,13 @@ class Average(InOut):
106
116
  @cached_property
107
117
  def _get_sample_freq(self):
108
118
  if self.source:
109
- return 1.0 * self.source.sample_freq / self.naverage
119
+ return 1.0 * self.source.sample_freq / self.num_per_average
110
120
  return None
111
121
 
112
122
  @cached_property
113
- def _get_numsamples(self):
123
+ def _get_num_samples(self):
114
124
  if self.source:
115
- return self.source.numsamples / self.naverage
125
+ return self.source.num_samples / self.num_per_average
116
126
  return None
117
127
 
118
128
  def result(self, num):
@@ -127,11 +137,11 @@ class Average(InOut):
127
137
  Returns
128
138
  -------
129
139
  Average of the output of source.
130
- Yields samples in blocks of shape (num, numchannels).
140
+ Yields samples in blocks of shape (num, num_channels).
131
141
  The last block may be shorter than num.
132
142
 
133
143
  """
134
- nav = self.naverage
144
+ nav = self.num_per_average
135
145
  for temp in self.source.result(num * nav):
136
146
  ns, nc = temp.shape
137
147
  nso = int(ns / nav)
@@ -158,7 +168,7 @@ class Cache(InOut):
158
168
  >>> cache = ac.Cache(source=fft) # cache the output of the FFT in cache file
159
169
  >>> for block in cache.result(num=1): # read the cached data block-wise
160
170
  ... print(block.shape)
161
- [('_cache.h5', 1)]
171
+ [('void_cache.h5', 1)]
162
172
  (1, 513)
163
173
 
164
174
  The caching behaviour can be controlled by the :class:`~acoular.configuration.Config` instance
@@ -172,13 +182,13 @@ class Cache(InOut):
172
182
  """
173
183
 
174
184
  # basename for cache
175
- basename = Property(depends_on='digest')
185
+ basename = Property(depends_on=['digest'])
176
186
 
177
187
  # hdf5 cache file
178
188
  h5f = Instance(H5CacheFileBase, transient=True)
179
189
 
180
190
  # internal identifier
181
- digest = Property(depends_on=['source.digest', '__class__'])
191
+ digest = Property(depends_on=['source.digest'])
182
192
 
183
193
  @cached_property
184
194
  def _get_digest(self):
@@ -186,17 +196,7 @@ class Cache(InOut):
186
196
 
187
197
  @cached_property
188
198
  def _get_basename(self):
189
- obj = self.source # start with source
190
- basename = 'void' # if no file source is found
191
- while obj:
192
- if 'basename' in obj.all_trait_names(): # at original source?
193
- basename = obj.basename # get the name
194
- break
195
- try:
196
- obj = obj.source # traverse down until original data source
197
- except AttributeError:
198
- obj = None
199
- return basename
199
+ return find_basename(self.source)
200
200
 
201
201
  def _pass_data(self, num):
202
202
  yield from self.source.result(num)
@@ -256,10 +256,10 @@ class Cache(InOut):
256
256
 
257
257
  Returns
258
258
  -------
259
- Samples in blocks of shape (num, numchannels).
259
+ Samples in blocks of shape (num, num_channels).
260
260
  The last block may be shorter than num.
261
261
  Echos the source output, but reads it from cache
262
- when available and prevents unnecassary recalculation.
262
+ when available and prevents unnecessary recalculation.
263
263
 
264
264
  """
265
265
  if config.global_caching == 'none':
@@ -282,8 +282,8 @@ class Cache(InOut):
282
282
  elif not self.h5f.get_data_by_reference(nodename).attrs['complete']:
283
283
  if config.global_caching == 'readonly':
284
284
  warn(
285
- "Cache file is incomplete for nodename %s. With config.global_caching='readonly', the cache file will not be used!"
286
- % str(nodename),
285
+ f"Cache file is incomplete for nodename {nodename}. With config.global_caching='readonly', \
286
+ the cache file will not be used!",
287
287
  Warning,
288
288
  stacklevel=1,
289
289
  )
@@ -298,9 +298,74 @@ class Cache(InOut):
298
298
 
299
299
 
300
300
  class SampleSplitter(InOut):
301
- """Distributes data blocks from source to several following objects.
302
- A separate block buffer is created for each registered object in
303
- (:attr:`block_buffer`) .
301
+ """
302
+ Distributes data from a source to several following objects in a block-wise manner.
303
+
304
+ The `SampleSplitter` class is designed to take data from a single
305
+ :class:`~acoular.base.Generator` derived source object and distribute it to multiple
306
+ :class:`~acoular.base.Generator` derived objects. For each object, the :class:`SampleSplitter`
307
+ holds a virtual block buffer from which the subsequently connected objects receive data in a
308
+ first-in-first-out (FIFO) manner. This allows for efficient data handling and processing in
309
+ parallel.
310
+
311
+ Examples
312
+ --------
313
+ Consider a time domain source signal stream from which the FFT spectra and the signal power
314
+ are calculated block-wise and in parallel by using the :class:`~acoular.fprocess.RFFT` as well
315
+ as the :class:`~acoular.tprocess.TimePower` and :class:`~acoular.process.Average`
316
+ objects. The `SampleSplitter` object is used to distribute the incoming blocks of data to the
317
+ `RFFT` and `TimePower` object buffers whenever one of these objects calls the :meth:`result`
318
+ generator.
319
+ For the `TimePower` object, the buffer size is set to 10 blocks. If the buffer is full, an error
320
+ is raised since the buffer overflow treatment is set to 'error'. For the `RFFT` object, the
321
+ block buffer size is set to 1 block, and the buffer overflow treatment is set to 'none'. This
322
+ is done to reduce latency in the FFT calculation, as the FFT calculation may take longer than
323
+ the signal power calculation. If new data is available and the block buffer for the `RFFT`
324
+ object is full, the `SampleSplitter` will drop the oldest block of data in the buffer. Thus, the
325
+ `RFFT` object will always receive the most recent block of data.
326
+
327
+ >>> import acoular as ac
328
+ >>> import numpy as np
329
+ >>>
330
+ >>> # create a time domain signal source
331
+ >>> ts = ac.TimeSamples(data=np.random.rand(1024, 1), sample_freq=51200)
332
+ >>>
333
+ >>> # create the sample splitter object
334
+ >>> ss = ac.SampleSplitter(source=ts)
335
+ >>>
336
+ >>> # create the FFT spectra and further objects that receive the data
337
+ >>> fft = ac.RFFT(source=ss, block_size=64)
338
+ >>> pow = ac.TimePower(source=ss)
339
+ >>> avg = ac.Average(source=pow, num_per_average=64)
340
+ >>>
341
+ >>> # register the subsequent processing block objects at the sample splitter
342
+ >>> ss.register_object(fft, buffer_size=1, buffer_overflow_treatment='none')
343
+ >>> ss.register_object(pow, buffer_size=10, buffer_overflow_treatment='error')
344
+
345
+ After object registration, the `SampleSplitter` object is ready to distribute the data to the
346
+ object buffers. The block buffers can be accessed via the `block_buffer` attribute of the
347
+ `SampleSplitter` object.
348
+
349
+ >>> ss.block_buffer.values()
350
+ dict_values([deque([], maxlen=1), deque([], maxlen=10)])
351
+
352
+ Calling the result method of the FFT object will start the data collection and distribution
353
+ process.
354
+
355
+ >>> generator = fft.result(num=1)
356
+ >>> fft_res = next(generator)
357
+
358
+ Although we haven't called the result method of the signal power object, one data block is
359
+ already available in the buffer.
360
+
361
+ >>> print(len(ss.block_buffer[pow]))
362
+ 1
363
+
364
+ To remove registered objects from the `SampleSplitter`, use the :meth:`remove_object` method.
365
+
366
+ >>> ss.remove_object(pow)
367
+ >>> print(len(ss.block_buffer))
368
+ 1
304
369
  """
305
370
 
306
371
  #: dictionary with block buffers (dict values) of registered objects (dict
@@ -308,7 +373,13 @@ class SampleSplitter(InOut):
308
373
  block_buffer = Dict(key_trait=Instance(Generator))
309
374
 
310
375
  #: max elements/blocks in block buffers.
311
- buffer_size = Int(100)
376
+ #: Can be set individually for each registered object.
377
+ #: Default is 100 blocks for each registered object.
378
+ buffer_size = Union(
379
+ Int,
380
+ Dict(key_trait=Instance(Generator), value_trait=Int),
381
+ default_value=100,
382
+ )
312
383
 
313
384
  #: defines behaviour in case of block_buffer overflow. Can be set individually
314
385
  #: for each registered object.
@@ -318,7 +389,7 @@ class SampleSplitter(InOut):
318
389
  #: * 'none': nothing happens. Possibly leads to lost blocks of data
319
390
  buffer_overflow_treatment = Dict(
320
391
  key_trait=Instance(Generator),
321
- value_trait=Trait('error', 'warning', 'none'),
392
+ value_trait=Enum('error', 'warning', 'none'),
322
393
  desc='defines buffer overflow behaviour.',
323
394
  )
324
395
 
@@ -330,13 +401,17 @@ class SampleSplitter(InOut):
330
401
  _buffer_overflow = Bool(False)
331
402
 
332
403
  # Helper Trait holds source generator
333
- _source_generator = Trait()
404
+ _source_generator = Instance(LockedGenerator)
334
405
 
335
- def _create_block_buffer(self, obj):
336
- self.block_buffer[obj] = deque([], maxlen=self.buffer_size)
406
+ def _create_block_buffer(self, obj, buffer_size=None):
407
+ if buffer_size is None:
408
+ buffer_size = self.buffer_size if isinstance(self.buffer_size, int) else self.buffer_size[obj]
409
+ self.block_buffer[obj] = deque([], maxlen=buffer_size)
337
410
 
338
- def _create_buffer_overflow_treatment(self, obj):
339
- self.buffer_overflow_treatment[obj] = 'error'
411
+ def _create_buffer_overflow_treatment(self, obj, buffer_overflow_treatment=None):
412
+ if buffer_overflow_treatment is None:
413
+ buffer_overflow_treatment = 'error'
414
+ self.buffer_overflow_treatment[obj] = buffer_overflow_treatment
340
415
 
341
416
  def _clear_block_buffer(self, obj):
342
417
  self.block_buffer[obj].clear()
@@ -349,7 +424,8 @@ class SampleSplitter(InOut):
349
424
 
350
425
  def _assert_obj_registered(self, obj):
351
426
  if obj not in self.block_buffer:
352
- raise OSError('calling object %s is not registered.' % obj)
427
+ msg = f'calling object {obj} is not registered.'
428
+ raise OSError(msg)
353
429
 
354
430
  def _get_objs_to_inspect(self):
355
431
  return [obj for obj in self.buffer_overflow_treatment if self.buffer_overflow_treatment[obj] != 'none']
@@ -360,7 +436,7 @@ class SampleSplitter(InOut):
360
436
  if self.buffer_overflow_treatment[obj] == 'error':
361
437
  self._buffer_overflow = True
362
438
  elif self.buffer_overflow_treatment[obj] == 'warning':
363
- warn('overfilled buffer for object: %s data will get lost' % obj, UserWarning, stacklevel=1)
439
+ warn(f'overfilled buffer for object: {obj} data will get lost', UserWarning, stacklevel=1)
364
440
 
365
441
  def _create_source_generator(self, num):
366
442
  for obj in self.block_buffer:
@@ -379,15 +455,45 @@ class SampleSplitter(InOut):
379
455
  self._remove_block_buffer(obj)
380
456
  self._create_block_buffer(obj)
381
457
 
382
- def register_object(self, *objects_to_register):
383
- """Function that can be used to register objects that receive blocks from this class."""
458
+ def register_object(self, *objects_to_register, buffer_size=None, buffer_overflow_treatment=None):
459
+ """Register one or multiple :class:`~acoular.base.Generator` objects to the SampleSplitter.
460
+
461
+ Creates a block buffer for each object and sets the buffer size and buffer
462
+ overflow treatment.
463
+
464
+ Parameters
465
+ ----------
466
+ objects_to_register : Generator
467
+ One or multiple :class:`~acoular.base.Generator` derived objects to be registered.
468
+ buffer_size : int, optional
469
+ Maximum number of elements/blocks in block buffer. If not set, the default buffer size
470
+ of 100 blocks is used.
471
+ buffer_overflow_treatment : str, optional
472
+ Defines the behaviour in case of reaching the buffer size.
473
+ Can be set individually for each object. Possible values are 'error', 'warning', and
474
+ 'none'. If not set, the default value is 'error'.
475
+ """
384
476
  for obj in objects_to_register:
385
477
  if obj not in self.block_buffer:
386
- self._create_block_buffer(obj)
387
- self._create_buffer_overflow_treatment(obj)
478
+ self._create_block_buffer(obj, buffer_size)
479
+ self._create_buffer_overflow_treatment(obj, buffer_overflow_treatment)
480
+ else:
481
+ msg = f'object {obj} is already registered.'
482
+ raise OSError(msg)
388
483
 
389
484
  def remove_object(self, *objects_to_remove):
390
- """Function that can be used to remove registered objects."""
485
+ """Function that can be used to remove registered objects.
486
+
487
+ If no objects are given, all registered objects are removed.
488
+
489
+ Parameters
490
+ ----------
491
+ objects_to_remove : list
492
+ One or multiple :class:`~acoular.base.Generator` derived objects to be removed.
493
+ If not set, all registered objects are removed.
494
+ """
495
+ if not objects_to_remove:
496
+ objects_to_remove = list(self.block_buffer.keys())
391
497
  for obj in objects_to_remove:
392
498
  self._remove_block_buffer(obj)
393
499
  self._remove_buffer_overflow_treatment(obj)
@@ -403,7 +509,7 @@ class SampleSplitter(InOut):
403
509
 
404
510
  Returns
405
511
  -------
406
- Samples in blocks of shape (num, numchannels).
512
+ Samples in blocks of shape (num, num_channels).
407
513
  Delivers a block of samples to the calling object.
408
514
  The last block may be shorter than num.
409
515
 
@@ -462,3 +568,204 @@ class TimeCache(Cache):
462
568
  DeprecationWarning,
463
569
  stacklevel=2,
464
570
  )
571
+
572
+
573
+ class SamplesBuffer(InOut):
574
+ """Handles buffering of samples from a source.
575
+
576
+ This class is used to buffer samples from a source and provide them in blocks
577
+ of a specified size. There are several usecases for this class, as demonstrated in
578
+ the following.
579
+
580
+ Examples
581
+ --------
582
+ Let us assume we want to draw blocks of 16 samples from our source, but we want to make sure
583
+ that we always have twice the number of samples in the buffer. We can achieve this simple
584
+ behaviour by using the following code:
585
+
586
+ >>> import acoular as ac
587
+ >>> import numpy as np
588
+ >>> # create a white noise source with 512 samples
589
+ >>> source = ac.TimeSamples(
590
+ ... data=ac.WNoiseGenerator(
591
+ ... sample_freq=64,
592
+ ... num_samples=512,
593
+ ... ).signal()[:, np.newaxis],
594
+ ... sample_freq=64,
595
+ ... )
596
+ >>> # create a buffer with a size of 32 samples
597
+ >>> buffer = ac.process.SamplesBuffer(source=source, length=32)
598
+ >>> # get the first block of 16 samples
599
+ >>> block = next(buffer.result(num=16))
600
+ >>> np.testing.assert_array_equal(block, source.data[:16])
601
+
602
+ Here, on the first call to the result method, the buffer will fill up by collecting blocks with
603
+ same size from the source. The buffer will then return the first block of 16 samples. On the
604
+ next call to the result method, the buffer will be filled again and returns the next block of 16
605
+ samples.
606
+
607
+ In some cases, we might want to draw a different number of samples from the source than we want
608
+ to return. This can be achieved by setting the `source_num` trait of the buffer. A special case
609
+ is the return of a variable number of samples. This is the case, for example, in the class
610
+ :class:`~acoular.tbeamform.BeamformerTimeTraj`, in which a different number of time samples is
611
+ required from the buffer for further delay-and-sum processing depending on the expected delay,
612
+ which can be vary for moving sources. At the same time, however, only 'num' samples should be
613
+ written to and removed from the buffer. This behavior can be achieved by setting the
614
+ `shift_index_by` trait to 'num' and by setting the `result_num` trait to the number of samples
615
+ that should be returned by the result function.
616
+
617
+ >>> buffer = ac.process.SamplesBuffer(source=source, length=32, result_num=20, shift_index_by='num')
618
+ >>> block_sizes = []
619
+ >>> block_sizes.append(
620
+ ... next(buffer.result(num=16)).shape[0]
621
+ ... ) # this time, the buffer will return 20 samples, but the buffer will only forget the first 16 samples
622
+ >>> buffer.result_num = 24
623
+ >>> block_sizes.append(
624
+ ... next(buffer.result(num=16)).shape[0]
625
+ ... ) # this time, the buffer will return 24 samples, but the buffer will only forget the first 16 samples
626
+ >>> np.testing.assert_array_equal(block_sizes, [20, 24])
627
+ """ # noqa: W505
628
+
629
+ #: number of samples that fit in the buffer
630
+ length = Int(desc='number of samples that fit in the buffer')
631
+
632
+ #: number of samples per block to obtain from the source. If 'None', use 'num' argument of
633
+ #: result method
634
+ source_num = Union(
635
+ None,
636
+ Int(),
637
+ default_value=None,
638
+ desc='number of samples to return from the source. If "None", use "num" argument of result method',
639
+ )
640
+
641
+ #: number of samples to return from the buffer. If 'None', use 'num' argument of result method
642
+ result_num = Union(
643
+ None,
644
+ Int(),
645
+ default_value=None,
646
+ desc="number of samples to return from the buffer. If 'None', use 'num' argument of result method",
647
+ )
648
+
649
+ #: index shift value for the buffer. If "result_num", buffer will return and forget 'result_num'
650
+ #: samples. If "num", buffer will return 'result_num' samples but will forget 'num' samples
651
+ shift_index_by = Enum(
652
+ ('result_num', 'num'),
653
+ desc=(
654
+ 'index shift value for the buffer. If "result_num", use "result_num" trait.'
655
+ ' If "num", use "num" argument of result method'
656
+ ),
657
+ )
658
+
659
+ #: current filling level of buffer
660
+ level = Property(desc='current filling level of buffer')
661
+
662
+ #: data type of the buffer elements
663
+ dtype = Any(desc='data type of the buffer')
664
+
665
+ # flag to indicate that the source is empty, for internal use
666
+ _empty_source = Bool(False, desc='flag to indicate that the source is empty')
667
+
668
+ # the buffer for processing
669
+ _buffer = Array(shape=(None, None), desc='buffer for block processing')
670
+
671
+ # current index in buffer
672
+ _index = Int(desc='current index in buffer')
673
+
674
+ def _get_level(self):
675
+ return self._buffer.shape[0] - self._index
676
+
677
+ def _create_new_buffer(self):
678
+ self._buffer = np.zeros((self.length, self.num_channels), dtype=self.dtype)
679
+ self._index = self.length
680
+ self._empty_source = False
681
+
682
+ def _write_to_buffer(self, data):
683
+ ns = data.shape[0]
684
+ self._buffer[0 : (self.length - ns)] = self._buffer[-(self.length - ns) :]
685
+ self._buffer[-ns:, :] = data.astype(self.dtype)
686
+ self._index -= ns
687
+
688
+ def increase_buffer(self, num):
689
+ """Increase the buffer by 'num' samples.
690
+
691
+ Returns
692
+ -------
693
+ None
694
+ """
695
+ ar = np.zeros((num, self.num_channels), dtype=self._buffer.dtype)
696
+ self._buffer = np.concatenate((ar, self._buffer), axis=0)
697
+ self._index += num
698
+ self.length += num
699
+
700
+ def read_from_buffer(self, num):
701
+ """Read samples from the buffer.
702
+
703
+ Parameters
704
+ ----------
705
+ num : int
706
+ number of samples to read from the buffer.
707
+
708
+ Returns
709
+ -------
710
+ numpy.ndarray
711
+ block of samples from the buffer
712
+
713
+ """
714
+ rnum = num if self.result_num is None else self.result_num
715
+ rnum = rnum if self.level >= rnum else self.level
716
+ data = self._buffer[self._index : self._index + rnum]
717
+ if self.shift_index_by == 'result_num':
718
+ self._index += rnum
719
+ else:
720
+ self._index += num
721
+ return data
722
+
723
+ def fill_buffer(self, snum):
724
+ """Fill the buffer with samples from the source.
725
+
726
+ Parameters
727
+ ----------
728
+ snum : int
729
+ number of samples to return from the source.
730
+
731
+ Yields
732
+ ------
733
+ None
734
+ """
735
+ source_generator = self.source.result(snum)
736
+ while not self._empty_source:
737
+ while self._index >= snum:
738
+ if self.result_num is not None:
739
+ while self.result_num > self.length:
740
+ self.increase_buffer(snum)
741
+ try:
742
+ self._write_to_buffer(next(source_generator))
743
+ except StopIteration:
744
+ self._empty_source = True
745
+ break
746
+ yield
747
+
748
+ def result(self, num):
749
+ """Return blocks of samples from the buffer.
750
+
751
+ Parameters
752
+ ----------
753
+ num : int
754
+ number of samples to return.
755
+
756
+ Yields
757
+ ------
758
+ numpy.ndarray
759
+ block of samples from the buffer
760
+ """
761
+ self._create_new_buffer()
762
+ snum = num
763
+ if self.source_num is not None:
764
+ snum = self.source_num
765
+ for _ in self.fill_buffer(snum):
766
+ if self.level > 0:
767
+ yield self.read_from_buffer(num)
768
+ else:
769
+ break
770
+ while self.level > 0:
771
+ yield self.read_from_buffer(num)