The recipes in this chapter are designed to help you with the injection of audio into and the monitoring of channels in your Asterisk environment. Many of the recipes focus on a particular aspect but can be built up or modified using the skills learned in other recipes in this book.
As the manager of a call center, you need to be able to listen in on calls to help with training new employees.
The most simple solution is to
simply connect to any active channel using the ChanSpy()
application, which then provides you
the ability to flip through active channels using DTMF. The b
option means to only listen to actively
bridged calls:
[CallCenterTraining] exten => 500,1,Verbose(2,Listening to live agents) same => n,ChanSpy(,b)
If you only want to spy on certain channels, you can use the chanprefix option to control which types of channels you want to listen to. So, if we just want to listen to SIP channels involved in bridged calls, we would do this:
[CallCenterTraining] exten => 500,1,Verbose(2,Listening to live agents) same => n,ChanSpy(SIP,b)
The default DTMF keys for
controlling ChanSpy()
are as
follows:
There are a lot more options
for ChanSpy()
and ways to use it in
your dialplan. With some creativity, ChanSpy()
can be used in many situations it
wasn’t necessarily designed for (see Triggering Audio Playback into a Call Using DTMF). Not only can ChanSpy()
be used to listen to the
conversation between channels, but you can also speak to a single
channel where only one person can hear you, referred to as
whispering.
Whispering to a channel is
commonplace in situations where a manager of a call center is training
an employee, and needs to listen to an agent during the call. To enable
whispering to channels that are being spied on, use the w
option:
[CallCenterTraining] exten => 500,1,Verbose(2,Listening to live agents with whisper) same => n,ChanSpy(,bw)
Of course, we’re going to be looking for some finer grain control
for who we’re listening to. Perhaps we have several groups of people
(multiple campaigns, different products, etc.) we want to separate and
listen to. One of these groups could simply be the training group. New
agents are all placed into the training group which makes it easy to
scan and listen to calls while providing help where necessary. To do
this, we need to associate channels with a spygroup using the SPYGROUP
channel variable. By setting the
channel variable for a particular channel, ChanSpy()
can then be directed to listen only
to channels in a particular spygroup.
As we show in Making Grandma Louder, we need to make sure the SPYGROUP
channel variable is set on the
channel we want to spy on and whisper to. In order to do this, we need
to use the U()
option of Dial()
, which will execute a subroutine which
sets the channel variable on the called channel verses the calling
channel. While setting the SPYGROUP
channel variable on the calling channel would still get us the ability
to listen to calls like we want, the whispering would be performed to
the wrong channel (i.e., the customer would be listening to the manager
speak, not the agent):
[InboundCallsToAgents] exten => 100,1,Goto(start,1) exten => start,1,Verbose(2,Placing a call to an agent) same => n,Dial(SIP/0000FFFF0001,15,U(subSetSpyGroup^training)) same => n,Hangup() [subSetSpyGroup] exten => s,1,Verbose(2,Setting spygroup) same => n,Set(SPYGROUP=${ARG1}) same => n,Return()
We can make sure the channel variable SPYGROUP
was set on the correct channel by
using the core show channel
CLI
command at the Asterisk console. In our example, the device
0000FFFF0004
placed a call to
0000FFFF0001
using the dialplan we just wrote. By
checking the channel variables set on the call, we can verify our
dialplan is working correctly:
*CLI> core show channels
Channel Location State Application(Data)
SIP/0000FFFF0004-000 start@InboundCalls:2 Up Dial(SIP/0000FFFF0001,15,U(sub
SIP/0000FFFF0001-000 s@app_dial_gosub_vir Up AppDial((Outgoing Line))
2 active channels
1 active call
2 calls processed
Having verified the
direction of the call (0000FFFF0004
used the Dial()
application which is
connected to 0000FFFF0001
), lets look
at the channel variables set for the call:
*CLI> core show channel SIP/0000FFFF0004-TAB
...[snip]...
[email protected]:5060
BRIDGEPEER=SIP/0000FFFF0001-00000001
DIALEDPEERNUMBER=0000FFFF0001
DIALEDPEERNAME=SIP/0000FFFF0001-00000001
DIALSTATUS=ANSWER
...[snip]...
We’ve snipped out a lot of
text, but what we’re looking for is to make sure the calling channel
doesn’t have the SPYGROUP
set for it
as we’re expecting it to be enabled on the channel being called (the
agent). Using the same technique we can verify that:
*CLI> core show channel SIP/0000FFFF0001-TAB
...[snip]...
BRIDGEPVTCALLID=ODIxNTAyNGUyNWViZmM5NGIyOGY1ZTVjYTQ1N2ExNTI.
BRIDGEPEER=SIP/0000FFFF0004-00000000
GOSUB_RETVAL=
SPYGROUP=training
...[snip]...
Having verified our channel variable has been set on the correct channel, lets create the dialplan that will allow our manager to listen and whisper to the channels in the training group:
[CallCenterTraining] exten => 500,1,Verbose(2,Listening to live agents) same => n,ChanSpy(SIP,bwg(training))
With minor modifications to
our existing ChanSpy()
code, we can
now listen to only channels in the training group (option g(training)
) and whisper to them (option
w
). This same technique could be
applied to only listening to specified extensions or callers by placing
only a single channel into the spygroup.
Some additional options
which you may find useful include (to see all available options, use
core show application ChanSpy
from the Asterisk CLI):
You want to appear as a larger company to the outside world by manipulating the pitch of audio when dialing certain extensions.
exten => 100,1,Verbose(2,${CALLERID(all)} is calling reception) same => n,Set(CALLERID(name)=RCP:${CALLERID(name)}) same => n,Set(PITCH_SHIFT(tx)=high) same => n,Dial(SIP/0000FFFF0001,30) same => n,Voicemail(reception@company,u) same => n,Hangup() exten => 200,1,Verbose(2,${CALLERID(all)} is calling sales) same => n,Set(CALLERID(name)=SLS:${CALLERID(name)}) same => n,Set(PITCH_SHIFT(tx)=low) same => n,Dial(SIP/0000FFFF0001,30) same => n,Voicemail(sales@company,u) same => n,Hangup()
Using PITCH_SHIFT()
you can modify the transmitted
and/or received audio pitch either up or down. By changing the pitch of
audio, you can sound like different people of the company. In our
example we’ve used the transmission modifier (tx
) of PITCH_SHIFT()
to raise the pitch of the
transmitted voice when extension 100 is dialed. Similarly we’ve modified
the transmitted pitch to be lower when dialing extension 200. In order
to know which department is being called, we’ve modified the callerID
name by prepending either RCP
for
reception or SLS
for sales.
For both extensions we’ve
dialed the same device within the company. However, depending on which
extension was dialed, we’ve configured separate voicemail boxes. When
setting up the voicemail greetings, you’ll need to modify the
transmitted audio using PITCH_SHIFT()
prior to calling VoicemailMain()
so that your voice
sounds the same across answered calls and the voicemail greeting.
PITCH_SHIFT()
contains several shorthands for
modifying pitch:
In addition to the shorthands,
you can pass the floating point number between 0.1
and 4.0
. A value of 1.0
has no effect on the pitch. A number lower
than 1.0
lowers the pitch and number
greater than 1.0
will raise the
pitch. This can provide some fine grained tuning of the pitch so that
you don’t sound like a robot or Mickey Mouse (which simply has the
effect of being funny, and also makes you difficult to
understand).
Testing can be done either by
dialing another extension at your desk, or making use of the Record()
application and then listening to the
audio recorded. With some fine tuning you can grow your company without
hiring additional employees!
[ConferenceAudio] ; Users would join the conference at extension 100 exten => 100,1,Goto(start,1) ; Trigger audio playback with extension 200 exten => 200,1,Originate(Local/inject@ConferenceAudio/n,exten, ConferenceAudio,quiet_join,1) ; Users join the conference here exten => start,1,NoOp() same => n,Answer() same => n,MeetMe(31337,d) same => n,Hangup() ; Use a couple flags to quietly enter the conference exten => quiet_join,1,NoOp() same => n,Answer() same => n,MeetMe(31337,dtq) ; This triggers the file to be played into the conference exten => inject,1,NoOp() same => n,Playback(silence/1&tt-weasels) same => n,Hangup()
Playing audio into a
conference is really quite a straightforward process. While there are
several ways of triggering the audio playback through a call origination
process (as shown in the See Also section) we’ve utilized the dialplan
origination method for our example. Using the Originate()
application, we’ve used a
Local
channel that plays back an audio file and
connected it to the conference bridge using MeetMe()
by connecting via a dialplan
extension[4]. When we dial extension 200
, the Originate()
application creates a
Local
channel that executes the inject
extension in the ConferenceAudio
context. Once the
Local
channel is created, it is connected to the
quiet_join
extension located in the
ConferenceAudio
context which then
connects to the MeetMe()
application.
Once all these pieces are
connected together, the audio is played into the conference bridge.
We’ve set 3 flags on MeetMe()
within
the quiet_join
extension. They are as
follows:
Our example is a barebones implementation, but with further development, this could be expanded to limit conference lengths with audio being injected periodically to let the participants know how long is left. Or for weekly meetings, agenda items could be injected into the conference to make sure things progress during the allocated meeting time much like is done on shows like Pardon The Interruption.
During a call you want to play back audio to a caller when they send a particular sequence of DTMF tones.
First we start with the creation of a new applicationmap:
; features.conf [applicationmap] play_message => #1,self/caller,Macro(PlayMessage)
Then we create our Macro()
for
triggering the message injection in extensions.conf:
[macro-PlayMessage] exten => s,1,NoOp() same => n,Set(EncodedChannelToPass=${URIENCODE(${DYNAMIC_PEERNAME})}) same => n,Originate(Local/spy-${EncodedChannelToPass}@whisper-channel/n, exten,whisper-channel,audio,1)
Our Macro()
calls via a Local
channel another context which does the audio injection:
[whisper-channel] exten => _spy-.,1,NoOp() same => n,Set(EncodedChannel=${CUT(EXTEN,-,2-3)}) same => n,Set(GROUP(whisper-channel)=${EncodedChannel}) same => n,ExecIf($[${GROUP_COUNT(${EncodedChannel}@ whisper-channel)} > 1]?Hangup()) same => n,Set(ChannelToSpy=${URIDECODE(${EncodedChannel})}) same => n,ChanSpy(${ChannelToSpy},wsqEB) same => n,Hangup() exten => audio,1,NoOp() same => n,Answer() same => n,Wait(0.4) same => n,Set(VOLUME(TX)=-4) ; One example is to return current cost of the call. A lookup to a database or ; webservice would be required to make the data dynamic. ; same => n,SayNumber(7) ; letters/dollar vs. digits/dollars (plural) same => n,Playback(digits/dollars) same => n,SayNumber(48) same => n,Playback(cents) same => n,Hangup()
And then to enable it we need to add the following line into the dialplan that is executed by the channel that will be triggering the audio playback:
exten => _2XX,1,NoOp() same => n,Set(DYNAMIC_FEATURES=play_message) same => n,Dial(SIP/${EXTEN},30) ...
In this solution we’ve
developed a system using the features.conf file to trigger playback of
audio via DTMF[5] to a channel without disconnecting the existing call.
We’ve configured the feature called play_message
(which could be any name not
already in use), and assigned the #1
DTMF key combination to be the trigger. The option defined as self/caller
means that the channel placing the
call will be the one able to trigger the playback, and will also be the
channel that hears the audio that is injected into the call. This is all
done via the PlayMessage
macro we’ll
be defining in the dialplan.
In the macro-PlayMessage
context, we’ve assigned the
name of the channel we’ll be injecting audio to the EncodedChannelToPass
channel variable. The
channel name is obtained from the DYNAMIC_PEERNAME
channel variable which was
set when the play_message
feature was
trigged via the #1
DTMF sequence.
We’ve used the URIENCODE()
dialplan
function to make it easier to pass the value of the channel using the
Local
channel inside the Originate()
application. URIENCODE()
is required as there will be
front-slash (/
) in the channel name,
which would break the format expected by the Originate()
command.
The Originate()
application is used to trigger
dialplan for playing back the audio prompt. The values for where to play
the audio are passed via the spy-
<channel
name>
extension in the whisper-channel
context. The value passed to
us through the extension was URIENCODE()
’d, so we need to decode it. The
value passed to URIENCODE()
comes
from the CUT()
function, which takes
the extension and separates on the hyphen, only passing the second and
third fields, and assigning them to the ChannelToSpy
variable. We are essentially
cutting off the “spy-” part of the extension, as it contains no useful
information. The rest of the extension is the channel name we need to
spy on, as passed to us from the Originate()
command in the PlayMessage
macro.
Using the ChannelToSpy
channel variable, we then inject
audio into the channel using the ChanSpy()
application to whisper to the
channel. Here we look back at the Originate()
line which had a
Local
channel passed to it as one of the values, with
the remaining values being exten
,
whisper-channel
, audio
, and 1
. The remaining values are telling the
Originate()
command to connect the
Local
channel with the extension audio
within the whisper-channel
context, starting at the first
priority. The audio
extension is what will contain the
instructions for what audio to be played into the channel.
The flags passed to the
ChanSpy()
application are as
follows:
Once the Originate()
application has connected the
ChanSpy()
application to the
requested channel, then we need to play back some audio. The audio to be
played into the channel is handled via the audio
extension within the whisper-channel
context. Because we want to
play the audio over the top to give the caller some information, but
don’t want to interrupt the call, we’ve lowered the volume of the audio
using the VOLUME()
function prior to
playing any audio.
We’ve defined some static data
as an example of what audio playback might be like. Of course, you’d
need to add a call above that to make the data dynamic, which could be
done via func_odbc (if the data was
in a relational database), via CURL()
(if returned from a webservice), or any other number of ways in which
the data could be looked up and returned.
Use the MixMonitor()
application:
exten => 7001,1,MixMonitor(${UNIQUEID}.ulaw) same => n,Dial(SIP/myphone)
MixMonitor()
records the audio from both
directions of the phone call and writes it to a file on disk in one of
the audio formats that Asterisk supports. You can see a list of the file
formats that your version of Asterisk supports at the Asterisk CLI. The
Extensions
column identifies which file format extensions can be used in the
recording filename:
*CLI>
core show file formats
Format Name Extensions ------ ---- ---------- gsm wav49 WAV|wav49 slin16 wav16 wav16 slin wav wav adpcm vox vox slin16 sln16 sln16 slin sln sln|raw siren7 siren7 siren7 siren14 siren14 siren14 g722 g722 g722 ulaw au au alaw alaw alaw|al|alw ulaw pcm pcm|ulaw|ul|mu|ulw ilbc iLBC ilbc h264 h264 h264 h263 h263 h263 gsm gsm gsm g729 g729 g729 g726 g726-16 g726-16 g726 g726-24 g726-24 g726 g726-32 g726-32 g726 g726-40 g726-40 g723 g723sf g723|g723sf g719 g719 g719 23 file formats registered.
The syntax for MixMonitor()
in the dialplan is as
follows:
MixMonitor(filename.extension
[,options
[,command
]])
The options
string can contain any of the
following options:
If the specified filename already exists, append to it instead of overwriting it.
Delay the start of the recording until the call has been bridged. Otherwise the recording will start during call setup. If you only want to record the parts of the call once both sides have been answered and are talking, use this option.
Adjust the volume of
the audio heard by the channel that is executing MixMonitor()
. The range is -4
to 4
.
Adjust the volume of
the audio spoken by the channel that is executing MixMonitor()
. The range is -4
to 4
.
Adjust the audio
coming from both directions. The range is -4
to 4
.
The final parameter for
MixMonitor()
is a command
. This is a custom command that will be
executed when the recording is complete. This can be useful for doing
post-processing of
recordings.
See Triggering Call Recording Using DTMF for another recipe that is
related to call recording. To see the built-in documentation that
Asterisk has for MixMonitor()
,
execute this command at the Asterisk CLI:
*CLI>
core show application MixMonitor
You would like to give some users the ability to enable call recording by pressing a key sequence during a call.
Use the automixmon
feature. This feature is built into
Asterisk. You just have to enable it in a couple of configuration files.
First, set the automixmon
option in
features.conf
to the key sequence you
would like to use for enabling or disabling recording:
[featuremap] automixmon = *3
Now that the key sequence
has been set, you must modify extensions.conf to allow callers to use this
feature. This is done by setting the x
and/or X
options in the Dial()
or Queue()
application:
; ; When someone calls my extension, dial my phone and give ; me the ability to enable recording on the fly. ; exten => 7001,1,Dial(SIP/myphone,30,x)
The featuremap
section of features.conf has some other features that
are configured and enabled in this same manner. You configure the key
sequence for the feature in features.conf and then enable the feature on
a per-call basis using arguments to the Dial()
or Queue()
applications. Here is a list of all of
the features in the featuremap
section:
DTMF triggered blind
transfers. Enable it using the t
and/or T
options to Dial()
or Queue()
.
DTMF triggered
attended transfers. Enable it using the t
and/or T
options to Dial()
or Queue()
.
DTMF triggered call
hangup. Enable it using the h
and/or H
options to Dial()
or Queue()
.
DTMF triggered call
parking. Enable it using the k
and/or K
options to Dial()
or Queue()
.
DTMF triggered call
recording using the MixMonitor()
application. Enable it
using the x
and/or X
options to Dial()
or Queue()
.
DTMF triggered call
recording using the Monitor()
application. We recommend using the automixmon
feature instead of this one
unless you have a specific need for using Monitor()
instead of MixMonitor()
. Enable this feature by
specifying the w
and/or
W
options to Dial()
or Queue()
.
For another recipe that uses
MixMonitor()
, see Recording Calls from the Dialplan.
You have a particular caller who is soft spoken, and need to increase the volume of her speech.
[VolumeAdjustment] exten => 100,1,Verbose(2,Incoming call from ${CALLERID(all)}) same => n,GoSubIf($[${CALLERID(num)} = 12565551111]?VolumeAdj,1) same => n,Dial(SIP/0000FFFF0001,30) same => n,Hangup() exten => VolumeAdj,1,Verbose(2,Adjusting volume for grandma) same => n,Set(VOLUME(TX)=3) same => n,Return()
In our solution, we’ve used
the CALLERID()
function to match on a
particular callerID number and, if matching, to execute the VolumeAdj
extension which increases the
receive volume. We’re making use of a GoSubIf()
to execute the VolumeAdj
extension and to return when the
volume adjustment is completed, and then dialing the device 0000FFFF0001
. We could then create a list of
callerID numbers to adjust the volume for if we wished. Because we’re
expecting the other side to be quieter than we would like, we are
increasing the volume of the transmitted audio by specifying VOLUME(TX)
. If we wanted to increase the
volume for the audio received by that channel, we would use VOLUME(RX)
.
The VOLUME()
function must be viewed from the
aspect of the channel that is executing the function. Because there
are two channels in every call, if the channel initiating the Dial()
application executed the VOLUME()
function, then
the RX
option adjusts their receive
volume, and TX
adjusts their
transmit volume.
In addition to changing the
volume prior to calling an application, with the p
option we can permit the VOLUME()
function to listen for DTMF to adjust
the volume of the channel. To increase volume, use the *
key. To lower the volume, use the #
key. With our current dialplan, the DTMF can
be adjusted by the caller, which may not necessarily be what we want. If
we want the VOLUME()
to be adjustable
on the called channel, we need to execute a subroutine on the other
channel just prior to bridging. We can do this with the U()
option to the Dial()
application:
exten => 100,1,Verbose(2,Incoming call from ${CALLERID(all)}) same => n,Dial(SIP/0000FFFF0001,30,U(VolumeAdjustment^3)) same => n,Hangup() exten => s,1,Verbose(2,Adjusting volume for other channel) same => n,Set(VOLUME(RX,p)=${ARG1}) same => n,Return()
We could use the IF()
function to control whether the
subroutine is executed by the Dial()
application:
same => n,Dial(SIP/0000FFFF0001,30,${IF($[${CALLERID(num) = 12565551212]? U(VolumeAdjustment^3))})
The nice thing about adjusting the volume with the VOLUME()
function is that you can apply this
not just to dialing end points, but also to adjust volume prior to
sending calls to other applications which may be recording audio, such
as Voicemail()
or Record()
. In this manner, you can adjust the
volume for people individually but continue utilizing the same dialplan
logic.
If you had a large list of
people with different volumes you needed to adjust, then making use of
the AstDB or func_odbc methods of looking up
information in a database would be the better way to go. By making the
data dynamic, you can handle tens or hundreds of different volume
configurations without much more dialplan. If using func_obdc
and a custom function, the dialplan
may look something like the following:
[VolumeAdjustment] exten => 100,1,Verbose(2,Incoming call from ${CALLERID(all)}) same => n,Set(ARRAY(VolLevel,VolDirection)= ${ODBC_GET_VOLUME_LEVEL(${CALLERID(num)})}) same => n,GoSubIf($[${EXISTS(${VolumeLevel})}]?VolumeAdj,1) same => n,Dial(SIP/0000FFFF0001,30) same => n,Hangup() exten => VolumeAdj,1,Verbose(2,Adjusting volume for grandma) same => n,Set(VOLUME(${VolDirection})=${VolLevel}) same => n,Return()
Once you start abstracting the data with things like func_odbc then you can even control volume based on who is being called (perhaps someone has impaired hearing and requires the volume to be adjusted) and all sorts of other situations.