Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement audio device classes using buffer objects #6

Draft
wants to merge 71 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
1c869f4
add init AudioDevice
mberz Oct 6, 2021
3505e19
fix import
mberz Oct 6, 2021
a12d420
wip: devices
mberz Oct 7, 2021
434ef95
ignore private folder in tests
mberz Oct 7, 2021
daa520d
docstrings
mberz Oct 8, 2021
e706ae9
Merge branch 'develop' into feature/devices
mberz Sep 21, 2022
3219436
remove arrayqueue
mberz Sep 21, 2022
7edfcc7
allow python >=3.8
mberz Sep 21, 2022
ae50473
start using generator functions
mberz Sep 22, 2022
62df206
experimenting with simple generators for arrays
mberz Sep 22, 2022
b530857
WIP: continued work on generators
mberz Sep 23, 2022
7e51420
experimenting with simple generators for arrays
mberz Sep 22, 2022
4c8ec9b
WIP: continued work on generators
mberz Sep 23, 2022
3e9e766
initial simple tests for generators
mberz Sep 23, 2022
14dc523
separate input and output buffers
mberz Sep 23, 2022
f171946
Merge branch 'feature/generators' into feature/devices_generators
mberz Sep 23, 2022
f47006b
clelaning up
mberz Sep 23, 2022
69f78a5
cleaning up
mberz Sep 23, 2022
f526aba
cleaning up AudioOutputDevice
mberz Sep 23, 2022
e20de7b
start writing mocks for query of sounddevices
mberz Sep 23, 2022
6a2d4c0
mocks for checking of input and output settings
mberz Sep 26, 2022
c2b67a9
start mocking sounddevice stream and ArrayBuffer
mberz Sep 26, 2022
f6f89b3
add number of channels, use abstract methods and properties, use
mberz Sep 27, 2022
242f683
implement simple signal buffer analogous to array buffers
mberz Sep 27, 2022
7cde47b
add active state to generators
mberz Oct 31, 2022
bd34211
improvements for state checking of buffer classes
mberz Oct 31, 2022
0c5c5d2
testing of sampling_rate and n_channels properties
mberz Oct 31, 2022
d33dadc
remove ArrayBuffer implementations
mberz Oct 31, 2022
fc65dfb
remove ArrayBuffer class implementation
mberz Nov 2, 2022
4485141
Merge branch 'develop' into feature/generators
mberz Nov 2, 2022
1b37d24
rename generators.py to buffers.py
mberz Nov 2, 2022
a482582
Merge branch 'develop' into feature/generators
mberz Nov 2, 2022
6d6e2eb
rough docstrings for Buffer
mberz Nov 2, 2022
3de3c2c
rough documentation of SignalBuffer
mberz Nov 2, 2022
1711e86
test the __iter__ method
mberz Nov 2, 2022
ee2c236
rename generator tests to buffer tests
mberz Nov 2, 2022
8930e48
add minimal example to SignalBuffer
mberz Nov 2, 2022
f4eeaba
add rst files for sphinx
mberz Nov 2, 2022
b1cb153
wip
mberz Nov 2, 2022
4666aa0
Merge branch 'feature/generators' into feature/devices_generators
mberz Nov 2, 2022
e6ca2cf
rename to InputAudioDevice and OutputAudioDevice
mberz Nov 2, 2022
3ac5d68
rename id to identifier
mberz Nov 2, 2022
7dea38c
explicitly raise error from previous exception
mberz Nov 2, 2022
13a660f
use SignalBuffer in test utils
mberz Nov 2, 2022
3cf58b4
Merge branch 'develop' into feature/devices_generators
mberz Nov 2, 2022
0ac1c16
update stubs and add conftest
mberz Nov 3, 2022
a57cc3e
fix sine buffer and properly wait for the playback to complete
mberz Nov 3, 2022
2fa3223
rename id to identifier to avoid re-using built in functions
mberz Nov 3, 2022
69c3569
improve check_settings method and add tests
mberz Nov 3, 2022
e852fc6
add proper input channel handling and buffer initialization
mberz Nov 4, 2022
3509ea3
WIP: Adapt Event based buffers
mberz Nov 14, 2022
ffa95dd
update stub
mberz Nov 14, 2022
47933cd
Use events for setting buffer states which can waited for
mberz Nov 14, 2022
6302ba5
make the buffer base class private
mberz Nov 14, 2022
9eaaeb4
minor review comments
mberz Nov 14, 2022
fb6f6f2
Fix catching updated error message
mberz Nov 14, 2022
45e8ff4
added test for creating buffers which require padding
mberz Nov 14, 2022
d374113
reset will clear the active and finished flags
mberz Nov 14, 2022
9dac88c
Merge branch 'feature/generators' into feature/devices_generators
mberz Nov 15, 2022
b7f69a2
add Scarlett 2i4 to valid testing devices
mberz Nov 15, 2022
b09692d
workaround for portaudio's broadcasting to 2 ch mono playback
mberz Nov 15, 2022
6677206
restructure tests
mberz Nov 15, 2022
5ed40aa
add method to initialize buffers
mberz Nov 15, 2022
2e3ea7f
replace deprecated @abstractproperty
mberz Mar 15, 2023
6d9264f
dont check imports on soundfile imports in draft files
mberz Mar 15, 2023
8b1e370
Buffer a block of output audio data containing all channels instead o…
mberz Mar 15, 2023
f8b41b8
move active checking when setting the block size to private method
mberz Mar 27, 2023
a728592
improve documentation of check_if_active
mberz Mar 27, 2023
5dc97cc
document inherited members in buffer classes
mberz Mar 27, 2023
ee86cdb
Merge branch 'feature/generators' into feature/devices_generators
mberz Mar 30, 2023
667b91c
change order of decorators
mberz Mar 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ Modules
The following gives detailed information about all haiopy modules.

