
Источник картинки: 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 пине, для воспроизведения аудио (ссылка на скетч):
То есть, другими словами, он берёт заранее заготовленный массив данных (подключаемый как библиотека sounddata.h) и воспроизводит его. Выглядит конечно «зело эффектно»:Код передатчика/* * 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); }
Чтобы использовать его как рацию, в качестве источника данных следует использовать микрофон, что потребует соответствующей доработки скетча. Так что заинтересовавшемуся читателю будет над чем поработать ;)
Хотя, честно говоря, требуется для этого не так уж много – обеспечить модуляцию лазерного луча. Другими словами, мерцать его в такт голосу. Например, захватывать сигнал с аналогового пина ардуино с подключенным к нему микрофоном и выводить на другом аналоговом пине, к которому подключен лазер (это на стороне передатчика). На стороне приёмника – обратное преобразование: снимать сигнал с аналогового пина и выводить на другом аналоговом пине, к которому подключено устройство воспроизведения – колонка, динамик и т. д. Понятно, что качество звучания будет оставлять желать лучшего – но для коммуникации сгодится ;)
Нечто примерно подобное было продемонстрировано вот в этой самоделке, только без участия ардуино (просто как принцип действия, который можно взять за основу):
Рассмотренные способы не охватывают все вероятные аппаратно-программные комбинации для передачи голоса, однако их вполне можно использовать в своих проектах, а вариант передачи с помощью радиомодуля nrf — вообще с минимальным вмешательством в код.
В заключение, если говорить об интересных дальнобойных способах передачи информации (хотя это уже и не относится напрямую к передаче голоса) — следует упомянуть ещё один, который вряд ли подойдёт для передачи голоса ввиду его низкой скорости, однако вполне может быть использован для передачи текстовых сообщений — LoRa.
LoRa (Long Range) — запатентованная проприетарная технология модуляции маломощной глобальной сети. Среди её особенностей в рамках нашей задачи можно выделить:
- Дальность передачи существенно выше, чем у других способов (10-15 км. Есть даже прецеденты передачи на 200! км).
- Высокая проникающая способность в городских условиях.
- Низкая пропускная способность (сотни бит/с – десятки кбит/сек). Да, «фоточки» уже не покачаешь. Если только текстовые сообщения.
В США есть целая сеть любителей на базе модулей LoRa — эдакая «сеть Судного Дня», которая позволяет обмениваться информацией внутри сети, даже если «цивилизация перестала существовать» :)
Если экспериментировать с LoRa, то имеет смысл это делать с модулями TTGO LoRa32 SX1276 OLED, которые построены на основе ESP32 и имеют кроме модуля LoRa в своём составе — ещё и OLED дисплей, что может быть достаточно полезно в целом. Например, вот тут есть подробная инструкция по соединению двух таких модулей и исходный код для этого.
Таким образом, можно сказать, что есть множество способов передачи голосовой информации на расстояние и выбор конкретного из них базируется только на конкретной поставленной задаче и аппаратных возможностях того железа, которое будет обеспечивать выбранный тип решения.
А какой способ предпочитаете вы?
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
