Запись звука

AVR
На AVR'ках можно неплохо работать со звуком. Сделать, например, какой-нибудь диктофончик или плеер.

В этом посте — только про захват звука. Впрочем, если кому-нибудь окажется интересно, можно написать ещё)

Для записи звука удобно юзать электретный микрофон. Это вот такая шняжка.

микрофончик

Такие микрофоны попадаются в куче китайских девайсов, в наушниках с микрофоном, etc.

Электретный микрофон — активная деталька (внутри стоит полевой транзистор), поэтому для него требуется питание. Одна ножка микрофона (правая на картинке) соединина с корпусом микрофона, её садим на землю. На другую ножку подаём питание через резистор на 1к, с неё же снимаем сигнал.

Также попадаются вот такие совковые электретные микрофоны.

советские

В них резистор встроен и наружу выходят 3 проводка. Их назначение определяется по цвету. Красный (или коричневый) — земля, синий (чёрный, зелёный) — плюс питания, белый (жёлтый, оранжевый) — выход.

Сигнал с микрофона нужно усилить. Вот, например, самый простой усилитель на операционнике.

усилитель

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

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

Выход операционного усилителя подцепляем к микроконтроллеру. Камрад kvm подсказывает, что между выходом усилителя и входом АЦП есть смысл поставить ФНЧ.

фильтр НЧ

Для экспериментов я взял свою, с позволения сказать, демоплату с Мегой32 и SD-карточкой. Вот так выглядит этот колхоз.

демоплатка

Примерная схема того, что получилось.

подключение

Фильтр на цепи AVCC на платке у меня не разведён, поэтому помехи от карточки при записи с радостью лезли на вход усилителя через резистор питания микрофона. Пришлось собрать фильтр на весу. Помехи карточка создаёт с удовольствием, так что, в принципе, можно и на неё тоже поставить фильтрик.

Перейдём к коду. Для захвата сигнала нам понадобиться ADC.

Про работу с ADC, кстати, можно почитать у Ди.

Инициализация. Для генерации правильной частоты сэмплов, заюзаем Таймер0 и привяжем триггер ADC к нему.

void capture_start(uint16_t freq)
{
    // Настраиваем ADC.
    // Выбираем канал ADC0, 16-битный сэмпл, частота = Fclk/8
    //    разрешаем срабатывание по прерыванию сравнения Таймера0,
    //    разрешаем прерывание ADC
    ADMUX = 1<<ADLAR; // 10 -> 16 bit, channel ADC0
    SFIOR |= (1<<ADTS1) | (1<<ADTS0); // trigger = OCI0
    ADCSRA = (1<<ADEN) | (1<<ADATE) | (1<<ADIE) | (1<<ADPS1) | (1<<ADPS0);

    // Настраиваем Таймер0.
    // Тактовая частота = Fclk/8, режим = CTC (частота = freq)
    //    разрешаем прерывание по сравнению
    OCR0 = F_CPU/8UL/freq;
    TIMSK |= 1<<OCIE0;
    TCCR0 = (1<<WGM01) | (1<<CS01);
}


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

// буфер для запихивания сконвертированных сэмплов
#define CAPTURE_BUFLEN  200 // 200 сэмплов = ~18 мс.

uint8_t capture_wp, capture_rp;
uint8_t capture_buf[CAPTURE_BUFLEN];

// Прерывание от таймера обрабатывать не будем
//    (ацп запускается от него автоматически),
//    но нужно сбрасывать флаг OCIF0,
//    иначе ADC запустится только 1 раз
//    EMPTY_INTERRUPT(); отличается от ISR() {} тем,
//    что не генерируется никакого лишнего кода,
//    только RETI
EMPTY_INTERRUPT(TIMER0_COMP_vect);

// Прерывание от ADC.
ISR(ADC_vect)
{
    uint8_t wp_next;
    
    // Смотрим, есть ли свободное место в буфере
    //    если нету - выходим
    // При отладке желательно ловить событие переполнения буфера,
    //    т.к. это плохо. Можно, например, зажигать светодиодик.
    wp_next = capture_wp + 1;
    if(wp_next == CAPTURE_BUFLEN)
        wp_next = 0;
    
    if(wp_next == capture_rp)
        return;
    
    // Сохраняем сэмпл в буфер
    capture_buf[capture_wp] = ADCH;
    capture_wp = wp_next;
}


От считанного сэмпла сохраняем мы только старшие 8 бит. Просто ради экономии памяти.

Доставать сэмплы из буфера можно такой штукой.

int capture_read()
{
    uint8_t sample;
    uint8_t rp = capture_rp;

    // Проверяем, есть ли в буфере что-нибудь
    if(rp == capture_wp)
        return -1;

    // Берём сэмпл из буфера
    sample = capture_buf[rp];

    // Сдвигаем указатель чтения
    if(++rp == CAPTURE_BUFLEN)
        rp = 0;
    capture_rp = rp;

    return sample;
}


Данные будем записывать в wav-файл. Формат такого файла разработан M$ и называется RIFF (resource interchange file format). В начале файла записывается сигнатура 'RIFF' (4 байта), полный размер файла (4 байта) и тип файла (4 байта) — 'WAVE', 'AVI ', etc.

После заголовка следуют чанки. Каждая чанка (чанк, блок, etc.) состоит из сигнатуры (4 байта), размера поля данных (4 байта) и, собственно, данных.

Wav-файл должен содержать как минимум 2 чанки — формат потока и сам поток. Формат потока записывается в виде структуры WAVEFORMATEX, про которую можно почитать в MSDN. Получается вот такая вещь.

typedef struct wave_header {

    // RIFF header
    uint32_t riffsig;    // 'RIFF'
    uint32_t filesize;    // общий размер файла
    uint32_t wavesig;    // 'WAVE'

    // format chunk
    uint32_t fmtsig;    // 'fmt '
    uint32_t fmtsize;    // 16
    uint16_t type;        // WAVE_FORMAT_PCM = 1
    uint16_t nch;        // число каналов, 1 или 2
    uint32_t freq;        // частота сэмплов
    uint32_t rate;        // датарейт
    uint16_t block;        // размер блока (размер сэмпла * число каналов)
    uint16_t bits;        // бит на сэмпл, 8 или 16

    // data chunk
    uint32_t datasig;    // 'data'
    uint32_t datasize;    // сколько данных
    uint8_t data[];        // собственно, поток

} wave_header_t;


8-битные сэмплы записываются в в wav-файле виде числа от 0 до 255 (как раз, как идут с ADC), 16-битные — в виде числа от -32768 до 32767. Так что если захотим записывать 16-битные сэмплы, нужно будет отнимать от них 32768, но нам хватит и 8-битных сэмплов)

Теперь можно написать код для записи wav-файла.

//Разные переменные:

// текужее состояние записи
// (остановлено, запущено,
//    запрос остановки, запрос запуска)
typedef enum recorder_status {
    REC_STARTING,
    REC_STARTED,
    REC_STOPPING,
    REC_STOPPED
} recorder_status_t;

recorder_status_t rec_stat;

// сколько свободно места на флешке
uint32_t diskfree;

// сколько байт аудиоданных записано на флешку
uint32_t bytes_recorded;

// заголовок wav-файла
//    (записывается после остановки записи,
//        т.к в нём нужно указывается размер 
//        файла и сколкьо записано данных)
wave_header_t wave_hdr;

// дескриптор файла
FIL rec_f;

// буфер для записи в файл
uint8_t rec_buf_bytes;
uint8_t rec_buf[64];

