Реализация голосового меню на perl через usb модем Huawei e1550

от автора

Совсем недавно я написал пост в котором дал немного теории, и описал практическую реализацию скрипта производящего голосовой обзвон (оповещение) абонентов по списку через usb модем Hyawei e1550. В одном из комментариев был задан вопрос о том как получить во время голосового соединения данные о нажатии кнопок на телефоне абонента. Детальное изучение этого вопроса и привело к созданию этого поста.

В данной статье будет представлена реализация колосового меню, с функциями:
— записи голосового сообщения
— выполнения системных команд
Все это стало возможным благодаря реализации декодера DTMF сигналов основанного на алгоритме Гёрцеля.
В качестве бонуса — архив с реализованными на perl-е скриптами голосового меню.

Среда разработки

операционная система: Linux
Дистрибутив: openSUSE 12.3
Ядро: 3.7.10-1.16-desktop #1 SMP PREEMPT Fri May 31 20:21:23 UTC 2013 (97c14ba) i686 i686 i386 GNU/Linux
Язык программирования: Perl
usb модем: Huawei e1550

Приступим

Реализация содержит следующие файлы и папки:
1. voice_menu.pl — основной скрипт с реализацией функций голосового меню
2. dtmf_decoder.pm — модуль декодирования dtmf сигналов (нажатия кнопок телефона в режиме тонового набора)
3. menu.01.pl — содержит описание голосового меню
4. menu.01 — папка с аудио файлами для menu.01.pl
5. messages — папка с записями голосовых сообщений

voice_menu.pl

