音質の問題
前回、ESP32のタイマ割り込みを使って波形を正しくPWM変調させて出力させることまでやったものの、あまり音質が良くないよなと思っていました。
ESP32では量子化ビット幅11bitだとPWM周波数は約39kHzが上限です。もちろん半分の18.5kHzというのは可聴周波数域のかなり上のほうにはなってますが、実際に聞こえる音は結構ノイジーです。歪とは違う雑音が聞こえてきます。どうなっているのか、実際に出力している音を FFT で分析してみましょう。
外付けローパスフィルタ
PWMの出力に R1とC1からなる簡単なLPF(low pass filter)をつけています。カットオフ周波数は約15.9kHzです。C2は直流成分をカットするためのキャパシタです。無信号時に 0V にするために付けます。
11bit, 39kHz PWM
正弦波 440Hz の音を、量子化ビット11bit, PWM周波数39KHz で上のLPF通過後の波形をFFTにかけるとこうでした(赤線は peak)。
-60~-70dB程度のノイズがたくさん... こんなものかなと思うものの、波形がちょっと階段状になっています。もちろん、前回の波形メモリ音源(32個のテーブル)なのである程度は仕方がないです。
これをロジアナのアナログ入力で波形を見ると
あらら... 太い正弦波ですね... 何が起きているのでしょうか??
拡大すると...
PWMのサイクルに応じて細かい up/down がジグザグと入ってます。これが太く見えている元凶ですね。
LPF fc=1.59kHz
高い周波数成分を減衰するために、先の LPF の C1 を 10倍大きく(0.01uF⇒0.1uF) にしました。 カットオフ周波数は 1.59KHzになり、かなり高音が抑制されるはずです。
7kHz以上は目立って右肩下がりで高音域を抑制しているのは分かります。
正弦波のガタガタもだいぶきれいになりました。
PWMのスイッチングノイズもかなり平坦化されているのがわかります。でもこれは LPF で通常の高音も抑制してしまうこととになり、やりたいこととは違うんですよね。
D級アンプなどの仕様を色々調べると、PWMの周波数は数百KHzとか (39KHzより) もっと高いところで使っているのですね。LPFで減衰してもPWMのスイッチングノイズは出てしまうのですが可聴範囲外の高いところでスイッチさせるなら(あまり)聞こえないでしょ、ということですね。
いままので私の試行では、11bit を確保するために PWM周波数を下げていたのでPWMのノイズが聞こえてきてしまっていました。では量子化ビット数を下げてでも PWM周波数を上げたらどうなるか調べてみましょう。
8bit, 312kHz PWM
ESP32では、量子化ビット8bitならPWM周波数は312KHzに上げられます。
// PWM tone with timer interrupt
// LED output on GPIO32 (A4 pin)
# define LEDC_PIN A4
# define LEDC_CHANNEL 0
# define LEDC_FREQ 312000
# define LEDC_TIMERBIT 8
// tone definition
# define TONE_A1 18181818 // A1 (55.000Hz)
# define TONE_A2 9090909 // A2 (110.000Hz)
# define TONE_A3 4545455 // A3 (220.000Hz)
# define TONE_A4 2272727 // A4 (440.000Hz)
# define TONE_A4_ 2267574 // A4 detuned (441.000Hz)
# define TONE_C5 1911129 // C5 (523.251Hz)
# define TONE_Db5 1803866 // C#5 (554.365Hz)
# define TONE_E5 1516865 // E5 (659.255Hz)
# define TONE_Ab5 1203936 // G#5 (830.609Hz)
# define TONE_A5 1136364 // A5 (880.000Hz)
# define TONE_A6 568182 // A6 (1760.000Hz)
# define TONE_A7 284091 // A7 (3520.000Hz)
# define TONE_A8 142045 // A8 (7040.000Hz)
// WAVE TABLE LENGTH
# define WAVE_TBL_LENGTH 32
// TIMER INTERVAL (ns)
# define INT_FREQ 25000
// MAX NOTES
# define MAX_NOTES 1
hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;
volatile int tonec[MAX_NOTES] ;
volatile uint8_t tbl_index[MAX_NOTES] ;
volatile int count_tone[MAX_NOTES] ;
volatile int pwm_tone = 0;
uint8_t wave[WAVE_TBL_LENGTH] ;
void IRAM_ATTR onTimer(){
portENTER_CRITICAL_ISR(&timerMux0) ; // exit critical range
pwm_tone = 0;
for ( int i=0; i<MAX_NOTES; i++ ) {
count_tone[i] -= INT_FREQ*WAVE_TBL_LENGTH ;
while( count_tone[i] <=0 ) {
count_tone[i] += tonec[i] ;
tbl_index[i] ++ ;
}
tbl_index[i] &= (WAVE_TBL_LENGTH-1) ;
pwm_tone += (int) wave[tbl_index[i]] ;
}
ledcWrite(LEDC_CHANNEL, pwm_tone) ;
portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}
void setup() {
Serial.begin(115200);
int i ;
// intialize wave table (Sine)
for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
wave[i] = (uint8_t) 128+((double) 127*sin((float) 2*3.14159265*i/WAVE_TBL_LENGTH)) ;
}
// initialize counter and preset tones
for (i=0;i<MAX_NOTES;i++) {
tbl_index[i] = WAVE_TBL_LENGTH -1;
count_tone[i] = 0;
}
tonec[0] = TONE_A4 ;
ledcSetup(LEDC_CHANNEL, LEDC_FREQ, LEDC_TIMERBIT) ;
ledcAttachPin(LEDC_PIN, LEDC_CHANNEL) ;
Serial.println("======= start timer ============");
timer0 = timerBegin(0, INT_FREQ/12.5, true); // timer 0, 12.5ns*1000 = 12.5us, count up
timerAttachInterrupt(timer0, &onTimer, true); // edge (not level) triggered
timerAlarmWrite(timer0, 1, true); // 12.5us * 1 = 12.5us
timerAlarmEnable(timer0); // enable
}
void loop() {
vTaskDelay(portMAX_DELAY);
}
波形の階段は見えますが、聞いた感じでは11→8bit にした分の音質劣化は分かりませんでした。 波形メモリの粗さが原因ならば、440Hz程度では差が出ないのでしょうね。
PWM周波数を39kHz⇒312kHz に大幅に引き上げたことで、PWMスイッチングノイズも細かく小さくなっているのがわかります。
さらにダメ押しでLPFもカットオフ周波数1.59kHz版にすればさらにノイズは高音とともに減衰します。
ここまでくればPWMスイッチングノイズは相当小さくなっています。
内蔵DACとおなじビット幅になってしまった。
PWM スイッチングノイズを回避するために PWM周波数を上げると 量子化ビット数は下げなければならない。結局 8bit になってしまいました。......ということは 内蔵DAC でいいのでは?
ということで、内蔵DAC (8bit)で鳴らしてみて比較します。GPIO25が DAC1 の出力ピンに固定されていますので、それに先の LPF をつけて鳴らします。
// DAC tone with timer interrupt
//
// DAC output is GPIO25
# define DAC_PIN 25
// tone definition
# define TONE_A1 18181818 // A1 (55.000Hz)
# define TONE_A2 9090909 // A2 (110.000Hz)
# define TONE_A3 4545455 // A3 (220.000Hz)
# define TONE_A4 2272727 // A4 (440.000Hz)
# define TONE_A4_ 2267574 // A4 detuned (441.000Hz)
# define TONE_C5 1911129 // C5 (523.251Hz)
# define TONE_Db5 1803866 // C#5 (554.365Hz)
# define TONE_E5 1516865 // E5 (659.255Hz)
# define TONE_Ab5 1203936 // G#5 (830.609Hz)
# define TONE_A5 1136364 // A5 (880.000Hz)
# define TONE_A6 568182 // A6 (1760.000Hz)
# define TONE_A7 284091 // A7 (3520.000Hz)
# define TONE_A8 142045 // A8 (7040.000Hz)
// WAVE TABLE LENGTH
# define WAVE_TBL_LENGTH 32
// TIMER INTERVAL (ns)
# define INT_FREQ 25000
// MAX NOTES
# define MAX_NOTES 1
hw_timer_t *timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;
volatile int tonec[MAX_NOTES] ;
volatile uint8_t tbl_index[MAX_NOTES] ;
volatile int count_tone[MAX_NOTES] ;
volatile int pwm_tone = 0;
uint8_t wave[WAVE_TBL_LENGTH] ;
void IRAM_ATTR onTimer(){
portENTER_CRITICAL_ISR(&timerMux0) ; // exit critical range
pwm_tone = 0;
for ( int i=0; i<MAX_NOTES; i++ ) {
count_tone[i] -= INT_FREQ*WAVE_TBL_LENGTH ;
while( count_tone[i] <=0 ) {
count_tone[i] += tonec[i] ;
tbl_index[i] ++ ;
}
tbl_index[i] &= (WAVE_TBL_LENGTH-1) ;
pwm_tone += (int) wave[tbl_index[i]] ;
}
dacWrite(DAC_PIN, pwm_tone) ;
portEXIT_CRITICAL_ISR(&timerMux0) ; // exit critical range
}
void setup() {
Serial.begin(115200);
int i ;
// intialize wave table (Sine)
for ( i=0; i<WAVE_TBL_LENGTH; i++ ) {
wave[i] = (uint8_t) 128+((double) 127*sin((float) 2*3.14159265*i/WAVE_TBL_LENGTH)) ;
}
// initialize counter and preset tones
for (i=0;i<MAX_NOTES;i++) {
tbl_index[i] = WAVE_TBL_LENGTH -1;
count_tone[i] = 0;
}
tonec[0] = TONE_A4 ;
Serial.println("======= start timer ============");
timer0 = timerBegin(0, INT_FREQ/12.5, true); // timer 0, 12.5ns*1000 = 12.5us, count up
timerAttachInterrupt(timer0, &onTimer, true); // edge (not level) triggered
timerAlarmWrite(timer0, 1, true); // 12.5us * 1 = 12.5us
timerAlarmEnable(timer0); // enable
}
void loop() {
vTaskDelay(portMAX_DELAY);
}
もう内蔵DACでいいのでは...?
階段状にはなっているものの
PWMスイッチングの up/down は見られないことから LPF は 元のR1, C1 のままでよくなりました。
小難しい PWM で頭を悩ますよりはシンプルに DAC でいいではないか、と思いました。
おまけ
このあと、内蔵I2Sは内蔵DACからも音を出せるという事実に気づきました。
どおりで ESP32では PWMを使ったオーディオの記事が少ないわけですね。みなさん I2S⇒内蔵DAC(8bit)で音を出しているのですね。もともと、I2S はDACを外付けしなければらならないと思い込んでいて、外付けしないで簡単に音を出したい、というのがPWM調査の動機だったので...。
次回からは I2S での発音方法を調べます。