Skip to content

Commit

Permalink
Formalize audio setup parameters (from SDP).
Browse files Browse the repository at this point in the history
Relocate QT atom creation to AudioSetup class.
  • Loading branch information
systemcrash committed Jan 4, 2022
1 parent bd3ce4e commit 52f827e
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 45 deletions.
32 changes: 25 additions & 7 deletions ap2-receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ap2.utils import get_volume, set_volume, set_volume_pid, get_screen_logger
from ap2.pairing.hap import Hap, HAPSocket
from ap2.connections.event import EventGeneric
from ap2.connections.audio import AudioSetup
from ap2.connections.stream import Stream
from ap2.dxxp import parse_dxxp
from enum import IntFlag, Enum
Expand Down Expand Up @@ -410,12 +411,28 @@ def __init__(self, sdp=''):
self.audio_format_bd = ''.join(filter(str.isdigit, self.audio_format_bd))
elif 'a=fmtp:' in k and self.payload_type in k:
self.audio_fmtp = k.split(':')[1]
self.audio_format_params = self.audio_fmtp.split(' ')
self.afp = self.audio_fmtp.split(' ') # audio format params
if self.audio_format == self.SDPAudioFormat.ALAC:
self.spf = self.audio_format_params[1] # samples per frame
self.audio_format_bd = self.audio_format_params[3]
self.audio_format_ch = self.audio_format_params[7]
self.audio_format_sr = self.audio_format_params[11]
self.spf = self.afp[1] # samples per frame
# a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
self.params = AudioSetup(
codec_tag='alac',
ver=0,
spf=self.afp[1],
compat_ver=self.afp[2],
ss=self.afp[3], # bitdepth
hist_mult=self.afp[4],
init_hist=self.afp[5],
rice_lmt=self.afp[6],
cc=self.afp[7],
max_run=self.afp[8],
mcfs=self.afp[9],
abr=self.afp[10],
sr=self.afp[11],
)
self.audio_format_bd = self.afp[3]
self.audio_format_ch = self.afp[7]
self.audio_format_sr = self.afp[11]
self.audio_desc = 'ALAC'
elif self.audio_format == self.SDPAudioFormat.AAC:
self.audio_desc = 'AAC_LC'
Expand All @@ -425,7 +442,7 @@ def __init__(self, sdp=''):
self.audio_desc = 'OPUS'
if 'mode=' in self.audio_fmtp:
self.audio_format = self.SDPAudioFormat.AAC_ELD
for x in self.audio_format_params:
for x in self.afp:
if 'constantDuration=' in x:
start = x.find('constantDuration=') + len('constantDuration=')
self.constantDuration = int(x[start:].rstrip(';'))
Expand Down Expand Up @@ -581,6 +598,7 @@ def do_ANNOUNCE(self):
sdp_body = self.rfile.read(content_len).decode('utf-8')
SCR_LOG.debug(sdp_body)
sdp = SDPHandler(sdp_body)
self.aud_params = sdp.params
if sdp.has_mfi:
SCR_LOG.warning("MFi not possible on this hardware.")
self.send_response(404)
Expand Down Expand Up @@ -660,7 +678,7 @@ def do_SETUP(self):
'controlPort': 0,
}

streamobj = Stream(stream, AIRPLAY_BUFFER, DEBUG)
streamobj = Stream(stream, AIRPLAY_BUFFER, DEBUG, self.aud_params)

self.server.streams.append(streamobj)

Expand Down
77 changes: 43 additions & 34 deletions ap2/connections/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,34 @@ class AirplayAudFmt(enum.Enum):
AAC_ELD_48000_1 = 1 << 32


class AudioSetup:
def __init__(self, sr, ss, cc, codec_tag, ver=0, spf=352, compat_ver=0,
hist_mult=40, init_hist=10, rice_lmt=14, max_run=255, mcfs=0, abr=0):
x = bytes() # a 36-byte QuickTime atom passed through as extradata
x += (36).to_bytes(4, byteorder='big') # 32 bits atom size
x += (codec_tag).encode() # 32 bits tag ('alac')
x += int(ver).to_bytes(4, byteorder='big') # 32 bits tag version (0)
x += int(spf).to_bytes(4, byteorder='big') # 32 bits samples per frame
x += int(compat_ver).to_bytes(1, byteorder='big') # 8 bits compatible version (0)
x += int(ss).to_bytes(1, byteorder='big') # 8 bits sample size
x += int(hist_mult).to_bytes(1, byteorder='big') # 8 bits history mult (40)
x += int(init_hist).to_bytes(1, byteorder='big') # 8 bits initial history (10)
x += int(rice_lmt).to_bytes(1, byteorder='big') # 8 bits rice param limit (14)
x += int(cc).to_bytes(1, byteorder='big') # 8 bits channels
x += int(max_run).to_bytes(2, byteorder='big') # 16 bits maxRun (255)
x += int(mcfs).to_bytes(4, byteorder='big') # 32 bits max coded frame size (0 means unknown)
x += int(abr).to_bytes(4, byteorder='big') # 32 bits average bitrate (0 means unknown)
x += int(sr).to_bytes(4, byteorder='big') # 32 bits samplerate
self.extradata = x
self.sr = sr
self.ss = ss
self.cc = cc
self.spf = spf

def get_extra_data(self):
return self.extradata


class Audio:
@staticmethod
def set_audio_params(self, audio_format):
Expand Down Expand Up @@ -245,44 +273,22 @@ def set_audio_params(self, audio_format):

self.audio_screen_logger.debug(f"Negotiated audio format: {AirplayAudFmt(audio_format)}")

def __init__(self, session_key, audio_format, buff_size, session_iv=None, isDebug=False):
def __init__(self, session_key, audio_format, buff_size, session_iv=None, isDebug=False, aud_params: AudioSetup = None):
self.isDebug = isDebug
if self.isDebug:
self.audio_file_logger = get_file_logger("Audio.debug", level="DEBUG")
self.audio_screen_logger = get_screen_logger("Audio.Main", level="DEBUG")
else:
self.audio_screen_logger = get_screen_logger("Audio.Main", level="INFO")
self.audio_format = audio_format
self.audio_params = aud_params
self.session_key = session_key
self.session_iv = session_iv
sk_len = len(session_key)
self.rsakey_and_iv = True if (sk_len == 16 or sk_len == 24 or sk_len == 32 and session_iv is not None) else False
self.rtp_buffer = RTPBuffer(buff_size, self.isDebug)
self.set_audio_params(self, audio_format)

@staticmethod
def set_alac_extradata(self, sr, ss, cc,
codec_tag='alac', ver=0, spf=352, compat_ver=0,
hist_mult=40, init_hist=10, rice_lmt=14,
max_run=255, mcfs=0, abr=0,
):
x = bytes() # a 36-byte QuickTime atom passed through as extradata
x += (36).to_bytes(4, byteorder='big') # 32 bits atom size
x += (codec_tag).encode() # 32 bits tag ('alac')
x += (ver).to_bytes(4, byteorder='big') # 32 bits tag version (0)
x += (spf).to_bytes(4, byteorder='big') # 32 bits samples per frame
x += (compat_ver).to_bytes(1, byteorder='big') # 8 bits compatible version (0)
x += (ss).to_bytes(1, byteorder='big') # 8 bits sample size
x += (hist_mult).to_bytes(1, byteorder='big') # 8 bits history mult (40)
x += (init_hist).to_bytes(1, byteorder='big') # 8 bits initial history (10)
x += (rice_lmt).to_bytes(1, byteorder='big') # 8 bits rice param limit (14)
x += (cc).to_bytes(1, byteorder='big') # 8 bits channels
x += (max_run).to_bytes(2, byteorder='big') # 16 bits maxRun (255)
x += (mcfs).to_bytes(4, byteorder='big') # 32 bits max coded frame size (0 means unknown)
x += (abr).to_bytes(4, byteorder='big') # 32 bits average bitrate (0 means unknown)
x += (sr).to_bytes(4, byteorder='big') # 32 bits samplerate
return x

def init_audio_sink(self):
codecLatencySec = 0
self.pa = pyaudio.PyAudio()
Expand All @@ -298,13 +304,16 @@ def init_audio_sink(self):
# codec = None
ed = None
if self.audio_format == AirplayAudFmt.ALAC_44100_16_2.value:
ed = self.set_alac_extradata(self, sr=44100, ss=16, cc=2)
ed = AudioSetup(codec_tag='alac', sr=44100, ss=16, cc=2).get_extra_data()
elif self.audio_format == AirplayAudFmt.ALAC_44100_24_2.value:
ed = self.set_alac_extradata(self, sr=44100, ss=24, cc=2)
ed = AudioSetup(codec_tag='alac', sr=44100, ss=24, cc=2).get_extra_data()
elif self.audio_format == AirplayAudFmt.ALAC_48000_16_2.value:
ed = self.set_alac_extradata(self, sr=48000, ss=16, cc=2)
ed = AudioSetup(codec_tag='alac', sr=48000, ss=16, cc=2).get_extra_data()
elif self.audio_format == AirplayAudFmt.ALAC_48000_24_2.value:
ed = self.set_alac_extradata(self, sr=48000, ss=24, cc=2)
ed = AudioSetup(codec_tag='alac', sr=48000, ss=24, cc=2).get_extra_data()

if self.audio_params:
ed = self.audio_params.get_extra_data()

if 'ALAC' in self.af:
self.codec = av.codec.Codec('alac', 'r')
Expand Down Expand Up @@ -409,8 +418,8 @@ def run(self, parent_reader_connection):
player_thread.start()

@classmethod
def spawn(cls, session_key, audio_format, buff, iv=None, isDebug=False):
audio = cls(session_key, audio_format, buff, iv, isDebug)
def spawn(cls, session_key, audio_format, buff, iv=None, isDebug=False, aud_params: AudioSetup = None):
audio = cls(session_key, audio_format, buff, iv, isDebug, aud_params)
# This pipe is reachable from receiver
parent_reader_connection, audio.audio_connection = multiprocessing.Pipe()
mainprocess = multiprocessing.Process(target=audio.run, args=(parent_reader_connection,))
Expand All @@ -421,8 +430,8 @@ def spawn(cls, session_key, audio_format, buff, iv=None, isDebug=False):

class AudioRealtime(Audio):

def __init__(self, session_key, audio_format, buff, iv, isDebug=False):
super(AudioRealtime, self).__init__(session_key, audio_format, buff, iv, isDebug)
def __init__(self, session_key, audio_format, buff, iv, isDebug=False, aud_params: AudioSetup = None):
super(AudioRealtime, self).__init__(session_key, audio_format, buff, iv, isDebug, aud_params)
self.isDebug = isDebug
self.socket = get_free_udp_socket()
self.port = self.socket.getsockname()[1]
Expand Down Expand Up @@ -455,8 +464,8 @@ def serve(self, playerconn):


class AudioBuffered(Audio):
def __init__(self, session_key, audio_format, buff, iv=None, isDebug=False):
super(AudioBuffered, self).__init__(session_key, audio_format, buff, iv, isDebug)
def __init__(self, session_key, audio_format, buff, iv=None, isDebug=False, aud_params: AudioSetup = None):
super(AudioBuffered, self).__init__(session_key, audio_format, buff, iv, isDebug, aud_params)
self.isDebug = isDebug
if self.isDebug:
self.ab_file_logger = get_file_logger("AudioBuffered", level="DEBUG")
Expand Down
1 change: 0 additions & 1 deletion ap2/connections/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def serve(self):
except BrokenPipeError:
pass
finally:
conn.close()
sock.close()
if self.isDebug:
self.logger.debug(f"Close connection to {addr[0]}:{addr[1]}")
Expand Down
6 changes: 3 additions & 3 deletions ap2/connections/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Stream:
REALTIME = 96
BUFFERED = 103

def __init__(self, stream, buff, isDebug=False):
def __init__(self, stream, buff, isDebug=False, aud_params=None):
# self.audioMode = stream["audioMode"] # default|moviePlayback
self.isDebug = isDebug
self.audio_format = stream["audioFormat"]
Expand All @@ -31,10 +31,10 @@ def __init__(self, stream, buff, isDebug=False):
self.latency_min = stream["latencyMin"]
self.latency_max = stream["latencyMax"]
self.data_port, self.data_proc, self.audio_connection = AudioRealtime.spawn(
self.session_key, self.audio_format, buff, self.session_iv, isDebug=self.isDebug)
self.session_key, self.audio_format, buff, self.session_iv, isDebug=self.isDebug, aud_params=None)
elif self.type == Stream.BUFFERED:
self.data_port, self.data_proc, self.audio_connection = AudioBuffered.spawn(
self.session_key, self.audio_format, buff, iv=None, isDebug=self.isDebug)
self.session_key, self.audio_format, buff, iv=None, isDebug=self.isDebug, aud_params=None)

def teardown(self):
self.data_proc.terminate()
Expand Down

0 comments on commit 52f827e

Please sign in to comment.