Skip to content

Commit

Permalink
refactor gain control to allow the inputs to be driven by the device …
Browse files Browse the repository at this point in the history
…config
  • Loading branch information
3ll3d00d committed May 27, 2023
1 parent 24a9a9d commit 67af4a8
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 145 deletions.
40 changes: 7 additions & 33 deletions ezbeq/apis/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,50 +275,24 @@ def get(self):
return {n: d.serialise() for n, d in self.__bridge.all_devices(refresh=True).items()}


slot_model_v1 = v1_api.model('Slot', {
gain_model = v2_api.model('Gain', {
'id': fields.String(required=True),
'active': fields.Boolean(required=False),
'gain1': fields.Float(required=False),
'gain2': fields.Float(required=False),
'mute1': fields.Boolean(required=False),
'mute2': fields.Boolean(required=False),
'entry': fields.String(required=False)
'value': fields.Float(required=True)
})


device_model_v1 = v1_api.model('Device', {
'mute': fields.Boolean(required=False),
'masterVolume': fields.Float(required=False),
'slots': fields.List(fields.Nested(slot_model_v1), required=False)
mute_model = v2_api.model('Mute', {
'id': fields.String(required=True),
'value': fields.Boolean(required=True)
})


@v1_api.route('/<string:device_name>')
class Device(Resource):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__bridge: DeviceRepository = kwargs['device_bridge']
self.__catalogue_provider: CatalogueProvider = kwargs['catalogue']

@v1_api.expect(device_model_v1, validate=True)
def patch(self, device_name: str):
payload = request.get_json()
logger.info(f"PATCHing {device_name} with {payload}")
if not self.__bridge.update(device_name, payload):
logger.info(f"PATCH {device_name} was a nop")
return self.__bridge.state(device_name).serialise()


slot_model_v2 = v2_api.model('Slot', {
'id': fields.String(required=True),
'active': fields.Boolean(required=False),
'gains': fields.List(fields.Float, required=False),
'mutes': fields.List(fields.Boolean,required=False),
'gains': fields.List(fields.Nested(gain_model), required=False),
'mutes': fields.List(fields.Nested(mute_model), required=False),
'entry': fields.String(required=False)
})


device_model_v2 = v2_api.model('Device', {
'mute': fields.Boolean(required=False),
'masterVolume': fields.Float(required=False),
Expand Down
19 changes: 11 additions & 8 deletions ezbeq/camilladsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import math
import time
from typing import List, Optional, Callable, Union, Tuple
from typing import List, Optional, Callable, Union, Tuple, Dict

from autobahn.exception import Disconnected
from autobahn.twisted.websocket import connectWS, WebSocketClientProtocol, WebSocketClientFactory
Expand All @@ -21,12 +21,12 @@ class CamillaDspSlotState(SlotState):

def __init__(self):
super().__init__(SLOT_ID)
self.has_gain = False
self.gains_by_channel: Dict[str, float] = {}

def as_dict(self) -> dict:
return {
**super().as_dict(),
'has_gain': self.has_gain
'gains': self.gains_by_channel
}


Expand Down Expand Up @@ -96,11 +96,14 @@ def upd():
logger.info(f'[{self.name}] current config has volume filter? {self._current_state.has_volume}')

if self.__input_gains and 'mixers' in cfg:
available_channels = {source['channel']
for v in cfg['mixers'].values()
for mapping in v['mapping']
for source in mapping['sources']}
self._current_state.slot.has_gain = set(self.__input_gains[1]) <= available_channels
mixer_cfg = cfg['mixers'].get(self.__input_gains[0], None)
gains = {}
if mixer_cfg:
for mapping in mixer_cfg['mapping']:
for source in mapping['sources']:
if source['channel'] in self.__input_gains[1]:
gains[str(source['channel'])] = source.get('gain', 0.0)
self._current_state.slot.gains_by_channel = gains

self._hydrate_cache_broadcast(upd)

Expand Down
12 changes: 8 additions & 4 deletions ezbeq/device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import math
import os
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, TypeVar, Generic, Callable
Expand Down Expand Up @@ -225,10 +226,13 @@ def _hydrate(self, refresh: bool = False) -> bool:
if not self.__hydrated or refresh is True:
self._current_state = self._load_initial_state()
if os.path.exists(self.__file_name):
with open(self.__file_name, 'r') as f:
cached_state = json.load(f)
logger.info(f"Loaded {cached_state} from {self.__file_name}")
self._current_state = self._merge_state(self._current_state, cached_state)
try:
with open(self.__file_name, 'r') as f:
cached_state = json.load(f)
logger.info(f"Loaded {cached_state} from {self.__file_name}")
self._current_state = self._merge_state(self._current_state, cached_state)
except Exception:
logger.exception(f'Failed to load content from {self.__file_name}')
else:
logger.info(f"No cached state found at {self.__file_name}")
if refresh is False:
Expand Down
88 changes: 29 additions & 59 deletions ezbeq/minidsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time
from concurrent.futures.thread import ThreadPoolExecutor
from contextlib import contextmanager
from typing import List, Optional, Union
from typing import List, Optional, Union, Dict

import yaml
from autobahn.exception import Disconnected
Expand All @@ -30,7 +30,7 @@ def __init__(self, name: str, descriptor: 'MinidspDescriptor', **kwargs):
self.master_volume: float = kwargs['mv'] if 'mv' in kwargs else 0.0
self.__mute: bool = kwargs['mute'] if 'mute' in kwargs else False
self.__active_slot: str = kwargs['active_slot'] if 'active_slot' in kwargs else ''
self.__descriptr = descriptor
self.__descriptor = descriptor
slot_ids = [str(i + 1) for i in range(4)]
self.__slots: List[MinidspSlotState] = [
MinidspSlotState(c_id,
Expand Down Expand Up @@ -124,15 +124,15 @@ def clear(self):
self.gains = self.__make_vals(0.0)
self.mutes = self.__make_vals(False)

def __make_vals(self, val):
return [val] * self.__input_channels
def __make_vals(self, val: Union[float, bool]) -> List[dict]:
return [{'id': str(i + 1), 'value': val} for i in range(self.__input_channels)]

def set_gain(self, channel: Optional[int], value: float):
if channel is None:
self.gains = self.__make_vals(value)
else:
if channel <= self.__input_channels:
self.gains[channel - 1] = value
next(g for g in self.gains if g['id'] == str(channel))['value'] = value
else:
raise ValueError(f'Unknown channel {channel} for slot {self.slot_id}')

Expand All @@ -144,7 +144,7 @@ def __do_mute(self, channel: Optional[int], value: bool):
self.mutes = self.__make_vals(value)
else:
if channel <= self.__input_channels:
self.mutes[channel - 1] = value
next(g for g in self.mutes if g['id'] == str(channel))['value'] = value
else:
raise ValueError(f'Unknown channel {channel} for slot {self.slot_id}')

Expand All @@ -153,44 +153,34 @@ def unmute(self, channel: Optional[int]):

def merge_with(self, state: dict) -> None:
super().merge_with(state)
# legacy (v1 api)
if 'gain1' in state and self.__input_channels > 0:
self.gains[0] = float(state['gain1'])
if 'gain2' in state and self.__input_channels > 1:
self.gains[1] = float(state['gain2'])
if 'mute1' in state and self.__input_channels > 0:
self.mutes[0] = bool(state['mute1'])
if 'mute2' in state and self.__input_channels > 1:
self.mutes[1] = bool(state['mute2'])
# current (v2 api)
if 'gains' in state and len(state['gains']) == self.__input_channels:
self.gains = [float(v) for v in state['gains']]
self.gains = []
for i, g in enumerate(state['gains']):
if isinstance(g, dict):
self.gains.append(g)
else:
self.gains.append({'id': str(i+1), 'value': float(g)})
if 'mutes' in state and len(state['mutes']) == self.__input_channels:
self.mutes = [bool(v) for v in state['mutes']]
self.mutes = []
for i, m in enumerate(state['mutes']):
if isinstance(m, dict):
self.mutes.append(m)
else:
self.mutes.append({'id': str(i+1), 'value': bool(m)})

def as_dict(self) -> dict:
sup = super().as_dict()
vals = {}
if self.__input_channels == 2:
# backwards compatibility
vals = {
'gain1': self.gains[0],
'gain2': self.gains[1],
'mute1': self.mutes[0],
'mute2': self.mutes[1],
}
return {
**sup,
**vals,
'gains': [g for g in self.gains],
'mutes': [m for m in self.mutes],
'gains': self.gains,
'mutes': self.mutes,
'canActivate': True,
'inputs': self.__input_channels,
'outputs': self.__output_channels
}

def __repr__(self):
vals = ' '.join([f"{i + 1}: {g:.2f}/{self.mutes[i]}" for i, g in enumerate(self.gains)])
vals = ' '.join([f"{g['id']}: {g['value']:.2f}/{self.mutes[i]['value']}" for i, g in enumerate(self.gains)])
return f"{super().__repr__()} - {vals}"


Expand All @@ -214,7 +204,7 @@ def __eq__(self, o: object) -> bool:
same = self.name == o.name and self.biquads == o.biquads and self.channels == o.channels and self.beq_slots == o.beq_slots
if same:
return (self.groups is None and o.groups is None) or (
self.groups is not None and self.groups == o.groups)
self.groups is not None and self.groups == o.groups)
return same
return NotImplemented

Expand Down Expand Up @@ -664,36 +654,16 @@ def __do_it() -> bool:
def __update_slot(self, slot: dict) -> bool:
any_update = False
current_slot = self._current_state.get_slot(slot['id'])
# legacy
if 'gain1' in slot:
self.set_gain(current_slot.slot_id, 1, slot['gain1'])
any_update = True
if 'gain2' in slot:
self.set_gain(current_slot.slot_id, 2, slot['gain2'])
any_update = True
if 'mute1' in slot:
if slot['mute1'] is True:
self.mute(current_slot.slot_id, 1)
else:
self.unmute(current_slot.slot_id, 1)
any_update = True
if 'mute2' in slot:
if slot['mute1'] is True:
self.mute(current_slot.slot_id, 2)
else:
self.unmute(current_slot.slot_id, 2)
any_update = True
# current
if 'gains' in slot:
for idx, gain in enumerate(slot['gains']):
self.set_gain(current_slot.slot_id, idx + 1, gain)
for gain in slot['gains']:
self.set_gain(current_slot.slot_id, int(gain['id']), gain['value'])
any_update = True
if 'mutes' in slot:
for idx, mute in enumerate(slot['mutes']):
if mute is True:
self.mute(current_slot.slot_id, idx + 1)
for mute in slot['mutes']:
if mute['value'] is True:
self.mute(current_slot.slot_id, int(mute['id']))
else:
self.unmute(current_slot.slot_id, idx + 1)
self.unmute(current_slot.slot_id, int(mute['id']))
any_update = True
if 'entry' in slot:
if slot['entry']:
Expand Down Expand Up @@ -742,7 +712,7 @@ def __send():
sched()

def on_ws_message(self, msg: dict):
logger.info(f"[{self.name}] Received {msg}")
logger.debug(f"[{self.name}] Received {msg}")
if 'master' in msg:
master = msg['master']
if master:
Expand Down
26 changes: 11 additions & 15 deletions tests/test_minidsp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ def verify_slot(slot: dict, idx: int, active: bool = False, gain = (0.0, 0.0), m
if gain:
assert len(slot['gains']) == len(gain)
for idx, g in enumerate(gain):
assert slot['gains'][idx] == g
assert slot['gains'][idx]['value'] == g
else:
assert len(slot['gains']) == 0
if mute:
assert len(slot['mutes']) == len(mute)
for idx, g in enumerate(mute):
assert slot['mutes'][idx] == g
assert slot['mutes'][idx]['value'] == g
else:
assert len(slot['mutes']) == 0
assert slot['last'] == last
Expand Down Expand Up @@ -1807,24 +1807,22 @@ def test_load_known_entry_to_10x10xo1_and_then_clear(minidsp_10x10xo1_client, mi
verify_slot(s, idx + 1, active=slot_is_active, gain=[0.0] * 8, mute=[False] * 8)


@pytest.mark.parametrize("v", [1, 2])
def test_patch_multiple_fields(minidsp_client, minidsp_app, v):
def test_patch_multiple_fields(minidsp_client, minidsp_app):
config: MinidspSpyConfig = minidsp_app.config['APP_CONFIG']
assert isinstance(config, MinidspSpyConfig)
# when: set master gain
# and: set input gains
gains = {'gain1': 5.1, 'gain2': 6.1} if v == 1 else {'gains': [5.1, 6.1]}
payload = {
'masterVolume': -10.2,
'mute': True,
'slots': [
{
'id': '2',
**gains
'gains': [{'id': '1', 'value': 5.1}, {'id': '2', 'value': 6.1}]
}
]
}
r = minidsp_client.patch(f"/api/1/devices/master", data=json.dumps(payload), content_type='application/json')
r = minidsp_client.patch(f"/api/2/devices/master", data=json.dumps(payload), content_type='application/json')
assert r.status_code == 200

# then: expected commands are sent
Expand All @@ -1843,29 +1841,27 @@ def test_patch_multiple_fields(minidsp_client, minidsp_app, v):
verify_slot(s, idx + 1)


@pytest.mark.parametrize("v", [1, 2])
def test_patch_multiple_slots(minidsp_client, minidsp_app, v):
def test_patch_multiple_slots(minidsp_client, minidsp_app):
config: MinidspSpyConfig = minidsp_app.config['APP_CONFIG']
assert isinstance(config, MinidspSpyConfig)
# when: set master gain
# and: set input gains
g1 = {'gain1': 5.1, 'gain2': 6.1} if v == 1 else {'gains': [5.1, 6.1]}
g2 = {'gain1': -1.1, 'gain2': -1.1, 'mute1': False, 'mute2': False} if v == 1 else {'gains': [-1.1, -1.1], 'mutes': [False]*2}
payload = {
'masterVolume': -10.2,
'slots': [
{
'id': '2',
**g1
'gains': [{'id': '1', 'value': 5.1}, {'id': '2', 'value': 6.1}]
},
{
'id': '3',
'entry': '123456_0',
**g2
'gains': [{'id': '1', 'value': -1.1}, {'id': '2', 'value': -1.1}],
'mutes': [{'id': '1', 'value': False}, {'id': '2', 'value': False}]
}
]
}
r = minidsp_client.patch(f"/api/1/devices/master", data=json.dumps(payload), content_type='application/json')
r = minidsp_client.patch(f"/api/2/devices/master", data=json.dumps(payload), content_type='application/json')
assert r.status_code == 200

# then: expected commands are sent
Expand Down Expand Up @@ -1927,7 +1923,7 @@ def test_reload_from_cache(minidsp_client, tmp_path):
expected.update_master_state(True, -5.4)
slot = expected.get_slot('2')
slot.mute(None)
slot.gains[0] = 4.8
slot.gains[0]['value'] = 4.8
slot.active = True
slot.last = 'Testing'
with open(os.path.join(tmp_path, 'master.json'), 'w') as f:
Expand Down

0 comments on commit 67af4a8

Please sign in to comment.