Skip to content

Commit

Permalink
Merge pull request #11 from HEnquist/next
Browse files Browse the repository at this point in the history
Next
  • Loading branch information
HEnquist authored Aug 11, 2021
2 parents 6eeefe9 + ca584e8 commit 701ab3f
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 24 deletions.
59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.|

Expand All @@ -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 |
Expand All @@ -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.|
Expand All @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion camilladsp/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from camilladsp.camilladsp import CamillaConnection, CamillaError
from camilladsp.camilladsp import CamillaConnection, CamillaError, ProcessingState, StopReason
103 changes: 96 additions & 7 deletions camilladsp/camilladsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions examples/get_config/get_config.py
Original file line number Diff line number Diff line change
@@ -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"])}')
1 change: 0 additions & 1 deletion examples/playwav/analyze_wav.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import struct
import logging

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

setuptools.setup(
name="camilladsp",
version="0.5.1",
version="0.6.0",
author="Henrik Enquist",
author_email="[email protected]",
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",
Expand Down
30 changes: 27 additions & 3 deletions tests/test_camillaws.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from camilladsp.camilladsp import StopReason
import pytest
from unittest.mock import MagicMock, patch
import camilladsp
Expand All @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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')

0 comments on commit 701ab3f

Please sign in to comment.