.. toctree::
:maxdepth: 1
:maxdepth: 1

modules/haiopy.buffers
8 changes: 8 additions & 0 deletions docs/modules/haiopy.buffers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
haiopy.buffers
==============

.. automodule:: haiopy.buffers
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
8 changes: 8 additions & 0 deletions haiopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
__author__ = """The pyfar developers"""
__email__ = '[email protected]'
__version__ = '0.1.0'


from .devices import AudioDevice


__all__ = [
'AudioDevice'
]
231 changes: 231 additions & 0 deletions haiopy/buffers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import numpy as np
import pyfar as pf
from abc import abstractmethod
from threading import Event


class _Buffer(object):
"""Abstract base class for audio buffers for block-wise iteration.

The base class primarily implements buffer state related functionality.
"""

def __init__(self, block_size) -> None:
"""Create a Buffer object with a given block size.

Parameters
----------
block_size : _type_
_description_
"""
self._check_block_size(block_size)
self._block_size = block_size
self._buffer = None
self._is_active = Event()
self._is_finished = Event()

def _check_block_size(self, block_size):
"""Check if the block size is an integer."""
if type(block_size) != int:
raise ValueError("The block size needs to be an integer")

def _set_block_size(self, block_size):
"""Private block size setter implementing validity checks."""
self.check_if_active()
self._check_block_size(block_size)
self._block_size = block_size

@property
def block_size(self):
"""Returns the block size of the buffer in samples"""
return self._block_size

@block_size.setter
def block_size(self, block_size):
"""Set the block size in samples. Only integer values are supported"""
self._set_block_size(block_size)

@property
@abstractmethod
def sampling_rate(self):
"""Return sampling rate."""
pass

def __iter__(self):
return self

def __next__(self):
"""Next dunder method for iteration"""
self._start()
return self.next()

@abstractmethod
def next(self):
"""Next method which for sub-class specific handling of data."""
raise NotImplementedError()

@property
def is_active(self):
"""Return the state of the buffer.
`True` if the buffer is active, `False` if inactive."""
return self._is_active.is_set()

