pyVoIP icon indicating copy to clipboard operation
pyVoIP copied to clipboard

Connected call has bad quality

Open Ondrej-Kuchar opened this issue 1 year ago • 10 comments

I would like to use your library for IVR testing. Unfortunately I'm not able to reach usable level of call quality (and it isn't about 8k sampling rate) in the call. I used your examples to check ability of the library, but received sound is bad. It is breaking. I used fayn.cz VoIP provider - they use standard SIP gateway for voice calls: sip.fayn.cz Testing PC is on local network behind more NATs from backbone perspective.

In attachment is original WAV (Unsigned 8-bit PCM, 8kHz, Mono) and recorded sound from connected mobile phone (iphone).

Note: Currently I tried only inbound call, but finally I want to use outbound call (your library should call IVR and interact with IVR commands).

here is my code:

def answer(call):
    try:
        print("INBOUND CALL PICKED UP!")
        f = wave.open('.\\pyVoIP\\AVS_WAV8x8x1.wav', 'rb')
        frames = f.getnframes()
        data = f.readframes(frames)
        f.close()
        print("FRAMES:",frames)

        call.answer()
        call.write_audio(data)  # This writes the audio data to the transmit buffer
        print("Data are in buffer and playing")

        stop = time.time() + (frames / 8000)  # frames/8000 is the length of the audio in seconds

        while time.time() <= stop and call.state == CallState.ANSWERED:
            time.sleep(0.1)
        print("Message finished")

        call.hangup()
    except InvalidStateError:
        pass
    except Exception as e:
        print("ERROR - Obecna chyba", e.__class__)
        call.hangup()
  
if __name__ == "__main__":
    phone=VoIPPhone("213.168.165.14", 5060, "XXXXXXXXX", "XXXXXXXXX", callCallback=answer, rtpPortLow=10000, rtpPortHigh=20000)
    phone.start()
    input('Please kill the app or press enter to disable the phone')
    phone.stop()

SourceFile&RecordFromPhone.zip

Ondrej-Kuchar avatar Oct 02 '23 13:10 Ondrej-Kuchar

I get poor quality with the functions 'read_audio' and 'write_audio' too. Are there any parameters to tweak to improve this? What is the reason for this?

Using Linux (Ubuntu).

haran00 avatar Jan 04 '24 12:01 haran00

@Ondrej-Kuchar I've confirmed your file is correctly formatted. I have sometimes had this issue on Windows myself so I would recommend changing pyVoIP.TRANSMIT_DELAY_REDUCTION Here is the documentation or it:

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, but you may need to tinker with this number on a per-system basis.

You can also try using 2.0.0a3 and see if you have any better results with that.

tayler6000 avatar Jan 12 '24 17:01 tayler6000

@haran00 The only way to tweak the transmission speed is through the variable I mentioned above.

tayler6000 avatar Jan 12 '24 17:01 tayler6000

change value of pyVoIP.TRANSMIT_DELAY_REDUCTION cannot handle sound quality. Please using my code:

from pyVoIP.VoIP import VoIPPhone, InvalidStateError, CallState import time import wave from pydub import AudioSegment from pydub.silence import split_on_silence def answer(call): try: sound = AudioSegment.from_file("/home/ivr.wav", format="wav") chunks = split_on_silence(sound, # experiment with this value for your target audio file min_silence_len = 500, # adjust this per requirement silence_thresh = sound.dBFS-14, # keep the silence for 1 second, adjustable as well keep_silence=500, ) call.answer() call.write_audio(sound._data) stop = time.time() + sound.duration_seconds while time.time() <= stop and call.state == CallState.ANSWERED: time.sleep(0.1) call.hangup() except InvalidStateError: pass except: call.hangup()

toandd-it avatar Mar 14 '24 11:03 toandd-it

Probably replacing two functions in RTP.py can help.

In my application this works very well. See recorded call with your audio sample attached: RecordedCall(fromMicroSIPsoftphone).zip

Code to change is:

1) Don’t use encode_pcmu for PCMA

    def encode_packet(self, payload: bytes) -> bytes:  
        if self.preference == PayloadType.PCMU:  
            return self.encode_pcmu(payload)  
        elif self.preference == PayloadType.PCMA:  
#            return self.encode_pcmu(payload)  
            return self.encode_pcma(payload)  
        else:  
            raise RTPParseError(  
                "Unsupported codec (encode): " + str(self.preference)  
            )  

2) To calculate the send-time for the next packet:

  • do not measure the code execution time, but
  • just wait until the time (20 ms) has come (pyVoIP.TRANSMIT_DELAY_REDUCTION is not needed anymore)
  • especially: do not rely on the accuracy of time.sleep in Windows !

