pyVoIP
pyVoIP copied to clipboard
Connected call has bad quality
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()
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).
@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.
@haran00 The only way to tweak the transmission speed is through the variable I mentioned above.
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()
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).
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
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.
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:
- Don’t use
encode_pcmu
for PCMAdef 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) )
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.