// Вызываестся периодически.
void record()
{
    FATFS *ff;
    uint16_t temp;
    int sample;

    // Команда - запуск записи?
    if(rec_stat == REC_STARTING) {

        // Смотрим сколько места свободно на диске,
        //    если меньше 256 КБ, ничего не записываем.
        diskfree = 0;
        if(f_getfree("0:", &diskfree, &ff) == FR_OK)
            diskfree *= ff->csize * 512;

        if(diskfree < 256UL * 1024) {
            rec_stat = REC_STOPPED;
            return;
        }

        // Создаём файл.
        if(f_open(&rec_f, "sound.wav", FA_WRITE|FA_CREATE_ALWAYS) != FR_OK) {
            rec_stat = REC_STOPPED;
            return;
        }


        // Заполняем WAV-заголовок. 
        //    (пока данных нету)

        // RIFF заголовок
        wave_hdr.riffsig        = 0x46464952;       // сигнатура ('RIFF')
        wave_hdr.filesize        = sizeof(wave_hdr); // размер файла
        wave_hdr.wavesig        = 0x45564157;       // тип фалйа ('WAVE')

        // чанка формата
        // (структура WAVEFORMAT)
        wave_hdr.fmtsig            = 0x20746D66;       // сигнатура ('fmt ')
        wave_hdr.fmtsize        = 16;               // размер структуры
        wave_hdr.type            = 1;                // WAVE_FORMAT_PCM
        wave_hdr.nch            = 1;                // 1 канал = моно
        wave_hdr.freq            = 11025;            // частота сэмплов
        wave_hdr.rate            = 11025;            // байт в секунду
        wave_hdr.block            = 1;                // 1 байт на сэмпл
        wave_hdr.bits            = 8;                // 8 бит на сэмпл

        // чанка данных
        // (собственно, записанные данные)
        wave_hdr.datasig        = 0x61746164;       // сигнатура ('data')
        wave_hdr.datasize        = 0;                // пока данных нет

        // пишем заголовок
        if(f_write(&rec_f, &wave_hdr, sizeof(wave_hdr), &temp) != FR_OK)
        {
            f_close(&rec_f);
            rec_stat = REC_STOPPED;
            return;
        }

        // начинаем записывать
        capture_start(wave_hdr.freq);

        // пока буфер пуст
        rec_buf_bytes = 0;
        
        // пока ничего не записано
        bytes_recorded = 0;

        rec_stat = REC_STARTED;
    }

    // Ведётся запись?
    if(rec_stat == REC_STARTED)
    {
        // Осталось меньше 100 КБ на флешке - останавливаем запись.
        if(diskfree < 100UL * 1024) {
            rec_stat = REC_STOPPING;
        }

        // Складываем в буфер то, что насчитывалось с ADC
        while( (rec_buf_bytes < sizeof(rec_buf)) && 
            ((sample = capture_read()) != -1) )
        {
            rec_buf[rec_buf_bytes++] = sample;
        }

        // Если буфер заполнен, сбрасываем его в файл
        if(rec_buf_bytes == sizeof(rec_buf)) {
            f_write(&rec_f, rec_buf, sizeof(rec_buf), &temp);
            bytes_recorded += temp;
            diskfree -= temp;
            rec_buf_bytes = 0;
        }
    }

    // Команда остановки записи?
    if(rec_stat == REC_STOPPING)
    {
        // Обновляем WAV-заголовок
        wave_hdr.filesize += bytes_recorded;
        wave_hdr.datasize = bytes_recorded;

        f_lseek(&rec_f, 0);
        f_write(&rec_f, &wave_hdr, sizeof(wave_hdr), &temp);
        
        // Закрываем файл
        f_close(&rec_f);

        rec_stat = REC_STOPPED;
    }
}

// Инициализация записи
void rec_init()
{
    rec_stat = REC_STOPPED;
    capture_init();
}

// Запуск записи
void rec_start()
{
    if(rec_stat == REC_STOPPED)
        rec_stat = REC_STARTING;
}

// Остановка записи
void rec_stop()
{
    if(rec_stat == REC_STARTED)
        rec_stat = REC_STOPPING;
}

// Запись запущена?
uint8_t rec_isstarted()
{
    return (rec_stat == REC_STARTED) ||
        (rec_stat == REC_STOPPING);
}

// Запись остановлена?
uint8_t rec_isstopped()
{
    return rec_stat == REC_STOPPED;
}


Вот так это можно использовать:

#include <avr/io.h>
#include <util/delay.h>
#include "recorder.h"
#include "mmc.h"

int main()
{
    uint8_t btn = 0;
    PORTB |= (1<<PB0); // кнопочка
    DDRB |= 1<<PB1;    // светодиодик

    // Инициализируем флешку,
    //    если не инциализируется - мигаем светодиодиком)
    while(!mmc_mount())
        PORTB ^= 1<<PB1;

    // Выключаем светодиодик
    PORTB |= 1<<PB1;

    // Инициализируем запись
    rec_init();
    sei();
    

    // Главный цикл
    for(;;)
    {
        // Если кнопочка нажата
        if(!(PINB & (1<<PB0)))
        {
            // И некоторое время до этого была отпущена
            //    (зашита от дребезга)
            if(!btn)
            {
                // запись запущена - останавливаем,
                //    остановлена  - запускаем
                if(rec_isstarted())
                    rec_stop();
                else if(rec_isstopped())
                    rec_start();
            }

            // Отмечаем, что была кнопка нажата
            btn = 255;
        }
        
        // Отсчитываем таймаут
        else if(btn) btn--;

        // Если запись запущена,
        //    включаем светодиодик
        if(rec_isstarted())
            PORTB &= ~(1<<PB1);
        else
            PORTB |= 1<<PB1;

        // Записываем данные
        record();
    }
    
    return 0;
}


Пока всё)

Файлы в топике: microphone.zip

Комментарии (36)

RSS свернуть / развернуть
Хотя ADC отдаёт нам 16-битные сэмплы
Вообще-то 10 бит. А с учетом шумов — все 8. :)
0
Ну с учётом сдвига будет 16)
0
… со звуком я не работял потому такой вопрос: а почему сигнал полученый на микрофоне инвертируется?
P.S. dcoder прав, в AVR максимальная разрядность ADC: 10бит, сдвигая их в лево разрядность не увеличится. А точность (некоторые путают точность с разрядностью) этих 10бит (по ДШ) еще и зависит от выбранной частоты работы АЦП. Минимальная погрешность АЦП равно 0.5*LSB = 0.5*(1/1023) ~ 0.5mV
0
Звуковой сигнал примерно симметричный, так что на инверсию просто забиваем. Если это принципиально, программно можно инвертировать обратно.
зы. Дык я и говорил про разрядность сэмпла а не про точность измерения. Если бит ADLAR у АЦП включен, с регистров ADC считываются 16-битные сэмплы.
0
Т.е. если алгоритм (скажем, IMA-сжатие или FFT) требует 16-битных сэмплов, а у нас есть тока 10 бит, мы просто сдвигаем сэмпл на 6 бит влево и получаем 16 бит. Здесь всего лишь такая фича аппаратно сделана.
0
… забыл добавить что погрешность 0.5мВ от каждого опорного вольта, тоесть при 5В опорного напряжения минимальная погрешность равна ~2.5мВ
0
Можно пример записанной этим диктофоном WAV'ки?
0
Это не диктофон, а просто пример. )
В архиве она есть
0
Красный (или коричневый) — земля, синий (чёрный, зелёный) — плюс питания, белый (жёлтый, оранжевый) — выход.
хе, я бы красный на плюс посадил, а чёрный на землю))
0
На самом-то деле у них синий — общий, а крансый — питание)
Тока питание отрицательное.
0
А не пробовал включить предварительный усилитель в ATMege32? У тебя усилитель с усилением 100, а в контроллере можно включить ADC0-ADC1 или ADC2-ADC3 в дифференциальном включении с усилением 200. Только при этом один из дифференциальных входов надо заземлить.
0
  • avatar
  • mzw
  • 29 мая 2011, 23:56
Даже не заземлить, а сделать искусственную среднюю точку, один из входов ADC подключить к ней напрямую, другой — через резистор, и к нему через конденсатор — микрофон.
0
0
Да можно, не спорю)
Но если девайс делается в единичном экземпляре и не стоит задача макисмально сэкономить, мне не влом и LM'ку поставить. К тому же её можно поставить максимально близко к микрофону.
0
Зачот :) Я нечто подобным на спектруме баловался. Приходилось внешнюю АЦП приколхозивать и качество оцифровки было куда хуже :(
0
Адовая макеточка, по ней-то я и понял, кто автор)
Особенно доставили SD слот и наклеечка)
0
  • avatar
  • Vga
  • 30 мая 2011, 05:40
