Skip to content

Commit 31dc4ab

Browse files
Add vol_f_buffer
1 parent a825552 commit 31dc4ab

File tree

10 files changed

+97
-62
lines changed

10 files changed

+97
-62
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# AmpliPi Software Releases
22

3+
## Future Release
4+
* Web App
5+
* Upgraded volume calculations to preserve relative positions when hitting the min or max setting via source volume bar
6+
37
## 0.4.8
48
* System
59
* Update our spotify provider `go-librespot` to `0.3.2` to accomodate spotify's API update

amplipi/ctrl.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,9 +852,14 @@ def set_vol():
852852
# Field precedence: vol (db) > vol_delta > vol (float)
853853
# NOTE: checks happen in reverse precedence to cover default case of unchanged volume
854854
if update.vol_delta_f is not None and update.vol is None:
855-
applied_delta = utils.clamp((vol_delta_f + zone.vol_f), 0, 1)
855+
true_vol_f = zone.vol_f + zone.vol_f_buffer
856+
totaled_delta = update.vol_delta_f + true_vol_f
857+
applied_delta = utils.clamp(totaled_delta, 0, 1)
858+
856859
vol_db = utils.vol_float_to_db(applied_delta, zone.vol_min, zone.vol_max)
857860
vol_f_new = applied_delta
861+
zone.vol_f_buffer = 0 if models.MIN_VOL_F < totaled_delta and totaled_delta < models.MAX_VOL_F else utils.clamp((totaled_delta - applied_delta), -1.0, 1.0)
862+
858863
elif update.vol_f is not None and update.vol is None:
859864
clamp_vol_f = utils.clamp(vol_f, 0, 1)
860865
vol_db = utils.vol_float_to_db(clamp_vol_f, zone.vol_min, zone.vol_max)
@@ -866,9 +871,12 @@ def set_vol():
866871
if self._rt.update_zone_vol(idx, vol_db):
867872
zone.vol = vol_db
868873
zone.vol_f = vol_f_new
874+
869875
else:
870876
raise Exception('unable to update zone volume')
871877

878+
zone.vol_f_buffer = 0 if models.MIN_VOL_F < vol_f_new and vol_f_new < models.MAX_VOL_F else zone.vol_f_buffer
879+
872880
# To avoid potential unwanted loud output:
873881
# If muting, mute before setting volumes
874882
# If un-muting, set desired volume first

