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:
parent
e0ae088c63
commit
82015dc14c
@ -107,6 +107,9 @@ namespace sip {
|
|||||||
|
|
||||||
virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override;
|
virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override;
|
||||||
|
|
||||||
|
virtual void playAudioFile(std::string file);
|
||||||
|
virtual void playAudioFile(std::string file, bool in_chan);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
sip::PjsuaCommunicator &communicator;
|
sip::PjsuaCommunicator &communicator;
|
||||||
pj::Account &account;
|
pj::Account &account;
|
||||||
@ -145,14 +148,24 @@ namespace sip {
|
|||||||
communicator.logger.notice(msgText);
|
communicator.logger.notice(msgText);
|
||||||
communicator.onStateChange(msgText);
|
communicator.onStateChange(msgText);
|
||||||
|
|
||||||
|
pj_thread_sleep(500); // sleep a moment to allow connection to stabilize
|
||||||
|
this->playAudioFile(communicator.file_welcome);
|
||||||
|
|
||||||
communicator.got_dtmf = "";
|
communicator.got_dtmf = "";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* if no pin is set, go ahead and turn off mute/deaf
|
* if no pin is set, go ahead and turn off mute/deaf
|
||||||
* otherwise, wait for pin to be entered
|
* 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);
|
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) {
|
} 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) {
|
void _Call::onDtmfDigit(pj::OnDtmfDigitParam &prm) {
|
||||||
//communicator.logger.notice("DTMF digit '%s' (call %d).",
|
//communicator.logger.notice("DTMF digit '%s' (call %d).",
|
||||||
// prm.digit.c_str(), getId());
|
// prm.digit.c_str(), getId());
|
||||||
pj::CallOpParam param;
|
pj::CallOpParam param;
|
||||||
|
|
||||||
if ( communicator.pin.length() > 0 ) {
|
/*
|
||||||
if ( prm.digit == "#" ) {
|
* DTMF CALLER MENU
|
||||||
//communicator.logger.notice("DTMF got string command %s",
|
*/
|
||||||
// communicator.got_dtmf.c_str());
|
|
||||||
if ( communicator.got_dtmf == communicator.pin ) {
|
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.logger.notice("Caller entered correct PIN");
|
||||||
|
communicator.dtmf_mode = DTMF_MODE_ROOT;
|
||||||
|
this->playAudioFile(communicator.file_entering_channel);
|
||||||
communicator.onMuteDeafChange(0);
|
communicator.onMuteDeafChange(0);
|
||||||
|
this->playAudioFile(communicator.file_announce_new_caller, true);
|
||||||
} else {
|
} else {
|
||||||
communicator.logger.notice("Caller entered wrong PIN");
|
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;
|
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->hangup(param);
|
||||||
}
|
}
|
||||||
|
this->playAudioFile(communicator.file_prompt_pin);
|
||||||
|
}
|
||||||
communicator.got_dtmf = "";
|
communicator.got_dtmf = "";
|
||||||
} else {
|
}
|
||||||
// communicator.logger.notice("DTMF append %s to %s",
|
break;
|
||||||
// prm.digit.c_str(), communicator.got_dtmf.c_str());
|
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;
|
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);
|
||||||
}
|
}
|
||||||
} 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) {
|
void _Account::onRegState(pj::OnRegStateParam &prm) {
|
||||||
|
@ -18,10 +18,14 @@
|
|||||||
#include <climits>
|
#include <climits>
|
||||||
#include <bits/unique_ptr.h>
|
#include <bits/unique_ptr.h>
|
||||||
|
|
||||||
|
enum dtmf_modes_t {DTMF_MODE_UNAUTH, DTMF_MODE_ROOT, DTMF_MODE_STAR};
|
||||||
|
|
||||||
namespace sip {
|
namespace sip {
|
||||||
|
|
||||||
constexpr int DEFAULT_PORT = 5060;
|
constexpr int DEFAULT_PORT = 5060;
|
||||||
constexpr int SAMPLING_RATE = 48000;
|
constexpr int SAMPLING_RATE = 48000;
|
||||||
|
constexpr int MAX_CALLER_PIN_LEN = 64;
|
||||||
|
constexpr int MAX_PIN_FAILS = 2;
|
||||||
|
|
||||||
class Exception : public std::runtime_error {
|
class Exception : public std::runtime_error {
|
||||||
public:
|
public:
|
||||||
@ -76,8 +80,22 @@ namespace sip {
|
|||||||
int16_t *samples,
|
int16_t *samples,
|
||||||
unsigned int length);
|
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;
|
std::string got_dtmf;
|
||||||
|
dtmf_modes_t dtmf_mode = DTMF_MODE_ROOT;
|
||||||
|
int pin_fails = 0;
|
||||||
|
|
||||||
std::function<void(int16_t *, int)> onIncomingPcmSamples;
|
std::function<void(int16_t *, int)> onIncomingPcmSamples;
|
||||||
|
|
||||||
@ -85,6 +103,8 @@ namespace sip {
|
|||||||
|
|
||||||
std::function<void(int)> onMuteDeafChange;
|
std::function<void(int)> onMuteDeafChange;
|
||||||
|
|
||||||
|
std::function<void(int)> onMuteChange;
|
||||||
|
|
||||||
pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame);
|
pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame);
|
||||||
|
|
||||||
pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);
|
pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);
|
||||||
|
@ -28,11 +28,28 @@ channelNameExpression =
|
|||||||
# in the same group
|
# in the same group
|
||||||
autodeaf = 0
|
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
|
# Caller PIN needed to authenticate the phone call itself. The caller presses
|
||||||
# the PIN, followed by the hash '#' key. On success, the caller is
|
# the PIN, followed by the hash '#' key. On success, the caller is
|
||||||
# unmuted/undeafened. On failure, the SIP call is hung up.
|
# unmuted/undeafened. On failure, the SIP call is hung up.
|
||||||
pin = 4321
|
pin = 4321
|
||||||
|
|
||||||
# Bitrate of Opus encoder in B/s
|
[files]
|
||||||
# Adjust it if you need to meet the specific bandwidth requirements of Murmur server
|
# These files are used for the caller and mumble channel audio clips.
|
||||||
opusEncoderBitrate = 16000
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
49
main.cpp
49
main.cpp
@ -98,9 +98,54 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
/* default to <no pin> */
|
/* default to <no pin> */
|
||||||
try {
|
try {
|
||||||
pjsuaCommunicator.pin = conf.getString("mumble.pin");
|
pjsuaCommunicator.caller_pin = conf.getString("app.caller_pin");
|
||||||
} catch (...) {
|
} 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);
|
mumbleCommunicator.connect(mumbleConf);
|
||||||
|
24
media/Makefile
Normal file
24
media/Makefile
Normal 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 "$@"
|
||||||
|
|
||||||
|
|
1
media/announce-new-caller.msg
Normal file
1
media/announce-new-caller.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
Caller joined
|
BIN
media/announce-new-caller.wav
Normal file
BIN
media/announce-new-caller.wav
Normal file
Binary file not shown.
BIN
media/blow.wav
Normal file
BIN
media/blow.wav
Normal file
Binary file not shown.
1
media/entering-channel.msg
Normal file
1
media/entering-channel.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
entering channel
|
BIN
media/entering-channel.wav
Normal file
BIN
media/entering-channel.wav
Normal file
Binary file not shown.
1
media/goodbye.msg
Normal file
1
media/goodbye.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
Goodbye
|
BIN
media/goodbye.wav
Normal file
BIN
media/goodbye.wav
Normal file
Binary file not shown.
1
media/invalid-pin.msg
Normal file
1
media/invalid-pin.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
Invalid pin
|
BIN
media/invalid-pin.wav
Normal file
BIN
media/invalid-pin.wav
Normal file
Binary file not shown.
3
media/menu.msg
Normal file
3
media/menu.msg
Normal 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
BIN
media/menu.wav
Normal file
Binary file not shown.
1
media/mute-off.msg
Normal file
1
media/mute-off.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
mute off
|
BIN
media/mute-off.wav
Normal file
BIN
media/mute-off.wav
Normal file
Binary file not shown.
1
media/mute-on.msg
Normal file
1
media/mute-on.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
mute on
|
BIN
media/mute-on.wav
Normal file
BIN
media/mute-on.wav
Normal file
Binary file not shown.
1
media/prompt-pin.msg
Normal file
1
media/prompt-pin.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
Please enter pin
|
BIN
media/prompt-pin.wav
Normal file
BIN
media/prompt-pin.wav
Normal file
Binary file not shown.
1
media/welcome.msg
Normal file
1
media/welcome.msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
Welcome to the conference bridge
|
BIN
media/welcome.wav
Normal file
BIN
media/welcome.wav
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user