Skip to content

Commit

Permalink
Merge branch 'master' into ptp-sync-callback
Browse files Browse the repository at this point in the history
  • Loading branch information
glmnet committed Jul 11, 2021
2 parents 4f9fe26 + 543dd0c commit 038621b
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 16 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ dmypy.json
.pyre/

artwork*
events.bin
events.bin

.vscode/*
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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} )
Expand Down
52 changes: 39 additions & 13 deletions ap2-receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion ap2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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":
Expand All @@ -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
Expand All @@ -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)

0 comments on commit 038621b

Please sign in to comment.