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

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

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


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


    операционная система: 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
    		}
    	}
    );
    


    Обещанный бонус

    по многочисленным просьбам залил все имеющиеся наработки по теме статьи на github

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 4

      0
      Ждём новую АТС на Perl!
      Спасибо!
      Удачи в дальнейших разработках!
        0
        Обнаружил следующие проблемы в работе скрипта:
        — На некоторых ПК (заведомо более мощных чем тот на котором велась разработка и тестирование) наблюдается задержка во время обмена аудиоданными с модемом. Это приводит к искажению воспроизводимого в модем аудио и к зависанию скрипта. Причину идентифицировать пока не удалось.
        — На разных ПК можно наблюдать различную служебную информацию которая сыпется из ttyUSB2, (RSSI, CLIP, BOOT). Есть предположение что операционная система компьютера сама (без ведома пользователя) выставляет некоторые из нижеприведенных параметров модема.

        Далее привожу ответ модема на команду AT&V. Ответ содержит различные настройки модема
        # &C: 2; &D: 2; &E: 0; &F: 0; &S: 0; &W: 0; E: 1; L: 0; M: 0; Q: 0; V: 1;
        # X: 0; Z: 0; \Q: 3; \S: 0; \V: 0; S0: 0; S2: 43; S3: 13; S4: 10; S5: 8;
        # S6: 2; S7: 50; S8: 2; S9: 6; S10: 14; S11: 95; S30: 0; S103: 1; S104: 1;
        # +FCLASS: 0; +ICF: 3,3; +IFC: 2,2; +IPR: 115200; +DR: 0; +DS: 0,0,2048,6;
        # +WS46: 12; +CBST: 0,0,1;
        # +CRLP: (61,61,48,6,0),(61,61,48,6,1),(240,240,52,6,2);
        # +CV120: 1,1,1,0,0,0; +CHSN: 0,0,0,0; +CSSN: 0,0; +CREG: 0; +CGREG: 0;
        # +CFUN:; +CSCS: «IRA»; +CSTA: 129; +CR: 0; +CRC: 0; +CMEE: 0; +CGDCONT: (1,«IP»,«internet.mts.ru»,«0.0.0.0»,0,0),(2,«IP»,«internet»,«0.0.0.0»,0,0),(3,«IP»,«internet.mts.ru»,«0.0.0.0»,0,0)
        #; +CGDSCONT:; +CGTFT:; +CGEQREQ: (1,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(2,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(3,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(4,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(5,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(6,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(7,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(8,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(9,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(10,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(11,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(12,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(13,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(14,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(15,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0),(16,4,0,0,0,0,2,0,«0E0»,«0E0»,3,0,0)
        #; +CGEQMIN:; +CGQREQ:; +CGQMIN:;; +CGEREP: 0,0; +CGCLASS: «B»;
        # +CGSMS: 1; +CSMS: 0; +CMGF: 0; +CSAS: 0; +CRES: 0;
        # +CSCA: "+79117654321",145; +CSMP: ,,0,0; +CSDH: 0; +CSCB: 0,"","";
        # +FDD: 0; +FAR: 0; +FCL: 0; +FIT: 0,0; +ES: ,,; +ESA: 0,,,,0,0,255,;
        # +CMOD: 0; +CVHU: 1;; +CPIN: ,; +CMEC: 0,0,0; +CKPD: 1,1; +CGATT: 1;
        # +CGACT: 0; +CPBS: «SM»; +CPMS: «SM»,«SM»,«SM»; +CNMI: 0,0,0,0,0;
        # +CMMS: 2; +FTS: 0; +FRS: 0; +FTH: 3; +FRH: 3; +FTM: 96; +FRM: 96;
        # +CCUG: 0,0,0; +COPS: 0,2,""; +CUSD: 0; +CAOC: 1; +CCWA: 0; +CCLK: "";
        # +CLVL: 2; +CMUT: 1; +CPOL: 0,2,"",0,0,0; +CPLS: 0; +CTZR: 0; +CTZU: 0;
        # +CLIP: 0; +COLP: 0; +CDIP: 0; +CLIR: 0; ^PORTSEL: 0; ^CPIN: ,;
        # ^ATRECORD: 0; ^FREQLOCK: 8859956,0; ^CVOICE: 0; ^DDSETEX: 0; ^CMSR: 0;;
        # ^AUTHDATA: 1,0,"",""; ^CRPN: 0,""

        На часть этих настроек не нашел никакой документации. Если у кого таковая имеется — подскажите.
          0
          Обнаружились проблемы с записью в порт голосовой информации на старых одноядерных компах, им не хватает скорости чтобы уложиться в 0.02 секунды. В результате вместо голоса получаем шумы/щелчки и подвисание скрипта. Все таки perl не лучшим образом подходит для потоковой обработки аудиоданных в режиме реального времени. Возможно для операций критичных ко времени выполнения — напишу Си библиотеки и перепишу perl-модули таким образом чтобы работа шла через эти библиотеки. Другими словами backend будет на Си, а perl будет в качестве frontend-а.
            0
            по многочисленным просьбам залил все имеющиеся наработки по теме статьи на github

            Only users with full accounts can post comments. Log in, please.