Repository: acoular/acoular

Issue:
BUG: Calib misses basename attribute
### Version Dependencies

affects master

### Bug Description

When `Calib` became an `InOut`, the basename attribute (property) was not implemented. As a consequence all cache files are now named after the Calib instance instead of being named after the origin of the data (i.e. the generator or h5 file)

### Reproducible Code

```python

```

### Error message:

```shell

```

### Issue Severity

High: It significantly affects my work, and I cannot find a workaround.

Discussion:

Patch:
diff --git a/.ruff.toml b/.ruff.toml
index 7407f9d..f4ce04d 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -70,6 +70,7 @@ docstring-code-line-length = "dynamic"
 "acoular/__init__.py" = ["F401"] 
 "acoular/demo/__init__.py" = ["F401"] 
 "acoular/tools/__init__.py" = ["F401"] 
+"acoular/aiaa/__init__.py" = ["F401"]
 "acoular/fastFuncs.py" = ["N802", "N803","N806","N999"] # allow different naming convention
 "acoular/environments.py" = ["N806"] 
 "acoular/sources.py" = ["N806"] 
diff --git a/acoular/__init__.py b/acoular/__init__.py
index c7c1a0b..9c18b83 100644
--- a/acoular/__init__.py
+++ b/acoular/__init__.py
@@ -9,7 +9,7 @@ import os  # noqa: I001
 # config must be imported before any submodules containing numpy, see #322.
 from .configuration import config
 
