Воспроизведение нескольких аудиофайлов на Arduino без плеера

В прошлой статье Воспроизведение аудиофайлов с помощью Ардуино без сторонних плееров и SD-карт я уже рассказывал, как воспроизводить небольшие аудиофайлы на Arduino без помощи сторонних плееров и без использования CD-карт. Там воспроизводился только один файл, записанный в созданной нами библиотеке sounddata.h. Но что, если в проекте требуется воспроизводить несколько небольших файлов, а тратиться на плеер и CD-карту все равно не хочется – модифицируем старый скетч. Тут придется повозиться с указателями и адресами.

Программу построим по следующему алгоритму.

Аудиофайлы лежат в библиотеке sounddata.h в виде массивов со значениями и переменных, хранящих их длину. Каждый файл будет иметь свое имя (я озвучиваю цифры, поэтому различаются названия номерами цифр в них, вы можете называть их как хотите, главное, чтобы не было одинаковых названий). Имена массивов должны отличатся от массивов, а имена переменных с их длинами от других переменных, и желательно что бы имена массива и соответствующей ему переменной перекликались, так потом будет удобнее добавлять и исключать аудиофайлы.

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

Для удобства создадим функцию play_trek с конструкцией swich-case в теле. На вход она будет принимать один параметр – номер воспроизводимого трека и в соответствии с ними присваивать data_it и sounddata_length необходимые значения и после вызывать функцию воспроизведения текущего аудиофайла.

Теперь немного подробнее о функции play_trek.

Объявление

void play_trek(byte num) 

где byte num будет хранить принятое значение. Отправляем значения num в конструкцию swich-case:

