VulpesOS/tundra/devices/beatles/stream_volume.py
2026-10-22 20:42:26 +02:00

71 lines
3.2 KiB
Python

# SPDX-License-Identifier: MPL-2.0
"""Stream volume for the Nokia Android 6 audio policy service.
AOSP android-6.0.1_r1 IAudioPolicyService: setStreamVolumeIndex=16,
getStreamVolumeIndex=17 (index, status), getDevicesForStream=24.
The firmware reports 0..5 for voice, 0..15 for the other streams below.
Only the currently routed outputs are changed; trial shutdown restores them.
"""
import math
import re
import subprocess
STREAMS={'telephony':(0,5),'notification':(5,15),'content':(3,15),'alarm':(4,15),'bt_sco':(6,15)}
class StreamVolume:
def __init__(self):
self.original={}
def transact(self,method,*values):
command=['/system/bin/service','call','media.audio_policy',str(method)]
for value in values:command.extend(('i32',str(value)))
result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
text=True,timeout=2,env={'PATH':'/system/bin','LD_LIBRARY_PATH':'/system/lib'})
if result.returncode:raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
# service prints either one line or address-prefixed Parcel rows.
text=result.stdout
if not text.startswith('Result: Parcel('):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
text=re.sub(r"'[^']*'",'',text)
text=re.sub(r'0x[0-9a-fA-F]+:', '',text)
return [int(word,16) for word in re.findall(r'\b[0-9a-fA-F]{8}\b',text)]
def get(self,stream,device,maximum):
reply=self.transact(17,stream,device)
if len(reply)!=2 or reply[1]!=0 or not 0<=reply[0]<=maximum:
raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
return reply[0]
def state(self):
result={}
for name,(stream,maximum) in STREAMS.items():
reply=self.transact(24,stream)
if len(reply)!=1 or reply[0]&0x80000000:raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
mask=reply[0] or 0x40000000
device=mask&-mask
result[name]=self.get(stream,device,maximum)/maximum
return result
def request(self,value):
if not isinstance(value,dict) or value.get('stream') not in STREAMS:
raise ValueError('INVALID_REQUEST')
level=value.get('level')
if type(level) not in (int,float) or not math.isfinite(level) or not 0<=level<=1:
raise ValueError('INVALID_REQUEST')
stream,maximum=STREAMS[value['stream']]
reply=self.transact(24,stream)
if len(reply)!=1 or reply[0]&0x80000000:raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
mask=reply[0] or 0x40000000
devices=[1<<bit for bit in range(31) if mask&(1<<bit)]
index=int(level*maximum+.5)
for device in devices:
old=self.get(stream,device,maximum)
self.original.setdefault((stream,device),old)
if self.transact(16,stream,index,device)!=[0] or self.get(stream,device,maximum)!=index:
raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
return {'stream':value['stream'],'index':index,'maximum':maximum,'devices':devices}
def restore(self):
for (stream,device),index in self.original.items():
try:self.transact(16,stream,index,device)
except (OSError,RuntimeError,subprocess.SubprocessError):pass