ATtiny13, 抄一個吉他調音器, 開源的 guitar tuner DIY


https://wp.me/ph3BR-2RA

 

諺語曰 : 若要人似我, 除非兩個我

辛棄疾曰 : 眾裡尋千百度, 驀然回首, 那人卻在燈火闌珊處

能找到生平想要的, 是一件幸福的事情

找到然後又明白了為何找得到, 或許發現那是早就註定好的, 找到的原因是偶然或者必然, 想得到又或者想不到, 都是思考的過程.

 

這個故事, 也是遵循了 “除非兩個我的定律", Autocorrelation (自相關性的算法), 現實中不可能有兩個我, 但是可拍張照片, 馬上比對一下真人, 就可以看到 “相似的兩個我". 越大範圍供對比用途, 認得可能性就愈高, 相似度越高, 隨著可比對的範圍逐漸縮小, 認得 “兩個我" 的可能性慢慢降低, 最後是完全不相關 (面目全非). 過程就重複一直在找尋很相似跟不相似兩種答案. 為了瞭解算法, 做了一個陽春的工具, 一直追尋內積和的變化規律, 圖畫比起個別數字更容易讓人感覺出來像/不像兩種截然不同的感覺, 為了更好重溫模仿, 也可以用印表機打印然後剪紙快速做成算法模擬的工具, 可以下載,

xiaolaba_autocorrelation_demo_tool.pdf

xiaolaba_autocorrelation_demo_tool.xlsx

使用方法跟視頻實驗的大致一樣, 設計的第二版還是厚紙板做的, 打印的第三版表格也一同備好, 以後就用這個, 除了SINE WAVE, 其實三角波, 方波都一樣適用. 剪紙的方法如圖所示, 印出來, 剪紙, 開一條縫插卡移動, 取樣點讀值做內積(乘法), 每行加總.

 

 

 

第一次做厚紙板的版本, 比較不好做.

 

看看到底有多相似的過程, 手工餅乾, 慢動作分解, 每4份間隔就出現極大值或極小值, 剛剛好對應原來訊號的取樣頻率.

 

 

 

 

故事開始, 有關一個 ATtiny13 吉他調音器. 先改好源碼, 抄好電路圖, 加點鹽加點醋, 小改一下. 大約20年前人家的設計, 買不到AT90S2323, 改用 ATtiny13,  看起來1K FLASH 夠用的.

 

 

源碼和燒綠檔案

https://github.com/xiaolaba/Guitar-tuner

 

燒綠好韌體的 IC 可以聯絡這裡購買

 

原作者的設計概念, 下載連接已經斷了, 自己存檔一個, Jesper’s_AVR_pages-Guitar_Tuner_sch.pdf

作者說明運作原理:

  • NPN BC547 是輸入訊號放大器, 靈敏度大約50mV-60mV, 聲音訊號放大約40倍至2-3V, 傳遞到 MCU 的 ADC 讀取.
  • MCU 的計時器速度為173KHz, 每次上邊緣讀取計時器的值, 累計32次, 求得平均值.
  • 用平均值比對 Transistion_Count 表格內, 判讀最接近調音.
  • 再用平均值比對 Center_Count 表格內, 判讀調音的準確度 (頻率的偏差).
  • LED_HI 和 LED_LO 會分別顯示結果, 如果兩個 LED 同時亮起, 表示調音準確.

大概解讀這樣, 不明白的地方還有很多. 例如, 計算頻率的原理.

 

 

另外一個設計, 也是用MCU做的, 也有源碼也有影片, 含水量估計不會大也會有營養. 據說是運用 AutoCorrelation 的原理, 當然也是看了還沒完全明白, 不過他用的 ATMega328, FLASH 容量是 16K, 比ATTINY13 大16倍, 當然也會有興趣看看能不能用 ATtiny13 來做實驗, 第一件事, 看看源碼, 看看能不能減肥縮小編碼的體積. 原文和源碼在這連結, https://arduinoguitartuner.wordpress.com/code/

 

既然是用 Arduino UNO (MCU 是 ATMega168 / ATMega328) 做的, 直接開工先編譯源碼看看有沒有問題, 因為是近幾年內的寫碼(2018年的學生課業), 估計編譯踩到坑的機率很低, 果然, 編譯毫無問題. 作者的視頻和設計解說, 有標明是參考另外一個 DSP (AutoCorrelation) 的設計的, 如以下,

