Пример #1
0
    def data_received(self, data):
        "Read fifo indefinitely and push data into queue"

        # TODO: Buffer needs to be only twice the size of the data
        # and could be handled without allocations/deallocations.
        # TODO: Use bytearray()
        self.buffer += data

        while len(self.buffer) >= self.audio_config.chunk_size:
            chunk = self.buffer[:self.audio_config.chunk_size]
            self.buffer = self.buffer[self.audio_config.chunk_size:]

            # Detect the end of current silence
            if self.silence_detect is True:
                if any(chunk):
                    self.silence_detect = 0
                    print("Silence - end")
                    now = time_machine.now()
                    if not self.stream_time or self.stream_time < now:
                        self.stream_time = now
                else:
                    # Still silence
                    continue
            else:
                # Heuristic detection of silence start
                if chunk[0] == 0 and chunk[-1] == 0:
                    self.silence_detect += 1
                else:
                    self.silence_detect = 0

                # Silence too long - stop transmission
                if self.silence_detect > self.SILENCE_TRESHOLD:
                    if any(chunk):  # Accurate check
                        self.silence_detect = 0
                    else:
                        print("Silence - start")
                        self.silence_detect = True
                        continue

            if self.stream_time is None:
                self.stream_time = time_machine.now()
            else:
                self.stream_time += self.audio_config.chunk_time
            self.sample_queue.put_nowait((self.stream_time, chunk))

        # Warning - might happen on slow UDP output sink
        if self.sample_queue.qsize() > 600:
            s = "WARNING: Samples in queue: %d - slow UDP transmission or eager input."
            s = s % self.sample_queue.qsize()
            print(s)

        if self.stream_time is not None:
            diff = self.stream_time - time_machine.now()
            if diff < min(-self.audio_config.latency_ms / 2, -1):
                print("WARNING: Input underflow.")
                self.stream_time = None
Пример #2
0
    def datagram_received(self, data, addr):
        "Handle incoming datagram - audio chunk, or status packet"
        header = data[:2]
        mark = data[2:4]
        chunk = data[4:]
        if header == Packetizer.HEADER_RAW_AUDIO:
            pass
        elif header == Packetizer.HEADER_COMPRESSED_AUDIO:
            try:
                chunk = zlib.decompress(chunk)
            except zlib.error:
                print("WARNING: Invalid compressed data - dropping")
                return
        elif header == Packetizer.HEADER_STATUS:
            # Status header!
            self._handle_status(data)
            return
        else:
            print("Invalid header!")
            return

        if self.chunk_queue.ignore_audio_packets != 0:
            self.chunk_queue.ignore_audio_packets -= 1
            return

        mark = time_machine.to_absolute_timestamp(time_machine.now(), mark)
        item = (mark, chunk)

        # Count received audio-chunks
        self.chunk_queue.chunk_no += 1

        self.chunk_queue.chunk_list.append((self.chunk_queue.CMD_AUDIO, item))
        self.chunk_queue.chunk_available.set()
Пример #3
0
    def _handle_status(self, data):

        if len(data) < (2 + 20):
            print("WARNING: Status header too short")

        (sender_timestamp, sender_chunk_no, rate, sample, channels, chunk_size,
         latency_ms) = struct.unpack('dIHBBHH',
                                     data[2:2 + 8 + 4 + 2 + 1 + 1 + 2 + 2])

        q = self.chunk_queue

        # Handle timestamp
        now = time_machine.now()
        self.stats.network_latency = (now - sender_timestamp)

        # Handle audio configuration
        audio_config = AudioConfig(rate,
                                   sample,
                                   channels,
                                   latency_ms,
                                   sink_latency_ms=self.sink_latency_ms)
        audio_config.chunk_size = chunk_size

        if audio_config != self.audio_config:
            # If changed - sent further
            q.chunk_list.append((q.CMD_CFG, audio_config))
            self.audio_config = audio_config

        # Handle dropped packets

        # If this is first status packet
        # or low sender_chunk_no indicates that sender was restarted
        if q.last_sender_chunk_no is None or sender_chunk_no < 1500:
            q.last_sender_chunk_no = sender_chunk_no
            q.chunk_no = 0
            return

        # How many chunks were transmitted since previous status packet?
        chunks_sent = sender_chunk_no - q.last_sender_chunk_no
        dropped = chunks_sent - q.chunk_no

        q.last_sender_chunk_no = sender_chunk_no
        q.chunk_no = 0

        self.stats.network_drops += dropped
        if dropped < 0:
            print("WARNING: More pkts received than sent! "
                  "You are receiving multiple streams or duplicates.")
        elif dropped > 0:
            q.chunk_list.append((q.CMD_DROPS, dropped))
            q.chunk_available.set()
