M5Stackの限界に挑戦～高音質スピーカー再生

## 0. はじめに

M5Stack のスピーカーは低音質で有名です。私も M5Stack 入手直後、サンプルスケッチで Wave File をスピーカーで再生してみて、前評判通りの低音質にがっかりしました。そこで、この音質問題に取り組まれた先駆者様の知恵を参考にしつつ、高音質化の限界[^1]に挑戦しました。この記事では、そのプロセス、ソフトウェア実装例、改善効果をレポートします。


### 先駆者様

　文献1 : Tw_Mhage 様 [M5Stackのスピーカーの音質が悪い原因と対策](https://qiita.com/Tw_Mhage/items/34be8e8b42580c449b56)
　文献2 : N.Yamazaki 様 [M5Stackの音量を抵抗1つで調節する - N.Yamazaki's blog](http://blog-yama.a-quest.com/?eid=970193)
　文献3 : macsbug 様 [M5Stack speaker noise reduction](https://macsbug.wordpress.com/2019/09/27/m5stack-speaker-noise-reduction/)
　他



## 1. 概要

M5Stack のスピーカーの低音質原因は、[文献1](https://qiita.com/Tw_Mhage/items/34be8e8b42580c449b56)で以下のように分析されています。

>1.アンプのゲインが大きすぎて音が割れている
>2.自身のディジタルノイズが重畳されている
>3.音声出力の振幅の分解能が低い

[文献１](https://qiita.com/Tw_Mhage/items/34be8e8b42580c449b56) / [文献２](http://blog-yama.a-quest.com/?eid=970193)では、それぞれソフト/ハードによる「音割れ対策」、[文献３](https://macsbug.wordpress.com/2019/09/27/m5stack-speaker-noise-reduction/)では、ハードによる「ノイズ対策」が提案されています。

本記事は、ソフトによる 「ノイズ対策」「分解能対策」を提案し、高音質化を目指します。

![progress_overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/4f5a73ee-1f67-3fae-bfc6-45251ce6ff88.png)


## 2. 対策詳細
###  2.1 音割れ対策
[文献1](https://qiita.com/Tw_Mhage/items/34be8e8b42580c449b56) のおさらいです。スピーカー駆動アンプ (NS4150B) のゲインが約3倍[^2]であり、音割れ(過増幅歪)を起こします。DACからアンプへの入力振幅が 3V の場合、アンプは本来 3V × 3倍 ⁼ 9V を出力すべきですが、実際の出力はアンプの電源電圧範囲 3.3Vに制限されます。出力波形はピークが飽和してしまい、拡声器を通したような歪んだ音になります。

DACの出力振幅を、あらかじめ 1/3(=アンプゲインの逆数)に絞り、アンプの飽和を対策します。



#### 効果確認

詳細な原因・対策方法は、[文献1](https://qiita.com/Tw_Mhage/items/34be8e8b42580c449b56)で説明されています。ここでは追試にとどめます。

ソフトウェアで 8bit 振幅のノコギリ波を生成し、これを1倍または1/3倍にして DAC1 で出力します[^3]。DAC1 出力(青)とアンプ出力(赤)を、同時にオシロスコープで観測します。アンプ出力は、PWM波形であるため、外付けローパスフィルタ(LPF)でアナログ振幅に変換して観測します。

![Distortion_eva_topology.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/6ce7e61d-45f3-8fba-ab46-58a511d583c5.png)
![DAC_x1p00_x0p33_AMP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/43f2b9ab-7027-051e-de1d-799d5c1710d0.png)


**DACフルスケール出力(左)** ：ノコギリ波を DAC フルスケールいっぱい (255LSBp-p) で出力した結果です。アンプ出力(赤)は、DAC出力中央 1/3 は増幅動作が保たれています。その外側、DAC出力上側・下側1/3では飽和しています。

**DAC1/3出力(右)**：ノコギリ波を DAC フルスケールの1/3 (85LSBp-p) に絞って出力した結果です。アンプ出力は、全範囲で増幅動作が保たれ、飽和が対策できています。[^4]





### 2.2 DACノイズ対策

M5Stack の回路は、CPU・GPIO・DAC などの電源が蜜結合になっています。CPU・GPIOが動作すると、電源電圧にノイズが生じ、DAC 出力に伝搬してしまいます。結果的にスピーカー再生音にノイズが乗ります。

先の[音割れ対策](#21-音割れ対策)では、DACの出力振幅を1/3に絞りました。これをDACフルスケールの中央1/3で出力するのではなく、電源ノイズの影響が小さい下1/3で出力します。これにより、DACに現れる電源ノイズを1/3に減らせます。



#### 電源と DAC の関係

[M5Stack 回路図](https://github.com/m5stack/M5-Schematic/blob/master/Core/Basic/M5-Core-Schematic(20171206).pdf)と[ESP32 マニュアル](https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf) から類推した M5Stack の DAC と電源の関係図を示します。[^5]

![DAC_Power_Topology.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/60bd9748-3fdd-ba75-c9da-3f82baa027be.png)


ESP32 には、複数の電源端子 (VDDxxx_yyy) があります。ここで関係するのは、CPU系の **VDD3P3_CPU**と、RTC系の **VDD3P3_RTC** の 2つです。 [ESP32 Technical Reference Manual](https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf) 29.5 DAC を意訳引用します。

> ・**抵抗ストリング型の 8-bit DAC** である
> ・**基準電圧は VDD3P3_RTC **である
> ・DAC 出力算出式 :  DACn_OUT = **VDD3P3_RTC** x PDACn_DAC/256 [V] ...…**式(1)**

この DAC は、 **VDD3P3_RTC** の電源電圧を抵抗で分圧し、選択出力する方式といえます。

M5Stack の回路は、**VDD3P3_RTC** と **VDD3P3_CPU** が共用電源となっています。共用ということは、CPU や GPIO の動作負荷で生じた電源ノイズが、DAC にも伝搬するということです。**VDD3P3_RTC** の電圧を、電源ノイズ混じりの (3.3 + **N**) [V] として、式 (1) に代入します。

　DACn_OUT = (3.3+**N**) x PDACn_DAC/256
						 = PDACn_DAC x 3.3/256 + **PDACn_DAC x N/256** [V] ....式(2)

DAC出力に現れる電源ノイズの大きさは、DAC出力レベルに比例し、255で最大、128で半分、**0で最小**[^6]となります。



#### ノイズ低減アイディア

さて、[音割れ対策](#21-音割れ対策)では、DACの出力振幅を1/3に絞りました。これを DAC フルスケールの中央1/3で出力するのではなく、電源ノイズが小さい下1/3で出力したらどうでしょう。式(2)で考えた場合、

　DAC中央1/3の中心出力 (PDACn_DAC = 128 ) : DACn_OUT = 1.65 + **0.5N**
　DAC下側1/3の中心出力 (PDACn_DAC =   43 ) : DACn_OUT = 0.55 + **0.168N** 

　DAC下側出力と DAC中央出力のノイズ比 : 0.168N / 0.5N = **1/3**

となり、電源ノイズの影響を1/3に抑えることが出来そうです。

DAC出力の下側1/3は、**音割れ対策前に飽和を起こしていた領域**でした。利用できるのでしょうか？大丈夫です。M5Stack の回路図によれば、DACとアンプはコンデンサ C43 を介して接続されています。DAC出力のDC(平均電圧)は、コンデンサの**DC カット**効果により通過しません。アンプ側では電源中点 (1.65V) にバイアスされます。DACの出力振幅がフルスケール1/3の場合、出力位置は下1/3でも問題ないということです。


![M5_CORE_AMP_wz_DCCut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/0b56c260-062c-e8d0-6a6f-1bfa216a7f08.png)


#### 効果確認

GPIO に疑似負荷を与え、その電源ノイズが DAC 出力に現れるようにします。そのうえで、DACフルスケールの上・中・下1/3出力による電源ノイズの差・飽和の有無を測定します。

**GPIO19**[^7] **(緑)**: 疑似負荷信号です。0.5ms 周期のパルス(緑)を出力します。
**1kΩ**			      : 疑似負荷です。GPIO19 = H の時、3.3V / 1kΩ = 3.3mA の電流が流れます。
**DAC1出力 (青)**: 振幅 85LSB の Sin波を、DAC フルスケールの上・中・下 各1/3で出力します。電源ノイズの電圧依存を確認します。
**アンプ出力(赤)**: LPF 経由で測定します。飽和の有無を確認します。

![DAC_Noise_Eva_Topology.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/95b00f0c-d082-4a2b-e51d-c71456cfaf09.png)
![DAC_Noise_Sin_H_C_L_WC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/b42578b1-ab22-4500-db95-7cf5f62c2b7c.png)


**疑似負荷出力(緑)**：このパルスのタイミングで、DAC1出力にノイズが生じています。
**DAC1出力(青)**：DAC出力の上→中→下 の順に、ノイズが減少しています。
**アンプ出力(赤)**：DAC出力の上/中/下によらず、飽和は発生していません。

DACの出力範囲を下1/3とすれば、電源ノイズが低減可能なことが判りました。

これは対処療法です。電源ノイズは1/3になるだけで、完全にはなくせません[^8]。しかし、1/3 は $20log(1/3) = -9.5dB$ に相当し、聴感上も十分にノイズ低減効果を感じることができます。



### 2.3 分解能対策
CD (コンパクトディスク) などの音源は、16bit(96dB) の分解能があります。これに対し、M5Stack / ESP32 の内蔵 DAC は、8bit(48dB) の分解能しかありません。16bit 音源の下位 8bit を切り捨て、8bit DAC で再生すると、量子化ノイズが増加し、アナログ電話のような音質になります。

先の２つの対策では、DAC の出力振幅を 1/3 に絞りました。8bit 分解能の 1/3 しか利用できないので、実質分解能は  $log_2(2^8/3) = 6.4bit$ 相当、ダイナミックレンジは $20log_{10}(2^8/3) = 38.6dB$​ になります。CDの 16bit(96dB)​ には程遠い分解能です。

そこで、音声信号の **オーバーサンプリングとマルチビットΔΣ(デルタシグマ)変調**処理により、この 6.4bit を超える実質分解能を実現します。



#### ΔΣ変調とは

ΔΣ変調は、入力の振幅情報を、時間軸の密度情報に変換します。限られた出力分解能で、それを超える実質分解能が得られる技術です。以下に、一般的な「ワンビットΔΣ変調」と、今回利用する「マルチビットΔΣ変調」の構成例・出力例を示します。 

![DeltaSigma_Overview_OneBit_vs_MultiBit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/e81c4a5f-752e-f0aa-fc80-f88c79d42bb5.png)

##### ワンビットΔΣ変調

PDM (Pulse Density Modulation) や DSD (Direct Stream Digital) の説明でよく見かける図です。入力と前回出力との差分(Δ)を、繰り越し(Σ)、量子化器 (Quantizer) で2値化して出力します。その出力は、次回の入力との差分演算に再利用します。これにより、入出力の誤差を常に打ち消すような負帰還がかかります。結果的に、入力の振幅情報は、時間軸を使った2値 ＝ ワンビット の密度情報に変換された出力となります。[^9] [^10]

##### マルチビットΔΣ変調

ワンビットΔΣ変調との違いは、 Quantizer が多値出力であること、最終出力が低ビットDAC出力であることです。低ビットDAC出力の各階調で、ワンビットΔΣ変調を行うような動作となります。[^11] 


#### 変調の動作例

「整数出力のみが許されたシステムで、何としても **365.25** という実数相当を表現せよ」という例題を、ΔΣ変調で解決する方法を考えます。先のマルチビットΔΣ変調のブロック図に、例題の要素を当てはめます。入力は 実数 365.25 固定[^12]、ΔΣは実数演算、Quantizer は実数→整数変換手段となります。



**ブロック図・タイミングチャート**
![DeltaSigma_365_366.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/5de7d319-dd26-35ca-2d2f-36578c0a7c7e.png)

**C言語実装例**

```DeltaSigmaExample.c
void main(void){
    float fin = 365.25;                     // 実数入力
    float fdelta;						    // Delta(Δ)
    float fsigma = 366;					    // Sigma(Σ) & 初期値
    int   iqtout = 366;                     // 整数(量子化)出力 & 初期値
    printf("Num, fin   , fdelta, fsigma, iqtout\n");

    for (int i = 1; i <= 8; i ++){
        fdelta  = fin - (float)iqtout;      // 入出力差分(Δ）
        fsigma += fdelta;                   // 入出力差分の繰越(Σ)
        iqtout  = (int)fsigma;              // 量子化
        printf("%3d, %6.2f, %6.2f, %6.2f, %3d \n",
                i,   fin,   fdelta,fsigma,iqtout);
    }
}
```

**実行結果**

```Output
Num, fin   , fdelta, fsigma, iqtout
  1, 365.25,  -0.75, 365.25, 365 
  2, 365.25,   0.25, 365.50, 365 
  3, 365.25,   0.25, 365.75, 365 
  4, 365.25,   0.25, 366.00, 366 
  5, 365.25,  -0.75, 365.25, 365 
  6, 365.25,   0.25, 365.50, 365 
  7, 365.25,   0.25, 365.75, 365 
  8, 365.25,   0.25, 366.00, 366 
```

実行結果の fsigma をみると、入力 fin と、前回の出力 iqout の差分 fdelta を、順次繰り越していく様子が判ります。

fsigma を整数化した iqtout をみます。1~3回目は**365**を出力します。4回目は繰り越し誤差をまとめた**366**、いわば「うるうデータ」を出力します。以降、**4回で1巡**する周期出力となります。出力を短周期でみると、「うるうデータ」の発生で凸凹しますが、長周期でみれば、**平均365.25**となります。無事、ΔΣ変調により、整数出力と時間軸を使って、実数相当が表現できました。[^13]



#### 量子化ノイズと周期ノイズ

実数を整数化したり、情報のビット数を減らしたりすると、元情報と誤差を生じます。これを量子化誤差、**量子化ノイズ**などといいます。先の例では、元の 365.25 が 365 になって、0.25 の誤差 = ノイズを生じている状態です。ΔΣ変調は、この誤差を繰り越し、365, 365, 365,  366という凸凹な周期出力に変換しました。凸凹ということは、ノイズです。これは、**量子化ノイズ**を、元情報にはない凸凹な**周期ノイズ**に変換することを意味します。不思議なことですが、この周期ノイズが、量子化ノイズを減らすのです。先の DAC ノイズ対策とは対照的です。

音質改善目的でΔΣ変調を使う場合、この周期ノイズが音として聞こえるようでは困ります。解決には、次の技術を使います。



#### 高次ΔΣ変調とオーバーサンプリング

ΔΣ変調による量子化ノイズ改善と、ノイズに変換される周波数との関係を示します[^14]。システムのサンプリング周波数を **fs** とします。

![DeltaSigma_NoiseShaping_vs_OSR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/29e11f37-a239-6ff0-0891-e9411db67558.png)

ΔΣ変調は、**fs / 6 より低域の量子化ノイズが、fs / 6 より高域に移動し、低域の実質分解能が改善する**特性をもちます。これを**ノイズシェーピング効果**といいます。先の例では、0.25の誤差を集めて365、366 の周期ノイズとし、実質 365.25 が表現できた状態です。また、ΔΣ変調の次数 (段数) が大きいほど、ノイズシェーピング効果も高くなります。

fs = 44.1kHz の音源に、直接ΔΣ変調を行なうと、fs / 6 = 7.35kHz つまり**可聴帯域内** ( < 20kHz )でノイズが増加してしまいます。また、7.35kHz以下で、十分な量子化ノイズ低減効果が得られません。

そこで、あらかじめ音源を**オーバーサンプリング**して、fs を高くし、fs/6 が**可聴帯域外** ( > 20kHz )となるようにします。元の fs に対する オーバーサンプリング fs の倍率を、**OSR** (**オーバーサンプリングレート**)といいます。

OSR ≧ 4 とすれば、4 x 44.1kHz/6 = 29.4kHz となり、ノイズ増加は可聴帯域外になります。また、3次以上のΔΣ変調を行なえば、8kHz付近までは -31dB (5.1bit相当) と、十分な量子化ノイズ低減効果が得られます。M5Stack の実質分解能 6.4bit と合わせて11.5bit 相当の分解能を持てることになります。



#### 効果確認

16bit 信号を 8bit 化する際の「ΔΣなし」「1次ΔΣ」「3次ΔΣ」の効果を、表計算ソフトでシミュレーションした結果を示します。

　**共通条件**：fs = 176.4kHz (OSR=4)
　**グラフ青**：16bit 1016LSBp-p (-36.2dB) 1kHz Sin波形入力
　**グラフ赤**：各処理の 8bit DAC 出力
　**グラフ緑**：各処理の DAC出力に可聴帯域の 20kHz LPF を通した結果[^15]

![DeltaSigma_Sim_Order_0_1_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/ccb2c57d-62b9-18b2-240b-77acbe355cd4.png)



**ΔΣなし**：16bit 入力の下位 8bit を単純に切り捨てた例です。M5Stack / ESP32の 8bit DAC で、16bit 音源の上位 8bit だけを再生する状態にあたります。8bit DAC出力は、階段状になってしまいます。LPF 出力も、階段状のままであり、もはや元のSin波形の再現は難しいようです。

**1次ΔΣ**：8bit DAC出力の各諧調で、入力レベルに応じた密度変調が行われています。LPF 出力は「ΔΣなし」に比べ、かなり元のSin波形に近づきました。しかし、若干のノイズがあり、効果が不十分です。

**3次ΔΣ**：{}内部のΔΣ処理を3回繰り返したものです。8bit DAC出力は、高次ΔΣ変調のノイズシェーピング効果により、高周波の頻度が多くなり、その振幅も増えています。LPF出力は、「1次ΔΣ」に比べてノイズも減り、かなり元のSin波形に近づきました。

マルチビットΔΣ変調で、低分解能 DAC でそれを超える実質分解能が得られることが判りました。 M5Stack / ESP32 のように、低 bit DAC しかないシステムには格好の技術といえるでしょう。[^16]


## 3. ソフトウェア実装例

文末に[ソースコード例](#ソースコード例)を掲載しました。[M5Stack FIRE](https://www.switch-science.com/catalog/3953/)、[M5Stack Basic](https://www.switch-science.com/catalog/3647/)[^21] で動作確認済みです。

### 3.1 ソフトウェアと周辺ブロック図

![Software_BlockDiagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/398fd692-4c8a-43d0-fad9-07dfc04ceb56.png)


### 3.2 ソースコード解説

基本構造は文献1を参照しました。各対策処理のご本尊は、Filter_Process 関数内にあります。

#### 音割れ・DACノイズ対策処理

一般的な PCM 音源データは、符号付き整数( int )型 です。ESP32 の 8bit DAC データ形式は 符号なし整数( unsigned )型 です。このため、各データの符号 bit を反転して符号付き→符号なし 変換を行います。次に、ステレオのL/Rデータを平均してモノラル化します。これを float 型に変換し、 Gain / Offset 処理をかけ、DACレンジの中央 1/3 や下1/3で出せるようにします。

```c
uint32_t ud = *(pti32++);                     // i16 stereo をu32で読出
ud ^= 0x80008000;                             // i16 stereo -> u16 stereo
ud = ((ud & 0xffff) + (ud >> 16)) >> 1;       // Stereo -> Mono (u16L+u16R)/2
float fd = (float)ud, fin;                    // 後工程用float変換
                                              // 音割れ/DACノイズ対策処理
fd    = fc[menu[1].now].gain * fd             // Mul. Gain
      + fc[menu[1].now].offset;               // Add. Offset
...
```

#### オーバーサンプリング処理

1データ(サンプル)の入力に対し、データ数を OSR 倍にして出力をします。

1つの入力データ fd を、OSR 回のループの間、保持 (0次ホールド)します。データ数が OSR倍になりますが、これが可聴帯域内となるようLPF処理をします。オーディオ処理では、位相特性の良い FIR型 LPF が好まれますが、今回は係数と演算回数が少ない Biquad IIR型 LPFを使用しました。

```c
for (int j = 0; j < OSR; j ++) {          ////// xOSR Over Sampling Loop
  z[2] = z[1];                                // Biquad z2 Shift
  z[1] = z[0];                                // Biquad z1 Shift
  z[0] = lpf.k *fd -lpf.a1*z[1] -lpf.a2*z[2]; // Biquad z0 Update
  fin  = z[0]      +lpf.b1*z[1] +lpf.b2*z[2]; // Biquad result
  ...
```

#### ΔΣ変調処理

[ΔΣ変調の動作例](#変調の動作例)では、float型の処理を例示しました。ここでは、DACデータ設定との親和性から、int32_t型でΔΣ変調処理をします。

```for( int k ...) {}``` のループ内部が、k次のΔΣ変調処理です。入出力の差( iin - iqtout ) を、isigma[] に繰り越し加算しています。ESP32 は、I2S 経由で 8bit DAC にデータを渡す際、16bit データとする必要があります。上位 8bit だけが DAC に設定され、下位 8bit は無視されます。これに合わせて、Quantizer 処理は、入力データの下位 8bitをマスク(ゼロデータ化)しています。

```c
  int32_t iin = (int32_t)(fin + 0.5);         // float -> i32 data
  for (int k = 0; k < dso; k++ ) {          //// xOrder DeltaSigma Loop(dso=0:Thru)
    isigma[k] += (iin - iqtout);              // Delta-Sigma Core
    iin = isigma[k];                          // Update Output (iin)
  }                                         //// Delta-Sigma Loop End
  iqtout = iin & 0xffffff00;                  // Quantize(8bDACで無視される下位8bitをMask)

  ud = (uint32_t)constrain(iqtout, 0, 65535); // Clip to u16 size
  ud |= (ud << 16);                           // u16 Mono → Dual u16 Mono
  *(pto32++) = ud;                            // DAC出力バッファに書込
}                                         ////// OverSampling Loop End
```



### 3.3 UI仕様

処理選択 UI (ユーザー・インターフェイス)を付け、各対策前後の効果を比較できるようにしました。M5Stack のA/B/Cボタンで処理を選択できます。



#### Source : 音源選択(ボタンA)  

##### Zero
無音です。起動時はこれが選択されます。再生停止やDACノイズ観測に使用します。

##### -39dB Sin / -26dB Sin 

 小振幅 1kHz Sin波形のテストトーンです。-39dB Sin は、Gain = 1/3(-9.5dB) のとき、8bit分解能 (-48dB) を下回る信号となります。ΔΣ変調で、これが再生可能かを確認できます。
-26dB Sin は、Gain = 1/3 のとき、8bit 分解能で 4LSBの振幅となります。ΔΣ変調後の波形・ノイズシェーピング特性観測や音質確認に利用します。

##### Decay Sin

振幅が指数的に減衰する 1kHz Sin波形のテストトーンです。0.25秒で振幅が半減(1bit分)し、4秒間で16bitから0bit振幅まで減衰します。2秒目に 8bit 未満の分解能となります。これが ΔΣ変調で再生可能か確認します。

##### Wave file

SD カードのルートフォルダにある sample.wav ファイル を再生します。16bit 44.1kHz stereo 専用です。



#### DAC Lvl. : DACレベル選択(ボタンB)

##### Thru (1/1)

音源の振幅を絞らずに出力します。音割れ対策とノイズ対策をする前の状態です。

##### Mid 1/3

音源の振幅を1/3に絞り、DAC中央1/3で出力します。音割れ対策のみの処理です。

##### Low 1/3

音源の振幅を1/3に絞り、DAC下1/3で出力します。音割れとノイズ対策の処理です。



#### DS Order : ΔΣ次数選択(ボタンC)

##### Thru

オーバーサンプリング処理のみです。ΔΣ変調処理をしません。

##### 1st / 2nd / 3rd / 4th DS

 1~4次 ΔΣ変調処理を選択します。

なお、オーバーサンプリングは OSR = 4 で常時処理されるようにしています。OSR は UIでは変更できませんが、ソースコード冒頭の  ```#define OSR     4``` を1~8の任意値に変更可能です。



## 4. 効果確認

　[ソースコード例](#ソースコード例)を使用して、各対策前後の出力を測定し、その効果を確認します。測定トポロジーは、[ソフトウェアと周辺ブロック図](#31-ソフトウェアと周辺ブロック図) を参照願います。



### 4.1 効果全貌

アンプ出力を、外付けLPF 経由で PC の Audio LINE に入力し、音声編集ソフト[Audacity](https://forest.watch.impress.co.jp/library/software/audacity/) で振幅を測定します。音源は [Decay Sin](#decay-sin) を使い、大振幅での飽和状態、小振幅でのDACノイズ限界や分解能限界を確認します。左から「対策なし」「音割れ対策」「DACノイズ対策」「分解能対策」と3つの対策を重ねていった結果です[^17]。上段は振幅をリニアに、下段は振幅を対数表示(㏈)したものです。横軸は「秒」です。

![eva_ac_decay_step_by_step.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/0e91f0e7-9fc2-dc3d-11f7-515cd40ae251.png)

**対策なし** : 開始直後の大振幅において、波形ピークが飽和しています。音源が小振幅になるにつれ、振幅表現が乏しく、段差状になっています。やがて音源の振幅が 8bitを下回ると、分解能限界をむかえ、ばっさり出力がなくなっています。DAC ノイズフロアも高いです。

**音割れ対策** : 音源の振幅をあらかじめ1/3にすることで、波形ピークの飽和が改善されました。しかし分解能が犠牲(8bit -> 6.4bit)となり、「対策なし」よりも早い段階で分解能限界をむかえています。DAC ノイズフロアも高いままです。

**DACノイズ対策** : DAC下1/3で出力することで、DACノイズフロアが -9dB 下がりました。ほぼ理論値(-9.5dB)通りです。このあたりが、M5Stack の DAC ノイズ改善限界と思われます。 

**分解能対策** : マルチビットΔΣ変調により、分解能は -50dB 付近の DAC ノイズフロアを超え、Audacity の対数表示限界の -60dB 付近 (10bit 相当) まで改善しました。このあたりが、 M5Stack DAC 分解能改善の限界と思われます。



### 4.2 DACノイズ対策効果

周波数軸(スペクトラム)で、DACノイズ対策効果を確認します。DAC1 出力を PC の Audio LINE に入力し、 [WaveSpectra](http://efu.jp.net/soft/ws/ws.html) で信号の振幅・周波数特性を観測します。上段は時間軸、下段は周波数軸です。確認を容易にするため、周波数軸は+40dBシフトしています。音源は、[Zero (無音)](#zero)です。

![eva_ws_zero_x0p33_mid_to_low.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/9f1ff403-c24a-f861-8785-c0a2e43ee217.png)

[**Mid 1/3 (対策前)**](#mid-13)：DAC中央1/3で「無音」を出力した結果です。「無音」であるはずですが、ノコギリ波状の3～4kHzのノイズが確認できます。周波数軸では、50Hz、3～4kHz付近、44.1kHzにピークが見られます。

[**Low 1/3 (対策後)**](#low-13) : DAC下側1/3で「無音」を出力した結果です。Mid 1/3 に比べ、ノイズ振幅が減衰しています。周波数軸では、帯域全体でノイズが約9dB下がっています。これは [DACノイズ対策](#22-dacノイズ対策) で解説したノイズ低減効果そのもので、聴感上もはっきりと効果が判ります。

##### 参考：ノイズ要因

**50Hz** :  ソフトウェア処理周期のノイズです。44.1kHz の音源を、882 サンプル単位でバッファ処理していますが、この周期が 44100 /  882 = 50[Hz] となります。サンプル単位数を変えると、この周波数も変わります。聴感上認知しにくいよう、なるべく低い周波数にするのが良いようです。

**3~4kHz 付近** : 周期変動を持つ下降ノコギリ波形であること、USB給電をすると止まることから、昇圧DCDCコンバータ由来のノイズと思われます。詳細解析には至っておりません。聴感上は「ギー」という不快な音に聞こえます。

**44.1kHz** : LCDバックライトPWMの周期ノイズです。setup関数内で `ledcWriteTone(7, SRC_FS );` として音源の fs と同じ周波数を指定し、可聴帯域外に移動しています。本対策は、[文献3](https://macsbug.wordpress.com/2019/09/27/m5stack-speaker-noise-reduction/)経由で [lovyan03 さんの対策案](https://github.com/m5stack/M5Stack/issues/142)を参考にしました。


### 4.3 分解能対策効果

ΔΣ変調の効果を周波数軸で確認します。測定条件は前項と同じです。音源は[-26dB Sin](#-39db-sin---26db-sin)です。

![eva_ws_26dBSin_thru_to_3rdDS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/3c2febde-c9a6-e8c7-7695-f6eab71aec2f.png)

**ΔΣ変調なし(左)**：基本波1kHz の奇数倍の量子化ノイズが発生しています。

**3次ΔΣ変調(右)**：ノイズシェーピング効果により、量子化ノイズ成分が可聴帯域(20kHz)より高域に移動しています。ノイズまみれの波形からは想像しづらいですが、聴感上、時報音のような「澄んだ」音になります。



### 4.4 まとめ

最後に「DACノイズ対策」と「分解能対策」効果を、アンプ出力波形( LPF 経由)で確認します。音源は[-26dB Sin](#-39db-sin---26db-sin)です。

![eva_ws_26dBSin_mid_low_3rdDS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/387543a4-ed2b-ca76-e45f-b7cd6cd7481b.png)

**「対策なし」**：DACの分解能に迫る大きさの電源ノイズが生じています。

**「DACノイズ対策」**：電源ノイズが小さくなり、DACの分解能があらわになりました。

**「分解能対策」**：量子化ノイズが減り、きれいな Sin 波形、きれいな再生音になりました。

以上、DAC下1/3出力による「DACノイズ対策」と、マルチビットΔΣ変調による「分解能対策」の効果が確認できました。



#### 課題

LCD 描画時や SD カード上の Wav File 再生時、小さく「ギョロギョロ」という音が聞こえます。これは SPI バス動作時のノイズなのですが、DAC ノイズ対策でも思ったほどに低減できませんでした。これは、[文献３](https://macsbug.wordpress.com/2019/09/27/m5stack-speaker-noise-reduction/)で分析されているように、SPI系GPIOから DAC1への輻射が関係しているかもしれません。DAC1 の出力インピーダンスが高く、ノイズを拾いやすい可能性があります。他にも、「ΔΣアイドルトーン[^18]」「高OSRでノイズ増加[^19]」などの課題があり、別の機会に調査したいと思います。


#### 参考：対策処理のCPU占有率

Filter_Process 関数を micros 関数で挟み込み、対策処理全体のCPU占有率を確認しました。CPU 240MHz の場合、最大17.2%と軽負荷です。CPU 80MHz の場合は、50%を超えてしまう場合があり、他のアプリケーション・タスクとの共存には配慮が必要です。ΔΣ次数を減らす、オーバーサンプリングのLPFフィルタ処理を省略する等の工夫が必要でしょう。

![cpu_load.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/630160/df411b8c-a9a6-5df4-18f7-feb211519258.png)


## 5. おわりに

M5Stack は、本当に素敵なハードウェアです。私もいっぺんに虜(とりこ)になってしまいました。しかし、今回の DAC やスピーカーのように、回路デザインにクセがあって、なかなか思い通りに動いてくれないところもあります。そこがまた愛しく、工夫のし甲斐があるところです。今回は、ハードに手を加えず、ソフトの創意工夫だけで、どこまでスピーカー音質の限界性能を引き出せるか挑戦してみました。

ご興味ある方は、ぜひ耳馴染んだ音源で、Wave file 再生をしてみてください[^20]。そして、ご自身の耳で対策前後の音質を比較してみてください。主観ですが、スピーカー音質は「玩具レベル」から「小型FMラジオ(ちょっとノイズ混じり)」くらいに改善できたのではないかと思います。残ったノイズ等は、まだ改善の余地があるかもしれません。

この記事が何かのお役に立てば幸いです。

なお、M5Stack / ESP32 で「DAC下1/3出力でノイズを低減する」「マルチビットΔΣで分解能を改善する」前例を調査しましたが、今のところ見つかっておりません。もし、前例をご存じでしたらお知らせください。



## 参考文献

[Philips TDA1540 16-BIT D/A conversion system (Philips, 1984) ~ DutchAudioClassics](https://www.dutchaudioclassics.nl/Philips-TDA1540-16bit-da-conversion/) 
CD (コンパクトディスク) 黎明期の1984年。まだ半導体プロセスが未熟で、CD規格にフィットする 16bit DAC の製造が簡単ではなかった頃の論文。14bit DAC と ノイズシェーピング処理 IC の組み合わせで、16bitを超える性能を得るというもの。このチップセットを搭載したCDプレーヤー [Marantz CD-34](https://audiof.zouri.jp/cd-34.htm) の音質は、現代でも通用するほど素晴らしく、今でもそこそこの価格で[取引](https://aucfan.com/search1/q-marantz.20CD.2d34/s-mix/?shipping=all&o=p1&location=0)されていたり、[専門の修理職人](http://www.iteck.co.jp/myweb1_011.htm)が活躍されたりしています。私も CD-34 オーナーです。

 [限界性能への挑戦と音質へのこだわり：河合　一 様 ~ 日本テキサスインスツルメンツ株式会社](https://www.tij.co.jp/jp/lit/pdf/jajt042?keyMatch=JAJT042&tisearch=Search-JP-everything) 　
マルチビットΔΣ DACの説明があります。最近の Audio DAC には積極活用されているようです。

[「ΔΣ変調」の解説(1) ~ しなぷすのハード製作記](https://synapse.kyoto/glossary/delta_sigma/page001.html) 
[少し詳しいΔΣ変調 ~ electric ホロン様 ](https://blog.goo.ne.jp/commux/e/4a7042ef558aae0e66a6e6498ef66949) 
[線形・非線形ロバスト制御理論を用いたデルタシグマ変調器の設計手法 : 喜田 健司様 ~ 九州大学学術情報リポジトリ](https://catalog.lib.kyushu-u.ac.jp/opac_download_md/1500737/design0176.pdf) 
ΔΣ変調、ノイズシェーピング効果の原理・効果を深く知りたい人向け。喜田様の論文は高次ΔΣ変調の理解と実現に役立ちました。

[階段グラフのつくりかた（カクカクな ～ おっ](https://ameblo.jp/xls/entry-10183164939.html)
Excel で DAC 出力をそれっぽく描く方法の参考。誤差表示を使うというのが目からウロコ。





## 利用ツール

[Audacity](https://forest.watch.impress.co.jp/library/software/audacity/) 
Windows用オーディオ編集ツール(フリーソフト)。秀逸です。

[WaveSpectra - efu's page](http://efu.jp.net/soft/ws/ws.html) 
Windows用オーディオアナライザ(フリーソフト)。秀逸です。

[Converting Analog into Digital (IIR) Filters - Manually or by AnaDigFilt.exe - - The Electronics Section of Beis.de](https://www.beis.de/Elektronik/Filter/AnaDigFilt/AnaDigFilt.html) 
オーバーサンプリング用 18-22kHz IIR LPF 設計に利用。

[RLCローパス・フィルタツール - OKAWA Erectric Design](http://sim.okawa-denshi.jp/RLClowkeisan.htm) 
D級アンプのPWM出力をアナログ振幅に変換するための外付けRLC LPF設計に利用。

[SideBB for M5Stack ~ スイッチサイエンス](https://www.switch-science.com/catalog/4098/) 
M5Stack の信号計測や外付けRLC LPFの構築に利用。ちょっとした実験にとても便利です。



## ソースコード例

IDE : [Arduino IDE](https://www.arduino.cc/en/main/software) 1.8.13
ターゲットボード : [M5Stack Basic](https://www.switch-science.com/catalog/3647/)[^21] / [M5Stack FIRE](https://www.switch-science.com/catalog/3953/)

```M5Stack_HiFi_Speaker_Estim.ino
// M5Stack_HiFi_Speaker_Estim.ino geachlab 2020
#define SD_WAV  "/sample.wav" // SDCard上のWaveFile定義．44.1kHz 16bit Stereo専用
#define SRC_FS  44100         // Source Sampling Rate [Hz]
#define OSR     4             // Over Sampling Rate (1~8) 
#define DAC_FS  (OSR *SRC_FS) // DAC Sampling Rate [Hz]
#define SPF     882           // Sample per Frame (Buffer処理単位)
#define DSOMAX  4             // Max. of DeltaSigma Order
#define SIN_FRQ 1000.0        // 1kHz
#define SINTBLMAX (SRC_FS /4) // 44100/100*25 = 11025 (.25s分。半減単位)
#define DECAY_MAX 16          // Sin振幅を半減させる回数 (16*.25s = 4sec) 

#include <M5Stack.h>
#include "driver/i2s.h"

char src_buf[4*SPF    ]={0};  // 16bit/spl*stereo*spl/Frame
char dac_buf[4*SPF*OSR]={0};  // 16bit/spl*stereo*spl/Frame *OverSamplRate

struct {                  ////// Biquad Filter Coef.
  const float k, a1, a2, b1, b2; // b0 is fixed to '1', b1 & b2 MUST BE normalized
} lpf = {                   //// OSR Val.
#if   OSR >= 8                // fc = 0.05fs
  .k = +0.0208307252, .a1 = -1.552247273, .a2 = +0.635570174,
#elif OSR >= 6                // fc = 0.067fs
  .k = +0.0347860374, .a1 = -1.407505344, .a2 = +0.546649494,
#elif OSR >= 4                // fc = 0.1fs
  .k = +0.0697808942, .a1 = -1.126428906, .a2 = +0.405552483,
#elif OSR >= 2                // fc = 0.2fs
  .k = +0.2132071969, .a1 = -0.339151185, .a2 = +0.191979973,
#else // OSR < 2              // fc = 0.4fs (18kHz at 44k1, 20k at 48k)
  .k = +0.6632098902, .a1 = +1.209579277, .a2 = +0.443260284,
#endif
  .b1 = +2, .b2 = +1,         // Fixed (b0,b1,b2)=(+1,+2,+1)
};

// UIメニュー定義
struct {
  const char list[6][9]; int max, old, now;
} menu[3] = { //list0      list1      list2      list3      list4       max old now
  {{"Source  ","Zero    ","-39dBSin","-26dBSin","DecaySin","Wavefile"},  5, -1,  0},
  {{"DAC Lvl.","Thru    ","Mid 1/3 ","Low 1/3 ","RESERVED","RESERVED"},  3, -1,  0},
  {{"DS Order","Thru    ","1st DS  ","2nd DS  ","3rd DS  ","4th DS  "},  5, -1,  0},
};

// Source Fillサブメニュー定義
typedef enum {
  Init = -1, Zero = 0, Sin = 1, Decay = 2, Wav = 3
} FillType;

// DAC Lvl.用 Gain/Offset定義
struct {
  const float gain, offset;
} fc[3] = {                       // MenuMode Gain 8bit切上補正 純オフセット
  { +1.000000000,   +128.00000 }, // Thru     1/1  +128
  { +0.333333333, +21973.33333 }, // Mid 1/3  1/3  +128       (1-1/3)*(2^15)
  { +0.333333333,   +213.33333 }, // Low 1/3  1/3  +128       +256/3
};

void i2s_init() {
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_DAC_BUILT_IN | I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = DAC_FS,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 内部DACは上位8bitが再生対象
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // ステレオ。左右データ書込必要
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = 0,
    .dma_buf_count = 16,      // 現物合わせ
    .dma_buf_len = SPF,       // 1024以下。今回はサンプルフレーム幅に合わせた
    .use_apll = false         // 75kHz以上(OSR>=2)でtrue指定すると正常動作しない
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL );
  i2s_set_pin( I2S_NUM_0, NULL );
}

void menu_print(void) {
  M5.Lcd.setTextSize(2);                            // Set Textsize for Menu
  for ( int i = 0; i < 3; i++ ) {                   // Main loop
    if ( menu[i].now != menu[i].old ) {             // When menu Update
      menu[i].old = menu[i].now;                    // Update menu_old
      for ( int j = 0; j <= menu[i].max; j++ ) {    // Sub loop
        if (( j == 0 ) || ( menu[i].now == j - 1))  // When Reference / Marked Menu
          M5.Lcd.setTextColor(BLACK, WHITE);        //   Inverted Text Color
        else                                        // When Unmarked Menu
          M5.Lcd.setTextColor(WHITE, BLACK);        //   Normal Text Color
        M5.Lcd.setCursor(i *9 *12 +4, (14 -j) *16); // Set location, Text Size:12x16
        M5.Lcd.print(menu[i].list[j]);              // print Marked/Unmarked Menu
      }
    }
  }
}

void menu_init(void) {
  M5.Lcd.clear(BLACK);
  M5.Lcd.setBrightness(255);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(0, 6);
  M5.Lcd.println("M5Stack HiFi Playback Demo");
  menu_print();
}

void menu_update(void) {
  M5.update();                              // ボタン操作情報の更新
  if        (M5.BtnA.wasReleased()) {       // Aボタン短押し : Menu0 Up
    if (++menu[0].now >= menu[0].max) menu[0].now = 0;
  } else if (M5.BtnA.wasReleasefor(500)) {  // Aボタン長押し : Menu0 Down
    if (--menu[0].now < 0) menu[0].now = menu[0].max - 1;
  } else if (M5.BtnB.wasReleased()) {       // Bボタン短押し : Menu1 Up
    if (++menu[1].now >= menu[1].max) menu[1].now = 0;
  } else if (M5.BtnB.wasReleasefor(500)) {  // Bボタン長押し : Menu1 Down
    if (--menu[1].now < 0) menu[1].now = menu[1].max - 1;
  } else if (M5.BtnC.wasReleased()) {       // Cボタン短押し : Menu2 Up
    if (++menu[2].now >= menu[2].max) menu[2].now = 0;
  } else if (M5.BtnC.wasReleasefor(500)) {  // Cボタン長押し : Menu2 Down
    if (--menu[2].now < 0) menu[2].now = menu[2].max - 1;
  } else return;  // ボタン操作がなかったときは menu表示をせずに終了
  menu_print();   // ボタン操作があったときのみ menu表示を更新
}

static float flat_sin_tbl[SINTBLMAX];
static float decaysin_tbl[SINTBLMAX];

size_t fill_data(int type, float dB) {
  static int32_t ct = 0, ct2 = 0;
  static File wav;
  if ( type == Init ) {
    wav = SD.open(SD_WAV);
    wav.seek(0x2C);  // Skip wav header
    for ( int32_t i = 0; i < SINTBLMAX; i ++ ) {
      flat_sin_tbl[i]
        = sin(2.0 * PI * i * SIN_FRQ / SRC_FS); //Sine wave, -1~+1
      decaysin_tbl[i]
        = flat_sin_tbl[i]
          * pow(0.5, (float)i / SINTBLMAX);     // x 減衰音 tbl長で振幅半減
    }
    return 0;
  }
  int32_t *pto32 = (int32_t *)src_buf;
  dB = constrain( dB, -96, 0);                  // Clip to -96~0
  float f;
  float g  = 32767.0 * pow(10, dB / 20);        // for flat gain
  float gd = g * pow(0.5, (float)ct2);          // for decay gain
  size_t r_size = sizeof(src_buf);
  if ( type == Wav ) {
    r_size = wav.readBytes(src_buf, sizeof(src_buf));
    if ( r_size != sizeof(src_buf) ) {
      wav.seek(0x2C); // rewind to start point
    }
  } else {
    for (int i = 0; i < (sizeof(src_buf) >> 2); i++) {
      switch ( type ) {
        case Decay: f = gd * decaysin_tbl[ct];  break;
        case Sin  : f = g  * flat_sin_tbl[ct];  break;
        case Zero :
        default   : f = 0;                      break;
      }
      if (++ct >= SINTBLMAX) {
        ct = 0;
        if (++ct2 >= DECAY_MAX) ct2 = 0;
        gd = g * pow(0.5, (float)ct2);          // update decay gain
      }
      int32_t id = (int32_t)(f + 0.5);          // f32->i32 & Round Up
      id = constrain( id, -32768, +32767);      // Clip to i16 Range
      *(pto32++) = (id << 16) | (id & 0xffff);  // L&R Dual Mono Data
    }
  }
  return r_size;
}

//////////////////////////////////////////////////// Gain/Offset/OverSampling/DeltaSigma
void Filter_Process( size_t r_size ) {
  static float    z[3] = { 32768.0 };             // Biquad IIR LPF z work
  static int32_t  iqtout = 0;                     // int Quantizer Output
  static int32_t  isigma[DSOMAX] = {0};           // int Sigma[Order] Data;
  uint32_t *pti32 = (uint32_t *)src_buf;          // 演算と高速化都合でi16 x2をu32で読出
  uint32_t *pto32 = (uint32_t *)dac_buf;          // 高速化都合でu16 x2をu32で書込
  int dso = menu[2].now;                          // DeltaSigma Order(次数)取得

  for (int i = 0; i < (r_size >> 2); i++) { //////// Word Loop
    uint32_t ud = *(pti32++);                     // i16 stereo をu32で読出
    ud ^= 0x80008000;                             // i16 stereo-> u16 stereo 変換
    ud = ((ud & 0xffff) + (ud >> 16)) >> 1;       // Stereo->Mono (u16L+u16R)/2
    float fd = (float)ud, fin;                    // 後工程用float変換
                                                  // 過増幅歪/DACノイズ対策処理
    fd    = fc[menu[1].now].gain * fd             // Mul. Gain
          + fc[menu[1].now].offset;               // Add. Offset

    for (int j = 0; j < OSR; j ++) {          ////// xOSR Over Sampling Loop
      z[2] = z[1];                                // Biquad z2 Shift
      z[1] = z[0];                                // Biquad z1 Shift
      z[0] = lpf.k *fd -lpf.a1*z[1] -lpf.a2*z[2]; // Biquad z0 Update
      fin  = z[0]      +lpf.b1*z[1] +lpf.b2*z[2]; // Biquad result

      int32_t iin = (int32_t)(fin + 0.5);         // float -> i32 data
      for (int k = 0; k < dso; k++ ) {          //// xOrder DeltaSigma Loop(dso=0:Thru)
        isigma[k] += (iin - iqtout);              // Delta-Sigma Core
        iin = isigma[k];                          // Update Output (iin)
      }                                         //// Delta-Sigma Loop End
      iqtout = iin & 0xffffff00;                  // Quantize(8bDACで無視される下位8bitをMask)

      ud = (uint32_t)constrain(iqtout, 0, 65535); // Clip to u16 size
      ud |= (ud << 16);                           // u16 Mono → Dual u16 Mono
      *(pto32++) = ud;                            // DAC出力バッファに書込
    }                                         ////// OverSampling Loop End
  }                                         //////// Word Loop End
}

void setup() {
  M5.begin();
//setCpuFrequencyMhz(80);     // 80MHzのほうがDACノイズ低減に有利。ただしBasicでSDカード再生に失敗する場合あり
  ledcWriteTone(7, SRC_FS );  // LCDバックライトのPWM周期をSRC_FSと同じにして可聴帯域外とする
  menu_init();
  i2s_init();
  fill_data( Init, 0 );
}

void loop() {
  size_t r_size = 0;
  for ( int i = 0; i < (SRC_FS / SPF); i++ ) {
    menu_update();                                    // ボタン操作読取＆メニュー更新
    switch (menu[0].now) {                          //// 音源選択
      case 4 : r_size = fill_data(Wav,     0); break; // Wav File出力
      case 3 : r_size = fill_data(Decay,   0); break; // 減衰Sin 出力
      case 2 : r_size = fill_data(Sin,   -26); break; // 1/3出力時8bitで4LSB程度
      case 1 : r_size = fill_data(Sin,   -39); break; // 1/3出力時8bitで1LSB未満
      case 0 :
      default: r_size = fill_data(Zero,    0); break; // ゼロ出力
    }
    Filter_Process( r_size );
    i2s_write_bytes( I2S_NUM_0, (char *)dac_buf, r_size * OSR, portMAX_DELAY );
  }
}
```



## 注釈

[^1]: 当方調べ
[^2]: アンプのゲインは、アンプの製造バラつき・動作温度などのアナログ要素で変化することがあります。
[^3]: ノコギリ波は、非常に大きく耳障りな音が鳴ります。同じ実験をされる方は、周囲へのご迷惑・過負荷によるスピーカーの焼損にご注意ください。
[^4]: アンプ出力が緩やかなカーブを描いているのは、DAC～アンプ間のコンデンサ : C43 の充放電特性によるものと思われます。
[^5]: あくまでも類推した概念図であり、M5Stack や ESP32 の構造を正確に表したものとは限りません。
[^6]: M5Stack では `dacWrite(25, 0)` と記述し、DAC出力を最小レベルにしてスピーカーノイズを低減する方法が知られていますが、まさにこの原理を利用したものといえるでしょう。
[^7]: GPIO18~23 は VDD3P3_CPU に所属する端子です。これらの GPIO は、M5Stack 内部で LCD・SDカードのアクセスに利用されています。
[^8]: 残念ながらM5Stack 回路デザインの問題です。CPU や GPIO が動作する限り、DACノイズが不可避な構造です。さらに、電源がバッテリ～5V昇圧DCDCコンバータ～3.3V降圧DCDCコンバータ型と2段構成で、そもそも電源がノイジーです。負荷電流の変化によって、DCDCコンバータの充放電周期が変調され、広帯域なノイズとなる課題もあります。せめて VDD3P3_CPU と VDD3P3_RTC が別電源であったならば…と思いますが、コスト・サイズ・歴史的背景等、様々な理由あっての現行デザインなのでしょう。黙って使いこなす他ありません。
[^9]: 説明図はデジタル入力～アナログ出力を前提としています。構成によっては入力、ΔΣ 手段、 Quantizer がアナログ構成であったり、2値出力をアナログ信号に戻す 1bit DAC が含まれる場合があります。
[^10]: 高い実質分解能を得る場合、高い fs (サンプリング周波数) が必要になります。DSD の場合、元の音源の 64倍以上の fs が採用されています。また、高次のΔΣ変調は発振しやすいため、配慮が必要です。信号の再現性は処理クロック＝時間の正確さに依存します。
[^11]: ワンビットΔΣ変調に比べ、fs (サンプリング周波数) を低くできたり、高次のΔΣ変調でも発振しにくい特徴があります。再現性は処理クロックの正確さと出力手段の DACのリニアリティに依存します。
[^12]: この例では、単純化のために固定入力で説明しています。音声信号のような可変入力であっても、同様に処理が可能です。
[^13]: ΔΣの仕組みを直感的に理解いただくため、あえて「うるう年」の概念に寄せた例としました。実際の地球の平均周回日数は、365.242189...日/年です。グレゴリオ暦の「うるう年」は、社会システムに馴染みやすい規則(西暦が400で割り切れる、または4で割り切れて100で割り切れない年) となっています。ここで扱うΔΣ変調の考え方とは違います。
[^14]:  効果説明のために、8bit DAC の既存Dレンジ/bit数を正領域にシフトしています。
[^15]:  LPFは波形観測のための手段であって、実際の信号処理には実装されません。
[^16]:  ΔΣ変調は大変奥が深く、私も理論を未消化のまま紹介しています。間違い等ありましたら、ご指摘願います。また、ESP32 の I2S内部には、ワンビットΔΣ変調を行う PDM が搭載されています。M5Stack のスピーカー音質改善に利用できるかは未検討です。出力が0V - 3.3V であるため、M5Stack ではアンプの飽和領域で利用することになります。
[^17]:  各対策の測定波形を編集したものです。ソースコード例には、対策処理を自動的に切り替える機能はありません。
[^18]: 　音源を有音から無音(ゼロデータ)に切り替えた場合、ΔΣ変調ループ内の残データによってアイドルトーンが発生する場合があります。微小なノイズデータを与え続けることで、アイドルトーンを「散らす」ことが出来ます。今回は未対策です。
[^19]: ΔΣの特性上、OSR は高いほうが有利です。調子に乗って OSR=8 (DAC_FS=352.8kHz) を試しました。動きますが、却って可聴帯域のノイズが増加しました。原因は未特定ですが、M5Stack のD級アンプのキャリアが400kHz付近であり、DAC_FSの352.8kHzと近く、折り返しが発生している可能性があります。 　
[^20]: 静かめで、減衰音が多いピアノソロ曲などで効果がわかりやすいと思います。
[^21]: M5Stack Basic で、まれに SDカード の wave file 再生が不安定になる場合があります。SDカードにも相性があるようですが、再現性に乏しく究明できておりません。FIREは問題ないようです。CPU周波数は、240MHzよりも80MHzのほうがDACノイズ低減に有利ですが、確率的に 80MHzで SDカードが不安定になりやすいようで、この対策のため`setCpuFrequencyMhz(80);`はコメントアウトし、240MHz 動作としています。