diff --git a/PjsuaCommunicator.cpp b/PjsuaCommunicator.cpp index a190717..5d4d56d 100644 --- a/PjsuaCommunicator.cpp +++ b/PjsuaCommunicator.cpp @@ -107,6 +107,9 @@ namespace sip { virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override; + virtual void playAudioFile(std::string file); + virtual void playAudioFile(std::string file, bool in_chan); + private: sip::PjsuaCommunicator &communicator; pj::Account &account; @@ -145,14 +148,24 @@ namespace sip { communicator.logger.notice(msgText); communicator.onStateChange(msgText); + pj_thread_sleep(500); // sleep a moment to allow connection to stabilize + this->playAudioFile(communicator.file_welcome); + communicator.got_dtmf = ""; /* * if no pin is set, go ahead and turn off mute/deaf * otherwise, wait for pin to be entered */ - if ( communicator.pin.length() == 0 ) { + if ( communicator.caller_pin.length() == 0 ) { + // No PIN set... enter DTMF root menu and turn off mute/deaf + communicator.dtmf_mode = DTMF_MODE_ROOT; communicator.onMuteDeafChange(0); + } else { + // PIN set... enter DTMF unauth menu and play PIN prompt message + communicator.dtmf_mode = DTMF_MODE_UNAUTH; + pj_thread_sleep(500); // pause briefly after announcement + this->playAudioFile(communicator.file_prompt_pin); } } else if (ci.state == PJSIP_INV_STATE_DISCONNECTED) { @@ -191,32 +204,184 @@ namespace sip { } } + void _Call::playAudioFile(std::string file) { + this->playAudioFile(file, false); // default is NOT to echo to mumble + } + + /* TODO: + * - local deafen before playing and undeafen after? + */ + void _Call::playAudioFile(std::string file, bool in_chan) { + communicator.logger.notice("Entered playAudioFile(%s)", file.c_str()); + pj::AudioMediaPlayer player; + pj::MediaFormatAudio mfa; + pj::AudioMediaPlayerInfo pinfo; + int wavsize; + int sleeptime; + + if ( ! pj_file_exists(file.c_str()) ) { + communicator.logger.warn("File not found (%s)", file.c_str()); + return; + } + + /* TODO: use some library to get the actual length in millisec + * + * This just gets the file size and divides by a constant to + * estimate the length of the WAVE file in milliseconds. + * This depends on the encoding bitrate, etc. + */ + + auto ci = getInfo(); + if (ci.media.size() != 1) { + throw sip::Exception("ci.media.size is not 1"); + } + + if (ci.media[0].status == PJSUA_CALL_MEDIA_ACTIVE) { + auto *aud_med = static_cast(getMedia(0)); + + try { + player.createPlayer(file, PJMEDIA_FILE_NO_LOOP); + pinfo = player.getInfo(); + sleeptime = pinfo.sizeBytes / (pinfo.payloadBitsPerSample * 3); + + /* + communicator.logger.notice("DEBUG: wavsize=%d pbps=%d bytes=%d samples=%d", + wavsize, pinfo.payloadBitsPerSample, pinfo.sizeBytes, pinfo.sizeSamples); + communicator.logger.notice("WAVE length in ms: %d", sleeptime); + */ + + if ( in_chan ) { // choose the target sound output + player.startTransmit(*communicator.media); + } else { + player.startTransmit(*aud_med); + } + + pj_thread_sleep(sleeptime); + + if ( in_chan ) { // choose the target sound output + player.stopTransmit(*communicator.media); + } else { + player.stopTransmit(*aud_med); + } + + } catch (...) { + communicator.logger.notice("Error playing file %s", file.c_str()); + } + } else { + communicator.logger.notice("Call not active - can't play file %s", file.c_str()); + } + } + void _Call::onDtmfDigit(pj::OnDtmfDigitParam &prm) { //communicator.logger.notice("DTMF digit '%s' (call %d).", // prm.digit.c_str(), getId()); pj::CallOpParam param; - - if ( communicator.pin.length() > 0 ) { - if ( prm.digit == "#" ) { - //communicator.logger.notice("DTMF got string command %s", - // communicator.got_dtmf.c_str()); - if ( communicator.got_dtmf == communicator.pin ) { - communicator.logger.notice("Caller entered correct PIN"); - communicator.onMuteDeafChange(0); - } else { - communicator.logger.notice("Caller entered wrong PIN"); - param.statusCode = PJSIP_SC_SERVICE_UNAVAILABLE; - this->hangup(param); + + /* + * DTMF CALLER MENU + */ + + switch ( communicator.dtmf_mode ) { + case DTMF_MODE_UNAUTH: + /* + * IF UNAUTH, the only thing we allow is to authorize. + */ + switch ( prm.digit[0] ) { + case '#': + /* + * When user presses '#', test PIN entry + */ + if ( communicator.caller_pin.length() > 0 ) { + if ( communicator.got_dtmf == communicator.caller_pin ) { + communicator.logger.notice("Caller entered correct PIN"); + communicator.dtmf_mode = DTMF_MODE_ROOT; + this->playAudioFile(communicator.file_entering_channel); + communicator.onMuteDeafChange(0); + this->playAudioFile(communicator.file_announce_new_caller, true); + } else { + communicator.logger.notice("Caller entered wrong PIN"); + this->playAudioFile(communicator.file_invalid_pin); + if ( communicator.pin_fails++ >= MAX_PIN_FAILS ) { + param.statusCode = PJSIP_SC_SERVICE_UNAVAILABLE; + pj_thread_sleep(500); // pause before next announcement + this->playAudioFile(communicator.file_goodbye); + pj_thread_sleep(500); // pause before next announcement + this->hangup(param); + } + this->playAudioFile(communicator.file_prompt_pin); + } + communicator.got_dtmf = ""; + } + break; + case '*': + /* + * Allow user to reset PIN entry by pressing '*' + */ + communicator.got_dtmf = ""; + this->playAudioFile(communicator.file_prompt_pin); + break; + default: + /* + * In all other cases, add input digit to stack + */ + communicator.got_dtmf = communicator.got_dtmf + prm.digit; + if ( communicator.got_dtmf.size() > MAX_CALLER_PIN_LEN ) { + // just drop 'em if too long + param.statusCode = PJSIP_SC_SERVICE_UNAVAILABLE; + this->playAudioFile(communicator.file_goodbye); + pj_thread_sleep(500); // pause before next announcement + this->hangup(param); + } } - communicator.got_dtmf = ""; - } else { - // communicator.logger.notice("DTMF append %s to %s", - // prm.digit.c_str(), communicator.got_dtmf.c_str()); - communicator.got_dtmf = communicator.got_dtmf + prm.digit; - } - } else { - communicator.logger.notice("DTMF ignoring %s", prm.digit.c_str()); + break; + case DTMF_MODE_ROOT: + /* + * User already authenticated; no data entry pending + */ + switch ( prm.digit[0] ) { + case '*': + /* + * Switch user to 'star' menu + */ + communicator.dtmf_mode = DTMF_MODE_STAR; + break; + default: + /* + * Default is to ignore all digits in root + */ + communicator.logger.notice("Ignore DTMF digit '%s' in ROOT state", prm.digit.c_str()); + } + break; + case DTMF_MODE_STAR: + /* + * User already entered '*'; time to perform action + */ + switch ( prm.digit[0] ) { + /* + case '5': + // Mute line + communicator.onMuteChange(1); + this->playAudioFile(communicator.file_mute_on); + break; + case '6': + // Un-mute line + this->playAudioFile(communicator.file_mute_off); + communicator.onMuteChange(0); + break; + */ + default: + communicator.logger.notice("Unsupported DTMF digit '%s' in state STAR", prm.digit.c_str()); + } + /* + * In any case, switch back to root after one digit + */ + communicator.dtmf_mode = DTMF_MODE_ROOT; + break; + default: + communicator.logger.notice("Unexpected DTMF '%s' in unknown state '%d'", prm.digit.c_str(), + communicator.dtmf_mode); } + } void _Account::onRegState(pj::OnRegStateParam &prm) { diff --git a/PjsuaCommunicator.hpp b/PjsuaCommunicator.hpp index 8bb8aa3..bd69979 100644 --- a/PjsuaCommunicator.hpp +++ b/PjsuaCommunicator.hpp @@ -18,10 +18,14 @@ #include #include +enum dtmf_modes_t {DTMF_MODE_UNAUTH, DTMF_MODE_ROOT, DTMF_MODE_STAR}; + namespace sip { constexpr int DEFAULT_PORT = 5060; constexpr int SAMPLING_RATE = 48000; + constexpr int MAX_CALLER_PIN_LEN = 64; + constexpr int MAX_PIN_FAILS = 2; class Exception : public std::runtime_error { public: @@ -76,8 +80,22 @@ namespace sip { int16_t *samples, unsigned int length); - std::string pin; + // config params we get from config.ini + std::string caller_pin; + std::string file_welcome; + std::string file_prompt_pin; + std::string file_entering_channel; + std::string file_announce_new_caller; + std::string file_invalid_pin; + std::string file_goodbye; + std::string file_mute_on; + std::string file_mute_off; + std::string file_menu; + + // TODO: move these to private? std::string got_dtmf; + dtmf_modes_t dtmf_mode = DTMF_MODE_ROOT; + int pin_fails = 0; std::function onIncomingPcmSamples; @@ -85,6 +103,8 @@ namespace sip { std::function onMuteDeafChange; + std::function onMuteChange; + pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame); pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame); diff --git a/config.ini.example b/config.ini.example index 2b735f6..bd40a0b 100644 --- a/config.ini.example +++ b/config.ini.example @@ -28,11 +28,28 @@ channelNameExpression = # in the same group autodeaf = 0 +# Bitrate of Opus encoder in B/s +# Adjust it if you need to meet the specific bandwidth requirements of Murmur server +opusEncoderBitrate = 16000 + +[app] + # Caller PIN needed to authenticate the phone call itself. The caller presses # the PIN, followed by the hash '#' key. On success, the caller is # unmuted/undeafened. On failure, the SIP call is hung up. pin = 4321 -# Bitrate of Opus encoder in B/s -# Adjust it if you need to meet the specific bandwidth requirements of Murmur server -opusEncoderBitrate = 16000 +[files] +# These files are used for the caller and mumble channel audio clips. +# The paths below assume that you are running ./mumsi in the build/ dir. +welcome = ../media/welcome.wav +prompt_pin = ../media/prompt-pin.wav +entering_channel = ../media/entering-channel.wav +announce_new_caller = ../media/announce-new-caller.wav +invalid_pin = ../media/invalid-pin.wav +goodbye = ../media/goodbye.wav +mute_on = ../media/mute-on.wav +mute_off = ../media/mute-off.wav +menu = ../media/menu.wav + + diff --git a/main.cpp b/main.cpp index 83d5078..8396f49 100644 --- a/main.cpp +++ b/main.cpp @@ -98,9 +98,54 @@ int main(int argc, char *argv[]) { /* default to */ try { - pjsuaCommunicator.pin = conf.getString("mumble.pin"); + pjsuaCommunicator.caller_pin = conf.getString("app.caller_pin"); } catch (...) { - pjsuaCommunicator.pin = ""; + pjsuaCommunicator.caller_pin = ""; + } + + try { pjsuaCommunicator.file_welcome = conf.getString("file.welcome"); + } catch (...) { + pjsuaCommunicator.file_welcome = "welcome.wav"; + } + + try { pjsuaCommunicator.file_prompt_pin = conf.getString("file.prompt_pin"); + } catch (...) { + pjsuaCommunicator.file_prompt_pin = "prompt-pin.wav"; + } + + try { pjsuaCommunicator.file_entering_channel = conf.getString("file.entering_channel"); + } catch (...) { + pjsuaCommunicator.file_entering_channel = "entering-channel.wav"; + } + + try { pjsuaCommunicator.file_announce_new_caller = conf.getString("file.announce_new_caller"); + } catch (...) { + pjsuaCommunicator.file_announce_new_caller = "announce-new-caller.wav"; + } + + try { pjsuaCommunicator.file_invalid_pin = conf.getString("file.invalid_pin"); + } catch (...) { + pjsuaCommunicator.file_invalid_pin = "invalid-pin.wav"; + } + + try { pjsuaCommunicator.file_goodbye = conf.getString("file.goodbye"); + } catch (...) { + pjsuaCommunicator.file_goodbye = "goodbye.wav"; + } + + try { pjsuaCommunicator.file_mute_on = conf.getString("file.mute_on"); + } catch (...) { + pjsuaCommunicator.file_mute_on = "mute-on.wav"; + } + + try { pjsuaCommunicator.file_mute_off = conf.getString("file.mute_off"); + } catch (...) { + pjsuaCommunicator.file_mute_off = "mute-off.wav"; + } + + try { pjsuaCommunicator.file_menu = conf.getString("file.menu"); + } catch (...) { + pjsuaCommunicator.file_menu = "menu.wav"; } mumbleCommunicator.connect(mumbleConf); diff --git a/media/Makefile b/media/Makefile new file mode 100644 index 0000000..3d305ac --- /dev/null +++ b/media/Makefile @@ -0,0 +1,24 @@ +# Makefile +# +# This file generates the WAVE files from text files on macOS +# + +WAVES := $(subst .msg,.wav,$(wildcard *.msg)) blow.wav + +VOICE := --voice=Samantha +RATE := --rate=15 +QUALITY := --quality=127 + + +all: $(WAVES) + +%.wav: %.aiff + afconvert "$<" -d LEI16 "$@" + +%.aiff: %.msg + say $(VOICE) $(RATE) -o $@ < $< + +#announce-new-caller.wav: /System/Library/Sounds/Blow.aiff +# afconvert "$<" -d LEI16 "$@" + + diff --git a/media/announce-new-caller.msg b/media/announce-new-caller.msg new file mode 100644 index 0000000..44706d9 --- /dev/null +++ b/media/announce-new-caller.msg @@ -0,0 +1 @@ +Caller joined diff --git a/media/announce-new-caller.wav b/media/announce-new-caller.wav new file mode 100644 index 0000000..e40a0d3 Binary files /dev/null and b/media/announce-new-caller.wav differ diff --git a/media/blow.wav b/media/blow.wav new file mode 100644 index 0000000..ca03ae5 Binary files /dev/null and b/media/blow.wav differ diff --git a/media/entering-channel.msg b/media/entering-channel.msg new file mode 100644 index 0000000..feace38 --- /dev/null +++ b/media/entering-channel.msg @@ -0,0 +1 @@ +entering channel diff --git a/media/entering-channel.wav b/media/entering-channel.wav new file mode 100644 index 0000000..a11bdad Binary files /dev/null and b/media/entering-channel.wav differ diff --git a/media/goodbye.msg b/media/goodbye.msg new file mode 100644 index 0000000..2b60207 --- /dev/null +++ b/media/goodbye.msg @@ -0,0 +1 @@ +Goodbye diff --git a/media/goodbye.wav b/media/goodbye.wav new file mode 100644 index 0000000..37f14e2 Binary files /dev/null and b/media/goodbye.wav differ diff --git a/media/invalid-pin.msg b/media/invalid-pin.msg new file mode 100644 index 0000000..eb5c136 --- /dev/null +++ b/media/invalid-pin.msg @@ -0,0 +1 @@ +Invalid pin diff --git a/media/invalid-pin.wav b/media/invalid-pin.wav new file mode 100644 index 0000000..a8240c3 Binary files /dev/null and b/media/invalid-pin.wav differ diff --git a/media/menu.msg b/media/menu.msg new file mode 100644 index 0000000..7644a28 --- /dev/null +++ b/media/menu.msg @@ -0,0 +1,3 @@ +Press star one for status. +Press star five to mute. +Press star six to un-mute. diff --git a/media/menu.wav b/media/menu.wav new file mode 100644 index 0000000..1a9df4e Binary files /dev/null and b/media/menu.wav differ diff --git a/media/mute-off.msg b/media/mute-off.msg new file mode 100644 index 0000000..3dd37bb --- /dev/null +++ b/media/mute-off.msg @@ -0,0 +1 @@ +mute off diff --git a/media/mute-off.wav b/media/mute-off.wav new file mode 100644 index 0000000..e7ae2f2 Binary files /dev/null and b/media/mute-off.wav differ diff --git a/media/mute-on.msg b/media/mute-on.msg new file mode 100644 index 0000000..970f633 --- /dev/null +++ b/media/mute-on.msg @@ -0,0 +1 @@ +mute on diff --git a/media/mute-on.wav b/media/mute-on.wav new file mode 100644 index 0000000..cc53ac1 Binary files /dev/null and b/media/mute-on.wav differ diff --git a/media/prompt-pin.msg b/media/prompt-pin.msg new file mode 100644 index 0000000..75a9622 --- /dev/null +++ b/media/prompt-pin.msg @@ -0,0 +1 @@ +Please enter pin diff --git a/media/prompt-pin.wav b/media/prompt-pin.wav new file mode 100644 index 0000000..d3a6ebd Binary files /dev/null and b/media/prompt-pin.wav differ diff --git a/media/welcome.msg b/media/welcome.msg new file mode 100644 index 0000000..ea56fa8 --- /dev/null +++ b/media/welcome.msg @@ -0,0 +1 @@ +Welcome to the conference bridge diff --git a/media/welcome.wav b/media/welcome.wav new file mode 100644 index 0000000..03bf5d0 Binary files /dev/null and b/media/welcome.wav differ