#!/usr/bin/perl  use v5.16;              # использовать версию Perl не ниже указанной use strict;             # включить дополнительные проверки use warnings;           # и расширенную диагностику use diagnostics;        # выводить подробную диагностику ошибок use utf8; use locale; no warnings 'utf8';  # подключаем модуль Time::HiRes и импортируем # в текущее пространство имен функцию sleep # особенность данной функции - возможность указывать # задержку меньше секунды use Time::HiRes qw(sleep usleep gettimeofday);  # подключаем модуль dtmf_decoder use dtmf_decoder;   # Для информации: # Сообщения типа CEND выдаются модемом при завершении вызова # и содержат в себе информацию о вызове, о причине завершения вызова # и о состоянии устройства. # формат вывода ^CEND:call_index, duration, end_status, cc_cause # где: # call_index - уникальный идентификатор вызова # duration - длительность вызова в секундах # end_status - код статуса устройства после завершения вызова # cc_cause - код причины завершения вызова  # при подключении модема к компьютеру с OS Linux # создаются 3 usb интерфейса для обмена данными с модемом # обычно это: # /dev/ttyUSB0 - командный интерфейс модема # /dev/ttyUSB1 - голосовой(при включенном голосовом режиме) интерфейс модема # /dev/ttyUSB2 - командный интерфейс модема. Отличается от /dev/ttyUSB0 тем # что с него можно читать не только ответы модема на команды, а также служебные # сообщения. Такие как данные о качестве сигнала, вывод ^CEND и прочее  # указываем порт для отсылки модему звука my $VOICE_PORT = "/dev/ttyUSB4";  # указываем порт для подачи модему команд my $COMMAND_PORT = "/dev/ttyUSB5";  # устанавливаем в: # 0 - чтобы отключить вывод отладочной информации # 1 - чтобы включить вывод отладочной информации my $VERBOSE = 0;  # Открываем командный порт модема на чтение и запись open my $SENDPORT, '+<', $COMMAND_PORT or die "Can't open '$COMMAND_PORT': $!\n";  # Открываем голосовой  порт модема на чтение и запись # чтение аудио потока из порта в данной программе не используется # но вам ничто не мешает превратить данный скрипт в автоответчик например open my $SENDPORT_WAV, '+<', $VOICE_PORT or die "Can't open '$VOICE_PORT': $!\n";   # вызываем функцию ожидания вызовов, которой передаются 1 параметр: #  - имя файла с голосовым меню expect_calls('menu.01.pl');  # по окончании обзвона закрываем все открытые файлы/порты exit_call();    # данная функция производит обзвон абонентов по списку sub expect_calls{     # получаем имя файла с голосовым меню     my $l_file = shift;      # загружаем голосовое меню (файл menu.01.pl)     my $menu = load_menu('menu.01.pl');       # данная команда включает в модеме голосовой режим     # один раз включив его можно удалить/заремарить     # эту команду. Модем запомнит состояние.     #at_send('AT^CVOICE=0');       # данная команда включает в модеме отображение номера звонящего     my $l_rec = at_send("AT+CLIP=1",qr/^(OK|ERROR)/);       # цикл ожидания входящего звонка     while ( ) {         # при входящем звонке должно поступить сообщение RING         $l_rec = at_rec(qr/^(RING)/);         accept_call($menu);     } }   # данная функция производит попытку вызова указного номера # и в случае успеха - транслирует голосовое сообщение sub accept_call{     my $menu = shift;      # в этом массиве хранится стtк перемещений по меню     my $position = [$menu];      # текущее меню     my $cmenu = $position->[0];      my %call_info = ();     # запоминаем время начала     $call_info{start_time} = time;     # ждем сообщения с номером телефона звонящего абонента #+CLIP: "+79117654321",145,,,,0     $call_info{phone} = at_rec(qr/^\+CLIP\: \"(\+\d+)/);     $call_info{phone} =~s/^\+\d//;     # генерим имя файла для записи     $call_info{record_fname} = "phone_$call_info{phone}.time_$call_info{start_time}";      # принимаем входящий вызов     my $l_rec = at_send("ATA",qr/^(OK|ERROR)/);     return 0 if $l_rec eq "ERROR";      # ожидаем установления соединения     $l_rec = at_rec(qr/^\^??(CONN\:1|CEND\:|ERROR)/);     return 0 if $l_rec ne "CONN:1";      # переключаем модем в режим приема/передачи голоса     # OK - переключение прошло успешно     # ERROR - переключение не произведено     # CEND:.... - абонент недоступен, занят или сбросил вызов     $l_rec = at_send('AT^DDSETEX=2',qr/(OK|ERROR|CEND\:)/);     return 0 if $l_rec ne "OK";      # Если дошли до сюда - значит вызов установлен     # Звук модему и от него передается порциями по 320 байт каждые 0.02 секунды     print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \t"."Вызов принят.\n";      my $checker = 0;      my $dtmf = 0;      # буфер для входящих аудиоданных данных     my $snd_in;          # буфер для исходящих аудиоданных данных     my $snd_out = $cmenu->{info_voice};     my $snd_count = 0;     my $snd_max = scalar @{$snd_out};      # открываем файл для записи входящего аудиопотока     my $l_fh = new IO::File "> ./messages/$call_info{record_fname}.raw" or die "Cannot open $call_info{record_fname}.raw : $!";     binmode($l_fh);      # Устанавливаем служебную переменную $| в единицу это отключает буферизацию.     # Таким образом данные в звуковой порт будут отправляться незамедлительно.     $|=1;      # проигрываем приветстви     #play_voice($snd_out);      # запоминаем время для отсчета 0.02 секунд     my $before = gettimeofday;      # основной цикл голосового меню     while (){         if ($snd_count == $snd_max) {             if ($cmenu->{record}==1){                     $snd_out = $menu->{standart_messages}{null}{title_voice};                     $snd_max = scalar @{$snd_out};                     $cmenu->{record}=2;                     print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tПроизводится запись голосового сообщения в [./messages/$call_info{record_fname}.raw].\n";             }              $snd_count = 0;         }          syswrite  $SENDPORT_WAV, $snd_out->[$snd_count] , 320;          sysread $SENDPORT_WAV, $snd_in, 320;         syswrite  $l_fh, $snd_in, 320 if $cmenu->{record} && $cmenu->{record} == 2;          $dtmf = dtmf_sample($snd_in);          if ($dtmf) {             #print "time: [$call_info{start_time}] \tphone: [$call_info{phoe}] \tНажата кнопка [$dtmf].\n";             if ($dtmf eq '#') {                 print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tВыбран возврат в главное меню.\n";                 $position = [$menu];                 $cmenu = $position->[0];                 $snd_out = $menu->{info_voice};                 $snd_count = 0;                 $snd_max = scalar @{$snd_out};             } elsif ($dtmf eq '*') {                 if ((scalar @{$position}) > 1) {                     print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tВыбран возврат в предыдущее меню.\n";                     shift @{$position};                     $cmenu = $position->[0];                     $snd_out = $cmenu->{info_voice};                     $snd_count = 0;                     $snd_max = scalar @{$snd_out};                 }             } elsif ($cmenu->{menu}) {                 if ($cmenu->{menu}{$dtmf}) {                     $cmenu = $cmenu->{menu}{$dtmf};                     print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tВыбран пункт меню [$cmenu->{title}].\n";                     unshift @{$position}, $cmenu;                     $snd_out = $cmenu->{info_voice};                     $snd_count = 0;                     $snd_max = scalar @{$snd_out};                     if ($cmenu->{command}) {                         print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tВыполнена команда [$cmenu->{command}].\n";                         system "$cmenu->{command} &";                     }                 }              }         }          # мониторим состояние звонка         if ($checker==10) {             $l_rec = at_send("AT+CLCC",qr/^\^??(OK|ERROR|CEND)/);             # выходим если сброшен             if ($l_rec eq "CEND") {                 print "time: [$call_info{start_time}] \tphone: [$call_info{phone}] \tВызов завершен.\n";                 return 0             }             $checker=0;         }          # ряд управляющих циклом переменных         $dtmf=0;         $checker++;         $snd_count++;          # ожидаем остаток времени         while( gettimeofday-$before < 0.02 ) { }         $before = gettimeofday;     }      # Вешаем трубку.     at_send('AT+CHUP');      # закрываем файл с полученным сообщением     close $l_fh; }  sub play_voice{     my $voice = shift;     my $count = shift || 1;     while ($count) {         for my $sampe (@{$voice}){             syswrite  $SENDPORT_WAV, $sampe, 320;             #sleep(0.02);             my $before = gettimeofday;             while( gettimeofday-$before < 0.02 ) { }         }         $count--;     } }  # данная функция загружает голосовое меню sub load_menu{     my $l_file_name = shift;     my %voice_menu = do $l_file_name;     $voice_menu{standart_messages}{null}{title_voice} = load_voice($voice_menu{standart_messages}{null}{title_voice_fname});     $voice_menu{standart_messages}{back}{title_voice} = load_voice($voice_menu{standart_messages}{back}{title_voice_fname});     $voice_menu{standart_messages}{back_to_main}{title_voice} = load_voice($voice_menu{title_voice_fname});     load_menu_voices(\%voice_menu,$voice_menu{standart_messages});     return \%voice_menu; }  # данная функция загружает аудиофайлы голосового меню sub load_menu_voices{     my $menu = shift;     my $standart_messages = shift;     $menu->{info_voice} = load_voice($menu->{info_voice_fname});     for my $key (sort {$a <=> $b} keys %{$menu->{menu}}){         my $cur = $menu->{menu}{$key};         my $sub_voice = load_menu_voices($cur,$standart_messages);         $menu->{info_voice} = [@{$menu->{info_voice}},@{$sub_voice}];     }     $menu->{info_voice} = [ @{$menu->{info_voice}},                             @{$standart_messages->{back}{title_voice}},                             @{$standart_messages->{back_to_main}{title_voice}},                             @{$standart_messages->{null}{title_voice}},                             @{$standart_messages->{null}{title_voice}}                           ];     return load_voice($menu->{title_voice_fname}); }  # данная функция загружает голосовое сообщение в массив кусками по 320 байт # принимает 1 параметр - имя файла # формат звуковых данных - pcm, моно, 8000 кГц, 16 бит, signed sub load_voice{     my $l_file_name = shift;     print "FILENAME: [$l_file_name]\n";     my $l_fh = new IO::File "< $l_file_name" or die "Cannot open $l_file_name : $!";     binmode($l_fh);     my @l_bufer = ();     my $i=0;     while (read($l_fh,$l_bufer[$i],320)) { $i++; }     close $l_fh;     return \@l_bufer; }   # данная функция отправляет команду в командный порт модема # и ждет ответа указанного в регулярном выражении # принимает 2 параметра: # 1-й - команда # 2-й - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK) sub at_send{     my $l_cmd = shift;     my $l_rx = shift || qr/(OK)/;     print $SENDPORT "$l_cmd\r";     print "SEND: [$l_cmd]\n" if $VERBOSE;     return at_rec($l_rx); }   # данная функция ждет от модема ответа указанного в регулярном выражении  # принимает 1 параметра - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK) sub at_rec{     my $l_rx = shift || qr/OK/;     my $recive='';     #print "white: [$l_rx]\n";     until ( $recive=~$l_rx ) { 	   $recive=<$SENDPORT>; 	   $recive=~s/[\n\r]+//msg; 	   print "RECIVE: [$recive]\n" if $VERBOSE && $recive;     }     $recive=~$l_rx;     print "END RECIVE: [$recive] [$1] [$l_rx]\n" if $VERBOSE;     return $1; }   # данная функция закрывает ранее открытые порты модема sub exit_call{     print "ОПОВЕЩЕНИЕ ОКОНЧЕНО\n";     close $SENDPORT_WAV;     at_send('AT+CHUP');     close $SENDPORT; } 

