Arduino Uno R4 Minimaで音声モノマネぬいぐるみを作る[1]仕様検討

# この記事について
話しかけるとオウム返しをしてくるぬいぐるみってありますよね。
あれをarduinoで作ってみたいと思います。
**まずは仕様検討とお試しだけです**
※注意：ぬいぐるみは作りません。買ってきます。

# 必要なもの
* arduino R4 Minima
  ※ATmega328だとスペック不足です
* マイクセンサ　(MAX4466)
　ゲイン調整機能が付いたモジュールを使用します。
* スピーカー　(100均の適当な奴)
　とりあえず確認するだけなら何でもよいです。イヤホンでもいい

# 仕様について
## 機能概要
音(人の声)を検知したら、３秒間音声の録音を行い、３秒後に録音した音声を再生します。


## タイミングチャート
ユーザーの発声をトリガーに録音を開始します。
ユーザーの発声が１秒間だったとしても3秒間録音をします。
その後すぐに３秒間の再生を行い、その期間は録音することができません。
```plantuml
@startuml
binary  "ユーザー音声"  as IN
binary  "スピーカー音声"  as OUT
robust  "状態" as S

@0
S is 待機

@100
IN is high
S is 録音中

@200
IN is low

@400
S is 再生中
OUT is high

@500
OUT is low

@700
S is 待機
@enduml
```

## 状態遷移図
「待機」「録音中」「再生中」の３状態を定義します。
録音から再生、再生から待機は時間経過によって遷移します。

```plantuml
@startuml

[*] --> 待機

待機 : 発声があるか監視
待機 --> 録音中 : 発声検知

録音中 : マイクからアナログ値を取得
録音中 : 音声データを格納する
録音中 -> 再生中 : 録音3秒経過

再生中 -up-> 待機 : 再生３秒経過
再生中 : 音声データを出力

@enduml
```

## 機能詳細
主に下記の機能が必要となります。
1. ユーザーの発声を検知する
2. MAX4466のセンサ値を音声データとして格納する
3. 音声データを再生する

### 1. ユーザーの発声を検知する(MAX4466)
 試しにマイクセンサの入力値をシリアルプロッタ出力してみると音声っぽい波形が取れました。
 単純に「値がいくつを越えたら」みたいな方法でもひとまず検知できそうです。
 無音状態と、人が発生した状態をそれぞれ定義して検知するのが良さそうですね。
 
 
![2024-11-26 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943443/9ef092e7-f14f-dfa6-cfc5-ea4ca179d169.png)




:::note info
シリアルプロッタ表示データ数のバージョンによる違い
arduino IDE 2だと50個までしかデータが表示されません。
古いバージョンだと500個表示できます。
この記事で使用しているバージョンは1.8.19
:::

マイク入力値をシリアルプロッタで出力するサンプルコードです。
```C:Max4466Test.ino

void setup() {
  pinMode(A3, INPUT);         
  analogReadResolution(14);   //デフォルトの分解能が10bitなので、14bitに変更する
  Serial.begin(115200);
}

void loop() {
    int micin;
    int Min=0;
    int Max=16383;
    
    micin = analogRead(A3);
    
    Serial.print(Min);
    Serial.print(",");
    Serial.print(Max);
    Serial.print(",");
    Serial.println(micin);

    delay(1);
}
```

### 2. MAX4466のセンサ値を音声データとして格納する

一般的な音楽のサンプリング周期は44.1kHzで、1秒間に44100個のデータを持っています。
1個当たりのデータサイズを16bit(2byte)とすると、1秒間で88200byteの容量です。
arduinno R4のRAM容量は32768byteなので、単純計算で**44.1kHZのサンプリング周期だと372ms**の音声しか取得できません。

```math
32768\div88200=0.372
```
逆に、３秒間のデータの保持には264kbyteのメモリが必要となります。


この容量足りない問題に対して、以下3つの解決策を挙げます。
**1. メモリを増設する**
SRAMやEEPROMを追加する。SDカードとかでもいいかも。
ただし、読み書きの速度はシリアル通信の速度に依存するので注意が必要

 https://www.switch-science.com/products/1072
 
**2\. サンプリング周波数を下げる**
例えば、44.1kHzのサンプリング周波数を10kHzにしたら、それだけで必要なデータ容量は1/4になります。
ただし、この場合取得できる音域は5kHzまでとなり、それ以上の「高い音」は取得できません。
↓このサイトで音を聞いてみたところ、12.5kHzまでは聞こえました。

https://www.gocha555.com/FreqSound/index.html

**3\. データを圧縮する(分解能を下げる)**
R4 MinimaのADCの分解能は14bitですが、これを8bitに圧縮する方法です。
8bitまで落とせば１byteで保持できるのでデータ容量を1/2にできます。
おそらく再生した時の音質が悪くなります。

