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).
This commit is contained in:
Scott Hardin 2017-05-22 12:15:07 +02:00
parent e0ae088c63
commit 82015dc14c
24 changed files with 309 additions and 27 deletions

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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
media/Makefile Normal file
View File

@ -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 "$@"

View File

@ -0,0 +1 @@
Caller joined

Binary file not shown.

BIN
media/blow.wav Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
entering channel

BIN
media/entering-channel.wav Normal file

Binary file not shown.

1
media/goodbye.msg Normal file
View File

@ -0,0 +1 @@
Goodbye

BIN
media/goodbye.wav Normal file

Binary file not shown.

1
media/invalid-pin.msg Normal file
View File

@ -0,0 +1 @@
Invalid pin

BIN
media/invalid-pin.wav Normal file

Binary file not shown.

3
media/menu.msg Normal file
View File

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

BIN
media/menu.wav Normal file

Binary file not shown.

1
media/mute-off.msg Normal file
View File

@ -0,0 +1 @@
mute off

BIN
media/mute-off.wav Normal file

Binary file not shown.

1
media/mute-on.msg Normal file
View File

@ -0,0 +1 @@
mute on

BIN
media/mute-on.wav Normal file

Binary file not shown.

1
media/prompt-pin.msg Normal file
View File

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

BIN
media/prompt-pin.wav Normal file

Binary file not shown.

1
media/welcome.msg Normal file
View File

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

BIN
media/welcome.wav Normal file

Binary file not shown.