dtmf_decoder.pm

# модуль: dtmf_detect # автор:  lastuniverse # за основу взят Си код Mr. Blue: #   http://www.phrack.org/issues.html?issue=50&id=13 # данный модуль содержит реализацию алгоритма Гёрцеля  #   http://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%93%D1%91%D1%80%D1%86%D0%B5%D0%BB%D1%8F #   http://www.dsplib.ru/content/goertzel/goertzel.html  use v5.16;              # использовать версию Perl не ниже указанной use strict;             # включить дополнительные проверки use warnings;           # и расширенную диагностику use diagnostics;        # выводить подробную диагностику ошибок use utf8; use locale;  package dtmf_decoder;   # указываем новое пространство имен  require Exporter;               # загрузить стандартный модуль Exporter our @ISA = qw(Exporter);        # неизвестные имена искать в нем   our @EXPORT = qw(dtmf_sample dtmf_clear);   # имена, экспортируемые по умолчанию our @EXPORT_OK = qw(_recalc );              # имена, экспортируемые по запросу  # в этом хэш массиве будем хранить все наши настройки и данные my %o = (   # хэш массив в котором будут храниться рассчитанные коэффициенты   # необходимые для работы алгоритма Гёрцеля   f => {     '697' => { K => 0, C => 0 },     '770' => { K => 0, C => 0 },     '852' => { K => 0, C => 0 },     '941' => { K => 0, C => 0 },     '1209' => { K => 0, C => 0 },     '1336' => { K => 0, C => 0 },     '1477' => { K => 0, C => 0 },     '1633' => { K => 0, C => 0 },   },   # список частот строк и столбцов таблицы dtmf сигналов   #          1209 Гц   1336 Гц   1477 Гц   1633 Гц   # 697 Гц   1         2         3         A    # 770 Гц   4         5         6         B   # 852 Гц   7         8         9         C   # 941 Гц   *         0         #         D      rf => [ '697', '770', '852', '941' ],   cf => [ '1209', '1336', '1477', '1633' ],   # Вспомогательный хэш массив для определения номера dtmf сигнала   dtmf => {     '697' => { '1209' => 1, '1336' => 2, '1477' => 3, '1633' => 4 },     '770' => { '1209' => 5, '1336' => 6, '1477' => 7, '1633' => 8  },     '852' => { '1209' => 9, '1336' => 10, '1477' => 11, '1633' => 12  },     '941' => { '1209' => 13, '1336' => 14, '1477' => 15, '1633' => 16  },   },   # Список наименований dtmf сигналов (входным параметром является    # значение из вспомогательного массива dtmf)   # выходным - наименование нажатой кгнопки   info => ['NONE', '1', '2', '3', 'A', '4', '5', '6', 'B', '7', '8', '9', 'C', '*', '0', '#', 'D'],   # далее идут параметры используемые для работы алгоритма   tones => 0,   # рассчитывается. содержит количество обрабатываемых частот   rate => 8000, # частота оцифровки обрабатываемого сигнала   len => 100,   # количество оцифровок обрабатываемых за раз                 # значение выбрано из расчета обработать за раз пакет из 320 байт                 # считанных из аудио порта модема (2 байта на 1 оцифровку)                  # (уменьшил до 100 для увеличения скорости обработки)   range => 0.15,      # используется для приведения значений максимальной мощности   thresh => 99999999, # используется для отсекания сигналов с мощностью меньше указанной   mincount => 4,      # минимальное количество пакетов в которых фиксируется нажатие кнопки                       # для того чтобы алгоритм считал кнопку нажатой                       # range, thresh и mincount подбирались опытным путем и тестировались                       # нескольких десятков звуковых файлов содержащих dtmf сигналы и посторонние                       # шумовые эффекты.    debug => 0,     # включает вывод отладочной информации     # хэш массив содержащий временные данные работы алгоритма   t => {     mincount => 0,     sample => [],     power =>  {},     maxpower => 0,     thresh => 0,     on    =>  {},     last_dtmf => ''   } );  # данная функция производит предварительный расчет коэффициентов необходимых для работы алгоритма sub _recalc {   $o{tones} = scalar keys %{$o{f}};   for my $f (sort { $a <=> $b } keys %{$o{f}}) {     $o{f}{$f}{K} = $o{len} * $f / $o{rate};     $o{f}{$f}{C} = 2.0 * cos( 2.0 * 3.14159265 * $o{f}{$f}{K} / $o{len} );     print "COEFF: [$f] \t[$o{f}{$f}{K}] \t[$o{f}{$f}{C}]\n" if $o{debug};   } }  # данная функция производит предварительный расчет мощностей гармоник (указанных частот) sub _calc_power {   my $freq_list = shift;   my @fk = @{$freq_list};   my %ff = %{$o{f}};   my %fp = %{$o{t}{power}};    my %u0 = ();   my %u1 = ();   my $t  = 0.0;   my $in = 0.0;   my $i  = 0;    for my $f (@fk) {     $u0{$f} = 0.0;     $u1{$f} = 0.0;   }    while ($i<$o{len}) {   # feedback     $in = $o{t}{sample}[$i] || 0; # >> 7;     for my $f (@fk) {       $t = $u0{$f};       $u0{$f} = $in + $ff{$f}{C} * $u0{$f} - $u1{$f};       $u1{$f} = $t;     }     $i++;   }    print "MAXPOWER: [" if $o{debug} > 1;   for my $f (@fk) {     $o{t}{power}{$f} = $u0{$f} * $u0{$f} + $u1{$f} * $u1{$f} - $ff{$f}{C} * $u0{$f} * $u1{$f};      $o{t}{maxpower} = $o{t}{power}{$f} if $o{t}{power}{$f} > $o{t}{maxpower};     print "$o{t}{maxpower}, " if $o{debug} > 1;   }   print "]\n" if $o{debug} > 1; }  # данная функция отсекает пакеты с мощьностью сигнала ниже $o{t}{maxpower} # расчитывает проходной уровень мощности для частот # и фиксирует частоты уровень мощности которых выше проходного в массиве $o{t}{on}{$f} sub _midle_calc {   my $freq_list = shift;   my @fl = @{$freq_list};   _calc_power($freq_list);    return 0 if $o{t}{maxpower} < $o{thresh};   $o{t}{thresh} = $o{range}  * $o{t}{maxpower};      my $on_count = 0;   for my $f (@fl) {     if ($o{t}{power}{$f} > $o{t}{thresh}) {       $o{t}{on}{$f} = 1;       $on_count++;     } else {       $o{t}{on}{$f} = 0;     }        }   return $on_count; }  # данная функция производит проверку наличия 2-х частот в обработанном пакете # 1-й частоты из группы частот означающих номер строки из таблицы dtmf # и 1-й частоты из группы частот означающих номер колонки из таблицы dtmf # если проверка пройдена - возвращает значение из массива dtmf (номер) sub _decode {   my $row_count = _midle_calc($o{rf});   return 0 unless $row_count;    my $col_count += _midle_calc($o{cf});   return 0 unless $col_count;   return 0 unless $row_count == 1 && $col_count == 1;   for my $dtmf (@{$o{rf}}) {     if ($o{t}{on}{$dtmf}) {       for my $f (@{$o{cf}}) {         return $o{dtmf}{$dtmf}{$f} if $o{t}{on}{$f};       }     }   }   #return 0 if $on_count == 0;   return 0;  }  # данная функция производит финальную проверку наличия dtmf сигнала # основываясь на его длительности (mincount) отсекая случайные срабатывания # и возвращает название нажатой кнопки из массива info sub _analise {   my $x = _decode();   _sample_clear();   #return $x;    if ($x && $x == $o{t}{last_dtmf}){     $o{t}{mincount}++;   } else {     if ( $o{t}{last_dtmf} && $x != $o{t}{last_dtmf} ) {       if ($o{t}{mincount} >= $o{mincount}){         my $r = $o{t}{last_dtmf};         $o{t}{last_dtmf} = $x;         return $r;       }     }     $o{t}{mincount} = 0;   }      $o{t}{last_dtmf} = $x;   return 0; }  # служебная функция для отчистки результатов промежуточных расчетов # вызывается после расчетов мощьностей для каждого пакета sub _sample_clear {   $o{t}{sample} = [];   $o{t}{power} = {};   $o{t}{maxpower} = 0;   $o{t}{on} = {};   $o{t}{thresh} = 0; }  # функция для отчистки результатов промежуточных расчетов # вызывается по завершении голосового вызова sub dtmf_clear {   _sample_clear();   $o{t}{mincount} = 0;   $o{t}{last_dtmf} = {}; }  # функция принимающая на обработку пакет аудиоданных # и возвращающая название нажатой кнопки в случае # обнаружения dtmf сигнала sub dtmf_sample {   my $_ = shift;   my @a = unpack("s$o{len}");   $o{t}{sample} = \@a;   my $x = _analise();    print "DTMF: [".$o{info}[$x]."]\n" if $x; #&& $o{debug};   return $o{info}[$x] if $x;  }  # производим расчет коэффициентов по умолчанию при подключении модуля _recalc();  1; 