**メモリ消費量と録音データについて**
一例を挙げます
| 番号| サンプリング周波数 | 録音時間 | 分解能 |メモリ消費量|
| -------| ------- | ------- | ------- | ------- |
| 1 | 44.1kHz| 3秒   | 16bit  |264kbyte   |
| 2 | 16kHz   | 1秒   | 16bit  |32kbyte   |
| 3 | 8kHz   | 1秒   | 16bit  |16kbyte   |
| 4 | 8kHz   | 2秒   | 8bit   |16kbyte   |
| 5 | 8kHz   | 1秒   | 8bit   |8kbyte   |

例えば、8kHzで1秒間、16bitの音声データをRAMに格納した場合、音声データの合計サイズは16kbyteです。


8kHzのサンプリング周期でRAMにセンサ値を格納した後、データを保持できているか確認するために、Serial.print()で16kByte分のRAMの中身を読み出しました。

サンプリングにはタイマー割り込みを使います。
こちらの記事を参考にして、**FspTimer**というライブラリをつかいました。

 https://qiita.com/yasuhiro-k/items/93efb640aa12f3db9086

8000個分のデータが保存されていることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943443/e831073b-6d04-c77b-7baf-52ffe63b9dd6.png)
実際のコードとしては、単純にグローバル変数で配列を宣言しています。

```C:Recording.ino

#define SAMPLING_FREQ 8000    //8kHz
#define RECORDINGTIME 1       //1秒
#define DATASIZE_MIC RECORDINGTIME*SAMPLING_FREQ
unsigned short MicInData[DATASIZE_MIC]; //音声データ(2byte×8000×1=16kByte)

```
ビルド結果を見てみると、
この時点で、RAMの使用率は61%(内50%が音声データ)になっていて、
ほかの処理を追加することを考えるとRAMでの保持は16kByteあたりが限界そうですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943443/b806ecb9-8e2c-d670-a241-0ebe6b360f51.png)

### 3. 音声データを再生する
マイクで取得したデータを電圧レベルで出力すれば良いです。
よくあるのはPWMを使って電圧を操作する方法ですが、R4にはDACが内蔵されているため、ピンから直接任意の電圧レベルを出力することができます。
下記のように分解能設定をすれば標準ライブラリのanalogWriteをPWMから内臓DACへの切り替えて使えるようです。

```C:
//setup()とかで設定をしておく
analogWriteResolution(12);  //スピーカー出力　内臓DACを使用するために12bitにする

//↓使うときはこう
analogWrite(DAC,Data);
```

以下の記事でATmega328で音声を再生する方法が紹介されています。
ATmega328にはDACが内蔵されていないので、analogWrite()はPWM出力になります。
しかも、周波数が490Hzに設定してあり、今回のサンプリング周期の8kHzよりも小さいので使えません。
そのため、タイマーカウンタのレジスタを直接設定して、分周なし(16MHz)のPWM動作をさせているようです。

https://nn-hokuson.hatenablog.com/entry/2017/09/01/092945

今回、この方のコードをR4向けに修正したものがこちらです。
※R4の場合、DACの出力ピンはA0です

```C:
const unsigned char sample_raw[] PROGMEM = {const unsigned char sample_raw[] PROGMEM = {
  0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x80,
・・・・・・
  0x7f, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
  0x80, 0x80, 0x7f
};

unsigned int sample_raw_len = 1200;

void setup() 
{
  //pinMode(3, OUTPUT);                             //ATmega328向けなので削除
  //TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); //ATmega328向けなので削除
  //TCCR2B = _BV(CS20);                             //ATmega328向けなので削除
  pinMode(DAC, OUTPUT);                             //R4向けに追加
  analogWriteResolution(12);                        //R4向けに追加

  play();
}

void play() {
  unsigned short data;
  for (int i = 0; i < sample_raw_len; i++) {
    //OCR2B = pgm_read_byte_near(&sample_raw[i]);   //ATmega328向けなので削除
    data = pgm_read_byte_near(&sample_raw[i])<<4;   //R4向けに追加
    analogWrite(DAC,data);                          //R4向けに追加
    delayMicroseconds(125);
  }
} 

void loop() {
}
```
https://nn-hokuson.hatenablog.com/entry/2017/09/01/092945


# まとめ
次回、下記のポイントに着目して設計実装を行っていきます。
+ 機能1:ユーザーの発声を検知する
  + **analogRead()** を14bitの分解能で使用する
  + マイクセンサ入力の変化を見て検知する
  + 無音状態と人が発声した状態を明確に定義する
+ 機能2:MAX4466のセンサ値を音声データとして格納する
  + **FspTimer** でサンプリング間隔のタイマ割り込みを作る
  + 外部メモリを追加して録音時間を増やす
  + サンプリングレートを落としてメモリ消費量を減らす
  + データサイズを落としてメモリ消費量を減らす
+ 機能3:音声データを再生する
  + **analogWrite()** を12bitの分解能で使用する
  + サンプリング間隔と同じ周期で電圧レベルを切り替える