Источник картинки: silenthollywood.com

Использование микроконтроллеров ESP32/Arduino позволяет создавать достаточно любопытные проекты, среди которых особняком стоят проекты для передачи голоса. Например, создать собственную рацию, которая может быть весьма тонко настроена на программном уровне. В этой статье мы обзорно рассмотрим несколько известных способов для реализации подобной задумки.

В одном из проектов был продемонстрирован достаточно любопытный подход: возможность выбора одного из двух способов передачи данных:

Использование широковещательной рассылки UDP


Широковещательная рассылка UDP — очень простой механизм. Вы отправляете UDP-пакет на специальный IP-адрес, и ваш маршрутизатор передаёт этот пакет всем другим устройствам в вашей сети.

Мы можем безопасно отправлять до 1436 байт в пакете UDP, поэтому, если мы семплируем на частоте 16 кГц и используем 8-битные сэмплы, это около 90 мс аудиоданных. Таким образом, нам нужно отправить около 11 пакетов в секунду. Это вполне соответствует возможностям ESP32.

Большим преимуществом использования широковещательного UDP является то, что нам не нужно знать о наших одноранговых узлах, мы можем просто передать сообщение, и любой, кто его слушает, получит его.

Однако у UDP есть и некоторые недостатки:

  • Доставка UDP-пакетов — это только максимальные усилия — нет никакой гарантии, что кто-то получит отправленный вами пакет.
  • Также нет гарантии порядка пакетов — кто-то может получить отправленные вами пакеты в совершенно случайном порядке.

В рассмотренном проекте эти две проблемы проигнорированы. С широковещательными пакетами мы обычно остаёмся в одной сети, поэтому мы, вполне вероятно, не потеряем слишком много пакетов, и наши пакеты, возможно, также будут поступать в правильном порядке. Если они этого не сделают, то мы просто получим немного шума и искажения звука.

Другим большим преимуществом широковещательной передачи UDP является ��о, что вы можете получать пакеты на свой настольный компьютер или телефон, поэтому было бы довольно легко создавать дополнительных клиентов, не основанных на ESP32.

Использование ESP-NOW


ESP-NOW — это протокол, разработанный Esppresif, который позволяет нескольким устройствам ESP взаимодействовать друг с другом без необходимости использования Wi-Fi.

Это даёт нам большое преимущество перед опцией UDP, заключающееся в том, что нам не нужна сеть Wi-Fi, чтобы наша рация работала.

Недостатком ESP-NOW является гораздо меньший размер пакета — 250 байт. Это означает, что нам нужно отправлять пакеты 64 раза в секунду.

У нас также есть все те же недостатки, что и у UDP — доставка пакетов осуществляется с максимальной эффективностью и нет гарантий, в каком порядке будут приходить пакеты.
Выбор одного из двух режимов осуществляется с помощью комментирования / раскомментирования строки 45 в файле Config:

// Which transport do you want to use? ESP_NOW or UDP?
// comment out this line to use UDP
// #define USE_ESP_NOW

Единственным минусом использования решения на базе радиомодуля ESP32 является некоторая ограниченность радиуса действия устройства, так как wi-fi модуль ESP32 не предназначен изначально до передачи на большие расстояния. Да, я понимаю, вы можете сказать, что с использованием отражателя или тарелки — расстояние можно существенно увеличить: мне известны подобные эксперименты, где радиус действия увеличили вплоть до 10 км. Однако подобное решение всё-таки существенно выбивается за рамки и требует достаточно точной настройки друг на друга как приёмника, так и передатчика.

Альтернативным способом передачи голосового потока является использование известных дальнобойных плат радиосвязи NRF24L01+PA+LNA, которые позволяют (при соответствующем падении скорости передачи с ростом дальности) увеличить радиус, вплоть до более чем 1 километра.



Для этого нам надо будет использовать библиотеку RF24Audio — это поистине библиотека «для ленивых людей», так как сам скетч полноценной рации не содержит даже и 10 строк:

