ESP32-S3 で I2S Audio CODEC を動かす

備忘録代わりの投稿.
ESP32-S3-DevKitC-1 を入手したので,I2S接続の Audio CODEC つなげて動かしてみた.
開発環境はArduino IDE.ESP32のボードマネージャはバージョン2.0.5をインストールした.
(2.0.3以降でS3に対応しているらしい)

CODECには PCM3060PW を使った.
まずESP32用のI2Sのスケッチ例があったので試してみたがまともに動作せず.これらは libraries フォルダにある I2S.h, I2S.cpp  使っていたのでその中身を見てみると,サンプリング周波数が16kHz以下かつ16bitで使うようにコメントされていて無駄な時間を費やしてしまった.
結局 driver フォルダの i2s.h を使い,下記のコードでうまく動作した.

// I2S on ESP32-S3
// T.Uebo  October 1 , 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 0
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <driver/i2s.h>

#define fs 48000
#define BLOCK_SAMPLES 128

#define Reset_CODEC 40 // HW Reset signal for CODEC 

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];


void setup(void) {
  //HW Reset for CODEC
  pinMode(Reset_CODEC, OUTPUT);
  digitalWrite(Reset_CODEC, HIGH);
  delay(1);
  digitalWrite(Reset_CODEC, LOW);
  delay(1);
  digitalWrite(Reset_CODEC, HIGH);

  // setup I2S 
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fs,
    .bits_per_sample = (i2s_bits_per_sample_t)32,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);
  i2s_zero_dma_buffer(I2S_NUM_0);
}


void loop(void) {
  size_t readsize = 0; 

  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {
    
    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }    
    
    //-------Signal process -------------------------------
    for (int i=0; i<BLOCK_SAMPLES; i++) {
    
      Lch_out[i] = Lch_in[i];
      Rch_out[i] = Rch_in[i];
            
    }
    //------------------------------------------------------


    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j] = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);    	  	    
  }

}

Freeverb (Teensy Audio ライブラリ)

Teensy 4 でリバーブの処理をさせようと思い,ライブラリにあるFreeverb を試してみたところ,FIRフィルタと同様信号レベルが低下してくるとノイズが生じてくる(残響音の減衰の最後の方は特に顕著).やはりこれも16bit整数処理の影響かと思われるので,32bitフロートで書き直すことにする.

書き直すにあたってまず,ライブラリのソースを調べて具体的な処理内容を確認した.

Freeverbの中身はSchroederのアルゴリズムがベースになっていた.ただし,よく見かける 4個のComb filterと2個のAllpass filter によるものではなく,下図のようにそれぞれ2倍の 8個と4個になっていて,”High quality Reverb effect”との表記があった.

ただし,この処理ブロックは,残響のテイルの部分を生成するものなので,空間の残響をそれなりにシミュレートするには,Early reflection の生成や Pre delay の処理を追加する必要がある.

Comb filter は以下のようになっていた.

図のようにダンピング処理のための1次LPFを持っているので自然なリバーブが得られそう.
(このフィルタをBPFとかにすれば,Shimmer Reverb になるのかも?)

Allpass filter は以下のとおり.

ライブラリソースでは a=0.5 に固定されているようだが,その場合 1-a^2 = 0.75 でなければならない.コードをみるとこれ(1-a^2)も 0.5 になっているように解釈できた(自分の読み間違いかもしれないが).単なるバグなのか何か意図があってそうしているのかわからないがもしそうなら特性はAllpass にはなっていない.とはいえ残響音の密度を上げるという観点からは聴感上大きな違いが出るようにも思えないのであまり気にしないでもいいか… 書き直しの際はセオリー通り 1-a^2=0.75 にする.

Teensy Audio ライブラリ

Teensy 4 でオーディオ信号処理を行うにはAudioライブラリを用いるのが簡便でよいが,以下のような制限がある.

(1) 処理が16bit整数のみ
(2) FIRフィルタのタップ数が200まで
(3) SDRで必要となる処理が用意されていない(直交・極変換,微分,平方根など)
など.

