From 37dd9512b4129aecdd5c78ec472509bbd2cdc96f Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Wed, 6 Oct 2021 18:05:31 +0200 Subject: [PATCH 1/8] added mock_utils and tests --- haiopy/mock_utils.py | 107 +++++++++++++++++++++++++++++++++++++++ tests/test_mock_utils.py | 22 ++++++++ 2 files changed, 129 insertions(+) create mode 100644 haiopy/mock_utils.py create mode 100644 tests/test_mock_utils.py diff --git a/haiopy/mock_utils.py b/haiopy/mock_utils.py new file mode 100644 index 0000000..e63827d --- /dev/null +++ b/haiopy/mock_utils.py @@ -0,0 +1,107 @@ +""" +Provides mocks of common haiopy classes, which help to test the hardware +communication. +""" +import numpy as np +from unittest import mock + + +class _Device(mock.Mock): + def __init__( + self, + id=0, + sampling_rate=44100, + block_size=2**5, + dtype=np.float64): + super().__init__() + self._sampling_rate = sampling_rate + self._dtype = dtype + + @property + def name(self): + raise NotImplementedError('Abstract method') + + @property + def sampling_rate(self): + return self._sampling_rate + + @property + def dtype(self): + return self._dtype + + def playback(): + pass + + def record(): + pass + + def playback_record(): + pass + + def initialize_playback(): + pass + + def initialize_record(): + pass + + def initialize_playback_record(): + pass + + def abort(): + pass + + +class AudioDevice(_Device, mock.Mock): + def __init__( + self, + id=0, + sampling_rate=44100, + block_size=2**5, + dtype=np.float64, + latency=None, + extra_settings=None, + # finished_callback=None, + clip_off=None, + dither_off=None, + never_drop_input=None, + prime_output_buffers_using_stream_callback=None + ): + super().__init__(id, sampling_rate, block_size, dtype) + + @property + def stream(): + pass + + @staticmethod + def callback(): + pass + + def playback(data): + # fill queue, stream.start() + pass + + def record(n_samples): + # stream start, read into the queue + pass + + def playback_record(data): + # see combination above + pass + + def initialize_playback(channels): + # init queue, define callback, init stream + pass + + def initialize_record(channels): + pass + + def initialize_playback_record(input_channels, output_channels): + pass + + def abort(): + # abort + pass + + def close(): + # remove stream + pass \ No newline at end of file diff --git a/tests/test_mock_utils.py b/tests/test_mock_utils.py new file mode 100644 index 0000000..1560641 --- /dev/null +++ b/tests/test_mock_utils.py @@ -0,0 +1,22 @@ +import haiopy.mock_utils as mock_utils +import haiopy.devices as devices + + +def test__Device_mock_properties(): + """ Test to check _Device mock initialization. + """ + mock_dir = dir(mock_utils._Device) + device_dir = dir(devices._Device) + + assert mock_dir.sort() == device_dir.sort() + + +def test_AudioDevice_mock_properties(): + """ Test to check AudioDevice mock initialization. + """ + mock_dir = dir(mock_utils.AudioDevice) + device_dir = dir(devices.AudioDevice) + + assert mock_dir.sort() == device_dir.sort() + + From fb65fc06a789be7871194cc7a876b8aa08ea457d Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Wed, 6 Oct 2021 18:07:36 +0200 Subject: [PATCH 2/8] first commit playback class, wip --- haiopy/io.py | 144 +++++++++++++++++++++++++++++++++++++++++++---- tests/test_io.py | 130 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 tests/test_io.py diff --git a/haiopy/io.py b/haiopy/io.py index 9cfa4bc..480c18c 100644 --- a/haiopy/io.py +++ b/haiopy/io.py @@ -1,18 +1,43 @@ +""" +Playback and recording functionality including classes and convenience +functions. +""" + +import numpy as np +import pyfar as pf + from . import devices +from abc import ABC, abstractmethod -class _AudioIO(object): +class _AudioIO(ABC): + """Abstract class for playback and recording. + + This class holds all the methods and properties that are common to its + three sub-classes :py:class:`Playback`, :py:class:`Record`, and + :py:class:`PlaybackRecord`. + """ def __init__( self, device ): - super().__init__() + if isinstance(device, devices._Device): + self._device = device + else: + raise ValueError("Incorrect device, needs to be a" + ":py:class:`~haiopy.AudioDevice` object.") + @abstractmethod def start(): + """ This function depends on the use case (playback, recording or + playback and record) and therefore is implemented in the subclasses. + """ pass - def stop(): - pass + def stop(device): + """ Immediately terminate the playback/recording.""" + device.abort() + print("Playback / Recording terminated.") def wait(): pass @@ -22,36 +47,120 @@ class Playback(_AudioIO): def __init__( self, device, - input_channels, - repetitions=1, - loop=False): + output_channels, + repetitions=1): super().__init__(device=device) + self.output_channels = output_channels + self.repetitions = repetitions self._output_signal = None + @property + def device(self): + return self._device + @property def output_signal(self): return self._output_signal @output_signal.setter - def output_signal(self, sig): - self._output_signal = sig + def output_signal(self, signal): + """Set ``pyfar.Signal` to be played back.""" + if not isinstance(signal, pf.Signal): + raise ValueError("Output signal needs to be a pyfar.Signal.") + elif signal.sampling_rate != self.device.sampling_rate: + raise ValueError( + f"Sampling rates of the signal ({signal.sampling_rate}) " + f"and the device ({self.device.sampling_rate}) " + f"do not match.") + elif signal.dtype != self.device.dtype: + raise ValueError( + f"Datatypes of the signal ({signal.dtype}) " + f"and the device ({self.device.dtype}) " + f"do not match.") + elif signal.cshape != self.output_channels.shape: + raise ValueError( + f"The cshape of the signal ({signal.cshape}) " + f"and the number of channels ({self.output_channels.shape}) " + f"do not match.") + else: + self._output_signal = signal + + @property + def output_channels(self): + """Output channels.""" + return self._output_channels + + @output_channels.setter + def output_channels(self, channels): + """Set output_channels parameter. It can be a single number, list, + tuple or a 1D array with unique values. + """ + channels_int = np.unique(channels).astype(int) + if np.atleast_1d(channels).shape != channels_int.shape: + raise ValueError("Output_channels must be a single number, list, " + "tuple or a 1D array with unique values.") + elif not np.all(channels == channels_int): + raise ValueError("Parameter output_channels must contain only" + "integers.") + else: + self._output_channels = channels_int + + @property + def repetitions(self): + """Number of repetitions of the playback.""" + return self._repetitions + + @repetitions.setter + def repetitions(self, value): + """Set the number of repetitions of the playback. ``repetitions`` can + be set to decimal numbers and ``numpy.inf``. The default is ``1``.""" + try: + value = float(value) + except (ValueError, TypeError): + raise ValueError("Repetitions must be a scalar number.") + if value > 0: + self._repetitions = value + else: + raise ValueError("Repetitions must be positive or numpy.inf.") + + def start(): + """ This function depends on the use case (playback, recording or + playback and record) and therefore is implemented in the subclasses. + """ + # repetitions + pass class Record(_AudioIO): def __init__( self, device, - output_channels, + input_channels, duration=None, fft_norm='amplitude', ): super().__init__(device=device) self._input_signal = None + self.input_channels = input_channels + + @property + def input_channels(self): + return self._input_channels + + @input_channels.setter + def input_channels(self, channels): + self._input_channels = channels @property def input_signal(self): return self._input_signal + def start(): + """ This function depends on the use case (playback, recording or + playback and record) and therefore is implemented in the subclasses. + """ + pass + class PlaybackRecord(Playback, Record): def __init__( @@ -60,11 +169,21 @@ def __init__( input_channels, output_channels, ): - super().__init__( + Record.__init__( + self, + device=device, + input_channels=input_channels) + Playback.__init__( + self, device=device, - input_channels=input_channels, output_channels=output_channels) + def start(): + """ This function depends on the use case (playback, recording or + playback and record) and therefore is implemented in the subclasses. + """ + pass + def playback(): pass @@ -76,3 +195,4 @@ def record(): def playback_record(): pass + diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 0000000..36b5fd5 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,130 @@ +import haiopy as hp +import pytest +import numpy as np +import pyfar as pf + +from unittest.mock import patch + +from haiopy import mock_utils +from haiopy import io +from haiopy import Playback +from haiopy import Record +from haiopy import PlaybackRecord + + +@patch('haiopy.devices._Device', new=mock_utils._Device) +@patch('haiopy.AudioDevice', new=mock_utils.AudioDevice) +class TestPlayback: + def test__AudioIO_init_error(self): + """ Test error for instatiating abstract class""" + with pytest.raises(TypeError): + io._AudioIO(0) + + def test_init(self): + device = hp.AudioDevice() + output_channels = 0 + Playback(device, output_channels) + + def test_init_default_parameters(self): + device = hp.AudioDevice() + output_channels = 0 + playback = Playback(device, output_channels) + assert playback.device == device + assert playback.output_channels == output_channels + assert playback.repetitions == 1 + assert playback.output_signal is None + + def test_init_set_parameters(self): + device = hp.AudioDevice() + output_channels = 2 + repetitions = 3 + playback = Playback( + device, output_channels, repetitions) + assert playback.output_channels == output_channels + assert playback.repetitions == repetitions + + def test_init_device_error(self): + device = 0 + channels = 0 + with pytest.raises(ValueError, match="Incorrect device"): + Playback(device, channels) + + def test_set_output_channels(self): + device = hp.AudioDevice() + pb = Playback(device, 0) + # Check allowed input formats + for ch in [1, [1, 2], np.array([1, 2]), (1, 2)]: + pb.output_channels = ch + assert np.all(pb.output_channels == ch) + assert len(pb.output_channels.shape) == 1 + + def test_set_output_channels_errors(self): + device = hp.AudioDevice() + # Error for non integer input + for ch in [1.1, [1.1, 2], np.array([1.1, 2]), (1.1, 2)]: + with pytest.raises(ValueError, match="integers"): + Playback(device, ch) + # Error for array which is not 2D + ch = np.array([[1, 2], [3, 4]]) + with pytest.raises(ValueError, match="1D array"): + Playback(device, ch) + # Error for non unique values + for ch in [[1, 1], np.array([1, 1]), (1, 1)]: + with pytest.raises(ValueError, match="unique"): + Playback(device, ch) + + def test_set_repetitions(self): + pb = Playback(hp.AudioDevice(), 0) + pb.repetitions = 2 + assert pb.repetitions == 2 + pb.repetitions = np.inf + assert pb.repetitions == np.inf + + def test_set_repetitions_errors(self): + pb = Playback(hp.AudioDevice(), 0) + for value in ['a', [1], np.array([1, 2])]: + with pytest.raises(ValueError, match="scalar number"): + pb.repetitions = value + for value in [0, -1]: + with pytest.raises(ValueError, match="positive"): + pb.repetitions = value + + def test_output_signal_setter(self): + signal = pf.signals.sine(100, 100) + pb = Playback(hp.AudioDevice(), 0) + pb.output_signal = signal + + signal = pf.signals.sine([100, 200], 100) + pb = Playback(hp.AudioDevice(), [0, 1]) + pb.output_signal = signal + + def test_output_signal_setter_errors(self): + pass + + def test_start(self): + pass + + def test_stop(self): + pass + + def test_wait(self): + pass + + def test_repetitions(self): + pass + + +class TestRecord: + def test_init_device_error(self): + device = 0 + channels = 0 + with pytest.raises(ValueError, match="Incorrect device"): + Record(device, channels) + + +class TestPlaybackRecord: + def test_init_device_error(self): + device = 0 + channels = 0 + with pytest.raises(ValueError, match="Incorrect device"): + PlaybackRecord(device, channels, channels) From dc16095b2d31d54b616a9b46149cf99406cbf904 Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Wed, 6 Oct 2021 18:08:36 +0200 Subject: [PATCH 3/8] added classes in init file --- haiopy/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/haiopy/__init__.py b/haiopy/__init__.py index ab59fe2..11b8af7 100644 --- a/haiopy/__init__.py +++ b/haiopy/__init__.py @@ -3,3 +3,13 @@ __author__ = """The pyfar developers""" __email__ = 'marco.berzborn@akustik.rwth-aachen.de' __version__ = '0.1.0' + +from .devices import AudioDevice +from .io import Playback, Record, PlaybackRecord + +__all__ = [ + 'AudioDevice' + 'Playback', + 'Record', + 'PlaybackRecord' + ] From 69a2fe37bcc987c706bc44e0eb8a9330b972cd3f Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Thu, 7 Oct 2021 17:56:03 +0200 Subject: [PATCH 4/8] playback class day 2, wip --- haiopy/io.py | 63 ++++++++++++++++++++++++++++--------------- tests/test_io.py | 69 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 32 deletions(-) diff --git a/haiopy/io.py b/haiopy/io.py index 480c18c..a8d1429 100644 --- a/haiopy/io.py +++ b/haiopy/io.py @@ -17,10 +17,7 @@ class _AudioIO(ABC): three sub-classes :py:class:`Playback`, :py:class:`Record`, and :py:class:`PlaybackRecord`. """ - def __init__( - self, - device - ): + def __init__(self, device): if isinstance(device, devices._Device): self._device = device else: @@ -44,28 +41,46 @@ def wait(): class Playback(_AudioIO): + """Class for playback of signals. + """ def __init__( - self, - device, - output_channels, - repetitions=1): + self, device, output_channels, repetitions=1, + output_signal=None): + """Create a Playback object. + + Parameters + ---------- + device : [type] + [description] + output_channels : [type] + [description] + repetitions : int, optional + [description], by default 1 + """ super().__init__(device=device) + # Set properties, check implicitly self.output_channels = output_channels self.repetitions = repetitions - self._output_signal = None + self.output_signal = output_signal + # Initialize device + self.device.initialize_playback(self.output_channels) @property def device(self): + """Output device.""" return self._device @property def output_signal(self): + """``pyfar.Signal`` to be played back.""" return self._output_signal @output_signal.setter def output_signal(self, signal): - """Set ``pyfar.Signal` to be played back.""" - if not isinstance(signal, pf.Signal): + """Set ``pyfar.Signal`` to be played back.""" + if signal is None: + self._output_signal = signal + elif not isinstance(signal, pf.Signal): raise ValueError("Output signal needs to be a pyfar.Signal.") elif signal.sampling_rate != self.device.sampling_rate: raise ValueError( @@ -113,7 +128,8 @@ def repetitions(self): @repetitions.setter def repetitions(self, value): """Set the number of repetitions of the playback. ``repetitions`` can - be set to decimal numbers and ``numpy.inf``. The default is ``1``.""" + be set to a decimal number. The default is ``1``, + which is a single playback.""" try: value = float(value) except (ValueError, TypeError): @@ -121,14 +137,20 @@ def repetitions(self, value): if value > 0: self._repetitions = value else: - raise ValueError("Repetitions must be positive or numpy.inf.") - - def start(): - """ This function depends on the use case (playback, recording or - playback and record) and therefore is implemented in the subclasses. - """ - # repetitions - pass + raise ValueError("Repetitions must be positive.") + + def start(self): + """Start the playback.""" + if self.output_signal is None: + raise ValueError("To start the playback, first set an output " + "signal.") + # Extract time data + data = self.output_signal.time + # Repeat and append + append_idx = int(self.repetitions % 1 * self.output_signal.n_samples) + data_out = np.tile(data, int(self.repetitions)) + data_out = np.append(data_out, data[..., :append_idx], axis=-1) + self.device.playback(data_out) class Record(_AudioIO): @@ -195,4 +217,3 @@ def record(): def playback_record(): pass - diff --git a/tests/test_io.py b/tests/test_io.py index 36b5fd5..1ef931c 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,9 +1,12 @@ +import unittest import haiopy as hp import pytest import numpy as np import pyfar as pf +import numpy.testing as npt +import unittest -from unittest.mock import patch +from unittest import mock from haiopy import mock_utils from haiopy import io @@ -12,9 +15,9 @@ from haiopy import PlaybackRecord -@patch('haiopy.devices._Device', new=mock_utils._Device) -@patch('haiopy.AudioDevice', new=mock_utils.AudioDevice) -class TestPlayback: +@mock.patch('haiopy.devices._Device', new=mock_utils._Device) +@mock.patch('haiopy.AudioDevice', new=mock_utils.AudioDevice) +class TestPlayback(): def test__AudioIO_init_error(self): """ Test error for instatiating abstract class""" with pytest.raises(TypeError): @@ -38,10 +41,12 @@ def test_init_set_parameters(self): device = hp.AudioDevice() output_channels = 2 repetitions = 3 + signal = pf.signals.sine(100, 100) playback = Playback( - device, output_channels, repetitions) + device, output_channels, repetitions, signal) assert playback.output_channels == output_channels assert playback.repetitions == repetitions + assert playback.output_signal == signal def test_init_device_error(self): device = 0 @@ -77,8 +82,6 @@ def test_set_repetitions(self): pb = Playback(hp.AudioDevice(), 0) pb.repetitions = 2 assert pb.repetitions == 2 - pb.repetitions = np.inf - assert pb.repetitions == np.inf def test_set_repetitions_errors(self): pb = Playback(hp.AudioDevice(), 0) @@ -99,10 +102,41 @@ def test_output_signal_setter(self): pb.output_signal = signal def test_output_signal_setter_errors(self): - pass - - def test_start(self): - pass + device = hp.AudioDevice(sampling_rate=44100, dtype=np.float64) + pb = Playback(device, 0) + with pytest.raises(ValueError, match="pyfar.Signal"): + pb.output_signal = 1 + with pytest.raises(ValueError, match="Sampling rate"): + signal = pf.Signal(np.random.rand(100), 1000, dtype=np.float64) + pb.output_signal = signal + with pytest.raises(ValueError, match="Datatypes"): + signal = pf.Signal(np.random.rand(100), 44100, dtype=np.int32) + pb.output_signal = signal + + @pytest.mark.parametrize( + "data, repetitions, expected", + [(np.array([[0., 1., 2.]]), 1, np.array([[0., 1., 2.]])), + (np.array([[0., 1., 2.]]), 1.5, np.array([[0., 1., 2., 0.]])), + (np.array([[0., 1., 2.]]), 1.7, np.array([[0., 1., 2., 0., 1.]])), + (np.array([[0., 1., 2., 3.]]), 1.5, + np.array([[0., 1., 2., 3., 0., 1.]])), + (np.array([[0, 1, 2], [3, 4, 5]]), 1.5, + np.array([[0., 1., 2., 0.], [3., 4., 5., 3.]]))]) + def test_start(self, data, repetitions, expected): + device = hp.AudioDevice() + signal = pf.Signal(data, 44100) + channels = np.arange(data.shape[0]) + pb = Playback(device, channels, repetitions, signal) + with mock.patch.object( + device, 'playback', + new=lambda x: npt.assert_array_equal(x, expected)): + pb.start() + + def test_start_missing_signal(self): + device = hp.AudioDevice() + pb = Playback(device, 0) + with pytest.raises(ValueError, match="set an output signal"): + pb.start() def test_stop(self): pass @@ -114,6 +148,19 @@ def test_repetitions(self): pass +@mock.patch('haiopy.devices._Device', autospec=True) +@mock.patch('haiopy.AudioDevice', autospec=True) +class TestPatching: + @mock.patch('__main__.isinstance', return_value=True) + def test_init(self, II, AD, DE): + device = hp.AudioDevice(0, 44100, 0, 0) + output_channels = 0 + Playback(device, output_channels) + +@mock.patch('__main__.isinstance', return_value=True) +def test_instance(II): + assert isinstance('a', int) + class TestRecord: def test_init_device_error(self): device = 0 From fb5369f7afd9a1322e37b6752ad064e26eca6dcb Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Fri, 8 Oct 2021 15:54:09 +0200 Subject: [PATCH 5/8] removed mock_utils, added conftest.py --- haiopy/mock_utils.py | 107 --------------------------------------- tests/conftest.py | 11 ++++ tests/test_mock_utils.py | 22 -------- 3 files changed, 11 insertions(+), 129 deletions(-) delete mode 100644 haiopy/mock_utils.py create mode 100644 tests/conftest.py delete mode 100644 tests/test_mock_utils.py diff --git a/haiopy/mock_utils.py b/haiopy/mock_utils.py deleted file mode 100644 index e63827d..0000000 --- a/haiopy/mock_utils.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Provides mocks of common haiopy classes, which help to test the hardware -communication. -""" -import numpy as np -from unittest import mock - - -class _Device(mock.Mock): - def __init__( - self, - id=0, - sampling_rate=44100, - block_size=2**5, - dtype=np.float64): - super().__init__() - self._sampling_rate = sampling_rate - self._dtype = dtype - - @property - def name(self): - raise NotImplementedError('Abstract method') - - @property - def sampling_rate(self): - return self._sampling_rate - - @property - def dtype(self): - return self._dtype - - def playback(): - pass - - def record(): - pass - - def playback_record(): - pass - - def initialize_playback(): - pass - - def initialize_record(): - pass - - def initialize_playback_record(): - pass - - def abort(): - pass - - -class AudioDevice(_Device, mock.Mock): - def __init__( - self, - id=0, - sampling_rate=44100, - block_size=2**5, - dtype=np.float64, - latency=None, - extra_settings=None, - # finished_callback=None, - clip_off=None, - dither_off=None, - never_drop_input=None, - prime_output_buffers_using_stream_callback=None - ): - super().__init__(id, sampling_rate, block_size, dtype) - - @property - def stream(): - pass - - @staticmethod - def callback(): - pass - - def playback(data): - # fill queue, stream.start() - pass - - def record(n_samples): - # stream start, read into the queue - pass - - def playback_record(data): - # see combination above - pass - - def initialize_playback(channels): - # init queue, define callback, init stream - pass - - def initialize_record(channels): - pass - - def initialize_playback_record(input_channels, output_channels): - pass - - def abort(): - # abort - pass - - def close(): - # remove stream - pass \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a945667 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import pytest +import numpy as np +from unittest import mock + +import haiopy as hp + + +@pytest.fixture +@mock.patch('haiopy.AudioDevice', autospec=True) +def device_stub(AudioDevice): + return hp.AudioDevice(0, 44100, 512, np.float) diff --git a/tests/test_mock_utils.py b/tests/test_mock_utils.py deleted file mode 100644 index 1560641..0000000 --- a/tests/test_mock_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -import haiopy.mock_utils as mock_utils -import haiopy.devices as devices - - -def test__Device_mock_properties(): - """ Test to check _Device mock initialization. - """ - mock_dir = dir(mock_utils._Device) - device_dir = dir(devices._Device) - - assert mock_dir.sort() == device_dir.sort() - - -def test_AudioDevice_mock_properties(): - """ Test to check AudioDevice mock initialization. - """ - mock_dir = dir(mock_utils.AudioDevice) - device_dir = dir(devices.AudioDevice) - - assert mock_dir.sort() == device_dir.sort() - - From 7b63e4b7692207d5c2a3f138f142a17c084a9d8d Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Fri, 8 Oct 2021 15:56:44 +0200 Subject: [PATCH 6/8] reworked tests with patch, stubs --- haiopy/io.py | 44 +++++++++-- tests/test_io.py | 186 ++++++++++++++++++++++++++++------------------- 2 files changed, 149 insertions(+), 81 deletions(-) diff --git a/haiopy/io.py b/haiopy/io.py index a8d1429..8b2ed15 100644 --- a/haiopy/io.py +++ b/haiopy/io.py @@ -17,13 +17,27 @@ class _AudioIO(ABC): three sub-classes :py:class:`Playback`, :py:class:`Record`, and :py:class:`PlaybackRecord`. """ - def __init__(self, device): + def __init__(self, device, blocking=False): if isinstance(device, devices._Device): self._device = device + self.blocking = blocking else: raise ValueError("Incorrect device, needs to be a" ":py:class:`~haiopy.AudioDevice` object.") + @property + def blocking(self): + """Boolean parameter blocking.""" + return self._blocking + + @blocking.setter + def blocking(self, value): + """Set blocking parameter to True or False.""" + if isinstance(value, bool): + self._blocking = value + else: + raise ValueError("Blocking needs to be True or False.") + @abstractmethod def start(): """ This function depends on the use case (playback, recording or @@ -31,9 +45,9 @@ def start(): """ pass - def stop(device): - """ Immediately terminate the playback/recording.""" - device.abort() + def stop(self): + """Immediately terminate the playback/recording.""" + self._device.abort() print("Playback / Recording terminated.") def wait(): @@ -45,8 +59,8 @@ class Playback(_AudioIO): """ def __init__( self, device, output_channels, repetitions=1, - output_signal=None): - """Create a Playback object. + output_signal=None, digital_level=0.): + """[summary] Parameters ---------- @@ -56,12 +70,17 @@ def __init__( [description] repetitions : int, optional [description], by default 1 + output_signal : [type], optional + [description], by default None + digital_level : [type], optional + [description], by default 0. """ super().__init__(device=device) # Set properties, check implicitly self.output_channels = output_channels self.repetitions = repetitions self.output_signal = output_signal + self.digital_level = digital_level # Initialize device self.device.initialize_playback(self.output_channels) @@ -94,8 +113,8 @@ def output_signal(self, signal): f"do not match.") elif signal.cshape != self.output_channels.shape: raise ValueError( - f"The cshape of the signal ({signal.cshape}) " - f"and the number of channels ({self.output_channels.shape}) " + f"The shapes of the signal ({signal.cshape}) " + f"and the channels ({self.output_channels.shape}) " f"do not match.") else: self._output_signal = signal @@ -139,6 +158,14 @@ def repetitions(self, value): else: raise ValueError("Repetitions must be positive.") + @property + def digital_level(self): + return self._digital_level + + @digital_level.setter + def digital_level(self, value): + self._digital_level = value + def start(self): """Start the playback.""" if self.output_signal is None: @@ -151,6 +178,7 @@ def start(self): data_out = np.tile(data, int(self.repetitions)) data_out = np.append(data_out, data[..., :append_idx], axis=-1) self.device.playback(data_out) + # TO DO: blocking, digital level class Record(_AudioIO): diff --git a/tests/test_io.py b/tests/test_io.py index 1ef931c..a209277 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,52 +1,47 @@ -import unittest -import haiopy as hp import pytest import numpy as np import pyfar as pf import numpy.testing as npt -import unittest from unittest import mock -from haiopy import mock_utils from haiopy import io from haiopy import Playback from haiopy import Record from haiopy import PlaybackRecord -@mock.patch('haiopy.devices._Device', new=mock_utils._Device) -@mock.patch('haiopy.AudioDevice', new=mock_utils.AudioDevice) class TestPlayback(): - def test__AudioIO_init_error(self): + @mock.patch('haiopy.devices._Device', autospec=True) + def test__AudioIO_init_error(self, de): """ Test error for instatiating abstract class""" with pytest.raises(TypeError): io._AudioIO(0) - def test_init(self): - device = hp.AudioDevice() + @mock.patch('haiopy.io.isinstance', return_value=True) + def test_init(self, isinstance_mock, device_stub): output_channels = 0 - Playback(device, output_channels) + Playback(device_stub, output_channels) - def test_init_default_parameters(self): - device = hp.AudioDevice() + def test_init_default_parameters(self, device_stub): output_channels = 0 - playback = Playback(device, output_channels) - assert playback.device == device - assert playback.output_channels == output_channels - assert playback.repetitions == 1 - assert playback.output_signal is None - - def test_init_set_parameters(self): - device = hp.AudioDevice() + pb = Playback(device_stub, output_channels) + assert pb._device == device_stub + assert pb._output_channels == output_channels + assert pb._repetitions == 1 + assert pb._output_signal is None + + def test_init_set_parameters(self, device_stub): output_channels = 2 repetitions = 3 signal = pf.signals.sine(100, 100) - playback = Playback( - device, output_channels, repetitions, signal) - assert playback.output_channels == output_channels - assert playback.repetitions == repetitions - assert playback.output_signal == signal + device_stub.sampling_rate = signal.sampling_rate + device_stub.dtype = signal.dtype + pb = Playback(device_stub, output_channels, repetitions, signal) + assert pb._device == device_stub + assert pb._output_channels == output_channels + assert pb._repetitions == repetitions + assert pb._output_signal == signal def test_init_device_error(self): device = 0 @@ -54,37 +49,50 @@ def test_init_device_error(self): with pytest.raises(ValueError, match="Incorrect device"): Playback(device, channels) - def test_set_output_channels(self): - device = hp.AudioDevice() - pb = Playback(device, 0) + def test_device_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._device = 1 + assert pb.device == 1 + + def test_output_channels_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._output_channels = 1 + assert pb.output_channels == 1 + + def test_output_channels_setter(self, device_stub): + pb = Playback(device_stub, 0) # Check allowed input formats for ch in [1, [1, 2], np.array([1, 2]), (1, 2)]: pb.output_channels = ch - assert np.all(pb.output_channels == ch) - assert len(pb.output_channels.shape) == 1 + assert np.all(pb._output_channels == ch) + assert len(pb._output_channels.shape) == 1 - def test_set_output_channels_errors(self): - device = hp.AudioDevice() - # Error for non integer input + def test_output_channels_errors(self, device_stub): + # non integer input for ch in [1.1, [1.1, 2], np.array([1.1, 2]), (1.1, 2)]: with pytest.raises(ValueError, match="integers"): - Playback(device, ch) - # Error for array which is not 2D + Playback(device_stub, ch) + # array which is not 2D ch = np.array([[1, 2], [3, 4]]) with pytest.raises(ValueError, match="1D array"): - Playback(device, ch) - # Error for non unique values + Playback(device_stub, ch) + # non unique values for ch in [[1, 1], np.array([1, 1]), (1, 1)]: with pytest.raises(ValueError, match="unique"): - Playback(device, ch) + Playback(device_stub, ch) - def test_set_repetitions(self): - pb = Playback(hp.AudioDevice(), 0) - pb.repetitions = 2 + def test_repetitions_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._repetitions = 2 assert pb.repetitions == 2 - def test_set_repetitions_errors(self): - pb = Playback(hp.AudioDevice(), 0) + def test_repetitions_setter(self, device_stub): + pb = Playback(device_stub, 0) + pb.repetitions = 2 + assert pb._repetitions == 2 + + def test_repetitions_errors(self, device_stub): + pb = Playback(device_stub, 0) for value in ['a', [1], np.array([1, 2])]: with pytest.raises(ValueError, match="scalar number"): pb.repetitions = value @@ -92,26 +100,68 @@ def test_set_repetitions_errors(self): with pytest.raises(ValueError, match="positive"): pb.repetitions = value - def test_output_signal_setter(self): + def test_output_signal_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._output_signal = 1 + assert pb.output_signal == 1 + + def test_output_signal_setter(self, device_stub): + # One channel signal = pf.signals.sine(100, 100) - pb = Playback(hp.AudioDevice(), 0) + device_stub.sampling_rate = signal.sampling_rate + device_stub.dtype = signal.dtype + pb = Playback(device_stub, 0) pb.output_signal = signal - + assert pb._output_signal == signal + # Two channels signal = pf.signals.sine([100, 200], 100) - pb = Playback(hp.AudioDevice(), [0, 1]) + pb = Playback(device_stub, [0, 1]) pb.output_signal = signal + assert pb._output_signal == signal - def test_output_signal_setter_errors(self): - device = hp.AudioDevice(sampling_rate=44100, dtype=np.float64) - pb = Playback(device, 0) + def test_output_signal_errors(self, device_stub): + device_stub.sampling_rate = 44100 + device_stub.dtype = np.float64 + pb = Playback(device_stub, 0) + # Signal with pytest.raises(ValueError, match="pyfar.Signal"): pb.output_signal = 1 + # sampling_rate with pytest.raises(ValueError, match="Sampling rate"): - signal = pf.Signal(np.random.rand(100), 1000, dtype=np.float64) + signal = pf.Signal( + np.random.rand(100), 1000, dtype=device_stub.dtype) pb.output_signal = signal + # dtype with pytest.raises(ValueError, match="Datatypes"): signal = pf.Signal(np.random.rand(100), 44100, dtype=np.int32) pb.output_signal = signal + # shape + with pytest.raises(ValueError, match="shapes"): + signal = pf.Signal( + np.array([[1, 2, 3], [4, 5, 6]]), 44100) + pb.output_signal = signal + with pytest.raises(ValueError, match="shapes"): + pb = Playback(device_stub, [0, 1]) + signal = pf.Signal(np.array([[1, 2, 3]]), 44100) + pb.output_signal = signal + + def test_blocking_getter(self): + pass + + def test_blocking_setter(self): + pass + + def test_blocking_errors(self): + pass + + def test_digital_level_getter(self): + pass + + def test_digital_levelsetter(self): + pass + + def test_digital_level_errors(self): + pass @pytest.mark.parametrize( "data, repetitions, expected", @@ -122,24 +172,27 @@ def test_output_signal_setter_errors(self): np.array([[0., 1., 2., 3., 0., 1.]])), (np.array([[0, 1, 2], [3, 4, 5]]), 1.5, np.array([[0., 1., 2., 0.], [3., 4., 5., 3.]]))]) - def test_start(self, data, repetitions, expected): - device = hp.AudioDevice() + def test_start(self, device_stub, data, repetitions, expected): signal = pf.Signal(data, 44100) channels = np.arange(data.shape[0]) - pb = Playback(device, channels, repetitions, signal) + device_stub.sampling_rate = signal.sampling_rate + device_stub.dtype = signal.dtype + pb = Playback(device_stub, channels, repetitions, signal) with mock.patch.object( - device, 'playback', + device_stub, 'playback', new=lambda x: npt.assert_array_equal(x, expected)): pb.start() - def test_start_missing_signal(self): - device = hp.AudioDevice() - pb = Playback(device, 0) + def test_start_missing_signal(self, device_stub): + pb = Playback(device_stub, 0) with pytest.raises(ValueError, match="set an output signal"): pb.start() - def test_stop(self): - pass + def test_stop(self, device_stub): + pb = Playback(device_stub, 0) + with mock.patch.object(device_stub, 'abort') as abort_mock: + pb.stop() + abort_mock.assert_called_with() def test_wait(self): pass @@ -148,19 +201,6 @@ def test_repetitions(self): pass -@mock.patch('haiopy.devices._Device', autospec=True) -@mock.patch('haiopy.AudioDevice', autospec=True) -class TestPatching: - @mock.patch('__main__.isinstance', return_value=True) - def test_init(self, II, AD, DE): - device = hp.AudioDevice(0, 44100, 0, 0) - output_channels = 0 - Playback(device, output_channels) - -@mock.patch('__main__.isinstance', return_value=True) -def test_instance(II): - assert isinstance('a', int) - class TestRecord: def test_init_device_error(self): device = 0 From 24f32cd5e301a9ec9a81cc7a70b4179d5ab9efc5 Mon Sep 17 00:00:00 2001 From: sikersten Date: Mon, 11 Oct 2021 16:01:52 +0200 Subject: [PATCH 7/8] implemented blocking and digital level --- haiopy/io.py | 29 ++++++++--- tests/test_io.py | 124 +++++++++++++++++++++++++++++++---------------- 2 files changed, 104 insertions(+), 49 deletions(-) diff --git a/haiopy/io.py b/haiopy/io.py index 8b2ed15..c05fe8a 100644 --- a/haiopy/io.py +++ b/haiopy/io.py @@ -17,7 +17,7 @@ class _AudioIO(ABC): three sub-classes :py:class:`Playback`, :py:class:`Record`, and :py:class:`PlaybackRecord`. """ - def __init__(self, device, blocking=False): + def __init__(self, device, blocking): if isinstance(device, devices._Device): self._device = device self.blocking = blocking @@ -50,7 +50,7 @@ def stop(self): self._device.abort() print("Playback / Recording terminated.") - def wait(): + def wait(self): pass @@ -59,7 +59,7 @@ class Playback(_AudioIO): """ def __init__( self, device, output_channels, repetitions=1, - output_signal=None, digital_level=0.): + output_signal=None, digital_level=0., blocking=False): """[summary] Parameters @@ -75,7 +75,7 @@ def __init__( digital_level : [type], optional [description], by default 0. """ - super().__init__(device=device) + super().__init__(device=device, blocking=blocking) # Set properties, check implicitly self.output_channels = output_channels self.repetitions = repetitions @@ -139,6 +139,9 @@ def output_channels(self, channels): else: self._output_channels = channels_int + # Initialize device + self.device.initialize_playback(self._output_channels) + @property def repetitions(self): """Number of repetitions of the playback.""" @@ -160,11 +163,21 @@ def repetitions(self, value): @property def digital_level(self): + """Digital output level in dB.""" return self._digital_level @digital_level.setter def digital_level(self, value): - self._digital_level = value + """Set the digital output level in dB. The level is referenced to an + amplitude of 1, so only levels <= 0 dB can be set.""" + try: + level = float(value) + except (ValueError, TypeError): + raise ValueError("The digital level must be single number.") + if level <= 0: + self._digital_level = level + else: + raise ValueError("The digital level must be <= 0.") def start(self): """Start the playback.""" @@ -173,12 +186,16 @@ def start(self): "signal.") # Extract time data data = self.output_signal.time + # Amplification / Attenuations + data = data * 10**(self._digital_level/20) # Repeat and append append_idx = int(self.repetitions % 1 * self.output_signal.n_samples) data_out = np.tile(data, int(self.repetitions)) data_out = np.append(data_out, data[..., :append_idx], axis=-1) self.device.playback(data_out) - # TO DO: blocking, digital level + # Block + if self._blocking: + pass class Record(_AudioIO): diff --git a/tests/test_io.py b/tests/test_io.py index a209277..f3cc87a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -13,7 +13,7 @@ class TestPlayback(): @mock.patch('haiopy.devices._Device', autospec=True) - def test__AudioIO_init_error(self, de): + def test__AudioIO_init_error(self, device_stub): """ Test error for instatiating abstract class""" with pytest.raises(TypeError): io._AudioIO(0) @@ -30,18 +30,25 @@ def test_init_default_parameters(self, device_stub): assert pb._output_channels == output_channels assert pb._repetitions == 1 assert pb._output_signal is None + assert pb._blocking is False def test_init_set_parameters(self, device_stub): output_channels = 2 repetitions = 3 signal = pf.signals.sine(100, 100) + digital_level = -10 + blocking = True device_stub.sampling_rate = signal.sampling_rate device_stub.dtype = signal.dtype - pb = Playback(device_stub, output_channels, repetitions, signal) + pb = Playback( + device_stub, output_channels, repetitions, signal, + digital_level, blocking) assert pb._device == device_stub assert pb._output_channels == output_channels assert pb._repetitions == repetitions assert pb._output_signal == signal + assert pb._digital_level == digital_level + assert pb._blocking is blocking def test_init_device_error(self): device = 0 @@ -59,27 +66,30 @@ def test_output_channels_getter(self, device_stub): pb._output_channels = 1 assert pb.output_channels == 1 - def test_output_channels_setter(self, device_stub): + @pytest.mark.parametrize("channels", [1, [1, 2], np.array([1, 2]), (1, 2)]) + def test_output_channels_setter(self, device_stub, channels): pb = Playback(device_stub, 0) - # Check allowed input formats - for ch in [1, [1, 2], np.array([1, 2]), (1, 2)]: - pb.output_channels = ch - assert np.all(pb._output_channels == ch) - assert len(pb._output_channels.shape) == 1 + pb.output_channels = channels + assert np.all(pb._output_channels == channels) + assert len(pb._output_channels.shape) == 1 - def test_output_channels_errors(self, device_stub): + @pytest.mark.parametrize( + "channels", [1.1, [1.1, 2], np.array([1.1, 2]), (1.1, 2)]) + def test_output_channels_setter_errors(self, device_stub, channels): # non integer input - for ch in [1.1, [1.1, 2], np.array([1.1, 2]), (1.1, 2)]: - with pytest.raises(ValueError, match="integers"): - Playback(device_stub, ch) + with pytest.raises(ValueError, match="integers"): + Playback(device_stub, channels) + + def test_output_channels_setter_errors_2D(self, device_stub): # array which is not 2D ch = np.array([[1, 2], [3, 4]]) with pytest.raises(ValueError, match="1D array"): Playback(device_stub, ch) - # non unique values - for ch in [[1, 1], np.array([1, 1]), (1, 1)]: - with pytest.raises(ValueError, match="unique"): - Playback(device_stub, ch) + + @pytest.mark.parametrize("channels", [[1, 1], np.array([1, 1]), (1, 1)]) + def test_output_channels_setter_errors_unique(self, device_stub, channels): + with pytest.raises(ValueError, match="unique"): + Playback(device_stub, channels) def test_repetitions_getter(self, device_stub): pb = Playback(device_stub, 0) @@ -91,14 +101,17 @@ def test_repetitions_setter(self, device_stub): pb.repetitions = 2 assert pb._repetitions == 2 - def test_repetitions_errors(self, device_stub): + @pytest.mark.parametrize("reps", ['a', [1], np.array([1, 2])]) + def test_repetitions_setter_errors_scalar_number(self, device_stub, reps): pb = Playback(device_stub, 0) - for value in ['a', [1], np.array([1, 2])]: - with pytest.raises(ValueError, match="scalar number"): - pb.repetitions = value - for value in [0, -1]: - with pytest.raises(ValueError, match="positive"): - pb.repetitions = value + with pytest.raises(ValueError, match="scalar number"): + pb.repetitions = reps + + @pytest.mark.parametrize("reps", [0, -1]) + def test_repetitions_setter_errors_positive(self, device_stub, reps): + pb = Playback(device_stub, 0) + with pytest.raises(ValueError, match="positive"): + pb.repetitions = reps def test_output_signal_getter(self, device_stub): pb = Playback(device_stub, 0) @@ -119,7 +132,7 @@ def test_output_signal_setter(self, device_stub): pb.output_signal = signal assert pb._output_signal == signal - def test_output_signal_errors(self, device_stub): + def test_output_signal_setter_errors(self, device_stub): device_stub.sampling_rate = 44100 device_stub.dtype = np.float64 pb = Playback(device_stub, 0) @@ -145,23 +158,41 @@ def test_output_signal_errors(self, device_stub): signal = pf.Signal(np.array([[1, 2, 3]]), 44100) pb.output_signal = signal - def test_blocking_getter(self): - pass + def test_blocking_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._blocking = 1 + assert pb.blocking == 1 - def test_blocking_setter(self): - pass + def test_blocking_setter(self, device_stub): + pb = Playback(device_stub, 0) + pb.blocking = True + assert pb._blocking is True - def test_blocking_errors(self): - pass + def test_blocking_setter_errors(self, device_stub): + pb = Playback(device_stub, 0) + with pytest.raises(ValueError, match="True or False"): + pb.blocking = 1 - def test_digital_level_getter(self): - pass + def test_digital_level_getter(self, device_stub): + pb = Playback(device_stub, 0) + pb._digital_level = 'a' + assert pb.digital_level == 'a' - def test_digital_levelsetter(self): - pass + def test_digital_level_setter(self, device_stub): + pb = Playback(device_stub, 0) + pb.digital_level = -10 + assert pb._digital_level == -10 - def test_digital_level_errors(self): - pass + @pytest.mark.parametrize("level", ['a', (1, 2), [1, 2], np.array([1, 2])]) + def test_digital_level_setter_errors_number(self, device_stub, level): + pb = Playback(device_stub, 0) + with pytest.raises(ValueError, match="single number"): + pb.digital_level = level + + def test_digital_level_setter_errors_positive(self, device_stub): + pb = Playback(device_stub, 0) + with pytest.raises(ValueError, match="<= 0"): + pb.digital_level = 10 @pytest.mark.parametrize( "data, repetitions, expected", @@ -172,7 +203,7 @@ def test_digital_level_errors(self): np.array([[0., 1., 2., 3., 0., 1.]])), (np.array([[0, 1, 2], [3, 4, 5]]), 1.5, np.array([[0., 1., 2., 0.], [3., 4., 5., 3.]]))]) - def test_start(self, device_stub, data, repetitions, expected): + def test_start_repetitions(self, device_stub, data, repetitions, expected): signal = pf.Signal(data, 44100) channels = np.arange(data.shape[0]) device_stub.sampling_rate = signal.sampling_rate @@ -183,6 +214,19 @@ def test_start(self, device_stub, data, repetitions, expected): new=lambda x: npt.assert_array_equal(x, expected)): pb.start() + def test_start_digital_level(self, device_stub): + signal = pf.Signal([1, 2, 3], 44100) + device_stub.sampling_rate = signal.sampling_rate + device_stub.dtype = signal.dtype + pb = Playback( + device=device_stub, output_channels=0, repetitions=2, + output_signal=signal, digital_level=-20) + expected = np.array([[.1, .2, .3, .1, .2, .3]]) + with mock.patch.object( + device_stub, 'playback', + new=lambda x: npt.assert_allclose(x, expected, atol=1e-15)): + pb.start() + def test_start_missing_signal(self, device_stub): pb = Playback(device_stub, 0) with pytest.raises(ValueError, match="set an output signal"): @@ -194,12 +238,6 @@ def test_stop(self, device_stub): pb.stop() abort_mock.assert_called_with() - def test_wait(self): - pass - - def test_repetitions(self): - pass - class TestRecord: def test_init_device_error(self): From 767a25a6e8a56ec691742b11e6ed5c88630453fa Mon Sep 17 00:00:00 2001 From: Simon Kersten Date: Tue, 12 Oct 2021 13:19:36 +0200 Subject: [PATCH 8/8] added blocking, wait tests --- haiopy/io.py | 42 +++++++++++++++++++++++++++--------------- tests/test_io.py | 11 ++++++++++- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/haiopy/io.py b/haiopy/io.py index c05fe8a..2d8595a 100644 --- a/haiopy/io.py +++ b/haiopy/io.py @@ -51,7 +51,8 @@ def stop(self): print("Playback / Recording terminated.") def wait(self): - pass + """Wait until playback/recording is finished.""" + self._device.wait() class Playback(_AudioIO): @@ -60,20 +61,27 @@ class Playback(_AudioIO): def __init__( self, device, output_channels, repetitions=1, output_signal=None, digital_level=0., blocking=False): - """[summary] + """Create a Playback object. Parameters ---------- - device : [type] - [description] - output_channels : [type] - [description] + device : haiopy.AudioDevice + The device to play the signal. + output_channels : array-like + The output channels. The parameter can be a single number, list, + tuple or a 1D array with unique values. repetitions : int, optional - [description], by default 1 - output_signal : [type], optional - [description], by default None - digital_level : [type], optional - [description], by default 0. + Number of repitions, the default ``1``. + output_signal : pyfar.Signal, optional + The signal to be played. The default ``None``, requires the signal + to be set before :py:func:`~play` is called. + digital_level : float, optional + Digital output level (the digital output amplification) in dB + referenced to an amplitude of 1, so only levels <= 0 dB can be set. + The default is ``0``, which results in an unmodified playback. + blocking : bool, optional + If ``True`` :py:func:`~play` function doesn’t return until the + playback is finished. The default is ``False``. """ super().__init__(device=device, blocking=blocking) # Set properties, check implicitly @@ -195,7 +203,7 @@ def start(self): self.device.playback(data_out) # Block if self._blocking: - pass + self.wait() class Record(_AudioIO): @@ -205,8 +213,9 @@ def __init__( input_channels, duration=None, fft_norm='amplitude', + blocking=False ): - super().__init__(device=device) + super().__init__(device=device, blocking=blocking) self._input_signal = None self.input_channels = input_channels @@ -235,15 +244,18 @@ def __init__( device, input_channels, output_channels, + blocking=False ): Record.__init__( self, device=device, - input_channels=input_channels) + input_channels=input_channels, + blocking=blocking) Playback.__init__( self, device=device, - output_channels=output_channels) + output_channels=output_channels, + blocking=blocking) def start(): """ This function depends on the use case (playback, recording or diff --git a/tests/test_io.py b/tests/test_io.py index f3cc87a..1b862ab 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -232,11 +232,20 @@ def test_start_missing_signal(self, device_stub): with pytest.raises(ValueError, match="set an output signal"): pb.start() + def test_start_blocking(self, device_stub): + signal = pf.Signal([1, 2, 3], 44100) + device_stub.sampling_rate = signal.sampling_rate + device_stub.dtype = signal.dtype + pb = Playback(device_stub, 0, output_signal=signal, blocking=True) + with mock.patch.object(device_stub, 'wait') as wait_mock: + pb.start() + wait_mock.assert_called_once() + def test_stop(self, device_stub): pb = Playback(device_stub, 0) with mock.patch.object(device_stub, 'abort') as abort_mock: pb.stop() - abort_mock.assert_called_with() + abort_mock.assert_called_once() class TestRecord: