diff --git a/ap2-receiver.py b/ap2-receiver.py index b636f90..0b2df88 100644 --- a/ap2-receiver.py +++ b/ap2-receiver.py @@ -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 @@ -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' @@ -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(';')) @@ -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) @@ -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) diff --git a/ap2/connections/audio.py b/ap2/connections/audio.py index d3e0163..a5072cd 100644 --- a/ap2/connections/audio.py +++ b/ap2/connections/audio.py @@ -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): @@ -245,7 +273,7 @@ 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") @@ -253,6 +281,7 @@ def __init__(self, session_key, audio_format, buff_size, session_iv=None, isDebu 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) @@ -260,29 +289,6 @@ def __init__(self, session_key, audio_format, buff_size, session_iv=None, isDebu 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() @@ -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') @@ -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,)) @@ -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] @@ -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") diff --git a/ap2/connections/event.py b/ap2/connections/event.py index 21c6f20..5a7c84f 100644 --- a/ap2/connections/event.py +++ b/ap2/connections/event.py @@ -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]}") diff --git a/ap2/connections/stream.py b/ap2/connections/stream.py index 08d3bf1..298d710 100644 --- a/ap2/connections/stream.py +++ b/ap2/connections/stream.py @@ -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"] @@ -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()