Make: Projects|技能培養系列:進階Arduino聲響合成技術

十一月 19, 2013
Facebook
Twitter
Arduino這個平臺在許多應用上都讓人讚嘆,然而,如果談到聲響系統,許多使用者都只能停留在「嗶嗶」聲的階段,如果加深一些對Arduino的認識,會發現其實Arduino可以製造出任何您可以想像的聲波樣貌,並達到即時調整聲響輸出的效果。

基礎聲響輸出

位脈衝(bit-banging)是用Arduino製造聲響最基本的方法,只要將1個數位針腳連上喇叭,再讓針腳從高低狀態之間不停轉換,就會發出聲響了。以下就是在Arduino上的陳述:

tone()

即使不經過放大,這個輸出針腳也可以直接驅動一個小型的(4公分以下)8歐姆喇叭發出聲音。

【圖A】:峰值時距(mark time)/谷值時距(space time)/單一方波

使數位針腳在高低之間擺盪就會產生方波(見圖A),其中,高點停留時間稱為峰值時距(mark time),低點停留時間則稱為谷值時距(space time),調整這兩者之間的比例(也就是所謂的工作週期),保持聲波的頻率不變,就會影響到產出的音色了。

【圖B】

以下這個Arduino函數

analogWrite()

會輸出固定頻率(490Hz)的方波,正好可以呈現這個概念。請將您的喇叭連接到D9針腳與接地針腳(見圖B),並執行以下程式碼:


void setup() {

  pinMode(9,OUTPUT);

}

void loop() {

  for (int i=0; i<255; i++) {

    analogWrite(9,i);

  delay(10);

  }

}

這個時候,您應該可以聽到一個音高固定的聲響,但是音色慢慢地從尖而細(谷值時距較長)轉變成圓潤、類似長笛的聲音(峰值時距與谷值時距等長),最後,又回到尖而細的聲音(峰值時距較長)。

如果方波的工作週期固定,我們稱為脈寬調變(pulse-width modulated 、簡稱PWM)波。如果要製造簡單的音響效果,只要改變工作週期來改變音色就行了,但是,如果要輸出更複雜的音響效果,就需要更進階的技術。

類比與數位轉換

PWM波是數位訊號,無論高低皆然,而針對類比的生波訊號,我們就必須產生極高與極低兩個端點之間的各種可能的數值,而這正是類比數位轉換器(digital-to-analog converter、DAC)的功用所在。

【圖C】

類比數位轉換器有許多種,其中最基本的形式大概非梯形R-2R類比數位轉換器莫屬(見圖C)。在這個範例當中,有4個數位訊號輸入,分別以D0–D3來表示,其中,D0的電壓最低,D3的電壓最高。

如果D0很高,電流就必須穿過2R + R + R + R = 5R這麼高的電阻才有辦法輸出,而且,因為2R並不算是很大的電阻值,有些電流會因此漏入地表。因此,如果將D0設得很高,輸出電壓反而會小於將D3電壓設得較高的狀態,在後者的情況下,輸出值只會遭遇2R的電阻,接地的電阻卻有5R。

【圖D】

請將D0–D3設為二進位值,從0000到1111(也就是十進位的0–15),再將它快速歸零,這樣一來,應該可以做出圖D那樣三角波的效果。理論上,如果要輸出其他波形,只要以正確的頻率將正確順序的二進為數值輸入D0–D3就可以了。

不幸的是,R-2R這種數位類比轉換器並非完美無瑕,它最大的限制在於電阻值必須非常精準,這樣才能防止加成的誤差導致波形扭曲變形。此外,必須用低頻濾除器(low-pass filter)將鋸齒狀的崎嶇輸出值撫平,否則製造出的聲音會有種不和諧的金屬感。最後,R-2R類比數位轉換器使用的輸出針腳較多,這並非最有效率的做法。

雖然理論較為複雜難懂,但是以下介紹的「單位元」類比數位轉換可以輸出非常平順、高品質的聲波,這一切都只要透過一個輸出針腳、一個電阻器、加上一個作濾除用的電容器就可以了。而且這樣一來,Arduino就可以在播放音響的時候進行其他的工作。


單位元類比數位轉換原理

