Русский
Русский
English
Статистика
Реклама

Wav

Учим железки разговаривать, или ESP32 DAC и немного таймера

14.11.2020 20:09:26 | Автор: admin
В ходе разработки одного очень интересного устройства (эх, лишь бы силенок хватило) я решил, что будет неплохо, если устройство это будет говорящим. Как нельзя кстати здесь пригодилось наличие в целевом микроконтроллере, ESP32 компании Espressif Systems, двухканального 8-битного ЦАПа.

В этом туториале (если его можно так назвать) я покажу, как можно быстро и довольно просто организовать проигрывание аудиофайла силами микроконтроллера ESP32.

Немного теории

Как нам сообщает Википедия, ESP32 это серия недорогих микроконтроллеров с низким энергопотреблением. Они представляют собой систему на кристалле (SoC) с интегрированными контроллерами Wi-Fi и Bluetooth, и антеннами. Основаны на ядре Tensilica Xtensa LX6 в вариантах с одним и двумя ядрами. В систему интегрирован радиочастотный тракт. МК создан и разработан китайской компанией Espressif Systems, а производится компанией TSMC по техпроцессу 40 нм. Подробнее о возможностях чипа можно прочитать на странице в Википедии и в официальной документации.

Однажды в рамках освоения этого контроллера мне захотелось воспроизвести на нем звук. Поначалу я думал, что придется использовать ШИМ. Однако, внимательнее почитав документацию, я обнаружил наличие двух каналов 8-битного ЦАПа. Разумеется, это в корне меняло дело.

В Technical Reference сказано, что ЦАП в ESP32 построен на цепочке резисторов (видимо, имеется ввиду R2R цепочка) с применением некоего буфера. Выходное напряжение может изменяться в пределах от 0 вольт до напряжения питания (3,3 вольта) с разрешением 8 бит (то есть 256 значений). Преобразование двух каналов независимое. Также имеется встроенный генератор косинуса (CW generator) и поддерживается DMA.

Я решил пока что не лезть в DMA, ограничившись построением проигрывателя на основе таймера. Как известно, чтобы воспроизвести простейший WAV-файл формата PCM, достаточно с указанной в файле частотой дискретизации читать из него сырые данные и распихивать по каналам ЦАПа, предварительно приводя (при необходимости) разрядность данных к разрядности ЦАП. Мне повезло: у меня нашелся набор звуков в формате WAV PCM 8 bit 11025 Hz mono, выдранный из ресурсов одной старой игры. Значит, будем использовать только один канал ЦАП.

Также нам понадобится таймер, способный генерировать прерывания с частотой 11025 Гц. Согласно все тому же Technical Reference, ESP32 имеет на борту два модуля таймеров по два таймера в каждом, итого четыре таймера. Они имеют разрядность 64 бита, у каждого имеется 16-битный предделитель и возможность генерации прерывания по уровню или фронту.

От теории к практике

Вооружившись примером wave_gen из esp-idf, я отправился писать код. Я не стал упарываться созданием файловой системы: цель была получить звук, а не сделать из ESP32 полноценный плеер.

Для начала перегнал один из WAV-файлов в сишный массив. В этом мне очень помогла встроенная в Дебиан утилита xxd. Простой командой
$ xxd -i file.wav > file.c

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