-from . import demo, tools
+from . import demo, tools, aiaa
 from .base import (
     Generator,
     InOut,
@@ -95,7 +95,8 @@ from .sources import (
     TimeSamples,
     UncorrelatedNoiseSource,
 )
-from .spectra import BaseSpectra, PowerSpectra, PowerSpectraImport, synthetic
+from .tools.helpers import synthetic
+from .spectra import BaseSpectra, PowerSpectra, PowerSpectraImport
 from .spectra import PowerSpectra as EigSpectra
 from .tbeamform import (
     BeamformerCleant,
diff --git a/acoular/aiaa/__init__.py b/acoular/aiaa/__init__.py
new file mode 100644
index 0000000..d3b8b36
--- /dev/null
+++ b/acoular/aiaa/__init__.py
@@ -0,0 +1,12 @@
+# ------------------------------------------------------------------------------
+# Copyright (c) Acoular Development Team.
+# ------------------------------------------------------------------------------
+"""Provides classes for importing AIAA Array Benchmarks.
+
+.. autosummary::
+    :toctree: generated/
+
+    aiaa
+"""
+
+from .aiaa import CsmAIAABenchmark, MicAIAABenchmark, TimeSamplesAIAABenchmark, TriggerAIAABenchmark
diff --git a/acoular/tools/aiaa.py b/acoular/aiaa/aiaa.py
similarity index 97%
rename from acoular/tools/aiaa.py
rename to acoular/aiaa/aiaa.py
index f7c4d47..26b76b5 100644
--- a/acoular/tools/aiaa.py
+++ b/acoular/aiaa/aiaa.py
@@ -1,8 +1,8 @@
 # ------------------------------------------------------------------------------
 # Copyright (c) Acoular Development Team.
 # ------------------------------------------------------------------------------
-"""Classes for importing AIAA Array Benchmarks
-from . import aiaa
+"""Classes for importing AIAA Array Benchmarks.
+
 These classes allow importing data from HDF5 files following the specifications of
 the AIAA microphone array methods benchmarking effort:
 https://www-docs.b-tu.de/fg-akustik/public/veroeffentlichungen/ArrayMethodsFileFormatsR2P4Release.pdf .
@@ -26,7 +26,6 @@ Examples
 """  # noqa: W505
 
 import contextlib
-from os import path
 
 from numpy import array
 from traits.api import (
@@ -44,6 +43,7 @@ from acoular.internal import digest
 from acoular.microphones import MicGeom
 from acoular.sources import TimeSamples
 from acoular.spectra import PowerSpectraImport
+from acoular.tools.utils import get_file_basename
 
 
 class TimeSamplesAIAABenchmark(TimeSamples):
@@ -112,7 +112,7 @@ class CsmAIAABenchmark(PowerSpectraImport):
 
     @cached_property
     def _get_basename(self):
-        return path.splitext(path.basename(self.file))[0]
+        return get_file_basename(self.file)
 
     @on_trait_change('basename')
     def load_data(self):
diff --git a/acoular/fprocess.py b/acoular/fprocess.py
index d9f72a4..2855fa4 100644
--- a/acoular/fprocess.py
+++ b/acoular/fprocess.py
@@ -24,8 +24,8 @@ from .base import SamplesGenerator, SpectraGenerator, SpectraOut, TimeOut
 from .deprecation import deprecated_alias
 from .fastFuncs import calcCSM
 from .internal import digest
+from .process import SamplesBuffer
 from .spectra import BaseSpectra
-from .tools.utils import SamplesBuffer
 
 
 @deprecated_alias({'numfreqs': 'num_freqs', 'numsamples': 'num_samples'}, read_only=True)
diff --git a/acoular/process.py b/acoular/process.py
index 645be82..fed5aa6 100644
--- a/acoular/process.py
+++ b/acoular/process.py
@@ -11,6 +11,7 @@
     SampleSplitter
     TimeAverage
     TimeCache
+    SamplesBuffer
 """
 
 import threading
@@ -18,7 +19,8 @@ from collections import deque
 from inspect import currentframe
 from warnings import warn
 
-from traits.api import Bool, Dict, Enum, Instance, Int, Property, Union, cached_property, on_trait_change
+import numpy as np
+from traits.api import Any, Array, Bool, Dict, Enum, Instance, Int, Property, Union, cached_property, on_trait_change
 
 # acoular imports
 from .base import Generator, InOut
@@ -27,6 +29,7 @@ from .deprecation import deprecated_alias
 from .h5cache import H5cache
 from .h5files import H5CacheFileBase
 from .internal import digest
+from .tools.utils import find_basename
 
 
 class LockedGenerator:
@@ -165,7 +168,7 @@ class Cache(InOut):
     >>> cache = ac.Cache(source=fft)  # cache the output of the FFT in cache file
     >>> for block in cache.result(num=1):  # read the cached data block-wise
     ...     print(block.shape)
-    [('_cache.h5', 1)]
+    [('void_cache.h5', 1)]
     (1, 513)
 
     The caching behaviour can be controlled by the :class:`~acoular.configuration.Config` instance
@@ -193,17 +196,7 @@ class Cache(InOut):
 
     @cached_property
     def _get_basename(self):
-        obj = self.source  # start with source
-        basename = 'void'  # if no file source is found
-        while obj:
-            if 'basename' in obj.all_trait_names():  # at original source?
-                basename = obj.basename  # get the name
-                break
-            try:
-                obj = obj.source  # traverse down until original data source
-            except AttributeError:
-                obj = None
-        return basename
+        return find_basename(self.source)
 
     def _pass_data(self, num):
         yield from self.source.result(num)
@@ -575,3 +568,204 @@ class TimeCache(Cache):
             DeprecationWarning,
             stacklevel=2,
         )
+
+
+class SamplesBuffer(InOut):
+    """Handles buffering of samples from a source.
+
+    This class is used to buffer samples from a source and provide them in blocks
+    of a specified size. There are several usecases for this class, as demonstrated in
+    the following.
+
+    Examples
+    --------
+    Let us assume we want to draw blocks of 16 samples from our source, but we want to make sure
+    that we always have twice the number of samples in the buffer. We can achieve this simple
+    behaviour by using the following code:
+
+    >>> import acoular as ac
+    >>> import numpy as np
+    >>> # create a white noise source with 512 samples
+    >>> source = ac.TimeSamples(
+    ...     data=ac.WNoiseGenerator(
+    ...         sample_freq=64,
+    ...         num_samples=512,
+    ...     ).signal()[:, np.newaxis],
+    ...     sample_freq=64,
+    ... )
+    >>> # create a buffer with a size of 32 samples
+    >>> buffer = ac.process.SamplesBuffer(source=source, length=32)
+    >>> # get the first block of 16 samples
+    >>> block = next(buffer.result(num=16))
+    >>> np.testing.assert_array_equal(block, source.data[:16])
+
+    Here, on the first call to the result method, the buffer will fill up by collecting blocks with
+    same size from the source. The buffer will then return the first block of 16 samples. On the
+    next call to the result method, the buffer will be filled again and returns the next block of 16
+    samples.
+
+    In some cases, we might want to draw a different number of samples from the source than we want
+    to return. This can be achieved by setting the `source_num` trait of the buffer. A special case
+    is the return of a variable number of samples. This is the case, for example, in the class
+    :class:`~acoular.tbeamform.BeamformerTimeTraj`, in which a different number of time samples is
+    required from the buffer for further delay-and-sum processing depending on the expected delay,
+    which can be vary for moving sources. At the same time, however, only 'num' samples should be
+    written to and removed from the buffer. This behavior can be achieved by setting the
+    `shift_index_by` trait to 'num' and by setting the `result_num` trait to the number of samples
+    that should be returned by the result function.
+
+    >>> buffer = ac.process.SamplesBuffer(source=source, length=32, result_num=20, shift_index_by='num')
+    >>> block_sizes = []
+    >>> block_sizes.append(
+    ...     next(buffer.result(num=16)).shape[0]
+    ... )  # this time, the buffer will return 20 samples, but the buffer will only forget the first 16 samples
+    >>> buffer.result_num = 24
+    >>> block_sizes.append(
+    ...     next(buffer.result(num=16)).shape[0]
+    ... )  # this time, the buffer will return 24 samples, but the buffer will only forget the first 16 samples
+    >>> np.testing.assert_array_equal(block_sizes, [20, 24])
+    """  # noqa: W505
+
+    #: number of samples that fit in the buffer
+    length = Int(desc='number of samples that fit in the buffer')
+
+    #: number of samples per block to obtain from the source. If 'None', use 'num' argument of
+    #: result method
+    source_num = Union(
+        None,
+        Int(),
+        default_value=None,
+        desc='number of samples to return from the source. If "None", use "num" argument of result method',
+    )
+
+    #: number of samples to return from the buffer. If 'None', use 'num' argument of result method
+    result_num = Union(
+        None,
+        Int(),
+        default_value=None,
+        desc="number of samples to return from the buffer. If 'None', use 'num' argument of result method",
+    )
+
+    #: index shift value for the buffer. If "result_num", buffer will return and forget 'result_num'
+    #: samples. If "num", buffer will return 'result_num' samples but will forget 'num' samples
+    shift_index_by = Enum(
+        ('result_num', 'num'),
+        desc=(
+            'index shift value for the buffer. If "result_num", use "result_num" trait.'
+            ' If "num", use "num" argument of result method'
+        ),
+    )
+
+    #: current filling level of buffer
+    level = Property(desc='current filling level of buffer')
+
+    #: data type of the buffer elements
+    dtype = Any(desc='data type of the buffer')
+
+    # flag to indicate that the source is empty, for internal use
+    _empty_source = Bool(False, desc='flag to indicate that the source is empty')
+
+    # the buffer for processing
+    _buffer = Array(shape=(None, None), desc='buffer for block processing')
+
+    # current index in buffer
+    _index = Int(desc='current index in buffer')
+
+    def _get_level(self):
+        return self._buffer.shape[0] - self._index
+
+    def _create_new_buffer(self):
+        self._buffer = np.zeros((self.length, self.num_channels), dtype=self.dtype)
+        self._index = self.length
+        self._empty_source = False
+
+    def _write_to_buffer(self, data):
+        ns = data.shape[0]
+        self._buffer[0 : (self.length - ns)] = self._buffer[-(self.length - ns) :]
+        self._buffer[-ns:, :] = data.astype(self.dtype)
+        self._index -= ns
+
+    def increase_buffer(self, num):
+        """Increase the buffer by 'num' samples.
+
+        Returns
+        -------
+        None
+        """
+        ar = np.zeros((num, self.num_channels), dtype=self._buffer.dtype)
+        self._buffer = np.concatenate((ar, self._buffer), axis=0)
+        self._index += num
+        self.length += num
+
+    def read_from_buffer(self, num):
+        """Read samples from the buffer.
+
+        Parameters
+        ----------
+        num : int
+            number of samples to read from the buffer.
+
+        Returns
+        -------
+        numpy.ndarray
+            block of samples from the buffer
+
+        """
+        rnum = num if self.result_num is None else self.result_num
+        rnum = rnum if self.level >= rnum else self.level
+        data = self._buffer[self._index : self._index + rnum]
+        if self.shift_index_by == 'result_num':
+            self._index += rnum
+        else:
+            self._index += num
+        return data
+
+    def fill_buffer(self, snum):
+        """Fill the buffer with samples from the source.
+
+        Parameters
+        ----------
+        snum : int
+            number of samples to return from the source.
+
+        Yields
+        ------
+        None
+        """
+        source_generator = self.source.result(snum)
+        while not self._empty_source:
+            while self._index >= snum:
+                if self.result_num is not None:
+                    while self.result_num > self.length:
+                        self.increase_buffer(snum)
+                try:
+                    self._write_to_buffer(next(source_generator))
+                except StopIteration:
+                    self._empty_source = True
+                    break
+            yield
+
+    def result(self, num):
+        """Return blocks of samples from the buffer.
+
+        Parameters
+        ----------
+        num : int
+            number of samples to return.
+
+        Yields
+        ------
+        numpy.ndarray
+            block of samples from the buffer
+        """
+        self._create_new_buffer()
+        snum = num
+        if self.source_num is not None:
+            snum = self.source_num
+        for _ in self.fill_buffer(snum):
+            if self.level > 0:
+                yield self.read_from_buffer(num)
+            else:
+                break
+        while self.level > 0:
+            yield self.read_from_buffer(num)
diff --git a/acoular/sources.py b/acoular/sources.py
index 629ad93..c9f2637 100644
--- a/acoular/sources.py
+++ b/acoular/sources.py
@@ -85,6 +85,7 @@ from .h5files import H5FileBase, _get_h5file_class
 from .internal import digest, ldigest
 from .microphones import MicGeom
 from .signals import NoiseGenerator, SignalGenerator
+from .tools.utils import get_file_basename
 from .tprocess import TimeConvolve
 from .trajectory import Trajectory
 
@@ -266,7 +267,7 @@ class TimeSamples(SamplesGenerator):
 
     @cached_property
     def _get_basename(self):
-        return path.splitext(path.basename(self.file))[0]
+        return get_file_basename(self.file)
 
     @on_trait_change('basename')
     def _load_data(self):
@@ -422,10 +423,6 @@ class MaskedTimeSamples(TimeSamples):
     def _get_digest(self):
         return digest(self)
 
-    @cached_property
-    def _get_basename(self):
-        return path.splitext(path.basename(self.file))[0]
-
     @cached_property
     def _get_channels(self):
         if len(self.invalid_channels) == 0:
diff --git a/acoular/spectra.py b/acoular/spectra.py
index 033f269..f5c01b3 100644
--- a/acoular/spectra.py
+++ b/acoular/spectra.py
@@ -8,7 +8,6 @@
 
     BaseSpectra
     PowerSpectra
-    synthetic
     PowerSpectraImport
 """
 
@@ -26,7 +25,6 @@ from numpy import (
     hamming,
     hanning,
     imag,
-    isscalar,
     linalg,
     ndarray,
     newaxis,
@@ -35,7 +33,6 @@ from numpy import (
     searchsorted,
     sum,  # noqa A004
     zeros,
-    zeros_like,
 )
 from scipy import fft
 from traits.api import (
@@ -62,6 +59,7 @@ from .fastFuncs import calcCSM
 from .h5cache import H5cache
 from .h5files import H5CacheFileBase
 from .internal import digest
+from .tools.utils import find_basename
 
 
 @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
@@ -308,9 +306,7 @@ class PowerSpectra(BaseSpectra):
 
     @cached_property
     def _get_basename(self):
-        if 'basename' in self.source.all_trait_names():
-            return self.source.basename
-        return self.source.__class__.__name__ + self.source.digest
+        return find_basename(self.source, alternative_basename=self.source.__class__.__name__ + self.source.digest)
 
     def calc_csm(self):
         """Csm calculation."""
@@ -463,101 +459,6 @@ class PowerSpectra(BaseSpectra):
         return sum(self.eva[f1:f2], 0)
 
 
-def synthetic(data, freqs, f, num=3):
-    """Returns synthesized frequency band values of spectral data.
-
-    If used with :meth:`Beamformer.result()<acoular.fbeamform.BeamformerBase.result>`
-    and only one frequency band, the output is identical to the result of the intrinsic
-    :meth:`Beamformer.synthetic<acoular.fbeamform.BeamformerBase.synthetic>` method.
-    It can, however, also be used with the
-    :meth:`Beamformer.integrate<acoular.fbeamform.BeamformerBase.integrate>`
-    output and more frequency bands.
-
-    Parameters
-    ----------
-    data : array of floats
-        The spectral data (squared sound pressures in Pa^2) in an array with one value
-        per frequency line.
-        The number of entries must be identical to the number of
-        grid points.
-    freqs : array of floats
-        The frequencies that correspond to the input *data* (as yielded by
-        the :meth:`PowerSpectra.fftfreq<acoular.spectra.PowerSpectra.fftfreq>`
-        method).
-    f : float or list of floats
-        Band center frequency/frequencies for which to return the results.
-    num : integer
-        Controls the width of the frequency bands considered; defaults to
-        3 (third-octave band).
-
-        ===  =====================
-        num  frequency band width
-        ===  =====================
-        0    single frequency line
-        1    octave band
-        3    third-octave band
-        n    1/n-octave band
-        ===  =====================
-
-    Returns
-    -------
-    array of floats
-        Synthesized frequency band values of the beamforming result at
-        each grid point (the sum of all values that are contained in the band).
-        Note that the frequency resolution and therefore the bandwidth
-        represented by a single frequency line depends on
-        the :attr:`sampling frequency<acoular.base.SamplesGenerator.sample_freq>`
-        and used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
-
-    """
-    if isscalar(f):
-        f = (f,)
-    if num == 0:
-        # single frequency lines
-        res = []
-        for i in f:
-            ind = searchsorted(freqs, i)
-            if ind >= len(freqs):
-                warn(
-                    f'Queried frequency ({i:g} Hz) not in resolved frequency range. Returning zeros.',
-                    Warning,
-                    stacklevel=2,
-                )
-                h = zeros_like(data[0])
-            else:
-                if freqs[ind] != i:
-                    warn(
-                        f'Queried frequency ({i:g} Hz) not in set of '
-                        'discrete FFT sample frequencies. '
-                        f'Using frequency {freqs[ind]:g} Hz instead.',
-                        Warning,
-                        stacklevel=2,
-                    )
-                h = data[ind]
-            res += [h]
-    else:
-        # fractional octave bands
-        res = []
-        for i in f:
-            f1 = i * 2.0 ** (-0.5 / num)
-            f2 = i * 2.0 ** (+0.5 / num)
-            ind1 = searchsorted(freqs, f1)
-            ind2 = searchsorted(freqs, f2)
-            if ind1 == ind2:
-                warn(
-                    f'Queried frequency band ({f1:g} to {f2:g} Hz) does not '
-                    'include any discrete FFT sample frequencies. '
-                    'Returning zeros.',
-                    Warning,
-                    stacklevel=2,
-                )
-                h = zeros_like(data[0])
-            else:
-                h = sum(data[ind1:ind2], 0)
-            res += [h]
-    return array(res)
-
-
 class PowerSpectraImport(PowerSpectra):
     """Provides a dummy class for using pre-calculated cross-spectral
     matrices.
diff --git a/acoular/tbeamform.py b/acoular/tbeamform.py
index e4939ad..34daa16 100644
--- a/acoular/tbeamform.py
+++ b/acoular/tbeamform.py
@@ -51,8 +51,8 @@ from .grids import RectGrid
 
 # acoular imports
 from .internal import digest
+from .process import SamplesBuffer
 from .tfastfuncs import _delayandsum4, _delayandsum5, _delays
-from .tools.utils import SamplesBuffer
 from .trajectory import Trajectory
 
 
diff --git a/acoular/tools/__init__.py b/acoular/tools/__init__.py
index 501abac..25d2296 100644
--- a/acoular/tools/__init__.py
+++ b/acoular/tools/__init__.py
@@ -6,18 +6,11 @@
 .. autosummary::
     :toctree: generated/
 
-    aiaa
     helpers
     metrics
     utils
 """
 
-from .aiaa import (
-    CsmAIAABenchmark,
-    MicAIAABenchmark,
-    TimeSamplesAIAABenchmark,
-    TriggerAIAABenchmark,
-)
 from .helpers import (
     bardata,
     barspectrum,
@@ -25,4 +18,4 @@ from .helpers import (
     return_result,
 )
 from .metrics import MetricEvaluator
-from .utils import SamplesBuffer
+from .utils import find_basename, get_file_basename
diff --git a/acoular/tools/helpers.py b/acoular/tools/helpers.py
index 0f106af..7d5c9ab 100644
--- a/acoular/tools/helpers.py
+++ b/acoular/tools/helpers.py
@@ -6,24 +6,125 @@
 .. autosummary::
     :toctree: generated/
 
+    synthetic
     return_result
     barspectrum
     bardata
     c_air
 """
 
+from warnings import warn
+
 from numpy import (
     array,
     concatenate,
+    isscalar,
     newaxis,
+    searchsorted,
+    sum,  # noqa A004
     where,
+    zeros_like,
 )
 from numpy.ma import masked_where
 
-from acoular.spectra import synthetic
 from acoular.tools.utils import mole_fraction_of_water_vapor
 
 
+def synthetic(data, freqs, f, num=3):
+    """Returns synthesized frequency band values of spectral data.
+
+    If used with :meth:`Beamformer.result()<acoular.fbeamform.BeamformerBase.result>`
+    and only one frequency band, the output is identical to the result of the intrinsic
+    :meth:`Beamformer.synthetic<acoular.fbeamform.BeamformerBase.synthetic>` method.
+    It can, however, also be used with the
+    :meth:`Beamformer.integrate<acoular.fbeamform.BeamformerBase.integrate>`
+    output and more frequency bands.
+
+    Parameters
+    ----------
+    data : array of floats
+        The spectral data (squared sound pressures in Pa^2) in an array with one value
+        per frequency line.
+        The number of entries must be identical to the number of
+        grid points.
+    freqs : array of floats
+        The frequencies that correspond to the input *data* (as yielded by
+        the :meth:`PowerSpectra.fftfreq<acoular.spectra.PowerSpectra.fftfreq>`
+        method).
+    f : float or list of floats
+        Band center frequency/frequencies for which to return the results.
+    num : integer
+        Controls the width of the frequency bands considered; defaults to
+        3 (third-octave band).
+
+        ===  =====================
+        num  frequency band width
+        ===  =====================
+        0    single frequency line
+        1    octave band
+        3    third-octave band
+        n    1/n-octave band
+        ===  =====================
+
+    Returns
+    -------
+    array of floats
+        Synthesized frequency band values of the beamforming result at
+        each grid point (the sum of all values that are contained in the band).
+        Note that the frequency resolution and therefore the bandwidth
+        represented by a single frequency line depends on
+        the :attr:`sampling frequency<acoular.base.SamplesGenerator.sample_freq>`
+        and used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
+
+    """
+    if isscalar(f):
+        f = (f,)
+    if num == 0:
+        # single frequency lines
+        res = []
+        for i in f:
+            ind = searchsorted(freqs, i)
+            if ind >= len(freqs):
+                warn(
+                    f'Queried frequency ({i:g} Hz) not in resolved frequency range. Returning zeros.',
+                    Warning,
+                    stacklevel=2,
+                )
+                h = zeros_like(data[0])
+            else:
+                if freqs[ind] != i:
+                    warn(
+                        f'Queried frequency ({i:g} Hz) not in set of '
+                        'discrete FFT sample frequencies. '
+                        f'Using frequency {freqs[ind]:g} Hz instead.',
+                        Warning,
+                        stacklevel=2,
+                    )
+                h = data[ind]
+            res += [h]
+    else:
+        # fractional octave bands
+        res = []
+        for i in f:
+            f1 = i * 2.0 ** (-0.5 / num)
+            f2 = i * 2.0 ** (+0.5 / num)
+            ind1 = searchsorted(freqs, f1)
+            ind2 = searchsorted(freqs, f2)
+            if ind1 == ind2:
+                warn(
+                    f'Queried frequency band ({f1:g} to {f2:g} Hz) does not '
+                    'include any discrete FFT sample frequencies. '
+                    'Returning zeros.',
+                    Warning,
+                    stacklevel=2,
+                )
+                h = zeros_like(data[0])
+            else:
+                h = sum(data[ind1:ind2], 0)
+            res += [h]
+    return array(res)
+
+
 def return_result(source, nmax=-1, num=128):
     """Collects the output from a
     :meth:`SamplesGenerator.result()<acoular.base.SamplesGenerator.result>`
diff --git a/acoular/tools/utils.py b/acoular/tools/utils.py
index 874b186..b27dc78 100644
--- a/acoular/tools/utils.py
+++ b/acoular/tools/utils.py
@@ -3,215 +3,58 @@
 .. autosummary::
     :toctree: generated/
 
-    SamplesBuffer
+    get_file_basename
+    find_basename
     mole_fraction_of_water_vapor
 """
 
-import numpy as np
-from traits.api import Any, Array, Bool, Enum, Int, Property, Union
-
-from acoular.process import InOut
-
-
-class SamplesBuffer(InOut):
-    """Handles buffering of samples from a source.
-
-    This class is used to buffer samples from a source and provide them in blocks
-    of a specified size. There are several usecases for this class, as demonstrated in
-    the following.
-
-    Examples
-    --------
-    Let us assume we want to draw blocks of 16 samples from our source, but we want to make sure
-    that we always have twice the number of samples in the buffer. We can achieve this simple
-    behaviour by using the following code:
-
-    >>> import acoular as ac
-    >>> import numpy as np
-    >>> # create a white noise source with 512 samples
-    >>> source = ac.TimeSamples(
-    ...     data=ac.WNoiseGenerator(
-    ...         sample_freq=64,
-    ...         num_samples=512,
-    ...     ).signal()[:, np.newaxis],
-    ...     sample_freq=64,
-    ... )
-    >>> # create a buffer with a size of 32 samples
-    >>> buffer = ac.tools.SamplesBuffer(source=source, length=32)
-    >>> # get the first block of 16 samples
-    >>> block = next(buffer.result(num=16))
-    >>> np.testing.assert_array_equal(block, source.data[:16])
-
-    Here, on the first call to the result method, the buffer will fill up by collecting blocks with
-    same size from the source. The buffer will then return the first block of 16 samples. On the
-    next call to the result method, the buffer will be filled again and returns the next block of 16
-    samples.
-
-    In some cases, we might want to draw a different number of samples from the source than we want
-    to return. This can be achieved by setting the `source_num` trait of the buffer. A special case
-    is the return of a variable number of samples. This is the case, for example, in the class
-    :class:`~acoular.tbeamform.BeamformerTimeTraj`, in which a different number of time samples is
-    required from the buffer for further delay-and-sum processing depending on the expected delay,
-    which can be vary for moving sources. At the same time, however, only 'num' samples should be
-    written to and removed from the buffer. This behavior can be achieved by setting the
-    `shift_index_by` trait to 'num' and by setting the `result_num` trait to the number of samples
-    that should be returned by the result function.
-
-    >>> buffer = ac.tools.SamplesBuffer(source=source, length=32, result_num=20, shift_index_by='num')
-    >>> block_sizes = []
-    >>> block_sizes.append(
-    ...     next(buffer.result(num=16)).shape[0]
-    ... )  # this time, the buffer will return 20 samples, but the buffer will only forget the first 16 samples
-    >>> buffer.result_num = 24
-    >>> block_sizes.append(
-    ...     next(buffer.result(num=16)).shape[0]
-    ... )  # this time, the buffer will return 24 samples, but the buffer will only forget the first 16 samples
-    >>> np.testing.assert_array_equal(block_sizes, [20, 24])
-    """  # noqa: W505
-
-    #: number of samples that fit in the buffer
-    length = Int(desc='number of samples that fit in the buffer')
-
-    #: number of samples per block to obtain from the source. If 'None', use 'num' argument of
-    #: result method
-    source_num = Union(
-        None,
-        Int(),
-        default_value=None,
-        desc='number of samples to return from the source. If "None", use "num" argument of result method',
-    )
-
-    #: number of samples to return from the buffer. If 'None', use 'num' argument of result method
-    result_num = Union(
-        None,
-        Int(),
-        default_value=None,
-        desc="number of samples to return from the buffer. If 'None', use 'num' argument of result method",
-    )
-
-    #: index shift value for the buffer. If "result_num", buffer will return and forget 'result_num'
-    #: samples. If "num", buffer will return 'result_num' samples but will forget 'num' samples
-    shift_index_by = Enum(
-        ('result_num', 'num'),
-        desc=(
-            'index shift value for the buffer. If "result_num", use "result_num" trait.'
-            ' If "num", use "num" argument of result method'
-        ),
-    )
-
-    #: current filling level of buffer
-    level = Property(desc='current filling level of buffer')
-
-    #: data type of the buffer elements
-    dtype = Any(desc='data type of the buffer')
-
-    # flag to indicate that the source is empty, for internal use
-    _empty_source = Bool(False, desc='flag to indicate that the source is empty')
+from pathlib import Path
 
-    # the buffer for processing
-    _buffer = Array(shape=(None, None), desc='buffer for block processing')
-
-    # current index in buffer
-    _index = Int(desc='current index in buffer')
-
-    def _get_level(self):
-        return self._buffer.shape[0] - self._index
-
-    def _create_new_buffer(self):
-        self._buffer = np.zeros((self.length, self.num_channels), dtype=self.dtype)
-        self._index = self.length
-        self._empty_source = False
-
-    def _write_to_buffer(self, data):
-        ns = data.shape[0]
-        self._buffer[0 : (self.length - ns)] = self._buffer[-(self.length - ns) :]
-        self._buffer[-ns:, :] = data.astype(self.dtype)
-        self._index -= ns
-
-    def increase_buffer(self, num):
-        """Increase the buffer by 'num' samples.
-
-        Returns
-        -------
-        None
-        """
-        ar = np.zeros((num, self.num_channels), dtype=self._buffer.dtype)
-        self._buffer = np.concatenate((ar, self._buffer), axis=0)
-        self._index += num
-        self.length += num
+import numpy as np
 
-    def read_from_buffer(self, num):
-        """Read samples from the buffer.
 
-        Parameters
-        ----------
-        num : int
-            number of samples to read from the buffer.
+def get_file_basename(file, alternative_basename='void'):
+    """Return the basename of the file.
 
-        Returns
-        -------
-        numpy.ndarray
-            block of samples from the buffer
+    Parameters
+    ----------
+    file : str
+        File path.
 
-        """
-        rnum = num if self.result_num is None else self.result_num
-        rnum = rnum if self.level >= rnum else self.level
-        data = self._buffer[self._index : self._index + rnum]
-        if self.shift_index_by == 'result_num':
-            self._index += rnum
-        else:
-            self._index += num
-        return data
+    Returns
+    -------
+    str
+        Basename of the file.
+    """
+    basename = Path(file).stem
+    return basename if basename else alternative_basename
 
-    def fill_buffer(self, snum):
-        """Fill the buffer with samples from the source.
 
-        Parameters
-        ----------
-        snum : int
-            number of samples to return from the source.
+def find_basename(source, alternative_basename='void'):
+    """Return the basename of the original source.
 
-        Yields
-        ------
-        None
-        """
-        source_generator = self.source.result(snum)
-        while not self._empty_source:
-            while self._index >= snum:
-                if self.result_num is not None:
-                    while self.result_num > self.length:
-                        self.increase_buffer(snum)
-                try:
-                    self._write_to_buffer(next(source_generator))
-                except StopIteration:
-                    self._empty_source = True
-                    break
-            yield
+    Traverses the source chain of the object and returns the basename of the original source.
+    If the source object does not have a basename, uses the alternative basename.
 
-    def result(self, num):
-        """Return blocks of samples from the buffer.
+    Parameters
+    ----------
+    source : instance
+        :class:`~acoular.base.Generator` derived object
+    alternative_basename : str
+        Alternative basename to use if the source object does not have a basename.
 
-        Parameters
-        ----------
-        num : int
-            number of samples to return.
 
-        Yields
-        ------
-        numpy.ndarray
-            block of samples from the buffer
-        """
-        self._create_new_buffer()
-        snum = num
-        if self.source_num is not None:
-            snum = self.source_num
-        for _ in self.fill_buffer(snum):
-            if self.level > 0:
-                yield self.read_from_buffer(num)
-            else:
-                break
-        while self.level > 0:
-            yield self.read_from_buffer(num)
+    Returns
+    -------
+    str
+        Basename of the original source.
+    """
+    while source:
+        basename = getattr(source, 'basename', None)
+        if basename is not None:
+            return basename
+        source = getattr(source, 'source', None)
+    return alternative_basename
 
 
 def mole_fraction_of_water_vapor(h, t, p=101325):
diff --git a/acoular/tprocess.py b/acoular/tprocess.py
index 36b7671..014f751 100644
--- a/acoular/tprocess.py
+++ b/acoular/tprocess.py
@@ -111,6 +111,7 @@ from .environments import cartToCyl, cylToCart
 from .h5files import _get_h5file_class
 from .internal import digest, ldigest
 from .microphones import MicGeom
+from .tools.utils import find_basename
 
 
 @deprecated_alias({'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'})
@@ -167,9 +168,15 @@ class MaskedTimeOut(TimeOut):
 
     @cached_property
     def _get_basename(self):
-        if 'basename' in self.source.all_trait_names():
-            return self.source.basename
-        return self.source.__class__.__name__ + self.source.digest
+        warn(
+            (
+                f'The basename attribute of a {self.__class__.__name__} object is deprecated'
+                ' and will be removed in a future release!'
+            ),
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return find_basename(self.source, alternative_basename=self.source.__class__.__name__ + self.source.digest)
 
     @cached_property
     def _get_channels(self):
@@ -1790,18 +1797,15 @@ class WriteWAV(TimeOut):
 
     @cached_property
     def _get_basename(self):
-        obj = self.source  # start with source
-        try:
-            while obj:
-                if 'basename' in obj.all_trait_names():  # at original source?
-                    basename = obj.basename  # get the name
-                    break
-                obj = obj.source  # traverse down until original data source
-            else:
-                basename = 'void'
-        except AttributeError:
-            basename = 'void'  # if no file source is found
-        return basename
+        warn(
+            (
+                f'The basename attribute of a {self.__class__.__name__} object is deprecated'
+                ' and will be removed in a future release!'
+            ),
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return find_basename(self.source)
 
     def save(self):
         """Saves source output to one- or multiple-channel `*.wav` file."""
diff --git a/docs/source/news/index.rst b/docs/source/news/index.rst
index 684a075..b6e6f72 100644
--- a/docs/source/news/index.rst
+++ b/docs/source/news/index.rst
@@ -74,12 +74,16 @@ Upcoming Release
             * adds unittests `test_process.py`, `test_fprocess.py`
             * adds documentation example `example_fft.py`
                 * allow more platforms to build the docs files including Linux, MacOS, and Windows
+            * move submodule `acoular.tools.aiaa` to `acoular.aiaa`
 
     **Bugfixes**
         * flush file in :class:`~acoular.tprocess.TimeCache` to prevent data loss / corruption
         * fixes use of already deprecated traits
         * fixes import order to avoid the deactivation of parallelization of numba
         * fixes numpy.matlib deprecation warning
+        * better handling of `basename` attributes:
+            * adds new utils functions `find_basename` and `get_file_basename` for consistent handling of `basename` attributes
+            * deprecates `basename` attribute of :class:`~acoular.tprocess.WriteWAV` and :class:`~acoular.tprocess.MaskedTimeOut` for reasons of inconsistency
 
     **Documentation**
         * updates literature and adds `sphinxcontrib-bibtex` extension to support bibtex citation in the documentation.
@@ -91,7 +95,7 @@ Upcoming Release
 
     **Internal**
         * refactoring of classes in :mod:`acoular.tbeamform` (moves buffer logic away from Beamformer classes)
-            * adds new :class:`~acoular.tools.utils.SamplesBuffer` class located in :mod:`~acoular.tools.utils`
+            * adds new :class:`~acoular.process.SamplesBuffer` class located in :mod:`~acoular.tools.utils`
         * replaces the markdown-link-check by lychee
             * faster CI (written in RUST)
             * allows more files to be checked, including the .rst files in the documentation


Test patch:
diff --git a/tests/cases/test_generator_cases.py b/tests/cases/test_generator_cases.py
index 77635c1..3f83717 100644
--- a/tests/cases/test_generator_cases.py
+++ b/tests/cases/test_generator_cases.py
@@ -202,10 +202,10 @@ class Generators:
         return ac.WriteWAV(source=time_data_source, file=tmp_path / 'test.wav')
 
     def case_TimeSamplesAIAABenchmark(self, aiaa_bechmark_time_data_file):
-        return ac.tools.TimeSamplesAIAABenchmark(file=aiaa_bechmark_time_data_file)
+        return ac.aiaa.TimeSamplesAIAABenchmark(file=aiaa_bechmark_time_data_file)
 
     def case_TriggerAIAABenchmark(self, aiaa_bechmark_trigger_file):
-        return ac.tools.TriggerAIAABenchmark(file=aiaa_bechmark_trigger_file)
+        return ac.aiaa.TriggerAIAABenchmark(file=aiaa_bechmark_trigger_file)
 
     @parametrize('domain', ['time', 'frequency'])
     def case_Calib(self, time_data_source, domain):
diff --git a/tests/unittests/test_aiaa.py b/tests/unittests/test_aiaa.py
index 8831583..2d88f88 100644
--- a/tests/unittests/test_aiaa.py
+++ b/tests/unittests/test_aiaa.py
@@ -14,8 +14,8 @@ def test_load_mics(aiaa_bechmark_time_data_file):
 
     """
     with pytest.deprecated_call():
-        mic_geom = ac.tools.MicAIAABenchmark(from_file=aiaa_bechmark_time_data_file)
-    mic_geom = ac.tools.MicAIAABenchmark(file=aiaa_bechmark_time_data_file)
+        mic_geom = ac.aiaa.MicAIAABenchmark(from_file=aiaa_bechmark_time_data_file)
+    mic_geom = ac.aiaa.MicAIAABenchmark(file=aiaa_bechmark_time_data_file)
     assert mic_geom.num_mics == 8
     assert mic_geom.pos.shape == (3, 8)
     assert mic_geom.pos_total.shape == (3, 8)
@@ -32,8 +32,8 @@ def test_load_csm(aiaa_bechmark_csm_file):
         Path to the AIAA benchmark file with cross spectral matrix data (fixture
     """
     with pytest.deprecated_call():
-        csm = ac.tools.CsmAIAABenchmark(name=aiaa_bechmark_csm_file)
-    csm = ac.tools.CsmAIAABenchmark(file=aiaa_bechmark_csm_file)
+        csm = ac.aiaa.CsmAIAABenchmark(name=aiaa_bechmark_csm_file)
+    csm = ac.aiaa.CsmAIAABenchmark(file=aiaa_bechmark_csm_file)
     assert csm.num_channels == 8
     assert csm.csm.shape == (csm.fftfreq().shape[0], csm.num_channels, csm.num_channels)
 
@@ -49,8 +49,8 @@ def test_load_time(aiaa_bechmark_time_data_file):
         Path to the AIAA benchmark file with time data (fixture).
     """
     with pytest.deprecated_call():
-        time_data = ac.tools.TimeSamplesAIAABenchmark(name=aiaa_bechmark_time_data_file)
-    time_data = ac.tools.TimeSamplesAIAABenchmark(file=aiaa_bechmark_time_data_file)
+        time_data = ac.aiaa.TimeSamplesAIAABenchmark(name=aiaa_bechmark_time_data_file)
+    time_data = ac.aiaa.TimeSamplesAIAABenchmark(file=aiaa_bechmark_time_data_file)
     assert time_data.num_channels == 8
     assert time_data.num_samples == 480
     assert time_data.sample_freq == 48000
@@ -69,8 +69,8 @@ def test_load_trigger(aiaa_bechmark_trigger_file):
         Path to the AIAA benchmark file with trigger data (fixture).
     """
     with pytest.deprecated_call():
-        trigger_data = ac.tools.TriggerAIAABenchmark(name=aiaa_bechmark_trigger_file)
-    trigger_data = ac.tools.TriggerAIAABenchmark(file=aiaa_bechmark_trigger_file)
+        trigger_data = ac.aiaa.TriggerAIAABenchmark(name=aiaa_bechmark_trigger_file)
+    trigger_data = ac.aiaa.TriggerAIAABenchmark(file=aiaa_bechmark_trigger_file)
     assert trigger_data.num_channels == 1
     assert trigger_data.num_samples == 480
     assert trigger_data.sample_freq == 48000
diff --git a/tests/unittests/test_sample_buffer.py b/tests/unittests/test_sample_buffer.py
index a3209fd..d424206 100644
--- a/tests/unittests/test_sample_buffer.py
+++ b/tests/unittests/test_sample_buffer.py
@@ -14,7 +14,7 @@ def test_num_args(num, create_time_data_source, num_channels):
     buffer_size = 512
     time_data_source = create_time_data_source(num_channels=num_channels, num_samples=buffer_size + 10)
     for snum in [128, None]:
-        buffer = ac.tools.utils.SamplesBuffer(source=time_data_source, length=buffer_size, source_num=snum)
+        buffer = ac.process.SamplesBuffer(source=time_data_source, length=buffer_size, source_num=snum)
         data = ac.tools.return_result(time_data_source, num=num)
         data_comp = ac.tools.return_result(buffer, num=num)
         np.testing.assert_array_equal(data, data_comp)
@@ -26,7 +26,7 @@ def test_increase_buffer(create_time_data_source, num_channels):
     time_data_source = create_time_data_source(num_channels=num_channels, num_samples=512 + 10)
     data = ac.tools.return_result(time_data_source, num=256)
     # test buffer
-    buffer = ac.tools.utils.SamplesBuffer(source=time_data_source, length=buffer_size, source_num=25)
+    buffer = ac.process.SamplesBuffer(source=time_data_source, length=buffer_size, source_num=25)
     gen = buffer.result(num=25)
     collected_data = np.zeros_like(data)
     i = 0