まず,2048タップのFIRフィルタを作ろうとして (2) の制限に引っかかった.
そこで,タップ数の少ないFIRフィルタを複数用いそれらを適切な Delay を通して駆動し,
出力をMIXする方法を試してみた(下図).

128タップのFIRフィルタを16個並べて2048タップのFIRフィルタを構成している.これはうまく動作していて問題は無いように思えたが,動作確認を続けていくうちに,信号レベルが低下してくるとバックグラウンドノイズではない別のノイズが生成されてくることに気づいた(信号レベルが大きいときは気にならない).これは以前に16bit整数処理のIIRフィルタ(8次)の動作確認したときと同様の現象で,このFIRフィルタもおそらくアンダーフローを起こしていると思われた(IIRのときよりは顕著でないが).

となるとAudioライブラリは16bit整数処理のみなので,このライブラリ使う限り改善は望めそうにない.そこで Audioライブラリでは I2S と queue のみを使用し,信号処理はコードで書くことにする(下図).

queue_send から信号をコードに取り込み,処理したあと queue_ret に信号を返す.
信号処理には CMSIS DSP Software Library で用意されている関数を用いる.信号処理関数が豊富に,かつ float で処理する関数も用意されているのでそれらを使えば上述のノイズの問題は解消できるはず.

ということで,以下のようなコード作成して動作確認をしてみた.
(なお,コードを見やすくするために実際に動作確認できた状態から抜粋して示しています.このままでおそらく動作するとは思いますが保証はしません.間違いのご指摘は歓迎いたしますが「動作しない」というクレームはご遠慮願います.)

//#include <arm_math.h>
#include <Audio.h>

AudioInputI2S            i2s_IN;
AudioMixer4              mixer_IN;
AudioRecordQueue         queue_send;
AudioPlayQueue           queue_ret;
AudioOutputI2S           i2s_OUT;
AudioConnection          patchCord1(i2s_IN, 0, mixer_IN, 0);
AudioConnection          patchCord2(i2s_IN, 1, mixer_IN, 1);
AudioConnection          patchCord3(mixer_IN, queue_send);
AudioConnection          patchCord4(queue_ret, 0, i2s_OUT, 0);
AudioConnection          patchCord5(queue_ret, 0, i2s_OUT, 1);

#define NtapFIR 2048
float32_t fircoeffs[ NtapFIR ];
float32_t firState[ NtapFIR + 2*AUDIO_BLOCK_SAMPLES - 1 ];

arm_fir_instance_f32 firEQ;

float32_t sigf_IN [AUDIO_BLOCK_SAMPLES];
float32_t sigf_OUT[AUDIO_BLOCK_SAMPLES];
int16_t sig16[AUDIO_BLOCK_SAMPLES];

#include <FlexiTimer2.h>


//Signal process --------------------
void sig_proc()
{
  int i;
  int16_t *p_sig;  
  if(queue_send.available() != 0){

    //INPUT
    p_sig = queue_send.readBuffer();
    for(i=0; i<AUDIO_BLOCK_SAMPLES; i++) 
              sigf_IN[i]=(float32_t)p_sig[i];
    
    //FIR
    arm_fir_f32(
          &firEQ,
          sigf_IN, 
          sigf_OUT, 
          AUDIO_BLOCK_SAMPLES
    );

    //OUTPUT
    for(i=0; i<AUDIO_BLOCK_SAMPLES; i++) 
              sig16[i]=(int16_t)(sigf_OUT[i]);
    queue_ret.play(sig16, AUDIO_BLOCK_SAMPLES);
    queue_send.freeBuffer();
  }
 
}

//--------------------------------------
void setup() {
  
  arm_fir_init_f32(
    &firEQ, 
    NtapFIR, 
    fircoeffs, 
    firState, 
    AUDIO_BLOCK_SAMPLES
  );

  //Set freq. response to flat   (Impulse)
  for(int i=0; i<NtapFIR; i++) fircoeffs[i] = 0;
  fircoeffs[1024] = 1.0f;
  
  //Reset CODEC
  pinMode(22, OUTPUT);
  digitalWrite(22, LOW);
  delay(100);
  digitalWrite(22, HIGH);
  
  AudioMemory(100);
  mixer_IN.gain(0, 1.0f);
  mixer_IN.gain(1, 1.0f);
  mixer_IN.gain(2, 0.0f);
  mixer_IN.gain(3, 0.0f);


  // call "sig_proc" every 2ms
  FlexiTimer2::set(2, 1.0/1000, sig_proc);
  FlexiTimer2::start();
 
  queue_send.begin();  
}