如果將bit-banging程式用的喇叭拆下來,改裝LED,您會發現將工作週期從0調到100%時,LED的亮度會隨著增加。其實,LED一直在以大約490Hz的頻率在這兩個極端值之間閃動,但是肉眼看來像是持續的亮著。


【圖E】

這個「撫平」閃爍間隙的現象稱為視覺暫留(persistence of vision),您可以把這個現象想像成圖E中呈現的低頻濾除器,而我們就是要用這個濾除器來撫平單位元數位類比轉換器的輸出波形。

在每個瞬間,傳進來的PWM波的峰值時距會決定Vout的電壓,舉例來說,如果峰值時距/谷值時距的比例為50:50,那麼輸出值就是輸入電壓高點的50%;如果比例是75:25,那麼輸出值就是電壓值的75%,依此類推。如果有一根Arduino數位針腳產生5V的波峰值,工作週期為50%,那麼Vout那端就會輸出2.5V。


 【圖】:如果用LED取代擴音器,會看到亮度隨著工作週期逐漸上升。

為了追求最佳的音質,PWM訊號的頻率應該越高越好。幸運的是,Arduino 可以產出高達62.5kHz的PWM波。此外,Arduino的硬體設備提供了一個方便的功能,就是在固定時間間隔根據數值表即時更新峰值時距,在此同時,Arduino就可以繼續進行其他工作。


Arduino上的單位元數位類比轉換

Arduino Nano 3內含ATmega328晶片,這款晶片內建有3個硬體計時器,這些計時器的數值會隨著時間推進而增加,加到極限之後(也就是發生溢位)再自動歸零。這些計數器被命名為TCNTn,n代表每一個計時器的個別編號。

Timer0與timer2都是八位元,所以


TCNT0



TCNT2

分別都反覆地從0計數到255。而Timer 1為十六位元,所以

TCNT1

則反覆地從0計數到65535,但是,Timer1也可以做為八位元計時器使用。事實上,每一個計時器都有不同模式,我們會用到的稱為快速PWM模式,只有timer1有這個功能。

在這個模式當中,只要


TCNT1

運算溢位到零,輸出值會衝回高的狀態,表示下一循環的開始。如果要設定峰值時距,timer1有一個暫存器,名稱是:

OCR1A

。每當

TCNT1

計數到 

OCR1A

儲存的數值,輸出值就會掉回低的狀態,這表示此循環的峰值結束, 進入谷值。

TCNT1

的數值會繼續增加,直到發生溢位為止,然後循環重新開始。

【圖F】
Value of TCNTn:TCNT值/TCNTn overflows back to zero:溢位時歸零/Time:時間/PWM output:PWM輸出

 
圖F以圖像化的形式呈現了這個過程,如果我們將OCR1A訂得越高,那麼PWM輸出的mark time就會越長,Vout的電壓值也會越高。如果我們將OCR1A的間隔預先設定妥當,就可以產生各種想要的聲波形式。

簡易波表播放

列表1(請用.zip的壓縮檔形式將列表1–6下載完成)內含一個程式檔,透過數值表(lookup table)、快速PWM模式與單位元類比數位轉換器來產生正弦波。

【列表1】

#include <avr/interrupt.h> // 使用計時器中斷程式庫

 

/********正弦波參數********/

#define PI2 6.283185 // 2*PI 儲存計算結果

#define AMP 127 // 正弦波比例因子

#define OFFSET 128 // 偏移量將波值轉換成正的數值

 

/******** 數值表 ********/

#define LENGTH 256 // 波數值表長度

byte wave[LENGTH]; // 波長儲存

 