Пример #4
0
    async def packetize(self):
        "Read pre-chunked samples from queue and send them over UDP"
        start = time()
        # Numer of sent packets
        stat_pkts = 0
        # Chunk number as seen by receivers
        chunk_no = 0
        bytes_sent = 0
        bytes_raw = 0
        cancelled_compressions = 0

        # Current speed measurement
        recent = 0
        recent_bytes = 0
        recent_start = time()

        # For local playback
        if self.chunk_queue is not None:
            self.chunk_queue.chunk_list.append((self.chunk_queue.CMD_CFG,
                                                self.audio_config))

        while not self.stop:
            # Block until samples are read by the reader.
            stream_time, chunk = await self.reader.get_next_chunk()

            # Handle input flood, to keep us within timemarking range.
            now = time_machine.now()
            diff = stream_time - now

            if diff > 0.5:
                print("Waiting to synchronize input stream. Stream-real, difference is",
                      diff)
                await asyncio.sleep(0.4)
            elif diff < -5:
                print("Input stream is lagging", diff)

            relative = stream_time
            future_ts, mark = time_machine.get_timemark(relative,
                                                        self.audio_config.latency_s)

            if self.chunk_queue is not None:
                item = (future_ts, chunk)
                self.chunk_queue.chunk_list.append((self.chunk_queue.CMD_AUDIO,
                                                    item))
                self.chunk_queue.chunk_available.set()

            chunk_len = len(chunk)
            if self.compress is not False:
                chunk_compressed = zlib.compress(chunk, self.compress)
                if len(chunk_compressed) < chunk_len:
                    # Go with compressed
                    dgram = Packetizer.HEADER_COMPRESSED_AUDIO + mark + chunk_compressed
                else:
                    # Cancel - compressed might not fit to packet
                    dgram = Packetizer.HEADER_RAW_AUDIO + mark + chunk
                    cancelled_compressions += 1
            else:
                dgram = Packetizer.HEADER_RAW_AUDIO + mark + chunk

            dgram_len = len(dgram)
            chunk_no += 1
            recent += 1
            for destination in self.destinations:
                try:
                    self.sock.sendto(dgram, destination)
                    bytes_sent += dgram_len
                    recent_bytes += dgram_len
                    bytes_raw += chunk_len + 4
                    stat_pkts += 1
                except OSError as ex:
                    import errno
                    if ex.errno == errno.EMSGSIZE:
                        s = "WARNING: UDP datagram size (%d) is too big for your network MTU"
                        s = s % len(dgram)
                        print(s)
                        new_size = self.reader.decrement_payload_size()
                        print("Trying MTU detection. New payload size is %d" % new_size)
                        break

            # Send small status datagram every 124 chunks - ~ 1 second
            # It's used to determine if some frames were lost on the network
            # and therefore if output buffer resync is required.
            # Contains the audio configuration too.
            if chunk_no % 124 == 0:
                dgram = self._create_status_packet(chunk_no)
                for destination in self.destinations:
                    self.sock.sendto(dgram, destination)

            if recent >= 100:
                # Main status line
                now = time()
                took_total = now - start
                took_recent = now - recent_start
                s = ("STATE: dsts=%d total: pkts=%d kB=%d time=%d "
                     "kB/s: avg=%.3f cur=%.3f")
                s = s % (
                    len(self.destinations),
                    stat_pkts,
                    bytes_sent / 1024, took_total,
                    bytes_sent / took_total / 1024,
                    recent_bytes / took_recent / 1024,
                )
                if self.compress:
                    s += ' compress_ratio=%.3f cancelled=%d'
                    s = s % (bytes_sent / bytes_raw, cancelled_compressions)
                print(s)

                recent_start = now
                recent_bytes = 0
                recent = 0
        print("- Packetizer stop")
Пример #5
0
    async def chunk_player(self):
        "Reads asynchronously chunks from the list and plays them"

        cnt = 0

        # Chunk/s stat
        recent_start = time()
        recent = 0

        mid_tolerance_s = self.tolerance_ms / 2 / 1000
        one_ms = 1 / 1000.0

        max_delay = 5

        while not self.stop:
            if not self.chunk_queue.chunk_list:

                if self.audio_config is not None:
                    print("Queue empty - waiting")

                self.chunk_queue.chunk_available.clear()
                await self.chunk_queue.chunk_available.wait()

                recent_start = time()
                recent = 0
                if self.audio_config is not None:
                    await asyncio.sleep(self.audio_config.latency_ms / 1000 / 4
                                        )
                    print("Got stream flowing. q_len=%d" %
                          len(self.chunk_queue.chunk_list))
                continue

            cmd, item = self.chunk_queue.chunk_list.popleft()

            if cmd == self.chunk_queue.CMD_CFG:
                print("Got new configuration - opening audio stream")
                self.clear_state()
                self.audio_config = item
                if self.stream:
                    self._close_stream()
                self._open_stream()
                # Calculate maximum sensible delay in given configuration
                max_delay = (2000 + self.audio_config.sink_latency_ms +
                             self.audio_config.latency_ms) / 1000
                print("Assuming maximum chunk delay of %.2fms in this setup" %
                      (max_delay * 1000))
                continue
            elif cmd == self.chunk_queue.CMD_DROPS:
                if item > 200:
                    print("Recovering after a huge packet loss of %d packets" %
                          item)
                    self.clear_state()
                else:
                    # Just slowly resync
                    self.silence_to_insert += item
                continue

            # CMD_AUDIO

            if self.stream is None:
                # No output, no playing.
                continue

            mark, chunk = item
            desired_time = mark - self.audio_config.sink_latency_ms

            # 0) We got the next chunk to be played
            now = time_machine.now()

            # Negative when we're lagging behind.
            delay = desired_time - now

            self.stat_total_delay += delay

            recent += 1
            cnt += 1

            # Probabilistic drop of lagging chunks to get back on track.
            # Probability of drop is higher, the more chunk lags behind current
            # time. Similar to the RED algorithm in TCP congestion.
            if delay < -mid_tolerance_s:
                over = -delay - mid_tolerance_s
                prob = over / mid_tolerance_s
                if random.random() < prob:
                    s = "Drop chunk: q_len=%2d delay=%.1fms < 0. tolerance=%.1fms: P=%.2f"
                    s = s % (len(self.chunk_queue.chunk_list), delay * 1000,
                             self.tolerance_ms, prob)
                    print(s)
                    self.stat_time_drops += 1
                    continue

            elif delay > max_delay:
                # Probably we hanged for so long time that the time recovering
                # mechanism rolled over. Recover
                print(
                    "Huge recovery - delay of %.2f exceeds the max delay of %.2f"
                    % (delay, max_delay))
                self.clear_state()
                continue

            # If chunk is in the future - wait until it's within the tolerance
            elif delay > one_ms:
                to_wait = max(one_ms, delay - one_ms)
                await asyncio.sleep(to_wait)

            # Wait until we can write chunk into output buffer. This might
            # delay us too much - the probabilistic dropping mechanism will kick
            # in.
            times = 0
            while True:
                buffer_space = self.stream.get_write_available()
                if buffer_space < self.chunk_frames:
                    self.stat_output_delays += 1
                    await asyncio.sleep(one_ms)
                    times += 1
                    if times > 200:
                        print("Hey, the output is STUCK!")
                        await asyncio.sleep(1)
                        break
                    continue
                self.stream.write(chunk)
                break

            # Main status line
            if recent > 200:
                frames_in_chunk = len(chunk) / self.audio_config.frame_size

                took = time() - recent_start
                chunks_per_s = recent / took

                if self.receiver is not None:
                    network_latency = self.receiver.stat_network_latency
                    network_drops = self.receiver.stat_network_drops
                else:
                    network_latency = 0
                    network_drops = 0

                s = ("STAT: chunks: q_len=%-3d bs=%4.1f "
                     "ch/s=%5.1f "
                     "net lat: %-5.1fms "
                     "avg_delay=%-5.2f drops: time=%d net=%d out_delay=%d")
                s = s % (
                    len(self.chunk_queue.chunk_list),
                    buffer_space / frames_in_chunk,
                    chunks_per_s,
                    1000.0 * network_latency,
                    1000.0 * self.stat_total_delay / cnt,
                    self.stat_time_drops,
                    network_drops,
                    self.stat_output_delays,
                )
                print(s)

                recent = 0
                recent_start = time()

                # Warnings
                if self.receiver is not None:
                    if self.receiver.stat_network_latency > 1:
                        print("WARNING: Your network latency seems HUGE. "
                              "Are the clocks synchronised?")
                    elif self.receiver.stat_network_latency <= -0.05:
                        print("WARNING: You either exceeded the speed of "
                              "light or have unsynchronised clocks")

        print("- Finishing chunk player")
Пример #6
0
    async def _handle_cmd_audio(self, item):
        "Handle chunk playback"
        mid_tolerance_s = self.tolerance_ms / 2 / 1000
        one_ms = 1/1000.0

        mark, chunk = item
        desired_time = mark - self.audio_output.config.sink_latency_s

        # 0) We got the next chunk to be played
        now = time_machine.now()

        # Negative when we're lagging behind.
        delay = desired_time - now

        self.stats.total_delay += delay
        self.stats.total_chunks += 1

        # Probabilistic drop of lagging chunks to get back on track.
        # Probability of drop is higher, the more chunk lags behind current
        # time. Similar to the RED algorithm in TCP congestion.
        if delay < -mid_tolerance_s:
            over = -delay - mid_tolerance_s
            prob = over / mid_tolerance_s
            if random.random() < prob:
                s = "Drop chunk: q_len=%2d delay=%.1fms < 0. tolerance=%.1fms: P=%.2f"
                s = s % (len(self.chunk_queue.chunk_list),
                         delay * 1000, self.tolerance_ms, prob)
                print(s)
                self.stats.time_drops += 1
                return

        elif delay > self.max_delay:
            # Probably we hanged for so long time that the time recovering
            # mechanism rolled over. Recover
            print("Huge recovery - delay of %.2f exceeds the max delay of %.2f" % (
                delay, self.max_delay))
            self.clear_state()
            return

        # If chunk is in the future - wait until it's within the tolerance
        elif delay > one_ms:
            to_wait = max(one_ms, delay - one_ms)
            await asyncio.sleep(to_wait)

        # Wait until we can write chunk into output buffer. This might
        # delay us too much - the probabilistic dropping mechanism will kick
        # in.
        times = 0
        while True:
            buffer_space = self.audio_output.get_write_available()
            if buffer_space < self.audio_output.chunk_frames:
                self.stats.output_delays += 1
                await asyncio.sleep(one_ms)
                times += 1
                if times > 200:
                    print("Hey, the output is STUCK!")
                    await asyncio.sleep(1)
                    break
                continue
            self.audio_output.write(chunk)
            return