//------------------------------------------
void loop() {
      //Set fircoeffs[] 
}

動作確認の結果 32bit float 処理にすることでノイズが消えた.やはり16bit整数処理が問題だったようだ.
AudioライブラリはGUIでシステムを構築できるので取っつきやすいが,結局コードで書いた方がfloat 処理ができるしFIRフィルタの処理も1行で済むので楽(設定はいろいろ必要だがそれはAudioライブラリも同じ).
あとは I2S のブロックが16bitでなく32bitでインターフェースできれば,CODECのダイナミックレンジを十分に活かせるのだが…
とはいえ内部処理は 32bit float にできたので微分処理なども少し安心してできると思う.

ヤマハPortaSound PS3修理

3か所,音の出ない鍵盤があるとのことで修理を依頼された.
PS3は最も初期のポータサウンドの一つらしい.

20180602_112810

 

とりあえず開腹して,一枚目の基板を外した.
YAMAHAのオリジナルICを使用している.
年代を感じさせる作りで,ピッチの基準はLC発振のようだ.

20180602_094518

 

鍵盤下の基板を外したところ.

20180602_100253
音の出ない鍵盤に対応する接点をショートさせてみても,やはり音が出ない.
ということで接点不良ではなさそう.

 

該当する接点につながっているパターンをたどっていくと,電池の液漏れによるパターンの腐食があった.
調べてみると導通がない.音の出ない鍵盤に対応する接点はすべてこのラインにつながっているので,故障の原因はこれだろう.

20180602_102749

 

 

ということで,バイパス.
隣りのパターンも怪しかったので処置しておいた.

20180602_105935

 

これで音も出るようになり,修理完了.

ヘッドマイク

最近は中国製の安いヘッドマイクが購入できる.これらはほぼエレクトレットコンデンサマイクでバイアス電源が要る.
一方ミキサのXLRマイク入力には,大抵ファンタム電源が供給できるようになっているが,これはコンデンサマイク用でエレクトレットコンデンサマイクのバイアスには使えない.
また,エレクトレットコンデンサマイクの出力は不平衡なので,そのまま長く引き回すとノイズがのる.
そのようなわけで,ヘッドマイクをミキサのマイク入力(XLRコネクタ)につなぐためのアダプタケーブルを製作した.

20180508_230606~2
ケースの中身は,ファンタム電源からエレクトレットコンデンサマイク用のバイアスを作る分圧回路と不平衡を平衡に変換する差動アンプである.
試作ということで,ケースを3Dプリンタで作り空中配線で仕上げたが,結構面倒でユニバーサル基板を使えばよかったかもしれない.

20180508_232430~2

ケースにはベルトなどに引っ掛けるようクリップをつけた.
こういうのは3Dプリンタのおかげで簡単にできるのでありがたい.

音響ホーン

以前は40KHzのトランスデューサ(写真左)を使ってバットディテクターを製作していたが,
40KHz付近しか観測できない,という制約があった.
今回のバットディテクターでは,
広帯域で超音波を観測するためにMEMSマイク(写真右)を使っている.
ただ,トランスデューサは40KHz付近の感度は高く,
40KHzに限った感度の比較では,MEMSマイクは今一つな感じがする.

20180311_133045~2

写真を見ればわかるように,
MEMSマイクの音響ホールは直径0.5mmしかない.
トランスデューサの方は,内部の振動板らしきものは,直径8mmほどある.
開口面積がMEMSマイクの方が格段に小さいため,
感度が悪いのも無理はない.

そこで,音響ホーンを用いて開口面積を大きくし,感度の向上を試みた.

20180311_131230~2

3Dプリンタで作成した超音波用の音響ホーン.

定量的なデータはとっていないが,聞き比べた感じでは,
トランスデューサと同等以上の感度が得られているようだ.