Skip to content

Commit d09231b

Browse files
committed
Add VU-meter command
1 parent 58ea613 commit d09231b

8 files changed

Lines changed: 262 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ Read the playback signal level continuously and print in the terminal, until sto
1515
python read_rms.py 1234
1616
```
1717

18+
## subscribe_events
19+
Subscribe to capture signal level events and state change events, and print incoming events in the terminal until stopped by Ctrl+c.
20+
```sh
21+
python subscribe_events.py 1234
22+
```
23+
1824
## get_config
1925
Read the configuration and print some parameters.
2026
```sh

camilladsp/general.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
This module contains commands of general nature.
55
"""
66

7-
from typing import Tuple, List, Optional
7+
from typing import Any, Callable, List, Optional, Tuple
88

99
from .commandgroup import _CommandGroup
1010
from .datastructures import (
@@ -32,6 +32,22 @@ def state(self) -> Optional[ProcessingState]:
3232
state = self.client.query("GetState")
3333
return _state_from_string(state)
3434

35+
def subscribe_state(self, callback: Callable[[Any], Optional[bool]]):
36+
"""
37+
Subscribe to state change events and call `callback` for each event.
38+
39+
This method blocks until `callback` returns `False`.
40+
41+
Args:
42+
callback: Function that receives event payloads.
43+
Typical payload keys are `state` and `stop_reason`.
44+
"""
45+
self.client.subscribe_events(
46+
command="SubscribeState",
47+
event_name="StateEvent",
48+
callback=callback,
49+
)
50+
3551
def stop_reason(self) -> StopReason:
3652
"""
3753
Get reason why processing stopped.

camilladsp/levels.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,34 @@ def subscribe_signal_levels(
227227
event_name="SignalLevelsEvent",
228228
callback=callback,
229229
)
230+
231+
def subscribe_vu_levels(
232+
self,
233+
callback: Callable[[Dict[str, Any]], Optional[bool]],
234+
max_rate: float,
235+
attack: float,
236+
release: float,
237+
):
238+
"""
239+
Subscribe to VU meter level events and call `callback` for each event.
240+
241+
This method blocks until `callback` returns `False`.
242+
243+
Args:
244+
callback: Function that receives event payloads.
245+
Typical payload keys are `playback_rms`, `playback_peak`,
246+
`capture_rms`, and `capture_peak`.
247+
max_rate (float): Maximum event rate in Hz. Use 0 to disable capping.
248+
attack (float): Attack time constant in milliseconds.
249+
release (float): Release time constant in milliseconds.
250+
"""
251+
self.client.subscribe_events(
252+
command="SubscribeVuLevels",
253+
arg={
254+
"max_rate": float(max_rate),
255+
"attack": float(attack),
256+
"release": float(release),
257+
},
258+
event_name="VuLevelsEvent",
259+
callback=callback,
260+
)

docs/general.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
This class is accessed via the `general` property on a `CamillaClient` instance.
33

44
It provides the basic methods such as starting and stopping processing.
5+
It also includes `subscribe_state` for listening to processing state changes.
6+
Subscription methods block while events are being received.
57

68
## class: `General`
79
::: camilladsp.general.General