Далее я закомментировал первые 44 байта массива заголовок WAV-файла. Попутно я разобрал его по полям и узнал всю необходимую мне информацию о нем:
const uint8_t sound_wav[] = {//  0x52, 0x49, 0x46, 0x46,// chunk "RIFF"//  0xaa, 0xb4, 0x01, 0x00,// chunk length//  0x57, 0x41, 0x56, 0x45,// "WAVE"//  0x66, 0x6d, 0x74, 0x20,// subchunk1 "fmt"//  0x10, 0x00, 0x00, 0x00,// subchunk1 length//  0x01, 0x00,// audio format PCM//  0x01, 0x00,// 1 channel, mono//  0x11, 0x2b, 0x00, 0x00,// sample rate//  0x11, 0x2b, 0x00, 0x00,// byte rate//  0x01, 0x00,// bytes per sample//  0x08, 0x00,// bits per sample per channel//  0x64, 0x61, 0x74, 0x61,// subchunk2 "data"//  0x33, 0xb4, 0x01, 0x00,// subchunk2 length, bytes

Отсюда видно, что наш файл имеет один канал, частоту дискретизации 11025 герц и разрешение 8 бит на семпл. Заметим, что если бы я захотел анализировать заголовок программно, то мне нужно было бы учитывать порядок байт: в WAV это Little-endian, то есть младшим байтом вперед.

В итоге я создал тип структуры для хранения информации о звуке:
typedef struct _audio_info{uint32_t sampleRate;uint32_t dataLength;const uint8_t *data;} audio_info_t;

И создал собственно экземпляр структуры, заполнив ее следующим образом:
const audio_info_t sound_wav_info ={11025, // sampleRate111667, // dataLengthsound_wav // data};

В этой структуре поле sampleRate это значение одноименного поля заголовка, поле dataLength это значение поля subchunk2 length, а поле data это указатель на массив с данными.

Далее я подключил заголовочные файлы
#include "driver/timer.h"#include "driver/dac.h"

и создал прототипы функций для инициализации таймера и обработчика его прерывания Alarm, как это сделано в примере wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr){}static void timerInit(){}

После чего принялся за наполнение функции инициализации.
Таймеры в ESP32 тактируются в конечном итоге от APB_CLK_FREQ, равного 80 МГц:

driver/timer.h:
#define TIMER_BASE_CLK   (APB_CLK_FREQ)  /*!< Frequency of the clock on the input of the timer groups */

soc/soc.h:
#define  APB_CLK_FREQ    ( 80*1000000 )       //unit: Hz

Чтобы получить значение счетчика, при котором нужно генерировать прерывание Alarm, нужно частоту тактирования таймера поделить на значение предделителя, а затем на требуемую частоту, с которой должно срабатывать прерывание (у нас это 11025 Гц). В обработчик прерывания мы будем передавать указатель на структуру с данными, которые мы хотим воспроизводить.

Таким образом, функция инициализации таймера получает следующий вид:
static void timerInit(){timer_config_t config = {.divider = 8, // Предделитель.counter_dir = TIMER_COUNT_UP, // Считать вверх.counter_en = TIMER_PAUSE, // Состояние - пауза.alarm_en = TIMER_ALARM_EN, // Включить прерывание Alarm.intr_type = TIMER_INTR_LEVEL, // Прерывание по уровню.auto_reload = 1, // Автоматически перезапускать счет};// Применить конфигESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config));// Установить начальное значение счетчикаESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL));// Установить значение счетчика для срабатывания прерывания AlarmESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate));// Разрешить прерыванияESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0));// Зарегистрировать обработчик прерыванияtimer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL);// Запустить таймерtimer_start(TIMER_GROUP_0, TIMER_0);}

Частота тактирования таймера не делится нацело на 11025, какой бы предделитель мы не установили. Поэтому я подобрал такой делитель, при котором частота максимально близка к требуемой.

Теперь переходим к написанию обработчика прерывания. Здесь все просто: берем очередной байт из массива, скармливаем его ЦАПу, продвигаемся по массиву дальше. Однако прежде всего нужно очистить флаги прерываний таймера и перезапустить прерывание Alarm:
static uint32_t wav_pos = 0;static void IRAM_ATTR timer0_ISR(void *ptr){// Очистить флаги прерыванийtimer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);// Перезапустить прерывание Alarmtimer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);audio_info_t *audio = (audio_info_t *)ptr;if (wav_pos >= audio->dataLength) wav_pos = 0;dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos));wav_pos ++;}

Да, работа со встроенным ЦАПом в ESP32 сводится к вызову одной встроенной функции dac_output_voltage (на самом деле нет).

Собственно, все. Теперь нужно внутри функции app_main() разрешить работу нужного нам канала ЦАП и инициализировать таймер:
void app_main(void){            ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));    timerInit();

Собираем, прошиваем, слушаем :) В принципе, можно подключить динамик напрямую к ножке контроллера играть будет. Но лучше все же воспользоваться усилителем. Я использовал завалявшуюся у меня в закромах TDA7050.

На этом все. Да, когда у меня наконец запело, я тоже подумал, что все оказалось гораздо проще, чем я думал. Однако может быть, эта статья чем-нибудь поможет тем, кто только начал осваивать ESP32.

Может быть, когда-нибудь (и если эта недо-статья кому-нибудь понравится) я заведу ЦАП ESP32 с использованием DMA. Там все еще интереснее, потому что в этом случае работать придется со встроенным модулем I2S.
Подробнее..

Перевод А вы когда-нибудь причиняли себе физическую боль собственным кодом?

04.06.2021 10:04:57 | Автор: admin
Приходилось ли вам когда-нибудь ненароком причинить себе или другим физический вред из-за ошибок в коде? Мне да.

Примерно год назад я работал со сгенерированными файлами в формате WAV, их было несколько тысяч. Я пытался снабдить их метками, рассортировать по папкам, создавал метаданные. В процессе я прослушал несколько штук, и, к моей досаде, оказалось, что все они начинаются с довольно продолжительной тишины. Это сильно раздражало, особенно когда прослушиваешь серию файлов подряд и постоянно спотыкаешься о паузы перед воспроизведением каждого следующего. Прекрасно, значит, еще и с этим придется что-то делать.

Я уже потратил сколько-то времени на поиск решений для удаления тишины из файлов, когда меня вдруг осенило: это ведь WAV! Данные в файлах формата WAV обычно представляют собой PCM-аудио, то есть каждое значение в файле задает амплитуду звука в некоторый момент времени. Соответственно, если у нас там действительно полная тишина, а не белый шум, то в файле этой тишине должны соответствовать сплошные нули, так ведь?

$ xxd testfile1.wav | head -n 10000000000: 5249 4646 64b9 0e00 5741 5645 666d 7420  RIFFd...WAVEfmt 00000010: 1000 0000 0100 0200 44ac 0000 10b1 0200  ........D.......00000020: 0400 1000 6461 7461 40b9 0e00 0000 0000  ....data@.......00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................# ... and a lot more zeros below

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

Как читаются файлы WAV


Сначала мне нужно было поближе познакомиться с форматом WAV, чтобы понять, как работать с такими файлами и управлять данными внутри них. Я подобрал несколько источников; одним из самых полезных оказалась старая страница со stanford.edu (сайт сейчас уже недоступен, но, к счастью, сохранился на Wayback Machine). Там была очень доходчивая диаграмма:


Итак, структура файла WAV представляется довольно простой: сначала заголовок объемом в 44 байта, а дальше уже собственно данные. С этой информацией уже можно было приступать к коду. Требовалось только пропустить первые 44 байта, убрать последовательность из нулей в начале секции с данными, а всё остальное отправить на воспроизведение в исходном виде. Хотя не могу не добавить, что в другом источнике мне попались такие сведения:

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

Ну, я решил, что ничего страшного: программу я писал на C, так что за безопасность можно было особенно не переживать.

Код


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

Целиком код приводить незачем, но вот та часть, которая будет нас интересовать:

// index was calculated above to be the index of// the last consecutive zero byteFILE *f = fopen(argv[1], "rb");int ind = 0;int current_byte;while ((current_byte = fgetc(f)) != EOF) {    if (ind < 44 || ind >= index) {        fputc(current_byte, stdout);    }    ind += 1;}fclose(f);

Всё круто, всё просто. Пора тестировать. Я запустил программу на одном из файлов с особенно продолжительной паузой.

./strip_audio testfile1.wav > testfile1.nosilence.wav

Проверил, что выдаёт xxd для testfile1.nosilence.wav. Отлично, никаких нулей в начале. Значит, сработало. Чтобы окончательно убедиться, открою-ка я по-быстрому файл в аудиопроигрывателе.



Источник

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

Где я ошибся?


В ушах всё еще звенело, а я сидел и пытался осмыслить свои опрометчивые решения.

  • Ошибка 1: надо было убавить звук.
  • Ошибка 2: не надо было сидеть в наушниках.
  • Ошибка 3: неучтённая единица.

А вы заметили третью ошибку в коде, который я приводил выше? Подсказка: смотрите на комментарий. Я рассчитал переменную index как индекс последнего байта, представляющего собой нули. А значит, за вычетом 44 байтов заголовка, теперь мы воспроизводим только то, что следует за индексом или накладывается на него. index у нас стоит на последнем нуле в серии, то есть мы включаем один лишний нулевой байт в секцию с данными.

Это можно исправить следующим образом:

//     replaced >= with just >if (ind < 44 || ind > index) {    fputc(current_byte, stdout);}

Теперь в выдаче нет лишних нулей, и если воспроизвести файл, ничего страшного не случится. Я всё починил Но стоп.

В файлах WAV у нас PCM-аудио, а нули в аудиоданных такого рода соответствуют полной тишине. Так разве этот лишний байт не должен быть совершенно бесшумным? Почему он оказался таким громким и таким статическим?

Для начала давайте сравним нормальный аудиофайл с монстром, которого я создал, при помощи Audacity:


Угадали, где монстр? Да, это тот самый, у которого амплитуда стабильно вывернута чуть ли не максимум. Почему так?

Как читаются аудиосэмплы


Я вернулся к источникам, которые отобрал, и попытался разобраться, как ошибка на единицу могла привести к подобному взрыву амплитуды. Я знал, что в моих файлах сэмпл содержит 16 бит, а канала два (стерео), поэтому стал искать соответствующую информацию. Вот что говорилось в разделе про 16-битное стерео PCM-аудио:

Каждый сэмпл содержится в целом числе i, которое представляет минимально достаточное количество байтов для хранения заданного размера сэмпла. Наименее значимый из байтов располагается в хранилище первым.

Минимально достаточное количество байтов для хранения заданного размера формулировка здесь излишне запутанная. i соответствует числу битов, которые содержатся в сэмпле. В нашем случае их шестнадцать. Соответственно, если у нас есть некое значение длиной в 16 битов, само собой, храниться оно будет в двух байтах. А дальше важный момент: наименее значимый из байтов располагается в хранилище первым. Вот оно.

Взгляните на график, который я сделал, чтобы показать, что привело к возникновению такого сильного сигнала:


В верхней части показан мой файл-монстр, в котором я случайно оставил лишний байт с нулями. В каждом из трёх сэмплов s1, s2 и s3 по два байта, причем второй более значимый. Следовательно, при переводе этих пар байтов в десятичную форму мы получаем очень высокую амплитуду.

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

Выходит, если бы у меня было 8-битное аудио, тогда пропущенный лишний байт не вызвал бы никаких проблем. Но оно было 16-битным, и в итоге, я сдвинул всю последовательность в сэмплах, так что наименее значимый байт стал читаться как наиболее значимый.

Выводы


  • Проверяйте звуковую волну аудиофайла, прежде чем врубать его на максимальной громкости
  • Читайте (и принимайте к сведенью) спецификации
  • Учитывайте, как код может повлиять на вас и других людей
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru