pyVoIP icon indicating copy to clipboard operation
pyVoIP copied to clipboard

Announcement board example returns slow audio

Open aerilox opened this issue 2 years ago • 6 comments

File is Microsoft WAV 8 bits unsigned 8000 Hz mono (64kbit/s). When receiving a call audio is returned much too slow with audible breaks of silence between packets. Codec auto selected is PCMA. Any help apprecited.

-- File Name : test.wav Channels : 1 Sample Rate : 8000 Precision : 8-bit Duration : 00:00:26.54 = 212328 samples ~ 1990.58 CDDA sectors File Size : 212k Bit Rate : 64.0k Sample Encoding: 8-bit Unsigned Integer PCM

aerilox avatar Mar 12 '23 20:03 aerilox

Are you running on Windows?

tayler6000 avatar Mar 15 '23 05:03 tayler6000

Yes Win 10 and Py3.8. I found on this particular PC that time.sleep makes either a too slow (>0.0145s) or too fast (<0.0145s) frame transmission rate. It seems to be a hardware implementation issue. On other PCs the precision seems to be closer to 1 ms. It would probably need a threading.Timer to make it work across the board. There is a new more precise implementation of sleep in Py3.11. I will try that one, too. https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep

aerilox avatar Mar 15 '23 06:03 aerilox

Update: time.monotonic_ns is broken, too Not really nice, but this resolves the issue Now trying to find a way to successfully send a BYE to the SIP server once the announcement is over ...

last_sent = time.time()
........
# Calculate how long it took to generate this packet.
# Then how long we should wait to send the next, then devide by 2.
delay = (1 / self.preference.rate) * 160
#sleep_time = max(0, delay - ((time.monotonic_ns() - last_sent) / 1000000000)
sleep_time = last_sent+delay
while time.time()<sleep_time:
    pass

aerilox avatar Mar 15 '23 09:03 aerilox

It is recommended to avoid time.time as it can break from things like leap seconds, daylight savings, change of time zones, or just any other time changes on the system. time.monotonic is just the CPU seconds running counter, and therefore can never go backward, or leap forwards. time.monotonic_ns is only as precise as the CPU and python lets it be, but it is generally the best option.

I also noticed Windows has weird issues with this timing, even more strangely it seems to be computer specific. I created the variable pyVoIP.TRANSMIT_DELAY_REDUCTION which has the following docstring:

"""
The higher this variable is, the more often RTP packets are sent.
This should only ever need to be 0.0. However, when testing on Windows,
there has sometimes been jittering, setting this to 0.75 fixed this in testing.
"""

It states 0.75 was found in testing, and that is true, but when testing on a different Windows machine later, I found a different number worked on that machine. Hope that helps!

tayler6000 avatar May 09 '23 03:05 tayler6000

Looking into this issue a bit more, sleep continues to be horribly flawed. One StackOverflow post references the new documentation to the Python 3.11 sleep function which you referenced. However, the line The suspension time may be longer than requested by an arbitrary amount, because of the scheduling of other activity in the system. To my understanding, this is because sleep releases the CPU temporarily, and we have to wait for the CPU to be ready for us again.

A different StackOverflow post references time.perf_counter and the Python documentation states:

Return the value (in fractional seconds) of a performance counter, i.e. a clock with the highest available resolution to measure a short duration. It does include time elapsed during sleep and is system-wide. The reference point of the returned value is undefined, so that only the difference between the results of two calls is valid.

The comments on that post are glowing for the performance of that method of sleeping. Which is close to the method you've written.

The problem with while True: pass is that the CPU hits 100% almost instantly. That is mentioned in the pyVoIP docs and is even pointed out in this comment.

All that being said, I think this is the correct approach to solving this problem. The best way to help with CPU usage hitting 100% is to give it something to do. So I think I'm going to try something to the effect of:

last_sent = time.perf_counter_ns()
packet_prepared = False
while self.NSD:
    if not packet_prepared:
        ...
        packet_prepared = True

    if time.perf_counter_ns() >= delay:
        send(packet)
        last_sent = time.perf_counter_ns()
        packed_prepared = False

This would allow us to do all the computations necessary for the next packet while we wait and we can send it immediately when it's needed.

P.S. For the writing of about half this comment and all of the last one I originally thought my code did do something to the effect of while time.time()<sleep_time: pass I realized that is not the case currently. I think it used to, but I must have changed it due to the CPU usage issue. So apologize for any change in tone halfway through this message. This was an extremely helpful issue.

tayler6000 avatar May 09 '23 04:05 tayler6000

Switching from python 3.9 to 3.11 was a quick fix for me

ostwilkens avatar Aug 30 '23 09:08 ostwilkens