diff --git a/README.md b/README.md index 5790495..1ff4ef7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # pyCamillaDSP Companion Python library for CamillaDSP. -Works with CamillaDSP version 0.5.0 and up. +Works with CamillaDSP version 0.6.0 and up. Download the library, either by `git clone` or by downloading a zip file of the code. Then unpack the files, go to the folder containing the `setup.py` file and run: ```sh @@ -87,9 +87,11 @@ The CamillaConnection class provides the following methods |`connect()` | Connect to the Websocket server. Must be called before any other method can be used.| |`disconnect()` | Close the connection to the websocket.| |`is_connected()` | Is websocket connected? Returns True or False.| -|`get_version()` | Read CamillaDSP version, returns a tuple with 3 elements| -|`get_library_version()` | Read pyCamillaDSP version, returns a tuple with 3 elements| -|`get_state()` | Get current processing state. Returns one of "RUNNING", "PAUSED" or "INACTIVE".| +|`get_version()` | Read CamillaDSP version, returns a tuple with 3 elements.| +|`get_library_version()` | Read pyCamillaDSP version, returns a tuple with 3 elements.| +|`get_state()` | Get current processing state. Returns a ProcessingState enum value, see "Enums" below.| +|`get_stop_reason()` | Get the reason that processing stopped. Returns a StopReason enum value, see "Enums" below. | +|`def get_supported_device_types()`| Read what device types the running CamillaDSP process supports. Returns a tuple with two lists of device types, the first for playback and the second for capture. | |`stop()` | Stop processing and wait for new config if wait mode is active, else exit. | |`exit()` | Stop processing and exit.| @@ -100,13 +102,14 @@ The CamillaConnection class provides the following methods |`reload()` | Reload config from disk.| |`get_config_name()` | Get path to current config file.| |`set_config_name(value)` | Set path to config file.| -|`get_config_raw()` | Get the active configuation in yaml format as a string.| -|`set_config_raw(value)` | Upload a new configuation in yaml format as a string.| -|`get_config()` | Get the active configuation as an object.| -|`set_config(config)` | Upload a new configuation from an object.| +|`get_config_raw()` | Get the active configuration in yaml format as a string.| +|`set_config_raw(value)` | Upload a new configuration in yaml format as a string.| +|`get_config()` | Get the active configuration as an object.| +|`get_previous_config()` | Get the previously active configuration as an object.| +|`set_config(config)` | Upload a new configuration from an object.| |`validate_config(config)` | Validate a configuration object. Returns the validated config with all optional fields filled with defaults. Raises a CamillaError on errors.| |`read_config_file(path)` | Read a config file from `path`. Returns the loaded config with all optional fields filled with defaults. Raises a CamillaError on errors.| -|`read_config(config)` | Read a config from yaml string and return the contents as an obect, with defaults filled out with their default values.| +|`read_config(config)` | Read a config from yaml string and return the contents as an object, with defaults filled out with their default values.| ### Reading status | Method | Description | @@ -118,7 +121,7 @@ The CamillaConnection class provides the following methods |`get_capture_signal_peak()` | Get capture signal level peak in dB. Full scale is 0 dB. Returns a list with one element per channel.| |`get_playback_signal_peak()` | Get playback signal level peak in dB. Full scale is 0 dB. Returns a list with one element per channel.| |`get_capture_rate_raw()` | Get current capture rate, raw value.| -|`get_capture_rate()` | Get current capture rate. Returns the nearest common value.| +|`get_capture_rate()` | Get current capture rate. Returns the nearest common rate, as long as it's within +-4% of the measured value.| |`get_update_interval()` | Get current update interval in ms.| |`set_update_interval(value)` | Set current update interval in ms.| |`get_rate_adjust()` | Get current value for rate adjust.| @@ -133,6 +136,36 @@ The CamillaConnection class provides the following methods |`get_mute()` | Get current mute setting.| |`set_mute(value)` | Set mute, true or false.| + +## Enums + +### ProcessingState +- RUNNING: Processing is running. +- PAUSED: Processing is paused. +- INACTIVE: CamillaDSP is inactive, and waiting for a new config to be supplied. +- STARTING: The processing is being set up. + +### StopReason +- NONE: Processing hasn't stopped yet. +- DONE: The capture device reached the end of the stream. +- CAPTUREERROR: The capture device encountered an error. +- PLAYBACKERROR: The playback device encountered an error. +- CAPTUREFORMATCHANGE: The sample format of the capture device changed. +- PLAYBACKFORMATCHANGE: The sample format of the capture device changed. + +The StopReason enums also carry additional data: +- CAPTUREERROR and PLAYBACKERROR: Carries the error message as a string. +- CAPTUREFORMATCHANGE and PLAYBACKFORMATCHANGE: Carries the estimated new sample rate as an integer. A value of 0 means the new rate is unknown. + +The additional data can be accessed by reading the `data` property: +```python +reason = cdsp.get_stop_reason() +if reason == StopReason.CAPTUREERROR: + error_msg = reason.data + print(f"Capture failed, error: {error_msg}) +``` + + # Included examples: ## read_rms @@ -141,6 +174,12 @@ Read the playback signal level continuously and print in the terminal, until sto python read_rms.py 1234 ``` +## get_config +Read the configuration and print some parameters. +```sh +python get_config.py 1234 +``` + ## set_volume Set the volume control to a new value. First argument is websocket port, second is new volume in dB. For this to work, CamillaDSP must be running a configuration that has Volume filters in the pipeline for every channel. diff --git a/camilladsp/__init__.py b/camilladsp/__init__.py index 0a95644..bc33ba3 100644 --- a/camilladsp/__init__.py +++ b/camilladsp/__init__.py @@ -1 +1 @@ -from camilladsp.camilladsp import CamillaConnection, CamillaError +from camilladsp.camilladsp import CamillaConnection, CamillaError, ProcessingState, StopReason diff --git a/camilladsp/camilladsp.py b/camilladsp/camilladsp.py index fc72fc3..5a94031 100644 --- a/camilladsp/camilladsp.py +++ b/camilladsp/camilladsp.py @@ -3,8 +3,9 @@ from websocket import create_connection import math from threading import Lock +from enum import Enum, auto -VERSION = (0, 5, 1) +VERSION = (0, 6, 0) STANDARD_RATES = [ 8000, @@ -22,6 +23,70 @@ 384000, ] +class ProcessingState(Enum): + RUNNING = auto() + PAUSED = auto() + INACTIVE = auto() + STARTING = auto() + +def _state_from_string(value): + if value == "Running": + return ProcessingState.RUNNING + elif value == "Paused": + return ProcessingState.PAUSED + elif value == "Inactive": + return ProcessingState.INACTIVE + elif value == "Starting": + return ProcessingState.STARTING + return None + + +class StopReason(Enum): + NONE = auto() + DONE = auto() + CAPTUREERROR = auto() + PLAYBACKERROR = auto() + CAPTUREFORMATCHANGE = auto() + PLAYBACKFORMATCHANGE = auto() + + def __new__(cls, value): + obj = object.__new__(cls) + obj._value_ = value + obj._data = None + return obj + + def set_data(self, value): + self._data = value + + @property + def data(self): + return self._data + +def _reason_from_reply(value): + if isinstance(value, dict): + reason, data = next(iter(value.items())) + else: + reason = value + data = None + + if reason == "None": + reasonenum = StopReason.NONE + elif reason == "Done": + reasonenum = StopReason.DONE + elif reason == "CaptureError": + reasonenum = StopReason.CAPTUREERROR + elif reason == "PlaybackError": + reasonenum = StopReason.PLAYBACKERROR + elif reason == "CaptureFormatChange": + reasonenum = StopReason.CAPTUREFORMATCHANGE + elif reason == "PlaybackFormatChange": + reasonenum = StopReason.PLAYBACKFORMATCHANGE + else: + raise ValueError(f"Invalid value for StopReason: {value}") + reasonenum.set_data(data) + return reasonenum + + class CamillaError(ValueError): """ @@ -123,6 +188,14 @@ def get_version(self): """Read CamillaDSP version, returns a tuple of (major, minor, patch).""" return self._version + def get_supported_device_types(self): + """ + Read what device types the running CamillaDSP process supports. + Returns a tuple with two lists of device types, the first for playback and the second for capture. + """ + (playback, capture) = self._query("GetSupportedDeviceTypes") + return (playback, capture) + def get_library_version(self): """Read pycamilladsp version, returns a tuple of (major, minor, patch).""" return VERSION @@ -132,7 +205,14 @@ def get_state(self): Get current processing state. """ state = self._query("GetState") - return state + return _state_from_string(state) + + def get_stop_reason(self): + """ + Get current processing state. + """ + reason = self._query("GetStopReason") + return _reason_from_reply(reason) def get_signal_range(self): """ @@ -220,13 +300,14 @@ def get_capture_rate_raw(self): def get_capture_rate(self): """ - Get current capture rate. Returns the nearest common value. + Get current capture rate. Returns the nearest common rate, as long as it's within +-4% of the measured value. """ rate = self.get_capture_rate_raw() - if 0.9 * STANDARD_RATES[0] < rate < 1.1 * STANDARD_RATES[-1]: - return min(STANDARD_RATES, key=lambda val: abs(val - rate)) - else: - return None + if 0.96 * STANDARD_RATES[0] < rate < 1.04 * STANDARD_RATES[-1]: + nearest = min(STANDARD_RATES, key=lambda val: abs(val - rate)) + if 0.96 < rate/nearest < 1.04: + return nearest + return None def get_update_interval(self): """ @@ -314,6 +395,14 @@ def get_config(self): config_object = yaml.safe_load(config_string) return config_object + def get_previous_config(self): + """ + Get the previously active configuation as a Python object. + """ + config_string = self._query("GetPreviousConfig") + config_object = yaml.safe_load(config_string) + return config_object + def read_config(self, config_string): """ Read a config from yaml string and return the contents diff --git a/examples/get_config/get_config.py b/examples/get_config/get_config.py new file mode 100644 index 0000000..f9e8f3c --- /dev/null +++ b/examples/get_config/get_config.py @@ -0,0 +1,27 @@ +# play wav +from camilladsp import CamillaConnection +import sys +import time + +try: + port = int(sys.argv[1]) +except: + print("Usage: Start CamillaDSP with the websocket server enabled:") + print("> camilladsp -p4321 yourconfig.yml") + print("Then run this script to print some parameters from the active config.") + print("> python get_config.py 4321") + sys.exit() + +cdsp = CamillaConnection("127.0.0.1", port) +cdsp.connect() + +conf = cdsp.get_config() + +# Get some single parameters +print(f'Capture device type: {conf["devices"]["capture"]["type"]}') +print(f'Sample rate: {conf["devices"]["samplerate"]}') +print(f'Resampling enabled: {conf["devices"]["enable_resampling"]}') + +# Print the whole playback and capture devices +print(f'Capture device: {str(conf["devices"]["capture"])}') +print(f'Playback device: {str(conf["devices"]["playback"])}') diff --git a/examples/playwav/analyze_wav.py b/examples/playwav/analyze_wav.py index 683c95e..38a3349 100644 --- a/examples/playwav/analyze_wav.py +++ b/examples/playwav/analyze_wav.py @@ -1,4 +1,3 @@ -import os import struct import logging diff --git a/setup.py b/setup.py index 7edeff4..c3f9cd4 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,10 @@ setuptools.setup( name="camilladsp", - version="0.5.1", + version="0.6.0", author="Henrik Enquist", author_email="henrik.enquist@gmail.com", - description="A library for comminucating with CamillaDSP", + description="A library for communicating with CamillaDSP", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/HEnquist/pycamilladsp", diff --git a/tests/test_camillaws.py b/tests/test_camillaws.py index e500c42..0dd5279 100644 --- a/tests/test_camillaws.py +++ b/tests/test_camillaws.py @@ -1,3 +1,4 @@ +from camilladsp.camilladsp import StopReason import pytest from unittest.mock import MagicMock, patch import camilladsp @@ -10,14 +11,18 @@ def __init__(self): self.value = None responses = { - '"GetState"': json.dumps({"GetState": {"result": "Ok", "value": "IDLE"}}), + '"GetState"': json.dumps({"GetState": {"result": "Ok", "value": "Inactive"}}), '"GetVersion"': json.dumps({"GetVersion": {"result": "Ok", "value": "0.3.2"}}), + '"GetSupportedDeviceTypes"': json.dumps({"GetSupportedDeviceTypes": {"result": "Ok", "value": [["a", "b"], ["c", "d"]]}}), '"GetSignalRange"': json.dumps({"GetSignalRange": {"result": "Ok", "value": "0.2"}}), '"GetCaptureSignalRms"': json.dumps({"GetCaptureSignalRms": {"result": "Ok", "value": [0.1, 0.2]}}), '"GetCaptureRate"': json.dumps({"GetCaptureRate": {"result": "Ok", "value": "88250"}}), '"GetErrorValue"': json.dumps({"GetErrorValue": {"result": "Error", "value": "badstuff"}}), '"GetError"': json.dumps({"GetError": {"result": "Error"}}), '"Invalid"': json.dumps({"Invalid": {"result": "Error", "value": "badstuff"}}), + '"GetStopReason"': json.dumps({"GetStopReason": {"result": "Ok", "value": "Done"}}), + '"GetStopReason2"': json.dumps({"GetStopReason": {"result": "Ok", "value": {'CaptureFormatChange': 44098}}}), + '"GetStopReason3"': json.dumps({"GetStopReason": {"result": "Ok", "value": {'CaptureError': 'error error'}}}), '"NotACommand"': json.dumps({"Invalid": {"result": "Error"}}), '{"SetSomeValue": 123}': json.dumps({"SetSomeValue": {"result": "Ok"}}), '"nonsense"': "abcdefgh", @@ -81,7 +86,7 @@ def test_connect(camilla_mockws): camilla_mockws.get_state() camilla_mockws.connect() assert camilla_mockws.is_connected() - assert camilla_mockws.get_state() == "IDLE" + assert camilla_mockws.get_state() == camilladsp.ProcessingState.INACTIVE assert camilla_mockws.get_version() == ('0', '3', '2') assert camilla_mockws.get_library_version() == camilladsp.camilladsp.VERSION camilla_mockws.disconnect() @@ -91,6 +96,10 @@ def test_connect_fail(camilla): with pytest.raises(IOError): camilla.connect() +def test_device_types(camilla_mockws): + camilla_mockws.connect() + assert camilla_mockws.get_supported_device_types() == (["a", "b"], ["c", "d"]) + def test_signal_range(camilla_mockws): camilla_mockws.connect() assert camilla_mockws.get_signal_range() == 0.2 @@ -118,6 +127,18 @@ def test_capture_rate(camilla_mockws): assert camilla_mockws.get_capture_rate() == 88200 assert camilla_mockws.get_capture_rate_raw() == 88250 +def test_stop_reason(camilla_mockws): + camilla_mockws.connect() + assert camilla_mockws.get_stop_reason() == StopReason.DONE + assert camilla_mockws.get_stop_reason().data == None + print(camilla_mockws.dummyws.responses) + camilla_mockws.dummyws.responses['"GetStopReason"'] = camilla_mockws.dummyws.responses['"GetStopReason2"'] + assert camilla_mockws.get_stop_reason() == StopReason.CAPTUREFORMATCHANGE + assert camilla_mockws.get_stop_reason().data == 44098 + camilla_mockws.dummyws.responses['"GetStopReason"'] = camilla_mockws.dummyws.responses['"GetStopReason3"'] + assert camilla_mockws.get_stop_reason() == StopReason.CAPTUREERROR + assert camilla_mockws.get_stop_reason().data == "error error" + def test_query(camilla_mockws): camilla_mockws.connect() with pytest.raises(camilladsp.CamillaError): @@ -133,10 +154,11 @@ def test_query(camilla_mockws): with pytest.raises(IOError): camilla_mockws._query("fail") -def test_query_setvalue(camilla_mockws): +def test_query_mockedws(camilla_mockws): camilla_mockws.connect() assert camilla_mockws._query("SetSomeValue", arg=123) is None assert camilla_mockws.dummyws.query == json.dumps({"SetSomeValue": 123}) + assert camilla_mockws.get_supported_device_types() == (["a", "b"], ["c", "d"]) def test_queries(camilla_mockquery): camilla_mockquery.get_capture_rate() @@ -200,3 +222,5 @@ def test_queries_adv(camilla_mockquery_yaml): camilla_mockquery_yaml._query.assert_called_with('ValidateConfig', arg='some: yaml\n') camilla_mockquery_yaml.get_config() camilla_mockquery_yaml._query.assert_called_with('GetConfig') + camilla_mockquery_yaml.get_previous_config() + camilla_mockquery_yaml._query.assert_called_with('GetPreviousConfig')