void setup() {

 

/* 使用正弦波來填滿波形表*/

for (int i=0; i<LENGTH; i++) { // 在波形表上移動

   float v = (AMP*sin((PI2/LENGTH)*i)); // 數值計算

   wave[i] = int(v+OFFSET); // 數字存為整數

 }

 

/****將timer1設為八位元快速PWM輸出****/

 pinMode(9, OUTPUT); // 將計時器的PWM針腳設為輸出

 TCCR1B = (1 << CS10); // 將預除器設為16MHz

 TCCR1A |= (1 << COM1A1); // 當TCNT1=OCR1A時輸出波谷值

 TCCR1A |= (1 << WGM10); // 使用八位元快速PWM模式

 TCCR1B |= (1 << WGM12);

 

/******** timer2呼叫ISR之設定 ********/

 TCCR2A = 0; // 沒有暫存器A的控制選項

 TCCR2B = (1 << CS21); // 將預除器被除數設為8

 TIMSK2 = (1 << OCIE2A); // 當TCNT2 = OCRA2時,呼叫ISR

 OCR2A = 32; // 設定產生音波頻率

 sei(); // 允許中斷以產生音波

}

 

void loop() { // 什麼事也沒發生!

}

 

/******** 當TCNT2 = OCR2A時執行呼叫 ********/

ISR(TIMER2_COMPA_vect) { // 當TCNT2 == OCR2A時執行呼叫

 static byte index=0; // 指向每一個數值表頭

 OCR1AL = wave[index++]; // 更新PWM輸出

 asm(“NOP;NOP”); // 微調

 TCNT2 = 6; // 補償ISR運作時間

}


首先,我們必須計算出波形,這些數值被存成數字字元陣列,它們會在適當的時間直接被載入OCR1A。接著,要開啟timer1來產生快速PWM波,因為timer1的預設值十六位元,我們必須將它設定成八位元。

另外,我們會用timer2來定時中斷CPU並呼叫一個特別的函數來將波形裡下一個數值載入OCR1A
這個函數稱為中斷服務常式(interrupt service routine、ISR),每當TCNT2的數值與OCR1A相等時,就會被timer2呼叫。ISR和其他函數沒有什麼不同,唯一的區別在於它沒有回傳類別(return type)。

Arduino Nano的系統時鐘運轉速率為16MHz,這會使得timer2呼叫ISR的速率太過頻繁。為了要減緩這個速度,我們必須使用預除器(pre-scaler),在數值疊加之前將系統時脈頻率除上一個參數TCNT2

我們將預除器參數設為8,這樣一來,TCNT2就會變成2MHz。

如果要控制產生的音波頻率,只要設定OCR2A就行了,如果要計算產出的頻率值,就把TCNT2更新後的值(2MHz)用OCR2A去除,再將結果除以數值表長度就行了。舉例來說,如果將OCR2A設為128,那麼頻率將會是:

图片

【圖】TCNY2速率/(OCR2A數值x波形表長度)

這大約相當於中央C以下兩個八度的B,從這裡可以連接到標準音階的頻率對照表。

因為ISR運作需要時間,為了補償這段時間差,我們必須將TCNT2設為6,而不是0,這些事情都要在回傳之前完成。而為了將時間算得更精確,我還加入了

asm(“NOP;NOP”)

指令來執行兩次「空白指令」,每個指令都使用一個時間循環。

【圖G】

現在請試著跑跑看程式,並接上電阻器與電容器(見圖G),如果將示波器連上Vout,您應該可以看到一個平滑的正弦波。如果您想要聽聽看喇叭放出來的音響效果,可以加上一個電晶體來放大訊號(見下圖H)。


【圖H】speaker:喇叭

簡單波形的程式編寫

知道如何從數值表中「播放」新的聲波之後,其實任何波形原理都一樣,只要事先更改數值表中的數值就可以任意創造出新的波形了,唯一的限制在於Arduino本身處理速度相對較低,無法讓您無限制地揮灑。


列表2包含了

waveform()

這個函數,將數值表預先用以下這些簡單的波形填滿,包括:SQUARESINETRIANGLERAMPRANDOM
您可以試著播放音效,聽聽看每種波形的不同聲響效果(見下圖)。

方波(square)可以製造出尖而細的音色,也可以製造出圓潤的音色,這一切都與工作週期有關。在我們一開始的實驗當中,我們已經知道如果工作週期是50%,那產出的聲音會有點類似長笛的音色。

正弦(sine)波的特色在於平滑的上下移動,這是三角函數中正弦函數的函數圖形。這樣的波形可以產出清晰、類似於敲玻璃的聲音,有點像是敲音叉時發出的聲音。