#include <RF24.h>
#include <SPI.h>
#include <RF24Audio.h>

RF24 radio(7,8);  // Set radio up using pins 7 (CE) 8 (CS)
RF24Audio rfAudio(radio,1); // Set up the audio using the radio, and set to radio number 0.
                                // Setting the radio number is only important if one-to-one communication is desired
    // in a multi-node radio group. See the privateBroadcast() function.

void setup() {   

  rfAudio.begin();  // The only thing to do is initialize the library.

}

void loop() {
  
  // Audio playback and button handling is all managed internally.
  // In this example, the radio is controlled by external buttons, so there is nothing to do here
  
}

Чтобы работать с ним, нам необходимо:

  • микрофон подключить к пину А0;
  • кнопку переключателя, которая переключает режимы, для осуществления передачи следует соединить с пином А1;
  • кнопка прибавления громкости должна быть подключена к пину A2;
  • убавление громкости к пину А3;
  • кстати сказать, библиотека предоставляет достаточно интересную функцию, которая позволяет включать удалённо передающее устройство! То есть не совсем включать, а переводить его в режим «прослушки, что вокруг происходит» ;). Для этого следует использовать кнопку, присоединённую к пину A4.

Да, на больших расстояниях качество передачи будет наверняка падать, однако, как могут подтвердить олдфаги, заставшие ещё времена модемной связи с интернетом, скорость в 250 кбит/сек (на расстоянии в 1100 м, которое заявляется максимальным для этого радиомодуля) является более чем достаточной для передачи хорошо слышимого голоса.

Следует отметить, что если кто-то соберётся экспериментировать с этим передатчиком, то стоит понимать, радиомодуль nrf является весьма чувствительным к качеству питания (напряжение и сила тока), поэтому питать его от стандартного пина Arduino совершенно недостаточно.

В идеале следует использовать специальные модули для питания:



Если же это недоступно, то тебе следует использовать любой другой модуль, например, регулятор/стабилизатор напряжения (так называемый DC-DC модуль). Например, MT3608:



Аппаратная часть проекта на основе этой идеи хорошо разобрана здесь, если кому интересно.

Сейчас я специально не останавливаюсь подробно на аппаратной части подобных затей, так как это выйдет далеко за пределы статьи, если описывать всё подробно. Поэтому я привёл выше только ссылки, которые помогут вам разобраться, если эта тема вас заинтересует.

Говоря о способах передачи голоса на расстояние, я совершенно не могу пройти мимо одного из проектов, поскольку он «реализует мои потаённые желания» и одну нереализованную задумку из далёкой молодости — передача голоса с помощью лазерного луча.

В своё время мне очень хотелось создать нечто подобное, так как в те времена, когда динозавры были зеленее, а сотовой связи не было, мы с одним товарищем жили в прямой видимости друг от друга, на расстоянии порядка 100 метров, и подобный проект был бы как нельзя кстати. Единственная проблема, что в те времена не существовало ещё Arduino :)

Приведённую ниже затею можно взять в качестве основы, так как основная задумка, положенная в основу проекта, заключается в использовании ШИМ-модуляции на 11 пине, для воспроизведения аудио (ссылка на скетч):
Код передатчика
/*
 * speaker_pcm
 *
 * Plays 8-bit PCM audio on pin 11 using pulse-width modulation (PWM).
 * For Arduino with Atmega168 at 16 MHz.
 *
 * Uses two timers. The first changes the sample value 8000 times a second.
 * The second holds pin 11 high for 0-255 ticks out of a 256-tick cycle,
 * depending on sample value. The second timer repeats 62500 times per second
 * (16000000 / 256), much faster than the playback rate (8000 Hz), so
 * it almost sounds halfway decent, just really quiet on a PC speaker.
 *
 * Takes over Timer 1 (16-bit) for the 8000 Hz timer. This breaks PWM
 * (analogWrite()) for Arduino pins 9 and 10. Takes Timer 2 (8-bit)
 * for the pulse width modulation, breaking PWM for pins 11 & 3.
 *
 * References:
 *     https://www.uchobby.com/index.php/2007/11/11/arduino-sound-part-1/
 *     https://www.atmel.com/dyn/resources/prod_documents/doc2542.pdf
 *     https://www.evilmadscientist.com/article.php/avrdac
 *     https://gonium.net/md/2006/12/27/i-will-think-before-i-code/
 *     https://fly.cc.fer.hr/GDM/articles/sndmus/speaker2.html
 *     https://www.gamedev.net/reference/articles/article442.asp
 *
 * Michael Smith <michael@hurts.ca>
 */

