Browse Source

Merge remote-tracking branch 'mrscotty/develop'

Patrik Dahlström 4 years ago
parent
commit
6fb224e23f

+ 12 - 0
Configuration.cpp

@@ -3,6 +3,7 @@
 #include <boost/property_tree/ptree.hpp>
 #include <boost/property_tree/ini_parser.hpp>
 #include <boost/format.hpp>
+#include <boost/foreach.hpp>
 
 using namespace config;
 
@@ -50,4 +51,15 @@ std::string config::Configuration::getString(const std::string &property) {
     return get<std::string>(impl->ptree, property);
 }
 
+// TODO: return set
+std::unordered_map<std::string, std::string> config::Configuration::getChildren(const std::string &property) {
+    std::unordered_map<std::string, std::string> pins;
+    BOOST_FOREACH(boost::property_tree::ptree::value_type &v,
+            impl->ptree.get_child(property)) {
+        //pins[v.first.data()] = get<std::string>(impl->ptree, property + "." + v.second.data());
+        pins[v.first.data()] = v.second.data();
+    }
+    return pins;
+}
+
 

+ 3 - 0
Configuration.hpp

@@ -5,6 +5,7 @@
 #include <vector>
 #include <string>
 #include <stdexcept>
+#include <unordered_map>
 
 namespace config {
 
@@ -31,6 +32,8 @@ namespace config {
 
         std::string getString(const std::string &property);
 
+        std::unordered_map<std::string, std::string> getChildren(const std::string &property);
+
     private:
         ConfigurationImpl *impl;
     };

+ 33 - 0
MumbleChannelJoiner.cpp

@@ -5,12 +5,20 @@ using namespace std;
 
 mumble::MumbleChannelJoiner::MumbleChannelJoiner(std::string channelNameRegex) : channelNameRegex(boost::regex(channelNameRegex)),
 logger(log4cpp::Category::getInstance("MumbleChannelJoiner")){
+    //std::vector<ChannelEntry> *channels = new std::vector<ChannelEntry>();
 }
 
+std::vector<mumble::MumbleChannelJoiner::ChannelEntry> mumble::MumbleChannelJoiner::channels;
+
 void mumble::MumbleChannelJoiner::checkChannel(std::string channel_name, int channel_id) {
     boost::smatch s;
+    ChannelEntry ent;
     logger.debug("Channel %s available (%d)", channel_name.c_str(), channel_id);
 
+    ent.name = channel_name;
+    ent.id = channel_id;
+
+    channels.push_back(ent);
 
     if(boost::regex_match(channel_name, s, channelNameRegex)) {
       this->channel_id = channel_id;
@@ -23,3 +31,28 @@ void mumble::MumbleChannelJoiner::maybeJoinChannel(mumble::MumbleCommunicator *m
 	}
 }
 
+/* This is a secondary channel-switching object that relys on updates to the
+ * class variable 'channels' for the channel list from the server.
+ */
+void mumble::MumbleChannelJoiner::findJoinChannel(mumble::MumbleCommunicator *mc) {
+    boost::smatch s;
+
+    int found = -1;
+
+    for(std::vector<ChannelEntry>::iterator it = channels.begin(); it != channels.end(); ++it) {
+        if(boost::regex_match(it->name, s, channelNameRegex)) {
+            found = it->id;
+        }
+    }
+
+	if(found > -1) {
+		mc->joinChannel(found);
+	}
+}
+
+void mumble::MumbleChannelJoiner::joinOtherChannel(mumble::MumbleCommunicator *mc, std::string channelNameRegex) {
+    this->channelNameRegex = boost::regex(channelNameRegex);
+    findJoinChannel(mc);
+}
+
+

+ 10 - 0
MumbleChannelJoiner.hpp

@@ -3,21 +3,31 @@
 #include <boost/noncopyable.hpp>
 #include <log4cpp/Category.hh>
 
+#include <vector>
 #include <string>
 #include <boost/regex.hpp>
 #include "MumbleCommunicator.hpp"
 
 namespace mumble {
     class MumbleChannelJoiner : boost::noncopyable {
+
+        struct ChannelEntry {
+            int id;
+            std::string name;
+        };
+
     public:
         MumbleChannelJoiner(std::string channelNameRegex);
 
         void checkChannel(std::string channel_name, int channel_id);
         void maybeJoinChannel(mumble::MumbleCommunicator *mc);
+        void findJoinChannel(mumble::MumbleCommunicator *mc);
+        void joinOtherChannel(mumble::MumbleCommunicator *mc, std::string channelNameRegex);
 
     private:
         log4cpp::Category &logger;
         boost::regex channelNameRegex;
         int channel_id;
+        static std::vector<ChannelEntry> channels;
     };
 }

+ 100 - 2
MumbleCommunicator.cpp

@@ -9,13 +9,14 @@ namespace mumble {
         std::shared_ptr<mumlib::Mumlib> mum;
         MumbleCommunicator *communicator;
 
+        // called by Mumlib when receiving audio from mumble server 
         virtual void audio(
                 int target,
                 int sessionId,
                 int sequenceNumber,
                 int16_t *pcm_data,
                 uint32_t pcm_data_size) override {
-            communicator->onIncomingPcmSamples(sessionId, sequenceNumber, pcm_data, pcm_data_size);
+            communicator->onIncomingPcmSamples(communicator->callId, sessionId, sequenceNumber, pcm_data, pcm_data_size);
         }
 
         virtual void channelState(
@@ -39,6 +40,26 @@ namespace mumble {
             communicator->onServerSync();
         };
 
+        /*
+        virtual void onUserState(
+                int32_t session,
+                int32_t actor,
+                std::string name,
+                int32_t user_id,
+                int32_t channel_id,
+                int32_t mute,
+                int32_t deaf,
+                int32_t suppress,
+                int32_t self_mute,
+                int32_t self_deaf,
+                std::string comment,
+                int32_t priority_speaker,
+                int32_t recording
+                ) override {
+            communicator->onUserState();
+        };
+        */
+
     };
 }
 
@@ -51,14 +72,48 @@ void mumble::MumbleCommunicator::connect(MumbleCommunicatorConfig &config) {
 
     callback.reset(new MumlibCallback());
 
+    mumbleConf = config;
+
     mumConfig = mumlib::MumlibConfiguration();
     mumConfig.opusEncoderBitrate = config.opusEncoderBitrate;
+    mumConfig.cert_file = config.cert_file;
+    mumConfig.privkey_file = config.privkey_file;
 
     mum.reset(new mumlib::Mumlib(*callback, ioService, mumConfig));
     callback->communicator = this;
     callback->mum = mum;
 
-    mum->connect(config.host, config.port, config.user, config.password);
+    // IMPORTANT: comment these out when experimenting with onConnect
+    if ( ! MUM_DELAYED_CONNECT ) {
+        mum->connect(config.host, config.port, config.user, config.password);
+        if ( mumbleConf.autodeaf ) {
+            mum->sendUserState(mumlib::UserState::SELF_DEAF, true);
+        }
+    }
+}
+
+void mumble::MumbleCommunicator::onConnect() {
+    if ( MUM_DELAYED_CONNECT ) {
+        mum->connect(mumbleConf.host, mumbleConf.port, mumbleConf.user, mumbleConf.password);
+    }
+
+    if ( mumbleConf.comment.size() > 0 ) {
+        mum->sendUserState(mumlib::UserState::COMMENT, mumbleConf.comment);
+    }
+    if ( mumbleConf.autodeaf ) {
+        mum->sendUserState(mumlib::UserState::SELF_DEAF, true);
+    }
+}
+
+void mumble::MumbleCommunicator::onDisconnect() {
+    if ( MUM_DELAYED_CONNECT ) {
+        mum->disconnect();
+    } else {
+    }
+}
+
+void mumble::MumbleCommunicator::onCallerAuth() {
+    //onServerSync();
 }
 
 void mumble::MumbleCommunicator::sendPcmSamples(int16_t *samples, unsigned int length) {
@@ -73,6 +128,49 @@ void mumble::MumbleCommunicator::sendTextMessage(std::string message) {
     mum->sendTextMessage(message);
 }
 
+/*
+void mumble::MumbleCommunicator::onUserState(
+        int32_t session,
+        int32_t actor,
+        std::string name,
+        int32_t user_id,
+        int32_t channel_id,
+        int32_t mute,
+        int32_t deaf,
+        int32_t suppress,
+        int32_t self_mute,
+        int32_t self_deaf,
+        std::string comment,
+        int32_t priority_speaker,
+        int32_t recording) {
+
+    logger::notice("Entered onUserState(...)");
+
+    userState.mute = mute;
+    userState.deaf = deaf;
+    userState.suppress = suppress;
+    userState.self_mute = self_mute;
+    userState.self_deaf = self_deaf;
+    userState.priority_speaker = priority_speaker;
+    userState.recording = recording;
+}
+*/
+
+
 void mumble::MumbleCommunicator::joinChannel(int channel_id) {
     mum->joinChannel(channel_id);
+
+    if ( mumbleConf.autodeaf ) {
+        mum->sendUserState(mumlib::UserState::SELF_DEAF, true);
+    }
 }
+
+
+void mumble::MumbleCommunicator::sendUserState(mumlib::UserState field, bool val) {
+    mum->sendUserState(field, val);
+}
+
+void mumble::MumbleCommunicator::sendUserState(mumlib::UserState field, std::string val) {
+    mum->sendUserState(field, val);
+}
+

+ 41 - 2
MumbleCommunicator.hpp

@@ -8,8 +8,13 @@
 #include <string>
 #include <stdexcept>
 
+// 0 = mumble users connected at start; 1 = connect at dial-in
+// TODO: fix mumlib::TransportException when this option is enabled
+#define MUM_DELAYED_CONNECT 0
+
 namespace mumble {
 
+
     class Exception : public std::runtime_error {
     public:
         Exception(const char *message) : std::runtime_error(message) { }
@@ -21,8 +26,25 @@ namespace mumble {
         std::string user;
         std::string password;
         std::string host;
+        std::string cert_file;
+        std::string privkey_file;
         int opusEncoderBitrate;
         int port = 0;
+        bool autodeaf;
+        std::string comment;
+        int max_calls = 1;
+        std::string authchan; // config.ini: channelAuthExpression
+    };
+
+    // This is the subset that is of interest to us
+    struct MumbleUserState {
+        int32_t mute;
+        int32_t deaf;
+        int32_t suppress;
+        int32_t self_mute;
+        int32_t self_deaf;
+        int32_t priority_speaker;
+        int32_t recording;
     };
 
     class MumbleCommunicator : boost::noncopyable {
@@ -31,6 +53,10 @@ namespace mumble {
                 boost::asio::io_service &ioService);
 
         void connect(MumbleCommunicatorConfig &config);
+        void onConnect();
+        void onDisconnect();
+        void onCallerAuth();
+        //void onCallerUnauth();
 
         virtual ~MumbleCommunicator();
 
@@ -38,9 +64,9 @@ namespace mumble {
 
         /**
          * This callback is called when communicator has received samples.
-         * Arguments: session ID, sequence number, PCM samples, length of samples
+         * Arguments: call ID, session ID, sequence number, PCM samples, length of samples
          */
-        std::function<void(int, int, int16_t *, int)> onIncomingPcmSamples;
+        std::function<void(int, int, int, int16_t *, int)> onIncomingPcmSamples;
 
         /**
          * This callback is called when a channel state message (e.g. Channel
@@ -50,15 +76,27 @@ namespace mumble {
 
         std::function<void()> onServerSync;
 
+        std::function<void()> onUserState;
+
         void sendTextMessage(std::string message);
 
         void joinChannel(int channel_id);
 
+        void sendUserState(mumlib::UserState field, bool val);
+
+        void sendUserState(mumlib::UserState field, std::string val);
+
+        MumbleUserState userState;
+
+        int callId;
+
     private:
         boost::asio::io_service &ioService;
 
         log4cpp::Category &logger;
 
+        MumbleCommunicatorConfig mumbleConf;
+
         mumlib::MumlibConfiguration mumConfig;
 
         std::shared_ptr<mumlib::Mumlib> mum;
@@ -66,5 +104,6 @@ namespace mumble {
         std::unique_ptr<MumlibCallback> callback;
 
         friend class MumlibCallback;
+
     };
 }

+ 285 - 32
PjsuaCommunicator.cpp

@@ -6,6 +6,8 @@
 #include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 
+#include "main.hpp"
+
 using namespace std;
 
 namespace sip {
@@ -38,9 +40,9 @@ namespace sip {
 
     class _MumlibAudioMedia : public pj::AudioMedia {
     public:
-        _MumlibAudioMedia(sip::PjsuaCommunicator &comm, int frameTimeLength)
+        _MumlibAudioMedia(int call_id, sip::PjsuaCommunicator &comm, int frameTimeLength)
                 : communicator(comm) {
-            createMediaPort(frameTimeLength);
+            createMediaPort(call_id, frameTimeLength);
             registerMediaPort(&mediaPort);
         }
 
@@ -62,7 +64,7 @@ namespace sip {
             return communicator->mediaPortPutFrame(port, frame);
         }
 
-        void createMediaPort(int frameTimeLength) {
+        void createMediaPort(int call_id, int frameTimeLength) {
 
             auto name = pj_str((char *) "MumsiMediaPort");
 
@@ -88,6 +90,8 @@ namespace sip {
             }
 
             mediaPort.port_data.pdata = &communicator;
+            // track call id in port_data
+            mediaPort.port_data.ldata = (long) call_id;
 
             mediaPort.get_frame = &callback_getFrame;
             mediaPort.put_frame = &callback_putFrame;
@@ -107,6 +111,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;
@@ -114,8 +121,8 @@ namespace sip {
 
     class _Account : public pj::Account {
     public:
-        _Account(sip::PjsuaCommunicator &comm)
-                : communicator(comm) { }
+        _Account(sip::PjsuaCommunicator &comm, int max_calls)
+                : communicator(comm) { this->max_calls = max_calls; }
 
         virtual void onRegState(pj::OnRegStateParam &prm) override;
 
@@ -124,7 +131,8 @@ namespace sip {
     private:
         sip::PjsuaCommunicator &communicator;
 
-        bool available = true;
+        int active_calls = 0;
+        int max_calls;
 
         friend class _Call;
     };
@@ -142,23 +150,67 @@ namespace sip {
         if (ci.state == PJSIP_INV_STATE_CONFIRMED) {
             auto msgText = "Incoming call from " + address + ".";
 
+            // first, login to Mumble (only matters if MUM_DELAYED_CONNECT)
+            communicator.calls[ci.id].onConnect();
+            pj_thread_sleep(500); // sleep a moment to allow connection to stabilize
+
             communicator.logger.notice(msgText);
-            communicator.onStateChange(msgText);
+            communicator.calls[ci.id].sendUserStateStr(mumlib::UserState::COMMENT, msgText);
+            communicator.calls[ci.id].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.pins.size() == 0 ) {
+                // No PIN set... enter DTMF root menu and turn off mute/deaf
+                communicator.dtmf_mode = DTMF_MODE_ROOT;
+                // turning off mute automatically turns off deaf
+                communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false);
+                pj_thread_sleep(500); // sleep a moment to allow connection to stabilize
+                this->playAudioFile(communicator.file_announce_new_caller, true);
+            } else {
+                // PIN set... enter DTMF unauth menu and play PIN prompt message
+                communicator.dtmf_mode = DTMF_MODE_UNAUTH;
+                communicator.calls[ci.id].joinDefaultChannel();
+                pj_thread_sleep(500); // pause briefly after announcement
+
+                this->playAudioFile(communicator.file_prompt_pin);
+            }
+
         } else if (ci.state == PJSIP_INV_STATE_DISCONNECTED) {
             auto &acc = dynamic_cast<_Account &>(account);
 
-            if (not acc.available) {
+            /*
+             * Not sure why we check acc.available, but with multi-call
+             * functionality, this check doesn't work.
+             */
+            //if (not acc.available) {
                 auto msgText = "Call from " + address + " finished.";
 
-                communicator.mixer->clear();
+                communicator.calls[ci.id].mixer->clear();
 
                 communicator.logger.notice(msgText);
-                communicator.onStateChange(msgText);
+                communicator.calls[ci.id].sendUserStateStr(mumlib::UserState::COMMENT, msgText);
+                communicator.calls[ci.id].onStateChange(msgText);
+                communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_DEAF, true);
+                communicator.calls[ci.id].joinDefaultChannel();
 
-                acc.available = true;
-            }
+                communicator.calls[ci.id].onDisconnect();
+
+                //acc.available = true;
+                acc.active_calls--;
+            //}
 
             delete this;
+        } else {
+            communicator.logger.notice("MYDEBUG: unexpected state in onCallState() call:%d state:%d",
+                    ci.id, ci.state);
         }
     }
 
@@ -172,15 +224,207 @@ namespace sip {
         if (ci.media[0].status == PJSUA_CALL_MEDIA_ACTIVE) {
             auto *aud_med = static_cast<pj::AudioMedia *>(getMedia(0));
 
-            communicator.media->startTransmit(*aud_med);
-            aud_med->startTransmit(*communicator.media);
+            communicator.calls[ci.id].media->startTransmit(*aud_med);
+            aud_med->startTransmit(*communicator.calls[ci.id].media);
         } else if (ci.media[0].status == PJSUA_CALL_MEDIA_NONE) {
-            dynamic_cast<_Account &>(account).available = true;
+            dynamic_cast<_Account &>(account).active_calls++;
+        }
+    }
+
+    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.info("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 * 2.75));
+
+                if ( in_chan ) { // choose the target sound output
+                    player.startTransmit(*communicator.calls[ci.id].media);
+                } else {
+                    player.startTransmit(*aud_med);
+                }
+
+                pj_thread_sleep(sleeptime);
+
+                if ( in_chan ) { // choose the target sound output
+                    player.stopTransmit(*communicator.calls[ci.id].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());
+        //communicator.logger.notice("DTMF digit '%s' (call %d).",
+        //        prm.digit.c_str(), getId());
+        pj::CallOpParam param;
+
+        auto ci = getInfo();
+        std::string chanName;
+
+        /*
+         * 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.pins.size() > 0 ) {
+                            if ( communicator.pins.count(communicator.got_dtmf) > 0 ) {
+                                communicator.logger.info("Caller entered correct PIN");
+                                communicator.dtmf_mode = DTMF_MODE_ROOT;
+                                communicator.logger.notice("MYDEBUG: %s:%s",
+                                        communicator.got_dtmf.c_str(),
+                                        communicator.pins[communicator.got_dtmf].c_str());
+                                communicator.calls[ci.id].joinOtherChannel(
+                                        communicator.pins[communicator.got_dtmf]);
+
+                                this->playAudioFile(communicator.file_entering_channel);
+                                communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false);
+                                this->playAudioFile(communicator.file_announce_new_caller, true);
+                            } else {
+                                communicator.logger.info("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);
+                        }
+                }
+                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.info("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.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, true);
+                        this->playAudioFile(communicator.file_mute_on);
+                        break;
+                    case '6':
+                        // Un-mute line
+                        this->playAudioFile(communicator.file_mute_off);
+                        communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false);
+                        break;
+                    case '9':
+                        if ( communicator.pins.size() > 0 ) {
+                            communicator.dtmf_mode = DTMF_MODE_UNAUTH;
+                            communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_DEAF, true);
+                            communicator.calls[ci.id].joinDefaultChannel();
+                            this->playAudioFile(communicator.file_prompt_pin);
+                        } else {
+                            // we should have a 'not supported' message
+                        }
+                        break;
+                    case '0': // block these for the menu itself
+                    case '*':
+                    default:
+                        // play menu
+                        communicator.logger.info("Unsupported DTMF digit '%s' in state STAR", prm.digit.c_str());
+                        this->playAudioFile(communicator.file_menu);
+                        break;
+                }
+                /*
+                 * In any case, switch back to root after one digit
+                 */
+                communicator.dtmf_mode = DTMF_MODE_ROOT;
+                break;
+            default:
+                communicator.logger.info("Unexpected DTMF '%s' in unknown state '%d'", prm.digit.c_str(),
+                        communicator.dtmf_mode);
+        }
+
     }
 
     void _Account::onRegState(pj::OnRegStateParam &prm) {
@@ -200,10 +444,12 @@ namespace sip {
 
         if (communicator.uriValidator.validateUri(uri)) {
 
-            if (available) {
+            if (active_calls < max_calls) {
                 param.statusCode = PJSIP_SC_OK;
-                available = false;
+                active_calls++;
             } else {
+                communicator.logger.notice("BUSY - reject incoming call from %s.", uri.c_str());
+                param.statusCode = PJSIP_SC_OK;
                 param.statusCode = PJSIP_SC_BUSY_EVERYWHERE;
             }
 
@@ -216,18 +462,20 @@ namespace sip {
     }
 }
 
-sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength)
+sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength, int maxCalls)
         : logger(log4cpp::Category::getInstance("SipCommunicator")),
           pjsuaLogger(log4cpp::Category::getInstance("Pjsua")),
           uriValidator(validator) {
 
     logWriter.reset(new sip::_LogWriter(pjsuaLogger));
+    max_calls = maxCalls;
+
 
     endpoint.libCreate();
 
     pj::EpConfig endpointConfig;
     endpointConfig.uaConfig.userAgent = "Mumsi Mumble-SIP gateway";
-    endpointConfig.uaConfig.maxCalls = 1;
+    endpointConfig.uaConfig.maxCalls = maxCalls;
 
     endpointConfig.logConfig.writer = logWriter.get();
     endpointConfig.logConfig.level = 5;
@@ -236,11 +484,12 @@ sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator
 
     endpoint.libInit(endpointConfig);
 
-    pj_caching_pool_init(&cachingPool, &pj_pool_factory_default_policy, 0);
-
-    mixer.reset(new mixer::AudioFramesMixer(cachingPool.factory));
-
-    media.reset(new _MumlibAudioMedia(*this, frameTimeLength));
+    for(int i=0; i<maxCalls; ++i) {
+        calls[i].index = i;
+        pj_caching_pool_init(&(calls[i].cachingPool), &pj_pool_factory_default_policy, 0);
+        calls[i].mixer.reset(new mixer::AudioFramesMixer(calls[i].cachingPool.factory));
+        calls[i].media.reset(new _MumlibAudioMedia(i, *this, frameTimeLength));
+    }
 
     logger.info("Created Pjsua communicator with frame length %d ms.", frameTimeLength);
 }
@@ -271,8 +520,8 @@ sip::PjsuaCommunicator::~PjsuaCommunicator() {
     endpoint.libDestroy();
 }
 
-void sip::PjsuaCommunicator::sendPcmSamples(int sessionId, int sequenceNumber, int16_t *samples, unsigned int length) {
-    mixer->addFrameToBuffer(sessionId, sequenceNumber, samples, length);
+void sip::PjsuaCommunicator::sendPcmSamples(int callId, int sessionId, int sequenceNumber, int16_t *samples, unsigned int length) {
+    calls[callId].mixer->addFrameToBuffer(sessionId, sequenceNumber, samples, length);
 }
 
 pj_status_t sip::PjsuaCommunicator::mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame) {
@@ -280,7 +529,8 @@ pj_status_t sip::PjsuaCommunicator::mediaPortGetFrame(pjmedia_port *port, pjmedi
     pj_int16_t *samples = static_cast<pj_int16_t *>(frame->buf);
     pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&(port->info));
 
-    const int readSamples = mixer->getMixedSamples(samples, count);
+    int call_id = (int) port->port_data.ldata;
+    const int readSamples = calls[call_id].mixer->getMixedSamples(samples, count);
 
     if (readSamples < count) {
         pjsuaLogger.debug("Requested %d samples, available %d, filling remaining with zeros.",
@@ -299,9 +549,11 @@ pj_status_t sip::PjsuaCommunicator::mediaPortPutFrame(pjmedia_port *port, pjmedi
     pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&port->info);
     frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
 
+    int call_id = (int) port->port_data.ldata;
+
     if (count > 0) {
-        pjsuaLogger.debug("Calling onIncomingPcmSamples with %d samples.", count);
-        onIncomingPcmSamples(samples, count);
+        pjsuaLogger.debug("Calling onIncomingPcmSamples with %d samples (call_id=%d).", count, call_id);
+        this->calls[call_id].onIncomingPcmSamples(samples, count);
     }
 
     return PJ_SUCCESS;
@@ -318,6 +570,7 @@ void sip::PjsuaCommunicator::registerAccount(string host, string user, string pa
     accountConfig.sipConfig.authCreds.push_back(cred);
 
     logger.info("Registering account for URI: %s.", uri.c_str());
-    account.reset(new _Account(*this));
+    account.reset(new _Account(*this, max_calls));
     account->create(accountConfig);
-}
+}
+

+ 50 - 9
PjsuaCommunicator.hpp

@@ -18,10 +18,19 @@
 #include <climits>
 #include <bits/unique_ptr.h>
 
+// for userState enum
+#include <mumlib.hpp>
+
+#include "main.hpp"
+
+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:
@@ -58,9 +67,27 @@ namespace sip {
 
     class _MumlibAudioMedia;
 
+    struct call {
+        unsigned index;
+        std::unique_ptr<mixer::AudioFramesMixer> mixer;
+        std::unique_ptr<sip::_MumlibAudioMedia> media;
+        pj_caching_pool cachingPool;
+        std::function<void(std::string)> onStateChange;
+        std::function<void(int16_t *, int)> onIncomingPcmSamples;
+        std::function<void(int)> onMuteDeafChange;
+        std::function<void(mumlib::UserState field, bool val)> sendUserState;
+        std::function<void(mumlib::UserState field, std::string val)> sendUserStateStr;
+        std::function<void()> onConnect;
+        std::function<void()> onDisconnect;
+        std::function<void()> onCallerAuth;
+        std::function<void()> joinAuthChannel; // DEPRECATE ?
+        std::function<void(std::string channelNameRegex)> joinOtherChannel;
+        std::function<void()> joinDefaultChannel;
+    };
+
     class PjsuaCommunicator : boost::noncopyable {
     public:
-        PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength);
+        PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength, int maxCalls);
 
         void connect(
                 std::string host,
@@ -71,30 +98,44 @@ namespace sip {
         virtual ~PjsuaCommunicator();
 
         void sendPcmSamples(
+                int callId,
                 int sessionId,
                 int sequenceNumber,
                 int16_t *samples,
                 unsigned int length);
 
-        std::function<void(int16_t *, int)> onIncomingPcmSamples;
-
-        std::function<void(std::string)> onStateChange;
+        // 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;
+        int max_calls;
+
+        // TODO: move these to private?
+        std::string got_dtmf;
+        dtmf_modes_t dtmf_mode = DTMF_MODE_ROOT;
+        int pin_fails = 0;
 
         pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame);
 
         pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);
 
+        call calls[MY_MAX_CALLS];
+
+        std::unordered_map<std::string, std::string> pins;
+
     private:
         log4cpp::Category &logger;
         log4cpp::Category &pjsuaLogger;
 
-        std::unique_ptr<mixer::AudioFramesMixer> mixer;
-
         std::unique_ptr<_LogWriter> logWriter;
         std::unique_ptr<_Account> account;
-        std::unique_ptr<_MumlibAudioMedia> media;
-
-        pj_caching_pool cachingPool;
 
         pj::Endpoint endpoint;
 

+ 91 - 1
README.md

@@ -47,6 +47,91 @@ Remember to add URIs which you want to make calls from. Calls from other URIs wo
 ./mumsi config.ini
 ```
 
+## Configuring
+
+### Multi-Line Support
+
+If your SIP provider allows multiple simultaneous calls, mumsi can be configured to accept
+calls and map them to separate Mumble users. The max\_calls is configure in *config.ini*:
+
+```
+[sip]
+...
+max_calls = 32
+...
+```
+
+Currently, the Mumble connections are established at server start. For usability, the following
+options are recommended:
+
+* caller\_pin
+* autodeaf
+* channelAuthExpression
+
+The maximum number of calls is set in *main.hpp* and should not exceed the
+*PJSUA_MAX_CALLS* in *pjsua.h*, which by default is 32. This can also be recompiled to
+more, if desired.
+
+When mumsi logs into Mumble, it uses the user name from *config.ini* and appends 
+the character '-', followed by the connection number (counter).
+
+*LIMITATIONS:* The code is _alpha_ and needs testing/debugging, especialy in
+the code that uses mumlib::Transport. Also, there is initial work on connecting
+the Mumble user only when the SIP call is active, so the UI for other users is 
+better, but this code is still very buggy and therefore disabled.
+
+### Caller PIN
+
+When the caller\_pin is set, the incoming SIP connection is mute/deaf until the
+caller enters the correct PIN, followed by the '#' symbol. On three failed 
+attempts, the SIP connection is hung up. On success, the Mumble user is moved
+into the channel matching channelAuthExpression, if specified, and then mute/deaf
+is turned off. As a courtesy to the other users, a brief announcement audio
+file is played in the Mumble channel.
+
+The caller\_pin is configured in *config.ini* in the *app* section:
+
+```
+[app]
+caller_pin = 12345
+```
+
+In addition to the caller\_pin, a channelAuthExpression can be set. After
+the caller authenticates with the PIN, the mumsi Mumble user will switch
+to the Mumble channel that matches this expression. When the call is
+completed, the mumsi Mumble user will return to the default channel that
+matches channelNameExpression.
+
+This helps keep the unused SIP connections from cluttering your channel.
+
+### Autodeaf
+
+By default (i.e. autodeaf=0), other Mumble users can only see whether the mumsi
+connection has an active caller if they are in the same channel. This is becaue
+the 'talking mouth' icon is not visible to users in other channels. The mute/deaf
+icons, on the other hand, can be seen by Mumble users when they are in different
+channels, making it easier to spot when a new caller has connected.
+
+Setting `autodeaf=1' causes the mumsi Mumble user to be mute/deaf when there
+is no active SIP call.
+
+### Audio Files
+
+When certain events occur, it is user-friendly to provide some sort of prompting
+confirmation to the user. An example set of WAV files is provided, but they
+can easily be customized or replaced with local versions, if needed. If the
+files are not found, no sound is played. The following events are supported:
+
+- welcome: Played to caller when first connecting to mumsi
+- prompt\_pin: Prompt the caller to enter the PIN
+- entering\_channel: Caller entered PIN and is now entering the Mumble channel
+- announce\_new\_caller: Played to the Mumble channel when adding a new caller
+- invalid\_pin: Let the caller know they entered the wrong PIN
+- goodbye: Hanging up on the caller
+- mute\_on: Self-mute has been turned on (not implemented)
+- mute\_off: Self-mute has been turned off (not implemented)
+- menu: Tell caller the menu options (not implemented)
+
 ## Start at boot
 
 *mumsi* provides no *init.d* scripts, but you can use great daemon mangaer, [Supervisor](http://supervisord.org/).
@@ -65,7 +150,6 @@ stdout_capture_maxbytes=1MB
 redirect_stderr=true
 ```
 
-
 ## Issues
 
 #### Port and NAT
@@ -82,6 +166,12 @@ pjsua_conf_add_port(mediaPool, (pjmedia_port *)port, &id) error: Invalid operati
 
 Some older versions of PJSIP are affected (confirmed for 2.3). In this case you have to update PJSIP to most recent version (2.4.5).
 
+#### mumlib::TrasportException
+
+The multi-caller code is _alpha_ and needs testing/debugging, especialy in
+the code that uses mumlib::Transport. Also, there is initial work on connecting
+the Mumble user only when the SIP call is active, so the UI for other users is 
+better, but this code is still very buggy and therefore disabled.
 
 ## TODO:
 

+ 36 - 1
config.ini.example

@@ -16,6 +16,10 @@ password = foobar
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
 frameLength = 40
 
+# Set the maximum number of SIP calls to allow simultaneously. This should be <= 32.
+# If you need more, recompile PJSUA LIB and also modify the define in main.hpp.
+max_calls = 1
+
 [mumble]
 host = example.org
 port = 64738
@@ -23,6 +27,37 @@ user = mumsi
 password = foobar
 channelNameExpression =
 
+# When here is no SIP connection, the mumble state is set to self_mute/self_deaf
+# so the other users can easily see whether the SIP is connected even when not
+# in the same group
+autodeaf = 1
+
 # Bitrate of Opus encoder in B/s
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
-opusEncoderBitrate = 16000
+opusEncoderBitrate = 16000
+
+# Set to 1 to use client certificates. The certs must be named <user>-cert.pem and
+# the private keys <user>-key.pem.
+use_certs = 0
+
+[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
+
+[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
+
+

+ 181 - 30
main.cpp

@@ -10,6 +10,8 @@
 
 #include <execinfo.h>
 
+#include "main.hpp"
+
 /*
  * Code from http://stackoverflow.com/a/77336/5419223
  */
@@ -26,13 +28,15 @@ static void sigsegv_handler(int sig) {
 
 int main(int argc, char *argv[]) {
     signal(SIGSEGV, sigsegv_handler);
+    int max_calls;
 
     log4cpp::OstreamAppender appender("console", &std::cout);
     log4cpp::PatternLayout layout;
     layout.setConversionPattern("%d [%p] %c: %m%n");
     appender.setLayout(&layout);
     log4cpp::Category &logger = log4cpp::Category::getRoot();
-    logger.setPriority(log4cpp::Priority::NOTICE);
+    logger.setPriority(log4cpp::Priority::DEBUG);
+    //logger.setPriority(log4cpp::Priority::NOTICE);
     logger.addAppender(appender);
 
     if (argc == 1) {
@@ -48,36 +52,18 @@ int main(int argc, char *argv[]) {
 
     boost::asio::io_service ioService;
 
-    sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength"));
-
-    mumble::MumbleCommunicator mumbleCommunicator(ioService);
-
-    mumble::MumbleChannelJoiner mumbleChannelJoiner(conf.getString("mumble.channelNameExpression"));
-
-    using namespace std::placeholders;
-    pjsuaCommunicator.onIncomingPcmSamples = std::bind(
-            &mumble::MumbleCommunicator::sendPcmSamples,
-            &mumbleCommunicator,
-            _1, _2);
-
-    pjsuaCommunicator.onStateChange = std::bind(
-            &mumble::MumbleCommunicator::sendTextMessage,
-            &mumbleCommunicator, _1);
-
-    mumbleCommunicator.onIncomingPcmSamples = std::bind(
-            &sip::PjsuaCommunicator::sendPcmSamples,
-            &pjsuaCommunicator,
-            _1, _2, _3, _4);
+    try {
+        max_calls = conf.getInt("sip.max_calls");
+    } catch (...) {
+        max_calls = 1;
+    }
 
-    mumbleCommunicator.onIncomingChannelState = std::bind(
-            &mumble::MumbleChannelJoiner::checkChannel,
-            &mumbleChannelJoiner,
-            _1, _2);
+    sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength"), max_calls);
 
-    mumbleCommunicator.onServerSync = std::bind(
-            &mumble::MumbleChannelJoiner::maybeJoinChannel,
-            &mumbleChannelJoiner,
-            &mumbleCommunicator);
+    try {
+        pjsuaCommunicator.pins = conf.getChildren("pins");
+    } catch (...) {
+    }
 
     mumble::MumbleCommunicatorConfig mumbleConf;
     mumbleConf.host = conf.getString("mumble.host");
@@ -85,8 +71,173 @@ int main(int argc, char *argv[]) {
     mumbleConf.user = conf.getString("mumble.user");
     mumbleConf.password = conf.getString("mumble.password");
     mumbleConf.opusEncoderBitrate = conf.getInt("mumble.opusEncoderBitrate");
+    /* default to 'false' if not found */
+    try {
+        mumbleConf.autodeaf = conf.getBool("mumble.autodeaf");
+    } catch (...) {
+        mumbleConf.autodeaf = false;
+    }
+
+    try {
+        mumbleConf.comment = conf.getString("app.comment");
+    } catch (...) {
+        mumbleConf.comment = "";
+    }
+
+    try { pjsuaCommunicator.file_welcome = conf.getString("files.welcome");
+    } catch (...) {
+        pjsuaCommunicator.file_welcome = "welcome.wav";
+    }
+
+    try { pjsuaCommunicator.file_prompt_pin = conf.getString("files.prompt_pin");
+    } catch (...) {
+        pjsuaCommunicator.file_prompt_pin = "prompt-pin.wav";
+    }
 
-    mumbleCommunicator.connect(mumbleConf);
+    try { pjsuaCommunicator.file_entering_channel = conf.getString("files.entering_channel");
+    } catch (...) {
+        pjsuaCommunicator.file_entering_channel = "entering-channel.wav";
+    }
+
+    try { pjsuaCommunicator.file_announce_new_caller = conf.getString("files.announce_new_caller");
+    } catch (...) {
+        pjsuaCommunicator.file_announce_new_caller = "announce-new-caller.wav";
+    }
+
+    try { pjsuaCommunicator.file_invalid_pin = conf.getString("files.invalid_pin");
+    } catch (...) {
+        pjsuaCommunicator.file_invalid_pin = "invalid-pin.wav";
+    }
+
+    try { pjsuaCommunicator.file_goodbye = conf.getString("files.goodbye");
+    } catch (...) {
+        pjsuaCommunicator.file_goodbye = "goodbye.wav";
+    }
+
+    try { pjsuaCommunicator.file_mute_on = conf.getString("files.mute_on");
+    } catch (...) {
+        pjsuaCommunicator.file_mute_on = "mute-on.wav";
+    }
+
+    try { pjsuaCommunicator.file_mute_off = conf.getString("files.mute_off");
+    } catch (...) {
+        pjsuaCommunicator.file_mute_off = "mute-off.wav";
+    }
+
+    try { pjsuaCommunicator.file_menu = conf.getString("files.menu");
+    } catch (...) {
+        pjsuaCommunicator.file_menu = "menu.wav";
+    }
+
+    std::string defaultChan = conf.getString("mumble.channelNameExpression"); 
+
+    mumble::MumbleChannelJoiner mumbleChannelJoiner(defaultChan);
+    mumble::MumbleChannelJoiner mumbleOtherChannelJoiner(defaultChan);
+
+    for (int i = 0; i<max_calls; i++) {
+
+        auto *mumcom = new mumble::MumbleCommunicator(ioService);
+        mumcom->callId = i;
+
+        using namespace std::placeholders;
+        // Passing audio input from SIP to Mumble
+        pjsuaCommunicator.calls[i].onIncomingPcmSamples = std::bind(
+                &mumble::MumbleCommunicator::sendPcmSamples,
+                mumcom,
+                _1, _2);
+
+        // PJ sends text message to Mumble
+        pjsuaCommunicator.calls[i].onStateChange = std::bind(
+                &mumble::MumbleCommunicator::sendTextMessage,
+                mumcom,
+                _1);
+
+        /*
+        // Send mute/deaf to Mumble
+        pjsuaCommunicator.calls[i].onMuteDeafChange = std::bind(
+                &mumble::MumbleCommunicator::mutedeaf,
+                mumcom,
+                _1);
+         */
+
+        // Send UserState to Mumble
+        pjsuaCommunicator.calls[i].sendUserState = std::bind(
+                static_cast<void(mumble::MumbleCommunicator::*)(mumlib::UserState, bool)>
+                (&mumble::MumbleCommunicator::sendUserState),
+                mumcom,
+                _1, _2);
+
+        // Send UserState to Mumble
+        pjsuaCommunicator.calls[i].sendUserStateStr = std::bind(
+                static_cast<void(mumble::MumbleCommunicator::*)(mumlib::UserState, std::string)>
+                (&mumble::MumbleCommunicator::sendUserState),
+                mumcom,
+                _1, _2);
+
+        // PJ triggers Mumble connect
+        pjsuaCommunicator.calls[i].onConnect = std::bind(
+                &mumble::MumbleCommunicator::onConnect,
+                mumcom);
+
+        // PJ triggers Mumble disconnect
+        pjsuaCommunicator.calls[i].onDisconnect = std::bind(
+                &mumble::MumbleCommunicator::onDisconnect,
+                mumcom);
+
+        // PJ notifies Mumble that Caller Auth is done
+        pjsuaCommunicator.calls[i].onCallerAuth = std::bind(
+                &mumble::MumbleCommunicator::onCallerAuth,
+                mumcom);
+
+        /*
+        // PJ notifies Mumble that Caller Auth is done
+        pjsuaCommunicator.calls[i].onCallerUnauth = std::bind(
+                &mumble::MumbleCommunicator::onCallerUnauth,
+                mumcom);
+                */
+
+        // PJ notifies Mumble that Caller Auth is done
+        pjsuaCommunicator.calls[i].joinDefaultChannel = std::bind(
+                &mumble::MumbleChannelJoiner::findJoinChannel,
+                &mumbleChannelJoiner,
+                mumcom);
+
+        // PJ notifies Mumble to join other channel
+        pjsuaCommunicator.calls[i].joinOtherChannel = std::bind(
+                &mumble::MumbleChannelJoiner::joinOtherChannel,
+                &mumbleOtherChannelJoiner,
+                mumcom,
+                _1);
+
+        // Passing audio from Mumble to SIP
+        mumcom->onIncomingPcmSamples = std::bind(
+                &sip::PjsuaCommunicator::sendPcmSamples,
+                &pjsuaCommunicator,
+                _1, _2, _3, _4, _5);
+
+        // Handle Channel State messages from Mumble
+        mumcom->onIncomingChannelState = std::bind(
+                &mumble::MumbleChannelJoiner::checkChannel,
+                &mumbleChannelJoiner,
+                _1, _2);
+
+        // Handle Server Sync message from Mumble
+        mumcom->onServerSync = std::bind(
+                &mumble::MumbleChannelJoiner::maybeJoinChannel,
+                &mumbleChannelJoiner,
+                mumcom);
+
+        mumbleConf.user = conf.getString("mumble.user") + '-' + std::to_string(i);
+        try {
+            if ( conf.getBool("mumble.use_certs") ) {
+                mumbleConf.cert_file = mumbleConf.user + "-cert.pem";
+                mumbleConf.privkey_file = mumbleConf.user + "-key.pem";
+            }
+        } catch (...) {
+            logger.info("Client certs not enabled in config");
+        }
+        mumcom->connect(mumbleConf);
+    }
 
     pjsuaCommunicator.connect(
             conf.getString("sip.host"),

+ 6 - 0
main.hpp

@@ -0,0 +1,6 @@
+#pragma once
+
+// IMPORTANT: The default PJSUA_MAX_CALLS in pjsua.h is 32.
+// If you need more, you'll need to re-compile pjsua, too.
+//
+#define MY_MAX_CALLS 32

+ 66 - 0
make-client-certs.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+#
+# make-client-certs.sh - creates the client certs for registering with Mumble
+#
+# Usage:
+#
+#   make-client-certs.sh <username>
+#
+#   make-client-certs.sh <userprefix> <count>
+#
+# Notes:
+#
+# * The certs are self-signed and are not passphrase protected. Depending on
+#   the target environment and usage, this may or may not be OK. If you need
+#   a passphrase, you'll need to hack Mumlib.
+#
+# * The names are hard-coded in mumsi to match <username>-key.pem and 
+#   <username>-cert.pem. This is done to make it easier to configure multi-line
+#   functionality.
+#
+# * When generating files for a series of users, the counter is appended to the
+#   user name, from '0' to one less than the COUNT.
+
+function usage {
+    cat <<EOF
+Usage:
+
+    $0 username
+    $0 user-prefix count
+EOF
+    exit 1
+}
+
+USER="$1"
+COUNT="$2"
+
+# In this 'format', the %s is replaced with the user name generated in
+# the for loop.
+SUBJFMT="/C=DE/ST=HE/L=Ffm/O=Mumble Ext./CN=%s"
+
+if [ -z "$USER" ]; then
+    usage
+fi
+
+if [ -n "$3" ]; then
+    usage
+fi
+
+if [ -z "$COUNT" ]; then
+    COUNT=1
+fi
+
+for ((i=0; i<$COUNT; i++)) {
+    prefix="${USER}${i}"
+    subj=$(printf "$SUBJFMT" $prefix)
+
+    openssl req \
+        -nodes \
+        -new \
+        -x509 \
+        -keyout ${prefix}-key.pem \
+        -out ${prefix}-cert.pem \
+        -subj "$subj"
+}
+
+

+ 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 five to mute.
+Press star six to un-mute.
+Press star nine to change channel.

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, followed by the hash symbol

BIN
media/prompt-pin.wav


+ 1 - 0
media/welcome.msg

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

BIN
media/welcome.wav