三角波(trangle)與正弦坡有點類似,只是波峰與坡谷並非圓滑的弧線,取而代之的是尖角與直線,或者像是兩個斜波背對背擠壓的結果。這種波形產出的聲音比正弦波更加圓潤,有點像是雙簧管的聲音。

斜波的特性在於從零開始穩定增加的數值,在波的最後數值突然掉回零,然後下一次循環再度開始,這種波產出的音色非常明亮,有銅管樂器的感覺。


理論上,隨機波的音色有無限種可能,但通常結果都是混雜一片。另外,因為Arduino只能產出擬隨機數字,所以,每一個特定randomSeed()數值產生的「隨機」波形都長得一樣。

RANDOM

這個函數的效果在於從種子數值產生一個擬隨機整數,如果要改變種子數值,必須使用

randomSeed()

這使得我們可以產出不同的擬隨機數列,從而嘗試不同的音響效果。有些聲音聽起來尖而細,有些聲音則色彩鮮明,隨機波形雖然有趣,卻常常是u一團吵雜,因此,我們需要更好的方法來做出想要的波形。

疊加式聲波合成概念

在十九世紀,約瑟夫‧傅立葉(Joseph Fourier)證明只要透過疊加不同頻率與振幅的正弦波,我們就可以合成或複製任何的波形,這些正弦波稱為「諧波」(harmonics)或「分波」(partials),其中,頻率最低的諧波稱為第一諧波(first harmonic)或者基波,而將不同諧波疊加以創造出新的波形的過程稱為疊加式聲波合成技術。

無論波形有多麼複雜,我們都可以透過疊加特定數量的諧波來合成,使用的諧波數量越多,合成的結果也將會越精確。

透過這樣的原理,專業的疊加合成設備可以結合一百種以上的諧波,並即時調整振福以製造出驚人的音色效果。當然,這已經遠超過Arduino的能力限制,但是我們依然可以做出許多有趣的音效來滿足所需。

【圖J】
Fundamental:基波
3rd harmonic, 1/4amplitude:第三諧波、振幅為1/4
Synthesized wave:合成波

如果加入第三個諧波,會產生方形的波和聲音,雖然看起來還是圓圓的。

還記得列表1 當中計算正弦波的迴圈嗎?如果我們將它當作基波,再加上第三諧波振幅的1/4(見圖J),我們就需要新的一個步驟:

for (int i=0; i<LENGTH; i++) { //

走過整張波形表

  float v = (AMP*sin((PI2/LENGTH)*i)); // 基波

  v += (AMP/4*sin((PI2/LENGTH)*(i*3))); // 新步驟

  wave[i]=int(v+OFFSET); // 以整數儲存

  }

在新的步驟中,我們將迴圈計數器乘以三,以產生第三個諧波,接者,再除以衰減因數4將振幅降低。

列表3(可以從makezine.com/35網頁下載)包含這個函數的一般形式,除了兩個陣列(我們打算結合的諧波,其中一個是基波)之外,還有衰減因數。

【圖K】加入前八個諧波後,就可以產出相當漂亮的方波了,其中,我們還是可以觀察到正弦波造成的漣漪狀起伏,只要加上更多分波,這些漣漪就會變得更小。

Fundamental:基波
Various harmonics:各種諧波
Synthesized wave:合成波
Harmonic(partials):諧波(分波)
Attenuate(partials):衰減因數(分波)

如果要改變載入數值表的音色,只要改變兩個陣列中的數字就行了;如果衰減因數為零,表示相對應的諧波會被忽略。您會發現,列表3包含的陣列,產出相當不錯的方波(見圖K),您可以用不同的陣列做實驗,看看產出的音響效果如何。

波形轉換

通常,專業級聲響合成系統會內建線路或程式,提供波形「濾除器」來製造特殊的聲響效果。舉例來說,許多合成系統都建有低頻濾除器,經過處理,聲波開頭會多了一個「哇」的音響,結尾則會出現「悠」的聲響。基本上,LPF的功能就是逐步濾除高頻的分波。雖然以Arduino的硬體設備來看,真正的濾除器功能超出負荷範圍,但是,我們還是可以做一些手腳,使得產出的音響有類色的特殊效果。