docs/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ state = client.general.state()
2727
capture_levels = client.levels.capture_rms()
2828
```
2929

30+
Subscriptions are also available for streaming updates. For example,
31+
`client.levels.subscribe_signal_levels(...)` can be used to receive signal
32+
level events, and `client.general.subscribe_state(...)` can be used to
33+
receive processing state changes. Subscription calls block while listening for
34+
events, so use a separate client connection for each concurrent subscription.
35+
3036
## Command group classes
3137
| Class | Via property | Description |
3238
|--------------|----------|-------------|
@@ -44,6 +50,8 @@ capture_levels = client.levels.capture_rms()
4450
### [General][camilladsp.general.General]
4551
These commands are accessed via the [general][camilladsp.CamillaClient.general]
4652
property of a [CamillaClient][camilladsp.CamillaClient] instance.
53+
This group also includes `subscribe_state`, which listens for processing state
54+
change events.
4755
::: camilladsp.general.General
4856
options:
4957
show_bases: false
@@ -84,6 +92,8 @@ property of a [CamillaClient][camilladsp.CamillaClient] instance.
8492
### [Levels][camilladsp.levels.Levels]
8593
These commands are accessed via the [levels][camilladsp.CamillaClient.levels]
8694
property of a [CamillaClient][camilladsp.CamillaClient] instance.
95+
This group also includes `subscribe_signal_levels`, which listens for signal
96+
level events on the selected side.
8797
::: camilladsp.levels.Levels
8898
options:
8999
show_bases: false

docs/levels.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
This class is accessed via the `levels` property on a `CamillaClient` instance.
33

44
It provides methods for reading signal levels.
5+
It also includes `subscribe_signal_levels` for listening to signal level
6+
events from the websocket server, and `subscribe_vu_levels` for listening to
7+
pre-smoothed, rate-limited VU meter events. Subscription methods block while
8+
events are being received.
59

610
## class: `Levels`
711
::: camilladsp.levels.Levels
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# subscribe to events
2+
from camilladsp import CamillaClient
3+
import sys
4+
import threading
5+
import time
6+
7+
8+
def print_capture_levels(event):
9+
print(f"capture levels: {event}")
10+
return True
11+
12+
13+
def print_state(event):
14+
print(f"state: {event}")
15+
return True
16+
17+
18+
def subscribe_capture_levels(port):
19+
cdsp = CamillaClient("127.0.0.1", port)
20+
cdsp.connect()
21+
try:
22+
cdsp.levels.subscribe_signal_levels(print_capture_levels, side="capture")
23+
finally:
24+
cdsp.disconnect()
25+
26+
27+
def subscribe_state(port):
28+
cdsp = CamillaClient("127.0.0.1", port)
29+
cdsp.connect()
30+
try:
31+
cdsp.general.subscribe_state(print_state)
32+
finally:
33+
cdsp.disconnect()
34+
35+
36+
try:
37+
port = int(sys.argv[1])
38+
except:
39+
print("Usage: start CamillaDSP with the socketserver enabled:")
40+
print("> camilladsp -p4321 yourconfig.yml")
41+
print("Then run this script to print capture signal levels and state events.")
42+
print("> python subscribe_events.py 4321")
43+
sys.exit()
44+
45+
46+
level_thread = threading.Thread(target=subscribe_capture_levels, args=(port,), daemon=True)
47+
state_thread = threading.Thread(target=subscribe_state, args=(port,), daemon=True)
48+
49+
level_thread.start()
50+
state_thread.start()
51+
52+
print("Listening for capture signal level and state events, press Ctrl+C to stop")
53+
54+
try:
55+
while True:
56+
time.sleep(1)
57+
except KeyboardInterrupt:
58+
pass

tests/test_camillaws.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,56 @@ def test_connect(camilla_mockws):
164164
camilla_mockws.general.state()
165165
camilla_mockws.connect()
166166
assert camilla_mockws.is_connected()
167+
def test_subscribe_vu_events(camilla_mockws):
168+
camilla_mockws.connect()
169+
sent = []
170+
replies = iter(
171+
[
172+
json.dumps({"SubscribeVuLevels": {"result": "Ok"}}),
173+
json.dumps(
174+
{
175+
"VuLevelsEvent": {
176+
"result": "Ok",
177+
"value": {
178+
"playback_rms": [-20.0, -21.0],
179+
"playback_peak": [-10.0, -11.0],
180+
"capture_rms": [-30.0, -31.0],
181+
"capture_peak": [-12.0, -13.0],
182+
},
183+
}
184+
}
185+
),
186+
json.dumps({"StopSubscription": {"result": "Ok"}}),
187+
]
188+
)
189+
190+
camilla_mockws.mockconnection.send = MagicMock(side_effect=lambda msg: sent.append(msg))
191+
camilla_mockws.mockconnection.recv = MagicMock(side_effect=lambda: next(replies))
192+
193+
events = []
194+
195+
def on_event(event_data):
196+
events.append(event_data)
197+
return False
198+
199+
camilla_mockws.levels.subscribe_vu_levels(
200+
on_event, max_rate=30, attack=10, release=200
201+
)
202+
203+
assert events == [
204+
{
205+
"playback_rms": [-20.0, -21.0],
206+
"playback_peak": [-10.0, -11.0],
207+
"capture_rms": [-30.0, -31.0],
208+
"capture_peak": [-12.0, -13.0],
209+
}
210+
]
211+
assert sent == [
212+
json.dumps(
213+
{"SubscribeVuLevels": {"max_rate": 30.0, "attack": 10.0, "release": 200.0}}
214+
),
215+
json.dumps("StopSubscription"),
216+
]
167217
assert camilla_mockws.general.state() == camilladsp.ProcessingState.INACTIVE
168218
assert camilla_mockws.versions.camilladsp() == ("0", "3", "2")
169219
assert camilla_mockws.versions.library() == tuple(camilladsp.VERSION.split("."))
@@ -424,6 +474,38 @@ def test_subscribe_signal_levels(camilla_mockquery):
424474
)
425475

426476

477+
def test_subscribe_vu_levels(camilla_mockquery):
478+
callback = MagicMock(return_value=False)
479+
camilla_mockquery.subscribe_events = MagicMock()
480+
481+
camilla_mockquery.levels.subscribe_vu_levels(
482+
callback, max_rate=30, attack=10, release=200
483+
)
484+
485+
camilla_mockquery.subscribe_events.assert_called_with(
486+
command="SubscribeVuLevels",
487+
arg={"max_rate": 30.0, "attack": 10.0, "release": 200.0},
488+
event_name="VuLevelsEvent",
489+
callback=callback,
490+
)
491+
492+
493+
def test_subscribe_vu_levels(camilla_mockquery):
494+
callback = MagicMock(return_value=False)
495+
camilla_mockquery.subscribe_events = MagicMock()
496+
497+
camilla_mockquery.levels.subscribe_vu_levels(
498+
callback, max_rate=30, attack=10, release=200
499+
)
500+
501+
camilla_mockquery.subscribe_events.assert_called_with(
502+
command="SubscribeVuLevels",
503+
arg={"max_rate": 30.0, "attack": 10.0, "release": 200.0},
504+
event_name="VuLevelsEvent",
505+
callback=callback,
506+
)
507+
508+
427509
def test_subscribe_events(camilla_mockws):
428510
camilla_mockws.connect()
429511
sent = []
@@ -470,3 +552,55 @@ def on_event(event_data):
470552
"peak": [-39.4, -38.9],
471553
}
472554
]
555+
556+
557+
def test_subscribe_vu_events(camilla_mockws):
558+
camilla_mockws.connect()
559+
sent = []
560+
replies = iter(
561+
[
562+
json.dumps({"SubscribeVuLevels": {"result": "Ok"}}),
563+
json.dumps(
564+
{
565+
"VuLevelsEvent": {
566+
"result": "Ok",
567+
"value": {
568+
"playback_rms": [-20.0, -21.0],
569+
"playback_peak": [-10.0, -11.0],
570+
"capture_rms": [-30.0, -31.0],
571+
"capture_peak": [-12.0, -13.0],
572+
},
573+
}
574+
}
575+
),
576+
json.dumps({"StopSubscription": {"result": "Ok"}}),
577+
]
578+
)
579+
580+
camilla_mockws.mockconnection.send = MagicMock(side_effect=lambda msg: sent.append(msg))
581+
camilla_mockws.mockconnection.recv = MagicMock(side_effect=lambda: next(replies))
582+
583+
events = []
584+
585+
def on_event(event_data):
586+
events.append(event_data)
587+
return False
588+
589+
camilla_mockws.levels.subscribe_vu_levels(
590+
on_event, max_rate=30, attack=10, release=200
591+
)
592+
593+
assert events == [
594+
{
595+
"playback_rms": [-20.0, -21.0],
596+
"playback_peak": [-10.0, -11.0],
597+
"capture_rms": [-30.0, -31.0],
598+
"capture_peak": [-12.0, -13.0],
599+
}
600+
]
601+
assert sent == [
602+
json.dumps(
603+
{"SubscribeVuLevels": {"max_rate": 30.0, "attack": 10.0, "release": 200.0}}
604+
),
605+
json.dumps("StopSubscription"),
606+
]

0 commit comments

Comments
 (0)