#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define SAMPLE_RATE 8000

/*
 * The audio data needs to be unsigned, 8-bit, 8000 Hz, and small enough
 * to fit in flash. 10000-13000 samples is about the limit.
 *
 * sounddata.h should look like this:
 *     const int sounddata_length=10000;
 *     const unsigned char sounddata_data[] PROGMEM = { ..... };
 *
 * You can use wav2c from GBA CSS:
 *     https://thieumsweb.free.fr/english/gbacss.html
 * Then add "PROGMEM" in the right place. I hacked it up to dump the samples
 * as unsigned rather than signed, but it shouldn't matter.
 *
 * https://musicthing.blogspot.com/2005/05/tiny-music-makers-pt-4-mac-startup.html
 * mplayer -ao pcm macstartup.mp3
 * sox audiodump.wav -v 1.32 -c 1 -r 8000 -u -1 macstartup-8000.wav
 * sox macstartup-8000.wav macstartup-cut.wav trim 0 10000s
 * wav2c macstartup-cut.wav sounddata.h sounddata
 *
 * (starfox) nb. under sox 12.18 (distributed in CentOS 5), i needed to run
 * the following command to convert my wav file to the appropriate format:
 * sox audiodump.wav -c 1 -r 8000 -u -b macstartup-8000.wav
 */

#include "sounddata.h"

int ledPin = 13;
int speakerPin = 11; // Can be either 3 or 11, two PWM outputs connected to Timer 2
volatile uint16_t sample;
byte lastSample;


void stopPlayback()
{
    // Disable playback per-sample interrupt.
    TIMSK1 &= ~_BV(OCIE1A);

    // Disable the per-sample timer completely.
    TCCR1B &= ~_BV(CS10);

    // Disable the PWM timer.
    TCCR2B &= ~_BV(CS10);

    digitalWrite(speakerPin, LOW);
}

// This is called at 8000 Hz to load the next sample.
ISR(TIMER1_COMPA_vect) {
    if (sample >= sounddata_length) {
        if (sample == sounddata_length + lastSample) {
            stopPlayback();
        }
        else {
            if(speakerPin==11){
                // Ramp down to zero to reduce the click at the end of playback.
                OCR2A = sounddata_length + lastSample - sample;
            } else {
                OCR2B = sounddata_length + lastSample - sample;                
            }
        }
    }
    else {
        if(speakerPin==11){
            OCR2A = pgm_read_byte(&sounddata_data[sample]);
        } else {
            OCR2B = pgm_read_byte(&sounddata_data[sample]);            
        }
    }

    ++sample;
}