列表4包含了一個這樣的函數,可以將數值表的每個數值逐一和另一個過濾用的數值表比較,如果兩邊數值不相同,這個函數會將數值推向濾除後的結果,在播放音響的同時調整音色。

使用正弦波作為濾除器的模型可以做出近似於低頻濾除器的效果,諧波逐漸被移除,在聲響後面加入「哇」的效果;而如果我們用另外一種方式轉換,也就是在濾除器表格中放入複雜的波形來濾除正弦波會發生什麼事呢?「哇」的聲響效果就會跑到最前頭去了。如您所見,在這兩個表格內放入不同的波形就會產生不同的聲響效果。

 
製造音符

要如何才能製造聲響漸弱的感覺呢?像是彈撥樂器之後那種餘音繚繞的感覺是擬真的重要課題。

列表5包含了這樣的函數,使得聲響穩定的衰減,直到完全靜默下來。從波形表來看,就是將數值慢慢拉回平坦的直線。它會走過整張波形表,確認每一個數值,如果數值大於127,就會開始下降,如果小於127,則會慢慢增加。衰減的速率由

delay()

這個函數決定,在每次表格掃描結束後會再次進行呼叫。

當波被壓回直線之後,執行ISR只是中斷服務,但不會產生任何聲響。

cli()

函數會將

sei()

函數中設立的中斷旗標清除,並將它關閉。

使用程式記憶體

Arduino的Atmel處理器奠基於「哈佛」(Harvard)結構之上,這意味者程式記憶體與變數記憶體與變數記憶體是分開的,分成揮發性記憶體與非揮發性記憶體兩部分。Nano的變數空間只有2KB,但是程式空間有30KB之多。

將波形資料存在空間中是可行的,大幅增加可播放的音響種類。值得注意的是,程式空間儲存的資料是唯讀的狀態,但我們仍然可以在裡面存放許多資料,在播放的時候利用RAM來玩出不同的聲音。

列表6的目的就是展示這項技術,從程式空間載入正弦波的數值陣列。同時,必須在程式檔開頭納入pgmspace.h 程式庫,並使用關鍵字

PROGMEN

在我們的陣列宣告當中:

prog_char ref[256] PROGMEM = {128,131,134,…};

Prog_char在pgmspace.h程式庫中有定義,和我們熟悉的「byte」資料型態相同。

如果我們嘗試讀取

ref[]

陣列資料,程式會在不同的空間中尋找數值。因此,我們必須使用內建函數

pgm_read_byte

。這個函數將您希望存取的陣列位置視為參數,加上指向個別陣列表頭的落差。

如果您想要用這個方法儲存不只一個波形,可以用二維陣列的方式存取pgm_read_byte中的陣列,如果這個陣列的兩個維度是

[10][256]

,那麼您就可以使用迴圈中的

pgm_read_byte(&ref[4][i])

來存取波形4,提醒您,千萬不要忘記陣列名稱前面的「&」符號!

 
產出波表資料

讀取程式記憶體的波形表時,您必須要直接把數值寫在程式檔裡,因為程式運轉時無法直接產出數值。所以,這些數值到底是從哪兒來的呢?一個方法是在試算表中產生波形表的數值,再把它貼到程式檔當中。我們做了一張試算表,讓您可以透過疊加式合成來產生波形表、預覽產出的波形,並將波形表的原始值貼入程式檔當中,您可以在這裡下載試算表

更進一步

聲響回饋可以用來傳達運作中程式的遭遇的各種即時情況,像是錯誤、按鍵或感測器感測到的任何事件等等。

另外,您的Arduino製造出的聲響可以透過軟體儲存、後製,就可以應用在音樂專題當中了。

將儲存的波形有規律(或隨機)轉換,甚至依照各種性能參數做調整,都可能再互動式藝術裝置上有很大的揮灑空間。

如果您將Arduino升級成Due版,事情會變得更加有趣。在84MHz時, Due的速度可以達到Nano的五倍,因此,它得以在快速PWM模式下處理更多、頻率更高的分波。理論上,Due甚至可以即時計算分波,進而成為真正的疊加式合成引擎。

[原文]


Social media & sharing icons powered by UltimatelySocial