2018年3月19日月曜日

ESP32でI2Sから音を出す・その2

その1の方法だと再生が終わるまで帰ってきてくれないので、音が出ている最中に止めたり他の音に変更することができません。そのヘン改良したのがこちらです。

なおこちらはKickstarter製品で使ったコードの一部です。

setupではI2S関連の初期化と音を出力するためのタスクsound_loopを準備しています。

loopでは割り込み要素として加速度計を使っていて、3軸合成加速度が平均値(ほぼ重力)を一定以上越えたら音を出力するフラグsoundOnを立てます。

sound_loopではループをぐるぐる回って、大きな加速度を検知してsoundOnがonになったら音源番号iSoundを変更して再生位置iを初期化し、内側のループでi2s_push_sampleを使って1音ずつバッファにプッシュし、音を出力します。再生が終わるか、音源番号が変わったらループを抜けます。

内側のループにはウエイトを入れていないですが、DMAバッファがいっぱいになるとi2s_push_sampleで止まるので、大丈夫っぽいです(ポイかよ)。

まぁこんな感じで…。


//
// Sound
//
#include "driver/i2s.h"
#include "driver/gpio.h"
#include "ching.h"
#include "clap.h"
#include "cymbal.h"
#include "voice.h"
const i2s_port_t I2S_NUM = (i2s_port_t)0;
const i2s_bits_per_sample_t BITS_PER_SAMPLE = (i2s_bits_per_sample_t)16;
#define ADC_MODE 1
// I2S
void set_16k_16() {
i2s_config_t i2s_config;
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); // Only TX
i2s_config.sample_rate = 16000;
i2s_config.bits_per_sample = BITS_PER_SAMPLE;
i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; //right channels
i2s_config.communication_format = I2S_COMM_FORMAT_PCM;
i2s_config.dma_buf_count = 4;
i2s_config.dma_buf_len = 1024;
i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; //Interrupt level 1
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
}
//
// 加速度
//
#include <Wire.h> // Must include Wire library for I2C
#include <SparkFun_MMA8452Q.h> // Includes the SFE_MMA8452Q library
MMA8452Q accel(0x1C);
volatile int soundOn = false;
volatile int iSound = 0;
volatile int prevSound = -1;
void sound_loop(void *param) {
char *buf = NULL;
int length;
while (1) {
if (soundOn == true) {
switch (iSound) {
case 0: buf = (char*)ching; length = sizeof(ching); break;
case 1: buf = (char*)clap; length = sizeof(clap); break;
case 2: buf = (char*)cymbal; length = sizeof(cymbal); break;
case 3: buf = (char*)voice; length = sizeof(voice); break;
default: buf = NULL; iSound = 0; break;
}
soundOn = false;
prevSound = iSound;
int i = 0;
while (i < length) {
int ret = i2s_push_sample(I2S_NUM, &buf[i], portMAX_DELAY);
if (ret <= 0) {
Serial.print("return = ");
Serial.print(ret);
delay(1);
iSound = -1;
break;
} else {
i += ret;
}
if (prevSound != iSound || soundOn == true) {
break;
}
}
i2s_zero_dma_buffer(I2S_NUM);
} else {
delay(1);
}
}
}
TaskHandle_t task;
void setup()
{
Serial.begin(115200);
Serial.println("MMA8452Q Test Code!");
pinMode(13, OUTPUT);
accel.init();
static i2s_pin_config_t pin_config;
pin_config.bck_io_num = 26;
pin_config.ws_io_num = 25;
pin_config.data_out_num = 27;
pin_config.data_in_num = -1; //Not used
set_16k_16();
i2s_set_pin(I2S_NUM, &pin_config);
xTaskCreatePinnedToCore(
sound_loop, // Handler
"sound_loop", // Task name
4096, // Stack size
NULL, // Parameter
1, // Priority : 25 is the most high
&task, // task handler
1); // Application CPU
Serial.println("setup end");
}
float ave = -1;
void loop()
{
if (accel.available())
{
// First, use accel.read() to read the new variables:
accel.read();
float gravity = sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z);
if (ave == -1) {
ave = gravity;
} else {
if (abs(gravity - ave) > 1000) {
digitalWrite(13, HIGH);
soundOn = true;
delay(25);
digitalWrite(13, LOW);
} else {
digitalWrite(13, LOW);
}
ave = (ave * 9.0 + gravity) / 10.0;
}
Serial.print("gravity=");
Serial.print(gravity, 3);
Serial.print(", ave=");
Serial.print(ave, 3);
Serial.print(", abs=");
Serial.print(abs(gravity - ave));
Serial.print(" ");
printCalculatedAccels();
Serial.println(); // Print new line every time.
delay(1);
} else {
Serial.println("not available");
delay(1000);
}
}
void printCalculatedAccels()
{
Serial.print(accel.cx, 3);
Serial.print("\t");
Serial.print(accel.cy, 3);
Serial.print("\t");
Serial.print(accel.cz, 3);
Serial.print("\t");
}