The latter can differ for several milliseconds (see how-accurate-is-pythons-time-sleep) and so cause gaps in packet transmission. These will then result in cracking and slowing down on the receiver side. (as you can hear in RecordedCall(fromIPhoneViaStandardMobileNetwork).m4a attached by @Ondrej-Kuchar)

    def trans(self) -> None:  
        delay = (1 / self.preference.rate) * 160   # 20 ms  
        next_send = time.perf_counter()  
  
        while self.NSD:  
            payload = self.pmout.read()  
            payload = self.encode_packet(payload)  
            packet = b"\x80"  # RFC 1889 Version 2 No Padding Extension or CC.  
            packet += chr(int(self.preference)).encode("utf8")  
            try:  
                packet += self.outSequence.to_bytes(2, byteorder="big")  
            except OverflowError:  # after 65536 packets = 13010 sec = 21 min  
                self.outSequence = 0  
                packet += self.outSequence.to_bytes(2, byteorder="big")  
            try:  
                packet += self.outTimestamp.to_bytes(4, byteorder="big")  
            except OverflowError:  # after 26.843.545 packets = 536870 sec  
                self.outTimestamp = 0  
                packet += self.outTimestamp.to_bytes(4, byteorder="big")  
            packet += self.outSSRC.to_bytes(4, byteorder="big")  
            packet += payload  
  
            # debug(payload)  
  
            to_wait = next_send - time.perf_counter()  
            if to_wait > 0.01: # more than 10 ms to wait     20 ms - 50...100 usec, if  "while self.rebuilding: time.sleep(0.01)" did not occur  
                time.sleep(0.005) # wait a first part of it non-CPU-intensive, but with time-sleep inaccuracy  
                                  # (depending on OS, see https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep)  
                                  # max. 5 ms on Windows 11 and Phyton 3.10  
  
            while True:  # wait the rest of it accurately, but CPU-intensive   
                delta = time.perf_counter() - next_send  
                if delta >= 0:  
                    break;  
  
            next_send = time.perf_counter() + delay - delta # - delta means: "send this package earlier if the last one was too late"  
            if self.NSD:  
                try:  
                    self.sout.sendto(packet, (self.outIP, self.outPort))  
                except OSError:  
                    warnings.warn(  
                        "RTP Packet failed to send!",  
                        RuntimeWarning,  
                        stacklevel=2,  
                    )  
  
            self.outSequence += 1  
            self.outTimestamp += len(payload)  

This change probably could also resolve #159 and the part of #167 where @jchatin and @FrankMeyerEDV commented that the sound was scratching.

@tayler6000 You might also test it on your side and consider to take it into your future code for both master and development (no 1 is already fixed there).