switch(num){ 

Она по сути является блоком if-ов, условиями которых являются случаи – case-ы. Например case 1: эквивалентно строчке if(num == 1).

Таким образом, если в одном из case-ов встретится пришедший в num номер (допустим это 1), то в sounddata_length:

sounddata_length = sounddata_length1; 

положится длина текущего аудиофайа, в data_it:

data_it = &data1[0]; 

положится адрес массива воспроизводимых значений, затем в ком порт выведется название пришедшей цифры на английском, после будет вызвана функция break, которая выйдет из конструкции swich-case, и тогда запустится воспроизведение текущего файла. Если же не один из case-ов не сработал, вызовется действие по умолчанию – default. В нем так, как мы не знаем, что нам пришло и как на это реагировать, не будем делать ничего и просто выйдем из функции через return. Поскольку swich-case работает с любым типом данных, вам не обязательно использовать цифры (они были более удобны для демонстрации) для выбора файлов, можно использовать символы типа char (только не забудьте при объявлении установить правильные типы данных).

void play_trek(byte num){  // функция, воспроизводящая файлы по пришедшему номеру
  switch(num){  // проверяем локальную переменную num 
    case 1:     // если она равна 1
      sounddata_length = sounddata_length1;  // длинна текущего массива равна длине массива первого файла
      data_it = &data1[0];     // адрес текущего массива равен адресу массива первого файла
      Serial.print("one");     // печатем в СОМ - порт название значения num на английском
      break;      // выходим из кострукции switch 
      // остальные случаи аналогично 
    case 2:
      sounddata_length = sounddata_length2;
      data_it = &data2[0];
      Serial.print("two");
      break;
    case 3:
      sounddata_length = sounddata_length3;
      data_it = &data3[0];
      Serial.print("three");
      break;
    default:  // случай по умолчанию, если ни один случай не сработал
      return;  // выходим из функции play_trek, ничего не воспроизводя так, как полученное число неизвестно
  }
  Serial.println();  // переводим строчку
  startPlayback(); // запускаем воспроизведение текущего аудиофайла 
}

Напишем простенькую программку, озвучивающую приходящие из COM-порта цифры. В void loop () помещаем строки чтения цифры и вызова воспроизведения.

Если в COM-порте есть доступные данные, то

If (Serial.available()) {  

COM-порт отдает нам символы, а не цифры, поэтому придется преобразовать символ в цифру. Для этого приводим пришедшее значение к типу int и отнимаем из него 48. В используемой COM-портом кодировке код «0» равен 48, «1» - 49, «2» - 50 и т.д. до «9» - 57. Полученное значение кладем в переменную inp (этот шаг на деле можно пропустить и сразу кидать приведенное значение в функцию).

inp = int (Serial.read()) - 48;

Теперь кидаем значение переменной inp (это номер трека, который нужно воспроизводить) в функцию.

play_trek (inp);
  }

Поскольку массивы, хранящие воспроизводимые треки имеют тип const unsigned char, а значит не могут быть изменены, мы можем расположить их в памяти, предназначенной для хранения программы. Положит их туда приставка PROGMEM в конце инициализации массива.

const unsigned char data1[5520] PROGMEM = {

Это выгоднее так, как «программной» памяти для наших нужд выделяется около 30 КБ, а оперативной только 2 КБ.

Ниже вы можете скопировать или скачать код для Ардуино:

#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include "sounddata.h"
#define SAMPLE_RATE 8000 // скорость воспроизведения (частота дискретизации, которую вы выбрали при конвертации файла)

int speakerPin = 11;  // пин на который подключен динамик
int sounddata_length;  // длина массива с данными для воспроизведения
volatile uint16_t sample;  
byte lastSample;
unsigned char *data_it;  // указатель на текущий массив данных
unsigned int inp = -1;   // переменная для ввода числа с клавиатуры компа. -1 для беззнакового типа является бесконечностью

void play_trek(byte num){  // функция, воспроизводящая файлы по пришедшему номеру
  switch(num){  // проверяем локальную переменную num 
    case 1:     // если она равна 1
      sounddata_length = sounddata_length1;  // длинна текущего массива равна длине массива первого файла
      data_it = &data1[0];     // адрес текущего массива равен адресу массива первого файла
      Serial.print("one");     // печатем в СОМ - порт название значения num на английском
      break;      // выходим из кострукции switch 
      // остальные случаи аналогично 
    case 2:
      sounddata_length = sounddata_length2;
      data_it = &data2[0];
      Serial.print("two");
      break;
    case 3:
      sounddata_length = sounddata_length3;
      data_it = &data3[0];
      Serial.print("three");
      break;
    default:  // случай по умолчанию, если ни один случай не сработал
      return;  // выходим из функции play_trek, ничего не воспроизводя так, как полученное число неизвестно
  }
  Serial.println();  // переводим строчку
  startPlayback(); // запускаем воспроизведение текущего аудиофайла 
}

  // действия при срабатывании прирывания по таймеру
  ISR(TIMER1_COMPA_vect) {
    if (sample >= sounddata_length) {
      if (sample == sounddata_length + lastSample) {
        stopPlayback();
      }
      else {
        // Рампа вниз до нуля, чтобы уменьшить щелчок в конце воспроизведения.
        OCR2A = sounddata_length + lastSample - sample;
      }
    }
    else {
      OCR2A = pgm_read_byte(&data_it[sample]);
    }
    ++sample;
  }

void startPlayback()
{
 
  // настраиваем второй таймер для работы с динамиком
  // Используем встроенные часы
  ASSR &= ~(_BV(EXCLK) | _BV(AS2));

  // Устанавливаем быстрый режим PWM
  TCCR2A |= _BV(WGM21) | _BV(WGM20);
  TCCR2B &= ~_BV(WGM22);

  // Неинвертируйте ШИМ на контакте OC2A
  // На Arduino этот вывод соответствует 11 пину.
  TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
  TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));

  //  Не используем предварительный делитель
  TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

  // считываем из массива данных частоту колебаний динамика
  OCR2A = pgm_read_byte(data_it[0]); 

  // Настраиваем Таймер 1, чтобы отправить образец каждого прерывания.

  cli();

  // Устанавливаем режим CTC (Очистить таймер на совпадении)
  // Необходимо установить OCR1A * после * ( *after*), иначе он будет сброшен на 0!
  TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
  TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));

  // Опять без предварительного делителя
  TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

  // Устанавливаем регистр сравнения (OCR1A).
  // OCR1A - это 16-разрядный регистр, поэтому мы должны сделать это с помощью
  // отключенных прерываний, чтобы быть в безопасности.
  OCR1A = F_CPU / SAMPLE_RATE;    // 16e6 / 8000 = 2000

  // Включаем прерывание, когда TCNT1 == OCR1A 
  TIMSK1 |= _BV(OCIE1A);

  lastSample = pgm_read_byte(data_it[sounddata_length - 1]); // change
  sample = 0;
  sei();
}

void stopPlayback()
{
  // Отключаем прерывание воспроизведения на выборку.
  TIMSK1 &= ~_BV(OCIE1A);

  // Полностью отключаем таймер за выборку.
  TCCR1B &= ~_BV(CS10);

  // Отключаем таймер PWM.
  TCCR2B &= ~_BV(CS10);

  // выключаем динамик
  digitalWrite(speakerPin, LOW);
}

void setup()
{
  Serial.begin(9600);  // настраиваем COM - порт на передачу данных со скоростью 9600
   // мне было удобнее назначить 10 пин на работу вкачестве земли (GND)
  pinMode(speakerPin, OUTPUT);  // пин для динамика на выход
  pinMode(10, OUTPUT);
  digitalWrite(10, LOW);
}

void loop()
{
  if(Serial.available()){  // если даннные COM - порта доступны
    inp = int(Serial.read()) - 48;  // считываем их и переводим в цифры
    play_trek(inp);  // запускаем соответсвующий трек
    //Serial.println(inp);  // выводим пришедшую цыфру (будет лишнее значение, не заню как его убирать)
  }
}

Если вам не будет хватать памяти на необходимое количество файлов, то при их конвертировании в WAV формат можно поставить частоту дискретизации 1000, а не 8000 и обязательно в программе в SAMPLE_RATE указать выбранную частоту. Звук значительно ухудшиться (для голоса опять же будет чувствоваться меньше), но памяти будет занимать в 8 раз меньше.

Ардуино+