diff --git a/.gitignore b/.gitignore index e16586e..18a4981 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,6 @@ dmypy.json .pyre/ artwork* -events.bin \ No newline at end of file +events.bin + +.vscode/* diff --git a/README.md b/README.md index e0c206b..d3ee689 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ virtualenv airplay2-receiver cd airplay2-receiver .\Scripts\activate pip install -r requirements.txt -pip install pipwin +pip install pipwin pycaw pipwin install pyaudio python ap2-receiver.py -m myap2 -n [YOUR_INTERFACE_GUID] (looks like this for instance {02681AC0-AD52-4E15-9BD6-8C6A08C4F836} ) diff --git a/ap2-receiver.py b/ap2-receiver.py index 264f82c..6e0f397 100644 --- a/ap2-receiver.py +++ b/ap2-receiver.py @@ -20,7 +20,7 @@ from ap2.connections.audio import RTPBuffer from ap2.playfair import PlayFair -from ap2.utils import get_volume, set_volume +from ap2.utils import get_volume, set_volume, set_volume_pid from ap2.pairing.hap import Hap, HAPSocket from ap2.connections.event import Event from ap2.connections.stream import Stream @@ -394,6 +394,7 @@ def do_SETUP(self): print("Sending CONTROL/DATA:") buff = 8388608 # determines how many CODEC frame size 1024 we can hold stream = Stream(plist["streams"][0], buff, self.server.ptp_link) + set_volume_pid(stream.data_proc.pid) self.server.streams.append(stream) sonos_one_setup_data["streams"][0]["controlPort"] = stream.control_port sonos_one_setup_data["streams"][0]["dataPort"] = stream.data_port @@ -841,31 +842,56 @@ def upgrade_to_encrypted(self, client_address, shared_key): return hap_socket +def list_network_interfaces(): + print("Available network interfaces:") + for interface in ni.interfaces(): + print(f' Interface: "{interface}"') + addresses = ni.ifaddresses(interface) + for address_family in addresses: + if address_family in [ni.AF_INET, ni.AF_INET6]: + for ak in addresses[address_family]: + for akx in ak: + if str(akx) == 'addr': + print(f" {'IPv4' if address_family == ni.AF_INET else 'IPv6'}: {str(ak[akx])}") + + if __name__ == "__main__": multiprocessing.set_start_method("spawn") parser = argparse.ArgumentParser(prog='AirPlay 2 receiver') - parser.add_argument("-m", "--mdns", required=True, help="mDNS name to announce") - parser.add_argument("-n", "--netiface", required=True, help="Network interface to bind to") - parser.add_argument("-nv", "--no-volume-management", required=False, help="Disable volume management", action='store_true') - parser.add_argument("-f", "--features", required=False, help="Features") + parser.add_argument("-m", "--mdns", help="mDNS name to announce", default="myap2") + parser.add_argument("-n", "--netiface", help="Network interface to bind to. Use the --list-interfaces option to list available interfaces.") + parser.add_argument("-nv", "--no-volume-management", help="Disable volume management", action='store_true') + parser.add_argument("-f", "--features", help="Features") + parser.add_argument("--list-interfaces", help="Prints available network interfaces and exits.", action='store_true') args = parser.parse_args() + if args.list_interfaces: + list_network_interfaces() + exit(0) + + if args.netiface is None: + print("[!] Missing --netiface argument. See below for a list of valid interfaces") + list_network_interfaces() + exit(-1) + try: IFEN = args.netiface ifen = ni.ifaddresses(IFEN) - DISABLE_VM = args.no_volume_management - if args.features: - try: - FEATURES = int(args.features, 16) - except Exception: - print("[!] Error with feature arg - hex format required") - exit(-1) except Exception: - print("[!] Network interface not found") + print("[!] Network interface not found.") + list_network_interfaces() exit(-1) + DISABLE_VM = args.no_volume_management + if args.features: + try: + FEATURES = int(args.features, 16) + except Exception: + print("[!] Error with feature arg - hex format required") + exit(-1) + DEVICE_ID = None IPV4 = None IPV6 = None diff --git a/ap2/utils.py b/ap2/utils.py index fd1cfc1..7862037 100644 --- a/ap2/utils.py +++ b/ap2/utils.py @@ -5,6 +5,15 @@ import subprocess +if platform.system() == "Windows": + try: + from pycaw.pycaw import AudioUtilities, ISimpleAudioVolume + except ImportError: + AudioUtilities = None + ISimpleAudioVolume = None + print('[!] Pycaw is not installed - volume control will be unavailable', ) + + def get_logger(name, level="INFO"): logging.basicConfig( filename="%s.log" % name, @@ -47,6 +56,26 @@ def interpolate(value, from_min, from_max, to_min, to_max): return to_min + (value_scale * to_span) +audio_pid = 0 + +def set_volume_pid(pid): + global audio_pid + audio_pid = pid + +def get_pycaw_volume_session(): + if platform.system() != 'Windows' or AudioUtilities is None: + return + session = None + for s in AudioUtilities.GetAllSessions(): + try: + if s.Process.pid == audio_pid: + session = s._ctl.QueryInterface(ISimpleAudioVolume) + break + except AttributeError: + pass + return session + + def get_volume(): subsys = platform.system() if subsys == "Darwin": @@ -63,7 +92,13 @@ def get_volume(): pct = 50 vol = interpolate(pct, 45, 100, -30, 0) elif subsys == "Windows": - # Volume get is not managed under windows, let's set to a default volume + volume_session = get_pycaw_volume_session() + if not volume_session: + vol = -15 + else: + vol = interpolate(volume_session.GetMasterVolume(), 0, 1, -30, 0) + else: + # This system is not supported, whatever it is. vol = 50 if vol == -30: return -144 @@ -82,3 +117,8 @@ def set_volume(vol): pct = int(interpolate(vol, -30, 0, 45, 100)) subprocess.run(["amixer", "set", "PCM", "%d%%" % pct]) + elif subsys == "Windows": + volume_session = get_pycaw_volume_session() + if volume_session: + pct = interpolate(vol, -30, 0, 0, 1) + volume_session.SetMasterVolume(pct, None)