I refrain from creating a PR, because I have made several other changes that would not fit in a single PR. (BTW: My PR #245 is still pending).

obrain17 avatar Mar 20 '24 11:03 obrain17

This change probably could also resolve #159 and the part of #167 where @jchatin and @FrankMeyerEDV commented that the sound was scratching.

Sound quality is better with your fix in the trans() method of RTP class !

Thanks @obrain17

jchatin avatar Mar 20 '24 17:03 jchatin

Here a new code without extra CPU utilization.

time.sleep() is called in a loop until it has waited at least until the given send_time. So a CPU intensive wait afterwards is not needed.

In case time.sleep() has waited is too long (e.g due to its inaccuracy) the next send_time will be respectively earlier.

This way over consecutive packages the average cycle time will be kept at 20 ms.

    def trans(self) -> None:  
        cycle = (1 / self.preference.rate) * 160   # 20 ms
        send_time = time.perf_counter()

        while self.NSD:
            payload = self.pmout.read()
            payload = self.encode_packet(payload)
            packet = b"\x80"  # RFC 1889 Version 2 No Padding Extension or CC.
            packet += chr(int(self.preference)).encode("utf8")
            try:
                packet += self.outSequence.to_bytes(2, byteorder="big")
            except OverflowError:
                self.outSequence = 0
                packet += self.outSequence.to_bytes(2, byteorder="big")
            try:
                packet += self.outTimestamp.to_bytes(4, byteorder="big")
            except OverflowError:
                self.outTimestamp = 0
                packet += self.outTimestamp.to_bytes(4, byteorder="big")
            packet += self.outSSRC.to_bytes(4, byteorder="big")
            packet += payload

            # debug(payload)

            while True: # firstly wait non-CPU-intensive, but with time-sleep inaccuracy
                        # (depending on OS, see https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep)
                to_wait = send_time - time.perf_counter()
                if (to_wait) <= 0:  # loop until no time due to the inaccuracy of time.sleep is remaining => always option b) below is taken
                    break;
                time.sleep(to_wait)

            while True:  # now correct the time-sleep inaccuracy, by either:
                         # a) waiting the remaining time accurately, but CPU-intensive - or
                         # b) set delta > 0 to have the next packet sent earlier
                delta = time.perf_counter() - send_time
                if delta >= 0:
                    break;

            send_time = time.perf_counter() + cycle - delta # delta means to keep the 20 ms cycle: "send the next package earlier if his one was too late"
            if self.NSD:
                try:
                    self.sout.sendto(packet, (self.outIP, self.outPort))
                except OSError:
                    warnings.warn(
                        "RTP Packet failed to send!",
                        RuntimeWarning,
                        stacklevel=2,
                    )

            self.outSequence += 1
            self.outTimestamp += len(payload)

I have tested this in both Linux and Windows with best results.

obrain17 avatar Mar 26 '24 13:03 obrain17

Probably replacing two functions in RTP.py can help.

In my application this works very well. See recorded call with your audio sample attached: RecordedCall(fromMicroSIPsoftphone).zip

Code to change is:

  1. Don’t use encode_pcmu for PCMA
    def encode_packet(self, payload: bytes) -> bytes:  
        if self.preference == PayloadType.PCMU:  
            return self.encode_pcmu(payload)  
        elif self.preference == PayloadType.PCMA:  
#            return self.encode_pcmu(payload)  
            return self.encode_pcma(payload)  
        else:  
            raise RTPParseError(  
                "Unsupported codec (encode): " + str(self.preference)  
            )  
  1. To calculate the send-time for the next packet:

    • do not measure the code execution time, but

    • just wait until the time (20 ms) has come (pyVoIP.TRANSMIT_DELAY_REDUCTION is not needed anymore)

    • especially: do not rely on the accuracy of time.sleep in Windows !

The latter can differ for several milliseconds (see how-accurate-is-pythons-time-sleep) and so cause gaps in packet transmission. These will then result in cracking and slowing down on the receiver side. (as you can hear in RecordedCall(fromIPhoneViaStandardMobileNetwork).m4a attached by @Ondrej-Kuchar)

    def trans(self) -> None:  
        delay = (1 / self.preference.rate) * 160   # 20 ms  
        next_send = time.perf_counter()  
  
        while self.NSD:  
            payload = self.pmout.read()  
            payload = self.encode_packet(payload)  
            packet = b"\x80"  # RFC 1889 Version 2 No Padding Extension or CC.  
            packet += chr(int(self.preference)).encode("utf8")  
            try:  
                packet += self.outSequence.to_bytes(2, byteorder="big")  
            except OverflowError:  # after 65536 packets = 13010 sec = 21 min  
                self.outSequence = 0  
                packet += self.outSequence.to_bytes(2, byteorder="big")  
            try:  
                packet += self.outTimestamp.to_bytes(4, byteorder="big")  
            except OverflowError:  # after 26.843.545 packets = 536870 sec  
                self.outTimestamp = 0  
                packet += self.outTimestamp.to_bytes(4, byteorder="big")  
            packet += self.outSSRC.to_bytes(4, byteorder="big")  
            packet += payload  
  
            # debug(payload)  
  
            to_wait = next_send - time.perf_counter()  
            if to_wait > 0.01: # more than 10 ms to wait     20 ms - 50...100 usec, if  "while self.rebuilding: time.sleep(0.01)" did not occur  
                time.sleep(0.005) # wait a first part of it non-CPU-intensive, but with time-sleep inaccuracy  
                                  # (depending on OS, see https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep)  
                                  # max. 5 ms on Windows 11 and Phyton 3.10  
  
            while True:  # wait the rest of it accurately, but CPU-intensive   
                delta = time.perf_counter() - next_send  
                if delta >= 0:  
                    break;  
  
            next_send = time.perf_counter() + delay - delta # - delta means: "send this package earlier if the last one was too late"  
            if self.NSD:  
                try:  
                    self.sout.sendto(packet, (self.outIP, self.outPort))  
                except OSError:  
                    warnings.warn(  
                        "RTP Packet failed to send!",  
                        RuntimeWarning,  
                        stacklevel=2,  
                    )  
  
            self.outSequence += 1  
            self.outTimestamp += len(payload)  

This change probably could also resolve #159 and the part of #167 where @jchatin and @FrankMeyerEDV commented that the sound was scratching.

@tayler6000 You might also test it on your side and consider to take it into your future code for both master and development (no 1 is already fixed there).

I refrain from creating a PR, because I have made several other changes that would not fit in a single PR. (BTW: My PR #245 is still pending).

I made PR #253 with your 1) findings anyway - works for me without further changes. Thank you - lets see when Tyler gets around.

agesagem avatar Mar 28 '24 10:03 agesagem