3 件のコメント:

  1. いつも興味深く拝見しています。
    ESP32で内蔵DAC+i2sで音を出そうと四苦八苦しているのですが、
    出てくるのは雑音ばかりです。
    「データは符号付き16ビット整数をみっちり詰め込んで」の
    意味が分からず先に進めないでいます。
    例えば上記のコード中の、ching.hなどは、
    signed 16bit変換したraw pcmがC言語配列初期化の形式で
    書かれていると推察されますが、このファイル
    どこかで公開していただけないでしょうか・・・

    返信削除
    返信
    1. ご拝読ありがとうございます。

      ching.hは音楽家さんが作成したデータが元なので公開は難しいですが、先頭はこんな感じです。

      const uint16_t ching[] = {
      // file length = 60044
      // id=RIFFÑÍ, file size=60036, wave=WAVEfmt , chunkSize=16, formatId=1,
      // ch=1, samples per sec=16000, bytes per sec=32000, bits per sample=16
      // sizeofformat=36, buf=data`Í, id=data`Í, data size=60000
      // lines=1875
      0x02d2,0xf6f9,0x09f8,0x02a2,0xfab6,0xfa03,0x0787,0x034e,0xf6f4,0x16d4,0xf5a0,0xe372,0x0e55,0xfcf5,0xfb2f,0x2224,
      0xfc30,0xee70,0x0df2,0xfe7e,0xde21,0x0c9e,0x158a,0xe082,0x100c,0x17dc,0xe705,0x0c68,0x0be5,0xde63,0x0b46,0x0d16,

      なお、上のソース、宣言は符号なしですが、中に入っているデータは符号付き16ビット整数です。最初音が出るまで苦労しました。とりあえず最初はsin関数などで正弦波を作って鳴らしてみると検証しやすいです。いきなり音を出すと回路的なノイズなのかデータが間違っているのか判断しにくいですが、正弦波なら耳で聞いたりオシロで波形を見れば何が問題なのかわかりやすいので。計算で作ったデータで正しい音が出るようになったら、raw pcmの方でも正弦波か正弦波っぽい音を録音して変換して試すと、切り分けが容易になるかと思います。

      雑音になってしまう理由として、他にはraw pcmのデータがLRLRLR…と並んでいるのに片チャンネルだけ出力しているケースもありました。

      // I2S
      void set_16k_16() {
      i2s_config_t i2s_config;
      i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); // Only TX
      i2s_config.sample_rate = 16000;
      i2s_config.bits_per_sample = BITS_PER_SAMPLE;
      i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; //right channels
      i2s_config.communication_format = I2S_COMM_FORMAT_PCM;
      i2s_config.dma_buf_count = 4;
      i2s_config.dma_buf_len = 1024;
      i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; //Interrupt level 1

      i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
      }

      ここのchannnel_formatを変えてみてください。

      削除
    2. sinのデータ、残っていたのでgistにあげてみました。

      ご覧のようにLRのデータです。たしか右と左で周波数の違う正弦波が入っていたと思います。こっちのデータはint16_tですが、中に入っているデータが同じなら関係ないです。元データはwavで単に読んだデータをそのままhexで変換して書き出しているだけです。


      https://gist.github.com/TareObjects/7ca5a9ee903ddb6c3e6eb504c8b9f1bd

      削除

注: コメントを投稿できるのは、このブログのメンバーだけです。