Browse Source

add playAudioFile() and caller menu skeleton

- playAudioFile() makes it possible to play a WAV file
to either the caller or the mumble channel

- menu skeleton has DTMF handling and is ready for
adding '*n' functions (like *5 = mute).
Scott Hardin 7 years ago
parent
commit
82015dc14c

+ 186 - 21
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<pj::AudioMedia *>(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) {

+ 21 - 1
PjsuaCommunicator.hpp

@@ -18,10 +18,14 @@
 #include <climits>
 #include <bits/unique_ptr.h>
 
+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<void(int16_t *, int)> onIncomingPcmSamples;
 
@@ -85,6 +103,8 @@ namespace sip {
 
         std::function<void(int)> onMuteDeafChange;
 
+        std::function<void(int)> onMuteChange;
+
         pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame);
 
         pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);

+ 20 - 3
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
+
+

+ 47 - 2
main.cpp

@@ -98,9 +98,54 @@ int main(int argc, char *argv[]) {
 
     /* default to <no pin> */
     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);

+ 24 - 0
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 "$@"
+
+

+ 1 - 0
media/announce-new-caller.msg

@@ -0,0 +1 @@
+Caller joined

BIN
media/announce-new-caller.wav


BIN
media/blow.wav


+ 1 - 0
media/entering-channel.msg

@@ -0,0 +1 @@
+entering channel

BIN
media/entering-channel.wav


+ 1 - 0
media/goodbye.msg

@@ -0,0 +1 @@
+Goodbye

BIN
media/goodbye.wav


+ 1 - 0
media/invalid-pin.msg

@@ -0,0 +1 @@
+Invalid pin

BIN
media/invalid-pin.wav


+ 3 - 0
media/menu.msg

@@ -0,0 +1,3 @@
+Press star one for status.
+Press star five to mute.
+Press star six to un-mute.

BIN
media/menu.wav


+ 1 - 0
media/mute-off.msg

@@ -0,0 +1 @@
+mute off

BIN
media/mute-off.wav


+ 1 - 0
media/mute-on.msg

@@ -0,0 +1 @@
+mute on

BIN
media/mute-on.wav


+ 1 - 0
media/prompt-pin.msg

@@ -0,0 +1 @@
+Please enter pin

BIN
media/prompt-pin.wav


+ 1 - 0
media/welcome.msg

@@ -0,0 +1 @@
+Welcome to the conference bridge

BIN
media/welcome.wav