class ResponsiveVoiceTTS(TTS): works_offline = False audio_ext = "mp3" def __init__(self, config=None): config = config or {"lang": "en", "pitch": 0.5, "rate": 0.5, "vol": 1} super().__init__(config, validator=ResponsiveVoiceValidator(self), ssml_tags=[]) self.clear_cache() self.pitch = config.get("pitch", 0.5) self.rate = config.get("rate", 0.5) self.vol = config.get("vol", 1) if self.voice: clazz = get_voices()[self.voice] self.engine = clazz(pitch=self.pitch, rate=self.rate, vol=self.vol) else: gender = config.get("gender", "male") self.engine = ResponsiveVoice(lang=self.lang, gender=gender, pitch=self.pitch, rate=self.rate, vol=self.vol) def get_tts(self, sentence, wav_file): self.engine.get_mp3(sentence, wav_file) return (wav_file, None) # No phonemes def describe_voices(self): voice_clazzes = get_voices() voices = {} for voice in voice_clazzes: lang = voice_clazzes[voice]().lang.split("_#")[0] if lang not in voices: voices[lang] = [] if lang.split("-")[0] not in voices: voices[lang.split("-")[0]] = [] voices[lang].append(voice) voices[lang.split("-")[0]].append(voice) return voices
class BareSIP(Thread): def __init__(self, user, pwd, gateway, tts=None, debug=False, block=True, audiodriver="alsa,default", port=5060): self.debug = debug self.user = user self.pwd = pwd self.gateway = gateway #Learn the second one when it has to reasign a unique name #There is an in and an out client name self.jackNames = ['baresip'] if tts: self.tts = tts else: try: from responsive_voice import ResponsiveVoice self.tts = ResponsiveVoice(gender=ResponsiveVoice.MALE) except ImportError: logging.exception( "No responsive_voice module, some features will not work") self._login = "******".format(u=self.user, p=self.pwd, g=self.gateway) self._prev_output = "" self.running = False #Ready immediately, this is a localhost thing self.ready = True self.mic_muted = False self.abort = False self.current_call = None self._call_status = None self.audio = None self._ts = None self.jack_port = "baresip" cnfdir = os.path.join(tmpdir, "ScullerySIP" + str(port)) self.cnfdir = cnfdir try: #Remove any existing shutil.rmtree(self.cnfdir) except: pass os.mkdir(os.path.join(tmpdir, "ScullerySIP" + str(port))) self.cnfdir = cnfdir #Using the template, create a configuration dir for #the baresip instance we are about to make. f = os.path.join(os.path.dirname(__file__), "baresip_template") drivers = "module\t\talsa.so" if 'jack' in audiodriver: drivers = "module\t\tjack.so" for i in os.listdir(f): with open(os.path.join(f, i)) as fd: x = fd.read() x = x.replace("USERNAME", user) x = x.replace("AUDIODRIVER", audiodriver) x = x.replace("PORT", str(port)) x = x.replace("DRIVERS", str(drivers)) with open(os.path.join(cnfdir, i), "w") as fd: fd.write(x) self.baresip = pexpect.spawn('baresip', ["-f", cnfdir]) super().__init__() self.daemon = True self.start() if block: self.wait_until_ready() # properties @property def call_established(self): return self.call_status == "ESTABLISHED" @property def call_status(self): return self._call_status or "DISCONNECTED" # actions def do_command(self, action): if self.ready: action = str(action) self.baresip.sendline(action) else: LOG.warning(action + " not executed!") LOG.exception("NOT READY! please wait") def login(self): LOG.info("Adding account: " + self.user) self.baresip.sendline("/uanew " + self._login) def call(self, number): LOG.info("Dialling: " + number) self.do_command("/dial " + number) def hang(self): if self.current_call: LOG.info("Hanging: " + self.current_call) self.do_command("/hangup") self.current_call = None self._call_status = None else: LOG.error("No active call to hang") def hold(self): if self.current_call: LOG.info("Holding: " + self.current_call) self.do_command("/hold") else: LOG.error("No active call to hold") def resume(self): if self.current_call: LOG.info("Resuming " + self.current_call) self.do_command("/resume") else: LOG.error("No active call to resume") def mute_mic(self): if not self.call_established: LOG.error("Can not mute microphone while not in a call") return if not self.mic_muted: LOG.info("Muting mic") self.do_command("/mute") else: LOG.info("Mic already muted") def unmute_mic(self): if not self.call_established: LOG.error("Can not unmute microphone while not in a call") return if self.mic_muted: LOG.info("Unmuting mic") self.do_command("/mute") else: LOG.info("Mic already unmuted") def accept_call(self): self.do_command("/accept") status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status def list_calls(self): self.do_command("/listcalls") def check_call_status(self): self.do_command("/callstat") sleep(0.1) return self.call_status def quit(self): LOG.info("Exiting") if self.running: if self.current_call: self.hang() self.baresip.sendline("/quit") self.running = False self.current_call = None self._call_status = None self.abort = True def send_dtmf(self, number): from opentone import ToneGenerator number = str(number) for n in number: if n not in "0123456789": LOG.error("invalid dtmf tone") return LOG.info("Sending dtmf tones for " + number) dtmf = join(tempfile.gettempdir(), "DTMF" + number + ".wav") ToneGenerator().dtmf_to_wave(number, dtmf) self.send_audio(dtmf) def speak(self, speech): if not self.call_established: LOG.error("Speaking without an active call!") else: LOG.info("Sending TTS for " + speech) self.send_audio(self.tts.get_mp3(speech)) sleep(0.5) def send_audio(self, wav_file): if not self.call_established: LOG.error("Can't send audio without an active call!") return wav_file, duration = self.convert_audio(wav_file) # send audio stream LOG.info("transmitting audio") self.do_command("/ausrc aufile," + wav_file) # wait till playback ends sleep(duration - 0.5) # avoid baresip exiting self.do_command("/ausrc alsa,default") @staticmethod def convert_audio(input_file, outfile=None): from pydub import AudioSegment input_file = expanduser(input_file) sound = AudioSegment.from_file(input_file) sound += AudioSegment.silent(duration=500) # ensure minimum time # workaround baresip bug while sound.duration_seconds < 3: sound += AudioSegment.silent(duration=500) outfile = outfile or join(tempfile.gettempdir(), "pybaresip.wav") sound = sound.set_frame_rate(48000) sound = sound.set_channels(2) sound.export(outfile, format="wav") return outfile, sound.duration_seconds # this is played out loud over speakers def say(self, speech): if not self.call_established: LOG.warning("Speaking without an active call!") self.tts.say(speech, blocking=True) def play(self, audio_file, blocking=True): if not audio_file.endswith(".wav"): audio_file, duration = self.convert_audio(audio_file) self.audio = self._play_wav(audio_file, blocking=blocking) def stop_playing(self): if self.audio is not None: self.audio.kill() @staticmethod def _play_wav(wav_file, play_cmd="aplay %1", blocking=False): play_mp3_cmd = str(play_cmd).split(" ") for index, cmd in enumerate(play_mp3_cmd): if cmd == "%1": play_mp3_cmd[index] = wav_file if blocking: return subprocess.call(play_mp3_cmd) else: return subprocess.Popen(play_mp3_cmd) # events def handle_incoming_call(self, number): LOG.info("Incoming call: " + number) if self.call_established: LOG.info("already in a call, rejecting") sleep(0.1) self.do_command("b") else: LOG.info("default behaviour, rejecting call") sleep(0.1) self.do_command("b") def handle_call_rejected(self, number): LOG.info("Rejected incoming call: " + number) def handle_call_timestamp(self, timestr): LOG.info("Call time: " + timestr) def handle_call_status(self, status): if status != self._call_status: LOG.debug("Call Status: " + status) def handle_call_start(self): number = self.current_call LOG.info("Calling: " + number) def handle_call_ringing(self): number = self.current_call LOG.info(number + " is Ringing") def handle_call_established(self): LOG.info("Call established") def handle_call_ended(self, reason): LOG.info("Call ended") LOG.debug("Reason: " + reason) def _handle_no_accounts(self): LOG.debug("No accounts setup") self.login() def handle_login_success(self): LOG.info("Logged in!") def handle_login_failure(self): LOG.error("Log in failed!") self.quit() def handle_ready(self): LOG.info("Ready for instructions") def handle_mic_muted(self): LOG.info("Microphone muted") def handle_mic_unmuted(self): LOG.info("Microphone unmuted") def handle_audio_stream_failure(self): LOG.debug("Aborting call, maybe we reached voicemail?") self.hang() def handle_error(self, error): LOG.error(error) if error == "failed to set audio-source (No such device)": self.handle_audio_stream_failure() def onJackAssigned(self, cname): if not cname in self.jackNames: self.jackNames.append(cname) #We only have 2, if there are more, #it means the default has been reassigned if len(self.jackNames) > 2: self.jackNames.pop(0) def on_audio_rtp(self, rtp): pass # event loop def run(self): self.running = True while self.running: try: out = self.baresip.readline().decode("utf-8") if out != self._prev_output: out = out.strip() if self.debug: LOG.debug(out) if "baresip is ready." in out: self.handle_ready() elif "account: No SIP accounts found" in out: self._handle_no_accounts() elif "All 1 useragent registered successfully!" in out: self.ready = True self.handle_login_success() elif "ua: SIP register failed:" in out or\ "401 Unauthorized" in out or \ "Register: Destination address required" in out or\ "Register: Connection timed out" in out: self.handle_error(out) self.handle_login_failure() elif "Incoming call from: " in out: num = out.split("Incoming call from: ")[1].split( " - (press 'a' to accept)")[0].strip() self.current_call = num self._call_status = "INCOMING" self.handle_incoming_call(num) elif "call: rejecting incoming call from " in out: num = out.split("rejecting incoming call from " )[1].split(" ")[0].strip() self.handle_call_rejected(num) elif "call: SIP Progress: 180 Ringing" in out: self.handle_call_ringing() status = "RINGING" self.handle_call_status(status) self._call_status = status elif "call: connecting to " in out: n = out.split("call: connecting to '")[1].split("'")[0] self.current_call = n self.handle_call_start() status = "OUTGOING" self.handle_call_status(status) self._call_status = status elif "Call established:" in out: status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status sleep(0.5) self.handle_call_established() elif "call: hold " in out: n = out.split("call: hold ")[1] status = "ON HOLD" self.handle_call_status(status) self._call_status = status elif "Call with " in out and \ "terminated (duration: " in out: status = "DISCONNECTED" duration = out.split("terminated (duration: ")[1][:-1] self.handle_call_status(status) self._call_status = status self.handle_call_timestamp(duration) self.mic_muted = False elif "call muted" in out: self.mic_muted = True self.handle_mic_muted() elif "call un-muted" in out: self.mic_muted = False self.handle_mic_unmuted() elif "session closed:" in out: reason = out.split("session closed:")[1].strip() status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status self.handle_call_ended(reason) self.mic_muted = False elif "(no active calls)" in out: status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status elif "incoming rtp for 'audio' established, receiving from " in out: rtp = out.split( "stream: incoming rtp for 'audio' established, receiving from " ) self.on_audio_rtp(rtp) elif "===== Call debug " in out: status = out.split("(")[1].split(")")[0] self.handle_call_status(status) self._call_status = status elif 'jack' in out: match = re.search( r"jack: unique name \`(.*?)\' assigned", out) if match: self.onJackAssigned(match.groups(1)[0]) elif "--- List of active calls (1): ---" in \ self._prev_output: if "ESTABLISHED" in out and self.current_call in out: ts = out.split("ESTABLISHED")[0].split( "[line 1]")[1].strip() if ts != self._ts: self._ts = ts self.handle_call_timestamp(ts) elif "failed to set audio-source (No such device)" in out: error = "failed to set audio-source (No such device)" self.handle_error(error) self._prev_output = out except pexpect.exceptions.EOF: # baresip exited self.quit() except pexpect.exceptions.TIMEOUT: # nothing happened for a while pass except Exception: print(traceback.format_exc()) def wait_until_ready(self): while not self.ready: sleep(0.1) if self.abort: return
from responsive_voice import ResponsiveVoice engine = ResponsiveVoice() engine.say("hello world") engine.say("hello world", gender="male", lang="en-GB", rate=0.4) file_path = engine.get_mp3(u"ola mundo", lang="pt-pt") engine.play_mp3(file_path)
from responsive_voice import ResponsiveVoice engine = ResponsiveVoice() engine.say("hello world") engine.say("hello world", gender=ResponsiveVoice.MALE, rate=0.45) engine = ResponsiveVoice(lang=ResponsiveVoice.PORTUGUESE_BR) file_path = engine.get_mp3(u"olá mundo") engine.play_mp3(file_path) from responsive_voice.voices import EnglishIndia, UKEnglishMale, \ FallbackUKEnglishMale india = EnglishIndia() uk = UKEnglishMale() uk2 = FallbackUKEnglishMale() india.say("hello world") uk.say("hello world") uk2.say("hello world")
class BareSIP(Thread): def __init__(self, user, pwd, gateway, tts=None, debug=False, block=True): self.debug = debug self.user = user self.pwd = pwd self.gateway = gateway if tts: self.tts = tts else: self.tts = ResponsiveVoice(gender=ResponsiveVoice.MALE) self._login = "******".format(u=self.user, p=self.pwd, g=self.gateway) self._prev_output = "" self.running = False self.ready = False self.mic_muted = False self.abort = False self.current_call = None self._call_status = None self.audio = None self._ts = None self.baresip = pexpect.spawn('baresip') super().__init__() self.start() if block: self.wait_until_ready() # properties @property def call_established(self): return self.call_status == "ESTABLISHED" @property def call_status(self): return self._call_status or "DISCONNECTED" # actions def do_command(self, action): if self.ready: action = str(action) self.baresip.sendline(action) else: LOG.warning(action + " not executed!") LOG.error("NOT READY! please wait") def login(self): LOG.info("Adding account: " + self.user) self.baresip.sendline("/uanew " + self._login) def call(self, number): LOG.info("Dialling: " + number) self.do_command("/dial " + number) def hang(self): if self.current_call: LOG.info("Hanging: " + self.current_call) self.do_command("/hangup") self.current_call = None self._call_status = None else: LOG.error("No active call to hang") def hold(self): if self.current_call: LOG.info("Holding: " + self.current_call) self.do_command("/hold") else: LOG.error("No active call to hold") def resume(self): if self.current_call: LOG.info("Resuming " + self.current_call) self.do_command("/resume") else: LOG.error("No active call to resume") def mute_mic(self): if not self.call_established: LOG.error("Can not mute microphone while not in a call") return if not self.mic_muted: LOG.info("Muting mic") self.do_command("/mute") else: LOG.info("Mic already muted") def unmute_mic(self): if not self.call_established: LOG.error("Can not unmute microphone while not in a call") return if self.mic_muted: LOG.info("Unmuting mic") self.do_command("/mute") else: LOG.info("Mic already unmuted") def accept_call(self): self.do_command("/accept") status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status def list_calls(self): self.do_command("/listcalls") def check_call_status(self): self.do_command("/callstat") sleep(0.1) return self.call_status def quit(self): LOG.info("Exiting") if self.running: if self.current_call: self.hang() self.baresip.sendline("/quit") self.running = False self.current_call = None self._call_status = None self.abort = True def send_dtmf(self, number): number = str(number) for n in number: if n not in "0123456789": LOG.error("invalid dtmf tone") return LOG.info("Sending dtmf tones for " + number) dtmf = join(tempfile.gettempdir(), number + ".wav") ToneGenerator().dtmf_to_wave(number, dtmf) self.send_audio(dtmf) def speak(self, speech): if not self.call_established: LOG.error("Speaking without an active call!") else: LOG.info("Sending TTS for " + speech) self.send_audio(self.tts.get_mp3(speech)) sleep(0.5) def send_audio(self, wav_file): if not self.call_established: LOG.error("Can't send audio without an active call!") return wav_file, duration = self.convert_audio(wav_file) # send audio stream LOG.info("transmitting audio") self.do_command("/ausrc aufile," + wav_file) # wait till playback ends sleep(duration - 0.5) # avoid baresip exiting self.do_command("/ausrc alsa,default") @staticmethod def convert_audio(input_file, outfile=None): input_file = expanduser(input_file) sound = AudioSegment.from_file(input_file) sound += AudioSegment.silent(duration=500) # ensure minimum time # workaround baresip bug while sound.duration_seconds < 3: sound += AudioSegment.silent(duration=500) outfile = outfile or join(tempfile.gettempdir(), "pybaresip.wav") sound = sound.set_frame_rate(48000) sound = sound.set_channels(2) sound.export(outfile, format="wav") return outfile, sound.duration_seconds # this is played out loud over speakers def say(self, speech): if not self.call_established: LOG.warning("Speaking without an active call!") self.tts.say(speech, blocking=True) def play(self, audio_file, blocking=True): if not audio_file.endswith(".wav"): audio_file, duration = self.convert_audio(audio_file) self.audio = self._play_wav(audio_file, blocking=blocking) def stop_playing(self): if self.audio is not None: self.audio.kill() @staticmethod def _play_wav(wav_file, play_cmd="aplay %1", blocking=False): play_mp3_cmd = str(play_cmd).split(" ") for index, cmd in enumerate(play_mp3_cmd): if cmd == "%1": play_mp3_cmd[index] = wav_file if blocking: return subprocess.call(play_mp3_cmd) else: return subprocess.Popen(play_mp3_cmd) # events def handle_incoming_call(self, number): LOG.info("Incoming call: " + number) if self.call_established: LOG.info("already in a call, rejecting") sleep(0.1) self.do_command("b") else: LOG.info("default behaviour, rejecting call") sleep(0.1) self.do_command("b") def handle_call_rejected(self, number): LOG.info("Rejected incoming call: " + number) def handle_call_timestamp(self, timestr): LOG.info("Call time: " + timestr) def handle_call_status(self, status): if status != self._call_status: LOG.debug("Call Status: " + status) def handle_call_start(self): number = self.current_call LOG.info("Calling: " + number) def handle_call_ringing(self): number = self.current_call LOG.info(number + " is Ringing") def handle_call_established(self): LOG.info("Call established") def handle_call_ended(self, reason): LOG.info("Call ended") LOG.debug("Reason: " + reason) def _handle_no_accounts(self): LOG.debug("No accounts setup") self.login() def handle_login_success(self): LOG.info("Logged in!") def handle_login_failure(self): LOG.error("Log in failed!") self.quit() def handle_ready(self): LOG.info("Ready for instructions") def handle_mic_muted(self): LOG.info("Microphone muted") def handle_mic_unmuted(self): LOG.info("Microphone unmuted") def handle_audio_stream_failure(self): LOG.debug("Aborting call, maybe we reached voicemail?") self.hang() def handle_error(self, error): LOG.error(error) if error == "failed to set audio-source (No such device)": self.handle_audio_stream_failure() # event loop def run(self): self.running = True while self.running: try: out = self.baresip.readline().decode("utf-8") if out != self._prev_output: out = out.strip() if self.debug: LOG.debug(out) if "baresip is ready." in out: self.handle_ready() elif "account: No SIP accounts found" in out: self._handle_no_accounts() elif "All 1 useragent registered successfully!" in out: self.ready = True self.handle_login_success() elif "ua: SIP register failed:" in out or\ "401 Unauthorized" in out or \ "Register: Destination address required" in out or\ "Register: Connection timed out" in out: self.handle_error(out) self.handle_login_failure() elif "Incoming call from: " in out: num = out.split("Incoming call from: ")[1].split( " - (press 'a' to accept)")[0].strip() self.current_call = num self._call_status = "INCOMING" self.handle_incoming_call(num) elif "call: rejecting incoming call from " in out: num = out.split("rejecting incoming call from " )[1].split(" ")[0].strip() self.handle_call_rejected(num) elif "call: SIP Progress: 180 Ringing" in out: self.handle_call_ringing() status = "RINGING" self.handle_call_status(status) self._call_status = status elif "call: connecting to " in out: n = out.split("call: connecting to '")[1].split("'")[0] self.current_call = n self.handle_call_start() status = "OUTGOING" self.handle_call_status(status) self._call_status = status elif "Call established:" in out: status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status sleep(0.5) self.handle_call_established() elif "call: hold " in out: n = out.split("call: hold ")[1] status = "ON HOLD" self.handle_call_status(status) self._call_status = status elif "Call with " in out and \ "terminated (duration: " in out: status = "DISCONNECTED" duration = out.split("terminated (duration: ")[1][:-1] self.handle_call_status(status) self._call_status = status self.handle_call_timestamp(duration) self.mic_muted = False elif "call muted" in out: self.mic_muted = True self.handle_mic_muted() elif "call un-muted" in out: self.mic_muted = False self.handle_mic_unmuted() elif "session closed:" in out: reason = out.split("session closed:")[1].strip() status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status self.handle_call_ended(reason) self.mic_muted = False elif "(no active calls)" in out: status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status elif "===== Call debug " in out: status = out.split("(")[1].split(")")[0] self.handle_call_status(status) self._call_status = status elif "--- List of active calls (1): ---" in \ self._prev_output: if "ESTABLISHED" in out and self.current_call in out: ts = out.split("ESTABLISHED")[0].split( "[line 1]")[1].strip() if ts != self._ts: self._ts = ts self.handle_call_timestamp(ts) elif "failed to set audio-source (No such device)" in out: error = "failed to set audio-source (No such device)" self.handle_error(error) self._prev_output = out except pexpect.exceptions.EOF: # baresip exited self.quit() except pexpect.exceptions.TIMEOUT: # nothing happened for a while pass def wait_until_ready(self): while not self.ready: sleep(0.1) if self.abort: return
def create_responsive_voice(): from responsive_voice import ResponsiveVoice engine_name = "responsive_voice" params = [ {"gender": "male"}, {"gender": "female"}, {"gender": "male", "rate": 0.4}, {"gender": "female", "rate": 0.4}, {"gender": "male", "rate": 0.6}, {"gender": "female", "rate": 0.6}, {"gender": "female", "lang": "en-GB"}, {"gender": "female", "rate": 0.4, "lang": "en-GB"}, {"gender": "female", "rate": 0.6, "lang": "en-GB"}, {"gender": "male", "pitch": 0.4}, {"gender": "female", "pitch": 0.4}, {"gender": "male", "rate": 0.4, "pitch": 0.4}, {"gender": "female", "rate": 0.4, "pitch": 0.4}, {"gender": "male", "rate": 0.6, "pitch": 0.4}, {"gender": "female", "rate": 0.6, "pitch": 0.4}, {"gender": "female", "lang": "en-GB", "pitch": 0.4}, {"gender": "female", "rate": 0.4, "lang": "en-GB", "pitch": 0.4}, {"gender": "female", "rate": 0.6, "lang": "en-GB", "pitch": 0.4}, {"gender": "male", "pitch": 0.6}, {"gender": "female", "pitch": 0.6}, {"gender": "male", "rate": 0.4, "pitch": 0.6}, {"gender": "female", "rate": 0.4, "pitch": 0.6}, {"gender": "male", "rate": 0.6, "pitch": 0.6}, {"gender": "female", "rate": 0.6, "pitch": 0.6}, {"gender": "female", "lang": "en-GB", "pitch": 0.6}, {"gender": "female", "rate": 0.4, "lang": "en-GB", "pitch": 0.6}, {"gender": "female", "rate": 0.6, "lang": "en-GB", "pitch": 0.6} ] for w in HOTWORD_LIST: w_path = join(DATA_PATH, w).replace(" ", "_") if not isdir(w_path): makedirs(w_path) for p in params: name = w for k in p: name += "-" + k + "-" + str(p[k]) + "-" + engine_name wp_path = join(w_path, name + ".mp3").replace(" ", "_") engine = ResponsiveVoice(**p) engine.get_mp3(w, wp_path, **p) for w in NEGATIVES_LIST: w_path = join(DATA_PATH, w).replace(" ", "_") if not isdir(w_path): makedirs(w_path) for p in params: name = w for k in p: name += "-" + k + "-" + str(p[k]) + "-" + engine_name wp_path = join(w_path, name + ".mp3").replace(" ", "_") engine = ResponsiveVoice(**p) engine.get_mp3(w, wp_path, **p) for idx, w in enumerate(SENTENCE_LIST): w_path = join(DATA_PATH, str(idx + 1)).replace(" ", "_") if not isdir(w_path): makedirs(w_path) for p in params: name = str(idx + 1) for k in p: name += "-" + k + "-" + str(p[k]) + "-" + engine_name wp_path = join(w_path, name + ".mp3").replace(" ", "_") engine = ResponsiveVoice(**p) engine.get_mp3(w, wp_path, **p)
class BareSIP(Thread): def __init__(self, user, pwd, gateway, tts=None, debug=False, block=True, config_path=None, sounds_path=None): config_path = config_path or join("~", ".baresipy") self.config_path = expanduser(config_path) if not isdir(self.config_path): makedirs(self.config_path) if isfile(join(self.config_path, "config")): with open(join(self.config_path, "config"), "r") as f: self.config = f.read() LOG.info("config loaded from " + self.config_path + "/config") self.updated_config = False else: self.config = baresipy.config.DEFAULT self.updated_config = True self._original_config = str(self.config) if sounds_path is not None and "#audio_path" in self.config: self.updated_config = True if sounds_path is False: # sounds disabled self.config = self.config.replace( "#audio_path /usr/share/baresip", "audio_path /dont/load") elif isdir(sounds_path): self.config = self.config.replace( "#audio_path /usr/share/baresip", "audio_path " + sounds_path) if self.updated_config: with open(join(self.config_path, "config.bak"), "w") as f: f.write(self._original_config) LOG.info("saving config") with open(join(self.config_path, "config"), "w") as f: f.write(self.config) self.debug = debug self.user = user self.pwd = pwd self.gateway = gateway if tts: self.tts = tts else: self.tts = ResponsiveVoice(gender=ResponsiveVoice.MALE) self._login = "******".format(u=self.user, p=self.pwd, g=self.gateway) self._prev_output = "" self.running = False self.ready = False self.mic_muted = False self.abort = False self.current_call = None self._call_status = None self.audio = None self._ts = None self.baresip = pexpect.spawn('baresip -f ' + self.config_path) super().__init__() self.start() if block: self.wait_until_ready() # properties @property def call_established(self): return self.call_status == "ESTABLISHED" @property def call_status(self): return self._call_status or "DISCONNECTED" # actions def do_command(self, action): if self.ready: action = str(action) self.baresip.sendline(action) else: LOG.warning(action + " not executed!") LOG.error("NOT READY! please wait") def login(self): LOG.info("Adding account: " + self.user) self.baresip.sendline("/uanew " + self._login) def call(self, number): LOG.info("Dialling: " + number) self.do_command("/dial " + number) def enable_recording(self): LOG.info("Enabling call recording") self.do_command("/insmod sndfile") def hang(self): if self.current_call: LOG.info("Hanging: " + self.current_call) self.do_command("/hangup") self.current_call = None self._call_status = None else: LOG.error("No active call to hang") def hold(self): if self.current_call: LOG.info("Holding: " + self.current_call) self.do_command("/hold") else: LOG.error("No active call to hold") def resume(self): if self.current_call: LOG.info("Resuming " + self.current_call) self.do_command("/resume") else: LOG.error("No active call to resume") def mute_mic(self): if not self.call_established: LOG.error("Can not mute microphone while not in a call") return if not self.mic_muted: LOG.info("Muting mic") self.do_command("/mute") else: LOG.info("Mic already muted") def unmute_mic(self): if not self.call_established: LOG.error("Can not unmute microphone while not in a call") return if self.mic_muted: LOG.info("Unmuting mic") self.do_command("/mute") else: LOG.info("Mic already unmuted") def accept_call(self): self.do_command("/accept") status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status def list_calls(self): self.do_command("/listcalls") def check_call_status(self): self.do_command("/callstat") sleep(0.1) return self.call_status @staticmethod def get_contacts(config_dir=join(expanduser("~"), ".baresip")): """ Read contacts and active_contact from config dir :param config_dir: baresip configration directory (default ~/.baresip) :return: (list) all_contacts(dict), current_contact(str) """ with open(f"{config_dir}/current_contact") as cur: cur_con = cur.readline() with open(f"{config_dir}/contacts") as cons: raw = cons.readlines() contacts_list = [] for line in raw: if not line.startswith("#") and line != "\n": contacts_list.append(line) contacts = {} for contact in contacts_list: name = contact.split('"', 2)[1] addr = contact.split('<', 1)[1].split('>', 1)[0] contacts[name] = addr return contacts, cur_con def quit(self): if self.updated_config: LOG.info("restoring original config") with open(join(self.config_path, "config"), "w") as f: f.write(self._original_config) LOG.info("Exiting") if self.running: if self.current_call: self.hang() self.baresip.sendline("/quit") self.running = False self.current_call = None self._call_status = None self.abort = True self.baresip.close() self.baresip.kill(signal.SIGKILL) def send_dtmf(self, number): number = str(number) for n in number: if n not in "0123456789": LOG.error("invalid dtmf tone") return LOG.info("Sending dtmf tones for " + number) dtmf = join(tempfile.gettempdir(), number + ".wav") ToneGenerator().dtmf_to_wave(number, dtmf) self.send_audio(dtmf) def speak(self, speech): if not self.call_established: LOG.error("Speaking without an active call!") else: LOG.info("Sending TTS for " + speech) self.send_audio(self.tts.get_mp3(speech)) sleep(0.5) def send_audio(self, wav_file): if not self.call_established: LOG.error("Can't send audio without an active call!") return wav_file, duration = self.convert_audio(wav_file) # send audio stream LOG.info("transmitting audio") self.do_command("/ausrc aufile," + wav_file) # wait till playback ends sleep(duration - 0.5) # avoid baresip exiting self.do_command("/ausrc alsa,default") @staticmethod def convert_audio(input_file, outfile=None): input_file = expanduser(input_file) sound = AudioSegment.from_file(input_file) sound += AudioSegment.silent(duration=500) # ensure minimum time # workaround baresip bug while sound.duration_seconds < 3: sound += AudioSegment.silent(duration=500) outfile = outfile or join(tempfile.gettempdir(), "pybaresip.wav") sound = sound.set_frame_rate(48000) sound = sound.set_channels(2) sound.export(outfile, format="wav") return outfile, sound.duration_seconds # this is played out loud over speakers def say(self, speech): if not self.call_established: LOG.warning("Speaking without an active call!") self.tts.say(speech, blocking=True) def play(self, audio_file, blocking=True): if not audio_file.endswith(".wav"): audio_file, duration = self.convert_audio(audio_file) self.audio = self._play_wav(audio_file, blocking=blocking) def stop_playing(self): if self.audio is not None: self.audio.kill() @staticmethod def _play_wav(wav_file, play_cmd="aplay %1", blocking=False): play_mp3_cmd = str(play_cmd).split(" ") for index, cmd in enumerate(play_mp3_cmd): if cmd == "%1": play_mp3_cmd[index] = wav_file if blocking: return subprocess.call(play_mp3_cmd) else: return subprocess.Popen(play_mp3_cmd) # events def handle_incoming_call(self, number): LOG.info("Incoming call: " + number) if self.call_established: LOG.info("already in a call, rejecting") sleep(0.1) self.do_command("b") else: LOG.info("default behaviour, rejecting call") sleep(0.1) self.do_command("b") def handle_call_rejected(self, number): LOG.info("Rejected incoming call: " + number) def handle_call_timestamp(self, timestr): LOG.info("Call time: " + timestr) def handle_call_status(self, status): if status != self._call_status: LOG.debug("Call Status: " + status) @staticmethod def handle_text_message(sender, text): LOG.info(f"rec'd: {text} | from: {sender}") def handle_call_start(self): number = self.current_call LOG.info("Calling: " + number) def handle_call_ringing(self): number = self.current_call LOG.info(number + " is Ringing") def handle_call_established(self): LOG.info("Call established") def handle_call_ended(self, reason): LOG.info("Call ended") LOG.debug("Reason: " + reason) def _handle_no_accounts(self): LOG.debug("No accounts setup") self.login() def handle_login_success(self): LOG.info("Logged in!") def handle_login_failure(self): LOG.error("Log in failed!") self.quit() def handle_ready(self): LOG.info("Ready for instructions") def handle_mic_muted(self): LOG.info("Microphone muted") def handle_mic_unmuted(self): LOG.info("Microphone unmuted") def handle_audio_stream_failure(self): LOG.debug("Aborting call, maybe we reached voicemail?") self.hang() def handle_error(self, error): LOG.error(error) if error == "failed to set audio-source (No such device)": self.handle_audio_stream_failure() # event loop def run(self): self.running = True while self.running: try: out = self.baresip.readline().decode("utf-8") if out != self._prev_output: out = out.strip() if self.debug: LOG.debug(out) if "baresip is ready." in out: self.handle_ready() elif "account: No SIP accounts found" in out: self._handle_no_accounts() elif "All 1 useragent registered successfully!" in out: self.ready = True self.handle_login_success() elif "ua: SIP register failed:" in out or\ "401 Unauthorized" in out or \ "Register: Destination address required" in out or\ "Register: Connection timed out" in out or\ "403 Wrong Password" in out or\ "400 Bad From URI" in out: self.handle_error(out) self.handle_login_failure() elif "Incoming call from: " in out: num = out.split("Incoming call from: ")[1].split( " - (press 'a' to accept)")[0].strip() self.current_call = num self._call_status = "INCOMING" self.handle_incoming_call(num) elif "call: rejecting incoming call from " in out: num = out.split("rejecting incoming call from " )[1].split(" ")[0].strip() self.handle_call_rejected(num) elif "call: SIP Progress: 180 Ringing" in out: self.handle_call_ringing() status = "RINGING" self.handle_call_status(status) self._call_status = status elif "call: connecting to " in out: n = out.split("call: connecting to '")[1].split("'")[0] self.current_call = n self.handle_call_start() status = "OUTGOING" self.handle_call_status(status) self._call_status = status elif "Call established:" in out: status = "ESTABLISHED" self.handle_call_status(status) self._call_status = status sleep(0.5) self.handle_call_established() elif "call: hold " in out: n = out.split("call: hold ")[1] status = "ON HOLD" self.handle_call_status(status) self._call_status = status elif "Call with " in out and \ "terminated (duration: " in out: status = "DISCONNECTED" duration = out.split("terminated (duration: ")[1][:-1] self.handle_call_status(status) self._call_status = status self.handle_call_timestamp(duration) self.mic_muted = False elif "call muted" in out: self.mic_muted = True self.handle_mic_muted() elif "call un-muted" in out: self.mic_muted = False self.handle_mic_unmuted() elif "session closed:" in out: reason = out.split("session closed:")[1].strip() status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status self.handle_call_ended(reason) self.mic_muted = False elif "(no active calls)" in out: status = "DISCONNECTED" self.handle_call_status(status) self._call_status = status elif "===== Call debug " in out: status = out.split("(")[1].split(")")[0] self.handle_call_status(status) self._call_status = status elif "--- List of active calls (1): ---" in \ self._prev_output: if "ESTABLISHED" in out and self.current_call in out: ts = out.split("ESTABLISHED")[0].split( "[line 1]")[1].strip() if ts != self._ts: self._ts = ts self.handle_call_timestamp(ts) elif "failed to set audio-source (No such device)" in out: error = "failed to set audio-source (No such device)" self.handle_error(error) elif "terminated by signal" in out or "ua: stop all" in out: self.running = False elif str(out).startswith("sip:") and str(out).endswith( '"'): _, sender, message = str(out).split(':', 2) sender = f"sip:{sender}" message = message.strip().strip('"') self.handle_text_message(sender, message) self._prev_output = out except pexpect.exceptions.EOF: # baresip exited self.running = False except pexpect.exceptions.TIMEOUT: # nothing happened for a while pass except KeyboardInterrupt: self.running = False self.quit() def wait_until_ready(self): while not self.ready: sleep(0.1) if self.abort: return