void startPlayback()
{
    pinMode(speakerPin, OUTPUT);

    // Set up Timer 2 to do pulse width modulation on the speaker
    // pin.

    // Use internal clock (datasheet p.160)
    ASSR &= ~(_BV(EXCLK) | _BV(AS2));

    // Set fast PWM mode  (p.157)
    TCCR2A |= _BV(WGM21) | _BV(WGM20);
    TCCR2B &= ~_BV(WGM22);

    if(speakerPin==11){
        // Do non-inverting PWM on pin OC2A (p.155)
        // On the Arduino this is pin 11.
        TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
        TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
        // No prescaler (p.158)
        TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set initial pulse width to the first sample.
        OCR2A = pgm_read_byte(&sounddata_data[0]);
    } else {
        // Do non-inverting PWM on pin OC2B (p.155)
        // On the Arduino this is pin 3.
        TCCR2A = (TCCR2A | _BV(COM2B1)) & ~_BV(COM2B0);
        TCCR2A &= ~(_BV(COM2A1) | _BV(COM2A0));
        // No prescaler (p.158)
        TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set initial pulse width to the first sample.
        OCR2B = pgm_read_byte(&sounddata_data[0]);
    }





    // Set up Timer 1 to send a sample every interrupt.

    cli();

    // Set CTC mode (Clear Timer on Compare Match) (p.133)
    // Have to set OCR1A *after*, otherwise it gets reset to 0!
    TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
    TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));

    // No prescaler (p.134)
    TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

    // Set the compare register (OCR1A).
    // OCR1A is a 16-bit register, so we have to do this with
    // interrupts disabled to be safe.
    OCR1A = F_CPU / SAMPLE_RATE;    // 16e6 / 8000 = 2000

    // Enable interrupt when TCNT1 == OCR1A (p.136)
    TIMSK1 |= _BV(OCIE1A);

    lastSample = pgm_read_byte(&sounddata_data[sounddata_length-1]);
    sample = 0;
    sei();
}


void setup()
{
    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, HIGH);
    startPlayback();
}

void loop()
{
    while (true);
}
То есть, другими словами, он берёт заранее заготовленный массив данных (подключаемый как библиотека sounddata.h) и воспроизводит его. Выглядит конечно «зело эффектно»:

Чтобы использовать его как рацию, в качестве источника данных следует использовать микрофон, что потребует соответствующей доработки скетча. Так что заинтересовавшемуся читателю будет над чем поработать ;)

Хотя, честно говоря, требуется для этого не так уж много – обеспечить модуляцию лазерного луча. Другими словами, мерцать его в такт голосу. Например, захватывать сигнал с аналогового пина ардуино с подключенным к нему микрофоном и выводить на другом аналоговом пине, к которому подключен лазер (это на стороне передатчика). На стороне приёмника – обратное преобразование: снимать сигнал с аналогового пина и выводить на другом аналоговом пине, к которому подключено устройство воспроизведения – колонка, динамик и т. д. Понятно, что качество звучания будет оставлять желать лучшего – но для коммуникации сгодится ;)

Нечто примерно подобное было продемонстрировано вот в этой самоделке, только без участия ардуино (просто как принцип действия, который можно взять за основу):

Рассмотренные способы не охватывают все вероятные аппаратно-программные комбинации для передачи голоса, однако их вполне можно использовать в своих проектах, а вариант передачи с помощью радиомодуля nrf — вообще с минимальным вмешательством в код.

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

LoRa (Long Range) — запатентованная проприетарная технология модуляции маломощной глобальной сети. Среди её особенностей в рамках нашей задачи можно выделить:

  • Дальность передачи существенно выше, чем у других способов (10-15 км. Есть даже прецеденты передачи на 200! км).
  • Высокая проникающая способность в городских условиях.
  • Низкая пропускная способность (сотни бит/с – десятки кбит/сек). Да, «фоточки» уже не покачаешь. Если только текстовые сообщения.

В США есть целая сеть любителей на базе модулей LoRa — эдакая «сеть Судного Дня», которая позволяет обмениваться информацией внутри сети, даже если «цивилизация перестала существовать» :)

Если экспериментировать с LoRa, то имеет смысл это делать с модулями TTGO LoRa32 SX1276 OLED, которые построены на основе ESP32 и имеют кроме модуля LoRa в своём составе — ещё и OLED дисплей, что может быть достаточно полезно в целом. Например, вот тут есть подробная инструкция по соединению двух таких модулей и исходный код для этого.

Таким образом, можно сказать, что есть множество способов передачи голосовой информации на расстояние и выбор конкретного из них базируется только на конкретной поставленной задаче и аппаратных возможностях того железа, которое будет обеспечивать выбранный тип решения.

А какой способ предпочитаете вы?


НЛО прилетело и оставило здесь промокод для читателей нашего блога:

15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.