Мне особенно понравилось, как карточка прижата согнутыми PLC-штырями =) Нормально придумано.
0
на картинке справа кажется МКЭ-3
или я путаю(очень похож)
0
Праваый мкэ-3)
А левый не помню, тож какой-то мкэ. С телефона выдрал.
0
Услитель для микрофона это хорошо! Но забыли, что перед оцифровкой необходимо обязательно отфильтровать аналоговый сигнал фильтром нижних астот- это позволит знаительно снизить искажения! Фильтр можно организовать на операционнике одновременно с усилителем.
0
  • avatar
  • kvm
  • 30 мая 2011, 11:09
Хмм, а ты прав. В аппноте у атмела тоже есть фильтр. Как приду домой, поправлю.
0
Да уж! В такой схеме не объединить усилитель и анти-аляйсинг фильтр (хотя-бы 2-го порядка!!!) — это несерьёзно! :-)
0
Плюс за интел атом :)
А диктофон с AVRкой и SD — да, идея в воздухе висит, только я так и не придумал, зачем он мне…
0
А можно скинуть исходники кода?
0
ты слеп, друг, или чудишь?
0
Нет, код программы я вижу, но вопрос у меня в следующем:
recorder.h что это за хидер
mmc.h — это c elm-chan.org?
0
Ну вот тебе. Всё ведь в архиве. FatFs от chan'а, остальное моё.
0
а можеш на pavelboshko@gmail.com кинуть архив, а то скачать щас не могу, спасибо
0
в мегу 48 влезет код?
0
и вообще прога полнофункциональна или как в статье набор кусочков :)?
0
Доброго времени суток.
В сборке устройств и программировании AVR я новичок и у меня возникло несколько вопросов:
1) не нахожу архив :-( о котором говорит уважаемый Lifelover
2) как убрать запись на SD, а вместо нее передать wav файл напрямую на SPI для передачи сигнала на ENC26J60 с последующим прослушиванием на компе по запросу.

Всем спасибо за дельные советы. Прошу сильно не материть за возможные глупые вопросы.
0
1) Аттач к статье — мелким шрифтом в футере.
2) Недостаточно данных.
0
Спасибо за ответ.
1) Все замечательно файл нашел и скачал.
2) Есть микрофон шорох на ВА3308 его нужно приаттачить к ATMEA328 и через модуль на базе ENC26J60 в режиме OnLine передавать по сети Ethernet на комп, передача должна осуществляться по запросу с компа с вводом имени пользователя и пароля дабы ограничить несанкционированный съем информации.

P.S. так же есть необходимость, не знаю в какой веке задать этот вопрос, собрать устройство которое будет включать в себя следующее: камера+AVR+SD или USB-flash card идея следующая ставится сей девайс в помещении подключается к нему камера и флешка подается питание и на флешку идет запись видео, приходишь через время изымаешь одну флешку ставишь другую, а на компе потом просматриваешь видео.
Насколько это реально?
0
Первое (микрофон) — реализуемо. Второе — зависит от того, какая камера, какой у нее интерфейс, в каком виде она данные выдает и в каком виде их надо писать. Но скорее всего ответ — нет. Нужен более производительный МК, имеющий интерфейс под камеру, а если необходимо перекодировывать видео перед записью на карту — то и вовсе нужен медиа-сок с аппаратно-ускоренным кодеком (другими словами — нужен полноценный одноплатник вроде Raspberry Pi).
0
Понятно.
по пункту №2 вопрос временно снят с повестки дня.
по пункту №1 как правильно отредактировать код (или где посмотреть инфу) чтобы вместо SD присоединить, с последующей передачей, Ehternet модуль (информацияс топика по ссыке we.easyelectronics.ru/electro-and-pc/podklyuchenie-mikrokontrollera-k-lokalnoy-seti.html сейчас изучается) интересует именно как модифицировать код из статьи «Запись звука» что бы была передача на Ethernet модуль, не понадобится ли использование внешней памяти?
0
По UDP — точно не понадобится. Пришли данные, пакет сформировал, пнул в сеть и забыл про него. На компе эти данные можно, например, ловить VLC плеером и писать в файл/перекодировать/делать с ними что нить еще.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.