def check_if_active(self):
"""Check if the buffer is active and raise an exception if so.
If the buffer is active a BufferError exception is raised. In case the
buffer is currently inactive, the method simply passes without any
return value. This method should always be called before attempting to
modify properties of the buffer to prevent undefined behavior during
iteration of the buffer.

Raises
------
BufferError
Exception is raised if the buffer is currently active.
"""
if self.is_active:
raise BufferError(
"The buffer needs to be inactive to be modified.")

def _stop(self, msg="Buffer iteration stopped."):
"""Stop buffer iteration and set the state to inactive."""
self._is_active.clear()
self._is_finished.set()
raise StopIteration(msg)

def _start(self):
"""Set the state to active.
Additional operations required before iterating the sub-class can be
implemented in the respective sub-class."""
self._is_active.set()
self._is_finished.clear()

def _reset(self):
"""Stop and reset the buffer.
Resetting the buffer is implemented in the respective sub-class"""
self._is_active.clear()
self._is_finished.clear()
raise StopIteration("Resetting the buffer.")


class SignalBuffer(_Buffer):
"""Buffer to block wise iterate a `pyfar.Signal`

Examples
--------

>>> import pyfar as pf
>>> from haiopy.buffers import SignalBuffer
>>> block_size = 512
>>> sine = pf.signals.sine(440, 4*block_size)
>>> buffer = SignalBuffer(block_size, sine)
>>> for block in buffer:
>>> print(block)


"""

def __init__(self, block_size, signal) -> None:
"""Initialize a `SignalBuffer` with a given block size from a
`pyfar.Signal`.
If the number of audio samples is not an integer multiple of the
block size, the last block will be filled with zeros.

Parameters
----------
block_size : int
The block size in samples
signal : pyfar.Signal
The audio data to be block wise iterated.

"""
super().__init__(block_size)
if not isinstance(signal, pf.Signal):
raise ValueError("signal must be a pyfar.Signal object.")
if signal.time.ndim > 2:
raise ValueError("Only one-dimensional arrays are allowed")
self._data = self._pad_data(signal)
self._update_data()
self._index = 0

def _pad_data(self, data):
"""Pad the signal with zeros to avoid partially filled blocks

Parameters
----------
data : pyfar.Signal
The input audio signal.

Returns
-------
pyfar.Signal
Zero-padded signal.
"""
n_samples = data.n_samples
if np.mod(n_samples, self._block_size) > 0:
pad_samples = self.block_size - np.mod(n_samples, self.block_size)
return pf.dsp.pad_zeros(data, pad_samples, mode='after')
else:
return data

@property
def n_channels(self):
"""The number of audio channels as integer."""
return self.data.cshape[0]

@property
def sampling_rate(self):
"""The sampling rate of the underlying data."""
return self.data.sampling_rate

@property
def n_blocks(self):
"""The number of blocks contained in the buffer."""
return self._n_blocks

@property
def index(self):
"""The current block index as integer."""
return self._index

@property
def data(self):
"""Return the underlying signal if the buffer is not active."""
self.check_if_active()
return self._data

@data.setter
def data(self, data):
"""Set the underlying signal if the buffer is not active."""
self.check_if_active()
self._data = self._pad_data(data)
self._update_data()

def _set_block_size(self, block_size):
super()._set_block_size(block_size)
self._update_data()

def _update_data(self):
"""Update the data block strided of the underlying data.
The function creates a block-wise view of the numpy data array storing
the time domain data.
"""
self.check_if_active()
self._n_blocks = int(np.ceil(self.data.n_samples / self.block_size))
self._strided_data = np.lib.stride_tricks.as_strided(
self.data.time,
(*self.data.cshape, self.n_blocks, self.block_size))

def next(self):
"""Return the next audio block as numpy array and increment the block
index.
"""
if self._index < self._n_blocks:
current = self._index
self._index += 1
return self._strided_data[..., current, :]
self._stop("The buffer is empty.")

def _reset(self):
self._index = 0
super()._reset()
Loading