amplipi/defaults.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@
4141
],
4242
"zones": [ # this is an array of zones, array length depends on # of boxes connected
4343
{"id": 0, "name": "Zone 1", "source_id": 0, "mute": True, "disabled": False,
44-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
44+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
4545
{"id": 1, "name": "Zone 2", "source_id": 0, "mute": True, "disabled": False,
46-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
46+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
4747
{"id": 2, "name": "Zone 3", "source_id": 0, "mute": True, "disabled": False,
48-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
48+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
4949
{"id": 3, "name": "Zone 4", "source_id": 0, "mute": True, "disabled": False,
50-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
50+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
5151
{"id": 4, "name": "Zone 5", "source_id": 0, "mute": True, "disabled": False,
52-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
52+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
5353
{"id": 5, "name": "Zone 6", "source_id": 0, "mute": True, "disabled": False,
54-
"vol_f": models.MIN_VOL_F, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
54+
"vol_f": models.MIN_VOL_F, "vol_f_buffer": 0.0, "vol_min": models.MIN_VOL_DB, "vol_max": models.MAX_VOL_DB},
5555
],
5656
"groups": [
5757
],

amplipi/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class fields(SimpleNamespace):
8787
VolumeMax = Field(ge=MIN_VOL_DB, le=MAX_VOL_DB, description='Max output volume in dB')
8888
GroupMute = Field(description='Set to true if output is all zones muted')
8989
GroupVolume = Field(ge=MIN_VOL_DB, le=MAX_VOL_DB, description='Average output volume')
90-
GroupVolumeF = Field(ge=MIN_VOL_F, le=MAX_VOL_F, description='Average output volume as a floating-point number')
90+
GroupVolumeF = Field(ge=MIN_VOL_F, le=MAX_VOL_F, description='Average output volume as a floating-point number.')
9191
Disabled = Field(description='Set to true if not connected to a speaker')
9292
Zones = Field(description='Set of zone ids belonging to a group')
9393
Groups = Field(description='List of group ids')
@@ -111,6 +111,8 @@ class fields_w_default(SimpleNamespace):
111111
Volume = Field(default=MIN_VOL_DB, ge=MIN_VOL_DB, le=MAX_VOL_DB, description='Output volume in dB')
112112
VolumeF = Field(default=MIN_VOL_F, ge=MIN_VOL_F, le=MAX_VOL_F,
113113
description='Output volume as a floating-point scalar from 0.0 to 1.0 representing MIN_VOL_DB to MAX_VOL_DB')
114+
VolumeFBuffer = Field(default=0.0, ge=(MIN_VOL_F - MAX_VOL_F), le=(MAX_VOL_F - MIN_VOL_F),
115+
description='Output volume as a floating-point scalar that has a range equal to MIN_VOL_F - MAX_VOL_F in both directions from zero, and is used to keep track of the relative distance between two or more zone volumes when they would otherwise have to exceed their VOL_F range')
114116
VolumeMin = Field(default=MIN_VOL_DB, ge=MIN_VOL_DB, le=MAX_VOL_DB,
115117
description='Min output volume in dB')
116118
VolumeMax = Field(default=MAX_VOL_DB, ge=MIN_VOL_DB, le=MAX_VOL_DB,
@@ -321,6 +323,7 @@ class Zone(Base):
321323
mute: bool = fields_w_default.Mute
322324
vol: int = fields_w_default.Volume
323325
vol_f: float = fields_w_default.VolumeF
326+
vol_f_buffer: float = fields_w_default.VolumeFBuffer
324327
vol_min: int = fields_w_default.VolumeMin
325328
vol_max: int = fields_w_default.VolumeMax
326329
disabled: bool = fields_w_default.Disabled

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "amplipi"
3-
version = "0.4.8"
3+
version = "0.4.8+88bf7ba-VolumeDeltaExpansion-dirty"
44
description = "A Pi-based whole house audio controller"
55
authors = [
66
"Lincoln Lorenz <lincoln@micro-nova.com>",

web/src/App.jsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ export const useStatusStore = create((set, get) => ({
5050
applyPlayerVol(vol, zones, sourceId, (zone_id, new_vol) => {
5151
for (const i in s.status.zones) {
5252
if (s.status.zones[i].id === zone_id) {
53-
s.status.zones[i].vol_f = new_vol;
53+
let true_vol = Math.round((new_vol + s.status.zones[i].vol_f_buffer) * 100) / 100;
54+
let clamped = Math.min(Math.max(true_vol, 0), 1);
55+
56+
s.status.zones[i].vol_f = clamped;
57+
s.status.zones[i].vol_f_buffer = true_vol - clamped;
5458
}
5559
}
5660
});
@@ -61,11 +65,17 @@ export const useStatusStore = create((set, get) => ({
6165
setZonesMute: (mute, zones, source_id) => {
6266
set(
6367
produce((s) => {
64-
for (const i of getSourceZones(source_id, zones)) {
65-
for (const j of s.status.zones) {
66-
if (j.id === i.id) {
67-
j.mute = mute;
68-
}
68+
const affectedZones = getSourceZones(source_id, zones).map(z => z.id);
69+
for (const j of s.status.zones) {
70+
if (affectedZones.includes(j.id)) {
71+
j.mute = mute;
72+
}
73+
}
74+
75+
// Also update groups that consist entirely of affected zones
76+
for (const g of s.status.groups) {
77+
if (g.zones.every(zid => affectedZones.includes(zid))) {
78+
g.mute = mute;
6979
}
7080
}
7181
})
@@ -131,7 +141,7 @@ export const useStatusStore = create((set, get) => ({
131141
if (get().skipUpdate) {
132142
// Does .then() into skipUpdate and ignores api output to help avoid race conditions
133143
set({ skipUpdate: false });
134-
} else {
144+
} else if(get().status == null){
135145
set({ status: s, loaded: true, disconnected: false });
136146
}
137147
});
@@ -164,7 +174,7 @@ export const useStatusStore = create((set, get) => ({
164174
const g = s.status.groups.filter((g) => g.id === groupId)[0];
165175
for (const i of g.zones) {
166176
s.skipUpdate = true;
167-
s.status.zones[i].vol_f = new_vol;
177+
s.status.zones[i].vol_f = new_vol + s.status.zones[i].vol_f_buffer;
168178
}
169179

170180
updateGroupVols(s);
@@ -198,7 +208,7 @@ export const useStatusStore = create((set, get) => ({
198208
const updateGroupVols = (s) => {
199209
s.status.groups.forEach((g) => {
200210
if (g.zones.length > 1) {
201-
const vols = g.zones.map((id) => s.status.zones[id].vol_f);
211+
const vols = g.zones.map((id) => s.status.zones[id].vol_f + s.status.zones[id].vol_f_buffer);
202212
let calculated_vol = Math.min(...vols) * 0.5 + Math.max(...vols) * 0.5;
203213
g.vol_f = calculated_vol;
204214
} else if (g.zones.length == 1) {
@@ -226,14 +236,14 @@ Page.propTypes = {
226236

227237
const App = ({ selectedPage }) => {
228238
return (
229-
<div className="app">
239+
<div className="app">
230240
<DisconnectedIcon />
231241
<div className="background-gradient"></div> {/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */}
232-
<div className="app-body">
233-
<Page selectedPage={selectedPage} />
234-
</div>
235-
<MenuBar pageNumber={selectedPage} />
242+
<div className="app-body">
243+
<Page selectedPage={selectedPage} />
236244
</div>
245+
<MenuBar pageNumber={selectedPage} />
246+
</div>
237247
);
238248
};
239249
App.propTypes = {

web/src/components/CardVolumeSlider/CardVolumeSlider.jsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const getPlayerVol = (sourceId, zones) => {
1111
let n = 0;
1212
for (const i of getSourceZones(sourceId, zones)) {
1313
n += 1;
14-
vol += i.vol_f;
14+
vol += i.vol_f + i.vol_f_buffer; // Add buffer to retain proper relative space when doing an action that would un-overload the slider
1515
}
1616

1717
const avg = vol / n;
@@ -27,22 +27,22 @@ export const applyPlayerVol = (vol, zones, sourceId, apply) => {
2727
let delta = vol - getPlayerVol(sourceId, zones);
2828

2929
for (let i of getSourceZones(sourceId, zones)) {
30-
let set_pt = Math.max(0, Math.min(1, i.vol_f + delta));
31-
apply(i.id, set_pt);
30+
apply(i.id, i.vol_f + delta);
3231
}
3332
};
3433

3534
// cumulativeDelta reflects the amount of movement that the
36-
let cumulativeDelta = 0;
35+
let cumulativeDelta = 0.0;
3736
let sendingPacketCount = 0;
3837

3938
// main volume slider on player and volume slider on player card
4039
const CardVolumeSlider = ({ sourceId }) => {
4140
const zones = useStatusStore((s) => s.status.zones);
4241
const setZonesVol = useStatusStore((s) => s.setZonesVol);
4342
const setZonesMute = useStatusStore((s) => s.setZonesMute);
43+
const setSystemState = useStatusStore((s) => s.setSystemState);
4444

45-
// needed to ensure that polling doesn't cause the delta volume to be made inacurrate during volume slider interactions
45+
// needed to ensure that polling doesn't cause the delta volume to be made inaccurate during volume slider interactions
4646
const skipNextUpdate = useStatusStore((s) => s.skipNextUpdate);
4747

4848
const value = getPlayerVol(sourceId, zones);
@@ -69,15 +69,16 @@ const CardVolumeSlider = ({ sourceId }) => {
6969
zones: getSourceZones(sourceId, zones).map((z) => z.id),
7070
update: { vol_delta_f: cumulativeDelta, mute: false },
7171
}),
72-
}).then(() => {
72+
}).then(res => {
7373
// NOTE: This used to just set cumulativeDelta to 0
7474
// that would skip all accumulated delta from fetch start to backend response time
7575
// causing jittering issues
7676
cumulativeDelta -= delta;
7777
sendingPacketCount -= 1;
78+
if(res.ok){res.json().then(s => setSystemState(s));}
7879
});
7980
}
80-
};
81+
}
8182

8283
const mute = getSourceZones(sourceId, zones)
8384
.map((z) => z.mute)

web/src/components/GroupVolumeSlider/GroupVolumeSlider.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,20 @@ let sendingRequestCount = 0;
1616
// volume slider for a group in the volumes drawer
1717
const GroupVolumeSlider = ({ groupId, sourceId, groupsLeft }) => {
1818
const group = useStatusStore(s => s.status.groups.filter(g => g.id === groupId)[0]);
19-
const volume = group.vol_f;
19+
const zones = useStatusStore(s => s.status.zones);
2020
const setGroupVol = useStatusStore(s => s.setGroupVol);
2121
const setGroupMute = useStatusStore(s => s.setGroupMute);
2222
const [slidersOpen, setSlidersOpen] = React.useState(false);
2323

24+
const getVolume = () => { // Make sure group sliders account for vol_f_buffer
25+
let v = 0;
26+
for(let i = 0; i < group.zones.length; i++){
27+
v += (zones[group.zones[i]].vol_f + zones[group.zones[i]].vol_f_buffer);
28+
}
29+
30+
return v / group.zones.length;
31+
};
32+
const volume = getVolume();
2433

2534
// get zones for this group
2635
const groupZones = getSourceZones(sourceId, useStatusStore(s => s.status.zones)).filter(z => group.zones.includes(z.id));

web/src/components/VolumeSlider/VolumeSlider.jsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,31 +71,31 @@ const VolumeSlider = ({ vol, mute, setVol, setMute, disabled }) => {
7171
>
7272
<VolIcon vol={vol} mute={mute} />
7373
</div>
74-
<Slider
75-
disabled={disabled}
76-
className="volume-slider"
77-
min={0}
78-
step={0.01}
79-
max={1}
80-
value={vol}
81-
onTouchStart={handleTouchStart}
82-
onTouchMove={handleTouchMove}
83-
// https://github.com/mui/material-ui/issues/32737#issuecomment-2060439608
84-
// ios does some weird emulation of mouse events from touch events, ignore those
85-
onChange={(e, val) => {
86-
if (isIOS() && e.type === "mousedown") {
87-
return;
88-
}
89-
handleVolChange(val);
90-
}}
91-
onChangeCommitted={(e, val) => {
92-
if (isIOS() && e.type === "mouseup") {
93-
return;
94-
}
95-
handleVolChange(val, true);
96-
}}
97-
/>
98-
</div>
74+
<Slider
75+
disabled={disabled}
76+
className="volume-slider"
77+
min={0}
78+
step={0.01}
79+
max={1}
80+
value={vol}
81+
onTouchStart={handleTouchStart}
82+
onTouchMove={handleTouchMove}
83+
// https://github.com/mui/material-ui/issues/32737#issuecomment-2060439608
84+
// ios does some weird emulation of mouse events from touch events, ignore those
85+
onChange={(e, val) => {
86+
if (isIOS() && e.type === "mousedown") {
87+
return;
88+
}
89+
handleVolChange(val);
90+
}}
91+
onChangeCommitted={(e, val) => {
92+
if (isIOS() && e.type === "mouseup") {
93+
return;
94+
}
95+
handleVolChange(val, true);
96+
}}
97+
/>
98+
</div>
9999
</StopProp>
100100
);
101101
};

web/src/components/VolumeZones/VolumeZones.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ const VolumeZones = ({ sourceId, open, zones, groups, groupsLeft, alone }) => {
3131
});
3232

3333
if(open){
34-
return (
35-
<div className="volume-sliders-container">
36-
{groupVolumeSliders}
37-
{zoneVolumeSliders}
38-
</div>
39-
);
34+
return (
35+
<div className="volume-sliders-container">
36+
{groupVolumeSliders}
37+
{zoneVolumeSliders}
38+
</div>
39+
);
4040
}
4141
};
4242
VolumeZones.propTypes = {

0 commit comments

Comments
 (0)