http://www.akellyirl.com/reliable-frequency-detection-using-dsp-techniques/

http://www.akellyirl.com/arduino-guitar-tuner/

 

俺編譯源碼的結果, 用 Atmega168, FLASH ROM 用掉 6824 佔總容量的 42%, RAM 用掉 751 佔總容量的 73%

空間夠大, 隨便寫都可以, 所以作者用了平舖和直下的方式, 每個調弦都有同樣的動作編碼, 調音太低/太高/剛好, 顯示亮起來三個不同的LED, 俺自己記註,

  • // lite LED LOW
  • // lite LED HI
  • // lite LED TUNED

所以吉他調音一共有6跟弦, EAGDBE, 一共重複這樣的編碼6次, arduino avr-gcc 編譯有沒有自動優化減低編碼空間, 不清楚, 試試看手工減肥. 原作片段如下,


//Elow Note 83.21Hz
if ( ( (freq_per >75)&&(freq_per < 85) )|| ( (freq_per>159)&&(freq_per<170) ) ){ // For Elow string
digitalWrite(ledPinEl, HIGH);
//delay(1500); // wait for a second
//digitalWrite(ledPinEl, LOW);

if (freq_per<80.4||(freq_per<161.91&&freq_per>159)){
digitalWrite(ledPinRedLeft, HIGH); // lite LED LOW
delay(1500); // wait for a second
digitalWrite(ledPinRedLeft, LOW);
}
if ((freq_per>84.4&&freq_per < 85)||(freq_per>167.91&&freq_per<170)){
digitalWrite(ledPinRedRight, HIGH); // lite LED HI
delay(1500); // wait for a second
digitalWrite(ledPinRedRight, LOW);
}
if ((freq_per>=80.4&&freq_per<=84.4)||(freq_per>=161.91&&freq_per<=167.91)){
digitalWrite(ledPinGreen, HIGH); // lite LED TUNED
delay(1500); // wait for a second
digitalWrite(ledPinGreen, LOW);
}

digitalWrite(ledPinEl, LOW);
}

把點亮調音太低/太高/剛好的三個不同的LED的工作抽取出來, 這樣寫,

void lite_LED (int ledPin){
     digitalWrite(ledPin, HIGH);
     delay(1500); // wait for 1.5 second
     digitalWrite(ledPin, LOW);
}

 

然後修改每根調弦的亮燈細節, 例如以下其中有關一條弦的部分, 類似共有6組弦, 頻率不同而已.

   if (freq_per<80.4||(freq_per<161.91&&freq_per>159)){
     lite_LED(ledPinRedLeft); // lite LED LOW
   }
   if ((freq_per>84.4&&freq_per < 85)||(freq_per>167.91&&freq_per<170)){
     lite_LED(ledPinRedRight); // lite LED HI
   }
   if ((freq_per>=80.4&&freq_per<=84.4)||(freq_per>=161.91&&freq_per<=167.91)){
     lite_LED(ledPinGreen); // lite LED TUNED
   }

結果, 編譯後用了 40% 的FLASH ROM 空間, -2% 沒什麼改善, 只是讀起來容易了一點點.

那再看看源碼寫什麼, 有關音樂, 每個音的基礎.

  • 現代音樂有七個全音, 1234567, 唱名 do, re, mi, fa, so, la, si
  • 升高到下一級也是同樣唱 do, re, mi, fa, so, la, si, 每個數字頭頂有個點, 這裡沒辦法顯示. 這樣每次重複提高一組就算八度, 該是8個全音就重複的意思,
  • 每個音提高8度後, 震盪頻率就增加一倍, 屬於等比級數,
  • 1234567 音名是 CDEFGAB, 不知道為何是 C 開頭而不是 ABCDEFG, 反正記住就可,
  • 1234567 之間還有5個半音, 一共12個半音, 例如用下滑線表示可以看成 1_2_34_5_6_7
  • 12個半音用音名表示, C, C#, D, D#, E, F, F#, G, G#, A, A#, B (# sharp, 升半音)
  • 12個半音用音名表示, C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B (b flat, 降半音)
  • 所以樂音的階梯一共有12級, 每一級是一個半音 (3-4之間是半音, 7-1之間也是半音, 直到摸過琴鍵看書以後才明白)
  • 1_2_34_5_6_71_2_34_5_6_7, 表示24個半音, 12 x 2 = 兩組
  • 現代音樂每個半音的頻率比前一個音高出 2^(1/12), 讀做 2 的12次開方, 不要問為什麼, 人類感覺出來的東西就這樣, 記得就好

 

