Title: alsa_volume.py: get_state() reads Capture volume instead of Playback on mixed-channel mixer elements · Issue #236 · Sendspin/sendspin-python-cli · GitHub
Open Graph Title: alsa_volume.py: get_state() reads Capture volume instead of Playback on mixed-channel mixer elements · Issue #236 · Sendspin/sendspin-python-cli
X Title: alsa_volume.py: get_state() reads Capture volume instead of Playback on mixed-channel mixer elements · Issue #236 · Sendspin/sendspin-python-cli
Description: On USB DACs where the ALSA mixer element exposes both Capture and Playback channels (e.g. CX31993+MAX97220 Headset element), AlsaVolumeController.get_state() returns the wrong volume. Root cause: get_state() runs amixer -M -c sget and se...
Open Graph Description: On USB DACs where the ALSA mixer element exposes both Capture and Playback channels (e.g. CX31993+MAX97220 Headset element), AlsaVolumeController.get_state() returns the wrong volume. Root cause: g...
X Description: On USB DACs where the ALSA mixer element exposes both Capture and Playback channels (e.g. CX31993+MAX97220 Headset element), AlsaVolumeController.get_state() returns the wrong volume. Root cause: g...
Opengraph URL: https://github.com/Sendspin/sendspin-python-cli/issues/236
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"alsa_volume.py: get_state() reads Capture volume instead of Playback on mixed-channel mixer elements","articleBody":" On USB DACs where the ALSA mixer element exposes both Capture and Playback channels (e.g. CX31993+MAX97220 Headset element), AlsaVolumeController.get_state() returns the wrong volume. \n \n Root cause:\n\n get_state() runs amixer -M -c \u003ccard> sget \u003celement> and searches the full output with _VOLUME_RE.search(output), which matches the first [XX%] it finds. On the CX31993+MAX97220, the Headset element outputs Capture before Playback:\n\n```\n Simple mixer control 'Headset',0\n Capabilities: pvolume cvolume cvolume-joined pswitch pswitch-joined cswitch cswitch-joined\n Playback channels: Front Left - Front Right\n Capture channels: Mono\n Limits: Playback 0 - 148 Capture 0 - 148\n Mono: Capture 140 [85%] [-4.00dB] [on]\n Front Left: Playback 75 [20%] [-36.50dB] [off]\n Front Right: Playback 75 [20%] [-36.50dB] [off]\n```\n\n The regex matches [85%] (Capture/mic input) instead of [20%] (Playback). The same issue affects the mute switch detection — it reads Capture [on] instead of Playback [off].\n\n Impact:\n\n When MA calls _refresh_client_info() on sync group playback start, it reads get_player_volume() from each client. Affected clients report 85% (the Capture value), and MA overwrites their tracked volume. The sync leader (which may use a different\n DAC without this issue) keeps its correct volume, but all affected followers jump to 85%.\n\n set_state() is not affected — it correctly targets playback explicitly in the amixer command.\n\n Test data:\n\n The amixer -M output above can be used as a test fixture to verify the fix. Expected parse result should be volume=20, muted=True (from the Playback lines), not volume=85, muted=False (from the Capture line).\n\n Suggested fix:\n\n Replace get_state() in AlsaVolumeController with:\n\n async def get_state(self) -> tuple[int, bool]:\n \"\"\"Read ALSA mixer volume and mute state.\"\"\"\n proc = await asyncio.create_subprocess_exec(\n \"amixer\",\n \"-M\",\n \"-c\",\n self._card,\n \"sget\",\n self._element,\n stdout=asyncio.subprocess.PIPE,\n stderr=asyncio.subprocess.PIPE,\n )\n stdout, stderr = await proc.communicate()\n if proc.returncode != 0:\n raise RuntimeError(\n f\"amixer sget failed (exit {proc.returncode}): \"\n f\"{stderr.decode().strip() if stderr else '(empty)'}\"\n )\n\n output = stdout.decode()\n\n # Filter for Playback lines to avoid matching Capture channel values\n # on mixer elements that expose both (e.g. CX31993+MAX97220 Headset).\n # Falls back to full output for elements without separate Playback lines.\n playback_lines = [l for l in output.splitlines() if \"Playback\" in l and \"%\" in l]\n vol_match = (\n _VOLUME_RE.search(playback_lines[0])\n if playback_lines\n else _VOLUME_RE.search(output)\n )\n\n playback_switch_lines = [l for l in output.splitlines() if \"Playback\" in l and \"[o\" in l]\n switch_match = (\n _SWITCH_RE.search(playback_switch_lines[0])\n if playback_switch_lines\n else _SWITCH_RE.search(output)\n )\n\n if vol_match is None:\n raise RuntimeError(f\"Could not parse volume from amixer output: {output!r}\")\n\n volume = int(vol_match.group(1))\n muted = switch_match.group(1) == \"off\" if switch_match else False\n return volume, muted\n\n Affected hardware: Any ALSA mixer element with both Capture and Playback channels where Capture appears first in amixer output. Confirmed on CX31993+MAX97220 USB DAC (Headset element).","author":{"url":"https://github.com/1liminal1","@type":"Person","name":"1liminal1"},"datePublished":"2026-04-28T02:02:38Z","url":"https://github.com/Sendspin/sendspin-python-cli/issues/236"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:9a2a4919-eb5b-afb8-97ee-5925d13b2921 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 8038:A2B38:F0528F:138FC17:6AED575C |
| html-safe-nonce | f4121d559e83613f65d0502e712bdee8fba35132282731ff62352fc0b868bb52 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4MDM4OkEyQjM4OkYwNTI4RjoxMzhGQzE3OjZBRUQ1NzVDIiwidmlzaXRvcl9pZCI6Ijg2MjgzODA4ODA4NDA1MTgwNSIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 028943ed482855b7e61eded66ae32e87b04a56995b51cd6c47b3b6fe57d68ae3 |
| hovercard-subject-tag | issue:4340045822 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/Sendspin/sendspin-python-cli/236/issue_layout |
| twitter:image | https://opengraph.githubassets.com/e6026fdc50dc1eeb67aaab86f5e3abe3c31074836ad75a97623dacdb66d6e5bb/Sendspin/sendspin-python-cli/issues/236 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/e6026fdc50dc1eeb67aaab86f5e3abe3c31074836ad75a97623dacdb66d6e5bb/Sendspin/sendspin-python-cli/issues/236 |
| og:image:alt | On USB DACs where the ALSA mixer element exposes both Capture and Playback channels (e.g. CX31993+MAX97220 Headset element), AlsaVolumeController.get_state() returns the wrong volume. Root cause: g... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | 1liminal1 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 56fb42677ae87ed3e64410323d67980aa65d0485568a7858f874e7edfc6d1c11 |
| turbo-cache-control | no-preview |
| go-import | github.com/Sendspin/sendspin-python-cli git https://github.com/Sendspin/sendspin-python-cli.git |
| octolytics-dimension-user_id | 214994244 |
| octolytics-dimension-user_login | Sendspin |
| octolytics-dimension-repository_id | 1109978202 |
| octolytics-dimension-repository_nwo | Sendspin/sendspin-python-cli |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1109978202 |
| octolytics-dimension-repository_network_root_nwo | Sendspin/sendspin-python-cli |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c9a37e2423ed382cf39f70b5aa4b4858b40a3b0 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width