0. はじめに
M5Stack のスピーカーは低音質で有名です。私も M5Stack 入手直後、サンプルスケッチで Wave File をスピーカーで再生してみて、前評判通りの低音質にがっかりしました。そこで、この音質問題に取り組まれた先駆者様の知恵を参考にしつつ、高音質化の限界1に挑戦しました。この記事では、そのプロセス、ソフトウェア実装例、改善効果をレポートします。
先駆者様
文献1 : Tw_Mhage 様 M5Stackのスピーカーの音質が悪い原因と対策
文献2 : N.Yamazaki 様 M5Stackの音量を抵抗1つで調節する - N.Yamazaki's blog
文献3 : macsbug 様 M5Stack speaker noise reduction
他
1. 概要
M5Stack のスピーカーの低音質原因は、文献1で以下のように分析されています。
1.アンプのゲインが大きすぎて音が割れている
2.自身のディジタルノイズが重畳されている
3.音声出力の振幅の分解能が低い
文献1 / 文献2では、それぞれソフト/ハードによる「音割れ対策」、文献3では、ハードによる「ノイズ対策」が提案されています。
本記事は、ソフトによる 「ノイズ対策」「分解能対策」を提案し、高音質化を目指します。
2. 対策詳細
2.1 音割れ対策
文献1 のおさらいです。スピーカー駆動アンプ (NS4150B) のゲインが約3倍2であり、音割れ(過増幅歪)を起こします。DACからアンプへの入力振幅が 3V の場合、アンプは本来 3V × 3倍 ⁼ 9V を出力すべきですが、実際の出力はアンプの電源電圧範囲 3.3Vに制限されます。出力波形はピークが飽和してしまい、拡声器を通したような歪んだ音になります。
DACの出力振幅を、あらかじめ 1/3(=アンプゲインの逆数)に絞り、アンプの飽和を対策します。
効果確認
詳細な原因・対策方法は、文献1で説明されています。ここでは追試にとどめます。
ソフトウェアで 8bit 振幅のノコギリ波を生成し、これを1倍または1/3倍にして DAC1 で出力します3。DAC1 出力(青)とアンプ出力(赤)を、同時にオシロスコープで観測します。アンプ出力は、PWM波形であるため、外付けローパスフィルタ(LPF)でアナログ振幅に変換して観測します。
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 出力に伝搬してしまいます。結果的にスピーカー再生音にノイズが乗ります。
先の音割れ対策では、DACの出力振幅を1/3に絞りました。これをDACフルスケールの中央1/3で出力するのではなく、電源ノイズの影響が小さい下1/3で出力します。これにより、DACに現れる電源ノイズを1/3に減らせます。
電源と DAC の関係
M5Stack 回路図とESP32 マニュアル から類推した M5Stack の DAC と電源の関係図を示します。5
ESP32 には、複数の電源端子 (VDDxxx_yyy) があります。ここで関係するのは、CPU系の VDD3P3_CPUと、RTC系の VDD3P3_RTC の 2つです。 ESP32 Technical Reference Manual 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となります。
ノイズ低減アイディア
さて、音割れ対策では、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でも問題ないということです。
効果確認
GPIO に疑似負荷を与え、その電源ノイズが DAC 出力に現れるようにします。そのうえで、DACフルスケールの上・中・下1/3出力による電源ノイズの差・飽和の有無を測定します。
GPIO197 (緑): 疑似負荷信号です。0.5ms 周期のパルス(緑)を出力します。
1kΩ : 疑似負荷です。GPIO19 = H の時、3.3V / 1kΩ = 3.3mA の電流が流れます。
DAC1出力 (青): 振幅 85LSB の Sin波を、DAC フルスケールの上・中・下 各1/3で出力します。電源ノイズの電圧依存を確認します。
アンプ出力(赤): LPF 経由で測定します。飽和の有無を確認します。
疑似負荷出力(緑):このパルスのタイミングで、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 で再生すると、量子化ノイズが増加し、アナログ電話のような音質になります。
先の2つの対策では、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 を超える実質分解能を実現します。
ΔΣ変調とは
ΔΣ変調は、入力の振幅情報を、時間軸の密度情報に変換します。限られた出力分解能で、それを超える実質分解能が得られる技術です。以下に、一般的な「ワンビットΔΣ変調」と、今回利用する「マルチビットΔΣ変調」の構成例・出力例を示します。
ワンビットΔΣ変調
PDM (Pulse Density Modulation) や DSD (Direct Stream Digital) の説明でよく見かける図です。入力と前回出力との差分(Δ)を、繰り越し(Σ)、量子化器 (Quantizer) で2値化して出力します。その出力は、次回の入力との差分演算に再利用します。これにより、入出力の誤差を常に打ち消すような負帰還がかかります。結果的に、入力の振幅情報は、時間軸を使った2値 = ワンビット の密度情報に変換された出力となります。9 10
マルチビットΔΣ変調
ワンビットΔΣ変調との違いは、 Quantizer が多値出力であること、最終出力が低ビットDAC出力であることです。低ビットDAC出力の各階調で、ワンビットΔΣ変調を行うような動作となります。11
変調の動作例
「整数出力のみが許されたシステムで、何としても 365.25 という実数相当を表現せよ」という例題を、ΔΣ変調で解決する方法を考えます。先のマルチビットΔΣ変調のブロック図に、例題の要素を当てはめます。入力は 実数 365.25 固定12、ΔΣは実数演算、Quantizer は実数→整数変換手段となります。
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);
}
}
実行結果
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 とします。
ΔΣ変調は、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
ΔΣなし: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、M5Stack Basic17 で動作確認済みです。
3.1 ソフトウェアと周辺ブロック図
3.2 ソースコード解説
基本構造は文献1を参照しました。各対策処理のご本尊は、Filter_Process 関数内にあります。
音割れ・DACノイズ対策処理
一般的な PCM 音源データは、符号付き整数( int )型 です。ESP32 の 8bit DAC データ形式は 符号なし整数( unsigned )型 です。このため、各データの符号 bit を反転して符号付き→符号なし 変換を行います。次に、ステレオのL/Rデータを平均してモノラル化します。これを float 型に変換し、 Gain / Offset 処理をかけ、DACレンジの中央 1/3 や下1/3で出せるようにします。
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を使用しました。
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をマスク(ゼロデータ化)しています。
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. 効果確認
ソースコード例を使用して、各対策前後の出力を測定し、その効果を確認します。測定トポロジーは、ソフトウェアと周辺ブロック図 を参照願います。
4.1 効果全貌
アンプ出力を、外付けLPF 経由で PC の Audio LINE に入力し、音声編集ソフトAudacity で振幅を測定します。音源は Decay Sin を使い、大振幅での飽和状態、小振幅でのDACノイズ限界や分解能限界を確認します。左から「対策なし」「音割れ対策」「DACノイズ対策」「分解能対策」と3つの対策を重ねていった結果です18。上段は振幅をリニアに、下段は振幅を対数表示(㏈)したものです。横軸は「秒」です。
対策なし : 開始直後の大振幅において、波形ピークが飽和しています。音源が小振幅になるにつれ、振幅表現が乏しく、段差状になっています。やがて音源の振幅が 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 で信号の振幅・周波数特性を観測します。上段は時間軸、下段は周波数軸です。確認を容易にするため、周波数軸は+40dBシフトしています。音源は、Zero (無音)です。
Mid 1/3 (対策前):DAC中央1/3で「無音」を出力した結果です。「無音」であるはずですが、ノコギリ波状の3~4kHzのノイズが確認できます。周波数軸では、50Hz、3~4kHz付近、44.1kHzにピークが見られます。
Low 1/3 (対策後) : DAC下側1/3で「無音」を出力した結果です。Mid 1/3 に比べ、ノイズ振幅が減衰しています。周波数軸では、帯域全体でノイズが約9dB下がっています。これは DACノイズ対策 で解説したノイズ低減効果そのもので、聴感上もはっきりと効果が判ります。
参考:ノイズ要因
50Hz : ソフトウェア処理周期のノイズです。44.1kHz の音源を、882 サンプル単位でバッファ処理していますが、この周期が 44100 / 882 = 50[Hz] となります。サンプル単位数を変えると、この周波数も変わります。聴感上認知しにくいよう、なるべく低い周波数にするのが良いようです。
3~4kHz 付近 : 周期変動を持つ下降ノコギリ波形であること、USB給電をすると止まることから、昇圧DCDCコンバータ由来のノイズと思われます。詳細解析には至っておりません。聴感上は「ギー」という不快な音に聞こえます。
44.1kHz : LCDバックライトPWMの周期ノイズです。setup関数内で ledcWriteTone(7, SRC_FS );
として音源の fs と同じ周波数を指定し、可聴帯域外に移動しています。本対策は、文献3経由で lovyan03 さんの対策案を参考にしました。
4.3 分解能対策効果
ΔΣ変調の効果を周波数軸で確認します。測定条件は前項と同じです。音源は-26dB Sinです。
ΔΣ変調なし(左):基本波1kHz の奇数倍の量子化ノイズが発生しています。
3次ΔΣ変調(右):ノイズシェーピング効果により、量子化ノイズ成分が可聴帯域(20kHz)より高域に移動しています。ノイズまみれの波形からは想像しづらいですが、聴感上、時報音のような「澄んだ」音になります。
4.4 まとめ
最後に「DACノイズ対策」と「分解能対策」効果を、アンプ出力波形( LPF 経由)で確認します。音源は-26dB Sinです。
「対策なし」:DACの分解能に迫る大きさの電源ノイズが生じています。
「DACノイズ対策」:電源ノイズが小さくなり、DACの分解能があらわになりました。
「分解能対策」:量子化ノイズが減り、きれいな Sin 波形、きれいな再生音になりました。
以上、DAC下1/3出力による「DACノイズ対策」と、マルチビットΔΣ変調による「分解能対策」の効果が確認できました。
課題
LCD 描画時や SD カード上の Wav File 再生時、小さく「ギョロギョロ」という音が聞こえます。これは SPI バス動作時のノイズなのですが、DAC ノイズ対策でも思ったほどに低減できませんでした。これは、文献3で分析されているように、SPI系GPIOから DAC1への輻射が関係しているかもしれません。DAC1 の出力インピーダンスが高く、ノイズを拾いやすい可能性があります。他にも、「ΔΣアイドルトーン19」「高OSRでノイズ増加20」などの課題があり、別の機会に調査したいと思います。
参考:対策処理のCPU占有率
Filter_Process 関数を micros 関数で挟み込み、対策処理全体のCPU占有率を確認しました。CPU 240MHz の場合、最大17.2%と軽負荷です。CPU 80MHz の場合は、50%を超えてしまう場合があり、他のアプリケーション・タスクとの共存には配慮が必要です。ΔΣ次数を減らす、オーバーサンプリングのLPFフィルタ処理を省略する等の工夫が必要でしょう。
5. おわりに
M5Stack は、本当に素敵なハードウェアです。私もいっぺんに虜(とりこ)になってしまいました。しかし、今回の DAC やスピーカーのように、回路デザインにクセがあって、なかなか思い通りに動いてくれないところもあります。そこがまた愛しく、工夫のし甲斐があるところです。今回は、ハードに手を加えず、ソフトの創意工夫だけで、どこまでスピーカー音質の限界性能を引き出せるか挑戦してみました。
ご興味ある方は、ぜひ耳馴染んだ音源で、Wave file 再生をしてみてください21。そして、ご自身の耳で対策前後の音質を比較してみてください。主観ですが、スピーカー音質は「玩具レベル」から「小型FMラジオ(ちょっとノイズ混じり)」くらいに改善できたのではないかと思います。残ったノイズ等は、まだ改善の余地があるかもしれません。
この記事が何かのお役に立てば幸いです。
なお、M5Stack / ESP32 で「DAC下1/3出力でノイズを低減する」「マルチビットΔΣで分解能を改善する」前例を調査しましたが、今のところ見つかっておりません。もし、前例をご存じでしたらお知らせください。
参考文献
Philips TDA1540 16-BIT D/A conversion system (Philips, 1984) ~ DutchAudioClassics
CD (コンパクトディスク) 黎明期の1984年。まだ半導体プロセスが未熟で、CD規格にフィットする 16bit DAC の製造が簡単ではなかった頃の論文。14bit DAC と ノイズシェーピング処理 IC の組み合わせで、16bitを超える性能を得るというもの。このチップセットを搭載したCDプレーヤー Marantz CD-34 の音質は、現代でも通用するほど素晴らしく、今でもそこそこの価格で取引されていたり、専門の修理職人が活躍されたりしています。私も CD-34 オーナーです。
限界性能への挑戦と音質へのこだわり:河合 一 様 ~ 日本テキサスインスツルメンツ株式会社
マルチビットΔΣ DACの説明があります。最近の Audio DAC には積極活用されているようです。
「ΔΣ変調」の解説(1) ~ しなぷすのハード製作記
少し詳しいΔΣ変調 ~ electric ホロン様
線形・非線形ロバスト制御理論を用いたデルタシグマ変調器の設計手法 : 喜田 健司様 ~ 九州大学学術情報リポジトリ
ΔΣ変調、ノイズシェーピング効果の原理・効果を深く知りたい人向け。喜田様の論文は高次ΔΣ変調の理解と実現に役立ちました。
階段グラフのつくりかた(カクカクな ~ おっ
Excel で DAC 出力をそれっぽく描く方法の参考。誤差表示を使うというのが目からウロコ。
利用ツール
Audacity
Windows用オーディオ編集ツール(フリーソフト)。秀逸です。
WaveSpectra - efu's page
Windows用オーディオアナライザ(フリーソフト)。秀逸です。
Converting Analog into Digital (IIR) Filters - Manually or by AnaDigFilt.exe - - The Electronics Section of Beis.de
オーバーサンプリング用 18-22kHz IIR LPF 設計に利用。
RLCローパス・フィルタツール - OKAWA Erectric Design
D級アンプのPWM出力をアナログ振幅に変換するための外付けRLC LPF設計に利用。
SideBB for M5Stack ~ スイッチサイエンス
M5Stack の信号計測や外付けRLC LPFの構築に利用。ちょっとした実験にとても便利です。
ソースコード例
IDE : Arduino IDE 1.8.13
ターゲットボード : M5Stack Basic17 / M5Stack FIRE
// 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 );
}
}
注釈
-
当方調べ ↩
-
アンプのゲインは、アンプの製造バラつき・動作温度などのアナログ要素で変化することがあります。 ↩
-
ノコギリ波は、非常に大きく耳障りな音が鳴ります。同じ実験をされる方は、周囲へのご迷惑・過負荷によるスピーカーの焼損にご注意ください。 ↩
-
アンプ出力が緩やかなカーブを描いているのは、DAC~アンプ間のコンデンサ : C43 の充放電特性によるものと思われます。 ↩
-
あくまでも類推した概念図であり、M5Stack や ESP32 の構造を正確に表したものとは限りません。 ↩
-
M5Stack では
dacWrite(25, 0)
と記述し、DAC出力を最小レベルにしてスピーカーノイズを低減する方法が知られていますが、まさにこの原理を利用したものといえるでしょう。 ↩ -
GPIO18~23 は VDD3P3_CPU に所属する端子です。これらの GPIO は、M5Stack 内部で LCD・SDカードのアクセスに利用されています。 ↩
-
残念ながらM5Stack 回路デザインの問題です。CPU や GPIO が動作する限り、DACノイズが不可避な構造です。さらに、電源がバッテリ~5V昇圧DCDCコンバータ~3.3V降圧DCDCコンバータ型と2段構成で、そもそも電源がノイジーです。負荷電流の変化によって、DCDCコンバータの充放電周期が変調され、広帯域なノイズとなる課題もあります。せめて VDD3P3_CPU と VDD3P3_RTC が別電源であったならば…と思いますが、コスト・サイズ・歴史的背景等、様々な理由あっての現行デザインなのでしょう。黙って使いこなす他ありません。 ↩
-
説明図はデジタル入力~アナログ出力を前提としています。構成によっては入力、ΔΣ 手段、 Quantizer がアナログ構成であったり、2値出力をアナログ信号に戻す 1bit DAC が含まれる場合があります。 ↩
-
高い実質分解能を得る場合、高い fs (サンプリング周波数) が必要になります。DSD の場合、元の音源の 64倍以上の fs が採用されています。また、高次のΔΣ変調は発振しやすいため、配慮が必要です。信号の再現性は処理クロック=時間の正確さに依存します。 ↩
-
ワンビットΔΣ変調に比べ、fs (サンプリング周波数) を低くできたり、高次のΔΣ変調でも発振しにくい特徴があります。再現性は処理クロックの正確さと出力手段の DACのリニアリティに依存します。 ↩
-
この例では、単純化のために固定入力で説明しています。音声信号のような可変入力であっても、同様に処理が可能です。 ↩
-
ΔΣの仕組みを直感的に理解いただくため、あえて「うるう年」の概念に寄せた例としました。実際の地球の平均周回日数は、365.242189...日/年です。グレゴリオ暦の「うるう年」は、社会システムに馴染みやすい規則(西暦が400で割り切れる、または4で割り切れて100で割り切れない年) となっています。ここで扱うΔΣ変調の考え方とは違います。 ↩
-
効果説明のために、8bit DAC の既存Dレンジ/bit数を正領域にシフトしています。 ↩
-
LPFは波形観測のための手段であって、実際の信号処理には実装されません。 ↩
-
ΔΣ変調は大変奥が深く、私も理論を未消化のまま紹介しています。間違い等ありましたら、ご指摘願います。また、ESP32 の I2S内部には、ワンビットΔΣ変調を行う PDM が搭載されています。M5Stack のスピーカー音質改善に利用できるかは未検討です。出力が0V - 3.3V であるため、M5Stack ではアンプの飽和領域で利用することになります。 ↩
-
M5Stack Basic で、まれに SDカード の wave file 再生が不安定になる場合があります。SDカードにも相性があるようですが、再現性に乏しく究明できておりません。FIREは問題ないようです。CPU周波数は、240MHzよりも80MHzのほうがDACノイズ低減に有利ですが、確率的に 80MHzで SDカードが不安定になりやすいようで、この対策のため
setCpuFrequencyMhz(80);
はコメントアウトし、240MHz 動作としています。 ↩ ↩2 -
各対策の測定波形を編集したものです。ソースコード例には、対策処理を自動的に切り替える機能はありません。 ↩
-
音源を有音から無音(ゼロデータ)に切り替えた場合、ΔΣ変調ループ内の残データによってアイドルトーンが発生する場合があります。微小なノイズデータを与え続けることで、アイドルトーンを「散らす」ことが出来ます。今回は未対策です。 ↩
-
ΔΣの特性上、OSR は高いほうが有利です。調子に乗って OSR=8 (DAC_FS=352.8kHz) を試しました。動きますが、却って可聴帯域のノイズが増加しました。原因は未特定ですが、M5Stack のD級アンプのキャリアが400kHz付近であり、DAC_FSの352.8kHzと近く、折り返しが発生している可能性があります。 ↩
-
静かめで、減衰音が多いピアノソロ曲などで効果がわかりやすいと思います。 ↩