不論基礎的頻率是多少, 隨便選一個頻率的數, 只要維持每一級往上的頻率差別是 2^(1/12), 人類就可以感覺到do, re, mi, fa, so, la, si 的聲音, 上次做合成器的時候已經感受過.

例如把頻率提高一倍, 220Hz x2 = 440Hz, 440Hz x2 = 880Hz, 聽到的都是同一個音 A (La), 只是感覺樂音的高低差別.

描述很多, 直接自己算一次, 結果就是人家寫的一樣, 吉他開放弦的調音,

E2, A2, D3, G3, B3, E4

頻率算出來這樣,

frequency (Hz) Note name Guitar A440 standard tuning
82.41 E2 string 6
110.00 A2 string 5
146.83 D3 string 4
196.00 G3 string 3
246.94 B3 string 2
329.63 E4 string 1

 

 

以下源碼的片段, 意思就是,

如果聽到的頻率比 105.61Hz 大, 同時小於 115.61Hz, 那就判斷接近 A2 (110Hz) 這個音, 該通知調音的大師, 亮燈什麼的. 太專業的名稱有些嚇人, 其實誰人在做都可以被稱為啥啥師的, 名稱而已, 玩具一般, 知道就笑死, 不知道就嚇死.


//A note (110Hz)
// musical A2 note
if ( (freq_per >105.61)&&(freq_per < 115.61) ){

 

繼續研讀怎樣偵測頻率的算法, 基本上就是 DFT, 以前學過的 DTMF 偵測雙音辨號的同樣原理, 把 time domain 的聲音訊號, 轉化成 Frequency domain, 再看每個尖峰的間隔, 用取樣頻率的倒數找回頻率, 算法暫時看不明白不重要, 那就用慢慢慢慢動作模擬一次, 自然就會明白算法的含意, 再配合內積和的概念 (共變異數 Covariance), 就可以得到頻率分布, 只要是同相的週期性的訊號都會被放大, 等於乘法器(調製) 的概念.

慢慢慢慢動作 的結果,

 

如果還是不明白, 再做一個超級慢動作的分析看看, 隨便自己捏造一個三角波的函數, 取樣就是每一間隔, 高度是 0 / 0.5 / 1, 所以週期就是8個間隔, 每8次就重複, 最簡單的波形做分析用. 因為是單一個訊號自相關性的計算, 就是用訊號本身自乘求和, 逐步移相 (向左向右結果都一樣). 再把每一次的總和數 (sum) 擷取出來畫個圖, 就看到最大值依然每8次就出現, 證明了自相關性求和可以找到重複週期的訊號.

左移相

右移相

 

求和相關性比較, 一樣的結果, 因為是單一個函數自己的相關性, 當然, 這個訊號自己跟自己基本上是100%類似的, 所以這個數學工具才能用來找出週期訊號的頻率, 原理就是正交和積 (dot product 沒記錯大該是這個名稱). 參考看看李永樂老師的FFT視頻解釋

另外也有全視頻化的解說 Fourier Transform

 

實際操練, 原作者說是鋼琴 C4 琴音的錄音檔的訊號, 左邊是 DFT 的結果, 右邊是原聲資料, 看得出來紅色的繪線幾個突出的尖峰之間的距離就是頻率的倒數, 只要知道取樣頻率22.05KHz, 就可以算出來尖峰之間的頻率, 這個算法搭配 avr-gcc 算出來是 259.41Hz, 跟這裡寫的鋼琴C4頻率261Hz差不多, 自己用EXCEL算到的 C4 是 261.63Hz, 所以真的差不多. 右邊藍色線段是原始音訊資料, 完全是看不出來有特別訊號在裡面.

實驗的源碼是這樣改的,


// Serial.print("i="); Serial.println(i,DEC);

for(k=0; k < len-i; k++) {
sum += (rawData[k]-128)*(rawData[k+i]-128)/256;
/*
//Serial.print("[0x73, 0x7E, 0x3E, 0x39, 0xB0] ");
Serial.print(" rawData[k="); Serial.print(k, DEC); Serial.print("]=0x"); Serial.print(rawData[k],HEX);
Serial.print(" rawData[k+i="); Serial.print(k+i, DEC); Serial.print("]=0x"); Serial.print(rawData[k+i],HEX);
Serial.print(" sum += (rawData[k]-128)*(rawData[k+i]-128)/256 = "); Serial.println(sum,DEC);
*/

}

// plot the raw data and sum, visual DFT result and the frequency domain
Serial.print(rawData[k]-128,DEC);
Serial.print(",");
Serial.print(sum,DEC);
Serial.print("\r\n");

 

另外, 解說 Autocorrelation 計算效率的手工方法, 用了不同的觀點, 普通的乘法, 加上右移位的做法, 其實是一樣的結論, 但是純粹手工, 例子, 數組 2,3,-1, 自相關計算結果是數組 -2,3,14,3,-2, 共有5個數以14為中心兩邊對稱的數組, 忽略左邊兩個數, 保留三個數就可得到電腦算法一樣的結論, 14,3,-2, 看圖容易記得. 如果看迷糊了, 忘記 Autocorrelation 的用途, 重溫一下, 這個數學工具可以把一串序列 (一堆) 數目找出週期性的資訊.

wiki 的中文解說看來很貼切. 例如,


自動相關(英語:Autocorrelation),也叫序列相關[1],
是一個訊號於其自身在不同時間點的互相關。
非正式地來說,它就是兩次觀察之間的相似度對它們之間的時間差的函數。
它是找出重複模式(如被噪聲掩蓋的週期訊號),或識別隱含在訊號諧波頻
率中消失的基頻的數學工具。它常用於訊號處理中,用來分析函數或一系列
值,如時域訊號。
 

 

動態顯示 Autocorrelation, 人家有做過, http://qingkaikong.blogspot.com/2017/01/signal-processing-how-autocorrelation.html, 經由動態顯示, 可以看到算法設計和運作過程的關係, 假設最簡單一個週期訊號某時刻的大小是1, 另外一個時刻是0, 然後重複, 不論左移還是右移, 用小學做過最簡單的豎乘法寫出來, 結果求和加總, 每次得到的數字會隨著移動距離改變, 每次移位, 峰值重合時乘法結果必然最大, 加總數也必然最大, 相反地, 峰值鏡像時時乘法結果必然最小, 加總數也必然最小.

 

 

實驗到這裡, 大概完全理解 Autocorrelation 的原理和用意, 作者的錄音取樣數據大約 1000 BYTE, 速度 22.05KHz . 餘下的一個問號, 實際用 ATmega168 的 ADC 讀取聲音訊號並且取樣時, 速度越高, 取樣資料越多, RAM 不夠就不行, 所以還是需要仔細考慮 ATtiny13 的小容量 RAM 只有 64 BYTE 是否滿足需要. 另外一個作者, 用 ATtiny 13 作了 DFT, 繼續讀書看看人家怎樣設計的, 也是開源的, 參考這裡 https://blog.podkalicki.com/attiny13-dance-lights-with-dft/

 

 

 

當然看了有很多謎團在心中, 因為人家肚子裡早有墨水, 俺需卻要慢慢學習才可以明白其中的原理. 最近一個業務妹妹, 常常有問題要答案, 可是不願意思考, 就算是開源文件的也不屑一看, 一心只想要終極答案, 或者給她代工做好. 給了答案給了指引又不願意跟隨, 很有個性很有主見按照個人意願自行處理, 回頭碰壁了又說這不懂那不明白的, 繼續要答案繼續要協助. 舉個例子, 小學生學習1+1, 必須要說明為何要叫她做1+1的算術目標, 她才願意練習2+2, 天兵一名. 再舉個例子給她, 若兒子等於客戶, 思考一下若兒子來說肚子餓, 已經是兩個兒子的她當媽的該如何反應, 回答是, “我兒子很特別, 不用管, 他們自然會找吃的", 那只能對她說, 客戶來找也不用管, 他們也很特別, 他們自然會找到願意為人著想和思考的業務員, 大家都下班回家吧. 所以開源不代表人人都能明白, 除非自己想辦法讀懂那些東西, 融為己用.

 

銷售, 炒股票, 也可以學習 Autocorrelation, 台灣清華大學有教學影片, 可以看看這個教授的一系列影片, 免費的教學, 用台北捷運的運量作為模型的教學. 用作 Forecast, 預測短期未來的趨勢.

 

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.