menu.01.pl

use utf8; use locale; ( 	standart_messages => { 		back => { 			title => "возврат в предыдущее меню", # заноситься в лог 			title_voice_fname	=> "./menu.01/back.raw" # озвучка при выборе меню (для возврата в предыдущее меню нажмите *) 		}, 		null => { 			title_voice_fname	=> "./menu.01/null.raw" # озвучка при выборе меню (для возврата в предыдущее меню нажмите *) 		} 	}, 	title => "главное меню", # заноситься в лог 	info_voice_fname	=> "./menu.01/main.menu.info.raw",	# озвучка при входе в меню	(вы находитесь в главном меню компании бла-бла-бла) 	title_voice_fname	=> "./menu.01/main.menu.title.raw",       # озвучка при выборе меню (для возврата в главное меню нажмите #) 	menu => { 		'1' => { 			title => "о нас", # заноситься в лог 			info_voice_fname	=> "./menu.01/sub.menu.1.info.raw",	# озвучка при входе в меню	(наша компания занимается предоставлением услуг в сфере бла-бла-бла) 			title_voice_fname	=> "./menu.01/sub.menu.1.title.raw"       # озвучка при выборе меню (если вы хотите узнать больше о нашей компании нажмите 1) 		}, 		'2' => { 			title => "наши услуги", # заноситься в лог 			info_voice_fname	=> "./menu.01/sub.menu.2.info.raw",	# озвучка при входе в меню	(вы находитесь в меню - наши услуги) 			title_voice_fname	=> "./menu.01/sub.menu.2.title.raw",      # озвучка при выборе меню (если вы хотите ознакомиться с предоставляемыми нами услугами нажмите 2) 			menu => { 				'1'	=> { 					title => "набить морду соседу", # заноситься в лог 					info_voice_fname	=> "./menu.01/sub.menu.2.1.info.raw",	# озвучка при входе в меню	(стоимость услуги "набить морду соседу" составляет бла-бла-бла) 					title_voice_fname	=> "./menu.01/sub.menu.2.1.title.raw"    # озвучка при выборе меню (если вы хотите ознакомиться с условиями предоставления услуги "набить морду соседу" нажмите 1) 				}, 				'2'	=> { 					title => "спровадить тещу", # заноситься в лог 					info_voice_fname	=> "./menu.01/sub.menu.2.2.info.raw",	# озвучка при входе в меню	(стоимость услуги "спровадить тещу" составляет бла-бла-бла) 					title_voice_fname	=> "./menu.01/sub.menu.2.2.title.raw"    # озвучка при выборе меню (если вы хотите ознакомиться с условиями предоставления услуги "спровадить тещу" нажмите 2) 				},  			}			 		}, 		'9' => { 			title => "послушать анекдот", # заноситься в лог 			info_voice_fname	=> "./menu.01/sub.menu.9.info.raw",	# озвучка при входе в меню	(анекдот) 			title_voice_fname	=> "./menu.01/sub.menu.9.title.raw"       # озвучка при выборе меню (если вы хотите послушать анекдот нажмите 9) 		}, 		'8' => { 			title => "удалить голосовое меню", # заноситься в лог 			info_voice_fname	=> "./menu.01/sub.menu.8.info.raw",	# озвучка при входе в меню	(голосовое меню удалено) 			title_voice_fname	=> "./menu.01/sub.menu.8.title.raw",      # озвучка при выборе меню (если вы хотите удалить программу "голосовое меню" нажмите 8) 			command	=> 'echo "Не стоит так делать -> rm -R *"' 		}, 		'7' => { 			title => "оставить голосовое сообщение", # заноситься в лог 			info_voice_fname	=> "./menu.01/sub.menu.7.info.raw",	# озвучка при входе в меню	(вы можете оставить ваше сообщение после гудка) 			title_voice_fname	=> "./menu.01/sub.menu.7.title.raw",      # озвучка при выборе меню (если вы хотите оставить голосовое сообщение нажмите 7) 			record	=> 1 		} 	} ); 
Обещанный бонус

архив с исходниками на случай хабрапарсера

Если найдете ошибки, пишите в личку, исправлю.

ссылка на оригинал статьи http://habrahabr.ru/post/193586/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *